pi-pretty-tui 0.2.0 → 0.2.2

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.
package/README.md CHANGED
@@ -6,11 +6,13 @@ A beautiful, polished TUI for the [Pi coding agent](https://pi.dev/).
6
6
 
7
7
  ## Features
8
8
 
9
- - **Collapsible activity timeline:** instead of filling the conversation with individual calls, all built-in and third-party tools are collected into a single `Running(...)` row while work is in progress and a quiet `Done(...)` row when it finishes. Parent rows count tool calls and thoughts; expanding one reveals ordered tools, `● thought` entries, and extension updates, with member summaries retained above nested details.
9
+ - **Collapsible activity timeline:** instead of filling the conversation with individual calls, thoughts, tools, and ordinary `info` notifications share one chronological activity stream. Work appears beneath a single `Running(...)` row and settles into a quiet `Done(...)` row. Parent rows count only tool calls and thoughts; expanding one reveals every ordered member with summaries retained above nested details. Thoughts and tools are peers: a turn that calls no tool still gets its own `Running(1 thought)` / `Done(1 thought)` parent row with the Thought as its child, so thinking is never dropped.
10
+ - **Transient UI status:** one line above the editor shows direct Pi UI feedback such as Thinking/tool-output visibility changes. A newer UI status replaces it, and it disappears automatically after ten seconds without entering the transcript or session.
11
+ - **Durable response footers:** once the complete agent run settles—including tools, retries, and compaction—the final answer ends with one quiet gray divider containing the elapsed-work time and, in fullscreen, a `[Copy]` control. It copies only the final visible answer, excluding thoughts, tools, statuses, and the footer itself; the footer survives reloads and session-tree navigation.
10
12
  - **Useful details on demand:** tool-aware summaries keep paths, commands, results, diffs, file previews, and live shell output concise without removing access to the original content.
11
- - **A cohesive visual finish:** rounded user messages and prompt input, theme-aware colors, a clear Markdown heading hierarchy, cleaner lists, and rounded syntax-highlighted code blocks make the entire TUI feel intentional. In fullscreen, each fenced code block also includes a native-feeling `[Copy]` control.
13
+ - **A cohesive visual finish:** transparent, right-aligned user chat bubbles size themselves to short messages and wrap longer ones at 75% of the terminal width, with an accent-blue rounded border and no redundant `User` label. Together with the rounded prompt input, theme-aware colors, clear Markdown heading hierarchy, cleaner lists, and rounded syntax-highlighted code blocks, they make the entire TUI feel intentional. In fullscreen, each fenced code block also includes a native-feeling `[Copy]` control.
12
14
 
13
- Pi's built-in `read`, `bash`, `edit`, `write`, `grep`, `find`, and `ls` tools receive purpose-built compact views. Every third-party tool automatically receives a consistent compact row; opening it preserves its native renderer while removing conflicting terminal background colors. System errors, steering messages, compaction summaries, and visible assistant text remain visible and create hard group boundaries. Pi's cursor, IME, autocomplete, mouse interaction, and tool execution behavior remain intact.
15
+ Pi's built-in `read`, `bash`, `edit`, `write`, `grep`, `find`, and `ls` tools receive purpose-built compact views. Every third-party tool automatically receives a consistent compact row; opening it preserves its native renderer while removing conflicting terminal background colors. User and steering messages, compaction summaries, visible assistant text, warnings, errors, aborted/truncated responses, and displayed custom messages with a registered renderer create hard group boundaries. Pi's cursor, IME, autocomplete, mouse interaction, and tool execution behavior remain intact.
14
16
 
15
17
  ## Install
16
18
 
@@ -26,7 +28,11 @@ Pi may warn that built-in tools are being overridden. This is expected: pi-prett
26
28
 
27
29
  Click a `Running(...)` or `Done(...)` row to keep it as a parent and reveal tools, completed `● thought` entries, and extension updates beneath it. Click an individual tool or thought to reveal nested details without replacing its summary, or click the parent again to collapse the whole group. Press `Ctrl+O` at any time to use Pi's global expansion control for all tool output and thinking content. Tool names use each definition's `label` verbatim, falling back to its raw `name`; pi-pretty-tui does not rename or title-case them.
28
30
 
29
- Informational extension notifications and displayed custom messages without their own renderer appear in chronological order as `◇` update members. If one arrives before its next tool turn, it stays visible as a standalone update and becomes the first member of that activity group once it exists. Grouped updates remain hidden with the collapsed parent and do not affect tool/thought counts. Displayed custom messages preserve their session persistence, while informational notifications remain runtime-only. Custom messages with a registered semantic renderer remain native standalone transcript blocks, so extensions such as background-task managers keep their concise status presentation. Warnings and errors retain Pi's native immediate presentation.
31
+ Extension calls to `ctx.ui.notify(message, "info")` (or with the type omitted) join an existing thought/tool group in chronological order; without existing work they remain independent and never attach to a future group. Warnings and errors stay standalone, use yellow `⚠` and red `✕`, and are hard boundaries. Tool failures remain owned by their tool details. None of these extension notifications or errors appear in the transient UI-status line.
32
+
33
+ Displayed custom messages without their own renderer use one generic accent-coloured `◇` Markdown presentation: they join current thought/tool work when it exists, otherwise remain independent, and preserve their session persistence. Messages with a registered renderer remain completely native and form hard boundaries. These decisions use only Pi's notification type and renderer presence—never extension names, custom message names, or content heuristics.
34
+
35
+ Pi's direct internal `showStatus()` feedback is transient: it appears only in the status line above the editor, a newer status replaces it, and a ten-second timer removes it. It never enters the transcript or activity timeline and is cleared on reload. In clean mode, `Ctrl+T` leaves Pi's persisted Thinking setting unchanged and shows a short hint instead: thought details expand only when their own row is clicked, while `Ctrl+O` retains global activity expansion. In full and compact modes, `Ctrl+T` keeps Pi's native behaviour.
30
36
 
31
37
  ## Settings and rendering modes
32
38
 
@@ -53,11 +59,11 @@ npm install
53
59
  npm test
54
60
  ```
55
61
 
56
- The suite covers live and restored activity timelines, system/steering/compaction boundaries, built-in and third-party tools, expandable thinking, parallel completion ownership, orphaned calls, duplicate-summary prevention, and fullscreen code-block copy hit regions. GitHub Actions runs the same checks on every push and pull request.
62
+ The suite covers live and restored activity timelines, retries and terminal states, user/steering/compaction/native-renderer boundaries, ten-second UI-status replacement, stale-timer and flash cleanup on reload and shutdown, standalone severity styling, built-in and third-party tools, expandable thinking, parallel completion ownership, orphaned calls, duplicate-summary prevention, durable response timing and answer-only copy controls, adaptive user chat bubbles, complete prototype-patch restoration, and fullscreen code-block copy hit regions. GitHub Actions runs the same checks on every push and pull request.
57
63
 
58
64
  ## Compatibility notice
59
65
 
60
- Tool rendering uses Pi's documented extension APIs. Thinking visibility and active-tool state follow Pi's built-in expansion and execution transitions through its exported interactive components. Framing native user messages, rounding the native prompt editor, changing unordered-list markers, and enhancing fenced code blocks require reload-safe runtime patches because Pi does not currently expose public renderer hooks for those presentation details. Markdown enhancements are activated only while rendering user and assistant messages in the main transcript; Markdown created by extension overlays keeps Pi's native layout. Markdown parsing, syntax highlighting, and terminal semantic zones remain handled by Pi.
66
+ Tool rendering uses Pi's documented extension APIs. Active-tool state follows Pi's built-in execution transitions through its exported interactive components. Clean-mode thought details use the activity hierarchy's own disclosure state; full and compact modes retain Pi's built-in Thinking visibility control. Rendering native user messages as adaptive chat bubbles, rounding the native prompt editor, changing unordered-list markers, and enhancing fenced code blocks require reload-safe runtime patches because Pi does not currently expose public renderer hooks for those presentation details. Markdown enhancements are activated only while rendering user and assistant messages in the main transcript; Markdown created by extension overlays keeps Pi's native layout. Markdown parsing, syntax highlighting, and terminal semantic zones remain handled by Pi.
61
67
 
62
68
  No Pi source files are modified. Runtime patches are removed during session shutdown, but a future Pi release may require this extension to be updated.
63
69
 
package/README.zh-CN.md CHANGED
@@ -6,11 +6,13 @@
6
6
 
7
7
  ## 功能特性
8
8
 
9
- - **可折叠的活动时间线:** 不再让大量独立调用填满对话。工作进行时,所有内置与第三方工具会集中显示为一行 `Running(...)`;工作结束后,则变为低调的 `Done(...)`。父级会统计工具调用和思考数量;展开后,工具、`● thought` 和扩展更新会按 transcript 顺序显示,成员摘要在展开嵌套详情后仍然保留。
9
+ - **可折叠的活动时间线:** 不再让大量独立调用填满对话。思考、工具和普通 `info` 通知会进入同一条按真实时间排序的活动流;工作进行时显示为 `Running(...)`,结束后变为低调的 `Done(...)`。父级只统计工具调用和思考数量;展开后可查看所有有序成员,成员摘要在展开嵌套详情后仍然保留。思考与工具地位相同:没有调用任何工具的那一轮同样会有自己的 `Running(1 thought)` / `Done(1 thought)` 父行,思考作为其子级,内容不会被丢弃。
10
+ - **可持久化的回答 footer:** 整轮 Agent 工作真正 settled 后(包括工具、自动重试和 compaction),最终回答底部会显示一条低调的灰色分隔线,工作时长与 fullscreen 模式下的 `[Copy]` 控件都嵌在线中。`[Copy]` 只复制最终可见回答,不包含 thought、工具、状态或 footer;reload 和 session tree 切换后仍会保留。
11
+ - **临时 UI 状态:** 编辑器上方的一行只显示 Pi 直接产生的 UI 操作反馈,例如 Thinking 或工具输出可见性变化。新状态会替换旧状态,并在十秒后自动消失,不进入 transcript 或 session。
10
12
  - **按需查看有效信息:** 针对不同工具设计的摘要,会简洁呈现路径、命令、结果、diff、文件预览和实时 shell 输出,同时保留查看原始完整内容的能力。
11
- - **统一、精致的视觉体验:** 圆角用户消息与输入框、主题感知配色、层级清晰的 Markdown 标题、更清爽的列表,以及带语法高亮的圆角代码块,让整个 TUI 更协调。在 fullscreen 模式下,每个围栏代码块还带有符合原生体验的 `[Copy]` 控件。
13
+ - **统一、精致的视觉体验:** 透明、右对齐的用户聊天气泡会根据短消息自适应宽度,长消息则在终端宽度的 75% 处换行;气泡使用 accent 浅蓝色圆角边框,并移除了多余的 `User` 标注。配合圆角输入框、主题感知配色、层级清晰的 Markdown 标题、更清爽的列表,以及带语法高亮的圆角代码块,让整个 TUI 更协调。在 fullscreen 模式下,每个围栏代码块还带有符合原生体验的 `[Copy]` 控件。
12
14
 
13
- Pi 内置的 `read`、`bash`、`edit`、`write`、`grep`、`find` 和 `ls` 工具使用专门设计的紧凑视图;所有第三方工具会自动获得统一的紧凑行,单独展开时继续使用原生 renderer,但会移除与整体冲突的终端背景色。系统错误、steer 消息、compaction 摘要和可见 assistant 正文保持可见,并形成硬分组边界。同时不会影响 Pi 原有的光标、输入法、自动补全、鼠标交互和工具执行行为。
15
+ Pi 内置的 `read`、`bash`、`edit`、`write`、`grep`、`find` 和 `ls` 工具使用专门设计的紧凑视图;所有第三方工具会自动获得统一的紧凑行,单独展开时继续使用原生 renderer,但会移除与整体冲突的终端背景色。用户与 steer 消息、compaction 摘要、可见 assistant 正文、warning、error、aborted/length 响应,以及注册了专用 renderer 的 displayed custom message 都会形成硬分组边界。同时不会影响 Pi 原有的光标、输入法、自动补全、鼠标交互和工具执行行为。
14
16
 
15
17
  ## 安装
16
18
 
@@ -26,7 +28,11 @@ Pi 可能会提示内置工具被覆盖。这是正常现象:pi-pretty-tui 会
26
28
 
27
29
  点击 `Running(...)` 或 `Done(...)` 行,可以保留父级并展开其下方的工具、已完成的 `● thought` 和扩展更新。点击单个工具或思考子项可在保留摘要的同时展开嵌套详情,再次点击父级可折叠整个分组。任何时候都可以按 `Ctrl+O`,使用 Pi 的全局展开控制来显示或隐藏全部工具输出和思考内容。工具名称会原样使用定义中的 `label`,没有 `label` 时回退到原始 `name`;pi-pretty-tui 不再重命名或转换大小写。
28
30
 
29
- 扩展 info 通知与没有自有 renderer 的 displayed custom message 会按真实时间顺序显示为 `◇` 更新成员。若消息在下一轮工具活动之前到达,它会先作为独立更新保持可见,并在活动组建立后成为该组的首个成员。进入分组后,更新成员随父级折叠隐藏,且不计入工具或思考数量。displayed custom message 保留 session 持久化语义,info 通知仍仅存在于当前 runtime。注册了语义化 renderer 的 custom message 会保留为 transcript 中的原生独立消息块,因此后台任务管理器等扩展仍能显示简洁的状态信息。warning 和 error 继续使用 Pi 原生的即时展示方式。
31
+ 扩展调用 `ctx.ui.notify(message, "info")`(或省略 type)时,如果当前已有 thought/tool 活动组,通知会按真实时间顺序加入该组;如果没有现有工作,则独立显示,并且不会附着到未来的工具组。warning 和 error 保持独立,分别使用黄色 `⚠` 和红色 `✕`,并形成硬边界。工具失败继续归属于对应工具详情。上述扩展通知和错误都不会进入临时 UI 状态栏。
32
+
33
+ 没有自有 renderer 的 displayed custom message 使用统一、accent 配色的 `◇` Markdown 显示:当前存在 thought/tool 工作时加入当前组,否则独立显示,并保留 session 持久化。注册了 renderer 的消息完全保留插件原生显示并形成硬边界。所有判断只依据 Pi 的通知 type 和 renderer 是否存在,不识别扩展名称、custom message 名称,也不根据文本内容推断语义。
34
+
35
+ Pi 内部直接调用 `showStatus()` 产生的反馈属于临时 UI 状态:它只显示在编辑器上方,新状态替换旧状态,并由十秒 timer 自动清除;它不会进入 transcript 或活动时间线,reload 时也会清除。在 clean 模式下,`Ctrl+T` 不修改 Pi 持久化的 Thinking 设置,只显示一条简短提示;thought 详情仅在点击对应子项时展开,`Ctrl+O` 继续负责全局活动展开。在 full 和 compact 模式下,`Ctrl+T` 保留 Pi 的原生行为。
30
36
 
31
37
  ## 设置与渲染模式
32
38
 
@@ -53,11 +59,11 @@ npm install
53
59
  npm test
54
60
  ```
55
61
 
56
- 测试覆盖实时与历史恢复的活动时间线、system/steer/compaction 边界、内置与第三方工具、可展开思考、并行工具完成归属、孤立 tool call、重复摘要防护,以及 fullscreen 代码块复制的点击区域。GitHub Actions 会在每次 push 和 pull request 时运行相同检查。
62
+ 测试覆盖实时与历史恢复的活动时间线、自动重试与终止状态、用户/steer/compaction/原生 renderer 边界、十秒 UI 状态替换、reload 与 shutdown 时的旧 timer 与状态清理、独立严重级别样式、内置与第三方工具、可展开思考、并行工具完成归属、孤立 tool call、重复摘要防护、可持久化的工作时长与仅回答复制控件、自适应用户聊天气泡、原型补丁的完整恢复,以及 fullscreen 代码块复制的点击区域。GitHub Actions 会在每次 push 和 pull request 时运行相同检查。
57
63
 
58
64
  ## 兼容性说明
59
65
 
60
- 工具渲染使用 Pi 的公开扩展 API。思考内容的可见性与活动工具状态,通过 Pi 导出的交互组件跟随其内置展开和执行状态变化。由于 Pi 目前没有为这些展示细节提供公开渲染钩子,原生用户消息框、原生输入编辑器圆角、无序列表标记和围栏代码块增强需要使用可安全重载的运行时补丁。Markdown 增强仅在渲染主 transcript 中的用户消息和助手回复时启用;扩展 overlay 创建的 Markdown 保持 Pi 原生布局。Markdown 解析、语法高亮和终端语义区域仍由 Pi 处理。
66
+ 工具渲染使用 Pi 的公开扩展 API。活动工具状态通过 Pi 导出的交互组件跟随其内置执行状态变化;clean 模式的 thought 详情使用活动时间线自身的展开状态,full 和 compact 模式则保留 Pi 内置的 Thinking 可见性控制。由于 Pi 目前没有为这些展示细节提供公开渲染钩子,自适应的原生用户消息气泡、原生输入编辑器圆角、无序列表标记和围栏代码块增强需要使用可安全重载的运行时补丁。Markdown 增强仅在渲染主 transcript 中的用户消息和助手回复时启用;扩展 overlay 创建的 Markdown 保持 Pi 原生布局。Markdown 解析、语法高亮和终端语义区域仍由 Pi 处理。
61
67
 
62
68
  本扩展不会修改 Pi 源文件。运行时补丁会在会话关闭时移除,但未来的 Pi 版本可能需要本扩展同步适配。
63
69
 
@@ -1,4 +1,5 @@
1
1
  export type ActivityMemberKind = "tool" | "thinking" | "update";
2
+ export type ActivitySeverity = "info" | "warning" | "error";
2
3
 
3
4
  export type ActivityMember = {
4
5
  id: string;
@@ -10,6 +11,7 @@ export type ActivityMember = {
10
11
  updateKey?: string;
11
12
  updateTitle?: string;
12
13
  updateContent?: string;
14
+ updateSeverity?: ActivitySeverity;
13
15
  persistent?: boolean;
14
16
  };
15
17
 
@@ -18,6 +20,7 @@ export type ActivityGroup = {
18
20
  members: ActivityMember[];
19
21
  toolCallIds: string[];
20
22
  thoughtCount: number;
23
+ hardBoundarySplit?: boolean;
21
24
  };
22
25
 
23
26
  const toolMemberId = (toolCallId: string) => `tool:${toolCallId}`;
@@ -35,6 +38,7 @@ export class ActivityTimeline {
35
38
  private toolMembers = new Map<string, string>();
36
39
  private thinkingMembers = new Map<string, string>();
37
40
  private updateMembers = new Map<string, string>();
41
+ private settledGroups = new Set<string>();
38
42
  private currentGroupId?: string;
39
43
  private sequence = 0;
40
44
 
@@ -45,14 +49,25 @@ export class ActivityTimeline {
45
49
  this.toolMembers.clear();
46
50
  this.thinkingMembers.clear();
47
51
  this.updateMembers.clear();
52
+ this.settledGroups.clear();
48
53
  this.currentGroupId = undefined;
49
54
  this.sequence = 0;
50
55
  }
51
56
 
52
- boundary(): void {
57
+ /**
58
+ * Ends the current group. Because every hard transcript boundary routes here,
59
+ * a closed group can never receive further members, which makes this the
60
+ * completion signal for groups that have no durable tool summary.
61
+ */
62
+ boundary(settled = true): void {
63
+ if (settled && this.currentGroupId) this.settledGroups.add(this.currentGroupId);
53
64
  this.currentGroupId = undefined;
54
65
  }
55
66
 
67
+ isSettled(groupId: string): boolean {
68
+ return this.settledGroups.has(groupId);
69
+ }
70
+
56
71
  private ensureCurrentGroup(): ActivityGroup {
57
72
  if (this.currentGroupId) {
58
73
  const current = this.groupsById.get(this.currentGroupId);
@@ -117,9 +132,10 @@ export class ActivityTimeline {
117
132
  title: string,
118
133
  content: string,
119
134
  persistent: boolean,
135
+ severity: ActivitySeverity = "info",
120
136
  ): ActivityMember | undefined {
121
137
  if (!this.currentGroupId) return undefined;
122
- return this.addUpdateToGroup(this.currentGroupId, updateKey, title, content, persistent);
138
+ return this.addUpdateToGroup(this.currentGroupId, updateKey, title, content, persistent, severity);
123
139
  }
124
140
 
125
141
  addPendingUpdate(
@@ -127,25 +143,23 @@ export class ActivityTimeline {
127
143
  title: string,
128
144
  content: string,
129
145
  persistent: boolean,
146
+ severity: ActivitySeverity = "info",
130
147
  ): ActivityMember {
131
148
  const existingId = this.updateMembers.get(updateKey);
132
149
  if (existingId) return this.member(existingId)!;
133
150
  const group = this.ensureCurrentGroup();
134
- return this.appendUpdate(group, updateKey, title, content, persistent);
151
+ return this.appendUpdate(group, updateKey, title, content, persistent, severity);
135
152
  }
136
153
 
137
- addUpdateToGroupStart(
138
- groupId: string,
139
- updateKey: string,
140
- title: string,
141
- content: string,
142
- persistent: boolean,
143
- ): ActivityMember | undefined {
144
- const existingId = this.updateMembers.get(updateKey);
145
- if (existingId) return this.member(existingId);
146
- const group = this.groupsById.get(groupId);
147
- if (!group || group.toolCallIds.length === 0) return undefined;
148
- return this.appendUpdate(group, updateKey, title, content, persistent, true);
154
+ /**
155
+ * A group can receive update members only once it holds real work. This is
156
+ * the single definition of that condition: thinking counts even before any
157
+ * tool has run, because an activity group is created by the first thought.
158
+ */
159
+ hasWork(group: ActivityGroup | undefined): boolean {
160
+ return Boolean(group?.members.some(
161
+ (member) => member.kind === "tool" || member.kind === "thinking",
162
+ ));
149
163
  }
150
164
 
151
165
  addUpdateToGroup(
@@ -154,12 +168,13 @@ export class ActivityTimeline {
154
168
  title: string,
155
169
  content: string,
156
170
  persistent: boolean,
171
+ severity: ActivitySeverity = "info",
157
172
  ): ActivityMember | undefined {
158
173
  const existingId = this.updateMembers.get(updateKey);
159
174
  if (existingId) return this.member(existingId);
160
175
  const group = this.groupsById.get(groupId);
161
- if (!group || group.toolCallIds.length === 0) return undefined;
162
- return this.appendUpdate(group, updateKey, title, content, persistent);
176
+ if (!this.hasWork(group)) return undefined;
177
+ return this.appendUpdate(group!, updateKey, title, content, persistent, severity);
163
178
  }
164
179
 
165
180
  private appendUpdate(
@@ -168,7 +183,7 @@ export class ActivityTimeline {
168
183
  title: string,
169
184
  content: string,
170
185
  persistent: boolean,
171
- atStart = false,
186
+ severity: ActivitySeverity = "info",
172
187
  ): ActivityMember {
173
188
  const member: ActivityMember = {
174
189
  id: updateMemberId(updateKey),
@@ -176,10 +191,10 @@ export class ActivityTimeline {
176
191
  updateKey,
177
192
  updateTitle: title.trim() || "Update",
178
193
  updateContent: content.trim(),
194
+ updateSeverity: severity,
179
195
  persistent,
180
196
  };
181
- if (atStart) group.members.unshift(member);
182
- else group.members.push(member);
197
+ group.members.push(member);
183
198
  this.membersById.set(member.id, member);
184
199
  this.memberGroups.set(member.id, group.id);
185
200
  this.updateMembers.set(updateKey, member.id);
@@ -222,14 +237,53 @@ export class ActivityTimeline {
222
237
  return memberId ? this.member(memberId) : undefined;
223
238
  }
224
239
 
240
+ /**
241
+ * Releases a claimed update and splits the group at that point, so a native
242
+ * custom renderer can take the message over without the remaining members
243
+ * being reordered around it.
244
+ */
225
245
  removeUpdate(updateKey: string): boolean {
226
246
  const memberId = this.updateMembers.get(updateKey);
227
- if (!memberId) return false;
247
+ if (!memberId) {
248
+ this.boundary();
249
+ return false;
250
+ }
228
251
  const groupId = this.memberGroups.get(memberId);
229
252
  const group = groupId ? this.groupsById.get(groupId) : undefined;
230
253
  if (group) {
231
254
  const index = group.members.findIndex((member) => member.id === memberId);
232
- if (index >= 0) group.members.splice(index, 1);
255
+ if (index >= 0) {
256
+ const following = group.members.slice(index + 1);
257
+ group.members = group.members.slice(0, index);
258
+ group.hardBoundarySplit = true;
259
+ group.toolCallIds = group.members
260
+ .filter((member) => member.kind === "tool" && member.toolCallId)
261
+ .map((member) => member.toolCallId!);
262
+ group.thoughtCount = group.members.filter((member) => member.kind === "thinking").length;
263
+
264
+ if (following.length > 0) {
265
+ const nextId = `activity:${++this.sequence}`;
266
+ const next: ActivityGroup = {
267
+ id: nextId,
268
+ members: following,
269
+ toolCallIds: following
270
+ .filter((member) => member.kind === "tool" && member.toolCallId)
271
+ .map((member) => member.toolCallId!),
272
+ thoughtCount: following.filter((member) => member.kind === "thinking").length,
273
+ hardBoundarySplit: true,
274
+ };
275
+ const ordered = new Map<string, ActivityGroup>();
276
+ for (const [id, existing] of this.groupsById) {
277
+ ordered.set(id, existing);
278
+ if (id === group.id) ordered.set(next.id, next);
279
+ }
280
+ this.groupsById = ordered;
281
+ for (const member of following) this.memberGroups.set(member.id, next.id);
282
+ if (this.currentGroupId === group.id) this.currentGroupId = next.id;
283
+ } else if (this.currentGroupId === group.id) {
284
+ this.currentGroupId = undefined;
285
+ }
286
+ }
233
287
  if (group.members.length === 0) {
234
288
  this.groupsById.delete(group.id);
235
289
  if (this.currentGroupId === group.id) this.currentGroupId = undefined;
@@ -282,5 +336,5 @@ export const visibleAssistantText = (message: any): boolean =>
282
336
  (item: any) => item?.type === "text" && typeof item.text === "string" && item.text.trim().length > 0,
283
337
  );
284
338
 
285
- export const assistantSystemBoundary = (message: any): boolean =>
339
+ export const assistantTerminalState = (message: any): boolean =>
286
340
  message?.role === "assistant" && ["error", "aborted", "length"].includes(message?.stopReason);