From da41677049e0530a696ac335fb359e5d4117a4b8 Mon Sep 17 00:00:00 2001 From: Hypatia May Date: Thu, 30 Jul 2026 17:10:45 +0800 Subject: [PATCH] fix(web): address transcript review follow-ups --- ...6-07-19-gui-web-client-architecture.i18n.yaml | 4 ++-- .../2026-07-19-gui-web-client-architecture.md | 2 +- .../2026-07-19-gui-web-client-architecture.zh.md | 4 ++-- .../2026-07-20-gui-testing-system.i18n.yaml | 4 ++-- .../process/2026-07-20-gui-testing-system.md | 2 +- .../process/2026-07-20-gui-testing-system.zh.md | 2 +- apps/web/tests/seeded-history.e2e.ts | 2 +- docs/core-data-structures/compaction.i18n.yaml | 4 ++-- docs/core-data-structures/compaction.md | 2 +- docs/core-data-structures/compaction.zh.md | 2 +- packages/client/runtime/README.i18n.yaml | 4 ++-- packages/client/runtime/README.md | 2 +- packages/client/runtime/README.zh.md | 2 +- packages/client/runtime/package.json | 2 +- .../src/client/sessions/transcript-adapter.ts | 16 ++++++---------- .../runtime/tests/compact-checkpoint-pin.spec.ts | 13 ++++++------- .../runtime/tests/transcript-adapter.spec.ts | 12 +----------- packages/client/test-runtime/README.i18n.yaml | 4 ++-- packages/client/test-runtime/README.md | 2 +- packages/client/test-runtime/README.zh.md | 2 +- packages/client/ui-conversation/README.i18n.yaml | 4 ++-- packages/client/ui-conversation/README.md | 3 +++ packages/client/ui-conversation/README.zh.md | 3 +++ pnpm-lock.yaml | 6 +++--- 24 files changed, 47 insertions(+), 56 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 6e1b952682..4530d5ee57 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: abe28b00638b4f01c7a50efe9ece5148e959a5ee -2026-07-19-gui-web-client-architecture.zh.md: 560015191fd8d99e2983a19d0b23aeb4dedff837 +2026-07-19-gui-web-client-architecture.md: 63b6f5795c3d49f25cd964cf04a0c9d41a667bfb +2026-07-19-gui-web-client-architecture.zh.md: 2d57c12ebae38aafa4e606da95af954990761b3c 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 abe28b0063..63b6f5795c 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 @@ -69,7 +69,7 @@ Notifier 微任务合批 ──► ConversationSnapshot 缓存 ──uSES── ``` - **Session** (session.ts): lazily built, resident — once created it keeps eating frames in the background, so switching away and back renders instantly. Operations: `prompt`/`cancel` (RPC passthrough; failures land in the snapshot's `promptError`), `open` (pull the tail history page, idempotent), `loadOlder` (upward paging, reentry-guarded), `resync` (reconnect = clear the window and rerun open). Subscription: `subscribe`/`getSnapshot` (always the cached reference) — `implements ObservableSnapshot`, with `useSelector = bindSnapshotSelector(this)` attached at construction, so a Session is directly a uSES source. Frame dispatch is one switch: `session/event` frames dedup by seq (the only dedup key), buffer while open is in flight, otherwise append + incremental projection; open/stitch merges the live buffer by seq and backfills once if `subscribed.lastSeq` outruns the window tail. -- **ConversationSnapshot** (conversation.ts): the immutable snapshot contract — `nodes` (the human transcript, log-ordered), `partial`, `runningCalls`, `pending`, `running`, `removed`, `openState`, `hasMore`, `promptError` and kin. **Reference discipline** (the premise of memo and uSES): the top-level object is fresh on every change; the nodes array is rebuilt but element references come from the cache; unchanged substructures reuse the previous snapshot's references. +- **ConversationSnapshot** (conversation.ts): the immutable snapshot contract — `nodes` (the human transcript, log-ordered), `partial`, `runningCalls`, `pending`, `running`, `removed`, `openState`, `hasMore`, `promptError` and kin. **Reference discipline** (the premise of memo and uSES): the top-level object is fresh on every change; an unchanged nodes projection keeps the same array reference, while a changed flow returns a new array that reuses unchanged element references; unchanged substructures reuse the previous snapshot's references. - **SessionManager** (manager.ts): instance cluster + frame entry + the session list. sessionId-bearing frames go only to existing instances (a mux broadcast must not instantiate every session); approval/question `requested` frames are the exception — they never land in history, so they buffer in `pendingBuffers` and replay on instantiation. - **Notifier** (notifier.ts): two channels chosen by change source. `markDirty()` (default; frame-driven changes always) batches per microtask — N changes, one notification, one re-render; the flush rebuilds the snapshot cache before notifying. `notifyNow()` (only direct echoes of user gestures) rebuilds and notifies in the same tick — controlled inputs roll the DOM back and jump the caret if their echo defers to a microtask. Frame-driven code using notifyNow collapses batching back to per-frame renders; banned. - **TranscriptAdapter / PartialAccumulator**: the transcript is the append-origin surface projected in log order (`isAppendSurfaceEvent` from `@deepseek-ai/dsh-session/surface`) plus one marker per landed compaction checkpoint — never the model surface, which shadows replaced ranges and would erase conversation the reader already saw. Node order is seq-monotonic by construction, so there is no core `seq === index` assertion to satisfy and no degradation branch. Chunks contribute no node (O(1) skip): the accumulator folds StreamChunks into `AssistantBlock[]`, a delta swapping only that block's reference, and the finalizing message discards the accumulator in the same batch (no flicker on promotion). Cost model: one chunk = one string concatenation + a dirty mark; an unsubscribed Session under a frame storm costs only the mark. 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 560015191f..2d57c12eba 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 @@ -68,8 +68,8 @@ Session.handleMuxEnvelope ──► events 窗口(seq 连续升序) Notifier 微任务合批 ──► ConversationSnapshot 缓存 ──uSES──► 组件 ``` -- **Session**(session.ts):懒建、常驻——建成后在后台持续吃帧,切走切回秒显。操作面:`prompt`/`cancel`(RPC 透传;失败落进快照的 `promptError`)、`open`(拉尾页 history,幂等)、`loadOlder`(向上翻页,防重入)、`resync`(重连 = 清窗口重跑 open)。订阅面:`subscribe`/`getSnapshot`(恒返缓存引用)——`implements ObservableSnapshot`,构造时挂 `useSelector = bindSnapshotSelector(this)`,Session 本身就是 uSES 源。帧分发是一个 switch:`session/event` 帧按 seq 去重(唯一去重键),open 在途时缓冲,否则追加 + 增量 fold;open/缝合按 seq 合并 live 缓冲并去重,`subscribed.lastSeq` 超出窗口尾则回补一次。 -- **ConversationSnapshot**(conversation.ts):不可变快照契约——`nodes`(人类对话记录,日志序)、`partial`、`runningCalls`、`pending`、`running`、`removed`、`openState`、`hasMore`、`promptError` 等。**引用纪律**(memo 与 uSES 的前提):顶层对象每变必新;nodes 数组重建但元素引用来自缓存;未变的子结构复用上一快照的引用。 +- **Session**(session.ts):懒建、常驻——建成后在后台持续吃帧,切走切回秒显。操作面:`prompt`/`cancel`(RPC 透传;失败落进快照的 `promptError`)、`open`(拉尾页 history,幂等)、`loadOlder`(向上翻页,防重入)、`resync`(重连 = 清窗口重跑 open)。订阅面:`subscribe`/`getSnapshot`(恒返缓存引用)——`implements ObservableSnapshot`,构造时挂 `useSelector = bindSnapshotSelector(this)`,Session 本身就是 uSES 源。帧分发是一个 switch:`session/event` 帧按 seq 去重(唯一去重键),open 在途时缓冲,否则追加 + 增量投影;open/缝合按 seq 合并 live 缓冲并去重,`subscribed.lastSeq` 超出窗口尾则回补一次。 +- **ConversationSnapshot**(conversation.ts):不可变快照契约——`nodes`(人类对话记录,日志序)、`partial`、`runningCalls`、`pending`、`running`、`removed`、`openState`、`hasMore`、`promptError` 等。**引用纪律**(memo 与 uSES 的前提):顶层对象每变必新;未变化的 nodes 投影保持同一数组引用,消息流变化时返回新数组并复用未变化的元素引用;未变的子结构复用上一快照的引用。 - **SessionManager**(manager.ts):实例簇 + 帧总入口 + 会话列表。带 sessionId 的帧只投已存在实例(mux 广播不得把每个会话都实例化);例外是审批/问答 `requested` 帧——它们不落 history、open 无法回补,故缓冲进 `pendingBuffers`,实例化时回放。 - **Notifier**(notifier.ts):两条通知通道,按变更来源取用。`markDirty()`(默认;帧驱动一律用它)按微任务合批——N 次变更、一次通知、一次重渲染;flush 先重建快照缓存再通知。`notifyNow()`(仅用户手势的直接回响)同 tick 重建并通知——受控输入的回响若延到微任务,DOM 会回滚、光标跳尾。帧驱动代码用 notifyNow 会让合批塌回逐帧渲染;禁。 - **TranscriptAdapter / PartialAccumulator**:对话记录是按日志顺序投影的 append 来源 surface(`@deepseek-ai/dsh-session/surface` 的 `isAppendSurfaceEvent`),外加每次落地的压缩检查点一个标记——绝不用模型 surface,后者遮蔽被替换的范围,会抹掉读者已经看过的对话。节点顺序天然按 seq 单调,因此既无核心 `seq === index` 断言需要满足,也没有降级分支。分片不贡献任何节点(O(1) 跳过):累积器把 StreamChunk 折叠成 `AssistantBlock[]`,一次增量只换该块引用;定稿消息到达即在同一批内弃掉累积器(提升无闪烁)。成本模型:一个分片 = 一次字符串拼接 + 一个脏标记;帧风暴下未订阅的 Session 只花那个标记。 diff --git a/.agents/notes/implemented/process/2026-07-20-gui-testing-system.i18n.yaml b/.agents/notes/implemented/process/2026-07-20-gui-testing-system.i18n.yaml index 05d448c79b..deafb47f70 100644 --- a/.agents/notes/implemented/process/2026-07-20-gui-testing-system.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-20-gui-testing-system.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/process/2026-07-20-gui-testing-system.md -2026-07-20-gui-testing-system.md: 8c6dafb18fc207fc4eac780ba18e108267bc28b1 -2026-07-20-gui-testing-system.zh.md: 9a0de4bfa8fa2f8de55beef53bedde51649c5d9c +2026-07-20-gui-testing-system.md: 4a1600bbef7ef795677a446228fcc279a4b53f39 +2026-07-20-gui-testing-system.zh.md: 2aa5d7f66783c69964cabf7eb18a018b54528a33 diff --git a/.agents/notes/implemented/process/2026-07-20-gui-testing-system.md b/.agents/notes/implemented/process/2026-07-20-gui-testing-system.md index 8c6dafb18f..4a1600bbef 100644 --- a/.agents/notes/implemented/process/2026-07-20-gui-testing-system.md +++ b/.agents/notes/implemented/process/2026-07-20-gui-testing-system.md @@ -22,7 +22,7 @@ Cut along the architecture's natural test seams into three tiers, bottom-up: | 2 Object-layer orchestration | `Session`/`SessionManager`/`ConnectionController` (state machines and timing: stitching / dedup / paging / optimistic draft clearing / pendingBuffers / reconnect / backoff) | **The "event sequence in → snapshot out" golden path**: programmable fakes + deferreds controlling timing + fake timers controlling backoff | `packages/client/{runtime,connection}/tests/` | | 3 Assembled presentation | Built artifacts × the real client loader and plugin composition | App-owned semantic snapshots boot all eight built client plugins under jsdom for deterministic cross-plugin state changes; bare Playwright smoke separately proves the real browser/carrier boundary, with real-host cases self-skipping without a key; the keyless browser e2e lane disables the shipped model-adapter row and replays recorded session fixtures through `dsh-llm-replay` in the real in-process web assembly against conversation aria goldens ([web e2e lane](../testing/2026-07-24-web-gui-browser-e2e-lane.md), [required CI gate](../testing/2026-07-30-web-browser-snapshot-ci-gate.md)) | `apps/web/tests/*.snapshot.ts`, `apps/web/tests/smoke-{fixture,real}.e2e.ts`, `apps/web/tests/{replay-round-trip,seeded-history}.e2e.ts` | -Inter-tier discipline: **each tier tests its own layer, upper tiers never re-test lower ones** — an app semantic snapshot pins only user-visible projection across the assembled plugin boundary, while Playwright smoke proves browser and carrier liveness; wire semantics belong to tier 1 and data semantics to tier 2. Pure-function layers (lineage/partial/notifier/fold-adapter) are tested directly with zero fakes in the same package's tests/ alongside tier 2. +Inter-tier discipline: **each tier tests its own layer, upper tiers never re-test lower ones** — an app semantic snapshot pins only user-visible projection across the assembled plugin boundary, while Playwright smoke proves browser and carrier liveness; wire semantics belong to tier 1 and data semantics to tier 2. Pure-function layers (lineage/partial/notifier/transcript-adapter) are tested directly with zero fakes in the same package's tests/ alongside tier 2. - **Host and client source** are under the repo-wide per-file 100% coverage gate except the narrow browser-grade exclusions annotated in `vitest.config.ts`; component suites use per-file jsdom pragmas and Testing Library without changing Node suites. - **App-owned semantic snapshots** read built client bundles, execute them through the real loader, and drive only deterministic fixture hooks. They own stable visible state such as sidebar labels, breadcrumbs, and `document.title`, not CSS pixels or lower-layer state-machine details. diff --git a/.agents/notes/implemented/process/2026-07-20-gui-testing-system.zh.md b/.agents/notes/implemented/process/2026-07-20-gui-testing-system.zh.md index 9a0de4bfa8..2aa5d7f667 100644 --- a/.agents/notes/implemented/process/2026-07-20-gui-testing-system.zh.md +++ b/.agents/notes/implemented/process/2026-07-20-gui-testing-system.zh.md @@ -22,7 +22,7 @@ GUI 栈需要考虑多种应用形态,同应用形态内的不同运行环境 | 2 对象层编排 | `Session`/`SessionManager`/`ConnectionController`(状态机与时序:缝合/去重/翻页/乐观清稿/pendingBuffers/重连/退避) | **「事件序列进→快照出」黄金路径**:可编程假体 + deferred 控时序 + fake timers 控退避 | `packages/client/{runtime,connection}/tests/` | | 3 组装呈现层 | 构建产物 × 真实 client loader 与插件组合 | 归应用所有的语义快照会在 jsdom 下启动全部 8 个已构建的 client 插件,以固定确定性的跨插件状态变化;独立使用 Playwright 裸库的冒烟测试负责验证真实浏览器/承载层边界,真 host 用例在无密钥时自行跳过;无密钥浏览器 e2e 车道会禁用交付配置中的模型适配器行,并通过 `dsh-llm-replay` 在真实进程内 web 组装中回放录制的会话 fixture,与会话区 aria 期望输出比对([web e2e 车道](../testing/2026-07-24-web-gui-browser-e2e-lane.md)、[必需 CI 门禁](../testing/2026-07-30-web-browser-snapshot-ci-gate.md)) | `apps/web/tests/*.snapshot.ts`、`apps/web/tests/smoke-{fixture,real}.e2e.ts`、`apps/web/tests/{replay-round-trip,seeded-history}.e2e.ts` | -层间纪律:**下层各测各的,上层不重测下层**:应用语义快照只固定组装后插件边界上的用户可见投影,Playwright 冒烟测试负责验证浏览器与承载层是否存活;wire 语义归 1 层,数据语义归 2 层。纯函数层(lineage/partial/notifier/fold-adapter)随 2 层同包 tests/ 零假体直测。 +层间纪律:**下层各测各的,上层不重测下层**:应用语义快照只固定组装后插件边界上的用户可见投影,Playwright 冒烟测试负责验证浏览器与承载层是否存活;wire 语义归 1 层,数据语义归 2 层。纯函数层(lineage/partial/notifier/transcript-adapter)随 2 层同包 tests/ 零假体直测。 - **host 与 client 源码**均纳入全仓 per-file 100% 覆盖率门禁,仅排除 `vitest.config.ts` 中带注释的少量浏览器级例外;组件套件通过逐文件 jsdom pragma 和 Testing Library 运行,不会改变 Node 套件。 - **归应用所有的语义快照**读取已构建的 client bundle,通过真实 loader 执行它们,并且只驱动确定性的 fixture 钩子。它们负责固定侧边栏标签、面包屑和 `document.title` 等稳定可见状态,而不固定 CSS 像素或下层状态机细节。 diff --git a/apps/web/tests/seeded-history.e2e.ts b/apps/web/tests/seeded-history.e2e.ts index 485c2bc05b..fb3b266625 100644 --- a/apps/web/tests/seeded-history.e2e.ts +++ b/apps/web/tests/seeded-history.e2e.ts @@ -246,7 +246,7 @@ describe('web e2e: seeded history renders through cold resume', () => { timeout: 5_000, }).toBe(1) expect(await page.getByText('The exact summary remains available.', { exact: false }).count()).toBeGreaterThan(0) - // Collapse again so the aria golden captured after this case is unaffected. + // Restore the shared page state for any later case. await marker.click() await expect.poll(() => marker.getAttribute('aria-expanded'), { timeout: 5_000 }).toBe('false') }) diff --git a/docs/core-data-structures/compaction.i18n.yaml b/docs/core-data-structures/compaction.i18n.yaml index 89783b5d16..a933e793c4 100644 --- a/docs/core-data-structures/compaction.i18n.yaml +++ b/docs/core-data-structures/compaction.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 docs/core-data-structures/compaction.md -compaction.md: 911b71d00fa4b42e9cdfa67f67d4e9b29e354a4a -compaction.zh.md: 643a116ff2edbbb53d300b4f5ff0ad36d401130b +compaction.md: 3ae4d7e50452b549654b7a7162141a4be505d784 +compaction.zh.md: 448c3aaf298b65ebe88565190c5b3978b975f5f3 diff --git a/docs/core-data-structures/compaction.md b/docs/core-data-structures/compaction.md index 911b71d00f..3ae4d7e504 100644 --- a/docs/core-data-structures/compaction.md +++ b/docs/core-data-structures/compaction.md @@ -60,7 +60,7 @@ Automatic callers state why policy is running; implementations may treat confirm type CompactionTrigger = 'pressure' | 'context-overflow' ``` -`CompactService` exposes `compactIfNeeded(agent, trigger, signal)` for automatic `pressure` or `context-overflow` policy, returning `null` when no safe work exists, and `compactRegion(...)` for an explicit inclusive surface range. Every backend marks its replacement `user/message` with the package-exported `COMPACT_CHECKPOINT_SOURCE`; consumers call `isCompactCheckpointSource()` instead of coupling checkpoint recognition to one backend. Implementations must forward the supplied signal to summarization. The seam owns no pricing API: the singleton [`ctx.tokenMeter`](token-meter.md) directly owns estimation and replay, while `dsh-compact-basic` owns retention, event sequencing, routed summarization calls, and their configuration. +`CompactService` exposes `compactIfNeeded(agent, trigger, signal)` for automatic `pressure` or `context-overflow` policy, returning `null` when no safe work exists, and `compactRegion(...)` for an explicit inclusive surface range. Every backend marks its replacement `user/message` with `COMPACT_CHECKPOINT_SOURCE`; client and wire consumers import that value and `isCompactCheckpointSource()` from the cordis-free `@deepseek-ai/dsh-compact/checkpoint` subpath, while the package root re-exports both for host consumers. Implementations must forward the supplied signal to summarization. The seam owns no pricing API: the singleton [`ctx.tokenMeter`](token-meter.md) directly owns estimation and replay, while `dsh-compact-basic` owns retention, event sequencing, routed summarization calls, and their configuration. Pressure compaction runs at serial `agent/step` before request derivation. Once pressure or canonical overflow qualifies, compact-basic invokes optional [`ctx.toolResultPrune`](../../packages/compact/compact-tool-result-prune/README.md) before range selection, remeasures through `ctx.tokenMeter`, and can advance the surface without a summary. Failed-request recovery runs through `agent/request-error` after the failed step closes and returns a retry action only when the surface replacement generation advances, even if later summary work throws after pruning; cancellation still wins. Region boundaries preserve tool-call/result pairing but not whole turns, allowing early closed steps of one oversized turn to compact. `dsh-compact-basic` owns thresholds, retained-tail policy, overflow caps, and failure handling. diff --git a/docs/core-data-structures/compaction.zh.md b/docs/core-data-structures/compaction.zh.md index 643a116ff2..448c3aaf29 100644 --- a/docs/core-data-structures/compaction.zh.md +++ b/docs/core-data-structures/compaction.zh.md @@ -60,7 +60,7 @@ interface CompactionResult { type CompactionTrigger = 'pressure' | 'context-overflow' ``` -`CompactService` 暴露 `compactIfNeeded(agent, trigger, signal)` 以执行自动 `pressure` 或 `context-overflow` 策略;没有可安全执行的工作时返回 `null`。它还针对显式、两端均包含的 surface 范围暴露 `compactRegion(...)`。每个后端都使用包导出的 `COMPACT_CHECKPOINT_SOURCE` 标记其替换用的 `user/message`;消费方调用 `isCompactCheckpointSource()`,而不是把检查点识别逻辑耦合到某一个后端。实现必须把传入的 signal 转发给摘要流程。该 seam 不拥有计价 API:单例 [`ctx.tokenMeter`](token-meter.md) 直接拥有估算与回放,而 `dsh-compact-basic` 拥有保留策略、事件排序、按路由执行的摘要调用及其配置。 +`CompactService` 暴露 `compactIfNeeded(agent, trigger, signal)` 以执行自动 `pressure` 或 `context-overflow` 策略;没有可安全执行的工作时返回 `null`。它还针对显式、两端均包含的 surface 范围暴露 `compactRegion(...)`。每个后端都使用 `COMPACT_CHECKPOINT_SOURCE` 标记其替换用的 `user/message`;client 与 wire 消费方从无 cordis 的 `@deepseek-ai/dsh-compact/checkpoint` 子路径导入该值和 `isCompactCheckpointSource()`,包根则为 host 消费方重新导出两者。实现必须把传入的 signal 转发给摘要流程。该 seam 不拥有计价 API:单例 [`ctx.tokenMeter`](token-meter.md) 直接拥有估算与回放,而 `dsh-compact-basic` 拥有保留策略、事件排序、按路由执行的摘要调用及其配置。 压力压缩在串行 `agent/step` 中运行,先于请求推导。一旦压力或规范化溢出满足条件,compact-basic 会在选择范围前调用可选的 [`ctx.toolResultPrune`](../../packages/compact/compact-tool-result-prune/README.md),再通过 `ctx.tokenMeter` 重新测量,并且可以在不生成摘要的情况下推进 surface。失败请求的恢复在失败的步骤关闭后通过 `agent/request-error` 运行;仅当 surface replacement generation 前进时才返回重试动作,即便后续摘要工作在剪枝后抛异常亦如此;取消仍然优先。区域边界保持工具调用/结果配对,但不保持整个轮次,因此一个过大轮次中较早关闭的步骤可以被压缩。`dsh-compact-basic` 拥有阈值、保留尾部策略、溢出上限与失败处理。 diff --git a/packages/client/runtime/README.i18n.yaml b/packages/client/runtime/README.i18n.yaml index 483d34e8d3..3b19ab54c4 100644 --- a/packages/client/runtime/README.i18n.yaml +++ b/packages/client/runtime/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/runtime/README.md -README.md: 51e13d2c68dce3c0c12e04ad0940af8a1e76a4bc -README.zh.md: 9cebc6e7489da65d18d6b308a4991aad06fc1193 +README.md: 63876e2f2c762c5eeff95e065338413017e0a333 +README.zh.md: efa5e841efad1e5ce0f8c3af63eba00bcae1a363 diff --git a/packages/client/runtime/README.md b/packages/client/runtime/README.md index 51e13d2c68..63876e2f2c 100644 --- a/packages/client/runtime/README.md +++ b/packages/client/runtime/README.md @@ -24,7 +24,7 @@ SlotsService gives the renderer separate bare observables for `useSessions` and `ConversationSnapshot.nodes` is the human transcript, not the model surface. `TranscriptAdapter` projects the raw window in log order — every append-origin surface event (`isAppendSurfaceEvent`) at its own log position, plus one `CompactionSummaryNode` marker per landed compaction checkpoint — and never consults surface order. A landed compaction therefore keeps the conversation it shadowed on the model side: the marker reports where the model stopped seeing that history instead of erasing it. Model-only replacement copies stay out: a pruned `tool/result` and a regenerated `assistant/message` rewrite one node for the model and mark no boundary. A checkpoint is a `user/message` carrying the compaction seam's plugin source that **replaced** a surface range; an appending plugin-sourced `user/message` is injected context, not a compaction. The adapter's plugin literal is pinned to the seam's own declaration by a type-only import of the cordis-free [`dsh-compact/checkpoint`](../../compact/compact/README.md) leaf, so renaming it there fails `tsc` here; a **value** import of the package would fail the client purity gate, and the package **root** is unreachable even as a type (it reaches `dsh-session`'s root, whose `Context` merge collides the host `sessions` with this program's). `tests/compact-checkpoint-pin.spec.ts` covers the same drift behaviorally. -Because the projection is log-ordered, the node array is seq-monotonic by construction: log-only `command/run` / `command/done` nodes splice in by seq, `Session` merges interrupted frozen nodes by their fractional seqs, and a window whose checkpoint cites a shadowed range outside it renders the marker with nothing logged. The marker's summary text comes from the checkpoint's `compact/summary` provenance; a window cut that left the provenance outside makes the row non-expandable rather than empty, and a later page that supplies it resolves the text. Performance contract: one append materializes one node, an event that changes no node keeps the previous array reference (a chunk storm costs nothing), and unchanged nodes keep their object identity. +Because the projection is log-ordered, the node array is seq-monotonic by construction: log-only `command/run` / `command/done` nodes splice in by seq, `Session` merges interrupted frozen nodes by their fractional seqs, and a window whose checkpoint cites a shadowed range outside it renders the marker with nothing logged. The marker's summary text comes from the checkpoint's `compact/summary` provenance; a window cut that left the provenance outside makes the row non-expandable rather than empty, and a later page that supplies it resolves the text. Performance contract: one append materializes at most one node and copies the projection only when it adds that node; an event that changes no node keeps the previous array reference (a chunk storm costs nothing), and unchanged nodes keep their object identity. ## Code Mode sub-dispatch index diff --git a/packages/client/runtime/README.zh.md b/packages/client/runtime/README.zh.md index 9cebc6e748..efa5e841ef 100644 --- a/packages/client/runtime/README.zh.md +++ b/packages/client/runtime/README.zh.md @@ -24,7 +24,7 @@ SlotsService 分别为 renderer 提供 `useSessions` 与 `useWorkspaces` 的裸 `ConversationSnapshot.nodes` 是人类对话记录,不是模型 surface。`TranscriptAdapter` 按日志顺序投影原始窗口——每个 append 来源的 surface 事件(`isAppendSurfaceEvent`)落在它自己的日志位置上,外加每次落地的压缩检查点贡献一个 `CompactionSummaryNode` 标记——且从不查询 surface 顺序。于是一次落地的压缩会保留它在模型侧遮蔽掉的对话:标记报告模型从哪里开始看不见那段历史,而不是把它抹掉。仅模型可见的 replacement 副本不进入记录:被裁剪的 `tool/result` 和重新生成的 `assistant/message` 只为模型重写一个节点,不标记任何边界。检查点是携带压缩缝隙插件来源、且**替换**了一段 surface 范围的 `user/message`;一条 append 的插件来源 `user/message` 是注入上下文,不是压缩。适配器的插件字面量通过对无 cordis 的 [`dsh-compact/checkpoint`](../../compact/compact/README.md) 叶子做仅类型导入,钉在压缩缝隙自己的声明上:在那里改名会让此处 `tsc` 失败;而对该包做**值**导入会被客户端纯度门禁拒绝,包的**根**即便作为类型也无法到达(它会到达 `dsh-session` 的根,其 `Context` 合并会让 host 的 `sessions` 与本程序的冲突)。`tests/compact-checkpoint-pin.spec.ts` 从行为侧覆盖同一漂移。 -由于投影按日志顺序,节点数组天然按 seq 单调:仅日志的 `command/run` / `command/done` 节点按 seq 插入,`Session` 按分数 seq 归并被打断的冻结节点,而检查点所引范围落在窗口之外的窗口会渲染出标记且不打印任何日志。标记的摘要文本来自检查点的 `compact/summary` 溯源;窗口切分把溯源留在窗口外时该行不可展开而非空白,后续补上溯源的分页会解析出文本。性能契约:一次追加物化一个节点,不改变任何节点的事件保持上一次的数组引用(分片风暴零成本),未变化的节点保持其对象标识。 +由于投影按日志顺序,节点数组天然按 seq 单调:仅日志的 `command/run` / `command/done` 节点按 seq 插入,`Session` 按分数 seq 归并被打断的冻结节点,而检查点所引范围落在窗口之外的窗口会渲染出标记且不打印任何日志。标记的摘要文本来自检查点的 `compact/summary` 溯源;窗口切分把溯源留在窗口外时该行不可展开而非空白,后续补上溯源的分页会解析出文本。性能契约:一次追加最多物化一个节点,并且仅在加入该节点时复制投影;不改变任何节点的事件保持上一次的数组引用(分片风暴零成本),未变化的节点保持其对象标识。 ## Code Mode 子调用索引 diff --git a/packages/client/runtime/package.json b/packages/client/runtime/package.json index 3868ab8d8c..a6f96b9d47 100644 --- a/packages/client/runtime/package.json +++ b/packages/client/runtime/package.json @@ -32,6 +32,7 @@ "license": "BSD-3-Clause", "dependencies": { "@deepseek-ai/dsh-client-connection": "workspace:^", + "@deepseek-ai/dsh-compact": "workspace:^", "@deepseek-ai/dsh-commands": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-host-apiproxy": "workspace:^", @@ -48,7 +49,6 @@ "cordis": "^4.0.0-rc.7" }, "devDependencies": { - "@deepseek-ai/dsh-compact": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@types/react": "~18.3.1", "cordis": "^4.0.0-rc.7" diff --git a/packages/client/runtime/src/client/sessions/transcript-adapter.ts b/packages/client/runtime/src/client/sessions/transcript-adapter.ts index 4985f263f4..6fae44ede4 100644 --- a/packages/client/runtime/src/client/sessions/transcript-adapter.ts +++ b/packages/client/runtime/src/client/sessions/transcript-adapter.ts @@ -33,8 +33,8 @@ import { toAssistantBlocks } from './conversation.ts' */ const COMPACT_PLUGIN: typeof COMPACT_CHECKPOINT_SOURCE.plugin = 'compact' -/** In-window tool/call index entry (result-card backfill + runningCalls material). */ -export interface CallIndexEntry { +/** In-window tool/call index entry used to materialize result cards. */ +interface CallIndexEntry { name: string argsRaw: string turn: number @@ -194,11 +194,6 @@ export class TranscriptAdapter { private rev = 0 private nodesResult: { rev: number; value: readonly ConversationNode[] } | null = null - /** In-window tool/call index (Session uses it for runningCalls and result-card backfill). */ - get callIndex(): ReadonlyMap { - return this.callIdx - } - /** * Window rebuild (after open/resync/page prepend): re-index the raw window * and re-project the transcript. @@ -230,9 +225,10 @@ export class TranscriptAdapter { /** * Tail append (live session/event): index the event and, when it belongs to - * the transcript, extend the projection by one node — O(1) per append. An - * event that changes no node (a chunk storm) bumps no revision, so nodes() - * keeps returning the same array reference. + * the transcript, extend the projection by one copy-on-write node so a + * published array never mutates. An event that changes no node (a chunk + * storm) bumps no revision, so nodes() keeps returning the same array + * reference. * @param event - the live event (seq = window tail + 1). * @param view - host-computed tool view paired with the event when it is a tool call/result; indexed for card rendering. */ diff --git a/packages/client/runtime/tests/compact-checkpoint-pin.spec.ts b/packages/client/runtime/tests/compact-checkpoint-pin.spec.ts index f7f345d931..ddc6c8adc5 100644 --- a/packages/client/runtime/tests/compact-checkpoint-pin.spec.ts +++ b/packages/client/runtime/tests/compact-checkpoint-pin.spec.ts @@ -5,14 +5,13 @@ * compile time through a type-only import of `dsh-compact/checkpoint`, so * renaming the seam's plugin already fails `tsc`. This spec covers the same * drift from the other side — end to end through the adapter, driving it with a - * checkpoint built from the canonical `COMPACT_CHECKPOINT_SOURCE` **value** and - * checking the seam's own predicate agrees. It runs in the client TEST program, - * which can value-import the package root; a `packages/client/*` package - * program cannot, because that root reaches `dsh-session`'s root and collides - * the host `Context.sessions` merge (`TS2717`). + * checkpoint built from the canonical `COMPACT_CHECKPOINT_SOURCE` value and + * checking the seam's own predicate agrees. Both values come from the + * cordis-free checkpoint leaf, so the client test program never loads the host + * package root or its `Context` merges. */ -import { COMPACT_CHECKPOINT_SOURCE, isCompactCheckpointSource } from '@deepseek-ai/dsh-compact' +import { COMPACT_CHECKPOINT_SOURCE, isCompactCheckpointSource } from '@deepseek-ai/dsh-compact/checkpoint' import { createUserMessage } from '@deepseek-ai/dsh-llm' import { describe, expect, it } from 'vitest' import type { SessionEvent } from '@deepseek-ai/dsh-session/types' @@ -40,7 +39,7 @@ describe('compaction checkpoint recognition', () => { expect(adapter.nodes()).toEqual([{ kind: 'compaction', seq: 1, time: 1_700_000_000_001, summary: null }]) }) - it('agrees with the seam s own predicate on the source it recognizes', () => { + it("agrees with the seam's own predicate on the source it recognizes", () => { // Both sides answer the same question about the same value: if the seam // renames its plugin, this equality is what breaks. const checkpoint = canonicalCheckpoint(1) diff --git a/packages/client/runtime/tests/transcript-adapter.spec.ts b/packages/client/runtime/tests/transcript-adapter.spec.ts index 0c8aa9e12b..cc03d349b8 100644 --- a/packages/client/runtime/tests/transcript-adapter.spec.ts +++ b/packages/client/runtime/tests/transcript-adapter.spec.ts @@ -316,15 +316,7 @@ describe('TranscriptAdapter', () => { expect(adapter.nodes()[0]).toMatchObject({ kind: 'tool-result', isError: true, error: { code: 'boom' } }) }) - it('exposes the in-window call index for runningCalls material', () => { - const adapter = new TranscriptAdapter() - adapter.reset([ev.toolCall(0, 1, 'c9', 'slow', '{}')]) - expect(adapter.callIndex.get('c9')).toMatchObject({ name: 'slow', turn: 1 }) - adapter.append(ev.toolCall(1, 1, 'c10', 'fast', '{}')) - expect(adapter.callIndex.size).toBe(2) - }) - - it('attaches wire views: callView into the call index, resultView onto the node by seq', () => { + it('attaches wire views to the materialized result node', () => { const adapter = new TranscriptAdapter() const callView = { for: 'call' as const, view: { card: 'terminal' as const, command: 'ls' } } const resultView = { for: 'result' as const, view: { card: 'generic' as const, title: '完成' } } @@ -332,7 +324,6 @@ describe('TranscriptAdapter', () => { ev.toolCall(0, 1, 'c1', 'bash', '{"cmd":"ls"}'), ev.toolResult(1, 1, 'c1', 'listing'), ], [callView, resultView] as never) - expect(adapter.callIndex.get('c1')).toMatchObject({ callView: { card: 'terminal' } }) expect(adapter.nodes().find(n => n.kind === 'tool-result')).toMatchObject({ callView: { card: 'terminal' }, resultView: { card: 'generic', title: '完成' }, }) @@ -343,7 +334,6 @@ describe('TranscriptAdapter', () => { adapter.reset(plainTurn(0, 0, 'a', 'b')) // no views argument adapter.append(ev.toolCall(6, 1, 'c2', 'echo', '{}'), { for: 'call', view: { card: 'generic', title: '回声' } } as never) adapter.append(ev.toolResult(7, 1, 'c2', 'ok')) // no view on the result - expect(adapter.callIndex.get('c2')).toMatchObject({ callView: { title: '回声' } }) expect(adapter.nodes().find(n => n.kind === 'tool-result')).toMatchObject({ callView: { title: '回声' }, resultView: null, }) diff --git a/packages/client/test-runtime/README.i18n.yaml b/packages/client/test-runtime/README.i18n.yaml index 73a4705b1b..fe40088b11 100644 --- a/packages/client/test-runtime/README.i18n.yaml +++ b/packages/client/test-runtime/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/test-runtime/README.md -README.md: 883d71224139dc409229fdfb35362d040e810cc7 -README.zh.md: a3daf112940b03b585d44bc5fd1317e43ebd35fe +README.md: dc8ee8cadf5e61af15f04b1b9842af1eb658c031 +README.zh.md: a4c889d8a0291b52c8509403748df6b93567788e diff --git a/packages/client/test-runtime/README.md b/packages/client/test-runtime/README.md index 883d712241..dc8ee8cadf 100644 --- a/packages/client/test-runtime/README.md +++ b/packages/client/test-runtime/README.md @@ -21,4 +21,4 @@ None; this package neither assembles nor sends a provider request. ## Known Limitations and Deferred Work - **Consumed through repository source aliases only.** Specs resolve the package through tsconfig `paths` to `src`; the built `lib/` artifact re-exports `@deepseek-ai/dsh-client-runtime/client`, whose bundle is a browser loader script with no Node ESM exports, so `lib/index.js` is not importable under plain Node. Acceptable while every consumer is an in-repo Vitest suite; a Node-compatible runtime entry is deferred until an out-of-repo consumer exists. -- **Conversation snapshots are fixture data, not replayed history.** `updateSnapshot` writes the snapshot store directly; the wire-to-snapshot computation stays covered by the runtime package's own tests and the replay e2e. A fixture can therefore express states the production fold would never produce. +- **Conversation snapshots are fixture data, not replayed history.** `updateSnapshot` writes the snapshot store directly; the wire-to-snapshot computation stays covered by the runtime package's own tests and the replay e2e. A fixture can therefore express states the production projection would never produce. diff --git a/packages/client/test-runtime/README.zh.md b/packages/client/test-runtime/README.zh.md index a3daf11294..a4c889d8a0 100644 --- a/packages/client/test-runtime/README.zh.md +++ b/packages/client/test-runtime/README.zh.md @@ -21,4 +21,4 @@ ## Known Limitations and Deferred Work - **仅可经仓内源码别名消费。** spec 通过 tsconfig `paths` 解析到 `src`;构建产物 `lib/` 再导出 `@deepseek-ai/dsh-client-runtime/client`,而该 bundle 是无 Node ESM 导出的浏览器 loader 脚本,故 `lib/index.js` 在纯 Node 下不可导入。当前所有消费方都是仓内 Vitest 套件,可接受;Node 兼容的运行时入口待出现仓外消费方再补。 -- **会话快照是 fixture 数据,不是重放历史。** `updateSnapshot` 直写快照 store;wire 到快照的运算仍由 runtime 包自身测试与 replay e2e 把守。因此 fixture 可以表达生产折叠永不产出的状态。 +- **会话快照是 fixture 数据,不是重放历史。** `updateSnapshot` 直写快照 store;wire 到快照的运算仍由 runtime 包自身测试与 replay e2e 把守。因此 fixture 可以表达生产投影永不产出的状态。 diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index 654722b589..d4ea0caf53 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: 3973c14f2b8fe746549bb74af85a7a60a7d66aea -README.zh.md: a6bb15c4cdd53d05bf28147b97d9d64d1c59da2b +README.md: b9a4fd7de61ed8ce01417cc97ebfd80c312d37bf +README.zh.md: 3a79336f3cb40bf5682aabeb6503d71a1ee36746 diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 3973c14f2b..b9a4fd7de6 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -4,6 +4,8 @@ English | [中文](README.zh.md) Conversation domain: skeleton (header/tabs/composer/empty state), chat view (grouped step-summary flow, streaming tail isolation, per-tool row slot with a bash sample registrant and the todo row), composer dock (session stats sticky with the input), input dock (queue rows plus the todo plan strip), minimal details panel, scope-addressed ConversationService. Contract: api-contracts v3 §7 plus the slot terminal design (store seat / props shares). +Compaction renders as one collapsed row at the checkpoint's flow position without replacing the transcript above it. The disclosure renders the checkpoint's `compact/summary` provenance; when that event is outside the loaded window, the row remains visible but non-expandable. The framed checkpoint payload is model-facing and never renders. + The resident conversation shell survives no-session and session transitions. Without a current session it renders a disabled input bar; its root-scoped `conversation.hero.workspace` slot hosts the Workspace picker. Selecting a Workspace connects or reuses its Host-owned blank session and opens that session without replacing the shell. Blank sessions render the same composer body as active sessions, while the InputHub carries drafts across Workspace switches and mirrors them into the session store. In the active phase the session header occupies the top as ordinary column chrome; beneath it a scrollport (`data-conversation-scroll`) holds the flowing views and the sticky composer stack (stats dock + input docks + bar). Wheel over the textarea chains: the capped draft scrolls locally until its edge, then forwards to that host. The view ring IS a slot: the conversation registration declares the `'conversation.view'` list slot (session scope) in its `children` table, ConversationRoot renders the active entry through its renderSlot share (`only: `), and view tabs project from the ring ledger's registration options (`id`/`order`/`label`). The chat view is this package's own ring entry; other plugins (ui-trajectory) contribute tabs through plain `ctx.slots.register` — the former package-local view registry (`registerView`/`ViewEntry`/`ConversationViewMap` and the chrome attachment table) is retired, with per-view chrome dissolved into the view components themselves. @@ -34,6 +36,7 @@ None; this package neither assembles nor sends a provider request. ## Known Limitations and Deferred Work +- **Compaction markers show no scale** — the row does not yet report how many messages or which range the checkpoint replaced. - **Stats-line durations cover the in-window flow only** — LLM and tool wall times fold the snapshot's assistant `timing` and tool call/result pairs, so nodes outside the loaded event window (older history) are not counted. - **Details panel is the minimal form and currently has no entry point** — selected call args/result raw display; the Input/Output/Metadata switch, Prev/Next stepping, and See-in-trajectory deep link are deferred. Tool rows stopped being details-panel click targets and nothing replaced that gesture, so `ChatViewInjected.openDetails` is implemented but uncalled and the panel (including its terminal card) is unreachable in the assembled application; its rendering stays covered by mounting it with a selection directly. - **Assistant per-message paging is a reserved slot** — drawn in the design, not implemented. The finalized content IconActions row (copy / branch / clock) ships under text output only; branch remains a chrome stub. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index a6bb15c4cd..3a79336f3c 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -4,6 +4,8 @@ 会话领域:骨架(标题栏/标签页/编辑器/空状态)、聊天视图(分组步骤摘要流、流式尾部隔离、逐工具行 slot 及一个 bash 示例注册方与 todo 行)、编辑器 dock(与输入区一同 sticky 的会话统计行)、输入区 dock(队列行加 todo 计划条)、最小详情面板、按 scope 寻址的 ConversationService。契约:api-contracts v3 §7 加 slot 终端设计(store seat/props share)。 +压缩在检查点自身的消息流位置渲染为一行折叠标记,不替换其上方的对话记录。展开内容来自检查点溯源的 `compact/summary`;该事件位于已加载窗口之外时,标记仍然可见但不可展开。面向模型的带框检查点载荷绝不渲染。 + 常驻会话壳会跨无会话与会话状态切换而保留。没有当前会话时,它会渲染禁用输入栏;其根作用域的 `conversation.hero.workspace` slot 承载 Workspace 选择器。选择 Workspace 会连接或复用由 Host 拥有的空白会话,并在不替换会话壳的情况下打开该会话。空白会话与活跃会话渲染相同的输入区主体;InputHub 则在 Workspace 切换间携带草稿,并将草稿镜像到会话 store。活跃阶段会话标题栏以普通列 chrome 占据顶部;其下滚动容器(`data-conversation-scroll`)承载流动排版的各视图与 sticky 编辑器栈(统计 dock+输入区 dock+输入栏)。textarea 上的滚轮会链式处理:限高草稿先在本地滚动,到达边缘后再转交给该宿主。 视图环本身就是 slot:会话注册声明 `'conversation.view'` 列表 slot(Session scope),并将其列在 `children` 表中;ConversationRoot 通过 renderSlot share 渲染活跃配置项(`only: `);视图标签页从环账本的注册选项(`id`/`order`/`label`)投影而来。聊天视图是该包自身的环配置项;其他插件(ui-trajectory)通过普通的 `ctx.slots.register` 贡献标签页。先前包内的视图注册表(`registerView`/`ViewEntry`/`ConversationViewMap` 及 chrome 附加表)已退役,逐视图 chrome 则被拆入视图组件自身。 @@ -34,6 +36,7 @@ todo 两个面就是在该形状上的两个注册项,都是普通注册方插 ## 已知限制与暂缓事项 +- **压缩标记不显示规模**:该行尚不报告检查点替换了多少条消息或哪段范围。 - **统计行的耗时只覆盖窗口内消息流**:LLM 与工具墙钟时间由快照的 assistant `timing` 与工具 call/result 配对折算,落在已加载事件窗口之外的节点(更早的历史)不计入。 - **详情面板是最小形态,且当前没有入口**:以原始形式显示已选择调用的参数/结果;Input/Output/Metadata 切换、Prev/Next 步进与 See-in-trajectory 深链接暂缓实现。工具行已不再是详情面板的点击目标,且没有任何手势接替它,因此 `ChatViewInjected.openDetails` 虽已实现却无人调用,该面板(含其终端卡片)在组装后的应用中不可达;其渲染仍由直接以选中态挂载它来覆盖。 - **assistant 逐消息分页是预留 slot**:设计中已有图稿,尚未实现。已定稿的内容 IconActions 行(复制/分支/时钟)只挂在 text 输出下;分支仍是 chrome stub。 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f1cf3d328..ea8592962e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -938,6 +938,9 @@ importers: '@deepseek-ai/dsh-client-connection': specifier: workspace:^ version: link:../connection + '@deepseek-ai/dsh-compact': + specifier: workspace:^ + version: link:../../compact/compact '@deepseek-ai/dsh-client-ui-slots': specifier: workspace:^ version: link:../ui-slots @@ -969,9 +972,6 @@ importers: specifier: ~4.4.7 version: 4.4.7(@types/react@18.3.31)(immer@10.2.0)(react@18.3.1) devDependencies: - '@deepseek-ai/dsh-compact': - specifier: workspace:^ - version: link:../../compact/compact '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../support/invariants