From 5ef5feb01accdc6bb67a918afacf3e5b6b0e32e7 Mon Sep 17 00:00:00 2001 From: imccyu Date: Sat, 1 Aug 2026 09:42:18 +0800 Subject: [PATCH] fix(web): reconcile rebased subagent contracts --- apps/web/tests/core-web-profile.snapshot.ts | 1 + apps/web/tests/shipped-composition.e2e.ts | 1 + .../subagent-conversation/ui.expected.md | 42 ++++++++++--------- docs/module-graph.md | 12 +++--- .../ui-conversation/tests/chat-view.spec.tsx | 2 +- .../tests/gate-branch-tails.spec.tsx | 4 +- .../client/SubagentCatalogAction.module.css | 2 + packages/host/apiproxy/src/api-proxy.ts | 23 ++++++---- 8 files changed, 53 insertions(+), 34 deletions(-) diff --git a/apps/web/tests/core-web-profile.snapshot.ts b/apps/web/tests/core-web-profile.snapshot.ts index 58f2a34858..5f3334c0e7 100644 --- a/apps/web/tests/core-web-profile.snapshot.ts +++ b/apps/web/tests/core-web-profile.snapshot.ts @@ -72,6 +72,7 @@ describe('core Web profile', () => { "tools": [ "bash", "str_replace_editor", + "list_agents", ], } `) diff --git a/apps/web/tests/shipped-composition.e2e.ts b/apps/web/tests/shipped-composition.e2e.ts index a9162b631a..f9da7d3bd3 100644 --- a/apps/web/tests/shipped-composition.e2e.ts +++ b/apps/web/tests/shipped-composition.e2e.ts @@ -28,6 +28,7 @@ const EXPECTED_TOOLS = [ 'edit', 'exit_plan_mode', 'get_goal', + 'list_agents', 'ralph', 'read', 'send_message', diff --git a/apps/web/tests/snapshots/subagent-conversation/ui.expected.md b/apps/web/tests/snapshots/subagent-conversation/ui.expected.md index dd7313bcbe..ada21f13c0 100644 --- a/apps/web/tests/snapshots/subagent-conversation/ui.expected.md +++ b/apps/web/tests/snapshots/subagent-conversation/ui.expected.md @@ -1,43 +1,47 @@ - banner: - - navigation "Session hierarchy": - - button "Ask a research subagent to" - - text: / - - button "event-sourcing researcher" [disabled] - - text: · 2 turns + - heading "event-sourcing researcher" [level=1] - button "1 个子代理": - text: 1 个子代理 - img - tablist: - tab "Chat" [selected] - tab "Trajectory" - - tab "Waterfall" -- text: Explain event sourcing in one sentence. -- button "复制": +- text: Explain event sourcing in one sentence. {{clock}} +- button "Copy": - img -- button "在新对话中分支": +- button "Branch into a new conversation": - img -- button "编辑": +- button "Context injection": - img + - img + - text: Context injection - button "Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls.": + - img - img - text: Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls. - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. -- text: Now give the same explanation to a human reader. -- button "复制": +- button "Copy": - img -- button "在新对话中分支": +- button "Branch into a new conversation": - img -- button "编辑": +- text: {{clock}} Now give the same explanation to a human reader. {{clock}} +- button "Copy": + - img +- button "Branch into a new conversation": - img - button "Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls.": + - img - img - text: Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls. - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. -- text: cache hit 99% · 15,738 tokens · 2 turns · 2 steps +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: {{clock}} - textbox "Message the agent" -- button "Add attachment": +- button "Commands": - img -- combobox "Access mode": - - option "Read-only" [selected] - - option "Read-write" +- 'button "Access mode, current: Full access"': Full access - button "Send message" [disabled] +- text: 2 turns · 2 steps Context 6% of 128K Cache hit 99% Input 15.6K tok · Output 158 tok diff --git a/docs/module-graph.md b/docs/module-graph.md index 99ec5d4561..4b0abb3fad 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -458,10 +458,6 @@ flowchart TD pkg_client_ui_skill --> pkg_client_ui_slash pkg_client_ui_skill --> pkg_client_ui_slots pkg_client_ui_skill --> pkg_invariants - pkg_client_ui_subagent --> pkg_client_runtime - pkg_client_ui_subagent --> pkg_client_ui_slash - pkg_client_ui_subagent --> pkg_client_ui_slots - pkg_client_ui_subagent --> pkg_invariants pkg_code_runtime_worker --> pkg_code_runtime pkg_code_runtime_worker --> pkg_invariants pkg_code_runtime_worker --> pkg_session @@ -835,6 +831,12 @@ flowchart TD pkg_client_ui_goal --> pkg_client_ui_slots pkg_client_ui_goal --> pkg_goal pkg_client_ui_goal --> pkg_invariants + pkg_client_ui_subagent --> pkg_client_runtime + pkg_client_ui_subagent --> pkg_client_ui_conversation + pkg_client_ui_subagent --> pkg_client_ui_primitives + pkg_client_ui_subagent --> pkg_client_ui_slash + pkg_client_ui_subagent --> pkg_client_ui_slots + pkg_client_ui_subagent --> pkg_invariants pkg_session_reference --> pkg_agent pkg_session_reference --> pkg_compact pkg_session_reference --> pkg_invariants @@ -1138,7 +1140,6 @@ flowchart TD | [`app-boot`](../packages/ui/app-boot) | `ui` | [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`system-prompt`](../packages/core/system-prompt) | | [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) | | [`client-ui-skill`](../packages/client/ui-skill) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | -| [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | | [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | | [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | @@ -1211,6 +1212,7 @@ flowchart TD | [`tool-ask-user`](../packages/ui/tool-ask-user) | `ui` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | | [`client-ui-command`](../packages/client/ui-command) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) | +| [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) | | [`workspace-context`](../packages/context/workspace-context) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | | [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) | diff --git a/packages/client/ui-conversation/tests/chat-view.spec.tsx b/packages/client/ui-conversation/tests/chat-view.spec.tsx index 4151221455..04f412fb25 100644 --- a/packages/client/ui-conversation/tests/chat-view.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-view.spec.tsx @@ -93,7 +93,7 @@ const runningCall = (callId: string, name = 'bash'): RunningToolCall => ({ /** Empty sessions-list hook for the global standard-kit seat. */ function emptySessions() { const store = createSnapshotStore( - { ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, currentAddress: undefined, }) + { ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, currentAddress: undefined }) return bindSnapshotSelector(store) } diff --git a/packages/client/ui-conversation/tests/gate-branch-tails.spec.tsx b/packages/client/ui-conversation/tests/gate-branch-tails.spec.tsx index 65c0a01009..4496423efa 100644 --- a/packages/client/ui-conversation/tests/gate-branch-tails.spec.tsx +++ b/packages/client/ui-conversation/tests/gate-branch-tails.spec.tsx @@ -76,7 +76,7 @@ describe('render branch tails', () => { const chat = createChatStore().create() chat.actions.select({ turnSeq: 1, callId: 'ghost' } satisfies SelectionTarget) const emptyList = createSnapshotStore( - { ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, currentAddress: undefined, }) + { ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, currentAddress: undefined }) const emptyWorkspaces = createSnapshotStore({ items: [], archivedSessionIds: [], state: 'idle', phase: 'ready', error: null, baselinesReady: true, recentWorkspaceId: undefined, @@ -113,7 +113,7 @@ describe('render branch tails', () => { const chat = createChatStore().create() chat.actions.select({ turnSeq: 8, callId: 'p1:code:1', toolName: 'read' } satisfies SelectionTarget) const emptyList = createSnapshotStore( - { ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, currentAddress: undefined, }) + { ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, currentAddress: undefined }) const emptyWorkspaces = createSnapshotStore({ items: [], archivedSessionIds: [], state: 'idle', phase: 'ready', error: null, baselinesReady: true, recentWorkspaceId: undefined, diff --git a/packages/client/ui-subagent/src/client/SubagentCatalogAction.module.css b/packages/client/ui-subagent/src/client/SubagentCatalogAction.module.css index c0f7428ce9..573689526d 100644 --- a/packages/client/ui-subagent/src/client/SubagentCatalogAction.module.css +++ b/packages/client/ui-subagent/src/client/SubagentCatalogAction.module.css @@ -46,6 +46,8 @@ border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; background: var(--dsw-specific-menu); + --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2); + --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2); box-shadow: var(--dsw-shadow-lv3); } diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index c339d38c87..37dd64466a 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -273,6 +273,19 @@ function sessionBlank(session: Session): boolean { return !session.events.some(event => event.type === 'turn/start') } +/** Shared Session-header projection for list baselines and creation frames. */ +function sessionListFields(header: SessionHeader): { + parentSessionId?: SessionId + origin?: 'subagent' + cwd?: string +} { + return { + ...header.parentSession === undefined ? {} : { parentSessionId: header.parentSession }, + ...header.origin === undefined ? {} : { origin: header.origin }, + ...header.cwd === undefined ? {} : { cwd: header.cwd }, + } +} + /** SessionSummary projection for attached (in-memory) sessions. */ function summarize(session: Session, running: boolean): SessionSummary { return { @@ -282,9 +295,7 @@ function summarize(session: Session, running: boolean): SessionSummary { updatedAt: lastActivityTime(session.events) ?? session.header.createdAt, running, blank: sessionBlank(session), - ...session.header.parentSession === undefined ? {} : { parentSessionId: session.header.parentSession }, - ...session.header.origin === undefined ? {} : { origin: session.header.origin }, - ...session.header.cwd === undefined ? {} : { cwd: session.header.cwd }, + ...sessionListFields(session.header), } } @@ -2378,10 +2389,8 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro // Derived at frame time like summarize(); a just-created session // has run no turn yet, so this is constantly true in practice. blank: sessionBlank(session), - ...session.header.parentSession === undefined ? {} : { parentSessionId: session.header.parentSession }, - ...session.header.origin === undefined ? {} : { origin: session.header.origin }, - // cwd rides the frame so the client list needs no refresh to group the new session. - ...session.header.cwd === undefined ? {} : { cwd: session.header.cwd }, + // Including cwd lets the client group the new session without refreshing the list. + ...sessionListFields(session.header), })) }), ctx.on('session/disposed', (session: Session) => {