docs(notes): archive low-value decision records
This commit is contained in:
-6
@@ -1,6 +0,0 @@
|
||||
# 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-20-code-mode-result-card-completeness.md: 05ff0ed41c94bef7eb41204d8dd81bbda3c06016
|
||||
2026-07-20-code-mode-result-card-completeness.zh.md: a93be4cc42fca87ce4ef11b6ad3a6cbe64bef66f
|
||||
@@ -1,39 +0,0 @@
|
||||
# Agent Note: Keep the Code Mode result card complete
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-20-code-mode-result-card-completeness.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The outer `run_code` tool persisted complete rendered content, but its UI presenter ignored that content and rebuilt the card body from a logs-only `presentationMeta` projection. A result-only run appeared correct because an empty presenter body let consumers fall back to `tool/result.content`. Once the program emitted a log, the presenter supplied non-empty content, that fallback stopped, and the returned value disappeared from the completed card. A spill policy's final head/tail preview was vulnerable to the same split ownership whenever captured logs made the stale projection non-empty.
|
||||
|
||||
Nested Code calls never owned cards, so producing metadata for the outer call solely to reconstruct one incomplete card also obscured the intended one-card boundary.
|
||||
|
||||
## Decision
|
||||
|
||||
The canonical tool registry pipeline owns the final model-facing outer content. On success, the `run_code` output renderer renders captured logs followed by the return value or the explicit no-output marker. Runtime failures and pre-execution policy denials are normalized into error content by `ToolRegistry` without invoking that renderer. A post-execute block runs after successful rendering and replaces the result with error content; other post-execute policy and spill decisions may replace content before persistence.
|
||||
|
||||
`run_code` omits `presentResult`. The established generic result fallback keeps the pending program title and renders the raw final `tool/result.content`; that durable, replayable, post-policy projection is the card's only result-content source. The host API proxy therefore omits a separate result view instead of serializing the same content in both `event.data.content` and `view.view.content`. The redundant logs-only `presentationMeta` projection remains removed.
|
||||
|
||||
Nested dispatch remains unchanged. Calls marked by `exec.parent` emit `tool/code-dispatch` events (full rendered content) but no `tool/call` or `tool/result` surface cards, so one outer `run_code` invocation still produces exactly one card.
|
||||
|
||||
## Testing
|
||||
|
||||
Tool unit coverage drives logs-only, result-only, logs-plus-result, no-output, spilled-result, and failure outcomes through the canonical registry, then pins the durable content and absence of a result presenter. A host-mux regression uses a call-only presenter to prove the result frame carries raw content exactly once and no view. These cases prove stale metadata cannot replace final content without making the host duplicate that content.
|
||||
|
||||
The keyless ACP backend and TUI Code Mode snapshots execute one outer program that performs two nested bash calls, logs `captured output`, and returns `CODE_ONE+CODE_TWO`. The persisted ACP log pins the complete result; the TUI surface shows one completed outer card containing both lines and no nested cards.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Append the return value to logs metadata.** Rejected because metadata would duplicate the renderer, need a second stable formatting contract for every JSON root, and still miss post-policy content replacement or spill previews.
|
||||
|
||||
**Merge presenter metadata with `result.content`.** Rejected because the rendered content already contains the logs; merging would duplicate them and require brittle deduplication.
|
||||
|
||||
**Forward `result.content` through a generic result presenter.** Rejected because the durable event already carries that content and UI consumers already have a generic raw-content fallback. The host mux serializes a tool-owned result view beside the event, so forwarding would duplicate the rendered content in one frame merely to recreate the fallback; the default worker alone admits a 64 MiB variable-payload budget before rendering.
|
||||
|
||||
**Create one card per nested dispatch.** Rejected because intermediate values are intentionally execution-local and never model-facing. Multiple cards would expose an implementation trace instead of the single Code Mode operation the model and user invoked.
|
||||
|
||||
## Consequences
|
||||
|
||||
TUI and JSON-RPC/Web display the same complete content the model receives and replay persists, including post-policy spill previews, through their generic result fallback. The host API retains the pending program title without duplicating the raw result in a separate view payload. New `run_code` results no longer carry the optional logs metadata, but this requires no session-format bump: existing records remain valid because presentation reads their durable rendered content.
|
||||
@@ -1,39 +0,0 @@
|
||||
# Agent Note: 保证 Code Mode 结果卡片内容完整
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-20-code-mode-result-card-completeness.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
外层 `run_code` 工具会持久化完整的渲染内容,但其 UI 展示器忽略了这些内容,转而根据仅含日志的 `presentationMeta` 投影重新构建卡片正文。仅有结果的运行看似正确,是因为展示器正文为空时,消费方会回退到 `tool/result.content`。只要程序输出一条日志,展示器就会提供非空内容,回退随即停止,返回值便会从完成态卡片中消失。当已捕获的日志使陈旧投影变为非空时,输出落盘策略最终生成的头尾预览也会受到同一职责拆分的影响。
|
||||
|
||||
嵌套 Code 调用从不生成自己的卡片。因此,仅仅为了重建这一张不完整卡片而给外层调用生成元数据,还掩盖了每次外层调用只生成一张卡片的预期边界。
|
||||
|
||||
## 决策
|
||||
|
||||
规范的工具注册表流水线负责最终面向模型的外层内容。成功时,`run_code` 输出渲染器先渲染已捕获的日志,然后渲染返回值或显式的无输出标记。运行时失败和执行前策略拒绝由 `ToolRegistry` 归一化为错误内容,过程中不会调用该渲染器。Post-execute 阻断发生在成功渲染之后,并把结果替换为错误内容;其他 post-execute 策略与输出落盘决策可以在持久化之前替换内容。
|
||||
|
||||
`run_code` 不提供 `presentResult`。既有的通用结果回退机制会保留待完成的程序标题,并渲染原始的最终 `tool/result.content`;这一持久、可回放且经过 post-policy 处理的投影是卡片中结果内容的唯一来源。宿主 API 代理因此不提供单独的结果视图,而不会在 `event.data.content` 与 `view.view.content` 中重复序列化同一内容。冗余的仅含日志的 `presentationMeta` 投影继续保持移除状态。
|
||||
|
||||
嵌套分发保持不变。带有 `exec.parent` 标记的调用会发出 `tool/code-dispatch` 事件(携带完整渲染内容),但不会生成与 `tool/call` 或 `tool/result` 对应的界面卡片,因此一次外层 `run_code` 调用仍然只会生成一张卡片。
|
||||
|
||||
## 测试
|
||||
|
||||
工具单元测试通过规范注册表覆盖仅有日志、仅有结果、日志与结果并存、无输出、结果落盘和失败的结果,然后固定持久内容以及结果展示器不存在这一事实。宿主 mux 回归测试使用仅有调用的展示器,证明结果帧恰好携带一次原始内容,且不含视图。这些案例证明陈旧元数据无法替换最终内容,同时不会让宿主重复该内容。
|
||||
|
||||
无密钥的 ACP(Agent Client Protocol)后端快照与 TUI Code Mode 快照会执行一个外层程序:程序进行两次嵌套 bash 调用,记录 `captured output`,并返回 `CODE_ONE+CODE_TWO`。ACP 持久化日志固定完整结果;TUI 界面只显示一张完成态外层卡片,其中包含这两行内容,且没有嵌套卡片。
|
||||
|
||||
## 备选方案
|
||||
|
||||
**把返回值追加到日志元数据:**不予采纳。元数据会与渲染器重复,并且需要为每一种 JSON 根另行维护稳定的格式化契约;post-policy 内容替换或输出落盘预览仍然会被遗漏。
|
||||
|
||||
**把展示元数据与 `result.content` 合并:**不予采纳。渲染内容已经包含日志;合并会造成重复,还需要依赖脆弱的去重逻辑。
|
||||
|
||||
**通过通用结果展示器转发 `result.content`:**不予采纳。持久事件已经携带该内容,UI 消费方也已有通用的原始内容回退机制。宿主 mux 会在事件旁序列化工具拥有的结果视图,因此转发仅仅是为了重建该回退机制,却会在一个帧中重复渲染内容;仅默认 worker 在渲染前允许 64 MiB 的可变载荷预算。
|
||||
|
||||
**为每次嵌套分发创建一张卡片:**不予采纳。中间值有意只存在于执行期间,永远不面向模型。多张卡片会暴露实现轨迹,而不是模型与用户调用的单次 Code Mode 操作。
|
||||
|
||||
## 影响
|
||||
|
||||
TUI 与 JSON-RPC/Web 通过通用结果回退机制显示与模型接收及回放持久化相同的完整内容,其中包括 post-policy 输出落盘预览。宿主 API 保留待完成的程序标题,同时不在单独的视图负载中重复原始结果。新的 `run_code` 结果不再携带可选的日志元数据,但无需提升会话格式版本:现有记录仍然有效,因为展示逻辑会读取其中持久化的渲染内容。
|
||||
@@ -1,6 +0,0 @@
|
||||
# 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-22-collapsed-sidebar-control-rail.md: 940fcabf126941cc0e411b01c337e45831e442aa
|
||||
2026-07-22-collapsed-sidebar-control-rail.zh.md: 70ace36fafcb28aa714000262e31c8555d394854
|
||||
@@ -1,29 +0,0 @@
|
||||
# Agent Note: A collapsed sidebar retains its control rail
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-22-collapsed-sidebar-control-rail.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The sidebar close action persisted a zero width preference, and the layout mapped that preference to a zero-width grid track. The only sidebar toggle and the settings entry both lived inside that clipped track, so closing the sidebar removed every visible recovery control. Reloading preserved the closed preference and reproduced the lockout.
|
||||
|
||||
## Decision
|
||||
|
||||
The layout maps a closed sidebar (persisted width `0`) to the fixed `SIDEBAR_COLLAPSED` width of 56px: a 24px icon column between the sidebar's 16px horizontal paddings. The sidebar track is fixed-width in the solver — open or collapsed it never concedes to viewport pressure (only details shrinks, then auto-closes) — and the rail retains its right border while the stored expanded width remains untouched.
|
||||
|
||||
`AppFrame` marks the sidebar collapsed from the persisted width preference rather than from the resolved track width, removes the resize handle while collapsed, and passes `collapsed` to the sidebar slot as owner props from the render site. Collapse and expand animate: the frame transitions `grid-template-columns` (and the remaining handle its `left`) on the deepsuite sider curve — `--ds-ease-in-out` over `--ds-transition-duration-slow`, both supplied by ui-theme's base sheet; transitions pause during drags and under `prefers-reduced-motion`.
|
||||
|
||||
`SidebarRoot` reads the owner `collapsed` prop and transitions as a slide + crossfade: the expanded content freezes at its width (inline style) and fades out in place over 150ms while the sliding grid column clips it — nothing reflows mid-slide. At settle the wide-only content (brand, labels, input, session tree) unmounts — dropping the sessions subscription and leaving the rendered and accessibility trees — and the control rows snap to the rail (open toggle, new session, new workspace, search, the same top-down order as their expanded rows) fading in as the slide ends. Each rail control keeps its expanded counterpart's behavior (the search icon expands the sidebar and focuses the search box after the slide), carries a tooltip, and the toggle rests as the whale mark with the panel icon on hover. The search query lives with the root and survives the round trip.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Render an expand button over the center column** — rejected because it recovers only the toggle, not the persistent settings area, and splits sidebar chrome across two package owners.
|
||||
- **Keep a zero-width grid track and let the rail overflow it** — rejected because the rail would overlap the center column and leave hit testing and responsive geometry disconnected from the grid.
|
||||
- **Keep the complete sidebar tree mounted and hide it with clipping** — rejected because hidden controls remain in the semantic tree and continue subscribing and rendering even though only two controls belong in the collapsed state.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A collapsed sidebar reserves 56px instead of yielding the entire width to the center column. Expanding restores the persisted width and drag behavior.
|
||||
- The settings entry remains visible but retains its existing placeholder behavior; this change does not introduce an account or settings screen.
|
||||
- Layout solver tests pin the compact width, sidebar component tests pin the visible controls, and the keyless real-bundle web smoke test pins collapse and recovery through the assembled client.
|
||||
@@ -1,29 +0,0 @@
|
||||
# Agent Note: 侧边栏折叠后保留控制栏
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-22-collapsed-sidebar-control-rail.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
侧边栏关闭操作会持久化宽度偏好 `0`,布局再将该偏好映射为宽度为零的网格轨道。侧边栏唯一的开关与设置入口都位于这个被裁切的轨道内,因此关闭侧边栏会移除所有可见的恢复控件。页面重新加载时仍会读取关闭偏好,从而再次陷入无法恢复的状态。
|
||||
|
||||
## 决策
|
||||
|
||||
布局将关闭的侧边栏(持久化宽度为 `0`)映射为固定的 `SIDEBAR_COLLAPSED` 宽度 56px:在侧边栏两侧各 16px 的水平内边距之间放置一列 24px 的图标控件。侧边栏轨道在求解器中是定宽的——无论展开还是折叠都不向视口压力让步(只有 details 会收缩、继而自动关闭);控制栏保留右侧边框,已存储的展开宽度保持不变。
|
||||
|
||||
`AppFrame` 根据持久化的宽度偏好标记侧边栏是否折叠,而不是根据求解后的轨道宽度来判断;折叠时移除尺寸调整手柄,并在渲染点把 `collapsed` 作为 owner props 传给侧边栏插槽。折叠与展开带动画:frame 对 `grid-template-columns`(以及余下手柄的 `left`)应用 deepsuite 侧栏曲线过渡——`--ds-ease-in-out` 配 `--ds-transition-duration-slow`,两个变量由 ui-theme 的 base 表提供;拖拽期间和 `prefers-reduced-motion` 下过渡暂停。
|
||||
|
||||
`SidebarRoot` 读取 owner 的 `collapsed` 属性,过渡是滑动 + 交叉淡变:展开内容以内联样式冻结在原宽度、150ms 原地淡出,滑动中的网格列裁切它——滑动途中不发生任何重排。settle 时宽态专属内容(品牌标识、文字标签、输入框、会话树)卸载——随之退订会话列表并离开渲染树与可访问性树——控件行落位到控制栏(打开开关、新建会话、新建工作区、搜索,自上而下与展开态各行顺序一致),随滑动结束淡入。每个控制栏控件保持与展开态对应控件一致的行为(搜索图标展开侧边栏并在滑动结束后聚焦搜索框)并带 tooltip;开关静止时显示鲸鱼标,悬停切换为面板图标。搜索关键词由根组件持有,折叠往返后保留。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **在中心列上方渲染展开按钮**:不予采纳,因为这只能恢复开关,无法保留常驻设置区域,同时还会让侧边栏 UI 由两个包(package)分别持有。
|
||||
- **保留宽度为零的网格轨道,让控制栏溢出显示**:不予采纳,因为控制栏会与中心列重叠,还会使命中测试和响应式几何关系脱离网格布局。
|
||||
- **保持完整侧边栏树挂载,并通过裁切将其隐藏**:不予采纳,因为隐藏控件仍留在语义树中,而且会继续订阅和渲染,尽管折叠状态下只需要两个控件。
|
||||
|
||||
## 后果
|
||||
|
||||
- 折叠的侧边栏占用 56px,而不是把全部宽度让给中心列。展开时恢复持久化宽度与拖动行为。
|
||||
- 设置入口持续可见,但保留既有占位行为;本次改动不提供账户或设置页面。
|
||||
- 布局求解器测试固定紧凑宽度,侧边栏组件测试固定可见控件,基于真实构建产物的无密钥 Web 冒烟测试则通过组装后的客户端固定折叠与恢复行为。
|
||||
@@ -1,6 +0,0 @@
|
||||
# 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-demo-web-builds-client-bundles.md: a7d21987d4544246fd3c53864cedfc86279e9440
|
||||
2026-07-23-demo-web-builds-client-bundles.zh.md: f10184642b0c7869378802d3040ebf4dbe67d4e0
|
||||
@@ -1,27 +0,0 @@
|
||||
# Agent Note: demo:web builds the client plugin bundles
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-23-demo-web-builds-client-bundles.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`dsh web` serves each web-client plugin's bundle from `GET /plugins/<id>/client.js`, resolving the path from the package's `exports["./client"]` (`lib/client.js`). Those bundles are produced only by the root `pnpm run build` (`tsc -b` then the per-package `tsdown.client.ts` configs); the Vite `build:web` step builds the frontend shell alone. `demo:web` and the README's Web UI instructions ran only `build:web`, so on a checkout without a prior full build every plugin bundle 404s, the client loader marks every plugin failed, and the boot screen shows "Failed to load plugins". The frontend shell built fine, hiding the missing artifact behind a runtime browser failure.
|
||||
|
||||
## Decision
|
||||
|
||||
`demo:web` runs `npm run build` before `npm run build:web`, so the plugin `lib/client.js` bundles exist before `dsh web` serves them. The README's Web UI section runs `pnpm run build && pnpm run build:web` for the installed `~/.dsh/source` checkout, which the installer never builds.
|
||||
|
||||
## Verification
|
||||
|
||||
After the full build, all eight `/plugins/<id>/client.js` endpoints return 200 and a headless Chromium load of `http://127.0.0.1:3080` renders the shell with no "Failed to load plugins" state.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Build the bundles inside `dsh web` at startup.** The app runs from source via tsx and owns no build step; folding an artifact build into the server boot crosses the source/artifact separation and slows every launch.
|
||||
|
||||
**Widen the tsdown root config to emit client bundles from `pnpm run build:web`.** `build:web` is the Vite frontend build; the client bundles are a separate tsdown pass over `lib/types`. Merging the two conflates the shell build with the package build and still leaves the root `build` as the only producer.
|
||||
|
||||
## Consequences
|
||||
|
||||
`demo:web` now pays the full `tsc -b && tsdown` cost on every invocation instead of only the Vite build. That is the price of a runnable web demo from a clean tree; a caller who already built can invoke `dsh web` directly.
|
||||
@@ -1,27 +0,0 @@
|
||||
# Agent Note: demo:web 构建客户端插件的打包产物
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-23-demo-web-builds-client-bundles.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
`dsh web` 通过 `GET /plugins/<id>/client.js` 提供每个 web 客户端插件的打包产物,其路径由包的 `exports["./client"]`(`lib/client.js`)解析得到。这些打包产物只由根目录的 `pnpm run build`(先 `tsc -b`,再执行各包的 `tsdown.client.ts` 配置)生成;Vite 的 `build:web` 步骤只构建前端外壳。`demo:web` 与 README 的 Web UI 说明只运行了 `build:web`,因此在未预先完整构建的检出上,每个插件的打包产物都返回 404,客户端 loader 将所有插件标记为失败,启动界面显示 "Failed to load plugins"。前端外壳能正常构建,把缺失的产物掩藏在浏览器运行时的失败背后。
|
||||
|
||||
## Decision
|
||||
|
||||
`demo:web` 在 `npm run build:web` 之前先运行 `npm run build`,使插件的 `lib/client.js` 打包产物在 `dsh web` 提供它们之前已经存在。README 的 Web UI 小节针对已安装的 `~/.dsh/source` 检出运行 `pnpm run build && pnpm run build:web`,因为安装器从不构建它。
|
||||
|
||||
## Verification
|
||||
|
||||
完整构建后,全部八个 `/plugins/<id>/client.js` 端点均返回 200,无头 Chromium 加载 `http://127.0.0.1:3080` 能渲染出外壳,不再出现 "Failed to load plugins" 状态。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**在 `dsh web` 启动时构建打包产物。** 该应用通过 tsx 从源码运行,本身没有构建步骤;把产物构建塞进服务器启动流程会越过源码与产物的分离,并拖慢每次启动。
|
||||
|
||||
**扩大 tsdown 根配置,使 `pnpm run build:web` 也产出客户端打包产物。** `build:web` 是 Vite 前端构建;客户端打包产物是对 `lib/types` 的另一趟独立 tsdown 处理。把两者合并会混淆外壳构建与包构建,而且根目录的 `build` 仍是唯一的产出者。
|
||||
|
||||
## Consequences
|
||||
|
||||
`demo:web` 现在每次调用都要付出完整的 `tsc -b && tsdown` 代价,而不再只是 Vite 构建。这是从干净的代码树运行 web 演示所要付出的代价;已经完成构建的调用方可以直接调用 `dsh web`。
|
||||
@@ -1,6 +0,0 @@
|
||||
# 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-thinking-row-disclosure-target.md: f698c3cb0b73bf5c65b5d4b5b3f29de3080e0af6
|
||||
2026-07-23-thinking-row-disclosure-target.zh.md: 0fba5c1d8f7beec7300dcd51e118a08d57d0e74f
|
||||
@@ -1,29 +0,0 @@
|
||||
# Agent Note: Thinking rows use one disclosure target
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-23-thinking-row-disclosure-target.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A collapsed reasoning entry presents `Think` and its one-line reasoning summary as one visual row, but an icon-only disclosure control leaves both visible labels inert. Applying title expansion to every tool row would instead break the generic tool-row contract, where the row opens details and only the leading control expands arguments.
|
||||
|
||||
## Decision
|
||||
|
||||
`ToolRow` exposes the opt-in `expandOnRowClick` policy. `ThinkRow` enables it so the title and reasoning summary form one accessible disclosure target; pointer clicks, Enter, and Space toggle the same component-local expanded state. Tool rows that do not opt in retain row-to-details selection and leading-control argument expansion.
|
||||
|
||||
## Verification
|
||||
|
||||
The component spec pins both Think click targets and the unchanged generic tool-row handoff. The keyless browser fixture loads the real sidebar and conversation bundles, opens an authored reasoning session, clicks the summary and title, and checks the disclosure state and expanded body.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Expand every tool row from its title.** Generic tool rows use row clicks for details selection, so sharing this behavior would conflate two controls.
|
||||
|
||||
**Keep icon-only disclosure.** The smallest hit target remains disconnected from the labels that describe the hidden content.
|
||||
|
||||
**Render separate title and summary buttons.** Two controls for one expanded state add duplicate focus stops and ambiguous semantics.
|
||||
|
||||
## Consequences
|
||||
|
||||
Thinking rows gain a larger pointer target and keyboard disclosure semantics without changing other tool interactions. The generic row component carries one optional policy because disclosure ownership differs between reasoning and tool calls.
|
||||
@@ -1,29 +0,0 @@
|
||||
# Agent Note: thinking 行使用单一展开目标
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-23-thinking-row-disclosure-target.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
折叠的推理(reasoning)条目在同一视觉行中呈现 `Think` 和单行推理摘要,但仅图标可展开会让两个可见标签都无法交互。若让所有工具行均可通过标题展开,又会破坏通用工具行的契约:整行负责打开详情,只有前导控件负责展开参数。
|
||||
|
||||
## 决策
|
||||
|
||||
`ToolRow` 提供显式启用的 `expandOnRowClick` 策略。`ThinkRow` 启用该策略,让标题和推理摘要组成单一且无障碍的展开目标;鼠标点击、Enter 和 Space 都切换同一个组件本地展开状态。未启用该策略的工具行仍由整行完成详情选择,由前导控件展开参数。
|
||||
|
||||
## 验证
|
||||
|
||||
组件测试固定两个 Think 点击目标以及未改变的通用工具行交接行为。无密钥浏览器 fixture(测试前置数据)加载真实的侧边栏与会话 bundle,打开包含推理内容的既定会话,点击摘要与标题,并检查展开状态和展开后的正文。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**让每个工具行都可通过标题展开。** 通用工具行将整行点击用于详情选择,共享这一行为会混淆两个控件。
|
||||
|
||||
**保留仅图标展开。** 最小的点击目标仍与描述隐藏内容的标签脱节。
|
||||
|
||||
**把标题和摘要分别渲染为按钮。** 两个控件共享一个展开状态,会增加重复的焦点停靠点并产生含糊语义。
|
||||
|
||||
## 后果
|
||||
|
||||
thinking 行获得更大的鼠标点击目标和键盘展开语义,同时不改变其他工具交互。通用行组件承担一个可选策略,因为推理与工具调用的展开所有权不同。
|
||||
@@ -1,6 +0,0 @@
|
||||
# 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-26-intent-draft-same-tick-echo.md: 1a4fdb48c0434bd37d7771dddb640720e1b610e6
|
||||
2026-07-26-intent-draft-same-tick-echo.zh.md: 9ecdf7154f5014de242021f99d2e51959c2a3169
|
||||
@@ -1,27 +0,0 @@
|
||||
# Agent Note: Intent draft echoes in the same tick
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-26-intent-draft-same-tick-echo.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The hero composer ("Let's start building") is a controlled textarea whose value is the frontend Session Intent's retained prompt, read from the sessions **list** snapshot (`EmptyState` binds `intent.prompt` via `useSessions`). Typing routed through `SessionManager.updateIntent → Session.updatePendingPrompt`, which flushes the **Session's own** notifier synchronously — but the list snapshot the composer actually renders from only heard about the change through the intent watch subscription in `startIntent`, which calls `markDirty()`, a microtask-deferred flush.
|
||||
|
||||
A deferred echo violates the controlled-input contract documented on the Notifier (see the [web client architecture note](../architecture/2026-07-19-gui-web-client-architecture.md)): React compares the DOM value against the still-stale snapshot during the same tick as `onChange` and rolls the textarea back. With plain typing this shows as caret jumps; with an IME it corrupts input — every composition update gets rolled back and re-applied against a stale value, so typing Pinyin "nihao" commits fragments like "nnini hni hani hao你好". The resident composer (`ConversationRoot`) was not affected: its draft lives in the chat store (sync flush) or comes from `updateSessionPrompt`, which reads the Session snapshot directly rather than the list projection.
|
||||
|
||||
## Decision
|
||||
|
||||
`SessionManager.updateIntent` calls `this.notifier.notifyNow()` after `updatePendingPrompt`, flushing the list snapshot in the same tick as the change event. This matches the Notifier's channel rule: a direct echo of a user gesture whose controlled input renders from this snapshot uses `notifyNow`; the intent watch keeps `markDirty` for every other (async) intent transition.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Change the intent watch callback in `startIntent` to `notifyNow`.** Wrong channel for that seam: the watch also fires on frame-driven Session changes (publication, send phases), and the architecture note bans `notifyNow` for frame-driven sources because it collapses batching.
|
||||
|
||||
**Have `EmptyState` read the prompt from the Session snapshot instead of the list.** Restructures the slot contract (EmptyState is deliberately bound to the standard `useSessions` feed and has no session scope yet — the frontend Session is page-local) for no gain over flushing the projection it already reads.
|
||||
|
||||
**Suppress the rollback in `InputBar` with local uncontrolled state.** Hides the symptom, forfeits the single-source-of-truth draft (the retained prompt must survive workspace retargeting and send/retry), and leaves every other list-snapshot-controlled input exposed.
|
||||
|
||||
## Consequences
|
||||
|
||||
Typing in the hero composer, IME composition included, echoes synchronously. `updateIntent` on a no-intent state stays a no-op with no notification. The web workspace-flow snapshot's composer helper now asserts the same-tick echo instead of waiting for it, so a regression to a deferred echo fails the keyless snapshot gate; a runtime unit test pins the same contract at the manager seam.
|
||||
@@ -1,27 +0,0 @@
|
||||
# Agent Note: Intent draft echoes in the same tick
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-26-intent-draft-same-tick-echo.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
hero composer(「Let's start building」)是一个受控(controlled)的 textarea,它的值取自前端 Session Intent 保留下来的提示词,读自会话**列表**快照(`EmptyState` 通过 `useSessions` 绑定 `intent.prompt`)。输入经由 `SessionManager.updateIntent → Session.updatePendingPrompt`,后者会同步刷新 **Session 自身的** notifier——但 composer 实际渲染所依据的那份列表快照,只能通过 `startIntent` 中的 intent watch 订阅得知这次变更,而该订阅调用的是 `markDirty()`,即一次延迟到微任务的刷新。
|
||||
|
||||
延迟的回显违反了 Notifier 上所记录的受控输入契约(见 [web 客户端架构笔记](../architecture/2026-07-19-gui-web-client-architecture.md)):React 在与 `onChange` 相同的 tick 内,把 DOM 值与仍然陈旧的快照相比对,随后把 textarea 回滚。普通输入时,这表现为光标跳动;使用输入法(IME)时,它会损坏输入——每一次 composition 更新都会被回滚,并针对陈旧的值重新应用,因此输入拼音「nihao」会提交出类似「nnini hni hani hao你好」这样的片段。resident composer(`ConversationRoot`)不受影响:它的草稿存放在 chat store 中(同步刷新),或来自 `updateSessionPrompt`,后者直接读取 Session 快照,而不是列表投影。
|
||||
|
||||
## Decision
|
||||
|
||||
`SessionManager.updateIntent` 在 `updatePendingPrompt` 之后调用 `this.notifier.notifyNow()`,从而在与变更事件相同的 tick 内刷新列表快照。这符合 Notifier 的通道规则:当某个用户手势的受控输入正是从该快照渲染时,对它的直接回显使用 `notifyNow`;而 intent watch 对其余所有(异步的)intent 状态转换仍保留 `markDirty`。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**把 `startIntent` 中的 intent watch 回调改为 `notifyNow`。** 对那个 seam 而言是错误的通道:该 watch 也会在帧驱动的 Session 变更(发布、发送阶段)时触发,而架构笔记禁止对帧驱动的来源使用 `notifyNow`,因为那会瓦解批处理。
|
||||
|
||||
**让 `EmptyState` 从 Session 快照而非列表读取提示词。** 这会重构槽位契约(EmptyState 有意绑定到标准的 `useSessions` 数据源,且尚无 session 作用域——前端 Session 是页面本地的),相比刷新它本就读取的那份投影并无收益。
|
||||
|
||||
**在 `InputBar` 中用本地的非受控状态抑制回滚。** 这只是掩盖症状,放弃了单一真源的草稿(保留下来的提示词必须在工作区重定向以及发送/重试后依然存在),并让其余每一个由列表快照控制的输入都暴露在同一问题之下。
|
||||
|
||||
## Consequences
|
||||
|
||||
在 hero composer 中输入(包括输入法 composition 在内)会同步回显。在无 intent 的状态上调用 `updateIntent` 仍是一次空操作,不发出任何通知。web workspace-flow 快照的 composer 辅助函数现在断言的是同一 tick 内的回显,而不是等待它,因此一旦回退成延迟回显,就会让无密钥快照门禁失败;一个运行时单元测试在 manager 这一 seam 处钉住了同一份契约。
|
||||
Reference in New Issue
Block a user