paseo-plugin-chat-view 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE.compact-agent-activity +21 -0
  3. package/LICENSE.mermaid +21 -0
  4. package/README.md +428 -2
  5. package/README_zh.md +429 -0
  6. package/client/activity/Activity.test.ts +220 -0
  7. package/client/activity/Activity.tsx +784 -0
  8. package/client/activity/detail/detail.test.ts +102 -0
  9. package/client/activity/detail/dispatch.tsx +94 -0
  10. package/client/activity/detail/file.tsx +93 -0
  11. package/client/activity/detail/index.tsx +39 -0
  12. package/client/activity/detail/parts.tsx +221 -0
  13. package/client/activity/detail/search.tsx +59 -0
  14. package/client/activity/detail/terminal.tsx +53 -0
  15. package/client/activity/detail/texts.tsx +42 -0
  16. package/client/activity/disclosure.ts +49 -0
  17. package/client/activity/dom.test.ts +52 -0
  18. package/client/activity/dom.ts +138 -0
  19. package/client/activity/group-store.test.ts +126 -0
  20. package/client/activity/group-store.ts +265 -0
  21. package/client/activity/highlight.test.ts +116 -0
  22. package/client/activity/highlight.ts +418 -0
  23. package/client/activity/history.test.ts +602 -0
  24. package/client/activity/history.ts +239 -0
  25. package/client/activity/index.ts +74 -0
  26. package/client/activity/latest-marker.test.ts +41 -0
  27. package/client/activity/latest-marker.ts +70 -0
  28. package/client/activity/styles.ts +535 -0
  29. package/client/activity/tools/child-agent.tsx +308 -0
  30. package/client/activity/tools/exa.tsx +99 -0
  31. package/client/activity/tools/github.tsx +501 -0
  32. package/client/activity/tools/list.tsx +102 -0
  33. package/client/activity/tools/lists.test.ts +193 -0
  34. package/client/activity/tools/paseo/agent.tsx +384 -0
  35. package/client/activity/tools/paseo/browser.tsx +322 -0
  36. package/client/activity/tools/paseo/index.tsx +79 -0
  37. package/client/activity/tools/paseo/parts.tsx +133 -0
  38. package/client/activity/tools/paseo/provider.tsx +220 -0
  39. package/client/activity/tools/paseo/schedule.tsx +246 -0
  40. package/client/activity/tools/paseo/speak.tsx +20 -0
  41. package/client/activity/tools/paseo/terminal.tsx +214 -0
  42. package/client/activity/tools/paseo/workspace.tsx +162 -0
  43. package/client/activity/tools/shared.tsx +431 -0
  44. package/client/activity/transform.test.ts +28 -0
  45. package/client/activity/transform.ts +43 -0
  46. package/client/agent-pills.ts +56 -0
  47. package/client/dom.ts +75 -0
  48. package/client/enhance/controller.ts +111 -0
  49. package/client/enhance/dom.test.ts +28 -0
  50. package/client/enhance/dom.ts +101 -0
  51. package/client/enhance/index.ts +95 -0
  52. package/client/enhance/pill.tsx +19 -0
  53. package/client/enhance/state.ts +58 -0
  54. package/client/feature.ts +22 -0
  55. package/client/features.test.ts +149 -0
  56. package/client/features.ts +83 -0
  57. package/client/locale.ts +45 -0
  58. package/client/mermaid/diagram.tsx +438 -0
  59. package/client/mermaid/index.ts +27 -0
  60. package/client/mermaid/item.tsx +28 -0
  61. package/client/mermaid/markdown.tsx +217 -0
  62. package/client/mermaid/registration.ts +32 -0
  63. package/client/mermaid/toolbar.tsx +175 -0
  64. package/client/mermaid/viewer.tsx +314 -0
  65. package/client/readonly/dom.test.ts +66 -0
  66. package/client/readonly/dom.ts +32 -0
  67. package/client/readonly/index.ts +27 -0
  68. package/client/registration.test.ts +243 -0
  69. package/client/settings.tsx +438 -0
  70. package/client/sticky/StickyBar.tsx +144 -0
  71. package/client/sticky/dom.test.ts +230 -0
  72. package/client/sticky/dom.ts +517 -0
  73. package/client/sticky/index.ts +38 -0
  74. package/client/sticky/pill.tsx +70 -0
  75. package/client/timeline-pager.test.ts +121 -0
  76. package/client/timeline-pager.ts +123 -0
  77. package/index.client.tsx +44 -0
  78. package/index.server.ts +12 -0
  79. package/package.json +44 -4
  80. package/paseo-plugin.json +6 -0
  81. package/server/enhance/index.ts +63 -0
  82. package/server/enhance/manifest.ts +176 -0
  83. package/server/enhance/probe.test.ts +215 -0
  84. package/server/enhance/probe.ts +118 -0
  85. package/server/enhance/provider.ts +149 -0
  86. package/shared/activity/child-agent.test.ts +27 -0
  87. package/shared/activity/child-agent.ts +26 -0
  88. package/shared/activity/diff.test.ts +74 -0
  89. package/shared/activity/diff.ts +207 -0
  90. package/shared/activity/exa.test.ts +66 -0
  91. package/shared/activity/exa.ts +183 -0
  92. package/shared/activity/file-kind.test.ts +13 -0
  93. package/shared/activity/file-kind.ts +112 -0
  94. package/shared/activity/github.test.ts +56 -0
  95. package/shared/activity/github.ts +174 -0
  96. package/shared/activity/palette.test.ts +26 -0
  97. package/shared/activity/palette.ts +106 -0
  98. package/shared/activity/parse.test.ts +39 -0
  99. package/shared/activity/parse.ts +115 -0
  100. package/shared/activity/paseo-tools.test.ts +163 -0
  101. package/shared/activity/paseo-tools.ts +497 -0
  102. package/shared/activity/text.test.ts +85 -0
  103. package/shared/activity/text.ts +202 -0
  104. package/shared/activity/timeline.test.ts +26 -0
  105. package/shared/activity/timeline.ts +108 -0
  106. package/shared/activity/tool-presentation.test.ts +154 -0
  107. package/shared/activity/tool-presentation.ts +352 -0
  108. package/shared/dependencies.test.ts +53 -0
  109. package/shared/enhance/contract.ts +105 -0
  110. package/shared/enhance/decide.ts +97 -0
  111. package/shared/enhance/enhance.test.ts +188 -0
  112. package/shared/enhance/failure.ts +40 -0
  113. package/shared/enhance/index.ts +8 -0
  114. package/shared/enhance/normalize.ts +16 -0
  115. package/shared/enhance/parse.ts +79 -0
  116. package/shared/enhance/policy.ts +48 -0
  117. package/shared/enhance/prompt.ts +134 -0
  118. package/shared/enhance/run.ts +84 -0
  119. package/shared/i18n.test.ts +81 -0
  120. package/shared/i18n.ts +300 -0
  121. package/shared/mermaid/flowchart.test.ts +169 -0
  122. package/shared/mermaid/flowchart.ts +271 -0
  123. package/shared/mermaid/layout.test.ts +305 -0
  124. package/shared/mermaid/layout.ts +496 -0
  125. package/shared/mermaid/parse.test.ts +110 -0
  126. package/shared/mermaid/parse.ts +193 -0
  127. package/shared/mermaid/segment.test.ts +55 -0
  128. package/shared/mermaid/segment.ts +95 -0
  129. package/shared/mermaid/sequence.test.ts +68 -0
  130. package/shared/mermaid/sequence.ts +130 -0
  131. package/shared/mermaid/transform.test.ts +52 -0
  132. package/shared/mermaid/transform.ts +45 -0
  133. package/shared/mermaid/zoom.test.ts +134 -0
  134. package/shared/mermaid/zoom.ts +114 -0
  135. package/shared/settings.ts +41 -0
  136. package/shared/sticky/history.test.ts +91 -0
  137. package/shared/sticky/history.ts +86 -0
package/README_zh.md ADDED
@@ -0,0 +1,429 @@
1
+ # Paseo Plugin: chat-view
2
+
3
+ [English](README.md) | [简体中文](README_zh.md)
4
+
5
+ > 专注于提升长对话阅读与调试效率的 Paseo 界面增强插件:提供**当前轮次提问智能吸顶**、**思考与工具调用紧凑聚合**以及 **Mermaid 图表交互式渲染**。
6
+
7
+ ---
8
+
9
+ ## 目录
10
+
11
+ - [核心特性概览](#核心特性概览)
12
+ - [平台支持与兼容性矩阵](#平台支持与兼容性矩阵)
13
+ - [宿主配置前置要求](#宿主配置前置要求)
14
+ - [快速开始](#快速开始)
15
+ - [安装与加载](#安装与加载)
16
+ - [日常维护命令](#日常维护命令)
17
+ - [功能模块详解](#功能模块详解)
18
+ - [1. 提问智能吸顶 (Sticky Questions)](#1-提问智能吸顶-sticky-questions)
19
+ - [2. 紧凑活动聚合 (Compact Activity)](#2-紧凑活动聚合-compact-activity)
20
+ - [3. Mermaid 交互图表 (Mermaid Diagrams)](#3-mermaid-交互图表-mermaid-diagrams)
21
+ - [4. 提示词增强 (Prompt Enhancement)](#4-提示词增强-prompt-enhancement)
22
+ - [插件配置指南](#插件配置指南)
23
+ - [架构设计与技术边界](#架构设计与技术边界)
24
+ - [项目结构](#项目结构)
25
+ - [本地开发与测试](#本地开发与测试)
26
+ - [致谢与开源许可](#致谢与开源许可)
27
+
28
+ ---
29
+
30
+ ## 核心特性概览
31
+
32
+ | 功能模块 | 说明 | 交互亮点 |
33
+ | :---------------------- | :------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
34
+ | 📌 **智能提问吸顶** | 在长对话滚动浏览时,将当前阅读轮次的用户提问固定在视口顶部。 | 100ms 视口防抖、原生视觉继承、默认三行自适应折叠、一键复制全文。 |
35
+ | ⚡ **紧凑活动展示** | 默认按 compact-agent-activity 的 Folded 模式显示单行活动摘要,点击后直接展开思考与工具详情。 | 平滑思考文字、点击展开命令输出、代码 Diff 与首帧语法高亮。 |
36
+ | 📊 **Mermaid 图表渲染** | 内置 Mermaid 解析与渲染引擎,自动将代码块渲染为高质量可视化图表。 | 支持 25%–400% 自由缩放、自适应宽度、源码/图表无缝切换、全屏弹窗预览。 |
37
+ | ✨ **提示词增强** | 把输入框里的模糊诉求改写为「目标 / 范围 / 约束 / 验收」四要素完整的可执行提示词并回填。 | 一键增强、结合当前项目环境、再次点击可撤销原文。 |
38
+
39
+ > [!NOTE]
40
+ > **设计哲学**:保持零后端侵入,严格基于 Paseo 公开 SDK 构建;用户提问与普通助手回答均完整保留 Paseo 原生分叉与复制功能。
41
+
42
+ ---
43
+
44
+ ## 平台支持与兼容性矩阵
45
+
46
+ | 平台 / 运行环境 | 智能提问吸顶 | 紧凑活动聚合 | Mermaid 图表渲染 | 体验增强细节 |
47
+ | :------------------------------ | :----------: | :----------: | :--------------: | :-------------------------------------------------------------- |
48
+ | **桌面端 (Electron / Desktop)** | ✅ 支持 | ✅ 支持 | ✅ 支持 | DOM 级行间距紧缩、图片自适应约束(最大 440 × 320) |
49
+ | **Web 浏览器** | ✅ 支持 | ✅ 支持 | ✅ 支持 | 完整的视口监测与 DOM 交互能力 |
50
+ | **原生移动端 (iOS / Android)** | ⏸️ 暂不支持 | ✅ 支持 | ✅ 支持\* | 基于标准 React Native 构建;无 DOM 行距微调(真机待进一步验收) |
51
+
52
+ > [!WARNING]
53
+ > **插件互斥提醒**:
54
+ > 请勿与 `compact-agent-activity`、`reasoning-display` 或独立的 `paseo-plugin-mermaid` 同时启用,以免多重消息转换器互相冲突或覆盖。
55
+
56
+ ---
57
+
58
+ ## 宿主配置前置要求
59
+
60
+ 为了让紧凑活动聚合正常解析工具调用的完整参数与执行结果,必须调整 Paseo 宿主设置:
61
+
62
+ > [!IMPORTANT]
63
+ > 请前往 Paseo 宿主配置,将 **Tool call detail** 设置为 **Detailed**。<br>
64
+ > _若设置为 `Overview`,宿主系统会在插件接管前预先合并工具项,导致紧凑活动详情无法正确采集数据。_
65
+
66
+ ---
67
+
68
+ ## 快速开始
69
+
70
+ ### 安装与加载
71
+
72
+ 本插件为本地源码包,通过 Paseo CLI 直接安装:
73
+
74
+ ```bash
75
+ # 1. 本地安装插件(指定当前目录或插件绝对路径)
76
+ paseo plugin install .
77
+ # 或:paseo plugin install /path/to/paseo-plugin-chat-view
78
+
79
+ # 2. 检查安装与运行状态
80
+ paseo plugin ls chat-view
81
+ ```
82
+
83
+ 安装完成后,打开或刷新任意 Agent 对话窗口即可生效。
84
+
85
+ ### 日常维护命令
86
+
87
+ - **热重载插件**(修改源码后即时生效,**严禁重启 6767 宿主守护进程**):
88
+ ```bash
89
+ paseo plugin reload chat-view
90
+ ```
91
+ - **查看插件列表**:
92
+ ```bash
93
+ paseo plugin ls
94
+ ```
95
+
96
+ ---
97
+
98
+ ## 功能模块详解
99
+
100
+ ### 1. 提问智能吸顶 (Sticky Questions)
101
+
102
+ 在查看长篇助手回复时,用户无需频繁往上回滚即可随时查看当前上下文对应的提问。
103
+
104
+ ```
105
+ +--------------------------------------------------------------+
106
+ | [用户提问内容摘要 (最多3行)...] [更多... / 📋] | <- 吸顶栏 (与正文等宽)
107
+ +--------------------------------------------------------------+
108
+ | (当前轮次的助手回答正文 / 工具执行记录持续滚动...) |
109
+ | ... |
110
+ ```
111
+
112
+ - **生命周期与控制开关**:
113
+ - 功能设置中提供按主机保存的「吸顶提问」总开关,默认开启;禁用时同时移除对话输入区的同名胶囊,重新开启后恢复。胶囊可临时关闭或重新启用单个对话,总开关优先。
114
+ - 吸顶逻辑依托于开关组件挂载,独立于助手回答的渲染周期。
115
+ - **触发与切换判定**:
116
+ - 当该轮用户提问**完全滚出视口顶部**时触发吸顶。
117
+ - 若提问正文有**任何像素残留于视口**中,吸顶条立即隐藏。
118
+ - 滚动进入下一轮对话时自动切换为下一轮提问;回滚向上浏览时智能恢复上一轮。
119
+ - 引入 **约 100ms 不可见状态确认机制**,彻底杜绝虚拟列表重绘或临界滚动时的画面闪烁。
120
+ - 浏览至会话最底部时规则保持一致:仅在提问离开视口后才维持置顶。
121
+ - **视觉风格与自适应**:
122
+ - 吸顶条宽度与正文完全对齐,完全继承原生用户气泡的背景色、圆角、内边距和字体,无缝适应浅色/深色主题。
123
+ - **自适应折叠**:默认最多显示 3 行正文,超出内容展示「更多…」操作;点击展开后支持在区域内部独立滚动,并可点击「收起」;切换轮次时自动重置为收起状态。
124
+ - **无损全文复制**:区域右下角内嵌复制图标,点击可复制该轮提问的完整文本(含折叠隐藏部分);悬停轻微高亮,复制成功后切换为对勾图标反馈,约 1.8 秒后自动复原。
125
+
126
+ ### 2. 紧凑活动聚合 (Compact Activity)
127
+
128
+ 活动展示按 [cnaron/compact-agent-activity](https://github.com/cnaron/compact-agent-activity) 的 Folded / Detailed 模式迁移,源码基准为 `4866482961c4c43a2f5f102fd63eadff85e15ef2`。默认 Folded 从活动开始即展示单行摘要,完成后不切换为“用时”或二级命令分组;展开后直接显示思考正文及工具详情。Detailed 保留上游逐项卡片、状态图标、输出、Diff 和结构化工具详情。旧 Codex 设置读取时兼容迁移为 Folded,配色与功能开关继续保留。用户提问与普通助手回答继续使用原生 Markdown、复制及分叉操作。
129
+
130
+ 迁移保留上游摘要、字体、颜色、详情树与整行点击交互,并针对公开 SDK 和虚拟列表做稳定性适配:按连接与 Agent 隔离历史,流式活动保持行身份,展开选择在行重挂载后保留,首帧直接生成语法高亮;恢复订阅时保留缓存直到确认历史 epoch 变化。历史首次读取尚未完成或无法匹配时显示收起的单项摘要,避免先闪现整块详情。DOM 只放在各功能域自己的 `dom.ts`,间距与图片尺寸使用声明式 CSS,不在挂载后遍历并调整活动节点的外边距。
131
+
132
+ 将 Agent 执行过程中密集的“思考链”与“工具调用”从瀑布流精简为高度紧凑的信息胶囊:
133
+
134
+ ```
135
+ Thought · Ran 17 commands · Edited 1 file [▼ 点击展开详情]
136
+ ```
137
+
138
+ - **聚合摘要与详情展开**:
139
+ - 连续思考与工具操作折叠为单行摘要条;
140
+ - 单击即可原位展开完整面板,查看结构化思考过程、命令与输出终端、语法高亮展示、文件修改 Diff、错误异常栈以及子 Agent 派发详情。
141
+ - **天然消息边界隔离**:
142
+ - 一旦遇到助手文本回答、用户提问或其他可见节点,当前活动分组立即封口,详情绝不跨消息错位聚合。
143
+ - **高性能与隔离机制**:
144
+ - 按 Paseo 会话连接与 Agent 实例双重隔离;
145
+ - 基于公开时间线接口按需向前加载历史;若连接尚未就绪或正在拉取,自动降级为逐项明细模式;
146
+ - 流式活动**历史读取**引入 **100ms 聚合窗口**,合并高频请求;渲染不做延后,而是靠行标识在流式增长期间保持稳定、折叠内容不进渲染路径来避免多余重排。超长输出(超过 12000 字符)自动退化为纯文本,避免逐帧全量重新分词。
147
+ - 思考文字连续增长时保留当前折叠分组;虚拟列表回收活动行后保留已加载的分组、暂停订阅,重新挂载时直接复用,减少行高变化造成的滚动拉扯。
148
+ - 订阅恢复期间保留分组,读取到相同历史内容时不重建分组或通知渲染;首次历史读取完成后才开始向前分页,避免重复请求。
149
+ - **桌面与 Web 专属微调**:
150
+ - 精细收敛对话消息与段落间距(助手消息垂直内边距缩减为 4px、段落间距优化为专业 8px 并自动消除尾部双重留白,列表/代码块/引用按黄金比例紧凑排列);
151
+ - 压缩折叠活动行外间距与提问衔接间隙,提升连续思考与工具输出的信息密度与可读性;
152
+ - 将对话内插入的图片尺寸智能限制在最大 440 × 320,保持流式版面工整(不影响点击后的全屏看图功能)。
153
+
154
+ ### 3. Mermaid 交互图表 (Mermaid Diagrams)
155
+
156
+ 完整吸收上游 [paseo-plugin-mermaid](https://github.com/dutchakdev/paseo-plugin-mermaid) 0.2.0 的渲染实现,原生赋能 Agent 输出可视化图表的能力。
157
+
158
+ - **动态交互控制**:
159
+ - 自动拦截并转换助手回答中的 ````mermaid` 代码块;
160
+ - 支持流式输出过程中渐进式呈现;
161
+ - 内置交互工具条:支持 **25%–400% 自由缩放**、**适应宽度**、**全屏弹窗浏览**以及**图表 / 源码视图一键切换**。
162
+ - **语法支持范围**:
163
+ - **全面支持**:`flowchart` / `graph`(支持 `TD`, `TB`, `BT`, `LR`, `RL` 全部方向,含**顶层子图(Subgraphs)分组框**)以及 `sequenceDiagram`(包括常用节点几何形状、连线箭头、文本标签、参与者 Participants、角色 Actors、行内注释 Notes 等)。
164
+ - **暂不支持**:嵌套子图(折叠进外层分组框渲染)、`style` / `classDef` 自定义样式及其他非常用图表类型。
165
+ - **优雅降级**:解析异常或遇不支持语法时,自动回退显示原始源码,并提供辅助错误提示。
166
+ - **接管行为说明**:
167
+ - 插件仅针对**含有 Mermaid 代码块的助手回答**进行接管渲染;
168
+ - 接管后的该条消息采用上游轻量级 Markdown 引擎渲染(支持标题、段落、列表、**表格(含对齐与单元格内强调)**、代码块、引用等),**不再提供 Paseo 原生分叉与复制按钮**(纯文本普通回答则不受任何影响)。
169
+
170
+ ---
171
+
172
+ ### 4. 提示词增强 (Prompt Enhancement)
173
+
174
+ 把输入框里的模糊诉求(如「加个缓存」)自动改写为包含**目标、范围、约束、验收**四要素的可执行提示词,并结合当前项目环境补充真实的技术栈与验证命令。
175
+
176
+ - **两个入口**:
177
+ - **composer 轨道 pill**(全平台):位于输入框上方的插件轨道的「增强」按钮,与「吸顶提问」并列;
178
+ - **`/enhance <原始提示词>`**(全平台):宿主未开放草稿读写 API,原生端与命令行走此入口。
179
+ - **再次点击即撤销**:回填后按钮变为撤销图标,点击恢复增强前的原文;增强结果以 `origin` 标记,不会被二次增强。
180
+ - **环境适配**:服务端只读采集项目清单文件(`package.json` / `Cargo.toml` / `go.mod` 等)、脚本命令、依赖框架、tsconfig strict、Git 改动文件与 `README` / `AGENTS` / `CLAUDE` 等说明文档,作为上下文注入提示词。**验收标准只能引用仓库里真实存在的命令**,探测不到时改为可人工核对的现象。
181
+ - **判定先行**:原文已是清晰可执行的指令时原样返回(`applied=false`),不做同义改写造句。原文本身已超过 200 字时同样保持原样——增强结果不可能比用户已写的内容更短。
182
+ - **长度上限 200 字**:改写正文硬上限 200 字,四要素各占一行、每项只给结论。上限与原文长度无关,短诉求不会被压缩成残句。
183
+ - **失败可见**:模型超时、端点报错、密钥缺失都会以宿主 toast 提示原因,并保留原文,不会静默失败。
184
+
185
+ > [!NOTE]
186
+ > 提示词增强使用自带的模型端点(设置中配置),不经过 Paseo 宿主账号,因此不产生任何宿主对话记录,也**只发送文本与上述环境画像摘要,不发送源码文件内容**。
187
+
188
+ ---
189
+
190
+ ## 插件配置指南
191
+
192
+ 可在 Paseo 客户端菜单中打开首选项进行个性化配置:
193
+
194
+ **路径**:`设置 (Settings)` → `插件 (Plugins)` → `chat-view` → `功能设置`
195
+
196
+ | 配置项 | 可选项 | 说明 |
197
+ | :------------------------------------- | :------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
198
+ | **Mermaid 图表** | 开启(默认) / 禁用 | 控制插件绘图;禁用后恢复 Paseo 原生消息与图表显示。 |
199
+ | **紧凑活动(compact-agent-activity)** | 开启(默认) / 禁用 | 控制思考与工具调用增强;禁用后恢复原生时间线。 |
200
+ | **吸顶提问** | 开启(默认) / 禁用 | 控制全部对话的吸顶提示;禁用后立即移除,仅桌面与 Web 支持。 |
201
+ | **锁定已发送消息** | 开启(默认) / 禁用 | 发送后隐藏「回退到此消息」,内容不可再改写;仅桌面与 Web 支持。 |
202
+ | **提示词增强** | 开启(默认关闭) / 禁用 | 控制输入框旁的增强按钮与 `/enhance` 命令;需先填写模型端点。 |
203
+ | **API 地址 / API 密钥 / 模型** | 文本 | 任意 OpenAI Chat Completions 兼容端点;密钥存于宿主设置(0600)。 |
204
+ | **测试连接** | 按钮 | 用当前配置发一次最小请求,验证地址、密钥与模型;显示延迟与实际端点。 |
205
+ | **展示模式 (Display Mode)** | `Folded` (默认) / `Detailed` | 选择上游聚合摘要或逐项明细;旧 Codex 设置迁移为 Folded。 |
206
+ | **配色方案 (Color Theme)** | `Vivid` / `Soft` / `High contrast` | 适配不同的终端审美与高对比度无障碍需求。 |
207
+ | **界面语言 (Language)** | `简体中文`(默认) / `English` / `跟随系统` | 插件自有文案的语言:设置页、吸顶条、胶囊与按钮,以及增强流程上报的提示。不是模型的回答语言——回答跟随你的提问语言。`跟随系统` 在浏览器端读取系统语言,原生端回落到简体中文。 |
208
+
209
+ _注:配置项基于 Host 独立持久化存储,已有展示模式与配色配置会保留。当前设置页和打开的对话即时更新;其他客户端的时间线开关最迟约 2 秒同步,无需重启 daemon。紧凑活动禁用时,其展示模式和配色选项暂不可修改;界面语言不受该开关限制,切换后设置页、吸顶条与输入区胶囊同时换词。_
210
+
211
+ _文本字段(API 地址 / 密钥 / 模型)在停止输入约 300ms 后写入,离开字段时立即写入;宿主以内容哈希作为 revision 并拒绝基于旧 revision 的写入,逐字提交会与往返竞争而丢字(见 `client/settings.tsx` 的 `DraftField`)。_
212
+
213
+ ---
214
+
215
+ ## 架构设计与技术边界
216
+
217
+ 1. **零私有侵入原则 (Public SDK Only)**
218
+ - 生产插件完全依赖 `@getpaseo/client`、`@getpaseo/plugin`、`@getpaseo/protocol`(基于 0.10.3 规范),不导入宿主私有源码,不入侵 Paseo Daemon。
219
+ 2. **DOM 增强边界隔离**
220
+ - 宿主 0.10.3 尚未开放原生对话浮层 API。吸顶能力通过 `client/sticky/dom.ts` 对原生滚动区域进行 DOM 注入增强,并借助时间线消息 ID 精确匹配轮次。
221
+ - 所有 DOM 访问被严格收敛在 Web / 桌面端环境下;原生移动端运行逻辑纯粹基于 React Native,无 DOM 污染。
222
+ - DOM 知识按功能域私有:吸顶、紧凑活动、提示词增强各自持有一份 `dom.ts`,互不可见;只有 `client/dom.ts` 共享其中用到的最小 DOM 类型声明。
223
+ 3. **只读会话边界**
224
+ - 历史归档的只读对话窗口因未挂载输入区吸顶控制器,当前暂不展示吸顶条。
225
+
226
+ ---
227
+
228
+ ## 项目结构
229
+
230
+ ### 双轴布局:外层由宿主锁定,内层按功能划分
231
+
232
+ 宿主编译器对插件 import 图里的**每个**模块做归属判定,只有落在下面三类目录(或根级两个入口文件)里的代码才会被编译,否则直接报错 `Plugin modules belong in client/, server/, or shared/`:
233
+
234
+ | 目录 | 归属 | 可用内容 |
235
+ | --------- | ------ | -------------------------------------------------------- |
236
+ | `client/` | 客户端 | React、React Native、hooks、样式、DOM 操作、回调 |
237
+ | `server/` | 服务端 | Node API、文件系统与进程访问、凭据、handler |
238
+ | `shared/` | 双端 | Zod 契约与纯值;**不得**引用 React 组件、hooks、Node API |
239
+
240
+ 因此本项目采用**双轴**结构:外层轴由宿主决定,内层轴(功能域)由本项目决定。四个功能域是 `sticky`(吸顶提问)、`activity`(紧凑活动)、`mermaid`(图表渲染)、`enhance`(提示词增强),每个域在需要它的运行层下各有一个同名目录。
241
+
242
+ ```text
243
+ chat-view/
244
+ ├── paseo-plugin.json 宿主清单(强校验)
245
+ ├── index.client.tsx 固定入口:只做注册接线
246
+ ├── index.server.ts 固定入口:注册 settings 与 provider
247
+ │
248
+ ├── client/ ── 客户端运行时
249
+ │ ├── features.ts 订阅宿主设置与 Agent 更新,把开关与 Agent 分发给各域
250
+ │ ├── settings.tsx 设置页:聚合四域开关与界面语言
251
+ │ ├── locale.ts 运行中的插件语言;唯一写入者是 createFeatureController.apply
252
+ │ ├── feature.ts 域与装配器之间的接口
253
+ │ ├── agent-pills.ts 按 Agent 维护 composer 胶囊的挂载与回收
254
+ │ ├── timeline-pager.ts 基于宿主时间线句柄的向前分页
255
+ │ ├── dom.ts 各域共用的最小 DOM 类型声明
256
+ │ │
257
+ │ ├── sticky/ ── 吸顶提问
258
+ │ │ ├── index.ts createStickyDomain
259
+ │ │ ├── StickyBar.tsx useStickyMessage 挂载吸顶条
260
+ │ │ ├── pill.tsx createStickyPill 与开关状态
261
+ │ │ └── dom.ts 滚动容器探测与吸顶条注入
262
+ │ │
263
+ │ ├── readonly/ ── 锁定已发送消息
264
+ │ │ ├── index.ts createReadonlyDomain(无 React 状态、无 Agent 接线)
265
+ │ │ └── dom.ts 隐藏宿主回退控件的样式表
266
+ │ │
267
+ │ ├── activity/ ── 紧凑活动
268
+ │ │ ├── index.ts createActivityDomain
269
+ │ │ ├── Activity.tsx 时间线行渲染(折叠组 / 详情面板)
270
+ │ │ ├── styles.ts 调色板与样式推导(无入边,勿反向依赖)
271
+ │ │ ├── history.ts 历史向前分页
272
+ │ │ ├── group-store.ts 折叠分组仓储
273
+ │ │ ├── disclosure.ts 展开 / 收起状态
274
+ │ │ ├── transform.ts 时间线转换器
275
+ │ │ ├── highlight.ts 首帧语法高亮
276
+ │ │ ├── latest-marker.ts 「最新条目」标记的延迟通知
277
+ │ │ ├── dom.ts 作用域样式表与滚动定位
278
+ │ │ ├── detail/ 折叠行正文,按 detail 类型各一个模块
279
+ │ │ │ ├── index.tsx DetailBody 分发
280
+ │ │ │ ├── parts.tsx 标签、路径行、高亮代码、Diff 块
281
+ │ │ │ ├── file.tsx read / write / edit
282
+ │ │ │ ├── search.tsx search / fetch
283
+ │ │ │ ├── terminal.tsx shell / worktree setup
284
+ │ │ │ ├── texts.tsx sub-agent / 纯文本 / plan
285
+ │ │ │ └── dispatch.tsx 未知 detail 的回退
286
+ │ │ └── tools/ 工具详情面板(第四层)
287
+ │ │ ├── shared.tsx 跨工具族复用的展示积木与取值助手
288
+ │ │ ├── list.tsx 共用的列表行渲染
289
+ │ │ ├── exa.tsx Exa 搜索工具族
290
+ │ │ ├── github.tsx GitHub 工具族
291
+ │ │ ├── child-agent.tsx 子 Agent 派发
292
+ │ │ └── paseo/ Paseo 工具族,按工具组各一个模块
293
+ │ │ (index · parts · agent · browser · provider · schedule · speak · terminal · workspace)
294
+ │ │
295
+ │ ├── mermaid/ ── Mermaid 图表
296
+ │ │ ├── index.ts createMermaidDomain
297
+ │ │ ├── registration.ts 宿主渲染器与转换器注册
298
+ │ │ ├── item.tsx 时间线条目
299
+ │ │ ├── markdown.tsx 轻量 Markdown 渲染
300
+ │ │ ├── diagram.tsx 图表绘制
301
+ │ │ ├── viewer.tsx 全屏预览
302
+ │ │ └── toolbar.tsx 缩放 / 适应宽度 / 源码切换
303
+ │ │
304
+ │ └── enhance/ ── 提示词增强
305
+ │ ├── index.ts createEnhanceDomain
306
+ │ ├── controller.ts 增强流程编排
307
+ │ ├── state.ts 跨会话的增强状态
308
+ │ ├── pill.tsx composer 轨道按钮
309
+ │ └── dom.ts 输入框探测与按钮注入
310
+ │
311
+ ├── server/ ── 服务端运行时(仅增强域需要 Node)
312
+ │ └── enhance/
313
+ │ ├── index.ts registerEnhance
314
+ │ ├── provider.ts 模型端点调用
315
+ │ ├── probe.ts 仓库环境探测
316
+ │ └── manifest.ts 清单文件读取
317
+ │
318
+ └── shared/ ── 双端共享
319
+ ├── settings.ts 宿主级设置契约(四域共用;宿主只接受单一契约,拆开会把一次写入变成跨模块协调,故不拆)
320
+ ├── i18n.ts 双语词表、`t()` 插值,以及增强 RPC 携带的 `localeSchema`
321
+ ├── sticky/history.ts PromptHistory 轮次数据
322
+ ├── activity/
323
+ │ ├── palette.ts 主题 token → 共用的 ActivityPalette
324
+ │ ├── tool-presentation.ts 时间线条目 → 标题、图标、分类、摘要
325
+ │ ├── text.ts 文本预算、宽松 JSON 格式化、任意值转文本
326
+ │ ├── diff.ts 行级差异与增删统计
327
+ │ ├── file-kind.ts 路径 → 语言、文件图标
328
+ │ ├── paseo-tools.ts Paseo 工具名 → 分类
329
+ │ ├── timeline.ts 渲染器 kind 与 schema
330
+ │ ├── parse.ts 思考正文的 Markdown 解析
331
+ │ ├── child-agent.ts 子 Agent 时间线契约
332
+ │ ├── github.ts GitHub 工具元数据
333
+ │ └── exa.ts Exa 搜索工具元数据
334
+ ├── mermaid/ parse · segment · flowchart · sequence
335
+ │ layout · zoom · transform
336
+ └── enhance/ contract · decide · parse · policy
337
+ normalize · prompt · run
338
+ failure(带类型的 provider 失败,按语言成文)
339
+ index(汇总导出)
340
+ ```
341
+
342
+ ### 命名约定
343
+
344
+ - 目录名与模块文件名一律 `kebab-case`;域的主组件文件用 `PascalCase`(`Activity.tsx`、`StickyBar.tsx`)。
345
+ - 每个域的出口是 `index.ts`,对外只暴露该域的公开面。
346
+ - 同名文件分属不同功能域时由目录消歧(各域都有自己的 `dom.ts` / `index.ts` / `parse.ts`),这是刻意的一致形态而非冲突。
347
+ - 解析层与渲染层同名时,解析层统一叫 `parse.ts`(`shared/*/parse.ts`),渲染层保留领域名(`client/mermaid/markdown.tsx`)。
348
+ - 测试与实现同目录成对,命名 `<被测模块>.test.ts`;测试文件一律为 `.ts`,与 `tsconfig.json` 的 `exclude` 保持一致。
349
+
350
+ ### 改动纪律
351
+
352
+ - **新增功能**:在 `client/` `server/` `shared/` 下新建对应功能目录,实现从该域 `index.ts` 导出,再把 `create*Domain` 加进 `client/features.ts` 的域列表。装配器不需要知道域内部任何细节;带 composer 胶囊的域另在 `FeatureDomain` 上实现 `registerAgent` / `unregisterAgent`,由装配器转发 Agent 增删。
353
+ - **DOM 相关改动**只能落在对应域的 `dom.ts`;新增 DOM 能力时在 `client/dom.ts` 补类型声明,不要把选择器散到别处。
354
+ - **不要**把代码放进 `client/` `server/` `shared/` 与根级两个入口之外——宿主编译器会拒绝。新增顶层目录前请先确认它不会进入 import 图。
355
+ - `client/activity/styles.ts` 必须保持无入边(除类型外不依赖本域其他模块),否则会重新引入 `Activity ↔ tools/*` 循环。
356
+ - **界面文案**统一放在 `shared/i18n.ts` 词表(每种语言各一条),React 里用 `t(locale, key)` 读取、组件外用 `tr(key)`;运行中的语言存在 `client/locale.ts`,只允许 `client/features.ts` 的 `apply()` 写入。`shared/i18n.test.ts` 会在词条没有任何源码引用时报错,弃用的词条要一并删掉。
357
+ - **模型自己的文本不进词表**:增强系统提示词与策略表述刻意固定为中文,活动域的字段标签刻意保持英文。
358
+
359
+ ---
360
+
361
+ ## 本地开发与测试
362
+
363
+ > 深入开发指南与架构约束请参考 [AGENTS.md](./AGENTS.md)。
364
+ > 原始需求档案(`PRD.md`)已不在工作区,需要时从 git 历史取出:`git show fb6d53f:PRD.md`。
365
+
366
+ ### 环境准备与静态检查
367
+
368
+ ```bash
369
+ # 安装依赖
370
+ npm ci
371
+
372
+ # TypeScript 类型检查
373
+ npm run typecheck
374
+
375
+ # 代码规范检查 (oxlint)
376
+ npm run lint
377
+
378
+ # 代码格式校验 (oxfmt)
379
+ npm run format:check
380
+
381
+ # 自动格式化
382
+ npm run format
383
+ ```
384
+
385
+ ### 运行单元测试
386
+
387
+ 遵循最小必要原则,仅运行受改动影响的针对性测试套件:
388
+
389
+ ```bash
390
+ # 1. 验证提问吸顶与历史轮次逻辑
391
+ npm test -- shared/sticky/history.test.ts --run --bail=1
392
+
393
+ # 2. 验证紧凑活动转换器、状态仓储与注册流程
394
+ npm test -- client/registration.test.ts client/activity --run --bail=1
395
+
396
+ # 3. 验证 Mermaid 语法解析与图表转换
397
+ npm test -- shared/mermaid --run --bail=1
398
+
399
+ # 4. 验证功能开关与滚动防闪回归
400
+ npm test -- client/features.test.ts client/sticky/dom.test.ts client/activity/dom.test.ts client/enhance/dom.test.ts --run --bail=1
401
+
402
+ # 5. 验证提示词增强核心逻辑与环境探测
403
+ npm test -- shared/enhance server/enhance --run --bail=1
404
+
405
+ # 6. 验证词表完整性与语言切换
406
+ npm test -- shared/i18n.test.ts --run --bail=1
407
+
408
+ # 7. 验证已发送消息锁定与时间线分页
409
+ npm test -- client/readonly client/timeline-pager.test.ts --run --bail=1
410
+ ```
411
+
412
+ > [!TIP]
413
+ > `client/registration.test.ts` 需要调用宿主真实的 Paseo 编译器。默认源码检索路径为同级目录 `../paseo`;若本地路径不同,可通过环境变量覆盖:<br>
414
+ > `PASEO_SOURCE_DIR=/path/to/paseo npm test -- client/registration.test.ts --run --bail=1`
415
+
416
+ ---
417
+
418
+ ## 致谢与开源许可
419
+
420
+ 本项目在实现过程中吸收并整合了以下优秀开源项目的核心成果:
421
+
422
+ - **[beautiful-chat](https://github.com/ABorakati/beautiful-chat)**
423
+ - 参考了其滚动容器检测与 DOM 注入吸顶的架构思路。
424
+ - **[compact-agent-activity](https://github.com/cnaron/compact-agent-activity)**
425
+ - 紧凑活动摘要计算、配色面板与详情面板渲染深度基于该项目实现。
426
+ - 版权归 Matt Cowger 所有,遵循 MIT 协议,完整声明见 [LICENSE.compact-agent-activity](./LICENSE.compact-agent-activity)。
427
+ - **[paseo-plugin-mermaid](https://github.com/dutchakdev/paseo-plugin-mermaid)**
428
+ - 吸收其 0.2.0 版本(Commit: `95cf07db794f6be3e121a41a7e774e3fdde3ccf5`)跨端图表解析与渲染模块。
429
+ - 版权归 dutchakdev 所有,遵循 MIT 协议,完整声明见 [LICENSE.mermaid](./LICENSE.mermaid)。
@@ -0,0 +1,220 @@
1
+ import React from "react";
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
4
+ import { activitySettings } from "../../shared/settings";
5
+ import { createToolCallData } from "../../shared/activity/timeline";
6
+ import { ColorfulReasoning, ColorfulToolCall } from "./Activity";
7
+ import { cleanupActivityHistory } from "./history";
8
+ import { cleanupActivityDisclosure } from "./disclosure";
9
+ import { buildActivityGroups, type ActivityEntry } from "./group-store";
10
+
11
+ const state = vi.hoisted(() => ({
12
+ values: {} as unknown,
13
+ activity: undefined as unknown,
14
+ timeline: vi.fn(),
15
+ refetch: vi.fn(async () => ({
16
+ entries: [],
17
+ epoch: "one",
18
+ startCursor: null,
19
+ hasOlder: false,
20
+ })),
21
+ actions: [] as Array<() => void>,
22
+ }));
23
+ vi.mock("react-native", async () => {
24
+ const native = await import("react-native-web");
25
+ const react = await import("react");
26
+ return {
27
+ ...native,
28
+ Pressable: (props: { onPress: () => void }) => {
29
+ state.actions.push(props.onPress);
30
+ return react.createElement(native.Pressable, props);
31
+ },
32
+ };
33
+ });
34
+ vi.mock("./history", () => ({
35
+ useActivityGroup: () => state.activity,
36
+ cleanupActivityHistory: vi.fn(),
37
+ }));
38
+ vi.mock("@getpaseo/plugin/client", () => {
39
+ // Turns now read history so a finished turn can collapse in the render layer.
40
+ const paseo = {
41
+ agents: {
42
+ ref: (agentId: string) => {
43
+ state.timeline(agentId);
44
+ return {
45
+ timeline: {
46
+ refetch: state.refetch,
47
+ subscribe: () => Object.assign(() => {}, { ready: Promise.resolve() }),
48
+ },
49
+ };
50
+ },
51
+ },
52
+ };
53
+ return {
54
+ useSettings: () => ({ status: "ready", values: state.values }),
55
+ usePaseo: () => paseo,
56
+ useAgent: () => ({ status: "idle" }),
57
+ };
58
+ });
59
+ vi.mock("@getpaseo/plugin/client/react-native", async () => ({
60
+ ScrollView: (await import("react-native-web")).ScrollView,
61
+ Icon: () => null,
62
+ useRevealedText: (text: string) => text,
63
+ }));
64
+
65
+ const theme = {
66
+ colors: {
67
+ foreground: "#222222",
68
+ foregroundMuted: "#777777",
69
+ border: "#dddddd",
70
+ surface0: "#ffffff",
71
+ surface1: "#eeeeee",
72
+ accent: "#0066cc",
73
+ statusSuccess: "#00aa44",
74
+ statusWarning: "#cc8800",
75
+ statusDanger: "#cc2222",
76
+ },
77
+ };
78
+ const shell = (status: "running" | "completed") =>
79
+ createToolCallData({
80
+ type: "tool_call",
81
+ callId: "shell",
82
+ name: "exec_command",
83
+ status,
84
+ error: null,
85
+ detail: {
86
+ type: "shell",
87
+ command: "npm test",
88
+ cwd: "/tmp",
89
+ output: "secret output",
90
+ exitCode: 0,
91
+ },
92
+ });
93
+ function renderTool(status: "running" | "completed", agentId = "agent") {
94
+ state.actions.length = 0;
95
+ return renderToStaticMarkup(
96
+ React.createElement(ColorfulToolCall, {
97
+ agentId,
98
+ timestamp: new Date(1000),
99
+ theme,
100
+ item: { type: "plugin", kind: "colorful-tool-call", version: 1, data: shell(status) },
101
+ } as never),
102
+ );
103
+ }
104
+ beforeEach(() => {
105
+ state.values = activitySettings.schema.parse({ displayMode: "folded" });
106
+ state.activity = undefined;
107
+ state.timeline.mockClear();
108
+ cleanupActivityDisclosure();
109
+ cleanupActivityHistory();
110
+ });
111
+ afterEach(() => {
112
+ cleanupActivityHistory();
113
+ });
114
+ const entry = (seq: number, item: ActivityEntry["item"]): ActivityEntry => ({
115
+ item,
116
+ seqStart: seq,
117
+ seqEnd: seq,
118
+ timestamp: new Date(seq * 1000).toISOString(),
119
+ });
120
+ function setGroup(finished: boolean, text = "checking the command") {
121
+ const entries = [
122
+ entry(1, { type: "user_message", text: "run the tests" }),
123
+ entry(2, { type: "reasoning", text }),
124
+ entry(3, {
125
+ type: "tool_call",
126
+ callId: "shell",
127
+ name: "exec_command",
128
+ status: finished ? "completed" : "running",
129
+ error: null,
130
+ detail: { type: "shell", command: "npm test", cwd: "/tmp", output: "secret output" },
131
+ }),
132
+ ];
133
+ if (finished) entries.push(entry(4, { type: "assistant_message", text: "done" }));
134
+ state.activity = buildActivityGroups(entries).get("reasoning:2000");
135
+ }
136
+ function renderThought() {
137
+ state.actions.length = 0;
138
+ return renderToStaticMarkup(
139
+ React.createElement(ColorfulReasoning, {
140
+ agentId: "agent",
141
+ timestamp: new Date(2000),
142
+ theme,
143
+ item: {
144
+ type: "plugin",
145
+ kind: "colorful-reasoning",
146
+ version: 1,
147
+ data: { text: "checking the command", phase: "streaming" },
148
+ },
149
+ } as never),
150
+ );
151
+ }
152
+ it("keeps the upstream folded summary unchanged through streaming and completion", () => {
153
+ setGroup(false);
154
+ const running = renderThought();
155
+ expect(running).toContain("folded-activity-group");
156
+ expect(running).toContain("Thought · Ran 1 command");
157
+ expect(running).not.toContain("checking the command");
158
+ expect(running).not.toContain("secret output");
159
+ setGroup(false, "checking the command with more streamed tokens");
160
+ expect(renderThought()).toBe(running);
161
+ setGroup(true);
162
+ expect(renderThought()).toBe(running);
163
+ });
164
+ it("keeps follower rows empty during running and completed turns", () => {
165
+ for (const finished of [false, true]) {
166
+ setGroup(finished);
167
+ state.activity = { ...(state.activity as object), isLeader: false };
168
+ const follower = renderTool(finished ? "completed" : "running");
169
+ expect(follower).toContain("folded-activity-follower");
170
+ expect(follower).not.toContain("secret output");
171
+ expect(follower).not.toContain("folded-activity-group");
172
+ }
173
+ });
174
+ it("shows direct upstream tool details and retains disclosure after remount", () => {
175
+ setGroup(true);
176
+ renderThought();
177
+ state.actions[0]!();
178
+ const expanded = renderThought();
179
+ expect(expanded).toContain("checking the command");
180
+ expect(expanded).not.toContain("command-run-group");
181
+ expect(expanded).not.toContain("用时");
182
+ state.actions[1]!();
183
+ const details = renderThought();
184
+ expect(details).toContain("secret output");
185
+ expect(details).not.toContain("codex-shell-panel");
186
+ expect(renderThought()).toBe(details);
187
+ state.actions[0]!();
188
+ expect(renderThought()).not.toContain("secret output");
189
+ state.actions[0]!();
190
+ expect(renderThought()).toContain("secret output");
191
+ });
192
+ it("does not flash expanded details before folded history becomes available", () => {
193
+ expect(renderTool("running")).not.toContain("secret output");
194
+ expect(renderTool("completed")).not.toContain("secret output");
195
+ });
196
+ it("preserves separate cards in Detailed mode", () => {
197
+ state.values = activitySettings.schema.parse({ displayMode: "detailed" });
198
+ const markup = renderTool("running");
199
+ expect(markup).not.toContain("folded-activity-group");
200
+ expect(markup).toContain("npm test");
201
+ expect(markup).toContain("secret output");
202
+ });
203
+
204
+ it("starts new conversations in the upstream Folded mode and migrates old Codex preferences", () => {
205
+ expect(activitySettings.schema.parse({}).displayMode).toBe("folded");
206
+ expect(activitySettings.schema.parse({ displayMode: "codex" }).displayMode).toBe("folded");
207
+ });
208
+
209
+ it("stays silent as zero-height follower when historical activity is unresolved", () => {
210
+ state.activity = undefined;
211
+ // 已完成的历史命令在未解析时作为 follower 占位,绝不作为折叠卡片撑开高度
212
+ const completedMarkup = renderTool("completed");
213
+ expect(completedMarkup).toContain("folded-activity-follower");
214
+ expect(completedMarkup).not.toContain("folded-activity-group");
215
+
216
+ // 运行中的命令在未解析时允许作为单条展示活跃进度
217
+ const runningMarkup = renderTool("running");
218
+ expect(runningMarkup).toContain("folded-activity-group");
219
+ expect(runningMarkup).not.toContain("folded-activity-follower");
220
+ });