Skip to content

feat(app): show todowrite todos as a pill above the composer - #59092

Merged
Hona merged 2 commits into
betafrom
todo-dock
Oct 25, 2026
Merged

Hona merged 2 commits into
betafrom
todo-dock

Conversation

@Hona

@Hona Hona commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

The experimental todowrite tool (experimental.todo) is back, but the app had nowhere to show the plan. This adds a todo pill next to the background activity pill above the composer. It replaces the old full-width pull-out dock.

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none" stroke="currentColor">
  <style>
    .ring{stroke-dasharray:31.4;stroke-dashoffset:18;animation:fill 6s steps(1) infinite}
    .dot{animation:pulse 1.6s ease-in-out infinite;transform-box:fill-box;transform-origin:center}
    @keyframes fill{0%{stroke-dashoffset:22.4}25%{stroke-dashoffset:18}50%{stroke-dashoffset:13.5}75%{stroke-dashoffset:9}}
    @keyframes pulse{50%{transform:scale(.6);opacity:.4}}
    @media (prefers-reduced-motion:reduce){.ring,.dot{animation:none}}
  </style>
  <rect x="8.5" y="50.5" width="223" height="17" rx="6" opacity=".35"/>
  <rect x="16" y="56" width="70" height="5" rx="2.5" fill="currentColor" stroke="none" opacity=".2"/>
  <rect x="8.5" y="30.5" width="58" height="14" rx="6" opacity=".35"/>
  <g fill="currentColor" stroke="none" opacity=".35"><circle cx="16" cy="35" r="1"/><circle cx="19" cy="35" r="1"/><circle cx="16" cy="38" r="1"/><circle cx="19" cy="38" r="1"/><circle cx="16" cy="41" r="1" opacity=".5"/><circle cx="19" cy="41" r="1" opacity=".5"/></g>
  <rect x="25" y="35.5" width="34" height="4" rx="2" fill="currentColor" stroke="none" opacity=".3"/>
  <rect x="71.5" y="30.5" width="104" height="14" rx="6" opacity=".55"/>
  <circle cx="81" cy="37.5" r="5" opacity=".2" stroke-width="1.5"/>
  <circle class="ring" cx="81" cy="37.5" r="5" stroke-width="1.5" transform="rotate(-90 81 37.5)" opacity=".8"/>
  <rect x="90" y="35.5" width="12" height="4" rx="2" fill="currentColor" stroke="none" opacity=".35"/>
  <rect x="106" y="35.5" width="62" height="4" rx="2" fill="currentColor" stroke="none" opacity=".6"/>
  <rect x="71.5" y="2.5" width="104" height="24" rx="5" opacity=".35"/>
  <circle cx="79" cy="9" r="2.2" fill="currentColor" stroke="none" opacity=".45"/>
  <rect x="84" y="7.5" width="60" height="3" rx="1.5" fill="currentColor" stroke="none" opacity=".25"/>
  <circle cx="79" cy="15" r="2.2" opacity=".7"/><circle class="dot" cx="79" cy="15" r="1" fill="currentColor" stroke="none"/>
  <rect x="84" y="13.5" width="52" height="3" rx="1.5" fill="currentColor" stroke="none" opacity=".7"/>
  <circle cx="79" cy="21" r="2.2" opacity=".35" stroke-dasharray="1.2 1"/>
  <rect x="84" y="19.5" width="70" height="3" rx="1.5" fill="currentColor" stroke="none" opacity=".35"/>
</svg>
Light Dark
Before
After
After, open

How it works

flowchart LR
  M[session messages] -->|latest completed todowrite before the revert point| T[sessionTodos]
  T --> V{turn running and work left?}
  V -->|yes| P[SessionTodoPill]
  A[SessionActivityChips] --> L[SessionComposerLane]
  P --> L
Loading
  • No server state: sessionTodos in composer/todos.ts reads the input of the latest completed todowrite call before the revert point. This is the same rule as the TUI's sessionTodos.
  • When it shows: only when experimental.todo is on, read from the location config the way the server reads it. The session must also be working with at least one todo not done or cancelled, like the old dock. Migrated V1 sessions keep their old todowrite calls, so without the flag check their last plan would come back.
  • One lane for both pills: SessionComposerLane now owns the out-of-flow row above the composer that the timeline reserves. It also owns the left/center/right alignment from the background activity setting. Both pills share one row, and the activity pill stays at the aligned edge.
  • Pill: progress ring, an animated done/total count, and the current todo. Click it to open a popover with the full list. The popover keeps the todo in progress in view.
  • Settings e2e: the background activity alignment check now measures against session-composer-lane. Before, the activity container filled the full width.
<SessionComposerLane>
  <SessionActivityChips subagents={subagents()} shells={shells()} onOpen={open} onStop={stop} />
  <Show when={todosVisible()}>
    <SessionTodoPill todos={todos()} />
  </Show>
</SessionComposerLane>

Stories: session-todo-pill--collapsed, --open, --without-activity.


Added by the opencode bot (AI-generated).

Related: #42421

@Hona
Hona requested a review from Brendonovich as a code owner October 25, 2026 23:11
Copilot AI balanced review requested due to automatic review settings October 25, 2026 23:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found. I couldn't run the app here (no browser in the sandbox), so I relied on your screenshots. In packages/app, tsc -b passes and the composer unit tests pass. A quick throwaway test of sessionTodos confirmed it handles the revert point, still-running calls, unknown statuses and empty items as described. The background activity alignment behaves the same for left, center and right in both LTR and RTL. With the setting "off" the lane now aligns to start rather than end, but only the todo pill shows in that case.

Public API is unchanged: these are app-internal components and new English i18n keys.

One small point: todos.ts re-implements sessionTodos and the todo parsing from packages/tui/src/util/todo.ts almost line for line (see inline).

Related: #42421


// The experimental `todowrite` tool keeps no server state: the current list is the input of the latest completed call
// before the revert boundary.
export function sessionTodos(messages: readonly SessionMessageInfo[], boundary?: string): Todo[] {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a near copy of sessionTodos/parseTodos in packages/tui/src/util/todo.ts, and the description says the two should follow the same rule. Could both import it from one shared place, so the revert-point and status handling can't drift apart between the TUI and the app?

@Hona
Hona merged commit c4c6065 into beta Oct 25, 2026
12 of 15 checks passed
@Hona
Hona deleted the todo-dock branch October 25, 2026 23:23

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit looks sound: the pill now shows only when experimental.todo is on. todoEnabled() uses the same rule as the server (the highest-priority config document that sets experimental decides), so migrated V1 sessions won't bring back their old plan. In packages/app, tsc -b passes and the composer unit tests pass. I couldn't render the pill here because the sandbox has no browser.

Public API is unchanged; the app only reads the existing experimental.todo config key.

The duplicated sessionTodos logic shared with the TUI is still open in its thread.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants