Merge remote-tracking branch 'origin/master' into feat/web-cards-toolrow
# Conflicts: # packages/client/connection/src/client/fixture.ts # packages/client/ui-conversation/README.i18n.yaml # packages/client/ui-conversation/src/client/apply.ts # packages/client/ui-conversation/src/client/chat/GenericToolCard.tsx # packages/client/ui-conversation/src/client/skeleton/DetailsPanel.tsx # packages/client/ui-conversation/src/client/toolviews/file-mutation-row.tsx # packages/client/ui-conversation/src/client/toolviews/search-row.tsx # packages/client/ui-conversation/tests/chat-apply.spec.tsx # packages/client/ui-primitives/src/index.ts
This commit is contained in:
@@ -0,0 +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 .agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md
|
||||
2026-07-29-projected-token-usage-and-request-context.md: 1e2c5ff067928620dee3d0937c247bec245e34f2
|
||||
2026-07-29-projected-token-usage-and-request-context.zh.md: 811d92e134b1df0fc6725e6c8d38b37efb57b3aa
|
||||
@@ -0,0 +1,59 @@
|
||||
# Agent Note: Projected token usage and context occupancy
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-29-projected-token-usage-and-request-context.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The Web stats line derived token totals from the currently loaded conversation nodes. That window is paged, so scrolling changed the totals, and compaction replaces visible content without preserving the billing behind it. Durable provider billing needs a source that survives both.
|
||||
|
||||
Context occupancy needs a numerator and a denominator that no existing surface carried to the browser: the prompt size of the latest request, and the capacity of the route it used.
|
||||
|
||||
## Decision
|
||||
|
||||
Both values are ordinary durable session-projection state. `@deepseek-ai/dsh-token-meter` registers two units when `ctx.sessionProjections` is present.
|
||||
|
||||
`tokenUsage` folds the complete durable log into uncached input, output, cache-read, and cache-write buckets. An `assistant/chunk` usage sample survives a later failed request; an `assistant/message` usage value for the same `(turn, step)` replaces the earlier sample instead of double-counting it. Reasoning stays an output subdivision. Compaction and surface replacement do not erase earlier billing.
|
||||
|
||||
`contextPressure` carries optional `pressureTokens` — the newest provider-reported prompt size, summing uncached input plus cache reads and writes, excluding output — and optional `contextWindow` from the newest `request/context` record. Neither field is synthesized before its source exists.
|
||||
|
||||
`request/context` is a new log-only session event recording registration-bound metadata for the route a request resolved to. AgentLoop appends it inside the step beside `request/header`, from the context metadata `prepareCall()` now returns alongside the resolved config — the same registration-bound lookup that already validated reasoning, so no second resolve happens. It is skipped when provider, model, and capacity all match the previous record. A route whose adapter advertises no capacity is recorded with `contextWindow` absent, clearing an older route's denominator.
|
||||
|
||||
Capacity deliberately stays out of `EpochHeader`. That type is the reconstruction contract — what a request was built from — and `headerEquals` compares it field-wise to decide whether a snapshot is a real `change`. Capacity is adapter metadata describing a route, so placing it there would let a capacity change masquerade as a request-envelope change and would drag it into the loop's reconstruction invariant.
|
||||
|
||||
Both units ride the standard projection lifecycle: history tail baselines, `session/projection` live frames, higher-seq-wins client storage, JSON checkpoints, cache recovery, and unit unload. There is no token-specific history field, mux frame, projector, revision counter, or client fence.
|
||||
|
||||
The Web `StatsLine` reads both through the standard `useProjection` seat. Window nodes still supply turn and step counts plus LLM and tool wall times — those answer "what is on screen" and are correctly window-scoped. Durable token and context groups remain when compaction leaves no visible assistant step. Cache writes count in billed input and in the cache-hit denominator. A deployment without token-meter drops the token groups; occupancy stays hidden until both pressure and capacity are known.
|
||||
|
||||
## Context occupancy is approximate, and that is the decision
|
||||
|
||||
`pressureTokens` and `contextWindow` are independent last-wins fields, not one atomic observation. Switching models pairs a fresh capacity with the previous route's pressure until the next request reports usage, and the numerator describes the last request rather than the surface as it currently stands.
|
||||
|
||||
This was accepted deliberately. An occupancy percentage is a user-facing reference figure: nothing in the harness makes decisions from it, and compaction reads `measure()` directly instead. The TUI status line has always computed occupancy this way, dividing a `measure()` total by a capacity resolved separately for the selected model — so an atomic variant here would have been the outlier, not the norm.
|
||||
|
||||
Reviewers should not treat the non-atomicity as a defect awaiting a fix. A consumer that genuinely needs an exact same-boundary figure should call `ctx.tokenMeter.measure()` at its own request boundary, where both values are available together, rather than read this projection.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**An atomic request-boundary snapshot delivered as a transient mux frame (implemented, then rejected).** An earlier revision of this branch emitted `session/model-request`: one non-replayable frame carrying `contextTokens` and `contextWindow` measured at the same `agent/model-request` boundary. Being the only non-replayable class on the mux stream is what broke it. Host and mux are independent SSE streams with no cross-stream ordering, so a request emitted before a removal could arrive after `host/session-removed` and revive a dead session's telemetry, while a legitimate request for a new lifecycle reusing the same id could be fenced by a late removal. `session/subscribed` is not lifecycle proof — it says a queue began subscribing to an id, not that a new in-memory session replaced an older one — and `lastSeq` is a durable watermark two lifecycles can share. A correct fix required a monotonic lifecycle generation on the frame, on subscription, and on removal, plus a client watermark comparison.
|
||||
|
||||
That cost bought a worse display: occupancy went blank after every reconnect and never moved while a conversation grew. It also made ApiProxy a measurement site calling the O(surface) `measure()` on every request, and expressed reconnect state through a synthetic `cancelled` open error the UI had to special-case.
|
||||
|
||||
**Fold the loaded node window in React.** Cannot survive pagination or compaction, and makes a presentation package reconstruct log semantics.
|
||||
|
||||
**Publish usage only with final assistant messages.** A request that reports a usage chunk and then fails would lose its billing.
|
||||
|
||||
**Resolve capacity inside token-meter.** The package documents itself as independent of model routing and is otherwise a pure reader that never appends to the log. AgentLoop already holds the resolved metadata where the header is written.
|
||||
|
||||
**Extend the `session.models` RPC with capacity.** The handler already resolves and discards it, so the field is nearly free — but `StatsLine` lives in `ui-conversation` while the model directory lives in `ui-model`, and `ui-conversation` cannot depend on `ui-model`. Delivering it would have required either a second dock entry splitting one text row across two plugins, or a cross-plugin store write.
|
||||
|
||||
**Add a context circle beside the model selector.** That placement suggests selected-model state. The stats line carries the figure without a duplicate UI or data path.
|
||||
|
||||
## Consequences
|
||||
|
||||
Token totals stay stable across pagination, compaction, replay, restart, and reconnect, because they are ordinary durable projection state recovered through the generic paths. The cross-stream reordering race is gone by construction rather than fenced.
|
||||
|
||||
Occupancy is approximate in the ways documented above. It is available immediately after restore or reconnect, since both fields are durable, at the cost of describing the last recorded request rather than an exact current boundary.
|
||||
|
||||
Each session log gains one small `request/context` record per route or advertised-capacity change. The token-meter projection is the canonical owner of durable session-projection usage semantics; the TUI retains its live per-step map because it does not mount the generic projection seam, and the standalone browser fixture mirrors the unit. ApiProxy carries no token-specific code, owns no per-session metrics cache, and performs no measurement. The browser keeps two generic projection values and no connection-local telemetry, and streaming text deltas still do not force the stats line to recompute.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Agent Note: token 用量投影与上下文占用率
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-29-projected-token-usage-and-request-context.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Web 统计行原先从当前已加载的会话节点推导 token 总量。该窗口是分页的,因此滚动会改变总量;压缩(compaction)又会替换可见内容,而不保留其背后的计费用量。持久的提供方计费用量需要一个能同时经受这两者的数据源。
|
||||
|
||||
上下文占用率需要一个分子和一个分母,而这两者都不曾由任何既有接口送达浏览器:最新一个请求的提示词规模,以及该请求所用路由的容量。
|
||||
|
||||
## 决策
|
||||
|
||||
这两个值都是普通的持久会话投影状态。当 `ctx.sessionProjections` 存在时,`@deepseek-ai/dsh-token-meter` 会注册两个单元。
|
||||
|
||||
`tokenUsage` 将完整持久日志归并为未缓存输入、输出、缓存读取和缓存写入四类计数项。即使后续请求失败,`assistant/chunk` 用量样本仍会保留;同一 `(turn, step)` 的 `assistant/message` 用量值会替换先前样本,不会重复计数。推理(reasoning)仍是输出的细分项。压缩和表层替换不会抹除先前的计费用量。
|
||||
|
||||
`contextPressure` 携带可选的 `pressureTokens`(提供方报告的最新提示词规模,为未缓存输入加缓存读取与写入之和,不含输出),以及来自最新一条 `request/context` 记录的可选 `contextWindow`。在各自来源出现前,两个字段都不会被合成。
|
||||
|
||||
`request/context` 是新增的仅入日志会话事件,记录请求所解析到的路由的、绑定注册项的元数据。AgentLoop 在步骤内紧随 `request/header` 追加它,数据取自 `prepareCall()` 现在与已解析配置一并返回的上下文元数据:正是那次已经校验过推理的、绑定注册项的查询,因此不会发生第二次解析。当提供方、模型和容量都与上一条记录相同时会跳过。适配器不公布容量的路由会以缺失 `contextWindow` 的形式记录,从而清除较早路由的分母。
|
||||
|
||||
容量刻意不进入 `EpochHeader`。该类型是重建契约,即请求由什么构建而成,而 `headerEquals` 会逐字段比较它,以判定某个快照是否真的是一次 `change`。容量是描述路由的适配器元数据,把它放进去会让容量变化伪装成请求封装的变化,还会把它拖进 AgentLoop 的重建不变式。
|
||||
|
||||
两个单元都沿用标准投影生命周期:历史尾页基线、`session/projection` 实时帧、seq 高者胜的客户端存储、JSON 检查点、缓存恢复和单元卸载。系统没有任何 token 专用的历史字段、mux 帧、投影器、修订计数器或客户端栅栏。
|
||||
|
||||
Web `StatsLine` 通过标准 `useProjection` 席位读取两者。窗口内节点仍提供轮次和步骤计数,以及 LLM(大语言模型)与工具的墙钟时间:它们回答的是「屏幕上有什么」,按窗口作用域正是正确的。压缩使可见 assistant 步骤归零后,持久 token 与上下文分组仍会保留。缓存写入会计入计费输入和缓存命中率分母。未部署 token-meter 时会去掉 token 分组;只有压力与容量都已知时才显示占用率。
|
||||
|
||||
## 上下文占用率是近似值,而这正是决策本身
|
||||
|
||||
`pressureTokens` 与 `contextWindow` 是两个各自后者胜的独立字段,不是一次原子观测。切换模型时,新容量会与上一路由的压力配对,直到下一个请求报告用量为止;分子描述的是最后一个请求,而不是此刻的表层。
|
||||
|
||||
这是刻意接受的结果。占用率百分比是面向用户的参考数字:harness 中没有任何环节依据它做决策,压缩改为直接读取 `measure()`。TUI 状态行一直以这种方式计算占用率,即用 `measure()` 总量除以为所选模型单独解析出的容量;因此在这里做成原子版本才是异类,而不是常态。
|
||||
|
||||
评审人不应把这种非原子性当作待修的缺陷。确实需要同一边界精确数字的消费方,应在自己的请求边界调用 `ctx.tokenMeter.measure()`,那里两个值同时可得,而不是读取该投影。
|
||||
|
||||
## 备选方案
|
||||
|
||||
**以临时 mux 帧交付请求边界上的原子快照(已实现,随后否决)。** 本分支较早的一个修订版会发出 `session/model-request`:一个不可回放的帧,携带在同一个 `agent/model-request` 边界测得的 `contextTokens` 与 `contextWindow`。真正让它失效的,是它成了 mux 流上唯一的不可回放类别。Host 流与 mux 流是两条独立的 SSE(Server-Sent Events)流,彼此之间没有顺序保证:在移除之前发出的请求可能在 `host/session-removed` 之后才到达,让一个已死会话的遥测数据复活;而复用同一 id 的新生命周期的合法请求,又可能被一条迟到的移除拦下。`session/subscribed` 不能证明生命周期:它只说明某个队列开始订阅某个 id,而不说明新的内存会话替换了较早的会话;`lastSeq` 则是两个生命周期可以共用的持久水位线。正确的修法需要在帧上、订阅上和移除上都带一个单调递增的生命周期代次,再加上一次客户端水位线比较。
|
||||
|
||||
这份代价换来的是更差的显示:占用率在每次重连后变为空白,而且会话增长期间从不移动。它还把 ApiProxy 变成一个测量点,每个请求都要调用 O(surface) 的 `measure()`,并通过一个 UI 必须特殊处理的、连接打开时的合成 `cancelled` 错误来表达重连状态。
|
||||
|
||||
**在 React 中归并已加载的节点窗口。** 无法跨分页或压缩保留数据,还会迫使展示包(package)重建日志语义。
|
||||
|
||||
**仅随最终 assistant 消息发布用量。** 如果请求报告一个用量分片后失败,就会丢失自己的计费用量。
|
||||
|
||||
**在 token-meter 内部解析容量。** 该包自述与模型路由无关,且在其他方面是一个从不向日志追加内容的纯读取方。AgentLoop 在写入请求头的位置已经持有已解析的元数据。
|
||||
|
||||
**为 `session.models` RPC 增加容量字段。** 其处理器已经解析出容量又将其丢弃,因此这个字段几乎是免费的;但 `StatsLine` 位于 `ui-conversation`,模型目录位于 `ui-model`,而 `ui-conversation` 不能依赖 `ui-model`。要送达它,就得增加第二个 dock 条目、把一行文本拆到两个插件里,或者做一次跨插件的 store 写入。
|
||||
|
||||
**在模型选择器旁增加上下文圆环。** 该位置会让人以为这是所选模型的状态。统计行可以承载该数字,无需引入重复的 UI 或数据路径。
|
||||
|
||||
## 后果
|
||||
|
||||
token 总量在分页、压缩、回放、重启和重连期间保持稳定,因为它们是通过通用路径恢复的普通持久投影状态。跨流重排序竞态从构造上就不存在,而不是被栅栏挡住。
|
||||
|
||||
占用率在上文记录的意义上是近似值。由于两个字段都是持久的,它在恢复或重连后立即可用;代价是它描述的是最后一条已记录的请求,而不是精确的当前边界。
|
||||
|
||||
每个会话日志会为每次路由或已公布容量变化增加一条小型 `request/context` 记录。token-meter 投影是持久会话投影用量语义的正典所有方;TUI 未挂载通用投影 seam,因此保留自己的实时逐步骤 map,而独立浏览器 fixture 会镜像该单元。ApiProxy 不携带任何 token 专用代码,不拥有逐会话指标缓存,也不执行测量。浏览器只保留两个通用投影值,不保留连接本地的遥测数据;流式文本增量仍不会迫使统计行重新计算。
|
||||
@@ -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-20-dsh-cli-personal-config.md
|
||||
2026-07-20-dsh-cli-personal-config.md: 3331e36c86002d91fb272868268707fed014d01a
|
||||
2026-07-20-dsh-cli-personal-config.zh.md: 172d84b075ed7ecc127c317b47e30581db67f89a
|
||||
2026-07-20-dsh-cli-personal-config.md: 259c3865a9edcbc77949a9fe401af9a77e1e32c4
|
||||
2026-07-20-dsh-cli-personal-config.zh.md: 8f7c15c3c683cc855c6e3b704bfde8f87d4009d2
|
||||
|
||||
@@ -39,9 +39,9 @@ Hot-reload interplay: the include re-applies its `patches` on every config re-re
|
||||
## Consequences
|
||||
|
||||
- `dsh` from any directory (and `pnpm run demo:tui`) boots the personal provider/model with zero repo changes; verified end-to-end against a personal Anthropic proxy with Opus 4.8, including a bash tool round trip.
|
||||
- Because an id-targeted patch replaces the whole `config`, a personal override restates the base fields it keeps and can drift when the base entry changes shape; the loader's entry-not-found/name-mismatch warnings are the only diagnostics.
|
||||
- Because an id-targeted patch replaces the whole `config`, a personal override restates the base fields it keeps and can drift when the base entry changes shape; the loader's entry-not-found/name-mismatch warnings and [`dsh --dump-config`](2026-07-30-dsh-dump-config.md) (which prints the composed tree those patches produce) are the diagnostics.
|
||||
- Personal patches resolve ids against the booted file's own tree, so nested-include overlays (Code Mode) are not personalized; live-run parity for those leaves is deferred.
|
||||
- `dsh-app-boot` depends on `js-yaml` (plus a load-only copy of the include's `!!js` YAML type) and, like `apps/cli`, on `@deepseek-ai/dsh-paths` for `resolveDshHome`.
|
||||
- `dsh-app-boot` depends on `js-yaml` and imports the include's `!!js` YAML dialect (`entryListSchema`) directly, and, like `apps/cli`, depends on `@deepseek-ai/dsh-paths` for `resolveDshHome`.
|
||||
- When PR #443 lands, `apps/cli/src/bin.ts`'s dispatch chain and `apps/cli/package.json`'s dependency list conflict textually; both resolve as unions (their `web`/`-p` branches plus our default-TUI branch).
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -39,9 +39,9 @@ PTY 冒烟测试的启动器把 `$DSH_HOME` 隔离到每个测试自己的目录
|
||||
## Consequences
|
||||
|
||||
- 在任意目录运行 `dsh`(以及 `pnpm run demo:tui`)即可零仓库改动地使用个人提供方/模型;已针对个人 Anthropic 代理与 Opus 4.8 端到端验证,包括一次 bash 工具往返。
|
||||
- 由于按 id 定位的补丁替换整个 `config`,个人覆盖必须复述它保留的基础字段,并可能随基础配置项形态变化而漂移;loader 的「配置项未找到/名称不匹配」警告是仅有的诊断。
|
||||
- 由于按 id 定位的补丁替换整个 `config`,个人覆盖必须复述它保留的基础字段,并可能随基础配置项形态变化而漂移;诊断手段是 loader 的「配置项未找到/名称不匹配」警告和 [`dsh --dump-config`](2026-07-30-dsh-dump-config.md)(打印这些补丁合成出的配置树)。
|
||||
- 个人补丁只在被启动文件自身的树里解析 id,因此嵌套 include 的 overlay(Code Mode)不会被个性化;这些叶子的实际运行等价性暂缓。
|
||||
- `dsh-app-boot` 依赖 `js-yaml`(外加一份只用于加载的 include `!!js` YAML 类型副本),并与 `apps/cli` 一样依赖 `@deepseek-ai/dsh-paths` 以获取 `resolveDshHome`。
|
||||
- `dsh-app-boot` 依赖 `js-yaml`,并直接导入 include 的 `!!js` YAML 方言(`entryListSchema`);与 `apps/cli` 一样依赖 `@deepseek-ai/dsh-paths` 以获取 `resolveDshHome`。
|
||||
- PR #443 落地时,`apps/cli/src/bin.ts` 的分发链与 `apps/cli/package.json` 的依赖列表会产生文本冲突;两者都按并集解决(他们的 `web`/`-p` 分支加上我们的默认 TUI 分支)。
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -0,0 +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 .agents/notes/implemented/feature/2026-07-30-dsh-dump-config.md
|
||||
2026-07-30-dsh-dump-config.md: bc6504541c7868bad019a1bcd9f551435109e4c6
|
||||
2026-07-30-dsh-dump-config.zh.md: 5e173305a6cd03de3db4c763f26eeda6fba68ec7
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: dsh --dump-config prints the composed config tree
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-30-dsh-dump-config.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The booted tree is a composition the user never sees: the shipped base, a surface overlay, and the `--config` or personal `~/.dsh/config.yaml` overlay apply as sibling patch lists where each id-targeted patch replaces the row's whole `config` and an unmatched id only warns. Debugging a misbehaving personal overlay (a restated field dropped, a row id typo, a patch applying to the wrong surface) required mentally replaying the patch algorithm across three files. There was no way to see the effective tree or to diff it against the shipped defaults.
|
||||
|
||||
## Decision
|
||||
|
||||
`dsh --dump-config` and `dsh web --dump-config` print the composed entry list — base, surface overlay, then the `--config` or personal overlay, exactly the layers that surface's boot assembles — as YAML on stdout and exit without booting. `dsh --dump-default-config` / `dsh web --dump-default-config` stop at the surface overlay, so diffing the two outputs shows precisely what the user layer changes.
|
||||
|
||||
The dump cannot drift from what boots because it shares the mounting code: the vendored include exports its patch algorithm as the pure `applyEntryPatches(data, patches, warn)` (the private `applyPatches` method now delegates to it) and its `!!js` YAML dialect as `entryListSchema`; `dsh-app-boot`'s `renderConfigDump()` composes labeled layers and renders through both, and `apps/cli/src/dump-config.ts` is a thin surface-selection wrapper. `!!js` expressions print verbatim and unevaluated — the dump shows composition, not one process's environment — and a patch whose target row is absent goes to stderr with its layer label, mirroring the Loader's boot-time warning. Launcher-owned boot-context values (session identity, web CLI-flag patches, the frontend dist path) are per-invocation facts outside the config tree and do not appear. The dump flags reject boot-only flags (`-p`, `--resume`, `--config-replace`) and each other, and `--dump-default-config` takes no `--config`.
|
||||
|
||||
Each run of same-provenance rows is preceded by a `# ==` comment naming the file that contributed the rows and the layers that patched them (`# == base.cordis.yml, patched by tui.cordis.yml`), so the output shows which section comes from which file while remaining one loadable YAML document. Composition is one flattened `applyEntryPatches` call over all layers — boot's exact call shape, so even patch-visibility corner cases (a later layer targeting a group child that a plain `config` replacement introduced, invisible to the single-pass id index) compose identically; applying one call per layer would rebuild the index between layers and print a tree boot never mounts. Provenance is derived from single-call prefix snapshots (base + layers 1..k) diffed positionally: the patch algorithm only rewrites rows in place or appends, so a top-level index identifies one row across snapshots, and a layer counts as having patched a row when adding it changed that row (config replacement, disable, group insert). Patch lists are cloned per snapshot because `applyEntryPatches` pushes `insert` rows by reference from the patch list.
|
||||
|
||||
`dsh-app-boot` previously duplicated the include's `!!js` YAML type for patch parsing; it now imports `entryListSchema`, so the dialect has one owner.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Boot the tree and dump `ctx.loader.entries()`.** Rejected: booting evaluates `!!js` expressions (leaking one machine's environment into the printed config), starts adapters and sessions as side effects, requires a TTY-independent teardown path, and is slow. The dump is for debugging composition, which is a pure function of the files.
|
||||
|
||||
**Reimplement the patch merge in the CLI.** Rejected: a second implementation of `applyPatches` would silently drift from the vendored include — the exact failure mode the feature exists to debug. Exporting the include's own algorithm costs one logged vendor modification and guarantees identity.
|
||||
|
||||
**A `/dump-config` TUI command instead of flags.** Rejected as the only form: the primary use is a piped `dsh --dump-config | diff - <(dsh --dump-default-config)` style workflow, which needs a boot-free non-TTY surface. A TUI command can be added later over the same `renderConfigDump`.
|
||||
|
||||
## Consequences
|
||||
|
||||
Config debugging becomes one command instead of mental patch replay, and support can ask for `--dump-config` output. The vendored include carries one more logged local modification (the `applyEntryPatches`/`entryListSchema` exports; behavior-preserving for mounting) to re-apply on upstream sync. Provenance tracking re-composes one prefix snapshot per layer and diffs rows by JSON stringify, so the dump does extra work proportional to layers² × rows; that cost lives only in the boot-free dump path. `renderConfigDump` is unit-tested for layer ordering, verbatim `!!js` round-tripping, provenance separators and grouping, labeled unmatched-patch warnings, and loud read/parse/shape failures; the built-bin e2e drives all four flag forms through `lib/bin.js` including the personal-overlay layer, its provenance label, and its stderr warning.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: dsh --dump-config 打印合成后的配置树
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-30-dsh-dump-config.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
启动的配置树是一份用户从未见过的合成结果:已交付的基础配置、界面覆盖层,以及 `--config` 或个人 `~/.dsh/config.yaml` 覆盖层作为同级补丁列表依次应用,其中每个按 id 定向的补丁替换目标行的整个 `config`,未匹配的 id 只产生警告。调试一个行为异常的个人覆盖层(漏掉需要重述的字段、行 id 拼错、补丁应用到了错误的界面)需要在脑中跨三个文件重放补丁算法。既没有办法看到生效的树,也没有办法把它与已交付的默认值做 diff。
|
||||
|
||||
## Decision
|
||||
|
||||
`dsh --dump-config` 和 `dsh web --dump-config` 把合成后的条目列表——基础配置、界面覆盖层、再叠 `--config` 或个人覆盖层,恰好是该界面启动时组装的那些层——以 YAML 打印到 stdout 后退出,不启动任何东西。`dsh --dump-default-config` / `dsh web --dump-default-config` 止步于界面覆盖层,因此对两份输出做 diff 就能精确看出用户层改了什么。
|
||||
|
||||
dump 不可能与实际启动漂移,因为它复用挂载代码:vendored include 把补丁算法导出为纯函数 `applyEntryPatches(data, patches, warn)`(私有的 `applyPatches` 方法现在委托给它),并把 `!!js` YAML 方言导出为 `entryListSchema`;`dsh-app-boot` 的 `renderConfigDump()` 通过这两者对带标签的层完成合成与渲染,`apps/cli/src/dump-config.ts` 只是选择界面的薄封装。`!!js` 表达式原样打印、不求值——dump 展示的是合成结果,不是某个进程的环境——目标行不存在的补丁会连同其层标签报到 stderr,与 Loader 启动时的警告一致。由启动器持有的启动上下文值(会话身份、web 的 CLI 标志补丁、前端 dist 路径)是每次调用的事实,位于配置树之外,不会出现。dump 标志拒绝仅用于启动的标志(`-p`、`--resume`、`--config-replace`)且两个 dump 标志互斥,`--dump-default-config` 不接受 `--config`。
|
||||
|
||||
每段来源相同的连续行之前都有一条 `# ==` 注释,标明贡献这些行的文件以及修补过它们的层(`# == base.cordis.yml, patched by tui.cordis.yml`),因此输出既展示每一节来自哪个文件,又仍是一份可加载的 YAML 文档。合成是对所有层展平后的一次 `applyEntryPatches` 调用——与启动的调用形状完全一致,因此即便是补丁可见性的边角情况(后一层定位到前一层通过普通 `config` 替换引入的组内子项,而单遍 id 索引看不到它)也与启动合成完全相同;若按层各调用一次,会在层与层之间重建索引,打印出一棵启动从不挂载的树。来源从单次调用的前缀快照(基础 + 第 1..k 层)按位置 diff 得出:补丁算法只会原地改写行或在末尾追加,因此顶层索引在各快照之间标识同一行;加入某层后该行发生变化(替换 config、禁用、组内插入)即视为该层修补了这一行。每个快照都会克隆补丁列表,因为 `applyEntryPatches` 会把 `insert` 行按引用从补丁列表推入结果。
|
||||
|
||||
`dsh-app-boot` 之前为解析补丁复制了 include 的 `!!js` YAML 类型;现在改为导入 `entryListSchema`,方言只有一个归属者。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**启动整棵树后 dump `ctx.loader.entries()`。** 拒绝:启动会求值 `!!js` 表达式(把某台机器的环境泄漏进打印的配置)、以副作用启动适配器和会话、需要独立于 TTY 的拆卸路径,而且慢。dump 是用来调试合成的,而合成是那些文件的纯函数。
|
||||
|
||||
**在 CLI 里重新实现补丁合并。** 拒绝:`applyPatches` 的第二个实现会与 vendored include 悄然漂移——这恰恰是该功能要调试的失败模式。导出 include 自己的算法只花费一条记录在案的 vendor 修改,却保证了同一性。
|
||||
|
||||
**用 `/dump-config` TUI 命令代替标志。** 作为唯一形式被拒绝:主要用法是 `dsh --dump-config | diff - <(dsh --dump-default-config)` 这类管道工作流,需要免启动、非 TTY 的界面。之后可以在同一个 `renderConfigDump` 之上再加 TUI 命令。
|
||||
|
||||
## Consequences
|
||||
|
||||
配置调试从脑中重放补丁变成一条命令,支持工作也可以直接索要 `--dump-config` 输出。vendored include 多出一条记录在案的本地修改(导出 `applyEntryPatches`/`entryListSchema`;对挂载行为无影响),上游同步时需重新应用。来源追踪为每层重新合成一次前缀快照并按 JSON stringify 对行做 diff,因此 dump 有与层数²×行数成正比的额外开销;该开销只存在于免启动的 dump 路径。`renderConfigDump` 的单元测试覆盖层叠顺序、`!!js` 原样往返、来源分隔与分组、带标签的未匹配补丁警告,以及读取/解析/形状失败的大声报错;built-bin e2e 通过 `lib/bin.js` 驱动全部四种标志形式,包括个人覆盖层、其来源标签及其 stderr 警告。
|
||||
@@ -0,0 +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 .agents/notes/implemented/simplification/2026-07-31-drop-user-message-edit-stub.md
|
||||
2026-07-31-drop-user-message-edit-stub.md: 5a34be1dd285f4aced9e6cfb2e324b20ec734bed
|
||||
2026-07-31-drop-user-message-edit-stub.zh.md: 768cb4618b4a85f61e6348f8c6fb6a93d686e7d3
|
||||
@@ -0,0 +1,27 @@
|
||||
# Agent Note: Drop the user-message edit stub
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-31-drop-user-message-edit-stub.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The user bubble's IconActions row carried an edit button beside copy and branch. Nothing backed it: the control had no click handler, no client mutation, and no host operation for resending an edited message. A user who found it saw an affordance the product cannot honor.
|
||||
|
||||
## Decision
|
||||
|
||||
`MessageIconActions` renders clock / copy / branch only, and its `edit` prop is gone with the button; `MessageItem` no longer passes it. The user bubble and the assistant chrome now differ only by clock side. The package README records the missing capability under Known Limitations, and the web message-actions golden pins the row without the control.
|
||||
|
||||
The common locale keeps its generic `edit` term, which is shared vocabulary rather than this component's copy.
|
||||
|
||||
Reintroduce the control together with the capability: a client mutation that edits a settled user message and the host behavior that decides what the edited message does to the turn that already consumed it.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Disable the button with a tooltip.** A visible-but-dead control still advertises editing and costs the same explaining; removal is the honest state.
|
||||
|
||||
**Wire it to the queue editor.** The queue edits a message that has not been sent. A settled user message is already in the transcript and in the model's context, so reusing that editor would silently mean something else.
|
||||
|
||||
## Consequences
|
||||
|
||||
Web offers no way to correct a sent message; branching from the message is the nearest available gesture. Reintroduction is a UI-only change once the mutation exists, since the row composes its actions from props.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Agent Note: 移除 user 消息的编辑存根
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-31-drop-user-message-edit-stub.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
user 气泡的 IconActions 行在复制和分支旁边还有一个编辑按钮,但其背后什么都没有:该控件没有点击处理、没有 client 侧变更,也没有 host 侧重新发送已编辑消息的操作。用户找到它时,看到的是一个产品无法兑现的可供性。
|
||||
|
||||
## 决策
|
||||
|
||||
`MessageIconActions` 只渲染时钟/复制/分支,其 `edit` prop 随按钮一并删除;`MessageItem` 不再传入该 prop。现在 user 气泡与 assistant chrome 只在时钟位置上不同。包 README 在 Known Limitations 中记录这项缺失的能力,web 的 message-actions 金样固定了不含该控件的动作行。
|
||||
|
||||
公共 locale 保留通用的 `edit` 词条:它是共享词汇,而非本组件的文案。
|
||||
|
||||
重新引入该控件时要与能力一起落地:既需要编辑已定稿 user 消息的 client 变更,也需要 host 侧决定这条编辑后的消息对已经消费过它的轮次意味着什么。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**把按钮置灰并加提示。** 一个可见但无效的控件仍在宣告可以编辑,解释成本相同;直接移除才是诚实的状态。
|
||||
|
||||
**接到队列编辑器上。** 队列编辑的是尚未发送的消息。已定稿的 user 消息已经进入转录和模型上下文,复用该编辑器会让同一个动作悄悄变成另一件事。
|
||||
|
||||
## 后果
|
||||
|
||||
Web 没有任何途径修正已发送的消息;从该消息分支是最接近的现有手势。由于动作行的内容完全由 props 组合而来,能力就绪后重新引入只是一次纯 UI 改动。
|
||||
Reference in New Issue
Block a user