pi-pretty-tui 0.1.8 → 0.2.0

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,11 @@ A beautiful, polished TUI for the [Pi coding agent](https://pi.dev/).
6
6
 
7
7
  ## Features
8
8
 
9
- - **Collapsible tool activity:** instead of filling the conversation with individual tool calls, supported tools are collected into a single `Running(...)` row while work is in progress and a quiet `Done(...)` row when it finishes. Click the group to reveal its compact child calls, click any child for full details, and click the parent again to collapse everything.
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.
10
10
  - **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
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.
12
12
 
13
- Collapsible grouping covers Pi's built-in `read`, `bash`, `edit`, `write`, `grep`, `find`, and `ls` tools. Third-party tools keep their own rendering, and Pi's cursor, IME, autocomplete, mouse interaction, and tool execution behavior remain intact.
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.
14
14
 
15
15
  ## Install
16
16
 
@@ -24,15 +24,19 @@ Pi may warn that built-in tools are being overridden. This is expected: pi-prett
24
24
 
25
25
  ## Working with tool calls
26
26
 
27
- Click a `Running(...)` or `Done(...)` row to keep it as a parent and reveal the tools beneath it. Click an individual child to toggle its complete view, or click the parent again to collapse the whole group. Press `Ctrl+O` at any time to use Pi's global expansion control for tool output and thinking content.
27
+ 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
28
 
29
- ## Rendering modes
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.
30
30
 
31
- The extension supports three persistent rendering modes: `clean` (default), `compact`, and `full`. Run `/pretty-tui` to choose one interactively, or use `/pretty-tui clean`, `/pretty-tui compact`, `/pretty-tui full`, and `/pretty-tui status` directly. The selection is stored in `~/.pi/agent/pretty-tui.json` or under the directory selected by `PI_CODING_AGENT_DIR`.
31
+ ## Settings and rendering modes
32
+
33
+ Run `/pretty-tui` to open the extension settings. The `Enabled` switch persists whether pi-pretty-tui installs its render patches and built-in tool renderers. Changing it automatically runs Pi's official reload flow, so no manual `/reload` is needed. You can also use `/pretty-tui enable` and `/pretty-tui disable` directly. While disabled, the settings command remains available and Pi uses its native rendering.
34
+
35
+ The extension supports three persistent rendering modes: `clean` (default), `compact`, and `full`. Choose one interactively, or use `/pretty-tui clean`, `/pretty-tui compact`, `/pretty-tui full`, and `/pretty-tui status` directly. Settings are stored in `~/.pi/agent/pretty-tui.json` or under the directory selected by `PI_CODING_AGENT_DIR`.
32
36
 
33
37
  ## Copy code blocks
34
38
 
35
- In fullscreen TUI mode, fenced Markdown code blocks include a clickable `[Copy]` control in the top rule:
39
+ In fullscreen TUI mode, fenced Markdown code blocks in user and assistant transcript messages include a clickable `[Copy]` control in the top rule:
36
40
 
37
41
  ```sh
38
42
  pi --tui-mode fullscreen
@@ -49,11 +53,11 @@ npm install
49
53
  npm test
50
54
  ```
51
55
 
52
- The suite covers live and restored steering/compaction boundaries, parallel completion ownership, orphaned tool calls, duplicate-summary prevention, and fullscreen code-block copy hit regions. GitHub Actions runs the same checks on every push and pull request.
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.
53
57
 
54
58
  ## Compatibility notice
55
59
 
56
- 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 parsing, syntax highlighting, and terminal semantic zones remain handled by Pi.
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.
57
61
 
58
62
  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.
59
63
 
package/README.zh-CN.md CHANGED
@@ -6,11 +6,11 @@
6
6
 
7
7
  ## 功能特性
8
8
 
9
- - **可折叠的工具活动:** 不再让大量独立工具调用填满对话。工作进行时,受支持的工具会集中显示为一行 `Running(...)`;工作结束后,则变为低调的 `Done(...)`。点击分组可展开紧凑的子工具列表,点击任一子工具可查看完整详情,再次点击父级即可全部折叠。
9
+ - **可折叠的活动时间线:** 不再让大量独立调用填满对话。工作进行时,所有内置与第三方工具会集中显示为一行 `Running(...)`;工作结束后,则变为低调的 `Done(...)`。父级会统计工具调用和思考数量;展开后,工具、`● thought` 和扩展更新会按 transcript 顺序显示,成员摘要在展开嵌套详情后仍然保留。
10
10
  - **按需查看有效信息:** 针对不同工具设计的摘要,会简洁呈现路径、命令、结果、diff、文件预览和实时 shell 输出,同时保留查看原始完整内容的能力。
11
11
  - **统一、精致的视觉体验:** 圆角用户消息与输入框、主题感知配色、层级清晰的 Markdown 标题、更清爽的列表,以及带语法高亮的圆角代码块,让整个 TUI 更协调。在 fullscreen 模式下,每个围栏代码块还带有符合原生体验的 `[Copy]` 控件。
12
12
 
13
- 可折叠分组覆盖 Pi 内置的 `read`、`bash`、`edit`、`write`、`grep`、`find` 和 `ls` 工具。第三方工具保留自己的渲染方式,同时不会影响 Pi 原有的光标、输入法、自动补全、鼠标交互和工具执行行为。
13
+ Pi 内置的 `read`、`bash`、`edit`、`write`、`grep`、`find` 和 `ls` 工具使用专门设计的紧凑视图;所有第三方工具会自动获得统一的紧凑行,单独展开时继续使用原生 renderer,但会移除与整体冲突的终端背景色。系统错误、steer 消息、compaction 摘要和可见 assistant 正文保持可见,并形成硬分组边界。同时不会影响 Pi 原有的光标、输入法、自动补全、鼠标交互和工具执行行为。
14
14
 
15
15
  ## 安装
16
16
 
@@ -24,15 +24,19 @@ Pi 可能会提示内置工具被覆盖。这是正常现象:pi-pretty-tui 会
24
24
 
25
25
  ## 使用工具调用
26
26
 
27
- 点击 `Running(...)` 或 `Done(...)` 行,可以保留父级并展开其下方的工具。点击单个子工具可切换完整视图,再次点击父级可折叠整个分组。任何时候都可以按 `Ctrl+O`,使用 Pi 的全局展开控制来显示或隐藏工具输出和思考内容。
27
+ 点击 `Running(...)` 或 `Done(...)` 行,可以保留父级并展开其下方的工具、已完成的 `● thought` 和扩展更新。点击单个工具或思考子项可在保留摘要的同时展开嵌套详情,再次点击父级可折叠整个分组。任何时候都可以按 `Ctrl+O`,使用 Pi 的全局展开控制来显示或隐藏全部工具输出和思考内容。工具名称会原样使用定义中的 `label`,没有 `label` 时回退到原始 `name`;pi-pretty-tui 不再重命名或转换大小写。
28
28
 
29
- ## 渲染模式
29
+ 扩展 info 通知与没有自有 renderer 的 displayed custom message 会按真实时间顺序显示为 `◇` 更新成员。若消息在下一轮工具活动之前到达,它会先作为独立更新保持可见,并在活动组建立后成为该组的首个成员。进入分组后,更新成员随父级折叠隐藏,且不计入工具或思考数量。displayed custom message 保留 session 持久化语义,info 通知仍仅存在于当前 runtime。注册了语义化 renderer 的 custom message 会保留为 transcript 中的原生独立消息块,因此后台任务管理器等扩展仍能显示简洁的状态信息。warning 和 error 继续使用 Pi 原生的即时展示方式。
30
30
 
31
- 扩展支持三种可持久化的渲染模式:`clean`(默认)、`compact` 和 `full`。运行 `/pretty-tui` 可交互选择,也可以直接使用 `/pretty-tui clean`、`/pretty-tui compact`、`/pretty-tui full` 和 `/pretty-tui status`。所选设置会保存到 `~/.pi/agent/pretty-tui.json`,或 `PI_CODING_AGENT_DIR` 指定目录中的对应位置。
31
+ ## 设置与渲染模式
32
+
33
+ 运行 `/pretty-tui` 可打开扩展设置。`Enabled` 开关会持久化控制 pi-pretty-tui 是否安装渲染补丁和内置工具 renderer。修改后会自动调用 Pi 官方的 reload 流程,无需手动执行 `/reload`。也可以直接使用 `/pretty-tui enable` 和 `/pretty-tui disable`。禁用期间设置命令仍然可用,Pi 会恢复原生渲染。
34
+
35
+ 扩展支持三种可持久化的渲染模式:`clean`(默认)、`compact` 和 `full`。可交互选择,也可以直接使用 `/pretty-tui clean`、`/pretty-tui compact`、`/pretty-tui full` 和 `/pretty-tui status`。设置会保存到 `~/.pi/agent/pretty-tui.json`,或 `PI_CODING_AGENT_DIR` 指定目录中的对应位置。
32
36
 
33
37
  ## 复制代码块
34
38
 
35
- 在 fullscreen TUI 模式下,Markdown 围栏代码块的顶部边框会显示可点击的 `[Copy]` 控件:
39
+ 在 fullscreen TUI 模式下,用户消息与助手回复中的 Markdown 围栏代码块会在顶部边框显示可点击的 `[Copy]` 控件:
36
40
 
37
41
  ```sh
38
42
  pi --tui-mode fullscreen
@@ -49,11 +53,11 @@ npm install
49
53
  npm test
50
54
  ```
51
55
 
52
- 测试覆盖实时与历史恢复时的 steer/compaction 边界、并行工具完成归属、孤立 tool call、重复摘要防护,以及 fullscreen 代码块复制的点击区域。GitHub Actions 会在每次 push 和 pull request 时运行相同检查。
56
+ 测试覆盖实时与历史恢复的活动时间线、system/steer/compaction 边界、内置与第三方工具、可展开思考、并行工具完成归属、孤立 tool call、重复摘要防护,以及 fullscreen 代码块复制的点击区域。GitHub Actions 会在每次 push 和 pull request 时运行相同检查。
53
57
 
54
58
  ## 兼容性说明
55
59
 
56
- 工具渲染使用 Pi 的公开扩展 API。思考内容的可见性与活动工具状态,通过 Pi 导出的交互组件跟随其内置展开和执行状态变化。由于 Pi 目前没有为这些展示细节提供公开渲染钩子,原生用户消息框、原生输入编辑器圆角、无序列表标记和围栏代码块增强需要使用可安全重载的运行时补丁。Markdown 解析、语法高亮和终端语义区域仍由 Pi 处理。
60
+ 工具渲染使用 Pi 的公开扩展 API。思考内容的可见性与活动工具状态,通过 Pi 导出的交互组件跟随其内置展开和执行状态变化。由于 Pi 目前没有为这些展示细节提供公开渲染钩子,原生用户消息框、原生输入编辑器圆角、无序列表标记和围栏代码块增强需要使用可安全重载的运行时补丁。Markdown 增强仅在渲染主 transcript 中的用户消息和助手回复时启用;扩展 overlay 创建的 Markdown 保持 Pi 原生布局。Markdown 解析、语法高亮和终端语义区域仍由 Pi 处理。
57
61
 
58
62
  本扩展不会修改 Pi 源文件。运行时补丁会在会话关闭时移除,但未来的 Pi 版本可能需要本扩展同步适配。
59
63
 
@@ -0,0 +1,286 @@
1
+ export type ActivityMemberKind = "tool" | "thinking" | "update";
2
+
3
+ export type ActivityMember = {
4
+ id: string;
5
+ kind: ActivityMemberKind;
6
+ toolCallId?: string;
7
+ toolName?: string;
8
+ messageKey?: string;
9
+ thinking?: string;
10
+ updateKey?: string;
11
+ updateTitle?: string;
12
+ updateContent?: string;
13
+ persistent?: boolean;
14
+ };
15
+
16
+ export type ActivityGroup = {
17
+ id: string;
18
+ members: ActivityMember[];
19
+ toolCallIds: string[];
20
+ thoughtCount: number;
21
+ };
22
+
23
+ const toolMemberId = (toolCallId: string) => `tool:${toolCallId}`;
24
+ const thinkingMemberId = (messageKey: string) => `thinking:${messageKey}`;
25
+ const updateMemberId = (updateKey: string) => `update:${updateKey}`;
26
+
27
+ /**
28
+ * Transcript-first activity grouping. The timeline owns chronology and hard
29
+ * boundaries; renderers only project its groups into Pi components.
30
+ */
31
+ export class ActivityTimeline {
32
+ private groupsById = new Map<string, ActivityGroup>();
33
+ private membersById = new Map<string, ActivityMember>();
34
+ private memberGroups = new Map<string, string>();
35
+ private toolMembers = new Map<string, string>();
36
+ private thinkingMembers = new Map<string, string>();
37
+ private updateMembers = new Map<string, string>();
38
+ private currentGroupId?: string;
39
+ private sequence = 0;
40
+
41
+ clear(): void {
42
+ this.groupsById.clear();
43
+ this.membersById.clear();
44
+ this.memberGroups.clear();
45
+ this.toolMembers.clear();
46
+ this.thinkingMembers.clear();
47
+ this.updateMembers.clear();
48
+ this.currentGroupId = undefined;
49
+ this.sequence = 0;
50
+ }
51
+
52
+ boundary(): void {
53
+ this.currentGroupId = undefined;
54
+ }
55
+
56
+ private ensureCurrentGroup(): ActivityGroup {
57
+ if (this.currentGroupId) {
58
+ const current = this.groupsById.get(this.currentGroupId);
59
+ if (current) return current;
60
+ }
61
+ const id = `activity:${++this.sequence}`;
62
+ const group: ActivityGroup = {
63
+ id,
64
+ members: [],
65
+ toolCallIds: [],
66
+ thoughtCount: 0,
67
+ };
68
+ this.groupsById.set(id, group);
69
+ this.currentGroupId = id;
70
+ return group;
71
+ }
72
+
73
+ addTool(toolCallId: string, toolName: string): ActivityMember {
74
+ const existingId = this.toolMembers.get(toolCallId);
75
+ if (existingId) return this.member(existingId)!;
76
+ const member: ActivityMember = {
77
+ id: toolMemberId(toolCallId),
78
+ kind: "tool",
79
+ toolCallId,
80
+ toolName,
81
+ };
82
+ const group = this.ensureCurrentGroup();
83
+ group.members.push(member);
84
+ group.toolCallIds.push(toolCallId);
85
+ this.membersById.set(member.id, member);
86
+ this.memberGroups.set(member.id, group.id);
87
+ this.toolMembers.set(toolCallId, member.id);
88
+ return member;
89
+ }
90
+
91
+ addThinking(messageKey: string, thinking: string): ActivityMember | undefined {
92
+ const normalized = thinking.trim();
93
+ if (!normalized) return undefined;
94
+ const existingId = this.thinkingMembers.get(messageKey);
95
+ if (existingId) {
96
+ const existing = this.member(existingId);
97
+ if (existing) existing.thinking = normalized;
98
+ return existing;
99
+ }
100
+ const member: ActivityMember = {
101
+ id: thinkingMemberId(messageKey),
102
+ kind: "thinking",
103
+ messageKey,
104
+ thinking: normalized,
105
+ };
106
+ const group = this.ensureCurrentGroup();
107
+ group.members.push(member);
108
+ group.thoughtCount += 1;
109
+ this.membersById.set(member.id, member);
110
+ this.memberGroups.set(member.id, group.id);
111
+ this.thinkingMembers.set(messageKey, member.id);
112
+ return member;
113
+ }
114
+
115
+ addUpdate(
116
+ updateKey: string,
117
+ title: string,
118
+ content: string,
119
+ persistent: boolean,
120
+ ): ActivityMember | undefined {
121
+ if (!this.currentGroupId) return undefined;
122
+ return this.addUpdateToGroup(this.currentGroupId, updateKey, title, content, persistent);
123
+ }
124
+
125
+ addPendingUpdate(
126
+ updateKey: string,
127
+ title: string,
128
+ content: string,
129
+ persistent: boolean,
130
+ ): ActivityMember {
131
+ const existingId = this.updateMembers.get(updateKey);
132
+ if (existingId) return this.member(existingId)!;
133
+ const group = this.ensureCurrentGroup();
134
+ return this.appendUpdate(group, updateKey, title, content, persistent);
135
+ }
136
+
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);
149
+ }
150
+
151
+ addUpdateToGroup(
152
+ groupId: string,
153
+ updateKey: string,
154
+ title: string,
155
+ content: string,
156
+ persistent: boolean,
157
+ ): ActivityMember | undefined {
158
+ const existingId = this.updateMembers.get(updateKey);
159
+ if (existingId) return this.member(existingId);
160
+ const group = this.groupsById.get(groupId);
161
+ if (!group || group.toolCallIds.length === 0) return undefined;
162
+ return this.appendUpdate(group, updateKey, title, content, persistent);
163
+ }
164
+
165
+ private appendUpdate(
166
+ group: ActivityGroup,
167
+ updateKey: string,
168
+ title: string,
169
+ content: string,
170
+ persistent: boolean,
171
+ atStart = false,
172
+ ): ActivityMember {
173
+ const member: ActivityMember = {
174
+ id: updateMemberId(updateKey),
175
+ kind: "update",
176
+ updateKey,
177
+ updateTitle: title.trim() || "Update",
178
+ updateContent: content.trim(),
179
+ persistent,
180
+ };
181
+ if (atStart) group.members.unshift(member);
182
+ else group.members.push(member);
183
+ this.membersById.set(member.id, member);
184
+ this.memberGroups.set(member.id, group.id);
185
+ this.updateMembers.set(updateKey, member.id);
186
+ return member;
187
+ }
188
+
189
+ currentGroup(): ActivityGroup | undefined {
190
+ return this.currentGroupId ? this.groupsById.get(this.currentGroupId) : undefined;
191
+ }
192
+
193
+ groups(): ActivityGroup[] {
194
+ return [...this.groupsById.values()];
195
+ }
196
+
197
+ group(groupId: string): ActivityGroup | undefined {
198
+ return this.groupsById.get(groupId);
199
+ }
200
+
201
+ member(memberId: string): ActivityMember | undefined {
202
+ return this.membersById.get(memberId);
203
+ }
204
+
205
+ groupForMember(memberId: string): ActivityGroup | undefined {
206
+ const groupId = this.memberGroups.get(memberId);
207
+ return groupId ? this.groupsById.get(groupId) : undefined;
208
+ }
209
+
210
+ memberForTool(toolCallId: string): ActivityMember | undefined {
211
+ const memberId = this.toolMembers.get(toolCallId);
212
+ return memberId ? this.member(memberId) : undefined;
213
+ }
214
+
215
+ memberForThinking(messageKey: string): ActivityMember | undefined {
216
+ const memberId = this.thinkingMembers.get(messageKey);
217
+ return memberId ? this.member(memberId) : undefined;
218
+ }
219
+
220
+ memberForUpdate(updateKey: string): ActivityMember | undefined {
221
+ const memberId = this.updateMembers.get(updateKey);
222
+ return memberId ? this.member(memberId) : undefined;
223
+ }
224
+
225
+ removeUpdate(updateKey: string): boolean {
226
+ const memberId = this.updateMembers.get(updateKey);
227
+ if (!memberId) return false;
228
+ const groupId = this.memberGroups.get(memberId);
229
+ const group = groupId ? this.groupsById.get(groupId) : undefined;
230
+ if (group) {
231
+ const index = group.members.findIndex((member) => member.id === memberId);
232
+ if (index >= 0) group.members.splice(index, 1);
233
+ if (group.members.length === 0) {
234
+ this.groupsById.delete(group.id);
235
+ if (this.currentGroupId === group.id) this.currentGroupId = undefined;
236
+ }
237
+ }
238
+ this.updateMembers.delete(updateKey);
239
+ this.memberGroups.delete(memberId);
240
+ this.membersById.delete(memberId);
241
+ return true;
242
+ }
243
+
244
+ groupForTool(toolCallId: string): ActivityGroup | undefined {
245
+ const member = this.memberForTool(toolCallId);
246
+ return member ? this.groupForMember(member.id) : undefined;
247
+ }
248
+
249
+ groupForThinking(messageKey: string): ActivityGroup | undefined {
250
+ const member = this.memberForThinking(messageKey);
251
+ return member ? this.groupForMember(member.id) : undefined;
252
+ }
253
+
254
+ groupForUpdate(updateKey: string): ActivityGroup | undefined {
255
+ const member = this.memberForUpdate(updateKey);
256
+ return member ? this.groupForMember(member.id) : undefined;
257
+ }
258
+ }
259
+
260
+ export const assistantMessageKey = (message: any): string => {
261
+ const timestamp = message?.timestamp;
262
+ if (typeof timestamp === "number" || typeof timestamp === "string") return String(timestamp);
263
+ const firstToolCall = Array.isArray(message?.content)
264
+ ? message.content.find((item: any) => item?.type === "toolCall" && item.id)?.id
265
+ : undefined;
266
+ return firstToolCall ? `tool-message:${firstToolCall}` : `message:${JSON.stringify(message?.content ?? "")}`;
267
+ };
268
+
269
+ export const thinkingText = (message: any): string => {
270
+ if (!Array.isArray(message?.content)) return "";
271
+ return message.content
272
+ .filter((item: any) => item?.type === "thinking" && typeof item.thinking === "string")
273
+ .map((item: any) => item.thinking.trim())
274
+ .filter(Boolean)
275
+ .join("\n\n");
276
+ };
277
+
278
+ export const visibleAssistantText = (message: any): boolean =>
279
+ typeof message?.content === "string"
280
+ ? message.content.trim().length > 0
281
+ : Array.isArray(message?.content) && message.content.some(
282
+ (item: any) => item?.type === "text" && typeof item.text === "string" && item.text.trim().length > 0,
283
+ );
284
+
285
+ export const assistantSystemBoundary = (message: any): boolean =>
286
+ message?.role === "assistant" && ["error", "aborted", "length"].includes(message?.stopReason);