Sfoglia il codice sorgente

feat(web): move context usage into composer stats

Yichen Jiang 3 giorni fa
parent
commit
b6726fe79d
50 ha cambiato i file con 92 aggiunte e 74 eliminazioni
  1. 2 2
      .agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml
  2. 3 3
      .agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md
  3. 3 3
      .agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md
  4. 1 1
      apps/web/tests/expected/cordis-history/ui.expected.md
  5. 1 1
      apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md
  6. 1 1
      apps/web/tests/expected/skill-user-invoke/ui.expected.md
  7. 1 1
      apps/web/tests/expected/steer-all/settled-expanded.expected.md
  8. 1 1
      apps/web/tests/expected/steer-all/settled.expected.md
  9. 2 4
      packages/client/ui-chat/src/client/chat/StatsPills.module.css
  10. 0 2
      packages/client/ui-chat/src/client/chat/StatsPills.tsx
  11. 0 2
      packages/client/ui-chat/tests/chat-stats.client.spec.tsx
  12. 2 2
      packages/client/ui-conversation/README.i18n.yaml
  13. 2 0
      packages/client/ui-conversation/README.md
  14. 2 0
      packages/client/ui-conversation/README.zh.md
  15. 16 10
      packages/client/ui-conversation/src/client/skeleton/ContextMeter.module.css
  16. 2 1
      packages/client/ui-conversation/src/client/skeleton/ContextMeter.tsx
  17. 15 4
      packages/client/ui-conversation/src/client/skeleton/InputBar.module.css
  18. 6 4
      packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
  19. 1 1
      snapshots/web/feedback-command/ack-expanded.expected.md
  20. 1 1
      snapshots/web/feedback-command/ack.expected.md
  21. 1 1
      snapshots/web/feedback-release/ack-expanded.expected.md
  22. 1 1
      snapshots/web/feedback-release/ack.expected.md
  23. 1 1
      snapshots/web/file-upload-round/ui.expected.md
  24. 1 1
      snapshots/web/fresh-round-trip/ui-expanded.expected.md
  25. 1 1
      snapshots/web/fresh-round-trip/ui.expected.md
  26. 1 1
      snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md
  27. 1 1
      snapshots/web/goal-multi-turn-actions/ui.expected.md
  28. 1 1
      snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md
  29. 1 1
      snapshots/web/lifecycle-chrome/reloaded.expected.md
  30. 1 1
      snapshots/web/live-interactions/retry-expanded.expected.md
  31. 1 1
      snapshots/web/live-interactions/retry.expected.md
  32. 1 1
      snapshots/web/minimal-preset/ui.expected.md
  33. 1 1
      snapshots/web/plan-review/approved-expanded.expected.md
  34. 1 1
      snapshots/web/plan-review/approved.expected.md
  35. 1 1
      snapshots/web/present/ui.expected.md
  36. 1 1
      snapshots/web/ptc-round/ui.expected.md
  37. 1 1
      snapshots/web/question-composer/answered-expanded.expected.md
  38. 1 1
      snapshots/web/question-composer/answered.expected.md
  39. 1 1
      snapshots/web/question-composer/cancelled.expected.md
  40. 1 1
      snapshots/web/queued-image/delivered.expected.md
  41. 1 1
      snapshots/web/steering/settled-expanded.expected.md
  42. 1 1
      snapshots/web/steering/settled.expected.md
  43. 1 1
      snapshots/web/subagent-conversation/ui-expanded.expected.md
  44. 1 1
      snapshots/web/subagent-conversation/ui.expected.md
  45. 1 1
      snapshots/web/turn-tail-actions/completed.expected.md
  46. 1 1
      snapshots/web/turn-tail-actions/focused.expected.md
  47. 1 1
      snapshots/web/turn-tail-actions/running.expected.md
  48. 1 1
      snapshots/web/turn-tail-actions/settled.expected.md
  49. 1 1
      snapshots/web/turn-tail-actions/usage-expanded.expected.md
  50. 1 1
      snapshots/web/web-search-round/ui.expected.md

+ 2 - 2
.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md
-2026-09-07-composer-session-stats-pills.md: 541e5a363d78c4333b1b8e3927928a19754822fa
-2026-09-07-composer-session-stats-pills.zh.md: b6ef6da3c37a46d9e07e7ebdf77439e26896fddc
+2026-09-07-composer-session-stats-pills.md: 8a1fc9408e9fd0fcfc127158d8165474fee33f23
+2026-09-07-composer-session-stats-pills.zh.md: ccd2fd6f7b8d909a12d846698d18e4dcd8becfa4

+ 3 - 3
.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md

@@ -13,9 +13,9 @@ The session stats strip under the composer (`StatsLine`, ui-chat, mounted on `co
 `StatsPills` (packages/client/ui-chat/src/client/chat/StatsPills.tsx) replaces `StatsLine` on the same `conversation.composer.dock` slot; the losing variant is deleted, its shared helpers (`deriveStats`, `formatDuration`, `cacheHitPercent`, `billedInputTokens`) absorbed into the new module, and the dead `stats.llm`, `stats.toolCall`, `stats.ttftAverage`, `stats.tokensPerSecond`, and `stats.tokens` locale keys removed.
 
 - **Two icon pills, two dialogs.** A gauge pill (new `IconGaugeOutline16`, dial center optically dropped to y=8.75 because the bottom-open arc reads high) shows `{turns} 轮 {steps} 步` plus output TPS and click-opens the 会话统计 dialog (LLM time, tool time, average TTFT, TPS); a log with no timed figure would open an empty dialog, so that pill renders as a static reading instead of a button. A database pill (`IconDatabaseOutline16`) shows the compact billed total plus cache-hit share and click-opens the Token 用量 dialog (cache hit, uncached input, cache read, output, and cache write when non-zero — exact counts). Both dialogs wear the shared `stat-dialog` module (portal panel, anchored placement, outside-dismiss, optionally externally owned open state) extracted for exactly this two-consumer split; the pills row owns one exclusive open slot, so opening either dialog closes the other, and each button carries an explicit `aria-label` that separates with ` · ` the segments the aria-hidden sep glyph joins visually. [Guide start page and stat pill refinements](2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns the zero cache-write omission.
-- **Data sourcing is unchanged in architecture.** Counts and times prefer the durable `sessionStats` projection with the window fold as the assembly-without-the-unit fallback ([whole-session counts](../../archived/bug-fix/2026-08-12-full-session-turn-step-counts.md)); token figures ride `tokenUsage` only, so an absent projection drops the usage pill rather than showing window-derived billing. Cache writes stay in the billed total and the cache-hit denominator ([projection decision](../architecture/2026-07-29-projected-token-usage-and-request-context.md)). Context occupancy stays on the composer's ContextMeter ring, where it already lived beside `StatsLine` — the strip never carried it.
+- **Data sourcing is unchanged in architecture.** Counts and times prefer the durable `sessionStats` projection with the window fold as the assembly-without-the-unit fallback ([whole-session counts](../../archived/bug-fix/2026-08-12-full-session-turn-step-counts.md)); token figures ride `tokenUsage` only, so an absent projection drops the usage pill rather than showing window-derived billing. Cache writes stay in the billed total and the cache-hit denominator ([projection decision](../architecture/2026-07-29-projected-token-usage-and-request-context.md)). Context occupancy remains owned by ui-conversation's `ContextMeter`, with its ring and percentage after the two stats pills below the input card. The common dock groups statistics together and leaves the toolbar for input actions; ui-chat does not import the context component.
 - **Render discipline.** The row folds settled nodes only (`chat.legacy.nodes` identity), so streaming chunk frames cause zero rerenders — pinned by a render-count unit test. A session with no closed step and no billed tokens renders nothing.
-- **`data-composer-stats` is a cross-package attribute contract.** The pills' root carries it; ui-conversation's `InputBar.module.css` `:has([data-composer-stats])` rule tightens the composer's bottom clearance to 4px when the row is mounted. The producer side pins the attribute in unit tests, following the `data-trigger-menu` precedent.
+- **The composer owns dock spacing.** `InputBar` places slot contributions and `ContextMeter` in one centered flex row, supplies its 4px top clearance, and tightens the bottom clearance only while the dock has content. `StatsPills` supplies shrinkable time and billing content; it does not claim the full row width or add outer padding.
 
 ## Alternatives considered
 
@@ -27,5 +27,5 @@ The session stats strip under the composer (`StatsLine`, ui-chat, mounted on `co
 
 - `ChatSnapshotBuilder`'s legacy slice now serves StatsPills; the [node-assembly note](../architecture/2026-08-09-client-conversation-node-assembly.md) tracks that consumer rename.
 - Exact token counts become reachable at all — one click — where `StatsLine` showed only compact totals; the strip itself carries only the two headline readings.
-- Web e2e strip assertions match substring text inside the time pill; the fresh-round-trip aria goldens pin the two-pill structure, and stats-paged-history pins the counts reading alone over a log with no billed tokens.
+- Web e2e strip assertions match substring text inside the time pill; the fresh-round-trip aria goldens pin time, billing, and context order below the submit action, and stats-paged-history pins the counts reading alone over a log with no billed tokens.
 - The `conversation.composer.dock` occupant in the generated slot catalog is `client-ui-chat StatsPills id 'stats'`.

+ 3 - 3
.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md

@@ -13,9 +13,9 @@ Status: implemented
 `StatsPills`(packages/client/ui-chat/src/client/chat/StatsPills.tsx)在同一 `conversation.composer.dock` 插槽上取代 `StatsLine`;落选变体已删除,其共享工具函数(`deriveStats`、`formatDuration`、`cacheHitPercent`、`billedInputTokens`)并入新模块,废弃的 `stats.llm`、`stats.toolCall`、`stats.ttftAverage`、`stats.tokensPerSecond`、`stats.tokens` 文案键一并移除。
 
 - **两个图标 pill、两个弹层。** 仪表盘 pill(新增 `IconGaugeOutline16`,因下开口圆弧视觉偏高而把表盘中心光学下移到 y=8.75)展示 `{turns} 轮 {steps} 步` 加输出 TPS,点击打开「会话统计」弹层(模型用时、工具调用用时、首 token 平均、输出速度);日志里没有任何计时数字时弹层会是空的,此时该 pill 渲染为静态读数而非按钮。数据库 pill(`IconDatabaseOutline16`)展示紧凑计费总量加缓存命中率,点击打开「Token 用量」弹层(缓存命中、未缓存输入、缓存读取、输出,以及非零时的缓存写入——精确计数)。两个弹层共用为这两处消费者抽出的 `stat-dialog` 模块(portal 面板、锚定定位、点击外部关闭、可选的外部持有开合状态);pill 行持有唯一的互斥开合槽位,打开任一弹层即关闭另一个,且每个按钮携带显式 `aria-label`,用 ` · ` 分隔 aria-hidden 分隔符在视觉上连接的两段文本。[引导起始页与统计 pill 的细化](2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责缓存写入为零时省略该行的规则。
-- **数据来源架构不变。** 计数与用时优先读取持久的 `sessionStats` 投影,窗口折叠仅作无该单元装配时的回退([全会话计数](../../archived/bug-fix/2026-08-12-full-session-turn-step-counts.md));token 数字只走 `tokenUsage`,投影缺席时直接不渲染用量 pill,而非展示窗口推算的计费。缓存写入仍计入计费总量与缓存命中分母([投影决定](../architecture/2026-07-29-projected-token-usage-and-request-context.zh.md))。上下文占用仍在输入框旁的 ContextMeter 圆环上,`StatsLine` 时代它就在那里——统计条从未承载过它
+- **数据来源架构不变。** 计数与用时优先读取持久的 `sessionStats` 投影,窗口折叠仅作无该单元装配时的回退([全会话计数](../../archived/bug-fix/2026-08-12-full-session-turn-step-counts.md));token 数字只走 `tokenUsage`,投影缺席时直接不渲染用量 pill,而非展示窗口推算的计费。缓存写入仍计入计费总量与缓存命中分母([投影决定](../architecture/2026-07-29-projected-token-usage-and-request-context.zh.md))。上下文占用仍归 ui-conversation 的 `ContextMeter` 所有,在输入卡片下方、两个统计 pill 之后显示圆环和百分比。共用 dock 将统计信息放在一起,工具栏保留给输入操作;ui-chat 不导入上下文组件
 - **渲染纪律。** 该行只折叠已定稿节点(`chat.legacy.nodes` 身份),流式 chunk 帧零重渲染——由渲染计数单测钉住。无已完成步且无计费 token 的会话什么都不渲染。
-- **`data-composer-stats` 是跨包属性契约。** pill 行根元素携带它;ui-conversation 的 `InputBar.module.css` 用 `:has([data-composer-stats])` 在该行挂载时把输入框底部留白收紧到 4px。生产方在单测里钉住该属性,沿用 `data-trigger-menu` 先例
+- **输入框负责 dock 间距。** `InputBar` 将 slot 贡献项和 `ContextMeter` 放在同一个居中的 flex 行中,提供 4px 顶部留白,并仅在 dock 有内容时收紧底部留白。`StatsPills` 提供可收缩的时间与计费内容,不占满整行宽度,也不添加外部 padding
 
 ## 备选方案
 
@@ -27,5 +27,5 @@ Status: implemented
 
 - `ChatSnapshotBuilder` 的 legacy 切片现在服务于 StatsPills;[节点装配 note](../architecture/2026-08-09-client-conversation-node-assembly.zh.md) 已跟进该消费者更名。
 - 精确 token 计数第一次变得可达——一次点击即可;`StatsLine` 只展示过紧凑总量。统计条本身只承载两个头条读数。
-- Web e2e 对统计条的断言匹配时间 pill 内的子串文本;fresh-round-trip 的 aria golden 钉住双 pill 结构,stats-paged-history 则在无计费 token 的日志上钉住单独的计数读数。
+- Web e2e 对统计条的断言匹配时间 pill 内的子串文本;fresh-round-trip 的 aria golden 钉住提交操作下方的时间、计费、上下文顺序,stats-paged-history 则在无计费 token 的日志上钉住单独的计数读数。
 - 生成的插槽目录中 `conversation.composer.dock` 占用者为 `client-ui-chat StatsPills id 'stats'`。

+ 1 - 1
apps/web/tests/expected/cordis-history/ui.expected.md

@@ -122,7 +122,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "0% of context used"
 - button "Send message" [disabled]
 - button "3 turns 7 steps":
   - img
@@ -130,3 +129,4 @@
 - button "66.8K tok · Cache hit 77%":
   - img
   - text: 66.8K tokCache hit 77%
+- button "0% of context used": 0%

+ 1 - 1
apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md

@@ -52,7 +52,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "0% of context used"
 - button "Send message" [disabled]
 - button "1 turns 1 steps · {{throughput}} tok/s":
   - img
@@ -60,3 +59,4 @@
 - button "272 tok · Cache hit 0%":
   - img
   - text: 272 tokCache hit 0%
+- button "0% of context used": 0%

+ 1 - 1
apps/web/tests/expected/skill-user-invoke/ui.expected.md

@@ -44,7 +44,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "0% of context used"
 - button "Send message" [disabled]
 - button "1 turns 1 steps · {{throughput}} tok/s":
   - img
@@ -52,3 +51,4 @@
 - button "272 tok · Cache hit 0%":
   - img
   - text: 272 tokCache hit 0%
+- button "0% of context used": 0%

+ 1 - 1
apps/web/tests/expected/steer-all/settled-expanded.expected.md

@@ -58,7 +58,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "0% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -66,3 +65,4 @@
 - button "40 tok · Cache hit 0%":
   - img
   - text: 40 tokCache hit 0%
+- button "0% of context used": 0%

+ 1 - 1
apps/web/tests/expected/steer-all/settled.expected.md

@@ -46,7 +46,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "0% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -54,3 +53,4 @@
 - button "40 tok · Cache hit 0%":
   - img
   - text: 40 tokCache hit 0%
+- button "0% of context used": 0%

+ 2 - 4
packages/client/ui-chat/src/client/chat/StatsPills.module.css

@@ -6,11 +6,9 @@
   display: flex;
   justify-content: center;
   gap: 12px;
-  max-width: var(--dsh-chat-content-width);
-  width: 100%;
-  margin: 0 auto;
+  min-width: 0;
+  max-width: 100%;
   box-sizing: border-box;
-  padding: 4px calc(var(--dsh-composer-side-clearance) + 16px) 0px;
   font-size: var(--dsh-content-font-size-secondary, 13px);
   line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));
 }

+ 0 - 2
packages/client/ui-chat/src/client/chat/StatsPills.tsx

@@ -330,8 +330,6 @@ export const StatsPills = memo(function StatsPills({ useChat, useProjection, t }
   const hasTokens = usage !== undefined
     && (billedInputTokens(usage) > 0 || usage.outputTokens > 0)
   if (stats.steps === 0 && !hasTokens) return null
-  // data-composer-stats: InputBar's `.root:has([data-composer-stats])` rule
-  // tightens the composer's bottom clearance only while this row renders.
   return (
     <div className={css.root} data-composer-stats>
       {stats.steps > 0 && (

+ 0 - 2
packages/client/ui-chat/tests/chat-stats.client.spec.tsx

@@ -161,8 +161,6 @@ describe('StatsPills', () => {
   it('renders the counts reading and usage pill and hides a brand-new empty session', () => {
     const { source } = makeSource({ nodes: [assistant(1, 1)] })
     const view = render(<StatsPills {...props(source)} />)
-    // InputBar's `.root:has([data-composer-stats])` bottom-clearance rule keys
-    // off this attribute: present exactly while the row renders.
     expect(view.container.querySelector('[data-composer-stats]')).toBeTruthy()
     // No timing on the fixture: the speed segment drops out and the dialog
     // would have no rows, so the counts reading stays a static pill (no button).

+ 2 - 2
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: 749c2d0d99ce7e5afe2f9a03b1451d62a17206e7
-README.zh.md: 98d7d9672922ca68ea4df5499f8789e8c2fde162
+README.md: 6cc6821213b0b4438590e86a332fa0d0e27876a0
+README.zh.md: 4179c56cfec2b6cbeb02aef3ea4ba5e9408aefab

+ 2 - 0
packages/client/ui-conversation/README.md

@@ -36,6 +36,8 @@ Target packages declaration-merge their snapshot and Location data maps, then re
 <a id="shell-and-standard-props"></a>
 ## Shell and standard props
 
+The context-occupancy button shows a ring and percentage below the input card, after the Session statistics. Clicking it opens the token breakdown; the button stays hidden until context usage and capacity are available.
+
 The composer registers the File command action and owns its label, availability, and native file-dialog callback. Menu availability and invocation both consult the mounted composer's current attachment-intake policy. Unmounting or locking the composer disables that action; disposing the plugin removes its registration. The callback binding stays inside the input module.
 
 `SessionInputShell` owns one Lexical editor per Session through its private [DraftEditorRuntime](src/client/input/editor/runtime.ts), while retaining submission, attachment selection, and recovery decisions. [DraftEditor](src/client/input/editor/DraftEditor.tsx) renders the borrowed editor; InputBar retains its Hooks and refs and installs DOM behavior through [view-binding](src/client/input/editor/view-binding.ts). Editor-facing types live in [draft-editor.ts](src/client/contract/draft-editor.ts), with shared input and submission types in [input.ts](src/client/contract/input.ts). This separation does not support simultaneous editable roots for one Session; [the two-stage isolation proposal](../../../.agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.md) defines the remaining work.

+ 2 - 0
packages/client/ui-conversation/README.zh.md

@@ -36,6 +36,8 @@ target package 通过 declaration merge 扩展 snapshot 与 Location data map,
 <a id="shell-and-standard-props"></a>
 ## Shell 与标准 props
 
+上下文占用按钮在输入卡片下方、会话统计右侧显示圆环和百分比。点击按钮可查看 token 构成;上下文用量和容量尚不可用时,按钮保持隐藏。
+
 输入框注册「文件」命令动作,负责其标题、可用性和原生文件选择器回调。菜单可用性与实际调用都读取已挂载输入框当前的附件接收策略。输入框卸载或锁定后该动作不可用,插件 dispose(资源释放)时移除注册。回调绑定留在输入模块内部。
 
 `SessionInputShell` 通过私有 [DraftEditorRuntime](src/client/input/editor/runtime.ts) 为每个 Session 持有一个 Lexical editor,同时保留提交、附件选择和恢复决策。[DraftEditor](src/client/input/editor/DraftEditor.tsx) 呈现借用的 editor;InputBar 保留钩子与 refs,并通过 [view-binding](src/client/input/editor/view-binding.ts) 安装 DOM 行为。编辑器类型位于 [draft-editor.ts](src/client/contract/draft-editor.ts),共享输入和提交类型位于 [input.ts](src/client/contract/input.ts)。这一拆分不支持同一 Session 同时挂载多个可编辑 root;[两阶段隔离提案](../../../.agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.zh.md) 定义剩余工作。

+ 16 - 10
packages/client/ui-conversation/src/client/skeleton/ContextMeter.module.css

@@ -1,28 +1,34 @@
-/* Context-occupancy ring beside the send button plus its click-open breakdown
+/* Context-occupancy pill below the composer plus its click-open breakdown
    panel (menu surface: r12, elevation-prominent). */
 
 .root {
   position: relative;
   display: inline-flex;
+  flex: none;
 }
 
-/* Same 28px circular hit target family as the composer's attach button. */
 .trigger {
-  display: grid;
-  place-items: center;
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
   flex: none;
-  width: 28px;
-  height: 28px;
+  padding: 1px 8px;
   border: none;
-  border-radius: 999px;
-  corner-shape: round;
+  border-radius: 24px;
   background: transparent;
-  color: var(--dsw-alias-label-secondary);
+  color: var(--dsw-alias-label-tertiary);
+  font-family: inherit;
+  font-size: var(--dsh-content-font-size-secondary, 13px);
+  font-variant-numeric: tabular-nums;
+  line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));
+  white-space: nowrap;
   cursor: pointer;
 }
 
-.trigger:hover {
+.trigger:hover,
+.trigger[aria-expanded='true'] {
   background: var(--dsw-alias-interactive-bg-hover);
+  color: var(--dsw-alias-label-secondary);
 }
 
 .track {

+ 2 - 1
packages/client/ui-conversation/src/client/skeleton/ContextMeter.tsx

@@ -1,4 +1,4 @@
-/** Composer context-occupancy meter: a ring beside the send button fed by the
+/** Composer context-occupancy meter: a ring and percentage below the card fed by the
  * `contextPressure` projection, with a click-open panel of the heuristic
  * `contextBreakdown` composition (system prompt, tools, conversation).
  * Renders nothing until a provider reports both pressure and a route
@@ -125,6 +125,7 @@ export function ContextMeter({ useProjection, t }: ContextMeterProps) {
               transform="rotate(-90 7 7)"
             />
           </svg>
+          <span>{reading}</span>
         </button>
       </Tooltip>
       {open && (

+ 15 - 4
packages/client/ui-conversation/src/client/skeleton/InputBar.module.css

@@ -10,13 +10,24 @@
   padding: 0 var(--dsh-composer-side-clearance) 8px;
 }
 
-/* A mounted stats row in the composer dock (data-composer-stats, ui-chat
-   StatsPills) brings its own 4px top pad, so the root's bottom clearance
-   tightens to keep the drawn B8 rhythm. */
-.root:has([data-composer-stats]) {
+/* The populated dock supplies the other 4px of the bottom clearance. */
+.root:has(.dock:not(:empty)) {
   padding-bottom: 4px;
 }
 
+.dock {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 12px;
+  max-width: 100%;
+  padding-top: 4px;
+}
+
+.dock:empty {
+  display: none;
+}
+
 .hero {
   /* No bottom pad in the centered hero, but the side clearance must survive:
      the hero wrapper is full-width on narrow viewports, so this padding is

+ 6 - 4
packages/client/ui-conversation/src/client/skeleton/InputBar.tsx

@@ -442,7 +442,6 @@ export const InputBar = memo(function InputBar({
               ? null
               : renderSlot('conversation.input.right', {})}
             {sessionId === undefined ? null : renderSlot('conversation.input.model', { locked: modelSeatLocked })}
-            <ContextMeter useProjection={useProjection} t={t} />
             {interruptible && (
               <Tooltip label={t('input.stop')} side="top" delayMs={500} disabled={stop === undefined}>
                 <button
@@ -482,9 +481,12 @@ export const InputBar = memo(function InputBar({
           </div>
         </div>
       </div>
-      {variant === 'composer' && input !== undefined && sessionId !== undefined
-        ? renderSlot('conversation.composer.dock', {})
-        : null}
+      <div className={css.dock}>
+        {variant === 'composer' && input !== undefined && sessionId !== undefined
+          ? renderSlot('conversation.composer.dock', {})
+          : null}
+        <ContextMeter useProjection={useProjection} t={t} />
+      </div>
     </div>
   )
 })

+ 1 - 1
snapshots/web/feedback-command/ack-expanded.expected.md

@@ -52,7 +52,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "6% of context used"
 - button "Send message" [disabled]
 - button "1 turns 1 steps · {{throughput}} tok/s":
   - img
@@ -60,3 +59,4 @@
 - button "7.8K tok · Cache hit 99%":
   - img
   - text: 7.8K tokCache hit 99%
+- button "6% of context used": 6%

+ 1 - 1
snapshots/web/feedback-command/ack.expected.md

@@ -44,7 +44,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "6% of context used"
 - button "Send message" [disabled]
 - button "1 turns 1 steps · {{throughput}} tok/s":
   - img
@@ -52,3 +51,4 @@
 - button "7.8K tok · Cache hit 99%":
   - img
   - text: 7.8K tokCache hit 99%
+- button "6% of context used": 6%

+ 1 - 1
snapshots/web/feedback-release/ack-expanded.expected.md

@@ -52,7 +52,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "6% of context used"
 - button "Send message" [disabled]
 - button "1 turns 1 steps · {{throughput}} tok/s":
   - img
@@ -60,3 +59,4 @@
 - button "7.8K tok · Cache hit 99%":
   - img
   - text: 7.8K tokCache hit 99%
+- button "6% of context used": 6%

+ 1 - 1
snapshots/web/feedback-release/ack.expected.md

@@ -44,7 +44,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "6% of context used"
 - button "Send message" [disabled]
 - button "1 turns 1 steps · {{throughput}} tok/s":
   - img
@@ -52,3 +51,4 @@
 - button "7.8K tok · Cache hit 99%":
   - img
   - text: 7.8K tokCache hit 99%
+- button "6% of context used": 6%

+ 1 - 1
snapshots/web/file-upload-round/ui.expected.md

@@ -46,7 +46,6 @@
 - button "Select model, current DeepSeek-V4-Flash-Vision-Exp, reasoning effort high":
   - text: DeepSeek-V4-Flash-Vision-Exp high
   - img
-- button "1% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -54,3 +53,4 @@
 - button "17.3K tok · Cache hit 51%":
   - img
   - text: 17.3K tokCache hit 51%
+- button "1% of context used": 1%

+ 1 - 1
snapshots/web/fresh-round-trip/ui-expanded.expected.md

@@ -56,7 +56,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "6% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -64,3 +63,4 @@
 - button "15.8K tok · Cache hit 99%":
   - img
   - text: 15.8K tokCache hit 99%
+- button "6% of context used": 6%

+ 1 - 1
snapshots/web/fresh-round-trip/ui.expected.md

@@ -40,7 +40,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "6% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -48,3 +47,4 @@
 - button "15.8K tok · Cache hit 99%":
   - img
   - text: 15.8K tokCache hit 99%
+- button "6% of context used": 6%

+ 1 - 1
snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md

@@ -234,7 +234,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "9% of context used"
 - button "Send message" [disabled]
 - button "2 turns 12 steps · {{throughput}} tok/s":
   - img
@@ -242,3 +241,4 @@
 - button "115K tok · Cache hit 91%":
   - img
   - text: 115K tokCache hit 91%
+- button "9% of context used": 9%

+ 1 - 1
snapshots/web/goal-multi-turn-actions/ui.expected.md

@@ -122,7 +122,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "9% of context used"
 - button "Send message" [disabled]
 - button "2 turns 12 steps · {{throughput}} tok/s":
   - img
@@ -130,3 +129,4 @@
 - button "115K tok · Cache hit 91%":
   - img
   - text: 115K tokCache hit 91%
+- button "9% of context used": 9%

+ 1 - 1
snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md

@@ -48,7 +48,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "8% of context used"
 - button "Send message" [disabled]
 - button "1 turns 1 steps · {{throughput}} tok/s":
   - img
@@ -56,3 +55,4 @@
 - button "10K tok · Cache hit 99.5%":
   - img
   - text: 10K tokCache hit 99.5%
+- button "8% of context used": 8%

+ 1 - 1
snapshots/web/lifecycle-chrome/reloaded.expected.md

@@ -40,7 +40,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "8% of context used"
 - button "Send message" [disabled]
 - button "1 turns 1 steps · {{throughput}} tok/s":
   - img
@@ -48,3 +47,4 @@
 - button "10K tok · Cache hit 99.5%":
   - img
   - text: 10K tokCache hit 99.5%
+- button "8% of context used": 8%

+ 1 - 1
snapshots/web/live-interactions/retry-expanded.expected.md

@@ -50,7 +50,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "6% of context used"
 - button "Send message" [disabled]
 - button "1 turns 1 steps · {{throughput}} tok/s":
   - img
@@ -58,3 +57,4 @@
 - button "7.9K tok · Cache hit 99%":
   - img
   - text: 7.9K tokCache hit 99%
+- button "6% of context used": 6%

+ 1 - 1
snapshots/web/live-interactions/retry.expected.md

@@ -40,7 +40,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "6% of context used"
 - button "Send message" [disabled]
 - button "1 turns 1 steps · {{throughput}} tok/s":
   - img
@@ -48,3 +47,4 @@
 - button "7.9K tok · Cache hit 99%":
   - img
   - text: 7.9K tokCache hit 99%
+- button "6% of context used": 6%

+ 1 - 1
snapshots/web/minimal-preset/ui.expected.md

@@ -45,7 +45,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "0% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -53,3 +52,4 @@
 - button "28 tok · Cache hit 0%":
   - img
   - text: 28 tokCache hit 0%
+- button "0% of context used": 0%

+ 1 - 1
snapshots/web/plan-review/approved-expanded.expected.md

@@ -66,7 +66,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "4% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -74,3 +73,4 @@
 - button "10.6K tok · Cache hit 51%":
   - img
   - text: 10.6K tokCache hit 51%
+- button "4% of context used": 4%

+ 1 - 1
snapshots/web/plan-review/approved.expected.md

@@ -46,7 +46,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "4% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -54,3 +53,4 @@
 - button "10.6K tok · Cache hit 51%":
   - img
   - text: 10.6K tokCache hit 51%
+- button "4% of context used": 4%

+ 1 - 1
snapshots/web/present/ui.expected.md

@@ -97,7 +97,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "8% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -105,3 +104,4 @@
 - button "19K tok · Cache hit 94%":
   - img
   - text: 19K tokCache hit 94%
+- button "8% of context used": 8%

+ 1 - 1
snapshots/web/ptc-round/ui.expected.md

@@ -67,7 +67,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "7% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -75,3 +74,4 @@
 - button "17.5K tok · Cache hit 52%":
   - img
   - text: 17.5K tokCache hit 52%
+- button "7% of context used": 7%

+ 1 - 1
snapshots/web/question-composer/answered-expanded.expected.md

@@ -58,7 +58,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "3% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -66,3 +65,4 @@
 - button "8.8K tok · Cache hit 95%":
   - img
   - text: 8.8K tokCache hit 95%
+- button "3% of context used": 3%

+ 1 - 1
snapshots/web/question-composer/answered.expected.md

@@ -40,7 +40,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "3% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -48,3 +47,4 @@
 - button "8.8K tok · Cache hit 95%":
   - img
   - text: 8.8K tokCache hit 95%
+- button "3% of context used": 3%

+ 1 - 1
snapshots/web/question-composer/cancelled.expected.md

@@ -37,7 +37,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "3% of context used"
 - button "Send message" [disabled]
 - button "1 turns 1 steps · {{throughput}} tok/s":
   - img
@@ -45,3 +44,4 @@
 - button "4.4K tok · Cache hit 97%":
   - img
   - text: 4.4K tokCache hit 97%
+- button "3% of context used": 3%

+ 1 - 1
snapshots/web/queued-image/delivered.expected.md

@@ -84,7 +84,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "6% of context used"
 - button "Send message" [disabled]
 - button "3 turns 3 steps · {{throughput}} tok/s":
   - img
@@ -92,3 +91,4 @@
 - button "15.7K tok · Cache hit 99%":
   - img
   - text: 15.7K tokCache hit 99%
+- button "6% of context used": 6%

+ 1 - 1
snapshots/web/steering/settled-expanded.expected.md

@@ -51,7 +51,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "0% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -59,3 +58,4 @@
 - button "30 tok · Cache hit 0%":
   - img
   - text: 30 tokCache hit 0%
+- button "0% of context used": 0%

+ 1 - 1
snapshots/web/steering/settled.expected.md

@@ -43,7 +43,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "0% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -51,3 +50,4 @@
 - button "30 tok · Cache hit 0%":
   - img
   - text: 30 tokCache hit 0%
+- button "0% of context used": 0%

+ 1 - 1
snapshots/web/subagent-conversation/ui-expanded.expected.md

@@ -80,7 +80,6 @@
 - button "Add files or run commands":
   - img
 - 'button "Access mode, current: Custom"': Custom
-- button "6% of context used"
 - button "Send message" [disabled]
 - button "2 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -88,3 +87,4 @@
 - button "15.7K tok · Cache hit 99%":
   - img
   - text: 15.7K tokCache hit 99%
+- button "6% of context used": 6%

+ 1 - 1
snapshots/web/subagent-conversation/ui.expected.md

@@ -68,7 +68,6 @@
 - button "Add files or run commands":
   - img
 - 'button "Access mode, current: Custom"': Custom
-- button "6% of context used"
 - button "Send message" [disabled]
 - button "2 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -76,3 +75,4 @@
 - button "15.7K tok · Cache hit 99%":
   - img
   - text: 15.7K tokCache hit 99%
+- button "6% of context used": 6%

+ 1 - 1
snapshots/web/turn-tail-actions/completed.expected.md

@@ -43,7 +43,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "6% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -51,3 +50,4 @@
 - button "15.8K tok · Cache hit 50%":
   - img
   - text: 15.8K tokCache hit 50%
+- button "6% of context used": 6%

+ 1 - 1
snapshots/web/turn-tail-actions/focused.expected.md

@@ -56,7 +56,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "6% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -64,3 +63,4 @@
 - button "15.8K tok · Cache hit 50%":
   - img
   - text: 15.8K tokCache hit 50%
+- button "6% of context used": 6%

+ 1 - 1
snapshots/web/turn-tail-actions/running.expected.md

@@ -40,7 +40,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "6% of context used"
 - button "Stop generating"
 - button "1 turns 1 steps · {{throughput}} tok/s":
   - img
@@ -48,3 +47,4 @@
 - button "7.9K tok · Cache hit 0%":
   - img
   - text: 7.9K tokCache hit 0%
+- button "6% of context used": 6%

+ 1 - 1
snapshots/web/turn-tail-actions/settled.expected.md

@@ -42,7 +42,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "6% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -50,3 +49,4 @@
 - button "7.9K tok · Cache hit 0%":
   - img
   - text: 7.9K tokCache hit 0%
+- button "6% of context used": 6%

+ 1 - 1
snapshots/web/turn-tail-actions/usage-expanded.expected.md

@@ -43,7 +43,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "6% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -51,3 +50,4 @@
 - button "15.8K tok · Cache hit 50%":
   - img
   - text: 15.8K tokCache hit 50%
+- button "6% of context used": 6%

+ 1 - 1
snapshots/web/web-search-round/ui.expected.md

@@ -48,7 +48,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "0% of context used"
 - button "Send message" [disabled]
 - button "1 turns 2 steps · {{throughput}} tok/s":
   - img
@@ -56,3 +55,4 @@
 - button "29 tok · Cache hit 0%":
   - img
   - text: 29 tokCache hit 0%
+- button "0% of context used": 0%