Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9664e6d
feat(web-shell): add contextual task panels
Jul 28, 2026
8cfff64
fix(web-shell): harden contextual task panels
Jul 28, 2026
f554cf8
fix(web-shell): preserve side task titles
Jul 28, 2026
9a926f3
Merge branch 'main' into feat/web-shell-context-panels
qwen-code-dev-bot Jul 28, 2026
8dbb4b8
Merge branch 'main' into feat/web-shell-context-panels
wenshao Jul 28, 2026
227cb88
merge: resolve conflict with origin/main in WorkspaceSection imports
qwen-code-dev-bot Jul 28, 2026
76ca3d3
Merge branch 'main' into feat/web-shell-context-panels
wenshao Jul 28, 2026
d5fc330
Merge branch 'main' into feat/web-shell-context-panels
qwen-code-dev-bot Jul 28, 2026
7d70f95
fix(web-shell): address review feedback on context panels PR (#7929)
qwen-code-ci-bot Jul 28, 2026
10dfb70
fix(cli): update telemetry-catalog route drift guard to 51 routes (#7…
qwen-code-ci-bot Jul 28, 2026
5f1ece3
fix(web-shell): address review feedback round 2 on context panels PR …
qwen-code-ci-bot Jul 28, 2026
247101b
Merge branch 'main' into feat/web-shell-context-panels
qwen-code-dev-bot Jul 29, 2026
89ccd93
Merge branch 'main' into feat/web-shell-context-panels
ytahdn Jul 29, 2026
9fb8670
Merge branch 'main' into feat/web-shell-context-panels
qwen-code-dev-bot Jul 29, 2026
fa3aa32
fix(web-shell): dedupe completed background agents in environment pan…
Jul 29, 2026
5877237
Merge branch 'main' into feat/web-shell-context-panels
qwen-code-dev-bot Jul 29, 2026
7b18f00
merge: sync main and fix context panel CI
Jul 29, 2026
5f55a94
feat(web-shell): support side tasks during active turns
Jul 29, 2026
de3816a
Merge remote-tracking branch 'origin/main' into feat/web-shell-contex…
Jul 29, 2026
94b2df8
fix(web-shell): deduplicate completed subagents and gate sourceType o…
Jul 29, 2026
2be5545
Merge remote-tracking branch 'origin/main' into feat/web-shell-contex…
Jul 29, 2026
ea27e82
fix(web-shell): restore background agent reconciliation and fix agent…
Jul 29, 2026
6fefb37
fix(web-shell): address critical review feedback on context panels (#…
Jul 29, 2026
5fb9583
fix(web-shell): reconcile side-task state across sessions and listing…
Jul 29, 2026
e14ce42
fix(web-shell): preserve contextual panel fallbacks
Jul 30, 2026
b00fe1c
Merge remote-tracking branch 'origin/main' into feat/web-shell-contex…
Jul 30, 2026
bb5732d
Merge remote-tracking branch 'origin/main' into feat/web-shell-contex…
Jul 30, 2026
1f5d68e
Merge remote-tracking branch 'origin/main' into feat/web-shell-contex…
Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions docs/design/web-shell-context-panels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Web Shell context panels

## Goal

Add a persistent header to active chat sessions and move supported workspace
and background-task context into a fixed-width environment panel. Keep the
existing artifact panel as an independent right-side surface.

## Header

The active chat header is opt-in so existing integrations without header props
keep their previous layout. Passing `header` enables the default header, whose
content is the current session title. `header.items` controls the title,
environment action, and artifact-panel action independently; an empty items
array hides the complete header. Passing `renderChatHeader` also enables the
header and replaces it completely; the renderer receives the session metadata,
enabled items, controlled panel state, and panel open-change callbacks. The
compact sidebar toggle remains owned by `sidebar` and renders outside the
custom header. While the artifact panel is closed, its toggle is in the chat
header. While it is open, that same toggle moves to the right edge of the
artifact-panel header, leaving the environment action at the right edge of the
chat header adjacent to the panel.

The artifact-panel action remains available when no tab exists. Opening an
The empty panel shows Review and, when session-source metadata is supported,
Comment on lines +24 to +25

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Deferred — critical-only mode (five change-producing rounds completed). The dangling "Opening an" fragment in the design doc is a valid doc fix; recorded for a follow-up so this round stays scoped to the Critical defect.

中文说明

暂缓——仅处理 Critical 模式(已完成五个产生改动的轮次)。设计文档中未写完的 "Opening an" 片段确实是需要修复的文档问题;已记录留待后续处理,以使本轮聚焦于 Critical 缺陷。

Comment on lines +24 to +25

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Deferred — this PR is in critical-only mode after five change-producing rounds, so this round addresses Critical feedback only. The dangling sentence fragment in the design doc is a real readability nit. It is non-Critical, recorded in the round summary, and left open for a follow-up so it is not silently dropped.

中文说明

延后 — 本 PR 在五个产生改动的轮次后已进入仅处理 Critical 的模式,因此本轮仅处理 Critical 反馈。设计文档中的残句确实是一个可读性小问题。 该项为非 Critical,已记录在本轮总结中,并保持开放留待后续跟进,不会被静默丢弃。

side-task history plus New side task. Once a tab is open, the panel header add
menu contains Review and New side task without repeating the side-task history.
Review opens the most recent transcript turn containing reviewable file
changes, is disabled when no such turn exists, and is hidden from the add menu
while a review tab is already open. Closing a populated artifact panel keeps
its tabs so the header action can reopen the existing content.

`rightPanel.items` independently controls whether Review and Side task appear
on the empty panel page. Both items are enabled by default.

## Side tasks

A side task is a distinct daemon thread session in the same workspace as its
parent. It renders the existing interactive chat pane, including the transcript,
composer, approval-mode selector, model selector, streaming state, and
permission handling. Creation uses the dedicated side-task endpoint to snapshot
the main session's complete persisted model context at that moment, then
continues independently. The snapshot is serialized against transcript writes,
so a side task can be created while the parent is responding without observing
a partial JSONL record. Inherited records are not replayed in the side-task
transcript; only messages created inside the side task are shown.

Side-task sessions record `sourceType: side_task` and the parent session id as
`sourceId`. The Web Shell session catalog filters this source type, so side
tasks do not appear as top-level sessions. With saved side tasks, hovering Side
task on the empty right-panel page opens a menu of those sessions and a New
action. With no saved task, clicking the row creates one directly. Selecting a
saved task restores it as a tab. Closing a tab only detaches its client; the
daemon transcript remains available for later conversation.

`/btw <question>` keeps the lightweight, one-shot BTW interaction.
`/btw side <question>` opens a new side-task draft and sends the question as
its first prompt when the daemon advertises `session_side_task`. Hosts can
trigger the same action through `shellRef.current.createSideTask()`.

## Environment panel

The environment panel uses only existing Web Shell capabilities:

- workspace path;
- Git branch and working-tree summary;
- working-tree diff and commit history entry points;
- configured agents entry point;
- background agent, shell, and monitor task summaries.

The environment action and environment section remain available throughout an
active chat session. A clean working tree is shown explicitly; agent and
background-task sections appear only when they have content.

`environmentPanel.items` independently controls the environment, subagent, and
background-task sections. All three sections are enabled by default.

The local `/fork` command refreshes the session task snapshot as soon as its
background agent launches. Fork agents have no parent transcript tool call, so
their right-panel detail resolves the virtual subagent session by agent task ID
instead of `toolUseId`.

The local `/tasks` command opens the environment panel and refreshes its task
snapshot instead of opening the legacy task dialog.

Side-task, subagent, and fork transcripts expose their own file changes and
artifacts through the main right panel. Their source session scopes tab
identities and workspace actions, so opening a nested output creates a separate
tab without replacing the main session's review or artifact tabs.

It is a fixed-width, non-resizable layout column styled as a floating card with
a border and shadow. At narrower message widths it opens as a dismissible
floating popover instead of consuming chat width.

The environment panel and artifact panel are independent. At desktop widths the
two may be visible together. When the viewport cannot fit both, the artifact
panel normally takes priority and the environment panel is hidden without
losing its open state. Opening a subagent or background task from the
environment panel keeps that panel visible beside the resulting detail. A
floating environment panel is positioned within the remaining message area and
never overlaps the artifact panel.

## Responsive behavior

The environment panel is hidden for split/full-page views. When the message
area cannot keep at least 800 pixels after docking the panel, the panel closes
and can be reopened as a floating popover. An open artifact panel takes
priority when both panels cannot fit, but the environment action remains
available for explicitly reopening the popover. The existing artifact drawer
behavior on narrow screens is unchanged. On desktop, the artifact panel is a
top-level layout column beside the chat shell, so it starts at the top of the
page and the chat header ends at the panel boundary.
1 change: 1 addition & 0 deletions integration-tests/cli/qwen-serve-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ describe('qwen serve — capabilities envelope', () => {
'session_list',
'session_info',
'session_source_metadata',
'session_side_task',
Comment on lines 312 to +313

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Deferred — this PR is in critical-only mode after five change-producing rounds, so this round addresses Critical feedback only. Whether the skipped Integration Tests (CLI, No Sandbox) job runs for this PR is a CI/workflow question for the maintainer rather than a code change. It is non-Critical, recorded in the round summary, and left open for a follow-up so it is not silently dropped.

中文说明

延后 — 本 PR 在五个产生改动的轮次后已进入仅处理 Critical 的模式,因此本轮仅处理 Critical 反馈。被跳过的 Integration Tests (CLI, No Sandbox) 任务是否对本 PR 运行,属于 CI/工作流层面的问题,需由维护者确认,而非代码改动。 该项为非 Critical,已记录在本轮总结中,并保持开放留待后续跟进,不会被静默丢弃。

'session_prompt',
'session_cancel',
'session_events',
Expand Down
98 changes: 98 additions & 0 deletions packages/acp-bridge/src/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import { TurnBoundaryCompactionEngine } from './compactionEngine.js';
import {
CHANNEL_STARTUP_PROFILE_META_KEY,
CHANNEL_STARTUP_PROFILE_VERSION,
LOAD_REPLAY_HIDE_INHERITED_META_KEY,
} from './bridgeTypes.js';
import {
ApprovalMode,
Expand Down Expand Up @@ -10827,6 +10828,103 @@ describe('createAcpSessionBridge', () => {
await bridge.shutdown();
});

it('creates a side task with hidden inherited replay', async () => {
const handle = makeChannel({
extMethodImpl: async (method) => {
if (method === SERVE_CONTROL_EXT_METHODS.sessionSideTask) {
return { newSessionId: 'side-1', title: 'Side task' };
}
if (method === SERVE_CONTROL_EXT_METHODS.sessionSource) {
return { persisted: true };
}
return {};
},
resumeSessionImpl: () => ({}),
});
const bridge = makeBridge({
channelFactory: async () => handle.channel,
});
const parent = await bridge.spawnOrAttach({
workspaceCwd: WS_A,
sessionScope: 'thread',
});

const sideTask = await bridge.createSideTaskSession(parent.sessionId, {
name: 'Side task',
});

expect(sideTask).toMatchObject({
sessionId: 'side-1',
sourceType: 'side_task',
sourceId: parent.sessionId,
sourcePersisted: true,
parentSessionId: parent.sessionId,
});
expect(bridge.getSessionSummary(sideTask.sessionId)).toMatchObject({
sourceType: 'side_task',
sourceId: parent.sessionId,
});
expect(handle.agent.extMethodCalls).toContainEqual({
method: SERVE_CONTROL_EXT_METHODS.sessionSource,
params: {
sessionId: sideTask.sessionId,
sourceType: 'side_task',
sourceId: parent.sessionId,
},
});
expect(handle.agent.loadSessionCalls[0]?._meta).toMatchObject({
[LOAD_REPLAY_HIDE_INHERITED_META_KEY]: true,
});

await bridge.shutdown();
});

it('creates a side task while the parent prompt is active', async () => {
const promptGate = deferred<void>();
const handle = makeChannel({
promptImpl: async () => {
await promptGate.promise;
return { stopReason: 'end_turn' };
},
extMethodImpl: async (method) => {
if (method === SERVE_CONTROL_EXT_METHODS.sessionSideTask) {
return { newSessionId: 'side-active', title: 'Side task' };
}
if (method === SERVE_CONTROL_EXT_METHODS.sessionSource) {
return { persisted: true };
}
return {};
},
resumeSessionImpl: () => ({}),
});
const bridge = makeBridge({
channelFactory: async () => handle.channel,
});
const parent = await bridge.spawnOrAttach({
workspaceCwd: WS_A,
sessionScope: 'thread',
});
const prompt = bridge.sendPrompt(parent.sessionId, {
sessionId: parent.sessionId,
prompt: [{ type: 'text', text: 'keep working' }],
});
await vi.waitFor(() => expect(handle.agent.promptCalls).toHaveLength(1));

await expect(
bridge.createSideTaskSession(parent.sessionId, { name: 'Side task' }),
).resolves.toMatchObject({
sessionId: 'side-active',
parentSessionId: parent.sessionId,
});
expect(bridge.getSessionSummary(parent.sessionId)).toMatchObject({
hasActivePrompt: true,
});

promptGate.resolve();
await prompt;
await bridge.shutdown();
});

it('carries persisted source metadata into ACP session restore', async () => {
for (const action of ['load', 'resume'] as const) {
const handle = makeChannel();
Expand Down
Loading
Loading