@esso0428/pi-subagents 0.15.6 → 0.15.7
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/CHANGELOG.md +9 -0
- package/dist/agent-history-list.d.ts +1 -0
- package/dist/agent-history-list.d.ts.map +1 -0
- package/dist/agent-history-list.js +1 -0
- package/dist/agent-history-list.js.map +1 -0
- package/dist/agent-history.d.ts +1 -0
- package/dist/agent-history.d.ts.map +1 -0
- package/dist/agent-history.js +1 -0
- package/dist/agent-history.js.map +1 -0
- package/dist/agent-manager.d.ts +2 -0
- package/dist/agent-manager.d.ts.map +1 -0
- package/dist/agent-manager.js +71 -0
- package/dist/agent-manager.js.map +1 -0
- package/dist/agent-runner.d.ts +1 -0
- package/dist/agent-runner.d.ts.map +1 -0
- package/dist/agent-runner.js +8 -0
- package/dist/agent-runner.js.map +1 -0
- package/dist/agent-types.d.ts +1 -0
- package/dist/agent-types.d.ts.map +1 -0
- package/dist/agent-types.js +1 -0
- package/dist/agent-types.js.map +1 -0
- package/dist/context.d.ts +1 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +1 -0
- package/dist/context.js.map +1 -0
- package/dist/cross-extension-rpc.d.ts +1 -0
- package/dist/cross-extension-rpc.d.ts.map +1 -0
- package/dist/cross-extension-rpc.js +1 -0
- package/dist/cross-extension-rpc.js.map +1 -0
- package/dist/custom-agents.d.ts +1 -0
- package/dist/custom-agents.d.ts.map +1 -0
- package/dist/custom-agents.js +1 -0
- package/dist/custom-agents.js.map +1 -0
- package/dist/default-agents.d.ts +1 -0
- package/dist/default-agents.d.ts.map +1 -0
- package/dist/default-agents.js +1 -0
- package/dist/default-agents.js.map +1 -0
- package/dist/enabled-models.d.ts +1 -0
- package/dist/enabled-models.d.ts.map +1 -0
- package/dist/enabled-models.js +1 -0
- package/dist/enabled-models.js.map +1 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +1 -0
- package/dist/env.js.map +1 -0
- package/dist/group-join.d.ts +1 -0
- package/dist/group-join.d.ts.map +1 -0
- package/dist/group-join.js +1 -0
- package/dist/group-join.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -10
- package/dist/index.js.map +1 -0
- package/dist/invocation-config.d.ts +1 -0
- package/dist/invocation-config.d.ts.map +1 -0
- package/dist/invocation-config.js +1 -0
- package/dist/invocation-config.js.map +1 -0
- package/dist/memory.d.ts +1 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +1 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-resolver.d.ts +1 -0
- package/dist/model-resolver.d.ts.map +1 -0
- package/dist/model-resolver.js +1 -0
- package/dist/model-resolver.js.map +1 -0
- package/dist/nico-overrides.d.ts +1 -0
- package/dist/nico-overrides.d.ts.map +1 -0
- package/dist/nico-overrides.js +1 -0
- package/dist/nico-overrides.js.map +1 -0
- package/dist/output-file.d.ts +1 -0
- package/dist/output-file.d.ts.map +1 -0
- package/dist/output-file.js +1 -0
- package/dist/output-file.js.map +1 -0
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +1 -0
- package/dist/prompts.js.map +1 -0
- package/dist/schedule-store.d.ts +1 -0
- package/dist/schedule-store.d.ts.map +1 -0
- package/dist/schedule-store.js +1 -0
- package/dist/schedule-store.js.map +1 -0
- package/dist/schedule.d.ts +1 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +1 -0
- package/dist/schedule.js.map +1 -0
- package/dist/settings.d.ts +1 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +1 -0
- package/dist/settings.js.map +1 -0
- package/dist/skill-loader.d.ts +1 -0
- package/dist/skill-loader.d.ts.map +1 -0
- package/dist/skill-loader.js +1 -0
- package/dist/skill-loader.js.map +1 -0
- package/dist/status-note.d.ts +1 -0
- package/dist/status-note.d.ts.map +1 -0
- package/dist/status-note.js +1 -0
- package/dist/status-note.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/agent-widget.d.ts +1 -0
- package/dist/ui/agent-widget.d.ts.map +1 -0
- package/dist/ui/agent-widget.js +4 -2
- package/dist/ui/agent-widget.js.map +1 -0
- package/dist/ui/ccstyle/diff/ansi-utils.d.ts +11 -0
- package/dist/ui/ccstyle/diff/ansi-utils.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/ansi-utils.js +145 -0
- package/dist/ui/ccstyle/diff/ansi-utils.js.map +1 -0
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts +12 -0
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/diff-presentation.js +49 -0
- package/dist/ui/ccstyle/diff/diff-presentation.js.map +1 -0
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts +46 -0
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/diff-renderer.js +2044 -0
- package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -0
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts +12 -0
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/line-width-safety.js +59 -0
- package/dist/ui/ccstyle/diff/line-width-safety.js.map +1 -0
- package/dist/ui/ccstyle/diff/render-utils.d.ts +6 -0
- package/dist/ui/ccstyle/diff/render-utils.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/render-utils.js +25 -0
- package/dist/ui/ccstyle/diff/render-utils.js.map +1 -0
- package/dist/ui/ccstyle/diff/types.d.ts +22 -0
- package/dist/ui/ccstyle/diff/types.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/types.js +10 -0
- package/dist/ui/ccstyle/diff/types.js.map +1 -0
- package/dist/ui/ccstyle/diff/write-display-utils.d.ts +2 -0
- package/dist/ui/ccstyle/diff/write-display-utils.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/write-display-utils.js +12 -0
- package/dist/ui/ccstyle/diff/write-display-utils.js.map +1 -0
- package/dist/ui/ccstyle/tool-renderer.d.ts +17 -0
- package/dist/ui/ccstyle/tool-renderer.d.ts.map +1 -0
- package/dist/ui/ccstyle/tool-renderer.js +58 -0
- package/dist/ui/ccstyle/tool-renderer.js.map +1 -0
- package/dist/ui/ccstyle/tool-result.d.ts +24 -0
- package/dist/ui/ccstyle/tool-result.d.ts.map +1 -0
- package/dist/ui/ccstyle/tool-result.js +193 -0
- package/dist/ui/ccstyle/tool-result.js.map +1 -0
- package/dist/ui/conversation-blocks.d.ts +28 -0
- package/dist/ui/conversation-blocks.d.ts.map +1 -0
- package/dist/ui/conversation-blocks.js +119 -27
- package/dist/ui/conversation-blocks.js.map +1 -0
- package/dist/ui/conversation-nvim.d.ts +1 -0
- package/dist/ui/conversation-nvim.d.ts.map +1 -0
- package/dist/ui/conversation-nvim.js +1 -0
- package/dist/ui/conversation-nvim.js.map +1 -0
- package/dist/ui/conversation-role.d.ts +13 -0
- package/dist/ui/conversation-role.d.ts.map +1 -0
- package/dist/ui/conversation-role.js +39 -0
- package/dist/ui/conversation-role.js.map +1 -0
- package/dist/ui/conversation-search.d.ts +1 -0
- package/dist/ui/conversation-search.d.ts.map +1 -0
- package/dist/ui/conversation-search.js +1 -0
- package/dist/ui/conversation-search.js.map +1 -0
- package/dist/ui/conversation-timeline.d.ts +62 -0
- package/dist/ui/conversation-timeline.d.ts.map +1 -0
- package/dist/ui/conversation-timeline.js +263 -0
- package/dist/ui/conversation-timeline.js.map +1 -0
- package/dist/ui/conversation-viewer.d.ts +7 -1
- package/dist/ui/conversation-viewer.d.ts.map +1 -0
- package/dist/ui/conversation-viewer.js +96 -54
- package/dist/ui/conversation-viewer.js.map +1 -0
- package/dist/ui/fleet-list.d.ts +1 -0
- package/dist/ui/fleet-list.d.ts.map +1 -0
- package/dist/ui/fleet-list.js +1 -0
- package/dist/ui/fleet-list.js.map +1 -0
- package/dist/ui/schedule-menu.d.ts +1 -0
- package/dist/ui/schedule-menu.d.ts.map +1 -0
- package/dist/ui/schedule-menu.js +1 -0
- package/dist/ui/schedule-menu.js.map +1 -0
- package/dist/ui/viewer-keys.d.ts +1 -0
- package/dist/ui/viewer-keys.d.ts.map +1 -0
- package/dist/ui/viewer-keys.js +1 -0
- package/dist/ui/viewer-keys.js.map +1 -0
- package/dist/usage.d.ts +1 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +1 -0
- package/dist/usage.js.map +1 -0
- package/dist/worktree.d.ts +1 -0
- package/dist/worktree.d.ts.map +1 -0
- package/dist/worktree.js +1 -0
- package/dist/worktree.js.map +1 -0
- package/dist/write-execution.d.ts +42 -0
- package/dist/write-execution.d.ts.map +1 -0
- package/dist/write-execution.js +138 -0
- package/dist/write-execution.js.map +1 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +61 -0
- package/src/agent-runner.ts +7 -0
- package/src/index.ts +35 -11
- package/src/types.ts +4 -0
- package/src/ui/agent-widget.ts +2 -1
- package/src/ui/ccstyle/ATTRIBUTION.md +37 -0
- package/src/ui/ccstyle/diff/ansi-utils.ts +168 -0
- package/src/ui/ccstyle/diff/diff-presentation.ts +70 -0
- package/src/ui/ccstyle/diff/diff-renderer.ts +3148 -0
- package/src/ui/ccstyle/diff/line-width-safety.ts +92 -0
- package/src/ui/ccstyle/diff/render-utils.ts +32 -0
- package/src/ui/ccstyle/diff/types.ts +33 -0
- package/src/ui/ccstyle/diff/write-display-utils.ts +12 -0
- package/src/ui/ccstyle/tool-renderer.ts +71 -0
- package/src/ui/ccstyle/tool-result.ts +238 -0
- package/src/ui/conversation-blocks.ts +160 -26
- package/src/ui/conversation-role.ts +45 -0
- package/src/ui/conversation-timeline.ts +315 -0
- package/src/ui/conversation-viewer.ts +91 -54
- package/src/write-execution.ts +180 -0
- package/test/output-file-history.test.ts +50 -0
- package/test/ui/conversation-blocks.test.ts +31 -0
- package/test/ui/conversation-diff.test.ts +91 -0
- package/test/ui/conversation-viewer.test.ts +50 -0
- package/test/write-execution.test.ts +83 -0
- package/tsconfig.json +16 -0
- package/vitest.temp.config.mts +6 -0
|
@@ -3,6 +3,15 @@ import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
export type ConversationBlockKind = "text" | "tool" | "meta";
|
|
4
4
|
export type ConversationBlockRole = "user" | "assistant" | "tool" | "custom" | "meta";
|
|
5
5
|
|
|
6
|
+
/** The durable result snapshot attached to a normalized tool block. */
|
|
7
|
+
export interface ConversationToolResultSnapshot {
|
|
8
|
+
toolCallId?: string;
|
|
9
|
+
toolName?: string;
|
|
10
|
+
content?: unknown;
|
|
11
|
+
details?: unknown;
|
|
12
|
+
isError?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
export interface ConversationBlock {
|
|
7
16
|
id: string;
|
|
8
17
|
kind: ConversationBlockKind;
|
|
@@ -12,6 +21,18 @@ export interface ConversationBlock {
|
|
|
12
21
|
copyText: string;
|
|
13
22
|
fullText: string;
|
|
14
23
|
toolLine?: string;
|
|
24
|
+
/** Stable call id, including a synthetic id when the provider omitted one. */
|
|
25
|
+
toolCallId?: string;
|
|
26
|
+
toolName?: string;
|
|
27
|
+
/** Original tool arguments are retained for rich input rendering. */
|
|
28
|
+
toolArguments?: unknown;
|
|
29
|
+
/** Compatibility-facing short alias used by the timeline renderer. */
|
|
30
|
+
args?: unknown;
|
|
31
|
+
/** Result details/content are retained for rich output and durable diff rendering. */
|
|
32
|
+
toolResult?: ConversationToolResultSnapshot;
|
|
33
|
+
/** Compatibility-facing short alias used by the timeline renderer. */
|
|
34
|
+
result?: ConversationToolResultSnapshot;
|
|
35
|
+
toolStatus?: "pending" | "success" | "error";
|
|
15
36
|
}
|
|
16
37
|
|
|
17
38
|
export interface ConversationFormatOptions {
|
|
@@ -29,11 +50,17 @@ type UnknownMessage = {
|
|
|
29
50
|
command?: unknown;
|
|
30
51
|
output?: unknown;
|
|
31
52
|
toolName?: unknown;
|
|
53
|
+
toolCallId?: unknown;
|
|
54
|
+
id?: unknown;
|
|
55
|
+
name?: unknown;
|
|
56
|
+
details?: unknown;
|
|
32
57
|
isError?: unknown;
|
|
33
58
|
customType?: unknown;
|
|
34
59
|
display?: unknown;
|
|
35
60
|
};
|
|
36
61
|
|
|
62
|
+
type UnknownToolCall = Record<string, unknown>;
|
|
63
|
+
|
|
37
64
|
function charLimit(value: number | undefined, fallback: number): number {
|
|
38
65
|
if (value === undefined || !Number.isFinite(value)) return fallback;
|
|
39
66
|
return Math.max(0, Math.floor(value));
|
|
@@ -49,7 +76,7 @@ function bound(value: string, limit: number): string {
|
|
|
49
76
|
}
|
|
50
77
|
|
|
51
78
|
function asString(value: unknown): string | undefined {
|
|
52
|
-
return typeof value === "string" ? value : undefined;
|
|
79
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
53
80
|
}
|
|
54
81
|
|
|
55
82
|
/** Extract displayable text without leaking provider-specific or unknown parts. */
|
|
@@ -76,7 +103,7 @@ function firstNonEmptyLine(text: string): string {
|
|
|
76
103
|
return line.trim().replace(/\s+/g, " ");
|
|
77
104
|
}
|
|
78
105
|
|
|
79
|
-
function toolSummary(name: string, call:
|
|
106
|
+
function toolSummary(name: string, call: UnknownToolCall): string {
|
|
80
107
|
const args = call.arguments && typeof call.arguments === "object"
|
|
81
108
|
? call.arguments as Record<string, unknown>
|
|
82
109
|
: call.input && typeof call.input === "object"
|
|
@@ -100,6 +127,18 @@ function blockId(messageIndex: number, emitted: number, toolIndex?: number): str
|
|
|
100
127
|
return `message-${messageIndex}-tool-${toolIndex ?? emitted}`;
|
|
101
128
|
}
|
|
102
129
|
|
|
130
|
+
function uniqueBlockId(preferred: string, used: Set<string>): string {
|
|
131
|
+
if (!used.has(preferred)) {
|
|
132
|
+
used.add(preferred);
|
|
133
|
+
return preferred;
|
|
134
|
+
}
|
|
135
|
+
let suffix = 2;
|
|
136
|
+
while (used.has(`${preferred}-${suffix}`)) suffix++;
|
|
137
|
+
const id = `${preferred}-${suffix}`;
|
|
138
|
+
used.add(id);
|
|
139
|
+
return id;
|
|
140
|
+
}
|
|
141
|
+
|
|
103
142
|
function textBlock(
|
|
104
143
|
id: string,
|
|
105
144
|
role: "user" | "assistant",
|
|
@@ -118,37 +157,73 @@ function textBlock(
|
|
|
118
157
|
|
|
119
158
|
function toolCallBlock(
|
|
120
159
|
id: string,
|
|
160
|
+
callId: string,
|
|
121
161
|
name: string,
|
|
162
|
+
call: UnknownToolCall,
|
|
122
163
|
summary: string,
|
|
123
164
|
): ConversationBlock {
|
|
124
165
|
const toolLine = `· ${name} → ${summary || "(no details)"}`;
|
|
125
166
|
return {
|
|
126
167
|
id,
|
|
127
168
|
kind: "tool",
|
|
128
|
-
role: "
|
|
129
|
-
header:
|
|
169
|
+
role: "tool",
|
|
170
|
+
header: name,
|
|
130
171
|
markdown: "",
|
|
131
172
|
copyText: toolLine,
|
|
132
173
|
fullText: toolLine,
|
|
133
174
|
toolLine,
|
|
175
|
+
toolCallId: callId,
|
|
176
|
+
toolName: name,
|
|
177
|
+
toolArguments: call.arguments ?? call.input,
|
|
178
|
+
args: call.arguments ?? call.input,
|
|
179
|
+
toolStatus: "pending",
|
|
134
180
|
};
|
|
135
181
|
}
|
|
136
182
|
|
|
137
|
-
function
|
|
138
|
-
|
|
139
|
-
|
|
183
|
+
function resultSnapshot(message: UnknownMessage): ConversationToolResultSnapshot {
|
|
184
|
+
return {
|
|
185
|
+
toolCallId: asString(message.toolCallId) ?? asString(message.id),
|
|
186
|
+
toolName: asString(message.toolName) ?? asString(message.name),
|
|
187
|
+
content: message.content,
|
|
188
|
+
details: message.details,
|
|
189
|
+
isError: message.isError === true,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function applyToolResult(
|
|
194
|
+
block: ConversationBlock,
|
|
195
|
+
result: ConversationToolResultSnapshot,
|
|
140
196
|
maxChars: number,
|
|
141
|
-
):
|
|
142
|
-
const name =
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
if (!normalized.trim() && maxChars === 0) return undefined;
|
|
146
|
-
const success = message.isError === true ? "✗" : "✓";
|
|
197
|
+
): void {
|
|
198
|
+
const name = result.toolName ?? block.toolName ?? "tool";
|
|
199
|
+
const normalized = contentText(result.content).replace(/\r\n?/g, "\n");
|
|
200
|
+
const success = result.isError ? "✗" : "✓";
|
|
147
201
|
const lineCount = normalized.length === 0 ? 0 : normalized.split("\n").length;
|
|
148
202
|
const charCount = normalized.length;
|
|
149
203
|
const summary = firstNonEmptyLine(normalized);
|
|
204
|
+
block.toolResult = result;
|
|
205
|
+
block.result = result;
|
|
206
|
+
block.toolStatus = result.isError ? "error" : "success";
|
|
207
|
+
block.toolName = name;
|
|
208
|
+
block.header = name;
|
|
209
|
+
block.copyText = bound(`${name} ${success}\n${normalized}`, maxChars);
|
|
210
|
+
block.fullText = block.copyText;
|
|
211
|
+
block.toolLine = `· ${name} ${success} ${summary} ${lineCount}L/${charCount}c`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function orphanToolResultBlock(
|
|
215
|
+
id: string,
|
|
216
|
+
result: ConversationToolResultSnapshot,
|
|
217
|
+
maxChars: number,
|
|
218
|
+
): ConversationBlock {
|
|
219
|
+
const name = result.toolName ?? "tool";
|
|
220
|
+
const output = contentText(result.content).replace(/\r\n?/g, "\n");
|
|
221
|
+
const success = result.isError ? "✗" : "✓";
|
|
222
|
+
const lineCount = output.length === 0 ? 0 : output.split("\n").length;
|
|
223
|
+
const charCount = output.length;
|
|
224
|
+
const summary = firstNonEmptyLine(output);
|
|
150
225
|
const toolLine = `· ${name} ${success} ${summary} ${lineCount}L/${charCount}c`;
|
|
151
|
-
const fullText = bound(`${name} ${success}\n${
|
|
226
|
+
const fullText = bound(`${name} ${success}\n${output}`, maxChars);
|
|
152
227
|
return {
|
|
153
228
|
id,
|
|
154
229
|
kind: "tool",
|
|
@@ -158,6 +233,11 @@ function toolResultBlock(
|
|
|
158
233
|
copyText: fullText,
|
|
159
234
|
fullText,
|
|
160
235
|
toolLine,
|
|
236
|
+
toolCallId: result.toolCallId,
|
|
237
|
+
toolName: name,
|
|
238
|
+
toolResult: result,
|
|
239
|
+
result,
|
|
240
|
+
toolStatus: result.isError ? "error" : "success",
|
|
161
241
|
};
|
|
162
242
|
}
|
|
163
243
|
|
|
@@ -165,11 +245,10 @@ function bashBlock(
|
|
|
165
245
|
id: string,
|
|
166
246
|
message: UnknownMessage,
|
|
167
247
|
maxChars: number,
|
|
168
|
-
): ConversationBlock
|
|
248
|
+
): ConversationBlock {
|
|
169
249
|
const command = asString(message.command) ?? "";
|
|
170
250
|
const output = asString(message.output) ?? contentText(message.content);
|
|
171
251
|
const body = command ? `$ ${command}${output ? `\n${output}` : ""}` : output;
|
|
172
|
-
if (!body.trim() && maxChars === 0) return undefined;
|
|
173
252
|
const toolLine = `· bash → ${command || "(no command)"}`;
|
|
174
253
|
const fullText = bound(body, maxChars);
|
|
175
254
|
return {
|
|
@@ -181,6 +260,7 @@ function bashBlock(
|
|
|
181
260
|
copyText: fullText,
|
|
182
261
|
fullText,
|
|
183
262
|
toolLine,
|
|
263
|
+
toolName: "bash",
|
|
184
264
|
};
|
|
185
265
|
}
|
|
186
266
|
|
|
@@ -204,6 +284,13 @@ function metaBlock(
|
|
|
204
284
|
};
|
|
205
285
|
}
|
|
206
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Convert provider messages into stable, render-oriented blocks.
|
|
289
|
+
*
|
|
290
|
+
* Tool results are attached to the original call when possible. This keeps the
|
|
291
|
+
* block in call/source order while preserving the complete result snapshot for
|
|
292
|
+
* rich rendering. Results that cannot be matched remain visible as orphan cards.
|
|
293
|
+
*/
|
|
207
294
|
export function formatConversationMessages(
|
|
208
295
|
messages: AgentSession["messages"],
|
|
209
296
|
options?: ConversationFormatOptions,
|
|
@@ -211,6 +298,26 @@ export function formatConversationMessages(
|
|
|
211
298
|
const maxEntryChars = charLimit(options?.maxEntryChars, DEFAULT_MAX_CHARS);
|
|
212
299
|
const maxToolResultChars = charLimit(options?.maxToolResultChars, DEFAULT_MAX_CHARS);
|
|
213
300
|
const blocks: ConversationBlock[] = [];
|
|
301
|
+
const usedBlockIds = new Set<string>();
|
|
302
|
+
const callsById = new Map<string, ConversationBlock>();
|
|
303
|
+
const callsByName = new Map<string, ConversationBlock[]>();
|
|
304
|
+
|
|
305
|
+
const registerCall = (name: string, block: ConversationBlock) => {
|
|
306
|
+
callsById.set(block.toolCallId!, block);
|
|
307
|
+
const nameKey = name.toLowerCase();
|
|
308
|
+
const candidates = callsByName.get(nameKey) ?? [];
|
|
309
|
+
candidates.push(block);
|
|
310
|
+
callsByName.set(nameKey, candidates);
|
|
311
|
+
};
|
|
312
|
+
const unregisterCall = (block: ConversationBlock) => {
|
|
313
|
+
if (block.toolCallId) callsById.delete(block.toolCallId);
|
|
314
|
+
const nameKey = (block.toolName ?? "tool").toLowerCase();
|
|
315
|
+
const candidates = callsByName.get(nameKey);
|
|
316
|
+
if (!candidates) return;
|
|
317
|
+
const index = candidates.indexOf(block);
|
|
318
|
+
if (index >= 0) candidates.splice(index, 1);
|
|
319
|
+
if (candidates.length === 0) callsByName.delete(nameKey);
|
|
320
|
+
};
|
|
214
321
|
|
|
215
322
|
for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
|
|
216
323
|
const message = messages[messageIndex] as unknown as UnknownMessage;
|
|
@@ -219,49 +326,76 @@ export function formatConversationMessages(
|
|
|
219
326
|
|
|
220
327
|
if (role === "user") {
|
|
221
328
|
const text = bound(contentText(message.content), maxEntryChars);
|
|
222
|
-
if (text.trim())
|
|
329
|
+
if (text.trim()) {
|
|
330
|
+
blocks.push(textBlock(uniqueBlockId(blockId(messageIndex, emitted), usedBlockIds), "user", text));
|
|
331
|
+
}
|
|
223
332
|
continue;
|
|
224
333
|
}
|
|
225
334
|
|
|
226
335
|
if (role === "assistant") {
|
|
227
336
|
const parts = Array.isArray(message.content) ? message.content : [];
|
|
228
337
|
const text = bound(contentText(parts), maxEntryChars);
|
|
229
|
-
if (text.trim())
|
|
338
|
+
if (text.trim()) {
|
|
339
|
+
blocks.push(textBlock(uniqueBlockId(blockId(messageIndex, emitted++), usedBlockIds), "assistant", text));
|
|
340
|
+
}
|
|
230
341
|
|
|
231
342
|
let toolIndex = 0;
|
|
232
343
|
for (const part of parts) {
|
|
233
344
|
if (!part || typeof part !== "object" || (part as any).type !== "toolCall") continue;
|
|
234
|
-
const call = part as
|
|
345
|
+
const call = part as UnknownToolCall;
|
|
235
346
|
const name = asString(call.name) ?? asString(call.toolName) ?? "unknown";
|
|
236
|
-
|
|
347
|
+
const preferredId = asString(call.id) ?? asString(call.toolCallId)
|
|
348
|
+
?? `message-${messageIndex}-tool-${toolIndex}`;
|
|
349
|
+
const callId = callsById.has(preferredId)
|
|
350
|
+
? uniqueBlockId(preferredId, new Set(callsById.keys()))
|
|
351
|
+
: preferredId;
|
|
352
|
+
const block = toolCallBlock(
|
|
353
|
+
uniqueBlockId(blockId(messageIndex, emitted++, toolIndex), usedBlockIds),
|
|
354
|
+
callId,
|
|
355
|
+
name,
|
|
356
|
+
call,
|
|
357
|
+
toolSummary(name, call),
|
|
358
|
+
);
|
|
359
|
+
blocks.push(block);
|
|
360
|
+
registerCall(name, block);
|
|
237
361
|
toolIndex++;
|
|
238
362
|
}
|
|
239
363
|
continue;
|
|
240
364
|
}
|
|
241
365
|
|
|
242
366
|
if (role === "toolResult") {
|
|
243
|
-
const
|
|
244
|
-
|
|
367
|
+
const result = resultSnapshot(message);
|
|
368
|
+
const direct = result.toolCallId ? callsById.get(result.toolCallId) : undefined;
|
|
369
|
+
let match = direct;
|
|
370
|
+
if (!match && result.toolName) {
|
|
371
|
+
match = callsByName.get(result.toolName.toLowerCase())?.[0];
|
|
372
|
+
}
|
|
373
|
+
if (match) {
|
|
374
|
+
applyToolResult(match, result, maxToolResultChars);
|
|
375
|
+
unregisterCall(match);
|
|
376
|
+
} else {
|
|
377
|
+
const preferred = blockId(messageIndex, emitted);
|
|
378
|
+
blocks.push(orphanToolResultBlock(uniqueBlockId(preferred, usedBlockIds), result, maxToolResultChars));
|
|
379
|
+
}
|
|
245
380
|
continue;
|
|
246
381
|
}
|
|
247
382
|
|
|
248
383
|
if (role === "bashExecution") {
|
|
249
|
-
|
|
250
|
-
if (block) blocks.push(block);
|
|
384
|
+
blocks.push(bashBlock(uniqueBlockId(blockId(messageIndex, emitted), usedBlockIds), message, maxToolResultChars));
|
|
251
385
|
continue;
|
|
252
386
|
}
|
|
253
387
|
|
|
254
388
|
if (role === "custom") {
|
|
255
389
|
if (message.display === false) continue;
|
|
256
390
|
const header = asString(message.customType) || "Custom";
|
|
257
|
-
const block = metaBlock(blockId(messageIndex, emitted), header, message.content, maxEntryChars);
|
|
391
|
+
const block = metaBlock(uniqueBlockId(blockId(messageIndex, emitted), usedBlockIds), header, message.content, maxEntryChars);
|
|
258
392
|
if (block) blocks.push({ ...block, role: "custom" });
|
|
259
393
|
continue;
|
|
260
394
|
}
|
|
261
395
|
|
|
262
396
|
if (role === "compactionSummary" || role === "branchSummary" || role === "summary") {
|
|
263
397
|
const header = role === "summary" ? "Summary" : role === "compactionSummary" ? "Compaction summary" : "Branch summary";
|
|
264
|
-
const block = metaBlock(blockId(messageIndex, emitted), header, message.summary, maxEntryChars);
|
|
398
|
+
const block = metaBlock(uniqueBlockId(blockId(messageIndex, emitted), usedBlockIds), header, message.summary, maxEntryChars);
|
|
265
399
|
if (block) blocks.push(block);
|
|
266
400
|
}
|
|
267
401
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { ConversationBlock } from "./conversation-blocks.js";
|
|
3
|
+
import type { Theme } from "./agent-widget.js";
|
|
4
|
+
|
|
5
|
+
export type ConversationRoleTheme = Pick<Theme, "fg">;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Render a semantic role label without coloring the whole message body.
|
|
9
|
+
* These names intentionally mirror the labels used by peek-style viewers.
|
|
10
|
+
*/
|
|
11
|
+
export function conversationRoleLabel(block: ConversationBlock): string {
|
|
12
|
+
if (block.kind === "tool") return (block.toolName || "TOOL").toUpperCase();
|
|
13
|
+
if (block.role === "user") return "USER";
|
|
14
|
+
if (block.role === "assistant") return "ASSISTANT";
|
|
15
|
+
if (block.role === "custom") return block.header || "CUSTOM";
|
|
16
|
+
return block.header || "META";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function renderConversationRoleHeader(
|
|
20
|
+
block: ConversationBlock,
|
|
21
|
+
theme: ConversationRoleTheme,
|
|
22
|
+
): string {
|
|
23
|
+
const label = conversationRoleLabel(block);
|
|
24
|
+
const color = block.kind === "tool"
|
|
25
|
+
? "toolTitle"
|
|
26
|
+
: block.role === "user"
|
|
27
|
+
? "userMessageText"
|
|
28
|
+
: block.role === "assistant"
|
|
29
|
+
? "accent"
|
|
30
|
+
: block.role === "custom"
|
|
31
|
+
? "customMessageLabel"
|
|
32
|
+
: "muted";
|
|
33
|
+
const detail = block.kind === "tool" && block.toolCallId
|
|
34
|
+
? theme.fg("dim", ` · ${block.toolCallId}`)
|
|
35
|
+
: "";
|
|
36
|
+
return `${theme.fg(color, label)}${detail}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Adapter useful to callers that need a Component-shaped role row. */
|
|
40
|
+
export function conversationRoleComponent(block: ConversationBlock, theme: ConversationRoleTheme): Component {
|
|
41
|
+
return {
|
|
42
|
+
render: () => [` ${renderConversationRoleHeader(block, theme)}`],
|
|
43
|
+
invalidate() {},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import * as CodingAgent from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
Markdown,
|
|
4
|
+
Text,
|
|
5
|
+
type Component,
|
|
6
|
+
type TUI,
|
|
7
|
+
type TuiMouseEvent,
|
|
8
|
+
truncateToWidth,
|
|
9
|
+
} from "@earendil-works/pi-tui";
|
|
10
|
+
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import type { AgentRecord } from "../types.js";
|
|
12
|
+
import type { Theme } from "./agent-widget.js";
|
|
13
|
+
import {
|
|
14
|
+
type ConversationBlock,
|
|
15
|
+
type ConversationToolResultSnapshot,
|
|
16
|
+
} from "./conversation-blocks.js";
|
|
17
|
+
import { renderConversationRoleHeader } from "./conversation-role.js";
|
|
18
|
+
import { stripAnsi } from "./conversation-search.js";
|
|
19
|
+
import {
|
|
20
|
+
createViewerCcstyleTool,
|
|
21
|
+
type ViewerToolRenderers,
|
|
22
|
+
} from "./ccstyle/tool-renderer.js";
|
|
23
|
+
|
|
24
|
+
export interface TimelineRenderLine {
|
|
25
|
+
text: string;
|
|
26
|
+
plain: string;
|
|
27
|
+
blockIndex: number;
|
|
28
|
+
railable?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TimelineToolSpan {
|
|
32
|
+
toolCallId: string;
|
|
33
|
+
startLine: number;
|
|
34
|
+
height: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ConversationTimelineSnapshot {
|
|
38
|
+
lines: TimelineRenderLine[];
|
|
39
|
+
messageStarts: number[];
|
|
40
|
+
messageBlocks: ConversationBlock[];
|
|
41
|
+
interactiveToolIds: string[];
|
|
42
|
+
toolSpans: TimelineToolSpan[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ConversationTimelineOptions {
|
|
46
|
+
cwd?: string;
|
|
47
|
+
record?: AgentRecord;
|
|
48
|
+
onChange?: () => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function safeWidth(width: number): number {
|
|
52
|
+
return Math.max(1, Math.floor(Number.isFinite(width) ? width : 1));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resultContent(result: ConversationToolResultSnapshot): Array<Record<string, unknown>> {
|
|
56
|
+
if (Array.isArray(result.content)) return result.content as Array<Record<string, unknown>>;
|
|
57
|
+
if (typeof result.content === "string") return [{ type: "text", text: result.content }];
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fallbackToolLines(block: ConversationBlock, width: number, expanded = false): string[] {
|
|
62
|
+
const summary = block.toolLine || block.fullText || block.toolName || "tool";
|
|
63
|
+
const output = block.toolResult
|
|
64
|
+
? block.toolResult.content
|
|
65
|
+
: undefined;
|
|
66
|
+
const outputText = typeof output === "string"
|
|
67
|
+
? output
|
|
68
|
+
: Array.isArray(output)
|
|
69
|
+
? output.filter((item: any) => item?.type === "text").map((item: any) => String(item.text ?? "")).join("\n")
|
|
70
|
+
: "";
|
|
71
|
+
const outputLines = outputText.split(/\r?\n/);
|
|
72
|
+
const displayOutput = expanded || outputLines.length <= 4
|
|
73
|
+
? outputLines
|
|
74
|
+
: [...outputLines.slice(0, 4), `… (${outputLines.length - 4} more lines · Enter to expand)`];
|
|
75
|
+
const body = outputText ? [summary, ...displayOutput].join("\n") : summary;
|
|
76
|
+
return body.split(/\r?\n/).map((line) => truncateToWidth(line, width, ""));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function applyToolComponentState(component: any, block: ConversationBlock, expanded: boolean): void {
|
|
80
|
+
component.updateArgs?.(block.args ?? block.toolArguments ?? {});
|
|
81
|
+
component.setArgsComplete?.();
|
|
82
|
+
if (block.result ?? block.toolResult) {
|
|
83
|
+
const result = (block.result ?? block.toolResult)!;
|
|
84
|
+
component.updateResult?.({
|
|
85
|
+
content: resultContent(result),
|
|
86
|
+
details: result.details,
|
|
87
|
+
isError: result.isError === true,
|
|
88
|
+
}, false);
|
|
89
|
+
}
|
|
90
|
+
component.setExpanded?.(expanded);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createToolComponent(
|
|
94
|
+
block: ConversationBlock,
|
|
95
|
+
tui: TUI,
|
|
96
|
+
cwd: string,
|
|
97
|
+
expanded: boolean,
|
|
98
|
+
): Component | undefined {
|
|
99
|
+
const Constructor = (CodingAgent as any).ToolExecutionComponent as (new (...args: any[]) => Component) | undefined;
|
|
100
|
+
if (typeof Constructor !== "function" || !block.toolName || !block.toolCallId) return undefined;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const renderer = createViewerCcstyleTool(block.toolName) as ViewerToolRenderers;
|
|
104
|
+
const component = new Constructor(
|
|
105
|
+
block.toolName,
|
|
106
|
+
block.toolCallId,
|
|
107
|
+
block.args ?? block.toolArguments ?? {},
|
|
108
|
+
{ showImages: false },
|
|
109
|
+
renderer,
|
|
110
|
+
tui,
|
|
111
|
+
cwd,
|
|
112
|
+
) as any;
|
|
113
|
+
component.markExecutionStarted?.();
|
|
114
|
+
applyToolComponentState(component, block, expanded);
|
|
115
|
+
return component;
|
|
116
|
+
} catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Component-aware, snapshot-driven conversation timeline.
|
|
123
|
+
* ToolExecutionComponent state is rebuilt from normalized blocks when a call id
|
|
124
|
+
* first appears and then reused; history never needs a live session, spinner,
|
|
125
|
+
* stream, or process handle.
|
|
126
|
+
*/
|
|
127
|
+
export class ConversationTimeline implements Component {
|
|
128
|
+
private blocks: ConversationBlock[] = [];
|
|
129
|
+
private showTools = true;
|
|
130
|
+
private focusedToolCallId: string | undefined;
|
|
131
|
+
private expandedTools = new Set<string>();
|
|
132
|
+
private toolComponents = new Map<string, Component & { handleMouse?: (event: TuiMouseEvent) => unknown }>();
|
|
133
|
+
private snapshot: ConversationTimelineSnapshot = {
|
|
134
|
+
lines: [],
|
|
135
|
+
messageStarts: [],
|
|
136
|
+
messageBlocks: [],
|
|
137
|
+
interactiveToolIds: [],
|
|
138
|
+
toolSpans: [],
|
|
139
|
+
};
|
|
140
|
+
private lastWidth = 0;
|
|
141
|
+
|
|
142
|
+
constructor(
|
|
143
|
+
private readonly tui: TUI,
|
|
144
|
+
private readonly theme: Theme,
|
|
145
|
+
private readonly options: ConversationTimelineOptions = {},
|
|
146
|
+
) {}
|
|
147
|
+
|
|
148
|
+
setBlocks(blocks: readonly ConversationBlock[]): void {
|
|
149
|
+
this.blocks = [...blocks];
|
|
150
|
+
const ids = new Set(this.blocks.filter((block) => block.kind === "tool" && block.toolCallId).map((block) => block.toolCallId!));
|
|
151
|
+
if (this.focusedToolCallId && !ids.has(this.focusedToolCallId)) this.focusedToolCallId = undefined;
|
|
152
|
+
for (const id of this.expandedTools) if (!ids.has(id)) this.expandedTools.delete(id);
|
|
153
|
+
for (const id of this.toolComponents.keys()) if (!ids.has(id)) this.toolComponents.delete(id);
|
|
154
|
+
this.invalidate();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setShowTools(show: boolean): void {
|
|
158
|
+
if (this.showTools === show) return;
|
|
159
|
+
this.showTools = show;
|
|
160
|
+
if (!show) this.focusedToolCallId = undefined;
|
|
161
|
+
this.invalidate();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
getShowTools(): boolean {
|
|
165
|
+
return this.showTools;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
setFocusedToolCallId(id: string | undefined): void {
|
|
169
|
+
this.focusedToolCallId = id;
|
|
170
|
+
this.invalidate();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
getFocusedToolCallId(): string | undefined {
|
|
174
|
+
return this.focusedToolCallId;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
getSnapshot(): ConversationTimelineSnapshot {
|
|
178
|
+
if (this.lastWidth <= 0) this.render(80);
|
|
179
|
+
return this.snapshot;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
moveFocus(direction: 1 | -1): boolean {
|
|
183
|
+
const ids = this.getSnapshot().interactiveToolIds;
|
|
184
|
+
if (ids.length === 0) return false;
|
|
185
|
+
const current = this.focusedToolCallId ? ids.indexOf(this.focusedToolCallId) : -1;
|
|
186
|
+
const next = current < 0
|
|
187
|
+
? direction > 0 ? 0 : ids.length - 1
|
|
188
|
+
: (current + direction + ids.length) % ids.length;
|
|
189
|
+
this.focusedToolCallId = ids[next];
|
|
190
|
+
this.invalidate();
|
|
191
|
+
this.options.onChange?.();
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
toggleFocusedTool(): boolean {
|
|
196
|
+
const id = this.focusedToolCallId;
|
|
197
|
+
if (!id) return false;
|
|
198
|
+
if (this.expandedTools.has(id)) this.expandedTools.delete(id);
|
|
199
|
+
else this.expandedTools.add(id);
|
|
200
|
+
this.invalidate();
|
|
201
|
+
this.options.onChange?.();
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
isToolFocused(): boolean {
|
|
206
|
+
return this.focusedToolCallId !== undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
render(width: number): string[] {
|
|
210
|
+
const requestedWidth = safeWidth(width);
|
|
211
|
+
const lines: TimelineRenderLine[] = [];
|
|
212
|
+
const messageStarts: number[] = [];
|
|
213
|
+
const messageBlocks: ConversationBlock[] = [];
|
|
214
|
+
const interactiveToolIds: string[] = [];
|
|
215
|
+
const toolSpans: TimelineToolSpan[] = [];
|
|
216
|
+
const visibleBlocks = this.blocks.filter((block) => this.showTools || block.kind !== "tool");
|
|
217
|
+
|
|
218
|
+
if (visibleBlocks.length === 0) {
|
|
219
|
+
const text = this.blocks.length === 0 ? "(waiting for first message...)" : "(tools hidden)";
|
|
220
|
+
lines.push({ text: this.theme.fg("dim", text), plain: text, blockIndex: -1 });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const block of visibleBlocks) {
|
|
224
|
+
const blockIndex = this.blocks.indexOf(block);
|
|
225
|
+
if (lines.length > 0) {
|
|
226
|
+
const separator = this.theme.fg("border", " " + "─ ".repeat(Math.max(1, Math.floor((requestedWidth - 6) / 2))).trimEnd());
|
|
227
|
+
lines.push({ text: truncateToWidth(separator, requestedWidth), plain: stripAnsi(separator), blockIndex });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const headerLine = ` ${renderConversationRoleHeader(block, this.theme)}`;
|
|
231
|
+
messageStarts.push(lines.length);
|
|
232
|
+
messageBlocks.push(block);
|
|
233
|
+
lines.push({ text: truncateToWidth(headerLine, requestedWidth), plain: stripAnsi(headerLine), blockIndex, railable: true });
|
|
234
|
+
|
|
235
|
+
if (block.kind === "tool") {
|
|
236
|
+
const id = block.toolCallId;
|
|
237
|
+
if (id) interactiveToolIds.push(id);
|
|
238
|
+
let component: (Component & { handleMouse?: (event: TuiMouseEvent) => unknown }) | undefined;
|
|
239
|
+
if (id) {
|
|
240
|
+
const existing = this.toolComponents.get(id);
|
|
241
|
+
if (existing) {
|
|
242
|
+
applyToolComponentState(existing, block, this.expandedTools.has(id));
|
|
243
|
+
component = existing;
|
|
244
|
+
} else {
|
|
245
|
+
component = createToolComponent(block, this.tui, this.options.cwd ?? process.cwd(), this.expandedTools.has(id)) as (Component & { handleMouse?: (event: TuiMouseEvent) => unknown }) | undefined;
|
|
246
|
+
if (component) this.toolComponents.set(id, component);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
let rendered: string[];
|
|
250
|
+
try {
|
|
251
|
+
rendered = component?.render(requestedWidth) ?? fallbackToolLines(block, requestedWidth, id ? this.expandedTools.has(id) : false);
|
|
252
|
+
} catch {
|
|
253
|
+
// Pi's interactive theme is a capability, not a viewer dependency;
|
|
254
|
+
// test hosts and older Pi versions can lack its initialized theme.
|
|
255
|
+
rendered = fallbackToolLines(block, requestedWidth, id ? this.expandedTools.has(id) : false);
|
|
256
|
+
}
|
|
257
|
+
for (const line of rendered) {
|
|
258
|
+
lines.push({ text: truncateToWidth(line, requestedWidth), plain: stripAnsi(line), blockIndex, railable: true });
|
|
259
|
+
}
|
|
260
|
+
if (id) toolSpans.push({ toolCallId: id, startLine: messageStarts[messageStarts.length - 1], height: lines.length - messageStarts[messageStarts.length - 1] });
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const markdown = new Markdown(block.markdown || "∅", 2, 0, getMarkdownTheme(), {
|
|
266
|
+
color: (text) => this.theme.fg(block.role === "meta" ? "muted" : "text", text),
|
|
267
|
+
});
|
|
268
|
+
for (const line of markdown.render(requestedWidth)) {
|
|
269
|
+
lines.push({ text: truncateToWidth(line, requestedWidth), plain: stripAnsi(line), blockIndex, railable: true });
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
try {
|
|
273
|
+
for (const line of new Text(block.markdown || "∅").render(requestedWidth)) {
|
|
274
|
+
lines.push({ text: truncateToWidth(line, requestedWidth), plain: stripAnsi(line), blockIndex, railable: true });
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
for (const line of (block.markdown || "∅").split("\n")) {
|
|
278
|
+
lines.push({ text: truncateToWidth(line, requestedWidth), plain: line, blockIndex, railable: true });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this.lastWidth = requestedWidth;
|
|
285
|
+
this.snapshot = { lines, messageStarts, messageBlocks, interactiveToolIds, toolSpans };
|
|
286
|
+
return lines.map((line) => line.text);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
handleMouse(event: TuiMouseEvent): { handled?: boolean; render?: boolean } | undefined {
|
|
290
|
+
if (event.type !== "click" && event.type !== "press") return undefined;
|
|
291
|
+
const span = this.snapshot.toolSpans.find((candidate) => event.y >= candidate.startLine && event.y < candidate.startLine + candidate.height);
|
|
292
|
+
if (!span) return undefined;
|
|
293
|
+
this.focusedToolCallId = span.toolCallId;
|
|
294
|
+
const component = this.toolComponents.get(span.toolCallId);
|
|
295
|
+
try {
|
|
296
|
+
// Keep Pi's normalized mouse contract in the component tree. The local
|
|
297
|
+
// role header occupies the first row; ToolExecutionComponent receives
|
|
298
|
+
// the remaining card-relative coordinate.
|
|
299
|
+
component?.handleMouse?.({ ...event, y: event.y - span.startLine, height: span.height });
|
|
300
|
+
} catch {
|
|
301
|
+
// A missing/older Pi mouse implementation must not break the viewer.
|
|
302
|
+
}
|
|
303
|
+
this.expandedTools.has(span.toolCallId)
|
|
304
|
+
? this.expandedTools.delete(span.toolCallId)
|
|
305
|
+
: this.expandedTools.add(span.toolCallId);
|
|
306
|
+
this.invalidate();
|
|
307
|
+
this.options.onChange?.();
|
|
308
|
+
return { handled: true, render: true };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
invalidate(): void {
|
|
312
|
+
this.lastWidth = 0;
|
|
313
|
+
this.snapshot = { lines: [], messageStarts: [], messageBlocks: [], interactiveToolIds: [], toolSpans: [] };
|
|
314
|
+
}
|
|
315
|
+
}
|