yoyo-pi 0.1.4
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/LICENSE +21 -0
- package/README.md +91 -0
- package/README.zh-CN.md +91 -0
- package/docs/previews/agent-status.svg +83 -0
- package/docs/previews/filetree.png +0 -0
- package/docs/previews/multiple-choice.png +0 -0
- package/docs/previews/pi-tui-agent-status.html +379 -0
- package/docs/previews/pi-tui-status-bar.html +481 -0
- package/docs/previews/single-choice.png +0 -0
- package/docs/previews/status-bar.png +0 -0
- package/docs/previews/todo-sidebar.png +0 -0
- package/extensions/choice-picker.ts +1404 -0
- package/extensions/clear-context.ts +164 -0
- package/extensions/gr0k-hack/index.ts +1231 -0
- package/extensions/kenx-infra/index.ts +1601 -0
- package/extensions/kenx-infra/themes/dark.json +80 -0
- package/extensions/kenx-infra/themes/light.json +80 -0
- package/extensions/kenx-infra/themes/paper.json +80 -0
- package/extensions/plan-mode/README.md +58 -0
- package/extensions/plan-mode/agents/plan-agent.md +98 -0
- package/extensions/plan-mode/index.ts +1956 -0
- package/extensions/plan-mode/sandbox.ts +332 -0
- package/extensions/vim-mode.ts +561 -0
- package/package.json +58 -0
|
@@ -0,0 +1,1231 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
createBashToolDefinition,
|
|
5
|
+
createEditToolDefinition,
|
|
6
|
+
createFindToolDefinition,
|
|
7
|
+
createGrepToolDefinition,
|
|
8
|
+
createLsToolDefinition,
|
|
9
|
+
createReadToolDefinition,
|
|
10
|
+
createWriteToolDefinition,
|
|
11
|
+
getAgentDir,
|
|
12
|
+
type AgentToolResult,
|
|
13
|
+
type BashToolDetails,
|
|
14
|
+
type EditToolDetails,
|
|
15
|
+
type ExtensionAPI,
|
|
16
|
+
type ExtensionCommandContext,
|
|
17
|
+
type ExtensionContext,
|
|
18
|
+
type FindToolDetails,
|
|
19
|
+
type GrepToolDetails,
|
|
20
|
+
type LsToolDetails,
|
|
21
|
+
type ReadToolDetails,
|
|
22
|
+
type Theme,
|
|
23
|
+
type ToolRenderContext,
|
|
24
|
+
} from "@earendil-works/pi-coding-agent";
|
|
25
|
+
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
26
|
+
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
27
|
+
|
|
28
|
+
const WIDGET_KEY = "gr0k-hack.agent-status";
|
|
29
|
+
const STATE_PATH = resolve(getAgentDir(), "state", "gr0k-hack.json");
|
|
30
|
+
const HIDE_DELAY_MS = 1200;
|
|
31
|
+
const MAX_CHANGED_FILES = 8;
|
|
32
|
+
|
|
33
|
+
const agentStatusVariantNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9] as const;
|
|
34
|
+
type AgentStatusVariant = (typeof agentStatusVariantNumbers)[number];
|
|
35
|
+
type AgentPhase = "thinking" | "executing" | "reading" | "writing";
|
|
36
|
+
type AgentTone = "red" | "yel" | "grn" | "blu" | "pur";
|
|
37
|
+
type AgentFillTone = "fred" | "fyel" | "fgrn" | "fblu" | "fdim";
|
|
38
|
+
|
|
39
|
+
type PersistedGr0kHackState = {
|
|
40
|
+
enabled?: boolean;
|
|
41
|
+
variant?: AgentStatusVariant;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type ToolActivity = {
|
|
45
|
+
name: string;
|
|
46
|
+
phase: AgentPhase;
|
|
47
|
+
path?: string;
|
|
48
|
+
startedAt: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type AgentStatusRuntimeState = {
|
|
52
|
+
enabled: boolean;
|
|
53
|
+
variant: AgentStatusVariant;
|
|
54
|
+
active: boolean;
|
|
55
|
+
phase: AgentPhase;
|
|
56
|
+
latestText: string;
|
|
57
|
+
latestTextSynthetic: boolean;
|
|
58
|
+
lastActivityText?: string;
|
|
59
|
+
currentPath?: string;
|
|
60
|
+
changedFiles: string[];
|
|
61
|
+
activeTools: Map<string, ToolActivity>;
|
|
62
|
+
requestRender?: () => void;
|
|
63
|
+
hideTimer?: ReturnType<typeof setTimeout>;
|
|
64
|
+
renderTicker?: ReturnType<typeof setInterval>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type AgentStatusScenario = {
|
|
68
|
+
label: AgentPhase;
|
|
69
|
+
tone: AgentTone;
|
|
70
|
+
tonefill: AgentFillTone;
|
|
71
|
+
glyph: string;
|
|
72
|
+
thought: string;
|
|
73
|
+
file: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
type TuiPalette = {
|
|
77
|
+
red: string;
|
|
78
|
+
yel: string;
|
|
79
|
+
grn: string;
|
|
80
|
+
blu: string;
|
|
81
|
+
pur: string;
|
|
82
|
+
dim: string;
|
|
83
|
+
faint: string;
|
|
84
|
+
redBg: string;
|
|
85
|
+
yelBg: string;
|
|
86
|
+
grnBg: string;
|
|
87
|
+
bluBg: string;
|
|
88
|
+
dimBg: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const RENDER_TICK_MS = 90;
|
|
92
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
93
|
+
const DOT_FRAMES = [" ", ". ", ".. ", "..."] as const;
|
|
94
|
+
const TUI_VARIANT_WIDTH = 86;
|
|
95
|
+
const TUI_MICRO_WIDTH = 64;
|
|
96
|
+
|
|
97
|
+
const TUI_PALETTES: Record<string, TuiPalette> = {
|
|
98
|
+
dark: {
|
|
99
|
+
red: "#D89B7E",
|
|
100
|
+
yel: "#D9B670",
|
|
101
|
+
grn: "#ADC79A",
|
|
102
|
+
blu: "#8FB4CE",
|
|
103
|
+
pur: "#BCA0C5",
|
|
104
|
+
dim: "#B0B0A5",
|
|
105
|
+
faint: "#777768",
|
|
106
|
+
redBg: "#464139",
|
|
107
|
+
yelBg: "#494739",
|
|
108
|
+
grnBg: "#40473D",
|
|
109
|
+
bluBg: "#3A4241",
|
|
110
|
+
dimBg: "#383A36",
|
|
111
|
+
},
|
|
112
|
+
light: {
|
|
113
|
+
red: "#A8734F",
|
|
114
|
+
yel: "#A88A50",
|
|
115
|
+
grn: "#6F8A68",
|
|
116
|
+
blu: "#5F8880",
|
|
117
|
+
pur: "#8A6E9C",
|
|
118
|
+
dim: "#6B6B60",
|
|
119
|
+
faint: "#9A9A8E",
|
|
120
|
+
redBg: "#EDE4DA",
|
|
121
|
+
yelBg: "#EDE7DB",
|
|
122
|
+
grnBg: "#E8E7DD",
|
|
123
|
+
bluBg: "#E6E6DF",
|
|
124
|
+
dimBg: "#E9DDD1",
|
|
125
|
+
},
|
|
126
|
+
paper: {
|
|
127
|
+
red: "#8C2A1F",
|
|
128
|
+
yel: "#8A6A1F",
|
|
129
|
+
grn: "#3F5C45",
|
|
130
|
+
blu: "#2F5A78",
|
|
131
|
+
pur: "#6E4A78",
|
|
132
|
+
dim: "#5B524A",
|
|
133
|
+
faint: "#8C8175",
|
|
134
|
+
redBg: "#E5D4C2",
|
|
135
|
+
yelBg: "#E3D8BE",
|
|
136
|
+
grnBg: "#DAD6C3",
|
|
137
|
+
bluBg: "#DCD9CB",
|
|
138
|
+
dimBg: "#DDC5B3",
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const agentStatusVariantLabels: Record<AgentStatusVariant, string> = {
|
|
143
|
+
1: "hairline readout",
|
|
144
|
+
2: "phase tag",
|
|
145
|
+
3: "left-rail accent",
|
|
146
|
+
4: "soft card",
|
|
147
|
+
5: "margin labels",
|
|
148
|
+
6: "sigil prefix",
|
|
149
|
+
7: "split-bar",
|
|
150
|
+
8: "micro-log",
|
|
151
|
+
9: "stamp head",
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
let persistedState = loadPersistedState();
|
|
155
|
+
|
|
156
|
+
const agentStatusState: AgentStatusRuntimeState = {
|
|
157
|
+
enabled: persistedState.enabled !== false,
|
|
158
|
+
variant: isAgentStatusVariant(persistedState.variant) ? persistedState.variant : 1,
|
|
159
|
+
active: false,
|
|
160
|
+
phase: "thinking",
|
|
161
|
+
latestText: "Waiting for model…",
|
|
162
|
+
latestTextSynthetic: true,
|
|
163
|
+
changedFiles: [],
|
|
164
|
+
activeTools: new Map(),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export default function gr0kHackExtension(pi: ExtensionAPI) {
|
|
168
|
+
registerCompactBuiltinToolRenderers(pi);
|
|
169
|
+
registerAgentStatusCommand(pi, "switch-agentStatus");
|
|
170
|
+
|
|
171
|
+
pi.on("session_start", (_event, ctx) => {
|
|
172
|
+
if (!ctx.hasUI) return;
|
|
173
|
+
applyAgentStatusUI(ctx);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
177
|
+
if (!ctx.hasUI) return;
|
|
178
|
+
clearHideTimer();
|
|
179
|
+
stopAgentStatusTicker();
|
|
180
|
+
agentStatusState.active = false;
|
|
181
|
+
agentStatusState.activeTools.clear();
|
|
182
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
183
|
+
ctx.ui.setWorkingVisible(true);
|
|
184
|
+
ctx.ui.setWorkingIndicator();
|
|
185
|
+
ctx.ui.setWorkingMessage();
|
|
186
|
+
ctx.ui.setHiddenThinkingLabel();
|
|
187
|
+
agentStatusState.requestRender = undefined;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
pi.on("before_agent_start", () => {
|
|
191
|
+
agentStatusState.lastActivityText = undefined;
|
|
192
|
+
startAgentStatusTurn("thinking");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
pi.on("agent_start", () => {
|
|
196
|
+
startAgentStatusTurn("thinking");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
pi.on("turn_start", () => {
|
|
200
|
+
startAgentStatusTurn("thinking");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
pi.on("message_start", (event) => {
|
|
204
|
+
if (isAssistantMessage(event.message)) {
|
|
205
|
+
setAgentStatusPhase("thinking");
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
pi.on("message_update", (event) => {
|
|
210
|
+
if (!isAssistantMessage(event.message)) return;
|
|
211
|
+
const text = extractLatestVisibleAssistantText(event.message);
|
|
212
|
+
if (!text) return;
|
|
213
|
+
if (agentStatusState.activeTools.size === 0) {
|
|
214
|
+
agentStatusState.latestText = text;
|
|
215
|
+
agentStatusState.latestTextSynthetic = false;
|
|
216
|
+
agentStatusState.phase = "thinking";
|
|
217
|
+
}
|
|
218
|
+
activateAgentStatus();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
pi.on("message_end", (event) => {
|
|
222
|
+
if (!isAssistantMessage(event.message)) return;
|
|
223
|
+
const text = extractLatestVisibleAssistantText(event.message);
|
|
224
|
+
if (!text) return;
|
|
225
|
+
agentStatusState.latestText = text;
|
|
226
|
+
agentStatusState.latestTextSynthetic = false;
|
|
227
|
+
requestAgentStatusRender();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
231
|
+
const phase = phaseForTool(event.toolName);
|
|
232
|
+
const path = pathForTool(event.toolName, event.args, ctx.cwd);
|
|
233
|
+
const activityText = activityTextForTool(event.toolName, event.args, ctx.cwd);
|
|
234
|
+
if (activityText) agentStatusState.lastActivityText = activityText;
|
|
235
|
+
agentStatusState.activeTools.set(event.toolCallId, {
|
|
236
|
+
name: event.toolName,
|
|
237
|
+
phase,
|
|
238
|
+
path,
|
|
239
|
+
startedAt: Date.now(),
|
|
240
|
+
});
|
|
241
|
+
agentStatusState.phase = phase;
|
|
242
|
+
if (path) agentStatusState.currentPath = path;
|
|
243
|
+
agentStatusState.latestText = activityText ?? syntheticToolText(event.toolName, path);
|
|
244
|
+
agentStatusState.latestTextSynthetic = true;
|
|
245
|
+
activateAgentStatus();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
pi.on("tool_execution_update", (event, ctx) => {
|
|
249
|
+
const existing = agentStatusState.activeTools.get(event.toolCallId);
|
|
250
|
+
if (!existing) return;
|
|
251
|
+
const path = pathForTool(event.toolName, event.args, ctx.cwd) ?? existing.path;
|
|
252
|
+
const activityText = activityTextForTool(event.toolName, event.args, ctx.cwd);
|
|
253
|
+
if (activityText) {
|
|
254
|
+
agentStatusState.lastActivityText = activityText;
|
|
255
|
+
agentStatusState.latestText = activityText;
|
|
256
|
+
agentStatusState.latestTextSynthetic = true;
|
|
257
|
+
}
|
|
258
|
+
existing.path = path;
|
|
259
|
+
if (path) agentStatusState.currentPath = path;
|
|
260
|
+
agentStatusState.phase = existing.phase;
|
|
261
|
+
requestAgentStatusRender();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
pi.on("tool_result", (event, ctx) => {
|
|
265
|
+
if (event.isError) return;
|
|
266
|
+
const activityText = activityTextForToolResult(event.toolName, event.input, event.content, ctx.cwd);
|
|
267
|
+
if (activityText) {
|
|
268
|
+
agentStatusState.lastActivityText = activityText;
|
|
269
|
+
agentStatusState.latestText = activityText;
|
|
270
|
+
agentStatusState.latestTextSynthetic = true;
|
|
271
|
+
}
|
|
272
|
+
if (event.toolName !== "edit" && event.toolName !== "write") return;
|
|
273
|
+
const path = pathForTool(event.toolName, event.input, ctx.cwd);
|
|
274
|
+
if (path) addChangedFile(path);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
pi.on("tool_execution_end", (event) => {
|
|
278
|
+
const activity = agentStatusState.activeTools.get(event.toolCallId);
|
|
279
|
+
if (activity && !event.isError && (activity.name === "edit" || activity.name === "write") && activity.path) {
|
|
280
|
+
addChangedFile(activity.path);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
agentStatusState.activeTools.delete(event.toolCallId);
|
|
284
|
+
const nextActivity = newestActiveTool();
|
|
285
|
+
if (nextActivity) {
|
|
286
|
+
agentStatusState.phase = nextActivity.phase;
|
|
287
|
+
if (nextActivity.path) agentStatusState.currentPath = nextActivity.path;
|
|
288
|
+
} else {
|
|
289
|
+
agentStatusState.phase = "thinking";
|
|
290
|
+
if (activity?.path) agentStatusState.currentPath = activity.path;
|
|
291
|
+
if (agentStatusState.latestTextSynthetic) {
|
|
292
|
+
agentStatusState.latestText = event.isError
|
|
293
|
+
? `${activity?.name ?? event.toolName} failed`
|
|
294
|
+
: (agentStatusState.lastActivityText ?? `${activity?.name ?? event.toolName} complete`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
activateAgentStatus();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
pi.on("agent_end", () => {
|
|
301
|
+
agentStatusState.activeTools.clear();
|
|
302
|
+
if (agentStatusState.changedFiles.length > 0) {
|
|
303
|
+
agentStatusState.currentPath = agentStatusState.changedFiles[agentStatusState.changedFiles.length - 1];
|
|
304
|
+
}
|
|
305
|
+
if (agentStatusState.latestTextSynthetic) agentStatusState.latestText = "Done";
|
|
306
|
+
agentStatusState.phase = "thinking";
|
|
307
|
+
requestAgentStatusRender();
|
|
308
|
+
scheduleHideAgentStatus();
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function registerAgentStatusCommand(pi: ExtensionAPI, name: "switch-agentStatus"): void {
|
|
313
|
+
pi.registerCommand(name, {
|
|
314
|
+
description: "Switch gr0k-hack agent status UI (1-9). Use 0/off to restore pi's default loader.",
|
|
315
|
+
getArgumentCompletions: (prefix: string) => {
|
|
316
|
+
const p = prefix.trim().toLowerCase();
|
|
317
|
+
const items = [
|
|
318
|
+
...agentStatusVariantNumbers.map((variant) => ({
|
|
319
|
+
value: String(variant),
|
|
320
|
+
label: String(variant),
|
|
321
|
+
description: agentStatusVariantLabels[variant],
|
|
322
|
+
})),
|
|
323
|
+
{ value: "status", label: "status", description: "show the current gr0k-hack agent status setting" },
|
|
324
|
+
{ value: "0", label: "0", description: "turn off the custom agent status widget" },
|
|
325
|
+
];
|
|
326
|
+
const completions = items.filter((item) => item.value.startsWith(p) || item.description.toLowerCase().includes(p));
|
|
327
|
+
return completions.length > 0 ? completions : null;
|
|
328
|
+
},
|
|
329
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
330
|
+
if (!ctx.hasUI) return;
|
|
331
|
+
|
|
332
|
+
const parsed = parseAgentStatusArg(args);
|
|
333
|
+
if (parsed === "status") {
|
|
334
|
+
ctx.ui.notify(
|
|
335
|
+
`Agent status: ${agentStatusState.enabled ? "on" : "off"}, variant ${agentStatusState.variant} (${agentStatusVariantLabels[agentStatusState.variant]})`,
|
|
336
|
+
"info",
|
|
337
|
+
);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (parsed === undefined) {
|
|
341
|
+
ctx.ui.notify("Usage: /switch-agentStatus <1-9|v1-v9|0|off|status>", "warning");
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (parsed === 0) {
|
|
346
|
+
agentStatusState.enabled = false;
|
|
347
|
+
persistAgentStatusState({ enabled: false, variant: agentStatusState.variant });
|
|
348
|
+
applyAgentStatusUI(ctx);
|
|
349
|
+
ctx.ui.notify("Agent status: default pi loader (persisted)", "info");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
agentStatusState.enabled = true;
|
|
354
|
+
agentStatusState.variant = parsed;
|
|
355
|
+
persistAgentStatusState({ enabled: true, variant: parsed });
|
|
356
|
+
applyAgentStatusUI(ctx);
|
|
357
|
+
ctx.ui.notify(`Agent status ${parsed}: ${agentStatusVariantLabels[parsed]} (persisted)`, "info");
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function parseAgentStatusArg(args: string): AgentStatusVariant | 0 | "status" | undefined {
|
|
363
|
+
const value = args.trim().toLowerCase();
|
|
364
|
+
if (!value || value === "status") return "status";
|
|
365
|
+
if (["0", "off", "default", "reset", "false", "disable", "disabled"].includes(value)) return 0;
|
|
366
|
+
const normalized = value.startsWith("v") ? value.slice(1) : value;
|
|
367
|
+
if (!/^\d+$/.test(normalized)) return undefined;
|
|
368
|
+
const numeric = Number.parseInt(normalized, 10);
|
|
369
|
+
return isAgentStatusVariant(numeric) ? numeric : undefined;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function isAgentStatusVariant(value: unknown): value is AgentStatusVariant {
|
|
373
|
+
return typeof value === "number" && (agentStatusVariantNumbers as readonly number[]).includes(value);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function applyAgentStatusUI(ctx: ExtensionContext): void {
|
|
377
|
+
if (!ctx.hasUI) return;
|
|
378
|
+
|
|
379
|
+
if (!agentStatusState.enabled) {
|
|
380
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
381
|
+
ctx.ui.setWorkingVisible(true);
|
|
382
|
+
ctx.ui.setWorkingIndicator();
|
|
383
|
+
ctx.ui.setWorkingMessage();
|
|
384
|
+
ctx.ui.setHiddenThinkingLabel();
|
|
385
|
+
stopAgentStatusTicker();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
ctx.ui.setWorkingVisible(false);
|
|
390
|
+
ctx.ui.setWorkingIndicator({ frames: [] });
|
|
391
|
+
ctx.ui.setHiddenThinkingLabel("");
|
|
392
|
+
ctx.ui.setWidget(WIDGET_KEY, (tui, theme) => new AgentStatusWidget(tui, theme, agentStatusState), {
|
|
393
|
+
placement: "aboveEditor",
|
|
394
|
+
});
|
|
395
|
+
requestAgentStatusRender();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
class AgentStatusWidget implements Component {
|
|
399
|
+
private readonly requestRender: () => void;
|
|
400
|
+
|
|
401
|
+
constructor(
|
|
402
|
+
private readonly tui: TUI,
|
|
403
|
+
private readonly theme: Theme,
|
|
404
|
+
private readonly state: AgentStatusRuntimeState,
|
|
405
|
+
) {
|
|
406
|
+
this.requestRender = () => this.tui.requestRender();
|
|
407
|
+
this.state.requestRender = this.requestRender;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
render(width: number): string[] {
|
|
411
|
+
if (!this.state.enabled || !this.state.active) return [];
|
|
412
|
+
const w = Math.max(1, width);
|
|
413
|
+
const lines = renderAgentStatusLines(this.state, this.theme, w);
|
|
414
|
+
return lines.map((line) => fitLine(line, w));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
invalidate(): void {}
|
|
418
|
+
|
|
419
|
+
dispose(): void {
|
|
420
|
+
if (this.state.requestRender === this.requestRender) this.state.requestRender = undefined;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function renderAgentStatusLines(state: AgentStatusRuntimeState, theme: Theme, _width: number): string[] {
|
|
425
|
+
const scenario = scenarioForState(state);
|
|
426
|
+
const markupLines = buildAgentStatusVariant(state.variant, scenario);
|
|
427
|
+
return markupLines.map((line) => renderTuiMarkup(line, theme));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function scenarioForState(state: AgentStatusRuntimeState): AgentStatusScenario {
|
|
431
|
+
const thought = cleanInline(state.latestText) || "Waiting for model…";
|
|
432
|
+
const file = displayPath(state);
|
|
433
|
+
|
|
434
|
+
switch (state.phase) {
|
|
435
|
+
case "executing":
|
|
436
|
+
return { label: "executing", tone: "red", tonefill: "fred", glyph: "▶", thought, file };
|
|
437
|
+
case "reading":
|
|
438
|
+
return { label: "reading", tone: "blu", tonefill: "fblu", glyph: "◐", thought, file };
|
|
439
|
+
case "writing":
|
|
440
|
+
return { label: "writing", tone: "grn", tonefill: "fgrn", glyph: "✎", thought, file };
|
|
441
|
+
case "thinking":
|
|
442
|
+
default:
|
|
443
|
+
return { label: "thinking", tone: "yel", tonefill: "fyel", glyph: "✱", thought, file };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function buildAgentStatusVariant(variant: AgentStatusVariant, scn: AgentStatusScenario): string[] {
|
|
448
|
+
switch (variant) {
|
|
449
|
+
case 2:
|
|
450
|
+
return v2Step(scn);
|
|
451
|
+
case 3:
|
|
452
|
+
return v3Rail(scn);
|
|
453
|
+
case 4:
|
|
454
|
+
return v4Card(scn);
|
|
455
|
+
case 5:
|
|
456
|
+
return v5Margin(scn);
|
|
457
|
+
case 6:
|
|
458
|
+
return v6Sigil(scn);
|
|
459
|
+
case 7:
|
|
460
|
+
return v7Split(scn);
|
|
461
|
+
case 8:
|
|
462
|
+
return v8Micro(scn);
|
|
463
|
+
case 9:
|
|
464
|
+
return v9Stamp(scn);
|
|
465
|
+
case 1:
|
|
466
|
+
default:
|
|
467
|
+
return v1Hairline(scn);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function v1Hairline(scn: AgentStatusScenario): string[] {
|
|
472
|
+
const W = TUI_VARIANT_WIDTH;
|
|
473
|
+
const badge = `«${scn.tonefill}» ${scn.label.toUpperCase()} «/»`;
|
|
474
|
+
return [
|
|
475
|
+
" " + badge + " «spin» «" + scn.tone + "»" + scn.glyph + "«/» «faint»" + new Date().toLocaleTimeString([], { hour12: false }) + "«/»",
|
|
476
|
+
" «dim»" + tuiPad(scn.thought + "«dots»", W - 2) + "«/»",
|
|
477
|
+
" «faint»↳«/» «" + scn.tone + "»" + truncStart(scn.file, W - 6) + "«/»",
|
|
478
|
+
];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function v2Step(scn: AgentStatusScenario): string[] {
|
|
482
|
+
const W = TUI_VARIANT_WIDTH;
|
|
483
|
+
return [
|
|
484
|
+
" «faint»[agent] «/»«" + scn.tone + "»" + scn.label.toUpperCase() + "«/»«dots»",
|
|
485
|
+
" «dim»❝ " + scn.thought + " ❞«/»",
|
|
486
|
+
" «faint»file «/»«" + scn.tone + "»" + truncStart(scn.file, W - 8) + "«/»",
|
|
487
|
+
];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function v3Rail(scn: AgentStatusScenario): string[] {
|
|
491
|
+
const W = TUI_VARIANT_WIDTH;
|
|
492
|
+
return [
|
|
493
|
+
"«" + scn.tone + "»▌«/» «b»" + scn.label.toUpperCase() + "«/» «spin» «faint»agent · turn 7«/»",
|
|
494
|
+
"«" + scn.tone + "»▌«/» «dim»" + scn.thought + "«/»«dots»",
|
|
495
|
+
"«" + scn.tone + "»▌«/» «faint»↳«/» «" + scn.tone + "»" + truncStart(scn.file, W - 6) + "«/»",
|
|
496
|
+
];
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function v4Card(scn: AgentStatusScenario): string[] {
|
|
500
|
+
const W = TUI_VARIANT_WIDTH;
|
|
501
|
+
const inner = W - 2;
|
|
502
|
+
const head = " «spin» «b»" + scn.label.toUpperCase() + "«/»«dots»" + " «faint»· agent · turn 7«/»";
|
|
503
|
+
const mid = " «dim»" + scn.thought + "«/»";
|
|
504
|
+
const foot = " «faint»↳«/» «" + scn.tone + "»" + truncStart(scn.file, inner - 4) + "«/»";
|
|
505
|
+
return [
|
|
506
|
+
"╭" + "─".repeat(W - 2) + "╮",
|
|
507
|
+
"│" + tuiPad(head, inner) + "│",
|
|
508
|
+
"├" + "┄".repeat(W - 2) + "┤",
|
|
509
|
+
"│" + tuiPad(mid, inner) + "│",
|
|
510
|
+
"│" + tuiPad(foot, inner) + "│",
|
|
511
|
+
"╰" + "─".repeat(W - 2) + "╯",
|
|
512
|
+
];
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function v5Margin(scn: AgentStatusScenario): string[] {
|
|
516
|
+
const W = TUI_VARIANT_WIDTH;
|
|
517
|
+
const G = 12;
|
|
518
|
+
const m = (value: string) => tuiPad(value, G);
|
|
519
|
+
return [
|
|
520
|
+
m("«faint»state«/»") + "«" + scn.tonefill + "» " + scn.label.toUpperCase() + " «/»" + " «spin» «faint»turn 7«/»",
|
|
521
|
+
m("«faint»thought«/»") + "«dim»" + scn.thought + "«/»«dots»",
|
|
522
|
+
m("«faint»file«/»") + "«" + scn.tone + "»" + truncStart(scn.file, W - G) + "«/»",
|
|
523
|
+
];
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function v6Sigil(scn: AgentStatusScenario): string[] {
|
|
527
|
+
const W = TUI_VARIANT_WIDTH;
|
|
528
|
+
return [
|
|
529
|
+
"«faint»::«/» «" + scn.tone + "»" + scn.label + "«/»«dots»" + " «faint»turn 7 · 1.4s«/»",
|
|
530
|
+
"«faint» ›«/» «dim»" + scn.thought + "«/»",
|
|
531
|
+
"«faint» ⌁«/» «" + scn.tone + "»" + truncStart(scn.file, W - 6) + "«/»",
|
|
532
|
+
];
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function v7Split(scn: AgentStatusScenario): string[] {
|
|
536
|
+
const W = TUI_VARIANT_WIDTH;
|
|
537
|
+
const topTab = `╴ «${scn.tonefill}» ${scn.label.toUpperCase()} «/» «faint»turn 7«/» ╴`;
|
|
538
|
+
const topPad = W - tuiLen(topTab);
|
|
539
|
+
const file = truncStart(scn.file, W - 14);
|
|
540
|
+
const botTab = `«faint»╴«/» «faint»file«/» «${scn.tone}»${file}«/» «faint»╴«/»`;
|
|
541
|
+
const botPad = W - tuiLen(botTab);
|
|
542
|
+
return [
|
|
543
|
+
topTab + "«faint»" + "─".repeat(Math.max(2, topPad)) + "«/»",
|
|
544
|
+
"",
|
|
545
|
+
" «spin» «dim»" + scn.thought + "«/»«dots»",
|
|
546
|
+
"",
|
|
547
|
+
botTab + "«faint»" + "─".repeat(Math.max(2, botPad)) + "«/»",
|
|
548
|
+
];
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function v8Micro(scn: AgentStatusScenario): string[] {
|
|
552
|
+
const W = TUI_MICRO_WIDTH;
|
|
553
|
+
return [
|
|
554
|
+
"«" + scn.tone + "»●«/» «b»" + scn.label + "«/»" + "«dots»",
|
|
555
|
+
"«faint»│«/» «dim»" + scn.thought + "«/»",
|
|
556
|
+
"«faint»╰─«/»«faint»file «/»«" + scn.tone + "»" + truncStart(scn.file, W - 12) + "«/»",
|
|
557
|
+
];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function v9Stamp(scn: AgentStatusScenario): string[] {
|
|
561
|
+
const W = TUI_VARIANT_WIDTH;
|
|
562
|
+
return [
|
|
563
|
+
"«" + scn.tonefill + "» " + scn.label.toUpperCase().padEnd(9, " ") + "«/» «spin» «dim»" + scn.thought + "«/»«dots»",
|
|
564
|
+
" ".repeat(11) + "«faint»" + "─".repeat(W - 12) + "«/»",
|
|
565
|
+
" ".repeat(11) + "«faint»↳ file«/» «" + scn.tone + "»" + truncStart(scn.file, W - 22) + "«/»",
|
|
566
|
+
];
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function renderTuiMarkup(line: string, theme: Theme): string {
|
|
570
|
+
const out: string[] = [];
|
|
571
|
+
const stack: string[] = [];
|
|
572
|
+
let buf = "";
|
|
573
|
+
let i = 0;
|
|
574
|
+
const flush = () => {
|
|
575
|
+
if (!buf) return;
|
|
576
|
+
out.push(applyTuiTokens(buf, stack, theme));
|
|
577
|
+
buf = "";
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
while (i < line.length) {
|
|
581
|
+
if (line[i] === "«" && line.charAt(i + 1) === "/" && line.charAt(i + 2) === "»") {
|
|
582
|
+
flush();
|
|
583
|
+
stack.pop();
|
|
584
|
+
i += 3;
|
|
585
|
+
} else if (line[i] === "«") {
|
|
586
|
+
const close = line.indexOf("»", i + 1);
|
|
587
|
+
if (close === -1) {
|
|
588
|
+
buf += line[i];
|
|
589
|
+
i++;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
flush();
|
|
593
|
+
const tok = line.slice(i + 1, close);
|
|
594
|
+
if (tok === "spin") {
|
|
595
|
+
out.push(currentSpinnerFrame());
|
|
596
|
+
} else if (tok === "dots") {
|
|
597
|
+
out.push(currentDotFrame());
|
|
598
|
+
} else if (tok === "pulse") {
|
|
599
|
+
out.push(" ");
|
|
600
|
+
} else {
|
|
601
|
+
stack.push(tok);
|
|
602
|
+
}
|
|
603
|
+
i = close + 1;
|
|
604
|
+
} else {
|
|
605
|
+
buf += line[i];
|
|
606
|
+
i++;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
flush();
|
|
610
|
+
return out.join("");
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function applyTuiTokens(text: string, tokens: readonly string[], theme: Theme): string {
|
|
614
|
+
let styled = text;
|
|
615
|
+
let fgToken: AgentTone | "dim" | "faint" | undefined;
|
|
616
|
+
let fillToken: AgentFillTone | undefined;
|
|
617
|
+
let bold = false;
|
|
618
|
+
let italic = false;
|
|
619
|
+
|
|
620
|
+
for (const token of tokens) {
|
|
621
|
+
switch (token) {
|
|
622
|
+
case "red":
|
|
623
|
+
case "yel":
|
|
624
|
+
case "grn":
|
|
625
|
+
case "blu":
|
|
626
|
+
case "pur":
|
|
627
|
+
case "dim":
|
|
628
|
+
case "faint":
|
|
629
|
+
fgToken = token;
|
|
630
|
+
break;
|
|
631
|
+
case "fred":
|
|
632
|
+
case "fyel":
|
|
633
|
+
case "fgrn":
|
|
634
|
+
case "fblu":
|
|
635
|
+
case "fdim":
|
|
636
|
+
fillToken = token;
|
|
637
|
+
break;
|
|
638
|
+
case "b":
|
|
639
|
+
bold = true;
|
|
640
|
+
break;
|
|
641
|
+
case "it":
|
|
642
|
+
italic = true;
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (bold) styled = theme.bold(styled);
|
|
648
|
+
if (italic) styled = theme.italic(styled);
|
|
649
|
+
if (fgToken) styled = applyTuiFg(theme, fgToken, styled);
|
|
650
|
+
if (fillToken) styled = applyTuiFill(theme, fillToken, styled);
|
|
651
|
+
return styled;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function applyTuiFg(theme: Theme, token: AgentTone | "dim" | "faint", text: string): string {
|
|
655
|
+
const palette = paletteForTheme(theme);
|
|
656
|
+
if (palette) return ansiFg(palette[token], text);
|
|
657
|
+
|
|
658
|
+
switch (token) {
|
|
659
|
+
case "red":
|
|
660
|
+
return theme.fg("accent", text);
|
|
661
|
+
case "yel":
|
|
662
|
+
return theme.fg("warning", text);
|
|
663
|
+
case "grn":
|
|
664
|
+
return theme.fg("success", text);
|
|
665
|
+
case "blu":
|
|
666
|
+
return theme.fg("mdLink", text);
|
|
667
|
+
case "pur":
|
|
668
|
+
return theme.fg("syntaxNumber", text);
|
|
669
|
+
case "dim":
|
|
670
|
+
return theme.fg("muted", text);
|
|
671
|
+
case "faint":
|
|
672
|
+
default:
|
|
673
|
+
return theme.fg("dim", text);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function applyTuiFill(theme: Theme, token: AgentFillTone, text: string): string {
|
|
678
|
+
const palette = paletteForTheme(theme);
|
|
679
|
+
if (palette) {
|
|
680
|
+
const colorToken = fillToColorToken(token);
|
|
681
|
+
const fg = colorToken ? ansiFg(palette[colorToken], text) : text;
|
|
682
|
+
return ansiBg(palette[fillToBgKey(token)], fg);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
switch (token) {
|
|
686
|
+
case "fred":
|
|
687
|
+
return theme.bg("toolErrorBg", theme.fg("accent", text));
|
|
688
|
+
case "fyel":
|
|
689
|
+
return theme.bg("selectedBg", theme.fg("warning", text));
|
|
690
|
+
case "fgrn":
|
|
691
|
+
return theme.bg("toolSuccessBg", theme.fg("success", text));
|
|
692
|
+
case "fblu":
|
|
693
|
+
return theme.bg("toolPendingBg", theme.fg("mdLink", text));
|
|
694
|
+
case "fdim":
|
|
695
|
+
default:
|
|
696
|
+
return theme.bg("selectedBg", text);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function fillToColorToken(token: AgentFillTone): AgentTone | undefined {
|
|
701
|
+
switch (token) {
|
|
702
|
+
case "fred":
|
|
703
|
+
return "red";
|
|
704
|
+
case "fyel":
|
|
705
|
+
return "yel";
|
|
706
|
+
case "fgrn":
|
|
707
|
+
return "grn";
|
|
708
|
+
case "fblu":
|
|
709
|
+
return "blu";
|
|
710
|
+
case "fdim":
|
|
711
|
+
default:
|
|
712
|
+
return undefined;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function fillToBgKey(token: AgentFillTone): keyof TuiPalette {
|
|
717
|
+
switch (token) {
|
|
718
|
+
case "fred":
|
|
719
|
+
return "redBg";
|
|
720
|
+
case "fyel":
|
|
721
|
+
return "yelBg";
|
|
722
|
+
case "fgrn":
|
|
723
|
+
return "grnBg";
|
|
724
|
+
case "fblu":
|
|
725
|
+
return "bluBg";
|
|
726
|
+
case "fdim":
|
|
727
|
+
default:
|
|
728
|
+
return "dimBg";
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function paletteForTheme(theme: Theme): TuiPalette | undefined {
|
|
733
|
+
const key = (theme.name ?? "").toLowerCase();
|
|
734
|
+
return TUI_PALETTES[key];
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function ansiFg(hex: string, text: string): string {
|
|
738
|
+
const rgb = hexToRgb(hex);
|
|
739
|
+
return rgb ? `\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m${text}\x1b[39m` : text;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function ansiBg(hex: string, text: string): string {
|
|
743
|
+
const rgb = hexToRgb(hex);
|
|
744
|
+
return rgb ? `\x1b[48;2;${rgb.r};${rgb.g};${rgb.b}m${text}\x1b[49m` : text;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function hexToRgb(hex: string): { r: number; g: number; b: number } | undefined {
|
|
748
|
+
const match = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
749
|
+
if (!match) return undefined;
|
|
750
|
+
const value = Number.parseInt(match[1], 16);
|
|
751
|
+
return { r: (value >> 16) & 255, g: (value >> 8) & 255, b: value & 255 };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function currentSpinnerFrame(): string {
|
|
755
|
+
return SPINNER_FRAMES[Math.floor(Date.now() / RENDER_TICK_MS) % SPINNER_FRAMES.length];
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function currentDotFrame(): string {
|
|
759
|
+
return DOT_FRAMES[Math.floor(Date.now() / 350) % DOT_FRAMES.length];
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function tuiLen(line: string): number {
|
|
763
|
+
return line.replace(/«spin»|«dots»|«pulse»/g, " ").replace(/«[^»]*»/g, "").length;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function tuiPad(line: string, width: number, fill = " "): string {
|
|
767
|
+
const n = tuiLen(line);
|
|
768
|
+
if (n >= width) return line;
|
|
769
|
+
return line + fill.repeat(width - n);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function truncStart(value: string, width: number): string {
|
|
773
|
+
if (width <= 0) return "";
|
|
774
|
+
const s = cleanInline(value);
|
|
775
|
+
if (s.length <= width) return s;
|
|
776
|
+
if (width === 1) return "…";
|
|
777
|
+
return "…" + s.slice(s.length - width + 1);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function fitLine(line: string, width: number): string {
|
|
781
|
+
return truncateToWidth(line, Math.max(1, width), "…", false);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function displayPath(state: AgentStatusRuntimeState): string {
|
|
785
|
+
const latestChanged = state.changedFiles[state.changedFiles.length - 1];
|
|
786
|
+
return state.currentPath ?? latestChanged ?? "no file yet";
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function startAgentStatusTurn(phase: AgentPhase): void {
|
|
790
|
+
clearHideTimer();
|
|
791
|
+
startAgentStatusTicker();
|
|
792
|
+
agentStatusState.active = true;
|
|
793
|
+
agentStatusState.phase = phase;
|
|
794
|
+
agentStatusState.latestText = agentStatusState.lastActivityText ?? "Waiting for next model output…";
|
|
795
|
+
agentStatusState.latestTextSynthetic = true;
|
|
796
|
+
agentStatusState.currentPath = agentStatusState.changedFiles[agentStatusState.changedFiles.length - 1];
|
|
797
|
+
requestAgentStatusRender();
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function setAgentStatusPhase(phase: AgentPhase): void {
|
|
801
|
+
clearHideTimer();
|
|
802
|
+
startAgentStatusTicker();
|
|
803
|
+
agentStatusState.active = true;
|
|
804
|
+
agentStatusState.phase = phase;
|
|
805
|
+
requestAgentStatusRender();
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function activateAgentStatus(): void {
|
|
809
|
+
clearHideTimer();
|
|
810
|
+
startAgentStatusTicker();
|
|
811
|
+
agentStatusState.active = true;
|
|
812
|
+
requestAgentStatusRender();
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function scheduleHideAgentStatus(): void {
|
|
816
|
+
clearHideTimer();
|
|
817
|
+
agentStatusState.hideTimer = setTimeout(() => {
|
|
818
|
+
agentStatusState.hideTimer = undefined;
|
|
819
|
+
agentStatusState.active = false;
|
|
820
|
+
requestAgentStatusRender();
|
|
821
|
+
stopAgentStatusTicker();
|
|
822
|
+
}, HIDE_DELAY_MS);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function clearHideTimer(): void {
|
|
826
|
+
if (!agentStatusState.hideTimer) return;
|
|
827
|
+
clearTimeout(agentStatusState.hideTimer);
|
|
828
|
+
agentStatusState.hideTimer = undefined;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function requestAgentStatusRender(): void {
|
|
832
|
+
if (!agentStatusState.enabled) return;
|
|
833
|
+
agentStatusState.requestRender?.();
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function startAgentStatusTicker(): void {
|
|
837
|
+
if (!agentStatusState.enabled || agentStatusState.renderTicker) return;
|
|
838
|
+
agentStatusState.renderTicker = setInterval(requestAgentStatusRender, RENDER_TICK_MS);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function stopAgentStatusTicker(): void {
|
|
842
|
+
if (!agentStatusState.renderTicker) return;
|
|
843
|
+
clearInterval(agentStatusState.renderTicker);
|
|
844
|
+
agentStatusState.renderTicker = undefined;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function newestActiveTool(): ToolActivity | undefined {
|
|
848
|
+
let newest: ToolActivity | undefined;
|
|
849
|
+
for (const activity of agentStatusState.activeTools.values()) {
|
|
850
|
+
if (!newest || activity.startedAt >= newest.startedAt) newest = activity;
|
|
851
|
+
}
|
|
852
|
+
return newest;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function addChangedFile(filePath: string): void {
|
|
856
|
+
const compact = cleanInline(filePath);
|
|
857
|
+
if (!compact) return;
|
|
858
|
+
agentStatusState.changedFiles = agentStatusState.changedFiles.filter((item) => item !== compact);
|
|
859
|
+
agentStatusState.changedFiles.push(compact);
|
|
860
|
+
if (agentStatusState.changedFiles.length > MAX_CHANGED_FILES) {
|
|
861
|
+
agentStatusState.changedFiles = agentStatusState.changedFiles.slice(-MAX_CHANGED_FILES);
|
|
862
|
+
}
|
|
863
|
+
agentStatusState.currentPath = compact;
|
|
864
|
+
requestAgentStatusRender();
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function phaseForTool(toolName: string): AgentPhase {
|
|
868
|
+
if (toolName === "read" || toolName === "grep" || toolName === "find" || toolName === "ls") return "reading";
|
|
869
|
+
if (toolName === "edit" || toolName === "write") return "writing";
|
|
870
|
+
return "executing";
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function syntheticToolText(toolName: string, path?: string): string {
|
|
874
|
+
const target = path ? ` ${path}` : "";
|
|
875
|
+
switch (phaseForTool(toolName)) {
|
|
876
|
+
case "reading":
|
|
877
|
+
return `reading${target}`;
|
|
878
|
+
case "writing":
|
|
879
|
+
return `${toolName === "write" ? "writing" : "editing"}${target}`;
|
|
880
|
+
case "executing":
|
|
881
|
+
return `${toolName}${target}`;
|
|
882
|
+
case "thinking":
|
|
883
|
+
default:
|
|
884
|
+
return "Waiting for model output…";
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function activityTextForTool(toolName: string, args: unknown, cwd: string): string | undefined {
|
|
889
|
+
const path = pathForTool(toolName, args, cwd);
|
|
890
|
+
const subject = path ? ` ${path}` : "";
|
|
891
|
+
const suffix = activitySuffixForToolInput(toolName, args);
|
|
892
|
+
return `${toolName.toUpperCase()}${subject}${suffix ? ` ${suffix}` : ""}`;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function activityTextForToolResult(toolName: string, input: unknown, content: unknown, cwd: string): string | undefined {
|
|
896
|
+
const path = pathForTool(toolName, input, cwd);
|
|
897
|
+
const subject = path ? ` ${path}` : "";
|
|
898
|
+
const suffix = activitySuffixForToolResult(toolName, input, content) ?? activitySuffixForToolInput(toolName, input);
|
|
899
|
+
return `${toolName.toUpperCase()}${subject}${suffix ? ` ${suffix}` : ""}`;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function activitySuffixForToolInput(toolName: string, input: unknown): string | undefined {
|
|
903
|
+
const record = isRecord(input) ? input : {};
|
|
904
|
+
switch (toolName) {
|
|
905
|
+
case "write": {
|
|
906
|
+
const content = typeof record.content === "string" ? record.content : "";
|
|
907
|
+
return `${lineCount(content)} lines`;
|
|
908
|
+
}
|
|
909
|
+
case "edit": {
|
|
910
|
+
const count = Array.isArray(record.edits) ? record.edits.length : 0;
|
|
911
|
+
return count > 0 ? `${count} replacements` : undefined;
|
|
912
|
+
}
|
|
913
|
+
case "grep": {
|
|
914
|
+
const pattern = typeof record.pattern === "string" ? cleanInline(record.pattern) : "";
|
|
915
|
+
return pattern ? `/${truncateToWidth(pattern, 64, "…")}/` : undefined;
|
|
916
|
+
}
|
|
917
|
+
case "find": {
|
|
918
|
+
const pattern = typeof record.pattern === "string" ? cleanInline(record.pattern) : "";
|
|
919
|
+
return pattern || undefined;
|
|
920
|
+
}
|
|
921
|
+
case "read": {
|
|
922
|
+
const parts = [typeof record.offset === "number" ? `offset=${record.offset}` : undefined, typeof record.limit === "number" ? `limit=${record.limit}` : undefined].filter(Boolean);
|
|
923
|
+
return parts.join(", ") || undefined;
|
|
924
|
+
}
|
|
925
|
+
case "bash": {
|
|
926
|
+
const timeout = typeof record.timeout === "number" ? `timeout=${record.timeout}s` : "";
|
|
927
|
+
return timeout || undefined;
|
|
928
|
+
}
|
|
929
|
+
default:
|
|
930
|
+
return undefined;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function activitySuffixForToolResult(toolName: string, input: unknown, content: unknown): string | undefined {
|
|
935
|
+
switch (toolName) {
|
|
936
|
+
case "read":
|
|
937
|
+
return `${lineCount(firstTextFromContent(content))} lines`;
|
|
938
|
+
case "grep":
|
|
939
|
+
return `${lineCount(firstTextFromContent(content))} matching lines`;
|
|
940
|
+
case "find":
|
|
941
|
+
return `${lineCount(firstTextFromContent(content))} paths`;
|
|
942
|
+
case "ls":
|
|
943
|
+
return `${lineCount(firstTextFromContent(content))} entries`;
|
|
944
|
+
case "bash":
|
|
945
|
+
return `${lineCount(firstTextFromContent(content))} output lines`;
|
|
946
|
+
case "write":
|
|
947
|
+
case "edit":
|
|
948
|
+
return activitySuffixForToolInput(toolName, input);
|
|
949
|
+
default:
|
|
950
|
+
return undefined;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function firstTextFromContent(content: unknown): string {
|
|
955
|
+
if (!Array.isArray(content)) return "";
|
|
956
|
+
const first = content[0];
|
|
957
|
+
return isRecord(first) && first.type === "text" && typeof first.text === "string" ? first.text : "";
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function pathForTool(toolName: string, args: unknown, cwd: string): string | undefined {
|
|
961
|
+
const record = isRecord(args) ? args : {};
|
|
962
|
+
const rawPath = typeof record.path === "string" ? record.path : undefined;
|
|
963
|
+
if (rawPath) return compactPath(rawPath, cwd);
|
|
964
|
+
if (toolName === "grep" && typeof record.glob === "string" && record.glob) return record.glob;
|
|
965
|
+
if (toolName === "find" && typeof record.pattern === "string" && record.pattern) return record.pattern;
|
|
966
|
+
if (toolName === "ls") return compactPath(".", cwd);
|
|
967
|
+
if (toolName === "bash" && typeof record.command === "string" && record.command) return `$ ${cleanInline(record.command)}`;
|
|
968
|
+
return undefined;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function compactPath(input: string, cwd = process.cwd()): string {
|
|
972
|
+
const trimmed = input.trim().replace(/^@/, "");
|
|
973
|
+
if (!trimmed) return trimmed;
|
|
974
|
+
if (trimmed === ".") return ".";
|
|
975
|
+
try {
|
|
976
|
+
const absolute = isAbsolute(trimmed) ? resolve(trimmed) : resolve(cwd, trimmed);
|
|
977
|
+
const rel = relative(cwd, absolute);
|
|
978
|
+
if (rel && !rel.startsWith("..") && !isAbsolute(rel)) return rel;
|
|
979
|
+
return trimmed;
|
|
980
|
+
} catch {
|
|
981
|
+
return trimmed;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function extractLatestVisibleAssistantText(message: unknown): string {
|
|
986
|
+
if (!isRecord(message) || message.role !== "assistant") return "";
|
|
987
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
988
|
+
const chunks: string[] = [];
|
|
989
|
+
for (const block of content) {
|
|
990
|
+
if (!isRecord(block) || block.type !== "text" || typeof block.text !== "string") continue;
|
|
991
|
+
chunks.push(block.text);
|
|
992
|
+
}
|
|
993
|
+
return latestNonEmptyLine(chunks.join(""));
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function latestNonEmptyLine(value: string): string {
|
|
997
|
+
const lines = value.replace(/\r/g, "").split("\n").map(cleanInline).filter(Boolean);
|
|
998
|
+
return lines[lines.length - 1] ?? "";
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function cleanInline(value: string): string {
|
|
1002
|
+
return value.replace(/\s+/g, " ").trim();
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function isAssistantMessage(message: unknown): boolean {
|
|
1006
|
+
return isRecord(message) && message.role === "assistant";
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1010
|
+
return typeof value === "object" && value !== null;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function loadPersistedState(): PersistedGr0kHackState {
|
|
1014
|
+
try {
|
|
1015
|
+
const raw = readFileSync(STATE_PATH, "utf-8");
|
|
1016
|
+
const parsed = JSON.parse(raw) as PersistedGr0kHackState;
|
|
1017
|
+
return isRecord(parsed) ? parsed : {};
|
|
1018
|
+
} catch {
|
|
1019
|
+
return {};
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function persistAgentStatusState(patch: PersistedGr0kHackState): void {
|
|
1024
|
+
persistedState = {
|
|
1025
|
+
...persistedState,
|
|
1026
|
+
...patch,
|
|
1027
|
+
};
|
|
1028
|
+
try {
|
|
1029
|
+
mkdirSync(dirname(STATE_PATH), { recursive: true });
|
|
1030
|
+
writeFileSync(STATE_PATH, JSON.stringify(persistedState, null, "\t") + "\n");
|
|
1031
|
+
} catch {
|
|
1032
|
+
// Preference persistence should never break the agent loop.
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function registerCompactBuiltinToolRenderers(pi: ExtensionAPI): void {
|
|
1037
|
+
const cwd = process.cwd();
|
|
1038
|
+
|
|
1039
|
+
const read = createReadToolDefinition(cwd);
|
|
1040
|
+
pi.registerTool({
|
|
1041
|
+
...read,
|
|
1042
|
+
renderCall: (args: unknown, theme: Theme, context: ToolRenderContext) => {
|
|
1043
|
+
const record = isRecord(args) ? args : {};
|
|
1044
|
+
const path = typeof record.path === "string" ? compactPath(record.path, context.cwd) : "file";
|
|
1045
|
+
const parts = [typeof record.offset === "number" ? `offset=${record.offset}` : undefined, typeof record.limit === "number" ? `limit=${record.limit}` : undefined].filter(Boolean);
|
|
1046
|
+
return compactToolText(theme, "READING", path, parts.join(", "));
|
|
1047
|
+
},
|
|
1048
|
+
renderResult: (result: AgentToolResult<ReadToolDetails | undefined>, _options, theme: Theme, context) => {
|
|
1049
|
+
if (context.isPartial) return compactToolText(theme, "READING", "in progress…");
|
|
1050
|
+
const error = compactError(result, context, theme);
|
|
1051
|
+
if (error) return new Text(error, 0, 0);
|
|
1052
|
+
const first = result.content[0];
|
|
1053
|
+
if (first?.type === "image") return compactToolText(theme, "READ", "image loaded");
|
|
1054
|
+
const content = first?.type === "text" ? first.text : "";
|
|
1055
|
+
const details = result.details;
|
|
1056
|
+
const totalLines = details?.truncation?.totalLines ?? lineCount(content);
|
|
1057
|
+
const suffix = details?.truncation?.truncated ? `truncated • ${totalLines} lines total` : `${totalLines} lines`;
|
|
1058
|
+
return compactToolText(theme, "READ", pathFromContext(context, "read"), suffix, "success");
|
|
1059
|
+
},
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
const grep = createGrepToolDefinition(cwd);
|
|
1063
|
+
pi.registerTool({
|
|
1064
|
+
...grep,
|
|
1065
|
+
renderCall: (args: unknown, theme: Theme, context: ToolRenderContext) => {
|
|
1066
|
+
const record = isRecord(args) ? args : {};
|
|
1067
|
+
const pattern = typeof record.pattern === "string" ? record.pattern : "pattern";
|
|
1068
|
+
const path = pathForTool("grep", args, context.cwd) ?? ".";
|
|
1069
|
+
return compactToolText(theme, "SEARCH", path, `/${cleanInline(pattern)}/`);
|
|
1070
|
+
},
|
|
1071
|
+
renderResult: (result: AgentToolResult<GrepToolDetails | undefined>, _options, theme: Theme, context) => {
|
|
1072
|
+
if (context.isPartial) return compactToolText(theme, "SEARCH", "in progress…");
|
|
1073
|
+
const error = compactError(result, context, theme);
|
|
1074
|
+
if (error) return new Text(error, 0, 0);
|
|
1075
|
+
const content = firstText(result);
|
|
1076
|
+
const details = result.details;
|
|
1077
|
+
const matches = lineCount(content);
|
|
1078
|
+
const suffix = [
|
|
1079
|
+
`${matches} matching lines`,
|
|
1080
|
+
details?.matchLimitReached ? `limit ${details.matchLimitReached}` : undefined,
|
|
1081
|
+
details?.truncation?.truncated || details?.linesTruncated ? "truncated" : undefined,
|
|
1082
|
+
]
|
|
1083
|
+
.filter(Boolean)
|
|
1084
|
+
.join(" • ");
|
|
1085
|
+
return compactToolText(theme, "SEARCH", pathFromContext(context, "grep"), suffix, "success");
|
|
1086
|
+
},
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
const find = createFindToolDefinition(cwd);
|
|
1090
|
+
pi.registerTool({
|
|
1091
|
+
...find,
|
|
1092
|
+
renderCall: (args: unknown, theme: Theme, context: ToolRenderContext) => {
|
|
1093
|
+
const record = isRecord(args) ? args : {};
|
|
1094
|
+
const path = pathForTool("find", args, context.cwd) ?? ".";
|
|
1095
|
+
const pattern = typeof record.pattern === "string" ? record.pattern : "*";
|
|
1096
|
+
return compactToolText(theme, "FIND", path, pattern);
|
|
1097
|
+
},
|
|
1098
|
+
renderResult: (result: AgentToolResult<FindToolDetails | undefined>, _options, theme: Theme, context) => {
|
|
1099
|
+
if (context.isPartial) return compactToolText(theme, "FIND", "in progress…");
|
|
1100
|
+
const error = compactError(result, context, theme);
|
|
1101
|
+
if (error) return new Text(error, 0, 0);
|
|
1102
|
+
const details = result.details;
|
|
1103
|
+
const suffix = [`${lineCount(firstText(result))} paths`, details?.resultLimitReached ? `limit ${details.resultLimitReached}` : undefined, details?.truncation?.truncated ? "truncated" : undefined]
|
|
1104
|
+
.filter(Boolean)
|
|
1105
|
+
.join(" • ");
|
|
1106
|
+
return compactToolText(theme, "FIND", pathFromContext(context, "find"), suffix, "success");
|
|
1107
|
+
},
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
const ls = createLsToolDefinition(cwd);
|
|
1111
|
+
pi.registerTool({
|
|
1112
|
+
...ls,
|
|
1113
|
+
renderCall: (args: unknown, theme: Theme, context: ToolRenderContext) => {
|
|
1114
|
+
const path = pathForTool("ls", args, context.cwd) ?? ".";
|
|
1115
|
+
return compactToolText(theme, "LIST", path);
|
|
1116
|
+
},
|
|
1117
|
+
renderResult: (result: AgentToolResult<LsToolDetails | undefined>, _options, theme: Theme, context) => {
|
|
1118
|
+
if (context.isPartial) return compactToolText(theme, "LIST", "in progress…");
|
|
1119
|
+
const error = compactError(result, context, theme);
|
|
1120
|
+
if (error) return new Text(error, 0, 0);
|
|
1121
|
+
const details = result.details;
|
|
1122
|
+
const suffix = [`${lineCount(firstText(result))} entries`, details?.entryLimitReached ? `limit ${details.entryLimitReached}` : undefined, details?.truncation?.truncated ? "truncated" : undefined]
|
|
1123
|
+
.filter(Boolean)
|
|
1124
|
+
.join(" • ");
|
|
1125
|
+
return compactToolText(theme, "LIST", pathFromContext(context, "ls"), suffix, "success");
|
|
1126
|
+
},
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
const bash = createBashToolDefinition(cwd);
|
|
1130
|
+
pi.registerTool({
|
|
1131
|
+
...bash,
|
|
1132
|
+
renderCall: (args: unknown, theme: Theme) => {
|
|
1133
|
+
const record = isRecord(args) ? args : {};
|
|
1134
|
+
const command = typeof record.command === "string" ? cleanInline(record.command) : "shell";
|
|
1135
|
+
const timeout = typeof record.timeout === "number" ? `timeout=${record.timeout}s` : "";
|
|
1136
|
+
return compactToolText(theme, "EXEC", `$ ${command}`, timeout);
|
|
1137
|
+
},
|
|
1138
|
+
renderResult: (result: AgentToolResult<BashToolDetails | undefined>, _options, theme: Theme, context) => {
|
|
1139
|
+
if (context.isPartial) return compactToolText(theme, "EXEC", "running…");
|
|
1140
|
+
const error = compactError(result, context, theme);
|
|
1141
|
+
if (error) return new Text(error, 0, 0);
|
|
1142
|
+
const content = firstText(result);
|
|
1143
|
+
const details = result.details;
|
|
1144
|
+
const suffix = [`${lineCount(content)} output lines`, details?.truncation?.truncated ? "truncated" : undefined, details?.fullOutputPath ? `full log ${compactPath(details.fullOutputPath, context.cwd)}` : undefined]
|
|
1145
|
+
.filter(Boolean)
|
|
1146
|
+
.join(" • ");
|
|
1147
|
+
return compactToolText(theme, "EXEC", "complete", suffix, "success");
|
|
1148
|
+
},
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
const edit = createEditToolDefinition(cwd);
|
|
1152
|
+
pi.registerTool({
|
|
1153
|
+
...edit,
|
|
1154
|
+
renderCall: (args: unknown, theme: Theme, context: ToolRenderContext) => {
|
|
1155
|
+
const record = isRecord(args) ? args : {};
|
|
1156
|
+
const path = typeof record.path === "string" ? compactPath(record.path, context.cwd) : "file";
|
|
1157
|
+
const editCount = Array.isArray(record.edits) ? record.edits.length : 0;
|
|
1158
|
+
return compactToolText(theme, "EDIT", path, `${editCount} replacements`);
|
|
1159
|
+
},
|
|
1160
|
+
renderResult: (result: AgentToolResult<EditToolDetails | undefined>, _options, theme: Theme, context) => {
|
|
1161
|
+
if (context.isPartial) return compactToolText(theme, "EDIT", "applying…");
|
|
1162
|
+
const error = compactError(result, context, theme);
|
|
1163
|
+
if (error) return new Text(error, 0, 0);
|
|
1164
|
+
const stats = editStats(result.details);
|
|
1165
|
+
const suffix = stats ? `${stats.added} added • ${stats.removed} removed • diff hidden` : "applied • diff hidden";
|
|
1166
|
+
return compactToolText(theme, "EDIT", pathFromContext(context, "edit"), suffix, "success");
|
|
1167
|
+
},
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
const write = createWriteToolDefinition(cwd);
|
|
1171
|
+
pi.registerTool({
|
|
1172
|
+
...write,
|
|
1173
|
+
renderCall: (args: unknown, theme: Theme, context: ToolRenderContext) => {
|
|
1174
|
+
const record = isRecord(args) ? args : {};
|
|
1175
|
+
const path = typeof record.path === "string" ? compactPath(record.path, context.cwd) : "file";
|
|
1176
|
+
const content = typeof record.content === "string" ? record.content : "";
|
|
1177
|
+
return compactToolText(theme, "WRITE", path, `${lineCount(content)} lines • content hidden`);
|
|
1178
|
+
},
|
|
1179
|
+
renderResult: (result: AgentToolResult<undefined>, _options, theme: Theme, context) => {
|
|
1180
|
+
if (context.isPartial) return compactToolText(theme, "WRITE", "saving…");
|
|
1181
|
+
const error = compactError(result, context, theme);
|
|
1182
|
+
if (error) return new Text(error, 0, 0);
|
|
1183
|
+
return compactToolText(theme, "WRITE", pathFromContext(context, "write"), "written • content hidden", "success");
|
|
1184
|
+
},
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function compactToolText(theme: Theme, label: string, subject: string, suffix = "", mode: "active" | "success" = "active"): Text {
|
|
1189
|
+
const color = mode === "success" ? "success" : label === "READING" || label === "SEARCH" || label === "FIND" || label === "LIST" ? "mdLink" : "warning";
|
|
1190
|
+
let text = `${theme.fg(color, theme.bold(label))} ${theme.fg("accent", truncateToWidth(cleanInline(subject), 96, "…"))}`;
|
|
1191
|
+
if (suffix) text += theme.fg("dim", ` ${truncateToWidth(cleanInline(suffix), 96, "…")}`);
|
|
1192
|
+
return new Text(text, 0, 0);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function compactError(result: AgentToolResult<unknown>, context: ToolRenderContext, theme: Theme): string | undefined {
|
|
1196
|
+
if (!context.isError) return undefined;
|
|
1197
|
+
const first = latestNonEmptyLine(firstText(result)) || "tool failed";
|
|
1198
|
+
return `${theme.fg("error", theme.bold("ERROR"))} ${theme.fg("dim", truncateToWidth(first, 120, "…"))}`;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function pathFromContext(context: ToolRenderContext, toolName: string): string {
|
|
1202
|
+
const args = isRecord(context.args) ? context.args : {};
|
|
1203
|
+
const path = typeof args.path === "string" ? compactPath(args.path, context.cwd) : undefined;
|
|
1204
|
+
if (path) return path;
|
|
1205
|
+
return pathForTool(toolName, context.args, context.cwd) ?? "done";
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function firstText(result: AgentToolResult<unknown>): string {
|
|
1209
|
+
const first = result.content[0];
|
|
1210
|
+
return first?.type === "text" ? first.text : "";
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function lineCount(value: string): number {
|
|
1214
|
+
if (!value) return 0;
|
|
1215
|
+
const normalized = value.endsWith("\n") ? value.slice(0, -1) : value;
|
|
1216
|
+
if (!normalized) return 0;
|
|
1217
|
+
return normalized.split(/\r?\n/).length;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function editStats(details: EditToolDetails | undefined): { added: number; removed: number } | undefined {
|
|
1221
|
+
const diff = details?.diff ?? details?.patch;
|
|
1222
|
+
if (!diff) return undefined;
|
|
1223
|
+
let added = 0;
|
|
1224
|
+
let removed = 0;
|
|
1225
|
+
for (const line of diff.split(/\r?\n/)) {
|
|
1226
|
+
if (line.startsWith("+++") || line.startsWith("---")) continue;
|
|
1227
|
+
if (line.startsWith("+")) added++;
|
|
1228
|
+
else if (line.startsWith("-")) removed++;
|
|
1229
|
+
}
|
|
1230
|
+
return { added, removed };
|
|
1231
|
+
}
|