Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.

[Suggestion] Dangling sentence fragment "Opening an" — the sentence is started but never completed; the next line begins a new, unrelated sentence. — Concrete cost: a reader encounters "Opening an" with no continuation, so the intended meaning (likely "Opening an empty panel shows Review…") is lost and the spec reads as though a line was deleted mid-edit.

Suggested change
The artifact-panel action remains available when no tab exists. Opening an
The empty panel shows Review and, when session-source metadata is supported,
The artifact-panel action remains available when no tab exists. The empty
panel shows Review and, when session-source metadata is supported,
中文说明

设计文档中有一个未写完的句子片段 "Opening an"——句子开了头却没有结尾,下一行直接开始了另一个无关的句子。读者会看到没有下文的 "Opening an",原本想表达的意思(大概率是 "Opening an empty panel shows Review…")丢失,文档读起来像是编辑时删漏了一行。建议把该片段并入下一句(上面 suggestion 仅为示意)。

— qwen3.8-max-preview via Qwen Code /review

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.

[Suggestion] Dangling sentence fragment — line 24 ends mid-phrase with "Opening an" and line 25 starts an unrelated sentence, so the intended description of the empty-panel open behavior is lost. — Concrete cost: a reader implementing or reviewing against this design doc hits a garbled run-on; the documented behavior is incomplete. Complete or remove the fragment (e.g. merge it into the next sentence).

中文说明

第 24 行以 "Opening an" 中途断句,第 25 行另起一句不相关的句子,导致空面板打开行为的描述缺失,读者会看到一段不通顺的句子。请补全或删除该残句(例如并入下一句)。

— qwen3.8-max-preview via Qwen Code /review

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,已记录在本轮总结中,并保持开放留待后续跟进,不会被静默丢弃。

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.

[Suggestion] The design-doc sentence is truncated: "...when no tab exists. Opening an The empty panel shows Review..." — the clause "Opening an" has no object and runs directly into the next sentence. — Failure scenario: a reader following the Header section hits an unparseable fragment; the description of what opening the panel does is silently lost.

Suggested change
The artifact-panel action remains available when no tab exists. Opening an
The empty panel shows Review and, when session-source metadata is supported,
The artifact-panel action remains available when no tab exists. The empty panel shows Review and, when session-source metadata is supported,
中文说明

设计文档中的句子被截断:"Opening an" 没有宾语并直接接入下一句,导致预期行为描述丢失。请补全或删除该悬空子句。

— qwen3.8-max-preview via Qwen Code /review

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.

[Suggestion] This integration test lives outside every npm workspace, so the per-workspace test command never collects it; the new session_side_task capability assertion here is exercised only by the dedicated Integration Tests (CLI, No Sandbox) job — which is in the skipped-check set for this commit. — Failure scenario: if that job stays skipped, a regression in the session_side_task route (the daemon stops advertising or honoring the capability) merges uncaught, because no test that actually runs gates it. Confirm the integration job runs for this PR. (This review's own build/test pass also did not run the integration suite.)

中文说明

该集成测试位于所有 npm workspace 之外,按 workspace 的测试命令不会收集它;此处对 session_side_task 能力位的断言仅由专门的 Integration Tests (CLI, No Sandbox) 任务执行,而该任务在本次 commit 处于被跳过状态。若该任务持续被跳过,session_side_task 路由的回归(daemon 不再声明或支持该能力)将无人拦截。请确认该集成任务对本 PR 实际运行。(本次评审自身的构建/测试也未运行该集成套件。)

— qwen3.8-max-preview via Qwen Code /review

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',
Comment on lines 312 to 314

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.

[Suggestion] [test] The only test asserting the new session_side_task route appears in the qwen serve capabilities envelope lives in integration-tests/, outside every npm workspace, so no workspace npm test (including the one run in this review) ever collects it — and the Integration Tests (CLI, No Sandbox) CI job that runs it was skipped for this PR. — Failure scenario: a regression that drops or renames session_side_task from the envelope would ship with every unit suite green and the integration job skipped, so nothing fails.

Suggested change
'session_source_metadata',
'session_side_task',
'session_prompt',
'session_source_metadata',
'session_side_task',
'session_prompt',
// confirm the Integration Tests (CLI, No Sandbox) CI check actually
// ran (was not skipped) and passed; locally this file runs via
// npm run test:integration:cli:sandbox:none
中文说明

唯一断言新 session_side_task 路由出现在 qwen serve 能力信封中的测试位于 integration-tests/,不在任何 npm workspace 内,因此没有 workspace 的 npm test 会收集它;而运行它的 "Integration Tests (CLI, No Sandbox)" CI 任务在本 PR 被跳过。若回归从信封中删除/重命名 session_side_task,所有单测仍绿、集成任务被跳过,无任何失败。建议确认集成 CI 确实运行并通过;本地可用 npm run test:integration:cli:sandbox:none 执行该文件。

— qwen3.8-max-preview via Qwen Code /review

'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