From 575e1217bb8b0aa77ac8263daefdd790c2898f54 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Fri, 31 Jul 2026 10:48:50 +0800 Subject: [PATCH 1/5] fix: remove scoped bash --- .../2026-07-19-gui-web-client-architecture.md | 2 +- ...26-07-19-gui-web-client-architecture.zh.md | 2 +- .../2026-07-23-toolview-dissolution.md | 2 +- .../2026-07-23-toolview-dissolution.zh.md | 2 +- .../2026-07-27-web-session-fork-actions.md | 2 +- .../2026-07-27-web-session-fork-actions.zh.md | 2 +- apps/web/tests/built-boot.snapshot.ts | 2 +- apps/web/tests/code-mode-round.e2e.ts | 4 +- apps/web/tests/navigation-panes.e2e.ts | 8 +-- apps/web/tests/smoke-real.e2e.ts | 2 +- packages/client/ui-conversation/README.md | 2 +- packages/client/ui-conversation/README.zh.md | 2 +- .../ui-conversation/src/client/apply.ts | 2 +- .../client/toolviews/bash-sample.module.css | 11 --- .../src/client/toolviews/bash-sample.tsx | 6 +- .../tests/assembly-surfaces.spec.tsx | 2 +- .../tests/chat-code-subcalls.spec.tsx | 4 +- .../tests/chat-stats-bash-sample.spec.tsx | 68 ++++--------------- .../tests/chat-toolview-slot.spec.tsx | 2 +- .../tests/coverage-tails.spec.tsx | 4 +- 20 files changed, 38 insertions(+), 93 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md index 63b6f5795c..b1f7771727 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md +++ b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md @@ -44,7 +44,7 @@ Implementation homes: registry core and the props-share types in `packages/clien A service is a plugin's only API surface toward other plugins (UI components and injection faces are not APIs; a plugin nobody calls mounts no service — ui-trajectory is the minimal-plugin exemplar: no ctx service, only view-slot registrations). The roster: `ctx.connection` (api client + stream handles), `ctx.slots` (registry wrapper emitting `slots/changed`, render entry, renderer install seam), `ctx.sessions` (list store, current-session state, scope tree), `ctx.loader`, `ctx.theme`, `ctx.i18n`, `ctx.layout` (cross-plugin view navigation), `ctx.conversation` (send/cancel/startSession). Viewing state that used to live in service stores (panel widths, selection, drafts) now lives in entry-declared stores per the [slot system standard](2026-07-22-slot-type-chain-implementation.md). -There is no registration model besides slots — the former view and tool rings both dissolved into it. Conversation views are entries of the `'conversation.view'` list slot ui-conversation declares, tab metadata rides the registration options (`id`/`order`/`label`), and per-view chrome lives inside the view components themselves. A tool row is a keyed child slot each view declares for itself — today `'conversation.chat.toolview'` (keyed/session), declared by the chat entry's `children` table; the key space is runtime-open (SlotMap declares slots, never keys), which is what the tool ring's open tool-name set required. The render site dispatches per row via `entryKey: toolName` with `GenericToolCard` as the call-site `fallback`; the owner payload is the uniform `ToolRowOwnerProps` (`callId`/`toolName`/`block`/`openDetails`), and `ToolRowProps` composes it with the session standard kit for registrant components. Registrants are plain plugins with zero dedicated machinery: `ctx.slots.register({ name: 'conversation.chat.toolview', key: '', inject? }, Row)`, with `inject: ['slots', 'conversation']` as the load-order seam (the conversation service being present guarantees the slot is declared). Session-dimension differentiation happens inside the component — `useSessions` reading `parentId` — not in registry predicates; interaction drafts and other row state ride the ordinary store seat. Trajectory/waterfall get same-shaped slots (names fixed by the slot-naming discipline `..`, one shared owner type) that land with their own row render sites — RendersCheck rejects a declaration nobody renders, so the two slots cannot be declared early. +There is no registration model besides slots — the former view and tool rings both dissolved into it. Conversation views are entries of the `'conversation.view'` list slot ui-conversation declares, tab metadata rides the registration options (`id`/`order`/`label`), and per-view chrome lives inside the view components themselves. A tool row is a keyed child slot each view declares for itself — today `'conversation.chat.toolview'` (keyed/session), declared by the chat entry's `children` table; the key space is runtime-open (SlotMap declares slots, never keys), which is what the tool ring's open tool-name set required. The render site dispatches per row via `entryKey: toolName` with `GenericToolCard` as the call-site `fallback`; the owner payload is the uniform `ToolRowOwnerProps` (`callId`/`toolName`/`block`/`openDetails`), and `ToolRowProps` composes it with the session standard kit for registrant components. Registrants are plain plugins with zero dedicated machinery: `ctx.slots.register({ name: 'conversation.chat.toolview', key: '', inject? }, Row)`, with `inject: ['slots', 'conversation']` as the load-order seam (the conversation service being present guarantees the slot is declared). Interaction drafts and other row state ride the ordinary store seat. Trajectory/waterfall get same-shaped slots (names fixed by the slot-naming discipline `..`, one shared owner type) that land with their own row render sites — RendersCheck rejects a declaration nobody renders, so the two slots cannot be declared early. **Scope addressing** mirrors the host's agent-scope idiom: services are root singletons whose methods take no sessionId — they read the caller's scope mark (`scopeOf(ctx)`). Inside a session scope, `ctx.conversation.send('hi', 'queue')` targets that session; cross-session calls re-target by switching ctx (`ctx.sessions.scope(id)!.conversation.send(...)`); calling a scoped method from root ctx throws. Client session scopes are minted like host agent scopes (a no-op plugin fiber + a scope-key extend), built lazily on first viewing and torn down only when the session is removed and unwatched — host-session death alone does not tear a scope (it freezes into a read-only viewport). diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md index 2d57c12eba..e43151b7d5 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md @@ -44,7 +44,7 @@ slot 体系有自己的 RFC——[slot 体系标准](2026-07-22-slot-type-chain- 服务是插件对其他插件的唯一 API 面(UI 组件与注入面都不是 API;无人调用的插件不挂服务——ui-trajectory 即最小插件样板:无 ctx 服务,只做视图坑注册)。名册:`ctx.connection`(api client + 流句柄)、`ctx.slots`(注册表包装层,发 `slots/changed`,渲染入口,渲染器安装缝)、`ctx.sessions`(列表 store、当前会话状态、scope 树)、`ctx.loader`、`ctx.theme`、`ctx.i18n`、`ctx.layout`(跨插件视图导航)、`ctx.conversation`(send/cancel/startSession)。过去住在服务 store 里的观看态(面板宽、选中、草稿)现按 [slot 体系标准](2026-07-22-slot-type-chain-implementation.md) 住 entry 声明的 store。 -slot 之外不存在第二种注册模型——原视图环与工具环都已溶解进来。会话视图即 ui-conversation 声明的 `'conversation.view'` list 坑的 entry,tab 元数据随注册 options(`id`/`order`/`label`)走,per-view chrome 住视图组件自身。工具行是各视图自己声明的 keyed 子槽——今天是 `'conversation.chat.toolview'`(keyed/session),由 chat 条目的 `children` 表声明;key 空间运行时开放(SlotMap 声明槽、从不声明 key),这正是工具环「tool 名开放集」的原需求。渲染点逐行以 `entryKey: toolName` 分发、以 `GenericToolCard` 作调用点 `fallback`;owner 载荷是统一的 `ToolRowOwnerProps`(`callId`/`toolName`/`block`/`openDetails`),`ToolRowProps` 把它与 session 标配 kit 预组合供注册方组件取用。注册方就是普通插件、零专用设施:`ctx.slots.register({ name: 'conversation.chat.toolview', key: '', inject? }, Row)`,以 `inject: ['slots', 'conversation']` 作加载序缝(conversation 服务在场即保证槽已声明)。会话维差异化在组件内完成——`useSessions` 读 `parentId`——不走注册表谓词;交互草稿等行内状态走普通 store 席位。trajectory/waterfall 得同形槽(槽名按槽名纪律 `<域>.<条目>.<孔位>` 已定死,共用一张 owner 类型),随各自的行渲染点落地——RendersCheck 拒绝无人渲染的声明,两槽无法提前声明。 +slot 之外不存在第二种注册模型——原视图环与工具环都已溶解进来。会话视图即 ui-conversation 声明的 `'conversation.view'` list 坑的 entry,tab 元数据随注册 options(`id`/`order`/`label`)走,per-view chrome 住视图组件自身。工具行是各视图自己声明的 keyed 子槽——今天是 `'conversation.chat.toolview'`(keyed/session),由 chat 条目的 `children` 表声明;key 空间运行时开放(SlotMap 声明槽、从不声明 key),这正是工具环「tool 名开放集」的原需求。渲染点逐行以 `entryKey: toolName` 分发、以 `GenericToolCard` 作调用点 `fallback`;owner 载荷是统一的 `ToolRowOwnerProps`(`callId`/`toolName`/`block`/`openDetails`),`ToolRowProps` 把它与 session 标配 kit 预组合供注册方组件取用。注册方就是普通插件、零专用设施:`ctx.slots.register({ name: 'conversation.chat.toolview', key: '', inject? }, Row)`,以 `inject: ['slots', 'conversation']` 作加载序缝(conversation 服务在场即保证槽已声明)。交互草稿等行内状态走普通 store 席位。trajectory/waterfall 得同形槽(槽名按槽名纪律 `<域>.<条目>.<孔位>` 已定死,共用一张 owner 类型),随各自的行渲染点落地——RendersCheck 拒绝无人渲染的声明,两槽无法提前声明。 **scope 寻址**与 host 侧 agent scope 惯例同构:服务是 root 单例,方法不收 sessionId——它们读调用方 ctx 上的 scope 标(`scopeOf(ctx)`)。在会话 scope 内,`ctx.conversation.send('hi', 'queue')` 自动打到该会话;跨会话调用换 ctx 定向(`ctx.sessions.scope(id)!.conversation.send(...)`);从 root ctx 直接调 scoped 方法即 throw。client 会话 scope 的铸造方式与 host agent scope 相同(no-op 插件 fiber + scope 键 extend),首次观看时惰性建,只有会话被移除且无人观看才拆——仅 host 会话死亡不拆 scope(冻结为只读视窗)。 diff --git a/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.md b/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.md index 80c2688b15..a695f98cb2 100644 --- a/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.md +++ b/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.md @@ -14,7 +14,7 @@ After the view ring dissolved into the slot system, the client kept exactly one The tool ring is gone as independent infrastructure: a tool row is a **keyed child slot each view declares for itself**, and the client has exactly one registration model. The justification above was hollow — a keyed slot's *key space* is already runtime-open (SlotMap declares slots, never keys; the ask-user composer's `key: 'question'` was the precedent), so the open tool-name set fits `entryKey` dispatch natively. -Shipped shape (current-state narrative also in the [architecture note](2026-07-19-gui-web-client-architecture.md)): the chat entry's `children` table declares `'conversation.chat.toolview'` (keyed/session); the render site dispatches per row via `entryKey: toolName` with `GenericToolCard` as the call-site `fallback` (the default card is domain property; the fallback option is ordinary renderSlot grammar). The owner payload is the uniform `ToolRowOwnerProps` (`callId`/`toolName`/`block`/`openDetails` — details being a session-level facility, not chat-private), and `ToolRowProps` pre-composes it with the session standard kit for registrant components. A registrant is a plain plugin: `ctx.slots.register({ name: 'conversation.chat.toolview', key: '', inject? }, Row)` with `inject: ['slots', 'conversation']` as the load-order seam — apply mounts `ConversationService` *after* the chat registration, so the service being present guarantees the slot is declared, by construction. Session-dimension differentiation happens inside the component (`useSessions` reading `parentId` — the decision sits where all the information already is); the bash sample is the third-party-posture exemplar and paints the same ToolRow chrome as Think (`Bash · {description}`, with a scoped badge only in child sessions). Trajectory/waterfall toolview slots share this exact shape (names fixed by the slot-naming discipline `..`, one shared owner type) and land with their own row render sites — RendersCheck rejects a declaration nobody renders, so the type system, not convention, blocks early empty declarations. +Shipped shape (current-state narrative also in the [architecture note](2026-07-19-gui-web-client-architecture.md)): the chat entry's `children` table declares `'conversation.chat.toolview'` (keyed/session); the render site dispatches per row via `entryKey: toolName` with `GenericToolCard` as the call-site `fallback` (the default card is domain property; the fallback option is ordinary renderSlot grammar). The owner payload is the uniform `ToolRowOwnerProps` (`callId`/`toolName`/`block`/`openDetails` — details being a session-level facility, not chat-private), and `ToolRowProps` pre-composes it with the session standard kit for registrant components. A registrant is a plain plugin: `ctx.slots.register({ name: 'conversation.chat.toolview', key: '', inject? }, Row)` with `inject: ['slots', 'conversation']` as the load-order seam — apply mounts `ConversationService` *after* the chat registration, so the service being present guarantees the slot is declared, by construction. The bash sample is the third-party-posture exemplar and paints the same ToolRow chrome as Think (`Bash · {description}`). Trajectory/waterfall toolview slots share this exact shape (names fixed by the slot-naming discipline `..`, one shared owner type) and land with their own row render sites — RendersCheck rejects a declaration nobody renders, so the type system, not convention, blocks early empty declarations. Registry-era responsibilities all have successor homes: inject caching and row error isolation ride the framework renderer (entry×scope cache, per-entry `SlotErrorBoundary`); subscribe/getVersion ride the slot core's per-key version machinery; the future "store seat" is the ordinary store seat keyed slots already have (interaction-draft durability is its first named consumer); miss fallback is the call-site `fallback` option. diff --git a/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.zh.md b/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.zh.md index 928c5f445d..e79f80216a 100644 --- a/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.zh.md @@ -14,7 +14,7 @@ Status: implemented 工具环作为独立基础设施已消失:工具行是**各视图为自己声明的 keyed 子槽**,client 全域只剩一种注册模型。上述理由是空的——keyed slot 的 *key 空间*本就运行时开放(SlotMap 声明槽、从不声明 key;ask-user composer 的 `key: 'question'` 即先例),开放的 tool 名集合天然适配 `entryKey` 分发。 -落地形态(现状叙述同见[架构注](2026-07-19-gui-web-client-architecture.md)):chat 条目的 `children` 表声明 `'conversation.chat.toolview'`(keyed/session);渲染点逐行以 `entryKey: toolName` 分发、以 `GenericToolCard` 作调用点 `fallback`(默认卡片是域产权;fallback 选项就是普通 renderSlot 文法)。owner 载荷是统一的 `ToolRowOwnerProps`(`callId`/`toolName`/`block`/`openDetails`——details 是会话级设施,非 chat 私货),`ToolRowProps` 把它与 session 标配 kit 预组合供注册方组件取用。注册方就是普通插件:`ctx.slots.register({ name: 'conversation.chat.toolview', key: '', inject? }, Row)`,以 `inject: ['slots', 'conversation']` 作加载序缝——apply 把 `ConversationService` 挂在 chat 注册*之后*,故服务在场即保证槽已声明,构造使然。会话维差异化在组件内完成(`useSessions` 读 `parentId`——决策放在已有全部信息的地方);bash 样例即第三方姿态的样板,并与 Think 绘制同一套 ToolRow chrome(`Bash · {description}`,scoped badge 仅出现在子会话)。trajectory/waterfall 的 toolview 槽共用这套形状(槽名按槽名纪律 `<域>.<条目>.<孔位>` 定死,共用一张 owner 类型),随各自的行渲染点落地——RendersCheck 拒绝无人渲染的声明,挡住提前空声明的是类型系统而非约定。 +落地形态(现状叙述同见[架构注](2026-07-19-gui-web-client-architecture.md)):chat 条目的 `children` 表声明 `'conversation.chat.toolview'`(keyed/session);渲染点逐行以 `entryKey: toolName` 分发、以 `GenericToolCard` 作调用点 `fallback`(默认卡片是域产权;fallback 选项就是普通 renderSlot 文法)。owner 载荷是统一的 `ToolRowOwnerProps`(`callId`/`toolName`/`block`/`openDetails`——details 是会话级设施,非 chat 私货),`ToolRowProps` 把它与 session 标配 kit 预组合供注册方组件取用。注册方就是普通插件:`ctx.slots.register({ name: 'conversation.chat.toolview', key: '', inject? }, Row)`,以 `inject: ['slots', 'conversation']` 作加载序缝——apply 把 `ConversationService` 挂在 chat 注册*之后*,故服务在场即保证槽已声明,构造使然。bash 样例即第三方姿态的样板,并与 Think 绘制同一套 ToolRow chrome(`Bash · {description}`)。trajectory/waterfall 的 toolview 槽共用这套形状(槽名按槽名纪律 `<域>.<条目>.<孔位>` 定死,共用一张 owner 类型),随各自的行渲染点落地——RendersCheck 拒绝无人渲染的声明,挡住提前空声明的是类型系统而非约定。 registry 时代的职责各有后继居所:inject 缓存与行错误隔离乘框架渲染器(entry×scope 缓存、per-entry `SlotErrorBoundary`);subscribe/getVersion 乘 slot core 的 per-key 版本机;将来的「store 席位」就是 keyed slot 本就拥有的普通 store 席位(交互草稿耐久性是其首个具名消费者);miss 兜底即调用点 `fallback` 选项。 diff --git a/.agents/notes/implemented/feature/2026-07-27-web-session-fork-actions.md b/.agents/notes/implemented/feature/2026-07-27-web-session-fork-actions.md index b5dc7e820d..58960169a2 100644 --- a/.agents/notes/implemented/feature/2026-07-27-web-session-fork-actions.md +++ b/.agents/notes/implemented/feature/2026-07-27-web-session-fork-actions.md @@ -14,7 +14,7 @@ The Web Session-row menu and message IconActions share the client runtime's `ses `forkAt(seq)` touches the session service only in ui-conversation's apply injection layer; message components report only the event `seq`. Session rows likewise initiate the operation only through ui-workspace's injected callback. Neither presentation package owns session mutation state or duplicates the host's boundary evaluation. -Session lineage is not projected into a list hierarchy. WorkSpace mode displays source sessions and all fork children as peer rows in the manual order from `WorkspaceView.sessionIds`; every row can be opened, searched, and dragged independently. In one list mode continues to sort strictly by `updatedAt`; the Ungrouped group also sorts by recency when no workspace ledger is available. `parentId` remains available for lineage, tool presentation, and later queries, but does not control session-list visibility. +Session lineage is not projected into a list hierarchy. WorkSpace mode displays source sessions and all fork children as peer rows in the manual order from `WorkspaceView.sessionIds`; every row can be opened, searched, and dragged independently. In one list mode continues to sort strictly by `updatedAt`; the Ungrouped group also sorts by recency when no workspace ledger is available. `parentId` remains available for lineage and later queries, but does not control session-list visibility. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-07-27-web-session-fork-actions.zh.md b/.agents/notes/implemented/feature/2026-07-27-web-session-fork-actions.zh.md index 774cd74d69..ea2f9030f6 100644 --- a/.agents/notes/implemented/feature/2026-07-27-web-session-fork-actions.zh.md +++ b/.agents/notes/implemented/feature/2026-07-27-web-session-fork-actions.zh.md @@ -14,7 +14,7 @@ Web 的 session 行菜单与消息 IconActions 共用 client runtime 的 `sessio `forkAt(seq)` 只在 ui-conversation 的 apply 注入层接触 session 服务,消息组件只回传事件 `seq`。Session 行同理只通过 ui-workspace 的注入回调发起操作;两个呈现包都不持有 session mutation 状态,也不复制 host 的边界求值。 -Session lineage 不投影成列表层级。WorkSpace 模式按 `WorkspaceView.sessionIds` 的手动序把源会话与所有 fork 子会话显示为同级行,每行都可独立打开、搜索和拖拽;In one list 模式继续按 `updatedAt` 严格排序;Ungrouped 组在没有 workspace 账本时也按 recency 排序。`parentId` 仍用于 lineage、工具呈现和后续查询,但不控制 session 列表可见性。 +Session lineage 不投影成列表层级。WorkSpace 模式按 `WorkspaceView.sessionIds` 的手动序把源会话与所有 fork 子会话显示为同级行,每行都可独立打开、搜索和拖拽;In one list 模式继续按 `updatedAt` 严格排序;Ungrouped 组在没有 workspace 账本时也按 recency 排序。`parentId` 仍用于 lineage 和后续查询,但不控制 session 列表可见性。 ## Alternatives considered diff --git a/apps/web/tests/built-boot.snapshot.ts b/apps/web/tests/built-boot.snapshot.ts index d2f6d913dd..707d7b72a0 100644 --- a/apps/web/tests/built-boot.snapshot.ts +++ b/apps/web/tests/built-boot.snapshot.ts @@ -108,7 +108,7 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn // Opening a session reaches chat content through the fixture transport. fireEvent.click(await within(tree).findByText('Fixture 历史会话')) await waitFor(() => { - expect(document.querySelector('[data-sample="bash-global"]')).not.toBeNull() + expect(document.querySelector('[data-sample="bash"]')).not.toBeNull() }, { timeout: 10_000 }) // The write/edit turns render a real diff card through the assembled graph diff --git a/apps/web/tests/code-mode-round.e2e.ts b/apps/web/tests/code-mode-round.e2e.ts index 6a0379de94..fd103ac53d 100644 --- a/apps/web/tests/code-mode-round.e2e.ts +++ b/apps/web/tests/code-mode-round.e2e.ts @@ -112,7 +112,7 @@ describe('web e2e: Code Mode round renders nested sub-calls', () => { // the bash sub-call landed in the bash sample registration. const nest = page.locator('[data-subcalls]').first() await nest.waitFor({ timeout: 10_000 }) - expect(await nest.locator('[data-sample="bash-global"]').count()).toBeGreaterThanOrEqual(1) + expect(await nest.locator('[data-sample="bash"]').count()).toBeGreaterThanOrEqual(1) // The failing read sub-call wears the same error state a native failed // row wears (the recorded program tolerates a read of missing.txt). expect(await nest.locator('[data-state="error"]').count()).toBeGreaterThanOrEqual(1) @@ -123,7 +123,7 @@ describe('web e2e: Code Mode round renders nested sub-calls', () => { const nest = page.locator('[data-subcalls]').first() const frame = page.locator('[style*="grid-template-columns"]').first() expect(await frame.getAttribute('data-details-collapsed')).toBe('true') - await nest.locator('[data-sample="bash-global"]').first().click() + await nest.locator('[data-sample="bash"]').first().click() // Tool rows do not drive layout geometry; the Session's default panel stays closed. await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true') }) diff --git a/apps/web/tests/navigation-panes.e2e.ts b/apps/web/tests/navigation-panes.e2e.ts index d6fe3d0942..d8d6de9036 100644 --- a/apps/web/tests/navigation-panes.e2e.ts +++ b/apps/web/tests/navigation-panes.e2e.ts @@ -184,7 +184,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => { it.skipIf(MODE === 'record')('bash and file-path rows leave the default details column closed', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-details')) await page.getByRole('tab', { name: 'Chat' }).click() - const bashRow = page.locator('[data-sample="bash-global"]').first() + const bashRow = page.locator('[data-sample="bash"]').first() await bashRow.waitFor({ timeout: 15_000 }) const frame = page.locator('[style*="grid-template-columns"]').first() expect(await frame.getAttribute('data-details-collapsed')).toBe('true') @@ -194,7 +194,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => { await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true') // The card's own controls are outside the summary row and must not open // details either — the expanded terminal card is read in place. - await page.locator('[data-sample="bash-global"] ~ div [data-terminal] [class*="_copyButton_"]').first().click() + await page.locator('[data-sample="bash"] ~ div [data-terminal] [class*="_copyButton_"]').first().click() await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true') // Read summaries are host-open file links; they also must not open details. const fileLink = page.locator('[data-variant="read"] button').first() @@ -210,10 +210,10 @@ describe('web e2e: navigation & panes over a rich seeded session', () => { // tool-row interaction): open it if a previous case left it collapsed. // Expanded, the recorded command's own output sits in the message flow, // derived from the logged call/result presentations alone. - const bashRow = page.locator('[data-sample="bash-global"]').first() + const bashRow = page.locator('[data-sample="bash"]').first() await bashRow.waitFor({ timeout: 15_000 }) if (await bashRow.getAttribute('aria-expanded') !== 'true') await bashRow.click() - const card = page.locator('[data-sample="bash-global"] ~ div [data-terminal]').first() + const card = page.locator('[data-sample="bash"] ~ div [data-terminal]').first() await card.waitFor({ timeout: 15_000 }) // Real layout, not jsdom's stub (which computes no geometry at all): // squeeze the output pane below its content width and the line must keep diff --git a/apps/web/tests/smoke-real.e2e.ts b/apps/web/tests/smoke-real.e2e.ts index 4a104724fb..5161bbf85d 100644 --- a/apps/web/tests/smoke-real.e2e.ts +++ b/apps/web/tests/smoke-real.e2e.ts @@ -588,7 +588,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke // Bash renders through the third-party sample registration. Match that // exact row: other clickable variants (for example Think disclosure) // may precede the tool call in document order. - const toolRow = page.locator('[data-sample="bash-global"]') + const toolRow = page.locator('[data-sample="bash"]') await toolRow.waitFor({ timeout: 120_000 }) await screen(page, '08-bash-round') expect(await detailsTrack(page)).toBe(0) diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 68115f8f9b..d4387f8f05 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -28,7 +28,7 @@ The chat flow projects consecutive model-retry nodes across retry turns into one A `grep`/`glob` call declaring the `search` render intent renders its result inline, at the same render sites, through ui-primitives' `SearchBlock` — grep's matches grouped by file (each a collapsible header of `lineNumber: line` rows), glob's flat path list. `contract/search-card-model.ts` is the single derivation from the snapshot's `resultView`; unlike the terminal card it reads no `callView`, since a search has no matches or paths before `execute`, so a running search shows its summary alone. It yields null — the generic path — for any non-search result view, a `card` or `kind` this client version does not compile, and (because those ride the untrusted wire frame) a known kind whose `files`/`paths` is malformed. The keyed `SearchRow`, registered under both `grep` and `glob` since the derived `kind` decides the shape, composes the shared `ToolRow`, feeding the card as ToolRow's `search` body, so it is the row's collapsed-by-default expanded card; the render-site fallback routes it the same way. Both cap at `CHAT_SEARCH_MAX_LINES` (8) against the panel's 16. A capped search drops rows from the card, but the locator to the rest — grep/glob's `Full … stored at …` footer — lives only in the result text, so the derivation surfaces that as a recovery footer below the card when (and only when) the result was truncated; a settled call with no card at all (an errored search, a nested `run_code` sub-dispatch, a legacy generic result) surfaces its flattened result text through ToolRow's Output section so nothing is lost behind a bare summary ([decision](../../../.agents/notes/implemented/feature/2026-07-30-web-search-card.md)). -Tool rows are slots too — the standalone tool ring (`ToolViewRegistry`/`ctx.toolviews`/outlet) is retired. The chat entry declares the keyed `'conversation.chat.toolview'` hole (session scope; the key space is runtime-open); its render site dispatches per row via `entryKey: toolName` with `GenericToolCard` as the call-site `fallback`. The owner payload is the uniform `ToolRowOwnerProps` (`callId`/`toolName`/`block`/`openFile`) and `ToolRowProps` pre-composes it with the session standard kit. A registrant is a plain plugin: `ctx.slots.register({ name: 'conversation.chat.toolview', key: '', inject? }, Row)` with `inject: ['slots', 'conversation']` as the load-order seam (apply mounts ConversationService after the chat registration, so the service being present guarantees the slot is declared); session differentiation happens inside the component (`useSessions` reading `parentId` — the bash sample is the third-party-posture exemplar). Trajectory/waterfall toolview slots share this shape and land with their own render sites (RendersCheck rejects a declaration nobody renders). +Tool rows are slots too — the standalone tool ring (`ToolViewRegistry`/`ctx.toolviews`/outlet) is retired. The chat entry declares the keyed `'conversation.chat.toolview'` hole (session scope; the key space is runtime-open); its render site dispatches per row via `entryKey: toolName` with `GenericToolCard` as the call-site `fallback`. The owner payload is the uniform `ToolRowOwnerProps` (`callId`/`toolName`/`block`/`openFile`) and `ToolRowProps` pre-composes it with the session standard kit. A registrant is a plain plugin: `ctx.slots.register({ name: 'conversation.chat.toolview', key: '', inject? }, Row)` with `inject: ['slots', 'conversation']` as the load-order seam (apply mounts ConversationService after the chat registration, so the service being present guarantees the slot is declared); the bash sample is the third-party-posture exemplar. Trajectory/waterfall toolview slots share this shape and land with their own render sites (RendersCheck rejects a declaration nobody renders). The todo surfaces are two registrations over that shape, both plain registrant plugins with `inject: ['slots', 'conversation']`. `TodoRow` takes the `'conversation.chat.toolview'` key `todo_write` and summarizes what the call attempted (`/ 已完成 · ` parsed from its args, falling back to the generic summary on malformed or wrongly-shaped model JSON, and keeping the generic dot for non-ok execution states so a cancelled call never reads as a completed update). `TodoDock` takes the `'conversation.input.dock'` list slot at `order: -1` — above the queue rows — and is the plan strip: it reads the host-computed `todos` projection via `useProjection` (standing plan: latest `todo/write` with no later `turn/start`) and renders `TodoPanel`, which takes the plain list, hides itself while the list is empty, and starts collapsed as a header of title plus `"/ tasks · in progress"` (status glyphs are the figma check / progress / dashed-pending set). The dock adapter owns the selection so the panel stays a pure function of its props; the standing list lives here rather than in the row so the row stays one line. Anything the input-zone composer chain hides (a `conversation.composer` takeover such as ui-question's) hides the whole dock, this strip included. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index 65a3b334ff..295954cf17 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -26,7 +26,7 @@ 声明 `search` 渲染意图的 `grep`/`glob` 调用,会在同样的渲染点上通过 ui-primitives 的 `SearchBlock` 内联渲染其结果——grep 的匹配按文件分组(每个是一个可折叠的头,下辖 `lineNumber: line` 行),glob 是扁平路径列表。`contract/search-card-model.ts` 是从快照的 `resultView` 推导的唯一位置;与终端卡片不同,它不读 `callView`,因为搜索在 `execute` 前没有匹配或路径,所以运行中的搜索只显示摘要。对任何非搜索的结果视图、当前客户端版本无法编译的 `card` 或 `kind`、以及(因为这些都与不可信的 wire 帧同行)一个 `files`/`paths` 格式错误的已知 kind,它都返回 null,落回通用路径。键控的 `SearchRow` 因推导出的 `kind` 决定形态而同时注册在 `grep` 与 `glob` 下,组合共享的 `ToolRow`,把卡片作为 ToolRow 的 `search` body 传入,因此它是该行默认折叠的展开卡片;渲染点兜底行以同样方式渲染它。两者上限都是 `CHAT_SEARCH_MAX_LINES`(8),面板为 16。被截断的搜索会从卡片里丢掉一些行,但通往其余部分的定位符——grep/glob 的 `Full … stored at …` 脚注——只存在于结果文本里,因此推导在(且仅在)结果被截断时把它作为恢复脚注画在卡片下方;一个完全没有卡片的已结算调用(出错的搜索、嵌套 `run_code` 子派发、旧日志的 generic 结果)则经 ToolRow 的 Output 区呈现其压平后的结果文本,从而不让任何内容丢失在一个光秃秃的摘要之后([决策](../../../.agents/notes/implemented/feature/2026-07-30-web-search-card.md))。 -工具行同样是 slot:独立工具环(`ToolViewRegistry`/`ctx.toolviews`/outlet)已经退役。聊天配置项声明键控的 `'conversation.chat.toolview'` 空位(Session scope;key 空间在运行时开放);其渲染点逐行通过 `entryKey: toolName` 分发,并以 `GenericToolCard` 作为调用点 `fallback`。owner 载荷是统一的 `ToolRowOwnerProps`(`callId`/`toolName`/`block`/`openFile`),`ToolRowProps` 则预先将其与 Session 标准工具包组合。注册方只是普通插件:`ctx.slots.register({ name: 'conversation.chat.toolview', key: '', inject? }, Row)`,以 `inject: ['slots', 'conversation']` 作为加载顺序 seam(apply 在聊天注册后挂载 ConversationService,因此服务存在即可保证 slot 已声明);Session 区分在组件内部完成(`useSessions` 读取 `parentId`,bash 示例是第三方姿态的范例)。Trajectory/waterfall(瀑布式事件)工具视图 slot 共享此形状,并随各自的渲染点落地(RendersCheck 会拒绝没有任何渲染方的声明)。 +工具行同样是 slot:独立工具环(`ToolViewRegistry`/`ctx.toolviews`/outlet)已经退役。聊天配置项声明键控的 `'conversation.chat.toolview'` 空位(Session scope;key 空间在运行时开放);其渲染点逐行通过 `entryKey: toolName` 分发,并以 `GenericToolCard` 作为调用点 `fallback`。owner 载荷是统一的 `ToolRowOwnerProps`(`callId`/`toolName`/`block`/`openFile`),`ToolRowProps` 则预先将其与 Session 标准工具包组合。注册方只是普通插件:`ctx.slots.register({ name: 'conversation.chat.toolview', key: '', inject? }, Row)`,以 `inject: ['slots', 'conversation']` 作为加载顺序 seam(apply 在聊天注册后挂载 ConversationService,因此服务存在即可保证 slot 已声明);bash 示例是第三方姿态的范例。Trajectory/waterfall(瀑布式事件)工具视图 slot 共享此形状,并随各自的渲染点落地(RendersCheck 会拒绝没有任何渲染方的声明)。 审批经由本包声明的链接管编辑器:`ApprovalPanel` 注册为按选择器路由的 `'conversation.composer'` 配置项(ui-question 模式),在审批等待未决期间取代 InputBar 占据编辑器(琥珀色条、理由标题、来自运行中调用参数的配对命令行、一次性的拒绝/允许)。`contract/slots.ts` 中的 `PendingApproval` 领域面在运行时 `PendingWait` 载体之上拥有 wire 编码——带审计关联的 `ApprovalResponsePayload` 值;广播的 `approval/resolved` 帧使等待落定并恢复编辑器。侧边栏通过 manager 跟踪的 `waitingApproval` 列表位(未实例化会话同样点亮)镜像该阻塞状态,其优先级高于运行中圆环,直至问题解决。未决等待完全离开消息流:问题(ui-question)与审批(ApprovalPanel)都经编辑器接管作答,不再保留只读占位卡。编辑器底行的 Access 席位挂载 `PermissionSelect`,由 host 计算的 `permissions` 投影经标准工具包 `useProjection` 供数(key 缺席即隐藏 chip);chip 打开 Menu 原语下拉,其中 kebab-case 预设名渲染为 Title Case 标签;普通安全预设会立即经输入栏注入的 `command` 回调提交 `/permission `,而 `danger-full-access` 在界面中显示为 `Full access`,选择后先打开页面内的 Modal 风险确认。用户勾选确认项前启用按钮始终不可用;取消、Escape、关闭按钮与点击遮罩都不会提交命令。 diff --git a/packages/client/ui-conversation/src/client/apply.ts b/packages/client/ui-conversation/src/client/apply.ts index 489ad34e3f..4f3536f4b2 100644 --- a/packages/client/ui-conversation/src/client/apply.ts +++ b/packages/client/ui-conversation/src/client/apply.ts @@ -319,7 +319,7 @@ export function apply(ctx: Context): void { ctx.plugin(ConversationService, { input: inputHub }) // The bash sample rides that exact seam, in third-party posture - // (ToolRow-matching Bash · {description} chrome; scoped badge in child sessions). + // (ToolRow-matching Bash · {description} chrome). ctx.plugin(bashToolviewSample) // The read row rides the same seam (a product registration, not a sample): diff --git a/packages/client/ui-conversation/src/client/toolviews/bash-sample.module.css b/packages/client/ui-conversation/src/client/toolviews/bash-sample.module.css index fa607a0880..ef9f246dd7 100644 --- a/packages/client/ui-conversation/src/client/toolviews/bash-sample.module.css +++ b/packages/client/ui-conversation/src/client/toolviews/bash-sample.module.css @@ -99,17 +99,6 @@ opacity: 1; } -.scopeBadge { - flex: none; - margin-right: 8px; - padding: 0 6px; - border-radius: 6px; - font-size: 11px; - line-height: 18px; - color: var(--dsw-alias-label-primary-foreground); - background: var(--dsw-alias-state-business-primary); -} - .title { flex: none; font-size: 14px; diff --git a/packages/client/ui-conversation/src/client/toolviews/bash-sample.tsx b/packages/client/ui-conversation/src/client/toolviews/bash-sample.tsx index 06eb42f741..5a3e3f40fe 100644 --- a/packages/client/ui-conversation/src/client/toolviews/bash-sample.tsx +++ b/packages/client/ui-conversation/src/client/toolviews/bash-sample.tsx @@ -1,8 +1,6 @@ // Bash toolview registrant: third-party posture over the keyed toolview hole // (ctx.slots.register + ToolRowProps only — never imports the chat domain). // Product chrome matches ToolRow / Think (figma: Bash · {description}). -// Child sessions keep a scoped badge so session-dimension differentiation stays -// observable inside the component (no parallel registry). // // A bash call declares the terminal render intent, so this row renders the // command's own output through TerminalBlock — expand-gated exactly like @@ -64,7 +62,6 @@ export function BashRow({ toolName, block, sessionId, useSessions, inspect, t }: const state = model.state === 'ok' && terminal !== null && terminalFailed(terminal) ? 'error' : model.state - const isChild = useSessions(list => list.byId[sessionId]?.parentId !== undefined) const status = stateStatus(state, t) const [expanded, setExpanded] = useState(false) const expandable = terminal !== null @@ -92,7 +89,7 @@ export function BashRow({ toolName, block, sessionId, useSessions, inspect, t }:
{leading} {status !== null && {status}} - {isChild && scoped} {model.title} {/* The terminal presenter's description is the contractual diff --git a/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx b/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx index 3ddaef873e..71141bcb1b 100644 --- a/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx +++ b/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx @@ -149,7 +149,7 @@ describe('terminal card assembly', () => { const view = runtime.renderRoot() // Keyed BashRow: collapsed by default, the whole summary row is the toggle. - const keyedRow = view.container.querySelector('[data-sample="bash-global"]') + const keyedRow = view.container.querySelector('[data-sample="bash"]') const keyed = keyedRow?.parentElement expect(keyed?.querySelector('[data-terminal]')).toBeNull() fireEvent.click(keyedRow!) diff --git a/packages/client/ui-conversation/tests/chat-code-subcalls.spec.tsx b/packages/client/ui-conversation/tests/chat-code-subcalls.spec.tsx index bafc6fe709..eaae31f72d 100644 --- a/packages/client/ui-conversation/tests/chat-code-subcalls.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-code-subcalls.spec.tsx @@ -181,7 +181,7 @@ describe('run_code sub-calls through the real chat machinery', () => { // sub-tool fell back to GenericToolCard at the same render site. const nest = view.container.querySelector('[data-subcalls]') expect(nest).not.toBeNull() - expect(nest!.querySelector('[data-sample="bash-global"]')).not.toBeNull() + expect(nest!.querySelector('[data-sample="bash"]')).not.toBeNull() expect(view.getByText('Bash')).toBeTruthy() expect(view.getByText('List notes')).toBeTruthy() expect(view.getByText('Tool call')).toBeTruthy() @@ -264,7 +264,7 @@ describe('run_code sub-calls through the real chat machinery', () => { expect(running).not.toBeNull() const nest = view.container.querySelector('[data-subcalls]') expect(nest).not.toBeNull() - expect(nest!.querySelector('[data-sample="bash-global"]')).not.toBeNull() + expect(nest!.querySelector('[data-sample="bash"]')).not.toBeNull() }) it('a started-but-unsettled sub-call renders the running state exactly like a native in-flight row', async () => { diff --git a/packages/client/ui-conversation/tests/chat-stats-bash-sample.spec.tsx b/packages/client/ui-conversation/tests/chat-stats-bash-sample.spec.tsx index 512ca2a86c..2220aa3048 100644 --- a/packages/client/ui-conversation/tests/chat-stats-bash-sample.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-stats-bash-sample.spec.tsx @@ -1,8 +1,7 @@ // @vitest-environment jsdom // StatsLine (composer.dock entry): totals derivation + the RFC -// hard acceptance — zero renders during streaming. Bash sample row: the -// canonical sub-agent differential decided INSIDE the component off the -// standard useSessions kit (no registry predicates — tool ring dissolved). +// hard acceptance — zero renders during streaming. Bash sample row: ToolRow +// chrome (Bash · description) without a row click target. import { afterEach, describe, expect, it, vi } from 'vitest' import { act, cleanup, render } from '@testing-library/react' @@ -232,8 +231,7 @@ describe('StatsLine', () => { }) describe('bash sample row', () => { - const ROOT = 'root-1' as SessionId - const CHILD = 'child-1' as SessionId + const SID = 'root-1' as SessionId const result = (callId: string): ToolResultNode => ({ kind: 'tool-result', seq: 3, time: 3_000, callId, @@ -242,68 +240,30 @@ describe('bash sample row', () => { content: [], isError: false, callView: null, resultView: null, }) - /** Real list-store engine: the family fixture the in-component parentId branch reads. */ function listStore() { return createSnapshotStore({ - ids: [ROOT, CHILD], + ids: [SID], byId: { - [ROOT]: { id: ROOT, title: 'r', displayTitle: 'r', running: false, waitingApproval: false, blank: false, updatedAt: 0 }, - [CHILD]: { id: CHILD, title: 'c', displayTitle: 'c', parentId: ROOT, running: false, waitingApproval: false, blank: false, updatedAt: 0 }, + [SID]: { id: SID, title: 'r', displayTitle: 'r', running: false, waitingApproval: false, blank: false, updatedAt: 0 }, }, current: undefined, phase: 'ready', }) } - const rowProps = (sessionId: SessionId, over?: { - store?: ReturnType - }): BashRowProps => ({ + const rowProps = (): BashRowProps => ({ callId: 'c1', toolName: 'bash', block: result('c1'), openFile: vi.fn(), - sessionId, - useSessions: bindSnapshotSelector(over?.store ?? listStore()), + sessionId: SID, + useSessions: bindSnapshotSelector(listStore()), t, } as unknown as BashRowProps) - it('differential rendering: the scoped variant in sub-sessions, global at roots', () => { - const scoped = render() - expect(scoped.container.querySelector('[data-sample="bash-scoped"]')).not.toBeNull() - expect(scoped.getByText('scoped')).toBeTruthy() - const plain = render() - expect(plain.container.querySelector('[data-sample="bash-global"]')).not.toBeNull() - }) - - it('a session outside the list renders the global arm (no parent known)', () => { - const view = render() - expect(view.container.querySelector('[data-sample="bash-global"]')).not.toBeNull() - }) - - it('a live parentId write flips the row to the scoped variant (store subscription)', () => { - const store = listStore() - const orphan = 'late-child' as SessionId - store.update((d) => { - d.ids.push(orphan) - d.byId[orphan] = { id: orphan, title: 'l', displayTitle: 'l', running: false, waitingApproval: false, blank: false, updatedAt: 0 } - }) - const view = render() - expect(view.container.querySelector('[data-sample="bash-global"]')).not.toBeNull() - act(() => { - store.update((d) => { d.byId[orphan]!.parentId = ROOT }) - }) - expect(view.container.querySelector('[data-sample="bash-scoped"]')).not.toBeNull() - }) - - it('summarizes as Bash · description on both arms without row click targets', () => { - const global = render() - // Two renders share document.body: query inside each container. - const globalRow = global.container.querySelector('[data-sample="bash-global"]')! - expect(globalRow.textContent).toContain('Bash') - expect(globalRow.textContent).toContain('Build') - expect(globalRow.getAttribute('data-clickable')).toBeNull() - const scoped = render() - const scopedRow = scoped.container.querySelector('[data-sample="bash-scoped"]')! - expect(scopedRow.textContent).toContain('Bash') - expect(scopedRow.textContent).toContain('Build') - expect(scopedRow.getAttribute('data-clickable')).toBeNull() + it('summarizes as Bash · description without a row click target', () => { + const view = render() + const row = view.container.querySelector('[data-sample="bash"]')! + expect(row.textContent).toContain('Bash') + expect(row.textContent).toContain('Build') + expect(row.getAttribute('data-clickable')).toBeNull() }) }) diff --git a/packages/client/ui-conversation/tests/chat-toolview-slot.spec.tsx b/packages/client/ui-conversation/tests/chat-toolview-slot.spec.tsx index eb48677d4f..34f2ff9cfd 100644 --- a/packages/client/ui-conversation/tests/chat-toolview-slot.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-toolview-slot.spec.tsx @@ -91,7 +91,7 @@ describe('keyed toolview hole through the real machinery', () => { const view = b.runtime.renderRoot() // bash: the sample plugin's keyed registration took the row (root // session → global arm, decided inside the component off useSessions). - expect(view.container.querySelector('[data-sample="bash-global"]')).not.toBeNull() + expect(view.container.querySelector('[data-sample="bash"]')).not.toBeNull() expect(view.getByText('Bash')).toBeTruthy() expect(view.getByText('Build')).toBeTruthy() // mystery: no registration under that key → render-site fallback. diff --git a/packages/client/ui-conversation/tests/coverage-tails.spec.tsx b/packages/client/ui-conversation/tests/coverage-tails.spec.tsx index 30ca8bccf7..24c900deeb 100644 --- a/packages/client/ui-conversation/tests/coverage-tails.spec.tsx +++ b/packages/client/ui-conversation/tests/coverage-tails.spec.tsx @@ -89,7 +89,7 @@ describe('tails', () => { expect(view.container.querySelector('[data-state="ok"]')).not.toBeNull() }) - it('BashRow carries data-state for running (row sweep) and StateDots for error/stopped (root session arm)', () => { + it('BashRow carries data-state for running (row sweep) and StateDots for error/stopped', () => { const sid = 'root-1' as SessionId const list = createSnapshotStore({ ids: [sid], @@ -125,7 +125,7 @@ describe('tails', () => { runningView.unmount() const errorView = render() - expect(errorView.container.querySelector('[data-sample="bash-global"]')).not.toBeNull() + expect(errorView.container.querySelector('[data-sample="bash"]')).not.toBeNull() expect(errorView.container.querySelector('[data-state="error"]')).not.toBeNull() expect(errorView.getByText('失败')).toBeTruthy() errorView.unmount() From c3e4aeca90b37cc515ae445152424a206e3e21ab Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Fri, 31 Jul 2026 10:49:03 +0800 Subject: [PATCH 2/5] fix: missing notes --- .../implemented/architecture/2026-07-23-toolview-dissolution.md | 2 +- .../architecture/2026-07-23-toolview-dissolution.zh.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.md b/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.md index a695f98cb2..406e5c181a 100644 --- a/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.md +++ b/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.md @@ -20,7 +20,7 @@ Registry-era responsibilities all have successor homes: inject caching and row e ## Accepted semantic changes -Four behavioral deltas were accepted deliberately, not overlooked. Cross-view appearance is per-view registration — a row must adapt to each view's layout anyway, so one registration per view is the correct coupling, and reuse is the same component in two register calls. Same-key double registration is a loud throw where the registry let later-wins silently override — a discipline correction, not a loss. Session-dimension dispatch moved from registry predicates into the component. Registry-level shape override by third parties (a scoped registration shadowing a global one) has no equivalent; a real future need routes through key-naming conventions or a small in-component resolver, never a revived parallel registry. +Four behavioral deltas were accepted deliberately, not overlooked. Cross-view appearance is per-view registration — a row must adapt to each view's layout anyway, so one registration per view is the correct coupling, and reuse is the same component in two register calls. Same-key double registration is a loud throw where the registry let later-wins silently override — a discipline correction, not a loss. Session-dimension dispatch, when a row needs it, belongs inside the component (the standard kit already carries `useSessions`), not in registry predicates — there is no shipped session-variant exemplar today. Registry-level shape override by third parties (a scoped registration shadowing a global one) has no equivalent; a real future need routes through key-naming conventions or a small in-component resolver, never a revived parallel registry. ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.zh.md b/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.zh.md index e79f80216a..311affcbd9 100644 --- a/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.zh.md @@ -20,7 +20,7 @@ registry 时代的职责各有后继居所:inject 缓存与行错误隔离乘 ## 接受的语义变化 -四项行为增量是刻意接受而非疏漏。跨视图出场=逐视图注册——行本须适配各视图版式,一视图一注册是正确耦合,复用即同一组件写两次 register。同 key 重复注册从注册表的 later-wins 静默覆盖变为 loud throw——纪律修正而非损失。会话维分发从注册表谓词移入组件。第三方在 registry 级覆盖形态(scoped 注册压过 global)不复存在;真出现的未来需求走 key 命名空间约定或组件内小 resolver,永不复活平行注册表。 +四项行为增量是刻意接受而非疏漏。跨视图出场=逐视图注册——行本须适配各视图版式,一视图一注册是正确耦合,复用即同一组件写两次 register。同 key 重复注册从注册表的 later-wins 静默覆盖变为 loud throw——纪律修正而非损失。会话维分发若行需要,归组件内部(标配 kit 已带 `useSessions`),不走注册表谓词——今天没有已落地的会话变体样例。第三方在 registry 级覆盖形态(scoped 注册压过 global)不复存在;真出现的未来需求走 key 命名空间约定或组件内小 resolver,永不复活平行注册表。 ## Alternatives considered From 84255e1d7157e8fd6c2becf8f230a198043dd08b Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Fri, 31 Jul 2026 11:29:44 +0800 Subject: [PATCH 3/5] fix: ci --- .../architecture/2026-07-23-toolview-dissolution.i18n.yaml | 6 +++--- .../feature/2026-07-27-web-session-fork-actions.i18n.yaml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.i18n.yaml index 2cba925d67..20745458d6 100644 --- a/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write -2026-07-23-toolview-dissolution.md: 80c2688b152d1afe1236d4815633a5bf024db1d2 -2026-07-23-toolview-dissolution.zh.md: 928c5f445d601b2246d3ae2f9360232643814468 +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.md +2026-07-23-toolview-dissolution.md: 406e5c181aabb635f9d6dcb12d8a9b8b6697368e +2026-07-23-toolview-dissolution.zh.md: 311affcbd9605ff81b78e974f75ee83328d93f68 diff --git a/.agents/notes/implemented/feature/2026-07-27-web-session-fork-actions.i18n.yaml b/.agents/notes/implemented/feature/2026-07-27-web-session-fork-actions.i18n.yaml index e4b5cd7778..21eea20254 100644 --- a/.agents/notes/implemented/feature/2026-07-27-web-session-fork-actions.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-27-web-session-fork-actions.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-web-session-fork-actions.md -2026-07-27-web-session-fork-actions.md: b5dc7e820de069a68b38ed87c7d29ffbdb4867bc -2026-07-27-web-session-fork-actions.zh.md: 774cd74d69eb02d43ca01c8ec7ba1cf94c24b1dc +2026-07-27-web-session-fork-actions.md: 58960169a2e499d953840e5769e7689b5cd48047 +2026-07-27-web-session-fork-actions.zh.md: ea2f9030f672f00fb91bce3546836689a7d41004 From 0c59e3e0892be3ffe5886b8c55e94fa0003bcee6 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Fri, 31 Jul 2026 19:15:40 +0800 Subject: [PATCH 4/5] fix: cr --- apps/web/tests/search-card.snapshot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/tests/search-card.snapshot.ts b/apps/web/tests/search-card.snapshot.ts index 2bfa4f9244..0315a29e37 100644 --- a/apps/web/tests/search-card.snapshot.ts +++ b/apps/web/tests/search-card.snapshot.ts @@ -143,7 +143,7 @@ describe('assembled search card', () => { // Wait for chat content to reach the fixture's later turns (the bash sample // is turn 65, the grep card turn 66). await waitFor(() => { - expect(document.querySelector('[data-sample="bash-global"]')).not.toBeNull() + expect(document.querySelector('[data-sample="bash"]')).not.toBeNull() }, { timeout: 10_000 }) // The grep turn's keyed SearchRow composes ToolRow: the card is collapsed // by default, so wait for the summary row, then expand it to reach the card. From ddda8be7367a16c3a232d5ddbd1b04c7307ad54c Mon Sep 17 00:00:00 2001 From: kingwl Date: Fri, 31 Jul 2026 20:48:52 +0800 Subject: [PATCH 5/5] docs: record scoped bash translation pairs --- .../2026-07-19-gui-web-client-architecture.i18n.yaml | 4 ++-- packages/client/ui-conversation/README.i18n.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml index 4530d5ee57..61e6a93e23 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md -2026-07-19-gui-web-client-architecture.md: 63b6f5795c3d49f25cd964cf04a0c9d41a667bfb -2026-07-19-gui-web-client-architecture.zh.md: 2d57c12ebae38aafa4e606da95af954990761b3c +2026-07-19-gui-web-client-architecture.md: b1f777172774f1cf8fef4d9494f15b38064d0c73 +2026-07-19-gui-web-client-architecture.zh.md: e43151b7d5ff096d574c786e3aae107523d22c96 diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index f8ac552846..0ef0573ed6 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-conversation/README.md -README.md: 68115f8f9b9225d1f6b0e9cc93394d042c2befaa -README.zh.md: 65a3b334ff316a37dfb8506396eae8c17cccb40d +README.md: d4387f8f0547e81211f6d33cb806456b6441066f +README.zh.md: 295954cf172b5f6c46fd70a992a7680183fed028