@esso0428/pi-subagents 0.15.5 → 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 +10 -19
- package/README.md +5 -5
- package/dist/agent-history-list.d.ts +19 -0
- package/dist/agent-history-list.d.ts.map +1 -0
- package/dist/agent-history-list.js +69 -0
- package/dist/agent-history-list.js.map +1 -0
- package/dist/agent-history.d.ts +2 -1
- package/dist/agent-history.d.ts.map +1 -0
- package/dist/agent-history.js +32 -34
- package/dist/agent-history.js.map +1 -0
- package/dist/agent-manager.d.ts +24 -8
- package/dist/agent-manager.d.ts.map +1 -0
- package/dist/agent-manager.js +171 -93
- 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 +75 -86
- 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 +28 -45
- 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 +3 -6
- 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 +6 -9
- 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 +16 -18
- 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 +2 -4
- 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 +12 -16
- 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 +2 -4
- 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 +2 -5
- 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 +306 -292
- 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 +3 -6
- 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 +25 -34
- 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 +2 -4
- 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 +15 -20
- 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 +17 -22
- 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 +2 -4
- 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 +17 -21
- 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 +9 -12
- 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 +16 -23
- 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 +21 -23
- 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 +2 -4
- package/dist/status-note.js.map +1 -0
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -4
- package/dist/types.js.map +1 -0
- package/dist/ui/agent-widget.d.ts +2 -5
- package/dist/ui/agent-widget.d.ts.map +1 -0
- package/dist/ui/agent-widget.js +29 -51
- 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 +46 -0
- package/dist/ui/conversation-blocks.d.ts.map +1 -0
- package/dist/ui/conversation-blocks.js +306 -0
- package/dist/ui/conversation-blocks.js.map +1 -0
- package/dist/ui/conversation-nvim.d.ts +8 -0
- package/dist/ui/conversation-nvim.d.ts.map +1 -0
- package/dist/ui/conversation-nvim.js +117 -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 +39 -0
- package/dist/ui/conversation-search.d.ts.map +1 -0
- package/dist/ui/conversation-search.js +124 -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 +62 -20
- package/dist/ui/conversation-viewer.d.ts.map +1 -0
- package/dist/ui/conversation-viewer.js +449 -224
- package/dist/ui/conversation-viewer.js.map +1 -0
- package/dist/ui/fleet-list.d.ts +24 -11
- package/dist/ui/fleet-list.d.ts.map +1 -0
- package/dist/ui/fleet-list.js +86 -78
- 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 +2 -4
- 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 +8 -10
- 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 +5 -10
- 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 +27 -31
- 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-history-list.ts +91 -0
- package/src/agent-history.ts +12 -17
- package/src/agent-manager.ts +169 -81
- package/src/agent-runner.ts +12 -13
- package/src/index.ts +117 -98
- package/src/output-file.ts +1 -1
- package/src/types.ts +4 -1
- package/src/ui/agent-widget.ts +3 -9
- 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 +404 -0
- package/src/ui/conversation-nvim.ts +127 -0
- package/src/ui/conversation-role.ts +45 -0
- package/src/ui/conversation-search.ts +161 -0
- package/src/ui/conversation-timeline.ts +315 -0
- package/src/ui/conversation-viewer.ts +418 -210
- package/src/ui/fleet-list.ts +73 -52
- package/src/write-execution.ts +180 -0
- package/test/agent-history-list.test.ts +121 -0
- package/test/agent-manager-history.test.ts +59 -58
- package/test/agent-menu-history-navigation.test.ts +86 -0
- package/test/output-file-history.test.ts +50 -0
- package/test/ui/conversation-blocks.test.ts +103 -0
- package/test/ui/conversation-diff.test.ts +91 -0
- package/test/ui/conversation-search.test.ts +55 -0
- package/test/ui/conversation-viewer.test.ts +182 -0
- package/test/ui/fleet-list.test.ts +131 -0
- package/test/write-execution.test.ts +83 -0
- package/tsconfig.json +16 -0
- package/vitest.temp.config.mts +6 -0
- package/dist/ui/markdown-result.d.ts +0 -3
- package/dist/ui/markdown-result.js +0 -53
- package/src/ui/markdown-result.ts +0 -56
- package/test/agent-history.test.ts +0 -63
- package/test/ui/markdown-result.test.ts +0 -45
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export type ConversationBlockKind = "text" | "tool" | "meta";
|
|
4
|
+
export type ConversationBlockRole = "user" | "assistant" | "tool" | "custom" | "meta";
|
|
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
|
+
|
|
15
|
+
export interface ConversationBlock {
|
|
16
|
+
id: string;
|
|
17
|
+
kind: ConversationBlockKind;
|
|
18
|
+
role: ConversationBlockRole;
|
|
19
|
+
header: string;
|
|
20
|
+
markdown: string;
|
|
21
|
+
copyText: string;
|
|
22
|
+
fullText: string;
|
|
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";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ConversationFormatOptions {
|
|
39
|
+
maxEntryChars?: number;
|
|
40
|
+
maxToolResultChars?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const DEFAULT_MAX_CHARS = 16_000;
|
|
44
|
+
const TRUNCATION_NOTICE = "\n… [truncated in subagent viewer]";
|
|
45
|
+
|
|
46
|
+
type UnknownMessage = {
|
|
47
|
+
role?: unknown;
|
|
48
|
+
content?: unknown;
|
|
49
|
+
summary?: unknown;
|
|
50
|
+
command?: unknown;
|
|
51
|
+
output?: unknown;
|
|
52
|
+
toolName?: unknown;
|
|
53
|
+
toolCallId?: unknown;
|
|
54
|
+
id?: unknown;
|
|
55
|
+
name?: unknown;
|
|
56
|
+
details?: unknown;
|
|
57
|
+
isError?: unknown;
|
|
58
|
+
customType?: unknown;
|
|
59
|
+
display?: unknown;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type UnknownToolCall = Record<string, unknown>;
|
|
63
|
+
|
|
64
|
+
function charLimit(value: number | undefined, fallback: number): number {
|
|
65
|
+
if (value === undefined || !Number.isFinite(value)) return fallback;
|
|
66
|
+
return Math.max(0, Math.floor(value));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Normalize terminal/platform line endings, then keep the result bounded. */
|
|
70
|
+
function bound(value: string, limit: number): string {
|
|
71
|
+
const text = value.replace(/\r\n?/g, "\n");
|
|
72
|
+
if (text.length <= limit) return text;
|
|
73
|
+
if (limit <= 0) return "";
|
|
74
|
+
if (limit <= TRUNCATION_NOTICE.length) return TRUNCATION_NOTICE.slice(0, limit);
|
|
75
|
+
return text.slice(0, limit - TRUNCATION_NOTICE.length) + TRUNCATION_NOTICE;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function asString(value: unknown): string | undefined {
|
|
79
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Extract displayable text without leaking provider-specific or unknown parts. */
|
|
83
|
+
function contentText(content: unknown): string {
|
|
84
|
+
if (typeof content === "string") return content;
|
|
85
|
+
if (!Array.isArray(content)) return "";
|
|
86
|
+
|
|
87
|
+
const parts: string[] = [];
|
|
88
|
+
for (const part of content) {
|
|
89
|
+
if (!part || typeof part !== "object") continue;
|
|
90
|
+
const item = part as Record<string, unknown>;
|
|
91
|
+
if (item.type === "text" && typeof item.text === "string") {
|
|
92
|
+
parts.push(item.text);
|
|
93
|
+
} else if (item.type === "image") {
|
|
94
|
+
parts.push("[image]");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return parts.join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function firstNonEmptyLine(text: string): string {
|
|
101
|
+
const line = text.split("\n").find((candidate) => candidate.trim().length > 0);
|
|
102
|
+
if (!line) return "(no output)";
|
|
103
|
+
return line.trim().replace(/\s+/g, " ");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function toolSummary(name: string, call: UnknownToolCall): string {
|
|
107
|
+
const args = call.arguments && typeof call.arguments === "object"
|
|
108
|
+
? call.arguments as Record<string, unknown>
|
|
109
|
+
: call.input && typeof call.input === "object"
|
|
110
|
+
? call.input as Record<string, unknown>
|
|
111
|
+
: {};
|
|
112
|
+
|
|
113
|
+
const path = asString(args.path) ?? asString(args.filePath) ?? asString(args.filename);
|
|
114
|
+
if (["read", "edit", "write"].includes(name) && path) return path;
|
|
115
|
+
if (asString(args.command)) return asString(args.command)!;
|
|
116
|
+
if (path) return path;
|
|
117
|
+
try {
|
|
118
|
+
const encoded = JSON.stringify(args);
|
|
119
|
+
return encoded === "{}" ? "" : encoded;
|
|
120
|
+
} catch {
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function blockId(messageIndex: number, emitted: number, toolIndex?: number): string {
|
|
126
|
+
if (emitted === 0) return `message-${messageIndex}`;
|
|
127
|
+
return `message-${messageIndex}-tool-${toolIndex ?? emitted}`;
|
|
128
|
+
}
|
|
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
|
+
|
|
142
|
+
function textBlock(
|
|
143
|
+
id: string,
|
|
144
|
+
role: "user" | "assistant",
|
|
145
|
+
markdown: string,
|
|
146
|
+
): ConversationBlock {
|
|
147
|
+
return {
|
|
148
|
+
id,
|
|
149
|
+
kind: "text",
|
|
150
|
+
role,
|
|
151
|
+
header: role === "user" ? "User" : "Assistant",
|
|
152
|
+
markdown,
|
|
153
|
+
copyText: markdown,
|
|
154
|
+
fullText: markdown,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function toolCallBlock(
|
|
159
|
+
id: string,
|
|
160
|
+
callId: string,
|
|
161
|
+
name: string,
|
|
162
|
+
call: UnknownToolCall,
|
|
163
|
+
summary: string,
|
|
164
|
+
): ConversationBlock {
|
|
165
|
+
const toolLine = `· ${name} → ${summary || "(no details)"}`;
|
|
166
|
+
return {
|
|
167
|
+
id,
|
|
168
|
+
kind: "tool",
|
|
169
|
+
role: "tool",
|
|
170
|
+
header: name,
|
|
171
|
+
markdown: "",
|
|
172
|
+
copyText: toolLine,
|
|
173
|
+
fullText: toolLine,
|
|
174
|
+
toolLine,
|
|
175
|
+
toolCallId: callId,
|
|
176
|
+
toolName: name,
|
|
177
|
+
toolArguments: call.arguments ?? call.input,
|
|
178
|
+
args: call.arguments ?? call.input,
|
|
179
|
+
toolStatus: "pending",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
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,
|
|
196
|
+
maxChars: number,
|
|
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 ? "✗" : "✓";
|
|
201
|
+
const lineCount = normalized.length === 0 ? 0 : normalized.split("\n").length;
|
|
202
|
+
const charCount = normalized.length;
|
|
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);
|
|
225
|
+
const toolLine = `· ${name} ${success} ${summary} ${lineCount}L/${charCount}c`;
|
|
226
|
+
const fullText = bound(`${name} ${success}\n${output}`, maxChars);
|
|
227
|
+
return {
|
|
228
|
+
id,
|
|
229
|
+
kind: "tool",
|
|
230
|
+
role: "tool",
|
|
231
|
+
header: `${name} result`,
|
|
232
|
+
markdown: "",
|
|
233
|
+
copyText: fullText,
|
|
234
|
+
fullText,
|
|
235
|
+
toolLine,
|
|
236
|
+
toolCallId: result.toolCallId,
|
|
237
|
+
toolName: name,
|
|
238
|
+
toolResult: result,
|
|
239
|
+
result,
|
|
240
|
+
toolStatus: result.isError ? "error" : "success",
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function bashBlock(
|
|
245
|
+
id: string,
|
|
246
|
+
message: UnknownMessage,
|
|
247
|
+
maxChars: number,
|
|
248
|
+
): ConversationBlock {
|
|
249
|
+
const command = asString(message.command) ?? "";
|
|
250
|
+
const output = asString(message.output) ?? contentText(message.content);
|
|
251
|
+
const body = command ? `$ ${command}${output ? `\n${output}` : ""}` : output;
|
|
252
|
+
const toolLine = `· bash → ${command || "(no command)"}`;
|
|
253
|
+
const fullText = bound(body, maxChars);
|
|
254
|
+
return {
|
|
255
|
+
id,
|
|
256
|
+
kind: "tool",
|
|
257
|
+
role: "tool",
|
|
258
|
+
header: "bash",
|
|
259
|
+
markdown: "",
|
|
260
|
+
copyText: fullText,
|
|
261
|
+
fullText,
|
|
262
|
+
toolLine,
|
|
263
|
+
toolName: "bash",
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function metaBlock(
|
|
268
|
+
id: string,
|
|
269
|
+
header: string,
|
|
270
|
+
value: unknown,
|
|
271
|
+
maxChars: number,
|
|
272
|
+
): ConversationBlock | undefined {
|
|
273
|
+
const text = typeof value === "string" ? value : contentText(value);
|
|
274
|
+
const markdown = bound(text, maxChars);
|
|
275
|
+
if (!markdown.trim()) return undefined;
|
|
276
|
+
return {
|
|
277
|
+
id,
|
|
278
|
+
kind: "meta",
|
|
279
|
+
role: "meta",
|
|
280
|
+
header,
|
|
281
|
+
markdown,
|
|
282
|
+
copyText: markdown,
|
|
283
|
+
fullText: markdown,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
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
|
+
*/
|
|
294
|
+
export function formatConversationMessages(
|
|
295
|
+
messages: AgentSession["messages"],
|
|
296
|
+
options?: ConversationFormatOptions,
|
|
297
|
+
): ConversationBlock[] {
|
|
298
|
+
const maxEntryChars = charLimit(options?.maxEntryChars, DEFAULT_MAX_CHARS);
|
|
299
|
+
const maxToolResultChars = charLimit(options?.maxToolResultChars, DEFAULT_MAX_CHARS);
|
|
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
|
+
};
|
|
321
|
+
|
|
322
|
+
for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
|
|
323
|
+
const message = messages[messageIndex] as unknown as UnknownMessage;
|
|
324
|
+
const role = message?.role;
|
|
325
|
+
let emitted = 0;
|
|
326
|
+
|
|
327
|
+
if (role === "user") {
|
|
328
|
+
const text = bound(contentText(message.content), maxEntryChars);
|
|
329
|
+
if (text.trim()) {
|
|
330
|
+
blocks.push(textBlock(uniqueBlockId(blockId(messageIndex, emitted), usedBlockIds), "user", text));
|
|
331
|
+
}
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (role === "assistant") {
|
|
336
|
+
const parts = Array.isArray(message.content) ? message.content : [];
|
|
337
|
+
const text = bound(contentText(parts), maxEntryChars);
|
|
338
|
+
if (text.trim()) {
|
|
339
|
+
blocks.push(textBlock(uniqueBlockId(blockId(messageIndex, emitted++), usedBlockIds), "assistant", text));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let toolIndex = 0;
|
|
343
|
+
for (const part of parts) {
|
|
344
|
+
if (!part || typeof part !== "object" || (part as any).type !== "toolCall") continue;
|
|
345
|
+
const call = part as UnknownToolCall;
|
|
346
|
+
const name = asString(call.name) ?? asString(call.toolName) ?? "unknown";
|
|
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);
|
|
361
|
+
toolIndex++;
|
|
362
|
+
}
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (role === "toolResult") {
|
|
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
|
+
}
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (role === "bashExecution") {
|
|
384
|
+
blocks.push(bashBlock(uniqueBlockId(blockId(messageIndex, emitted), usedBlockIds), message, maxToolResultChars));
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (role === "custom") {
|
|
389
|
+
if (message.display === false) continue;
|
|
390
|
+
const header = asString(message.customType) || "Custom";
|
|
391
|
+
const block = metaBlock(uniqueBlockId(blockId(messageIndex, emitted), usedBlockIds), header, message.content, maxEntryChars);
|
|
392
|
+
if (block) blocks.push({ ...block, role: "custom" });
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (role === "compactionSummary" || role === "branchSummary" || role === "summary") {
|
|
397
|
+
const header = role === "summary" ? "Summary" : role === "compactionSummary" ? "Compaction summary" : "Branch summary";
|
|
398
|
+
const block = metaBlock(uniqueBlockId(blockId(messageIndex, emitted), usedBlockIds), header, message.summary, maxEntryChars);
|
|
399
|
+
if (block) blocks.push(block);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return blocks;
|
|
404
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { TUI } from "@earendil-works/pi-tui";
|
|
8
|
+
import type { ConversationBlock } from "./conversation-blocks.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_EDITOR = "nvim";
|
|
11
|
+
|
|
12
|
+
function safeFilename(role: string, id: string): string {
|
|
13
|
+
const safeRole = role.replace(/[^a-z0-9]/gi, "") || "message";
|
|
14
|
+
const safeId = id.replace(/[^a-z0-9]/gi, "").slice(0, 24) || "x";
|
|
15
|
+
return `${safeRole}-${safeId}.md`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function vimStringLiteral(value: string): string {
|
|
19
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function notify(ctx: ExtensionContext, message: string, level: "error" | "warning" | "info" = "error"): void {
|
|
23
|
+
ctx.ui.notify(message, level);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function runFullscreen(tui: TUI, ctx: ExtensionContext, editor: string, args: string[]): number | null {
|
|
27
|
+
try {
|
|
28
|
+
tui.stop();
|
|
29
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
30
|
+
const result = spawnSync(editor, args, {
|
|
31
|
+
cwd: ctx.cwd,
|
|
32
|
+
env: process.env,
|
|
33
|
+
stdio: "inherit",
|
|
34
|
+
});
|
|
35
|
+
if (result.error) {
|
|
36
|
+
notify(ctx, `${editor} failed: ${result.error.message}`);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return result.status;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
notify(ctx, `${editor} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
42
|
+
return null;
|
|
43
|
+
} finally {
|
|
44
|
+
try {
|
|
45
|
+
tui.start();
|
|
46
|
+
tui.requestRender(true);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
notify(ctx, `Could not restore terminal UI: ${error instanceof Error ? error.message : String(error)}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Open a bounded conversation block without allowing edits to the transcript. */
|
|
54
|
+
export async function viewConversationBlockInNvim(
|
|
55
|
+
tui: TUI,
|
|
56
|
+
ctx: ExtensionContext,
|
|
57
|
+
block: ConversationBlock,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
let dir: string | undefined;
|
|
60
|
+
try {
|
|
61
|
+
dir = await mkdtemp(path.join(tmpdir(), "pi-subagents-view-"));
|
|
62
|
+
const filePath = path.join(dir, safeFilename(block.role, block.id));
|
|
63
|
+
await writeFile(filePath, `${block.fullText}\n`, "utf8");
|
|
64
|
+
const editor = process.env.VISUAL || process.env.EDITOR || DEFAULT_EDITOR;
|
|
65
|
+
runFullscreen(tui, ctx, editor, ["-R", filePath]);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
notify(ctx, `Could not open block in nvim: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
+
} finally {
|
|
69
|
+
if (dir) {
|
|
70
|
+
try {
|
|
71
|
+
await rm(dir, { recursive: true, force: true });
|
|
72
|
+
} catch (error) {
|
|
73
|
+
notify(ctx, `Could not remove temporary nvim file: ${error instanceof Error ? error.message : String(error)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Edit a live assistant block and send the saved text as a new user message. */
|
|
80
|
+
export async function editLiveAssistantBlockInNvim(
|
|
81
|
+
pi: ExtensionAPI,
|
|
82
|
+
tui: TUI,
|
|
83
|
+
ctx: ExtensionContext,
|
|
84
|
+
block: ConversationBlock,
|
|
85
|
+
): Promise<boolean> {
|
|
86
|
+
if (block.role !== "assistant") {
|
|
87
|
+
notify(ctx, "Only assistant messages can be edited and sent back", "warning");
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let dir: string | undefined;
|
|
92
|
+
try {
|
|
93
|
+
dir = await mkdtemp(path.join(tmpdir(), "pi-subagents-edit-"));
|
|
94
|
+
const filePath = path.join(dir, safeFilename(block.role, block.id));
|
|
95
|
+
const savedPath = path.join(dir, "saved");
|
|
96
|
+
await writeFile(filePath, `${block.fullText}\n`, "utf8");
|
|
97
|
+
const editor = process.env.VISUAL || process.env.EDITOR || DEFAULT_EDITOR;
|
|
98
|
+
const autocmd = `autocmd BufWritePost <buffer> call writefile(['1'], ${vimStringLiteral(savedPath)}) | qall`;
|
|
99
|
+
const status = runFullscreen(tui, ctx, editor, ["-c", autocmd, filePath]);
|
|
100
|
+
if (status !== 0) {
|
|
101
|
+
notify(ctx, `${editor} exited with code ${status ?? "unknown"}`, "warning");
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if (!existsSync(savedPath)) {
|
|
105
|
+
notify(ctx, "No save detected; nothing sent", "info");
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
const edited = (await readFile(filePath, "utf8")).trimEnd();
|
|
109
|
+
if (!edited.trim()) {
|
|
110
|
+
notify(ctx, "Saved text is empty; nothing sent", "warning");
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
pi.sendUserMessage(`I edited an earlier assistant response in Neovim. Treat this as my correction / direction:\n\n${edited}`);
|
|
114
|
+
return true;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
notify(ctx, `Could not edit block in nvim: ${error instanceof Error ? error.message : String(error)}`);
|
|
117
|
+
return false;
|
|
118
|
+
} finally {
|
|
119
|
+
if (dir) {
|
|
120
|
+
try {
|
|
121
|
+
await rm(dir, { recursive: true, force: true });
|
|
122
|
+
} catch (error) {
|
|
123
|
+
notify(ctx, `Could not remove temporary nvim file: ${error instanceof Error ? error.message : String(error)}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -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
|
+
}
|