Skip to content

feat(chat): 输入框下方的会话统计状态栏(桌面端 + WebUI) - #627

Open
AlphaCatMeow wants to merge 18 commits into
Stack-Cairn:mainfrom
AlphaCatMeow:feat/composer-context-stats-bar
Open

feat(chat): 输入框下方的会话统计状态栏(桌面端 + WebUI)#627
AlphaCatMeow wants to merge 18 commits into
Stack-Cairn:mainfrom
AlphaCatMeow:feat/composer-context-stats-bar

Conversation

@AlphaCatMeow

@AlphaCatMeow AlphaCatMeow commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

docs/design/composer-context-stats-bar.md 在输入框正下方新增会话统计状态栏,桌面端(Tauri GUI)与 WebUI 双端接线;并按后续产品决策(设计文档 §4.6)把状态栏与上下文用量环做成严格互斥的两种展示样式,在供应商设置页的高级设置抽屉里一键切换,默认统计状态栏。

状态栏截图

截图为桌面端实机运行态:5 轮 · 84 步 | 上下文 19% | LLM 30m19s · 工具调用 30s | 输入 1049.2万 tok · 输出 7.2万 tok

内容

  • 聚合层 aggregateTrajectoryStats(agent-ui 共享层纯函数):复用 buildTrajectoryLedger 做事件去重收敛,不自造去重逻辑;输出轮/步/时长/token/性能指标与格式化函数。
  • hook 层 useConversationStats:首窗加载 + 后台分页补全 + 模块级 LRU 缓存(8 会话),实时事件流增量合并,authoritativeRevision 变化整体重载;区分 authoritative/observed 两种实时所有权语义。
  • 组件层 ConversationStatsBar:单行淡色小字,容器查询四档收缩(28/40/52rem),运行中 1s 心跳折算实时时长,空态等高占位避免布局跳动;hover 明细 tooltip;上下文占用百分比进恒显分组(与用量环同源同口径),占用 ≥50% 时整条可点、确认后触发手动压缩。
  • i18n:chat.stats.* 中英文案。
  • 硬约束落实:后端零改动(Rust/Go 均不动,新设置字段经既有 opaque JSON 通道持久化与中继);无新增轨迹埋点;agent-ui 共享层不 import 宿主代码。

状态栏与用量环严格互斥(设计文档 §4.6)

开发期先按试用反馈让两者并存(环 hideBelowWarn ≥50% 才浮现、状态栏恒显占用读数),试用后最终决策收敛为严格二态,不保留并存/自动的第三态:

  • 设置字段 customSettings.composerContextDisplay: "statsBar" | "ring":归一化兜底 statsBar,脏值/旧配置一律落回默认;作为全局产品偏好随 gateway 设置同步(不进 syncableCustomSettings 的本地重置清单,与字体/宽度类设备偏好不同),老对端 payload 缺字段时保留本地值不重置;桌面端 localStorage 读取器同口径补齐,防止重启后偏好丢失。
  • 互斥在 ChatComposerBar 内统一强制:两端宿主只透传 contextDisplayMode,环渲不渲染、statsBar 插槽挂不挂载由组件裁决,宿主无法配置出并存形态。statsBar 模式(默认)环整枚不渲染;ring 模式环 0% 起常显(不再挂低占用隐藏门槛——环此时是唯一占用读数),状态栏即使传入也不挂载、数据 hook 不 mount 零拉取。两种样式都保留 ≥50% 的手动压缩入口(同一 canManualCompact 门槛)。
  • 开关 UI:供应商设置页高级设置抽屉新增 Switch(开 = 用量环,关 = 状态栏),中英文案说明两种形态与压缩入口。
  • ContextUsageRinghideBelowWarn 保留为共享环的通用显示选项(DOM 验收测试继续覆盖),仅 composer 挂载点不再使用。
  • 双端已实机验收:桌面端 + 手机浏览器访问 Gateway WebUI(窄屏形态,正是 §4.5 反馈的场景),切换开关两端随设置同步生效。

修复(开发中发现)

  • 插槽 key 碰撞导致 DOM 残留累积(00f5299):statsBar 与 taskProgressBar 同父容器且都用裸会话 id 作 key,React 同键兄弟 keyed diff 下旧 fiber 逃过删除,任务胶囊随切换会话越堆越多。statsBar key 加前缀消除;CDP 实机验证重载后零报错、残留清零。
  • runtime 缓存被旧 React state 踩回(5fc1965):切走会话瞬间用未跟上的镜像 state 回写 registry,导致重开会话 isSending 卡 true。改为缓存条目优先。
  • 新设置字段桌面端持久化缺口(8796981 内):桌面 localStorage 本地设置读取器自建 CustomSettings 字面量,不补新字段会在应用重启后把偏好静默丢回默认——由 tsc 必填字段检查暴露,随功能一并修复。

测试

  • node scripts/run-node-tests.mjs crates/agent-gui/test crates/agent-gateway/web/test:3009 项通过 3002;7 项失败均为本地环境问题(core.autocrlf=true 使源码扫描型断言在 CRLF 工作区必挂 ×5、CWD 相对路径读取 ×1、cargo target 目录锁 ×1),与此前基线清单逐项一致、与本分支改动无关,LF 环境应全绿。
  • 设计文档第 9 节测试计划已落地(conversation-stats-bar / conversation-stats-hook / stats / context-usage / task-progress-indicator / runtime-slot-convergence);§4.6 互斥新增:composerContextDisplay 二态归一化与 gateway 同步透传(normalization)、composer 环/状态栏互斥渲染源码断言(context-usage、conversation-stats-bar)。
  • agent-ui / agent-gui / gateway-webui 三包 tsc 全绿。
image image image image 1787846970641_d

已知问题

  • 上下文占用超 50% 的历史会话重新载入后用量环仍会显示,5fc1965e 未完全根治,继续跟进中,不阻塞本 PR 功能。严格互斥落地后该现象仅在用户主动切到 ring 模式时可见(默认 statsBar 模式不渲染环)。

Closes #628

@StackCairn
StackCairn marked this pull request as draft August 25, 2026 19:17
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

PR governance checks passed. Awaiting human review.

复用 buildTrajectoryLedger 的收敛产物做纯算术聚合,不自造事件去重逻辑。
运行中的 step/tool 回传起点而非折进已完成时长,聚合保持纯函数、可缓存,
由展示层用心跳补齐。单测钉死黄金样例、幂等回放与格式化边界。
- loadWindow 拉首窗后在空闲回调里向前分页补全,页页并入同一 ledger
- live 事件 1s 节流合并,避免运行中每事件重算
- 模块级 LRU 缓存 8 会话,authoritativeRevision 变化时整体重载
- 50k 事件挡板:超限停止补全并标记读数为近似值
按设计文档第 6 节对照表补齐 12 个 key;先于组件落地,组件与测试直接
使用真实文案。
单行淡色小字,自带 @container 四档收缩(恒显轮·步,28/40/52rem 逐级
展开时长、token、性能分组);运行中 1s 心跳把 RunningSinceAt 折算进
显示值,空闲零定时器;空态与全零读数返回 null 不占高度;容器
role="status" + aria-label 拼完整文本,aria-live 关闭避免流式刷屏。
账本把崩溃遗留的 step/tool 收敛成 aborted 时不补 endedAt,原先按
endedAt === null 判定运行段,会让已死会话的时长随心跳无限增长。改为只
认 status === "running"。

同时给 useConversationStats 补 liveOwnership(桌面 authoritative /
Web observed),与 TrajectoryView 的中断收敛口径保持一致。
环承担的是「当前上下文占用」与压缩入口,占用低于警戒线(50%,手动压缩
尚不可用)时整枚不渲染;累计读数由下方会话统计状态栏承担。ChatComposerBar
传 true,其他调用方默认行为不变。

隐藏时同步归位 tooltipOpen,避免占用回升后 tooltip 无悬停自动弹开。
桌面 ConversationPaneHost 与 Web GatewayAppView 各加一个薄包装组件,把
共享层的 useConversationStats 接到本端的实时事件源与轨迹宿主上:

- 包装独立成组件而非内联进宿主视图:运行中状态栏每秒重渲染一次,隔在
  这里只重画这一行,不带着整个视图回流
- 桌面 liveOwnership=authoritative(持有权威实时尾巴),Web 用 observed
  (页面刚重载时不把运行中的回合误判为中断)
- 轨迹视图挂起输入区时 enabled=false,不做无谓拉取
- key 绑 conversationId,切会话时丢弃上一会话的心跳与节流状态

顺带修正 StatGroup 字面量的类型注解位置:注解写在 .filter() 结果上会让
minWidth 先宽化成 string。
设计文档 §4.2 的两个可选增强:

- 整条可点击跳到该会话的轨迹视图。桌面经 ConversationPaneTrajectory.onOpen
  注入,Web 用 setActiveConversationView;两端都与各自的轨迹入口同门槛
  (无 assistant 回复时不给入口,否则是死点击:状态写进去但视图被门控在
  conversation,等回复到达再突然跳走)
- hover 出 tooltip,给出被容器查询收缩掉的分组,外加只在此处露出的压缩
  次数与 ≈ 释义(新增 chat.stats.approximateHint)

未接线时仍是纯展示,不渲染按钮。分组加 data-stats-group,收缩档位断言
不再依赖 DOM 层级——整条可点击时会多包一层 button。
首条 assistant 回复落地前统计恒为空;此前空态返回 null 会让 composer
在统计浮现的瞬间整体上移一次。改为渲染同高度的空占位容器(h-5,
aria-hidden,不带 role=status),稳态下视觉不可见,消除跳动。
@AlphaCatMeow
AlphaCatMeow force-pushed the feat/composer-context-stats-bar branch from 5fc1965 to b515de8 Compare August 25, 2026 19:29
@AlphaCatMeow
AlphaCatMeow marked this pull request as ready for review August 25, 2026 19:29
@StackCairn
StackCairn marked this pull request as draft August 25, 2026 19:29
强制分包配置(minSize/maxSize)会把体积不足的 chunk 与其他模块图合并,
不保证跨来源模块顶层副作用按 import 顺序执行,在生产构建下先后触发
"Class extends value undefined"(gatewayTerminalStreamClient.ts 里
GatewayTerminalStreamHandle 跨包 extends TerminalStreamBuffer)和
streamdown 包内 CJS-interop 样板代码顶层调用崩溃,导致 #root 挂载后
零子节点、整个应用白屏。

开启 rolldown 的 strictExecutionOrder 从 bundler 层面根治这类顺序
竞争;同时把 gatewayTerminalStreamClient.ts 里的类声明改为惰性工厂,
避免其 extends 求值依赖跨 chunk 初始化时序。
按用户反馈修订 §4.5 语义分工:上下文占用百分比进状态栏恒显档(与用量环
同源同口径),窄屏下轮·步之外仍能看到上下文信息;占用 ≥50% 时整条可点,
经确认弹层触发手动压缩(门槛与 ContextUsageRing 一致)。相应移除上一版
的轨迹跳转入口,双端宿主改传 contextUsageTokensSource/contextWindow/
onManualCompactConfirm。
statsBar 与 taskProgressBar 两个插槽元素同挂在 composer 层同一父容器下,
且都以裸会话 id 作 key。React 对同键兄弟做 keyed diff 时,复用 map 中先
写入的旧 fiber 会被后写入的覆盖,既不被复用也不进删除队列,其宿主 DOM
就地残留;审批栏出现/消失与切换会话反复触发该路径,残留逐次累积——表现
为任务胶囊在 composer 上方越堆越多且切走不消失(伴随控制台
"Encountered two children with the same key" 报错)。给 statsBar 的
key 加 stats- 前缀消除碰撞,保留按会话重挂载语义;CDP 实机验证重载后
130 秒零报错、fiber 树无同键兄弟、残留清零。
buildRuntimeEntryFromVisibleState 回写 registry 缓存时,
compactionStatus/isSending/errorMessage/hookWarning 取的是镜像 React
state;后台发送/压缩已把缓存条目更新,而镜像 state 要到下一次渲染才跟上,
切走会话的瞬间用旧值把缓存踩回去——重新打开该会话时 isSending 卡 true、
用量环该隐藏不隐藏。改为缓存条目优先,React state 仅作无缓存时的兜底。
- agentDebug 的 SAFE_TOKEN_METADATA_KEYS 放行 fixedTokens/observedTokens/
  trailingTokens 等账本与请求侧计数字段,供上下文占用诊断日志使用
- agent-runner 补断言:onRequestStart 观察者必须拿到与流式请求完全一致的
  过滤后 provider 上下文(含跨轮激活的延迟工具)
产品决策(严格二态,docs §4.6):§4.5 的「环与状态栏并存」不再保留,
二者改为互斥的两种展示样式,默认统计状态栏。

- 设置:customSettings.composerContextDisplay("statsBar"|"ring"),
  归一化兜底 statsBar,脏值/旧配置不留第三态;作为全局偏好随 gateway
  同步(不进 syncableCustomSettings 重置清单),老对端 payload 缺字段时
  保留本地值;桌面 localStorage 读取器同口径补齐,防重启丢偏好
- 互斥在 ChatComposerBar 内统一强制:宿主只透传 contextDisplayMode,
  ring 模式环常显(0% 起,不再传 hideBelowWarn——环是唯一读数)且
  statsBar 插槽不挂载;statsBar 模式环整枚不渲染。两种样式都保留
  ≥50% 的手动压缩入口
- ContextUsageRing 的 hideBelowWarn 保留为共享环通用显示选项
- 开关 UI:ProvidersSection 高级设置抽屉 Switch(开=用量环),中英文案
- 接线:桌面 ChatPage 两处 composer 绑定 + WebUI GatewayAppView 各一行;
  ConversationPaneHost 与两端 ConversationStatsBarHost 零改动
- 测试:归一化二态/同步透传新用例;composer 源码断言改为互斥语义
AlphaCatMeow and others added 2 commits August 27, 2026 20:31
…xt-stats-bar

# Conflicts:
#	crates/agent-ui/src/pages/settings/ProvidersSection.tsx
设置抽屉里"用量环替代统计状态栏"的互斥 Switch 换成三档状态滑块,
从左到右:状态栏 → 都显示 → 用量环。新增中档让状态栏与常显用量环
并存,左右两档行为与原关/开完全一致,默认仍是状态栏。

- 设置模型:ComposerContextDisplayMode 扩展为 "statsBar" | "both" | "ring";
  两处归一化(agent-ui index.ts、agent-gui storage.ts)接受三值,脏值
  (含曾设想的 "auto")仍落回 statsBar;gateway 同步透传零改动,
  老对端 payload 缺字段时保留本地值
- 滑块控件:新增通用 SegmentedSlider(components/ui/segmented-slider.tsx),
  等宽分段 + 滑动指示块,底层同名原生 radio,方向键换档、Tab 进出
  分组由浏览器原生承担;ProvidersSection 抽屉替换 Switch,中英文案
  按三档语义重写(标题改为「上下文占用展示」,新增三个档位标签 key)
- 渲染行为:ChatComposerBar 环渲染条件放宽为 "ring" || "both",statsBar
  插槽维持 !== "ring" 挂载,"both" 自然同时命中两分支;三档都保留
  ≥50% 的手动压缩入口;两宿主(ChatPage/GatewayAppView)零改动
  继续只透传,WebUI 经共享 SettingsPage/ChatComposerBar 自动同步
- 配套:normalization / context-usage / conversation-stats-bar 测试改为
  三态断言;设计文档新增 §4.7 修订小节并同步文件表/边界表/测试计划;
  types / ProvidersSection / ChatComposerBar / ContextUsageRing 里的
  "严格二态/互斥"注释一并更新

验证:normalization 137 + context-usage/conversation-stats-bar 52 用例通过;
pnpm typecheck:ui、agent-gui tsc、webui tsc 干净;test:webui 631 全绿;
改动文件 biome lint 规则通过(formatter 全仓 CRLF 存量问题除外)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(chat): 输入框下方的会话统计状态栏(桌面端 + WebUI)

1 participant