与日期感知时钟:设计决策与源码实现)
人工智能AI AgentAgent 框架DeepSeek【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址https://gitcode.com/gh_mirrors/de/deepseek-harness点击查看免费下载导读本文基于 DeepSeek Harness 仓库中归档的特性笔记 2026-07-29-web-message-icon-actions-and-clock.zh.md讲解 Web 聊天界面中消息级操作栏复制、分支、时钟的完整设计与实现用户气泡何时展示时钟、assistant 回答何时获得正文下方的复制/分支/时钟行、时间标签如何在跨日与跨年时自动加宽以及流式输出期间为何绝不闪现操作栏。读者读完可掌握这套“挂载即可见、午夜自动刷新、仅轮次尾部内容获得座位”的展示层设计模式以及其在ui-chat包中的真实源码落点。一、问题背景消息 chrome 的不一致与跨午夜失效在引入本特性之前Web 聊天存在三个相互纠缠的展示问题用户气泡有操作栏、assistant 没有。用户气泡一侧已经具备复制、分支、编辑等 IconActions但均不携带时间信息而已定稿的 assistant 叙述下方完全没有操作栏尽管 harness 设计稿要求回答结束后展示复制、分支与时钟。流式输出期间不允许闪现操作栏。若操作栏在逐 token 输出过程中出现会干扰阅读并诱使用户复制半截回答。跨午夜的时间标签会“卡死”。经 memo 优化的消息行在跨午夜后仍保持稳定的 props一次性的Date.now()求值会让昨日消息一直显示为HH:mm而不会加宽为带日期的格式。二、核心决策两条消息行一个共享时钟特性笔记给出的最终决策可以概括为一句话用户气泡在既有 IconActions 行的开头添加感知日期的本地时钟每个轮次中最后一条带 text 内容的 assistant 在正文下追加带margin-top: 16px的复制、分支、时钟两边只要挂载就保持可见并在下一个本地午夜重新格式化。该决策还吸收了另外两条归档决策的约束assistant 一侧的座位由轮次完成状态收紧只有存在turn/end的轮次才授予该操作栏仍在产出步骤streaming的轮次不会把该行交给任何节点见归档笔记archived/bug-fix/2026-08-05-turn-tail-actions-require-a-completed-turn。user 一侧移除分支控件user 行的 IconActions 最终只保留时钟与复制见归档笔记archived/simplification/2026-08-06-user-bubbles-drop-the-branch-action。换句话说两端共用同一个时钟格式化逻辑但在操作种类与布局位置上刻意保持差异user 时钟在前clock: startassistant 时钟在后clock: end且位于分支按钮之后。三、时间标签的三种形态formatMessageClock时钟的全部格式化逻辑集中在 message-chrome.ts核心函数formatMessageClock(time, t, now)是一个纯函数——日期模板经由调用方的 locale seatt注入now默认为墙钟时间。其加宽规则为消息时间与参考时间的关系显示格式示例同一本地日历日HH:mm24 小时制、补零14:24同年但更早跨日M月D日 HH:mm1月1日 14:24跨年YYYY年M月D日 HH:mm2025年12月31日 09:05实现上先取d.getHours()/d.getMinutes()拼出补零的HH:mm再比较年月日判断是否处于同一日历日跨日时根据是否同年选择clock.md月日模板或clock.ymd年月日模板最后把日期前缀拼到时钟前面。该文件的类型签名ClockTranslate Translateclock.md | clock.ymd表明这两种日期模板由会话词典提供组件本身不持有任何硬编码的日期文案。同文件还提供了三个供其他 chrome 复用的格式化工具formatRunDuration运行耗时分钟级显示duration.minutes、秒级显示duration.seconds、formatLatencySeconds亚轮次时延10 秒内保留一位小数、formatTokensPerSecond解码吞吐。它们与时钟一起被MessageIconActions消费用于在时钟旁追加“· Ran for 15s”“· TTFT 1.2s”“· 34 tok/s”等运行统计读数。四、午夜自动刷新组件本地日刻度useCalendarDay跨午夜问题之所以棘手是因为 memo 化的消息行在日历日变化时 props 完全不变React 不会重渲染。解法不是引入全局状态而是组件本地的“日刻度”钩子 use-calendar-day.tsuseState(() startOfLocalDay(Date.now()))在首次挂载时记录“本地午夜毫秒时间戳”startOfLocalDay将Date归零到当日00:00:00useEffect中通过msUntilNextLocalMidnight(now)计算距下一个本地午夜还有多少毫秒用setTimeout定时到边界触发回调中先更新day状态再递归arm下一次定时卸载时clearTimeout清理避免泄漏。由于formatMessageClock接收的是这个“日刻度”而不是直接调用Date.now()日历日翻转时day变化会驱动组件重渲染昨日消息自动从14:24加宽为6月29日 14:24且无需任何消息数据变更。笔记中明确记录了否决项不通过 chat store 或 inject 钩子发布日历日因为日刻度只是展示层本地状态、没有跨入口消费方组件本地 timeout 符合「行为钩子可拥有不订阅外部源的状态」这一客户端规则。五、共享组件MessageIconActions复制、时钟、分支的组装user 与 assistant 两侧共用的操作栏组件位于 MessageIconActions.tsx其 props 设计如下Prop类型语义textstring复制动作写入剪贴板的纯文本timenumber \| undefined时钟标签的事件 Unix 毫秒时间戳瞬态消息省略则不渲染时钟runMs/ttftMs/tokensPerSecondnumber \| undefined轮次运行时长、首 token 时延、解码吞吐均以·分隔附加在时钟后clockstart \| end时钟放在图标前user还是图标后assistantonBranch(() void) \| undefined在该消息处 fork 会话的回调省略则隐藏分支按钮branchUnavailableboolean消息不是已完成轮次尾部时分支按钮保留但不可用aria-disabledextraActionsReactNode由独立插件通过 slot 渲染的额外动作插在复制与分支之间tlocale seat视图的翻译座位作为普通 prop 下传组件行为要点复制调用writeClipboard(text)来自dsh-client-ui-primitives成功后短暂切换为对勾图标约 1 秒后还原期间通过copyPendingref 与copyEpoch纪元号双重门控保证重复点击既不会重复写入也不会堆积定时器卸载时清空挂起的反馈定时器。分支onBranch存在时渲染分支图标按钮branchUnavailable为 true 时保留按钮但禁用点击并附加aria-disabled与aria-describedby提示原生 disabled 按钮无法传递 Tooltip 所需的 hover/focus 事件因此用 JS 禁用而非原生禁用。时钟按clock取值渲染在行首或行尾组件每次渲染都从useCalendarDay()取当前日刻度从而在午夜后自动加宽。六、挂载门控只有轮次尾部的内容回答拥有座位这是整个设计的灵魂——操作栏不是“每条消息都有”而是由三层条件精确门控内容门控只有节点含非空 text 内容时才渲染。纯 Think推理节点、只有工具调用的节点没有可复制的正文不授予操作栏。轮次尾部门控多步骤轮次中工具调用之前的中间叙述不是已定稿答案不授予操作栏只有该轮次中最后一条带 text 内容的 assistant 节点拥有座位。ChatView通过assistantActionsSeqs推导轮次尾部的 seq且不为轮次中间的内容传入time。流式门控AssistantMarkdown仅在streaming为 false、已知事件时间、且节点含非空 text 内容时渲染该行。流式尾部仍在产出步骤不把操作栏交给任何节点杜绝了逐 token 输出期间操作栏闪现。布局上user 侧把时钟标签放在复制之前对应设计稿 figma388:20051assistant 侧把整行放在正文之后、分支按钮之后对应设计稿 figma43:32997并带margin-top: 16px的间距。复制时写入的是拼接后的全部 text 块。两种消息行都把自身事件的seq交给同一个 fork 回调真实的 mutation 契约边界选择、失败行为、切换语义由 Web session fork 操作笔记定义本组件不成为 session mutation 的第二正家。七、被否决的方案与理由笔记忠实记录了五个被否决的备选方案理解它们有助于避免在后续改动中重蹈覆辙备选方案否决理由流式过程中展示 assistant IconActions需求是输出完成后才展示中途 chrome 会闪烁并诱使复制半截回答给每个已定稿 assistant 节点含纯 Think都挂操作栏无 text 内容时复制没有可写内容在每一步/Think 下重复 chrome 会打乱流程给多步骤轮次中每条带 text 的 assistant 都挂操作栏轮次中间的叙述工具调用前的 text不是已定稿答案重复复制、分支、时钟会打乱流程在具备 hover 能力的指针上用 hover 揭示操作行行一旦存在就应保持可发现用 opacity 隐藏容易漏看且需要父级 hover 选择器重复挂载门控由 IconActions 决策同时定义 session fork 语义边界选择、失败行为与切换语义属于独立的 fork 笔记避免展示组件成为 session mutation 的第二正家八、测试验证包级单测与 Web e2e 双重钉住该特性由两层测试钉住确保行为不回归包级组件测试chat-branch-tails.client.spec.tsx 覆盖formatMessageClock的三种时钟形态同日14:24、同年跨日1月1日 14:24、跨年2025年12月31日 09:05午夜加宽mock 时钟到23:50渲染同日时钟推进到下一个本地午夜后断言标签自动加宽user 气泡只暴露时钟/复制且既无分支也无编辑复制写入的是消息 text复制在navigator.clipboard不可用时的execCommand回退、宿主拒绝写入时不显示成功态、成功态 1 秒后还原、卸载时清理定时器轮次尾部 seq 门控与 user/assistant 分支按钮各自传递的事件seq。Web e2emessage-actions.e2e.ts 使用冷 seed 历史 aria golden 钉住组装后的完整操作栏 chromearia 归一化会把每种时钟形态折叠为统一的{{clock}}占位符从而让跨时区、跨日期的运行环境都能得到稳定断言。九、后果与边界该特性落地后形成了清晰的行为契约每个轮次中最后一条已定稿的内容回答在行挂载后立刻暴露复制、分支与事件时钟轮次中间的内容与纯 Think 节点不带任何 chrome。用户与 assistant 时钟共用同一套跨天、跨年加宽规则并在午夜后无需消息变更即可自动刷新。逐消息分页仍是包 README 中记录的暂缓 footer 功能位不属于本特性范围。对于希望深入源码的读者推荐按以下路径继续阅读先从 MessageItem.tsx 看 user 与 assistant 两个挂载点如何传time与seq再对照 AssistantMarkdown.tsx 的streaming门控理解座位授予条件最后回到 message-chrome.ts 与 use-calendar-day.ts 验证纯函数与午夜定时器的实现细节。这一整套「纯格式化函数 组件本地日刻度 三层挂载门控」的模式同样适用于任何需要展示“感知日期的消息时间”且不允许引入全局状态的客户端界面。赞分享人工智能AI AgentAgent 框架DeepSeek【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址https://gitcode.com/gh_mirrors/de/deepseek-harness点击查看免费下载相关推荐DeepSeek Harness 消息操作栏与日期感知时钟Web 对话 IconActions 的源码级实现解析DeepSeek Harness 消息操作栏与日期感知时钟Web 对话 IconActions 的源码级实现解析 在 DeepSeek Harness 的 W人工智能AI AgentAgent 框架DeepSeekTolaria ADR 0032将 Git 操作从侧边栏迁入底部状态栏的设计决策与源码实现Tolaria ADR 0032将 Git 操作从侧边栏迁入底部状态栏的设计决策与源码实现 本文基于 Tolaria一个用于管理 Markdown 知识库的桌面应用知识管理AI 应用MCP 服务ClickHouse-Grafana插件安全配置全解析TLS认证、Yandex Cloud授权和权限管理ClickHouse Grafana插件安全配置全解析TLS认证、Yandex Cloud授权和权限管理 ClickHouse Grafana插件是企业级数据创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考