Skip to content

feat(app): add side chat tabs - #54356

Open
wr-web wants to merge 6 commits into
anomalyco:devfrom
wr-web:side-chat-v2
Open

wr-web wants to merge 6 commits into
anomalyco:devfrom
wr-web:side-chat-v2

Conversation

@wr-web

@wr-web wr-web commented Oct 10, 2026 •

Copy link
Copy Markdown

Issue for this PR

Related to #33469 (closed as stale) and #41185, the V1 version of this. dev is V2 now, so this is a port rather than a rebase.

Type of change

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

What does this PR do?

Adds a built-in side-chat GUI plugin next to btw. /btw gives a one-shot answer; a side chat is a full multi-turn chat, with tools, that branches from the open session.

Ctrl/Cmd+Alt+N (or "New side chat" in the command palette) forks the open session with the existing session.fork and opens the fork as a numbered tab in the side panel. The tab shows only what is said after the fork, and nothing said in it reaches the main session. Closing the tab deletes the fork. A new chat starts with any selected conversation text as a quotation, and "Quote in main chat" attaches selected side chat text to the main composer.

It is a plain fork, not a child session, so parentID keeps meaning "subagent" (Copilot request labelling, restart recovery, stats, the background task list). No server, protocol or client change. Because the main session's docks only follow children, the tab answers its own permission requests and offers to skip its own questions.

Known limits: the fork is a normal session, so it is listed on Home while its tab is open, and a crash can leave one behind. Not in this PR: a + menu entry. A second item would turn the web + button, which opens the file launcher directly today, into a menu and touch every file-open flow. Happy to add either if you want them.

How did you verify your code works?

  • gui-plugins typecheck, lint and bun test src/side-chat (a small table for the numbering, hidden history and quoting).
  • New e2e/regression/side-chat.spec.ts (fork request, hidden inherited history, prompt goes to the fork, close deletes it and frees its number, empty session, permission request answered in the tab). Run serially: the three side-chat cases and the other btw-sidebar cases pass; answers /btw in the side panel without admitting a prompt fails with or without this PR on my machine (a Solid createRoot warning). The plugin-graph and plugin-host component tests: 31 passed.
  • Not tested: the Electron desktop app (so the shortcut against the native menu), a real model, macOS/Linux. typecheck in packages/app stops at src/custom-elements.d.ts here (a symlink that Windows checks out as a file) and typecheck:e2e has 4 errors in timeline-stream.spec.ts; both are untouched by this PR.

Screenshots / recordings

Captured with Playwright against the e2e mock server, not a real model, so the replies are canned.

A side chat beside the main chat. The fork inherited the main conversation, but its tab shows only what was said after it.

Side chat tab next to the main chat

A second tab, with a permission request answered in the tab.

Second side chat with a permission request

Checklist

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

Added by the opencode bot (AI-generated).

Related: #41910

Adds a built-in `side-chat` GUI extension. Ctrl+Shift+N (or "New side chat" in
the command palette) forks the open session into a child session and opens it as
a numbered tab in the side panel. The fork inherits the conversation at that
moment but its history stays out of the tab, and nothing said in it reaches the
main session. Closing the tab deletes the child session. Selected conversation
text starts the new chat as a quotation, and text selected in a side chat can be
attached to the main composer.

`session.fork` takes an optional `child` flag that links the fork to its source,
so the source's permission and question docks surface the child's requests and
deleting the source deletes its side chats.
@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 verify this end to end. Core fork tests (45), the side-chat unit tests (3) and typecheck in gui-extensions, core, server and app all pass. The new Playwright spec wouldn't start here (the headless browser is missing a system library), and the desktop app and a real model weren't available, so I haven't seen the side chat UI working. Before/after screenshots or a short recording of a side chat in the app would help.

Public API changes, all additive and backwards compatible: POST /api/session/:sessionID/fork gains an optional child: boolean, and the durable session.forked event gains an optional child field (old events replay unchanged). The regenerated client types match.

Concerns, details inline:

  • Shortcut clash: Ctrl+Shift+N is already "New Window" in the desktop menu.
  • The child flag does more than share the docks and delete side chats with their session: core and app treat any session with a parent as a subagent. That affects Copilot request labelling (user-typed side chat turns are sent as subagent/agent-initiated requests), resuming a turn after a restart, usage stats, and the background task list.

Related: #41910

description: ctx.t("command.description"),
group: ctx.t("command.category.session"),
section: "session",
bind: "mod+shift+n",

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.

mod+shift+n is already the desktop menu's "New Window" accelerator (packages/app/src/shell/commands/desktop-menu.ts: Cmd+Shift+N / Ctrl+Shift+N). In Electron the native menu accelerator will likely take the key first, so either the side chat shortcut won't work on desktop or it will fight with New Window. Please pick a free binding, or check that both still work in the desktop app.

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.

Moved to Ctrl/Cmd+Alt+N, which nothing in the app, the extensions or the desktop menu binds. I have not been able to try it in the Electron app, so the native menu is still unchecked.

Comment thread packages/core/src/session/projector.ts Outdated
.values({
id: event.data.sessionID,
parent_id: null,
parent_id: event.data.child ? event.data.parentID : null,

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.

Setting parent_id does more than share the permission/question docks and delete side chats with their session. Everything that treats "has a parent" as "is a subagent" now treats a side chat that way too:

  • plugin/provider/github-copilot.ts sends turns the user typed in a side chat as X-Interaction-Type: conversation-subagent with x-initiator: agent.
  • SessionStore.listSuspended skips child sessions and releaseChildClaims clears them, so a side chat turn cut off by a restart is never resumed.
  • session/stats.ts leaves side chat prompts out of the totals.
  • In the app, requests/background.ts lists a running side chat as a background subagent task of the main session.

Could the side chat be identified by its fork link (plus an explicit marker if needed), with the docks and cascade delete reading that, so parentID keeps meaning "subagent"? Otherwise, please go through each of these cases and handle the ones that shouldn't apply to side chats.

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.

Agreed, parentID should keep meaning subagent. I dropped the child flag and the projector change, so this PR no longer touches core, protocol, schema, server or the client. A side chat is now a plain fork. The docks only follow children, so the tab handles its own permission requests and offers to skip its own questions. The cost is that the fork is listed on Home while its tab is open.

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.

Thanks, confirmed: with child and the projector change gone, the diff now only touches gui-extensions and the app's e2e tests, so parentID keeps its subagent meaning and the public API is unchanged. The new head has the same code as the one I last reviewed, so two points from that review are still open:

  • Orphaned forks. Being listed on Home is one cost. The bigger one is that a fork outlives the main session: if the main session is deleted, a remove fails, or the app crashes, nothing ever deletes the fork.
  • Questions can only be skipped. In a side chat, a question from the model's question tool or a web search provider prompt can't be answered.

A side chat is no longer linked to its source as a child session, so `parentID`
keeps meaning "subagent" for Copilot request labelling, restart recovery, usage
stats and the background task list. The `child` flag on `session.fork` and the
event are dropped, so this change no longer touches the server API.

Without the link the main session's docks no longer show a side chat's requests,
so the tab answers its own permission requests and skips its own questions.
The shortcut moves to Ctrl/Cmd+Alt+N, since Ctrl/Cmd+Shift+N is New Window in the
desktop menu.

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

Both earlier concerns are fixed. The shortcut is now Ctrl/Cmd+Alt+N, which doesn't clash with the desktop menu (still untested in Electron). Side chats are plain forks, so parentID keeps its subagent meaning. The dialog.tsx and command-palette changes I asked about came from merging dev, not from this PR. gui-extensions and app typecheck pass, and the side-chat unit tests pass. I couldn't run the Playwright spec here (the headless browser is missing a system library), so I haven't seen the UI working; a short recording or screenshots would help.

The public API is now unchanged: no server, protocol, client, event or plugin API changes.

Remaining concerns, details inline: side chat sessions can be left behind with nothing to clean them up, and questions in a side chat can only be skipped.

Related: #41910

comment: quote,
})
},
/** Closing a tab deletes its session. */

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 the only place a fork gets deleted, and the open tabs are stored with the main session. Without the parent link, nothing cleans up a side chat if:

  • the main session is deleted while side chats are open (its store goes away, the forks stay);
  • this remove call fails, which is swallowed by .catch(() => undefined);
  • the app crashes or the store is lost.

In each case the fork stays behind as a normal top-level session. While the tab is open it also shows on Home next to real sessions. Could the extension delete its forks when the main session is deleted, and filter them out of Home? Or, if the server has to know about side chats anyway, could they be marked explicitly in a way that doesn't reuse parentID?

</div>
)}
</For>
<For each={forms()}>

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.

Questions in a side chat can only be skipped: the model's question tool, and web search provider prompts (also forms), are cancelled without an answer. If the model asks something in a side chat, the user can't answer it. Could the main session's question dock (SessionQuestionDock in the app) be made available to extensions and reused here? If that's too much for this PR, please mention the limitation in the side chat.

@Yihao-G

Yihao-G commented Oct 28, 2026

Copy link
Copy Markdown

Can this be reviewed and merged soon?

I know we can now build a GUI plugin which could achieve something similar (which is what've already done), but the plugin has no access to the SessionTimeline component. Unless I duplicate SessionTimeline and its child components, there is no way to render the timeline the same as the main chat. And even if I want to duplicate SessionTimeline, it won't be able to access the contexts in the main app, so the behaviour/appearance will be different...

@wr-web

wr-web commented Oct 28, 2026

Copy link
Copy Markdown
Author

Can this be reviewed and merged soon?

CC @Hona @Brendonovich

Wang Rui added 2 commits October 30, 2026 22:38
# Conflicts:
#	packages/app/e2e/utils/mock-api.ts
#	packages/gui-extensions/README.md
#	packages/gui-extensions/src/main.ts
#	packages/gui-extensions/src/renderer.ts

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.

2 participants