Skip to content

feat(app): add subagents side-panel tab - #55496

Open
sdpfigueiredo wants to merge 2 commits into
anomalyco:devfrom
sdpfigueiredo:sub-agents-tab-v2
Open

sdpfigueiredo wants to merge 2 commits into
anomalyco:devfrom
sdpfigueiredo:sub-agents-tab-v2

Conversation

@sdpfigueiredo

@sdpfigueiredo sdpfigueiredo commented Oct 14, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #37267

This ports #39382 (closed as V1-only) to the current architecture.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Adds a "Subagents" side-panel tab so subagent activity can be followed without being buried by the main agent's log stream, as described in #37267.

The tab lists each subagent spawned by the current session with its task name, live status, cost, token usage and model. Clicking a row opens that subagent's session, which is the drill-down to its full output. New subagents appear at the top. A header button with a busy dot and a + menu entry open the panel.

How it works, briefly: the panel is a gui-extensions side-region contribution (packages/gui-extensions/src/agents). Children come from the live sync store filtered by parentID plus a one-shot family backfill, so there is no polling and no per-child requests. Running/retrying state comes from the live status map; terminal states (completed / cancelled / failed) come from the parent session's subagent tool parts, mirroring how the header's background-task reader derives them (including aborted structured errors and synthetic completion notices). Foreground subagent calls that have no child session yet render as static Running rows.

Status icons match the timeline's task cards (spinner while running, subagent glyph otherwise). Copy lives in the extension's own i18n/en.ts (62 locales ship translated).

How did you verify your code works?

  • bun test src/agents/ in packages/gui-extensions (33 tests): status derivation precedence (running part wins, backgrounded completions, synthetic-notice expiry, stale-part rejection, aborted/interrupted cancellation), children sorting/filtering, cost/token formatting, link shape.
  • bun run typecheck in packages/gui-extensions: clean. oxlint on touched files: 0 warnings.
  • extension-graph component spec: all built-ins including agents boot with every optional provider disabled.
  • Manually in Chromium against storybook with a seeded store: empty state, rows with cost/model/tokens, live Running → Completed transition in the same DOM node without remount, new rows appearing, cancel rendering, collapse/expand, search, tabs, sort, history detail.

Screenshots / recordings

subagents

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@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.

I couldn't check this in the app: the panel is desktop/web-only and this environment only drives the TUI. The 33 new tests pass and bun run typecheck in packages/gui-extensions is clean. A short recording against a real session, with running and finished subagents, would help.

Public API: unchanged. This is a new built-in extension only.

Main concerns (details inline):

  • Copied status logic: status.ts copies how packages/app/src/session/requests/background.ts (#53247) works out background subagents (synthetic completion notices, the latest unfinished message, backgrounded completions) instead of sharing it. That logic changes often, so the panel and the header's running menu will start to disagree. Please move the shared reader to one place both can import, e.g. session-ui or the client.
  • Translations: every non-English locale file has only 12 keys. They include showAll, which nothing uses, and are missing about 20 keys from en.ts (search, sort, tabs, empty states, elapsed, footer…). Most of the panel would show in English everywhere. Please either translate the full key set or ship only en.ts.
  • Repeated fetches: while subagents are running, every change to the running set re-fetches the messages of all running children.
  • English-only times: the relative times in the activity feed are hardcoded English.

Smaller points:

  • The first-open fetch swallows every error.
  • renderer.tsx hasn't been through the formatter (), draggable: false).
  • This adds a second header button next to the existing running menu. Worth checking with maintainers that both are wanted.

* the latest uncompleted assistant message (older ones are stale), and a
* backgrounded completion only counts until its synthetic notice lands.
*/
export function subagentTasks(messages: SessionMessageInfo[]): SubagentTasks {

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 copies how packages/app/src/session/requests/background.ts reads background tasks: the synthetic source: "subagent" notices, "latest uncompleted assistant message" and metadata.status === "running" backgrounded completions. The two copies will drift, and the panel and the header's running menu will then show different states for the same subagent. Could this reader move to a shared package (e.g. session-ui or the client data layer) so both use it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 0dc2b8b: new shared reader at packages/session-ui/src/subagent-transcript.ts (scanSubagentTranscript plus taskStatus, partBackgrounded, toolSessionID, toolDescription, toolAgent), exported via ./subagent-transcript. Both packages/app/src/session/requests/background.ts and packages/gui-extensions/src/agents/status.ts now consume it, so header menu and panel cannot drift on notices, liveness, or backgrounded completions. The scan returns raw calls with live/backgrounded flags; shell handling stays in background.ts and row derivation stays in the panel. Parity is locked by new background.test.ts (4 tests) and the moved subagent-transcript.test.ts (14 tests). session-ui typecheck clean, import-boundaries green.

return ids.length > 0 ? ids.join("\0") : undefined
})

createKeyed(runningKey, (key) => {

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.

runningKey changes whenever any subagent starts or finishes. Each change then re-fetches messages for every running child, not only the new ones. With N parallel subagents finishing one by one, that is roughly N² requests, which contradicts the "no per-child requests" claim in the description. Syncing only the IDs that weren't synced before (or relying on the live stream after the first sync) would avoid this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 718354e: the panel keeps a synced set and each runningKey change syncs only IDs not synced before (fresh), so membership churn no longer refetches survivors. One-shot per child; the live event stream owns freshness after that, and history rows sync on expand.

"total": "Gesamtkosten",
"subagents": "Sub-Agents",
"tokens": "Token",
"showAll": "Alle anzeigen ({{count}})",

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.

All 62 locale files have the same 12 keys, apparently carried over from the v1 attempt. They include showAll, which en.ts doesn't have and nothing uses, and they are missing about 20 keys en.ts has (view, count, search.placeholder, sort.*, tab.all/running/history, empty*, elapsed, latestActivity, lastResponse, footerNote, cost, session). Non-English users would mostly see English. Please translate the full key set, or drop these files and ship only en.ts.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 0dc2b8b per the suggested option: dropped the 61 carried-over locale files (stale 12-key set with the unused showAll) and ship i18n: { en } only, with runtime fallback to English. Full translation of the ~32-key set is left for a follow-up by translators.

}

/** Short relative stamp: 2s ago, 18m ago, 3h ago, older falls back to the clock. */
export function formatRelative(when: number, now: number, locale: string): string {

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.

"s ago", "m ago" and "h ago" are hardcoded English, so the activity feed won't be translated. Intl.RelativeTimeFormat(locale) or i18n keys would fix that.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 718354e: formatRelative now uses Intl.RelativeTimeFormat(locale, { numeric: "auto", style: "narrow" }) for the seconds/minutes/hours stamps; 24h+ still falls back to the localized clock.

export type DerivedStatus = "idle" | "busy" | "cancelled" | "completed" | "error"

/** Tool names that spawn a child session, current first for the common case. */
const subagentNames = ["subagent", "task"]

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.

task is the v1 tool name. In v2 subagents are spawned only by subagent (task appears only in the v1 database migration). Unless there is a v2 path that still produces task parts, this can be dropped.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Kept intentionally: the V1 migration (packages/core/src/database/v1-migration.bun.ts, migrateTool) preserves the original tool name (name: part.tool), so migrated transcripts still contain task parts. The shared matcher defaults to ["subagent", "task"] with subagent first, and accepts an override. Covered by the legacy-parts test in subagent-transcript.test.ts. Happy to drop task if maintainers prefer subagent-only going forward.

@sdpfigueiredo

Copy link
Copy Markdown
Author

All review points addressed in 718354ec + 0dc2b8b9 (pushed); per-thread replies inline. Summary of the remaining review-level notes:

  • Shared reader: packages/session-ui/src/subagent-transcript.ts (scanSubagentTranscript), consumed by both background.ts and agents/status.ts. Parity tests: new background.test.ts (4), moved subagent-transcript.test.ts (14). session-ui typecheck clean, import-boundaries green.
  • Repeated fetches: synced set, only unsynced IDs per runningKey change.
  • Translations: en-only (i18n: { en }), 61 stale files dropped.
  • Relative times: Intl.RelativeTimeFormat(locale).
  • task name: kept deliberately, migrated transcripts still carry it (migrateTool preserves part.tool); matcher defaults to ["subagent", "task"].
  • First-open fetch swallowing errors: intentional best-effort one-shot; the live stream owns freshness after that, and a failed sync must not break panel open. Can switch to surfacing/logged errors if preferred.
  • Formatter: prettier applied to all touched files; oxlint 0 warnings on all of them.
  • Second header button: the panel complements the running menu (menu = quick switch; panel = full history, search, sort, costs). Both read through the same shared scan now, so states agree. Maintainer call on whether both ship.
  • Recording: this environment only drives the TUI, so no in-app capture here; panel was verified via storybook + Playwright live QA (green), and screenshots are attached on [FEATURE]: Add a dedicated view for sub‑agent outputs and status in the desktop app #37267.

Verification: agents 20/20, subagent-transcript 14/14, background 4/4; gui-extensions + session-ui typecheck clean. (app/session-ui full-suite failures are pre-existing jsxDEV/solid-happydom environment issues, unaffected files only.)

@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.

Re-reviewed at 0dc2b8b. I still couldn't run the panel itself: it is desktop/web-only and this environment only drives the TUI. New tests pass in session-ui (14), app (4, background.test.ts) and gui-extensions (20), and typecheck is clean in all three packages.

Public API: unchanged. The new @opencode/session-ui/subagent-transcript export is internal to the workspace.

Earlier points:

  • Copied status logic: mostly resolved. Both readers now use scanSubagentTranscript. One remaining piece is noted inline.
  • Locale files: resolved, only en.ts ships.
  • Refetching every running child: resolved, only new IDs are fetched now.
  • English-only relative times: resolved, Intl.RelativeTimeFormat is used.
  • task tool name: keeping it for transcripts migrated from v1 makes sense.
  • Formatting: fixed.

]
})

const assistant = messages.reduce<SessionMessageAssistant | undefined>((latest, message) => {

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 reduce still finds the latest uncompleted assistant message itself, and blocking re-checks the subagent parts in it. scanSubagentTranscript already returns live and per-call live flags. Building the subagent half of blocking from scan.calls.filter((c) => c.live && c.status === "running") (and using scan.live for the shell half) would finish the de-duplication. As it is, the header's "what counts as live" rule can still drift from the panel's.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 968ef8b: the reduce is gone. Subagent blocking now builds from scan.calls (live && status === "running", legacy task parts surfacing as subagent, matching the panel) and the shell half reads scan.live content. Shell background-task collection stays a separate pass over all messages, untouched. The type assertion and its suppression are gone with it (plain as const literals). Guarded by the existing running/stale cases plus one new shell-blocking case (live vs stale shell parts); 5/5 pass, oxlint 0, no background.ts typecheck errors. Focused-suite timing unchanged vs baseline (background ~300ms, transcript ~130ms).

@opencode-agent

Copy link
Copy Markdown
Contributor

Thanks. I checked 1ebcd4c: blocking now uses the shared scan for subagent calls (live && status === "running") and scan.live for shells, and the separate latest-message reduce and the type assertion are gone. That resolves the last point from my review. One small side effect: running subagents are now listed before running shells, rather than in transcript order. That only matters if the header's running menu relies on that order. Your other replies (syncing only new running children, shipping only en.ts, Intl.RelativeTimeFormat, keeping task for v1-migrated transcripts) match what I checked last time.

@sdpfigueiredo

Copy link
Copy Markdown
Author

Good catch, but it turns out to be unobservable: the only renderer of blocking items is SessionRunningMenu (session-running-menu.tsx), which keeps only task.type === "subagent" entries (foreground shells stay out by design, the timeline shows them at the bottom). The relative order among subagent entries is transcript order in both the old and the new code (scan.calls is transcript order), so the menu renders identically. Every other consumer is order-independent (.length, a Set of partIDs, boolean coercion). No change made; existing blocking tests own these contracts.

@opencode-agent

Copy link
Copy Markdown
Contributor

Agreed, I checked it: session-running-menu.tsx keeps only subagent entries, which are still in transcript order. message-timeline.tsx turns blocking into a Set of part IDs, and the composer and session commands only check whether it's empty. So the new order can't be seen anywhere, and no change is needed. Thanks for checking.

@sdpfigueiredo
sdpfigueiredo force-pushed the sub-agents-tab-v2 branch 5 times, most recently from f726ae4 to 67a15f5 Compare October 22, 2026 08:53

@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 latest push only rebases the same four commits onto newer dev; their content is unchanged since 1ebcd4c. Re-ran the tests on the rebased head: session-ui 14, app 5, gui-extensions 20, all passing, and typecheck is clean in all three packages. I couldn't run the panel itself, since it is desktop/web-only.

Public API: unchanged.

Resolved: the shared subagent transcript reader (now also used for blocking), syncing only new running children, shipping only en.ts, and localized relative times. No new concerns.

@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 latest push only rebases the same four commits onto newer dev; the only difference is rebase context in main.ts and the README. Re-ran the tests on the new head: session-ui 14, app 5, gui-extensions 20, all passing, and tsgo -b is clean in all three packages. I couldn't run the panel itself, since it is desktop/web-only.

Public API: unchanged. No new concerns.

@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 latest push only rebases the same four commits onto newer dev, with no content changes. Re-ran the tests on the new head: session-ui 14, app 5, gui-extensions 20, all passing, and tsgo -b is clean in all three packages. I couldn't run the panel itself, since it is desktop/web-only.

Public API: unchanged. No new concerns.

@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 four panel commits are unchanged, only rebased again. gui-extensions tests pass (20). I couldn't run the panel itself, since it is desktop/web-only.

The new commit 29313d4 (.opencode/ tool import, README and test) is unrelated to this feature and broken:

  • It switches github-pr-search.ts to @opencode-ai/plugin/tool, the v1 package, which isn't installed here. Latest dev still imports @opencode/plugin.
  • The new test imports ../tool/github-triage.ts, which #58888 removed.
  • bun test ./test/tool-sdk.test.ts in .opencode fails with "Cannot find module '@opencode-ai/plugin/tool'".

Please drop this commit from the PR. If the local tool import really is broken, a separate PR against current dev is the place for it.

Public API: unchanged.

Comment thread .opencode/tool/github-pr-search.ts Outdated

@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 unrelated .opencode commit is gone. The PR is back to the four panel commits, unchanged in content. Tests pass in app (5), gui-extensions (20) and session-ui (14). I couldn't run the panel itself, since it is desktop/web-only.

Public API: unchanged. No new concerns.

This branch has not been deployed

No deployments
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.

[FEATURE]: Add a dedicated view for sub‑agent outputs and status in the desktop app

1 participant