@thoth-agents/pi-thoth-theme 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/dist/index.ts +3686 -0
- package/package.json +7 -9
- package/src/index.ts +0 -53
- package/src/input-box/decorate.ts +0 -304
- package/src/input-box/frame.ts +0 -182
- package/src/input-box/gradient.ts +0 -92
- package/src/input-box/state.ts +0 -90
- package/src/render-kit/index.ts +0 -305
- package/src/render-kit/working.ts +0 -25
- package/src/shared/cache.ts +0 -21
- package/src/shared/config.ts +0 -86
- package/src/shared/duration.ts +0 -1
- package/src/shared/icons.ts +0 -89
- package/src/status-line/cost.ts +0 -56
- package/src/status-line/index.ts +0 -242
- package/src/status-line/layout.ts +0 -272
- package/src/status-line/snapshot.ts +0 -11
- package/src/status-line/throughput.ts +0 -61
- package/src/status-line/tokens.ts +0 -51
- package/src/tools/bash.ts +0 -237
- package/src/tools/border.ts +0 -21
- package/src/tools/box.ts +0 -258
- package/src/tools/edit.ts +0 -228
- package/src/tools/file-icons.ts +0 -139
- package/src/tools/find.ts +0 -227
- package/src/tools/frame.ts +0 -166
- package/src/tools/generic.ts +0 -195
- package/src/tools/grep.ts +0 -371
- package/src/tools/index.ts +0 -135
- package/src/tools/ls.ts +0 -231
- package/src/tools/ownership.ts +0 -70
- package/src/tools/powershell.ts +0 -1
- package/src/tools/read.ts +0 -194
- package/src/tools/ticker.ts +0 -101
- package/src/tools/write.ts +0 -188
- package/src/welcome/age.ts +0 -11
- package/src/welcome/index.ts +0 -132
- package/src/welcome/logo.ts +0 -35
- package/src/welcome/render.ts +0 -323
- package/src/welcome/resources.ts +0 -251
package/dist/index.ts
ADDED
|
@@ -0,0 +1,3686 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
|
|
3
|
+
// ../pi-core/src/validation.ts
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function isNonNegativeInteger(value) {
|
|
8
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ../pi-core/src/channels.ts
|
|
12
|
+
function defineChannel(definition) {
|
|
13
|
+
return definition;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ../pi-core/src/duration.ts
|
|
17
|
+
function formatDuration(ms) {
|
|
18
|
+
if (!Number.isFinite(ms) || ms <= 0) return "0s";
|
|
19
|
+
if (ms < 1e3) return `${Math.floor(ms)}ms`;
|
|
20
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1).replace(/\.0$/, "")}s`;
|
|
21
|
+
const totalMinutes = Math.floor(ms / 6e4);
|
|
22
|
+
if (ms < 36e5) {
|
|
23
|
+
const seconds = Math.floor(ms / 1e3) % 60;
|
|
24
|
+
return `${totalMinutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
25
|
+
}
|
|
26
|
+
const hours = Math.floor(ms / 36e5);
|
|
27
|
+
return `${hours}h ${String(totalMinutes % 60).padStart(2, "0")}m`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ../pi-core/src/render-kit.ts
|
|
31
|
+
var nativeFrames = {
|
|
32
|
+
spinnerFrames: Object.freeze([
|
|
33
|
+
"\u280B",
|
|
34
|
+
"\u2819",
|
|
35
|
+
"\u2839",
|
|
36
|
+
"\u2838",
|
|
37
|
+
"\u283C",
|
|
38
|
+
"\u2834",
|
|
39
|
+
"\u2826",
|
|
40
|
+
"\u2827",
|
|
41
|
+
"\u2807",
|
|
42
|
+
"\u280F"
|
|
43
|
+
]),
|
|
44
|
+
workingFrames: Object.freeze(["\u25B3", "\u25ED", "\u25B2", "\u25EE"])
|
|
45
|
+
};
|
|
46
|
+
var terminalFooterGlyphs = {
|
|
47
|
+
completed: "\u2713",
|
|
48
|
+
deleted: "\u2713",
|
|
49
|
+
failed: "\u2717",
|
|
50
|
+
cancelled: "\u2717",
|
|
51
|
+
interrupted: "\u2717",
|
|
52
|
+
blocked: "\u2717"
|
|
53
|
+
};
|
|
54
|
+
function getToolElapsedMs(options) {
|
|
55
|
+
const context = options.context;
|
|
56
|
+
const state = context?.state;
|
|
57
|
+
if (options.elapsedMs !== void 0) {
|
|
58
|
+
const elapsedMs2 = Number.isFinite(options.elapsedMs) ? Math.max(0, options.elapsedMs) : void 0;
|
|
59
|
+
if (state && terminalFooterGlyphs[options.status] && elapsedMs2 !== void 0) {
|
|
60
|
+
state.completedElapsedMs = elapsedMs2;
|
|
61
|
+
}
|
|
62
|
+
return elapsedMs2;
|
|
63
|
+
}
|
|
64
|
+
if (!state) return void 0;
|
|
65
|
+
if (context.executionStarted && (options.status === "running" || options.status === "in_progress") && state.startedAt === void 0) {
|
|
66
|
+
state.startedAt = Date.now();
|
|
67
|
+
}
|
|
68
|
+
if (typeof state.completedElapsedMs === "number" && Number.isFinite(state.completedElapsedMs)) {
|
|
69
|
+
return Math.max(0, state.completedElapsedMs);
|
|
70
|
+
}
|
|
71
|
+
if (typeof state.startedAt !== "number" || !Number.isFinite(state.startedAt)) {
|
|
72
|
+
return void 0;
|
|
73
|
+
}
|
|
74
|
+
const elapsedMs = Math.max(0, Date.now() - state.startedAt);
|
|
75
|
+
if (terminalFooterGlyphs[options.status])
|
|
76
|
+
state.completedElapsedMs = elapsedMs;
|
|
77
|
+
return elapsedMs;
|
|
78
|
+
}
|
|
79
|
+
var RENDER_KIT_VERSION = 1;
|
|
80
|
+
var registryKey = /* @__PURE__ */ Symbol.for(
|
|
81
|
+
`thoth-agents.pi-core.render-kit.v${RENDER_KIT_VERSION}`
|
|
82
|
+
);
|
|
83
|
+
var shared = globalThis;
|
|
84
|
+
function registration() {
|
|
85
|
+
try {
|
|
86
|
+
const record = shared[registryKey];
|
|
87
|
+
if (record && typeof record.token === "symbol" && record.owner !== null && (typeof record.owner === "object" || typeof record.owner === "function"))
|
|
88
|
+
return record;
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
return void 0;
|
|
92
|
+
}
|
|
93
|
+
function registerRenderKit(kit, owner) {
|
|
94
|
+
const token = /* @__PURE__ */ Symbol("render-kit-registration");
|
|
95
|
+
shared[registryKey] = { kit, owner, token };
|
|
96
|
+
return token;
|
|
97
|
+
}
|
|
98
|
+
function withdrawRenderKit(token) {
|
|
99
|
+
if (registration()?.token === token) delete shared[registryKey];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ../pi-core/src/todo.ts
|
|
103
|
+
var todoStatuses = [
|
|
104
|
+
"pending",
|
|
105
|
+
"in_progress",
|
|
106
|
+
"completed",
|
|
107
|
+
"deleted"
|
|
108
|
+
];
|
|
109
|
+
function isTodoSnapshot(value) {
|
|
110
|
+
try {
|
|
111
|
+
if (!isRecord(value)) return false;
|
|
112
|
+
const { tasks, nextId, counts } = value;
|
|
113
|
+
if (!Array.isArray(tasks) || !isTaskId(nextId) || !isRecord(counts))
|
|
114
|
+
return false;
|
|
115
|
+
for (const task of tasks) {
|
|
116
|
+
if (!isTodoTask(task)) return false;
|
|
117
|
+
}
|
|
118
|
+
return todoStatuses.every((status) => isNonNegativeInteger(counts[status]));
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function isTaskId(value) {
|
|
124
|
+
return isNonNegativeInteger(value) && value > 0;
|
|
125
|
+
}
|
|
126
|
+
function isOptionalString(value) {
|
|
127
|
+
return value === void 0 || typeof value === "string";
|
|
128
|
+
}
|
|
129
|
+
function isTodoTask(value) {
|
|
130
|
+
if (!isRecord(value) || !isTaskId(value.id) || typeof value.subject !== "string" || !todoStatuses.some((status) => status === value.status) || !Array.isArray(value.blockedBy) || !isOptionalString(value.description) || !isOptionalString(value.activeForm) || !isOptionalString(value.owner))
|
|
131
|
+
return false;
|
|
132
|
+
for (const id of value.blockedBy) {
|
|
133
|
+
if (!isTaskId(id)) return false;
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
var TODO_STATE_CHANNEL = defineChannel({
|
|
138
|
+
name: "thoth:todo:state",
|
|
139
|
+
version: 1,
|
|
140
|
+
validate: isTodoSnapshot
|
|
141
|
+
});
|
|
142
|
+
function isTodoStateRequest(value) {
|
|
143
|
+
try {
|
|
144
|
+
if (!isRecord(value)) return false;
|
|
145
|
+
const prototype = Object.getPrototypeOf(value);
|
|
146
|
+
return (prototype === Object.prototype || prototype === null) && Reflect.ownKeys(value).length === 0;
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
var TODO_STATE_REQUEST = defineChannel({
|
|
152
|
+
name: "thoth:todo:state:request",
|
|
153
|
+
version: 1,
|
|
154
|
+
validate: isTodoStateRequest
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ../pi-core/src/tool-registry.ts
|
|
158
|
+
var TOOL_DEFINITION_REGISTRY_VERSION = 1;
|
|
159
|
+
var registryKey2 = /* @__PURE__ */ Symbol.for(
|
|
160
|
+
`thoth-agents.pi-core.tool-definitions.v${TOOL_DEFINITION_REGISTRY_VERSION}`
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// src/render-kit/index.ts
|
|
164
|
+
import {
|
|
165
|
+
stripTerminalSequences,
|
|
166
|
+
truncateToWidth as truncateToWidth3,
|
|
167
|
+
wrapTextWithAnsi
|
|
168
|
+
} from "@earendil-works/pi-tui";
|
|
169
|
+
|
|
170
|
+
// src/shared/cache.ts
|
|
171
|
+
function cachedComponent(renderFn) {
|
|
172
|
+
const cache = /* @__PURE__ */ new Map();
|
|
173
|
+
return {
|
|
174
|
+
render(width) {
|
|
175
|
+
let lines = cache.get(width);
|
|
176
|
+
if (lines === void 0) {
|
|
177
|
+
lines = renderFn(width);
|
|
178
|
+
cache.set(width, lines);
|
|
179
|
+
}
|
|
180
|
+
return lines;
|
|
181
|
+
},
|
|
182
|
+
invalidate() {
|
|
183
|
+
cache.clear();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/shared/icons.ts
|
|
189
|
+
var ICONS = {
|
|
190
|
+
branch: { nerd: "\uE0A0", unicode: "\u2442", ascii: "git" },
|
|
191
|
+
folder: { nerd: "\uF07C", unicode: "dir", ascii: "dir" },
|
|
192
|
+
model: { nerd: "\u{F06A9}", unicode: "\u25CF", ascii: "*" },
|
|
193
|
+
effort: { nerd: "\u{F09D1}", unicode: "\u25D0", ascii: "o" },
|
|
194
|
+
context: { nerd: "\uF2DB", unicode: "ctx", ascii: "ctx" },
|
|
195
|
+
cost: { nerd: "\uF155", unicode: "$", ascii: "$" },
|
|
196
|
+
tokensIn: { nerd: "\uF062", unicode: "\u2191", ascii: "^" },
|
|
197
|
+
tokensOut: { nerd: "\uF063", unicode: "\u2193", ascii: "v" },
|
|
198
|
+
cache: { nerd: "\u{F01BC}", unicode: "cache", ascii: "cache" },
|
|
199
|
+
throughput: { nerd: "\u{F04C5}", unicode: "tok/s", ascii: "tok/s" },
|
|
200
|
+
agent: { nerd: "\u{F08C7}", unicode: "\u2699", ascii: "@" },
|
|
201
|
+
file: { nerd: "\u{F0214}", unicode: "file", ascii: "[file]" },
|
|
202
|
+
tool: { nerd: "\uF0AD", unicode: "*", ascii: "*" },
|
|
203
|
+
bash: { nerd: "\uF489", unicode: "$", ascii: "$" },
|
|
204
|
+
powershell: { nerd: "\uE70F", unicode: "PS", ascii: "PS" },
|
|
205
|
+
read: { nerd: "\uF06E", unicode: "read", ascii: "[read]" },
|
|
206
|
+
write: { nerd: "\uF0C7", unicode: "write", ascii: "[write]" },
|
|
207
|
+
edit: { nerd: "\uF044", unicode: "edit", ascii: "[edit]" },
|
|
208
|
+
search: { nerd: "\uF002", unicode: "search", ascii: "?" },
|
|
209
|
+
separator: { nerd: "\xB7", unicode: "\xB7", ascii: "|" },
|
|
210
|
+
ellipsis: { nerd: "\u2026", unicode: "\u2026", ascii: "..." },
|
|
211
|
+
arrowUp: { nerd: "\u2191", unicode: "\u2191", ascii: "^" },
|
|
212
|
+
arrowDown: { nerd: "\u2193", unicode: "\u2193", ascii: "v" },
|
|
213
|
+
arrowLeft: { nerd: "\u2190", unicode: "\u2190", ascii: "<" },
|
|
214
|
+
arrowRight: { nerd: "\u2192", unicode: "\u2192", ascii: ">" },
|
|
215
|
+
selection: { nerd: "\u203A", unicode: "\u203A", ascii: ">" },
|
|
216
|
+
selectionSelected: { nerd: "\uF111", unicode: "\u25CF", ascii: "*" },
|
|
217
|
+
selectionUnselected: { nerd: "\uF10C", unicode: "\u25CB", ascii: "o" },
|
|
218
|
+
taskInProgress: { nerd: "\u25D0", unicode: "\u25C7", ascii: "*" },
|
|
219
|
+
separatorHeavy: { nerd: "\u2503", unicode: "\u2503", ascii: "|" },
|
|
220
|
+
boxTopLeft: { nerd: "\u256D", unicode: "\u256D", ascii: "+" },
|
|
221
|
+
boxTopRight: { nerd: "\u256E", unicode: "\u256E", ascii: "+" },
|
|
222
|
+
boxVertical: { nerd: "\u2502", unicode: "\u2502", ascii: "|" },
|
|
223
|
+
boxBottomLeft: { nerd: "\u2570", unicode: "\u2570", ascii: "+" },
|
|
224
|
+
boxBottomRight: { nerd: "\u256F", unicode: "\u256F", ascii: "+" },
|
|
225
|
+
boxHorizontal: { nerd: "\u2500", unicode: "\u2500", ascii: "-" },
|
|
226
|
+
boxTDown: { nerd: "\u252C", unicode: "\u252C", ascii: "+" },
|
|
227
|
+
boxTUp: { nerd: "\u2534", unicode: "\u2534", ascii: "+" },
|
|
228
|
+
boxTRight: { nerd: "\u251C", unicode: "\u251C", ascii: "+" },
|
|
229
|
+
boxTLeft: { nerd: "\u2524", unicode: "\u2524", ascii: "+" },
|
|
230
|
+
boxCross: { nerd: "\u253C", unicode: "\u253C", ascii: "+" },
|
|
231
|
+
close: { nerd: "\uF00D", unicode: "\u2715", ascii: "x" },
|
|
232
|
+
scrollUp: { nerd: "\u2191", unicode: "\u2191", ascii: "^" },
|
|
233
|
+
scrollDown: { nerd: "\u2193", unicode: "\u2193", ascii: "v" },
|
|
234
|
+
ready: { nerd: "\u25B2", unicode: "\u25B2", ascii: "^" },
|
|
235
|
+
warning: { nerd: "\uF071", unicode: "\u26A0", ascii: "!" }
|
|
236
|
+
};
|
|
237
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
238
|
+
var WORKING_FRAMES = ["\u25B3", "\u25ED", "\u25B2", "\u25EE"];
|
|
239
|
+
var FRAMES = {
|
|
240
|
+
spinnerFrames: {
|
|
241
|
+
nerd: SPINNER_FRAMES,
|
|
242
|
+
unicode: SPINNER_FRAMES,
|
|
243
|
+
ascii: ["|", "/", "-", "\\"]
|
|
244
|
+
},
|
|
245
|
+
workingFrames: {
|
|
246
|
+
nerd: WORKING_FRAMES,
|
|
247
|
+
unicode: WORKING_FRAMES,
|
|
248
|
+
ascii: [".", "o", "O", "0"]
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
var STOP = { nerd: "\uF05E", unicode: "\u25A0", ascii: "/" };
|
|
252
|
+
var BLOCKED = { nerd: "\uF05E", unicode: "\u2298", ascii: "/" };
|
|
253
|
+
var STATUS_ICONS = {
|
|
254
|
+
pending: { nerd: "\uF10C", unicode: "\u25CB", ascii: "-" },
|
|
255
|
+
queued: { nerd: "\u{F051F}", unicode: "\u25CB", ascii: "~" },
|
|
256
|
+
in_progress: { nerd: "\u25D0", unicode: "\u25D0", ascii: "*" },
|
|
257
|
+
running: { nerd: "\u25D0", unicode: "\u25D0", ascii: "*" },
|
|
258
|
+
completed: { nerd: "\uF00C", unicode: "\u2713", ascii: "+" },
|
|
259
|
+
failed: { nerd: "\uF00D", unicode: "\u2717", ascii: "x" },
|
|
260
|
+
cancelled: STOP,
|
|
261
|
+
interrupted: STOP,
|
|
262
|
+
stopping: STOP,
|
|
263
|
+
deleted: BLOCKED,
|
|
264
|
+
blocked: BLOCKED,
|
|
265
|
+
warning: { nerd: "\uF071", unicode: "!", ascii: "!" },
|
|
266
|
+
unknown: { nerd: "\uF128", unicode: "?", ascii: "?" }
|
|
267
|
+
};
|
|
268
|
+
function icon(name, mode) {
|
|
269
|
+
return ICONS[name][mode];
|
|
270
|
+
}
|
|
271
|
+
function frames(name, mode) {
|
|
272
|
+
return FRAMES[name][mode];
|
|
273
|
+
}
|
|
274
|
+
function statusIcon(status, mode) {
|
|
275
|
+
return STATUS_ICONS[status][mode];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/tools/border.ts
|
|
279
|
+
function getBorderTone(state) {
|
|
280
|
+
return state.isError ? "error" : state.isSuccess ? "success" : "accent";
|
|
281
|
+
}
|
|
282
|
+
function getToolBorderTone(context) {
|
|
283
|
+
return getBorderTone({
|
|
284
|
+
isError: context?.isError,
|
|
285
|
+
isSuccess: !context?.isPartial
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/tools/box.ts
|
|
290
|
+
import {
|
|
291
|
+
DEFAULT_MAX_BYTES,
|
|
292
|
+
formatSize
|
|
293
|
+
} from "@earendil-works/pi-coding-agent";
|
|
294
|
+
import {
|
|
295
|
+
truncateToWidth,
|
|
296
|
+
visibleWidth
|
|
297
|
+
} from "@earendil-works/pi-tui";
|
|
298
|
+
var BOX_HORIZONTAL = "\u2500";
|
|
299
|
+
var BOX_VERTICAL = "\u2502";
|
|
300
|
+
var BOX_ROUND_TOP_LEFT = "\u256D";
|
|
301
|
+
var BOX_ROUND_TOP_RIGHT = "\u256E";
|
|
302
|
+
var BOX_ROUND_BOTTOM_LEFT = "\u2570";
|
|
303
|
+
var BOX_ROUND_BOTTOM_RIGHT = "\u256F";
|
|
304
|
+
function getRawResultText(result) {
|
|
305
|
+
if (!result || !result.content || !Array.isArray(result.content)) return "";
|
|
306
|
+
return result.content.filter(
|
|
307
|
+
(block) => block && typeof block === "object" && block.type === "text"
|
|
308
|
+
).map((block) => String(block.text ?? "")).join("\n");
|
|
309
|
+
}
|
|
310
|
+
function getResultText(result) {
|
|
311
|
+
return getRawResultText(result).replace(/\r/g, "");
|
|
312
|
+
}
|
|
313
|
+
function escapeControlCharacters(text) {
|
|
314
|
+
let escaped = "";
|
|
315
|
+
for (const character of text) {
|
|
316
|
+
const code = character.charCodeAt(0);
|
|
317
|
+
if (code < 32 && character !== " ") {
|
|
318
|
+
escaped += String.fromCharCode(9216 + code);
|
|
319
|
+
} else if (code === 127) {
|
|
320
|
+
escaped += "\u2421";
|
|
321
|
+
} else if (code >= 128 && code <= 159) {
|
|
322
|
+
escaped += `\\x${code.toString(16)}`;
|
|
323
|
+
} else {
|
|
324
|
+
escaped += character;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return escaped;
|
|
328
|
+
}
|
|
329
|
+
function stripSgr(text) {
|
|
330
|
+
return text.replace(/(?:\x1b\[|\x9b)[0-9;:]*m/g, "");
|
|
331
|
+
}
|
|
332
|
+
function escapeOutputRow(text) {
|
|
333
|
+
return escapeControlCharacters(stripSgr(text));
|
|
334
|
+
}
|
|
335
|
+
function hasImageContent(result) {
|
|
336
|
+
if (!result || !result.content || !Array.isArray(result.content))
|
|
337
|
+
return false;
|
|
338
|
+
return result.content.some(
|
|
339
|
+
(block) => block && typeof block === "object" && block.type === "image"
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
function splitResultNotice(text, details, tool) {
|
|
343
|
+
if (!details || typeof details !== "object") return { text, notices: [] };
|
|
344
|
+
const metadata = details;
|
|
345
|
+
let limitNotices = [""];
|
|
346
|
+
if (tool === "ls" && typeof metadata.entryLimitReached === "number") {
|
|
347
|
+
const limit = metadata.entryLimitReached;
|
|
348
|
+
limitNotices = [
|
|
349
|
+
`${limit} entries limit reached. Use limit=${limit * 2} for more`
|
|
350
|
+
];
|
|
351
|
+
} else if (tool === "find" && typeof metadata.resultLimitReached === "number") {
|
|
352
|
+
const limit = metadata.resultLimitReached;
|
|
353
|
+
limitNotices = [
|
|
354
|
+
`${limit} results limit reached. Use limit=${limit * 2} for more, or refine pattern`,
|
|
355
|
+
// SDK custom glob operations use the shorter variant.
|
|
356
|
+
`${limit} results limit reached`
|
|
357
|
+
];
|
|
358
|
+
} else if (tool === "grep" && typeof metadata.matchLimitReached === "number") {
|
|
359
|
+
const limit = metadata.matchLimitReached;
|
|
360
|
+
limitNotices = [
|
|
361
|
+
`${limit} matches limit reached. Use limit=${limit * 2} for more, or refine pattern`
|
|
362
|
+
];
|
|
363
|
+
}
|
|
364
|
+
const commonNotices = [];
|
|
365
|
+
if (metadata.truncation?.truncated === true) {
|
|
366
|
+
commonNotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
367
|
+
}
|
|
368
|
+
if (tool === "grep" && metadata.linesTruncated === true) {
|
|
369
|
+
commonNotices.push(
|
|
370
|
+
"Some lines truncated to 500 chars. Use read tool to see full lines"
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
for (const limitNotice of limitNotices) {
|
|
374
|
+
const notice = [limitNotice, ...commonNotices].filter(Boolean).join(". ");
|
|
375
|
+
if (!notice) continue;
|
|
376
|
+
const appendix = `[${notice}]`;
|
|
377
|
+
const suffix = `
|
|
378
|
+
|
|
379
|
+
${appendix}`;
|
|
380
|
+
if (text.endsWith(suffix)) {
|
|
381
|
+
return { text: text.slice(0, -suffix.length), notices: [appendix] };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return { text, notices: [] };
|
|
385
|
+
}
|
|
386
|
+
function truncateBoxLine(line, limit) {
|
|
387
|
+
return line.length <= limit && !/[^\x20-\x7e]/.test(line) ? line : truncateToWidth(line, limit);
|
|
388
|
+
}
|
|
389
|
+
function renderBox(theme, bodyLines, width, options = {}) {
|
|
390
|
+
const safeWidth = Math.max(0, Math.floor(width));
|
|
391
|
+
const borderColor = getBorderTone(options);
|
|
392
|
+
if (safeWidth <= 0) {
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
395
|
+
if (safeWidth <= 4) {
|
|
396
|
+
const raw = [options.title, ...bodyLines, options.footer].filter(
|
|
397
|
+
(l) => typeof l === "string" && l.length > 0
|
|
398
|
+
);
|
|
399
|
+
const lines = raw.length > 0 ? raw : bodyLines;
|
|
400
|
+
return lines.map((l) => truncateBoxLine(l, safeWidth));
|
|
401
|
+
}
|
|
402
|
+
const result = [];
|
|
403
|
+
const innerWidth = Math.max(1, safeWidth - 2);
|
|
404
|
+
if (options.title) {
|
|
405
|
+
const rawTitle = ` ${options.title} `;
|
|
406
|
+
const maxTitleWidth = Math.max(1, innerWidth - 4);
|
|
407
|
+
const fitTitle = visibleWidth(rawTitle) > maxTitleWidth ? ` ${truncateToWidth(options.title, Math.max(1, maxTitleWidth - 2))} ` : rawTitle;
|
|
408
|
+
const titleWidth = visibleWidth(fitTitle);
|
|
409
|
+
const remainingDashes = Math.max(1, innerWidth - 2 - titleWidth);
|
|
410
|
+
const top = `${theme.fg(borderColor, `${BOX_ROUND_TOP_LEFT}${BOX_HORIZONTAL}${BOX_HORIZONTAL}`)}${fitTitle}${theme.fg(
|
|
411
|
+
borderColor,
|
|
412
|
+
`${BOX_HORIZONTAL.repeat(remainingDashes)}${BOX_ROUND_TOP_RIGHT}`
|
|
413
|
+
)}`;
|
|
414
|
+
result.push(truncateToWidth(top, safeWidth));
|
|
415
|
+
} else {
|
|
416
|
+
const top = `${theme.fg(borderColor, `${BOX_ROUND_TOP_LEFT}${BOX_HORIZONTAL.repeat(innerWidth)}${BOX_ROUND_TOP_RIGHT}`)}`;
|
|
417
|
+
result.push(truncateToWidth(top, safeWidth));
|
|
418
|
+
}
|
|
419
|
+
const contentWidth = Math.max(1, innerWidth - 2);
|
|
420
|
+
for (const line of bodyLines) {
|
|
421
|
+
const truncated = truncateBoxLine(line, contentWidth);
|
|
422
|
+
const pad = Math.max(0, contentWidth - visibleWidth(truncated));
|
|
423
|
+
const framed = `${theme.fg(borderColor, `${BOX_VERTICAL} `)}${truncated}${" ".repeat(pad)}${theme.fg(borderColor, ` ${BOX_VERTICAL}`)}`;
|
|
424
|
+
result.push(truncateBoxLine(framed, safeWidth));
|
|
425
|
+
}
|
|
426
|
+
if (options.footer) {
|
|
427
|
+
const rawFooter = ` ${options.footer} `;
|
|
428
|
+
const maxFooterWidth = Math.max(1, innerWidth - 4);
|
|
429
|
+
const fitFooter = visibleWidth(rawFooter) > maxFooterWidth ? ` ${truncateToWidth(options.footer, Math.max(1, maxFooterWidth - 2))} ` : rawFooter;
|
|
430
|
+
const footerWidth = visibleWidth(fitFooter);
|
|
431
|
+
const remainingDashes = Math.max(1, innerWidth - 2 - footerWidth);
|
|
432
|
+
const bottom = `${theme.fg(borderColor, `${BOX_ROUND_BOTTOM_LEFT}${BOX_HORIZONTAL}${BOX_HORIZONTAL}`)}${fitFooter}${theme.fg(
|
|
433
|
+
borderColor,
|
|
434
|
+
`${BOX_HORIZONTAL.repeat(remainingDashes)}${BOX_ROUND_BOTTOM_RIGHT}`
|
|
435
|
+
)}`;
|
|
436
|
+
result.push(truncateToWidth(bottom, safeWidth));
|
|
437
|
+
} else {
|
|
438
|
+
const bottom = `${theme.fg(
|
|
439
|
+
borderColor,
|
|
440
|
+
`${BOX_ROUND_BOTTOM_LEFT}${BOX_HORIZONTAL.repeat(innerWidth)}${BOX_ROUND_BOTTOM_RIGHT}`
|
|
441
|
+
)}`;
|
|
442
|
+
result.push(truncateToWidth(bottom, safeWidth));
|
|
443
|
+
}
|
|
444
|
+
return result.map((l) => truncateBoxLine(l, safeWidth));
|
|
445
|
+
}
|
|
446
|
+
function createComponent(renderFn) {
|
|
447
|
+
return cachedComponent((width) => {
|
|
448
|
+
const safeWidth = Math.max(0, Math.floor(width));
|
|
449
|
+
if (safeWidth <= 0) return [];
|
|
450
|
+
const lines = renderFn(safeWidth);
|
|
451
|
+
return lines.map((line) => truncateToWidth(line, safeWidth));
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/tools/frame.ts
|
|
456
|
+
import { relative, resolve } from "node:path";
|
|
457
|
+
import { truncateToWidth as truncateToWidth2, visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
|
|
458
|
+
var FRAME_HORIZONTAL = "\u2500";
|
|
459
|
+
var FRAME_VERTICAL = "\u2502";
|
|
460
|
+
var FRAME_ROUND_TOP_LEFT = "\u256D";
|
|
461
|
+
var FRAME_ROUND_TOP_RIGHT = "\u256E";
|
|
462
|
+
var FRAME_ROUND_BOTTOM_LEFT = "\u2570";
|
|
463
|
+
var FRAME_ROUND_BOTTOM_RIGHT = "\u256F";
|
|
464
|
+
var FRAME_DIVIDER_LEFT = "\u251C";
|
|
465
|
+
var FRAME_DIVIDER_RIGHT = "\u2524";
|
|
466
|
+
function formatDisplayPath(targetPath, cwd) {
|
|
467
|
+
if (!targetPath) return "";
|
|
468
|
+
if (!cwd) return targetPath;
|
|
469
|
+
try {
|
|
470
|
+
const absTarget = resolve(cwd, targetPath);
|
|
471
|
+
const absCwd = resolve(cwd);
|
|
472
|
+
const rel = relative(absCwd, absTarget);
|
|
473
|
+
if (!rel.startsWith("..") && !rel.startsWith("/") && !/^[A-Za-z]:/.test(rel)) {
|
|
474
|
+
return rel.replace(/\\/g, "/") || ".";
|
|
475
|
+
}
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
return targetPath;
|
|
479
|
+
}
|
|
480
|
+
function hasToolResult(context) {
|
|
481
|
+
if (!context) return false;
|
|
482
|
+
return Boolean(context.state?.hasResult);
|
|
483
|
+
}
|
|
484
|
+
function isFramedContext(context) {
|
|
485
|
+
if (!context) return false;
|
|
486
|
+
return Boolean(context.toolCallId || context.state);
|
|
487
|
+
}
|
|
488
|
+
function renderFrameTop(theme, title, width, borderColor = "accent") {
|
|
489
|
+
const safeWidth = Math.max(0, Math.floor(width));
|
|
490
|
+
if (safeWidth <= 0) return [];
|
|
491
|
+
if (safeWidth <= 4) {
|
|
492
|
+
return [truncateToWidth2(title, safeWidth)];
|
|
493
|
+
}
|
|
494
|
+
const innerWidth = Math.max(1, safeWidth - 2);
|
|
495
|
+
const rawTitle = ` ${title} `;
|
|
496
|
+
const maxTitleWidth = Math.max(1, innerWidth - 4);
|
|
497
|
+
const fitTitle = visibleWidth2(rawTitle) > maxTitleWidth ? ` ${truncateToWidth2(title, Math.max(1, maxTitleWidth - 2))} ` : rawTitle;
|
|
498
|
+
const titleWidth = visibleWidth2(fitTitle);
|
|
499
|
+
const remainingDashes = Math.max(1, innerWidth - 2 - titleWidth);
|
|
500
|
+
const top = `${theme.fg(borderColor, `${FRAME_ROUND_TOP_LEFT}${FRAME_HORIZONTAL}${FRAME_HORIZONTAL}`)}${fitTitle}${theme.fg(
|
|
501
|
+
borderColor,
|
|
502
|
+
`${FRAME_HORIZONTAL.repeat(remainingDashes)}${FRAME_ROUND_TOP_RIGHT}`
|
|
503
|
+
)}`;
|
|
504
|
+
return [truncateToWidth2(top, safeWidth)];
|
|
505
|
+
}
|
|
506
|
+
function renderFrameRow(theme, line, width, borderColor = "accent") {
|
|
507
|
+
const safeWidth = Math.max(0, Math.floor(width));
|
|
508
|
+
if (safeWidth <= 0) return [];
|
|
509
|
+
if (safeWidth <= 4) {
|
|
510
|
+
return [truncateToWidth2(line, safeWidth)];
|
|
511
|
+
}
|
|
512
|
+
const innerWidth = Math.max(1, safeWidth - 2);
|
|
513
|
+
const contentWidth = Math.max(1, innerWidth - 2);
|
|
514
|
+
const truncated = truncateToWidth2(line, contentWidth);
|
|
515
|
+
const pad = Math.max(0, contentWidth - visibleWidth2(truncated));
|
|
516
|
+
const framed = `${theme.fg(borderColor, `${FRAME_VERTICAL} `)}${truncated}${" ".repeat(pad)}${theme.fg(borderColor, ` ${FRAME_VERTICAL}`)}`;
|
|
517
|
+
return [truncateToWidth2(framed, safeWidth)];
|
|
518
|
+
}
|
|
519
|
+
function renderFrameDivider(theme, title, width, borderColor = "accent") {
|
|
520
|
+
const safeWidth = Math.max(0, Math.floor(width));
|
|
521
|
+
if (safeWidth <= 0) return [];
|
|
522
|
+
if (safeWidth <= 4) {
|
|
523
|
+
return [truncateToWidth2(title, safeWidth)];
|
|
524
|
+
}
|
|
525
|
+
const innerWidth = Math.max(1, safeWidth - 2);
|
|
526
|
+
const rawTitle = ` ${title} `;
|
|
527
|
+
const maxTitleWidth = Math.max(1, innerWidth - 4);
|
|
528
|
+
const fitTitle = visibleWidth2(rawTitle) > maxTitleWidth ? ` ${truncateToWidth2(title, Math.max(1, maxTitleWidth - 2))} ` : rawTitle;
|
|
529
|
+
const titleWidth = visibleWidth2(fitTitle);
|
|
530
|
+
const remainingDashes = Math.max(1, innerWidth - 2 - titleWidth);
|
|
531
|
+
const divider = `${theme.fg(borderColor, `${FRAME_DIVIDER_LEFT}${FRAME_HORIZONTAL}${FRAME_HORIZONTAL}`)}${theme.fg("dim", fitTitle)}${theme.fg(
|
|
532
|
+
borderColor,
|
|
533
|
+
`${FRAME_HORIZONTAL.repeat(remainingDashes)}${FRAME_DIVIDER_RIGHT}`
|
|
534
|
+
)}`;
|
|
535
|
+
return [truncateToWidth2(divider, safeWidth)];
|
|
536
|
+
}
|
|
537
|
+
function renderFrameBottom(theme, footer, width, borderColor = "accent") {
|
|
538
|
+
const safeWidth = Math.max(0, Math.floor(width));
|
|
539
|
+
if (safeWidth <= 0) return [];
|
|
540
|
+
if (safeWidth <= 4) {
|
|
541
|
+
return footer ? [truncateToWidth2(footer, safeWidth)] : [];
|
|
542
|
+
}
|
|
543
|
+
const innerWidth = Math.max(1, safeWidth - 2);
|
|
544
|
+
if (footer) {
|
|
545
|
+
const rawFooter = ` ${footer} `;
|
|
546
|
+
const maxFooterWidth = Math.max(1, innerWidth - 4);
|
|
547
|
+
const fitFooter = visibleWidth2(rawFooter) > maxFooterWidth ? ` ${truncateToWidth2(footer, Math.max(1, maxFooterWidth - 2))} ` : rawFooter;
|
|
548
|
+
const footerWidth = visibleWidth2(fitFooter);
|
|
549
|
+
const remainingDashes = Math.max(1, innerWidth - 2 - footerWidth);
|
|
550
|
+
const bottom2 = `${theme.fg(borderColor, `${FRAME_ROUND_BOTTOM_LEFT}${FRAME_HORIZONTAL}${FRAME_HORIZONTAL}`)}${fitFooter}${theme.fg(
|
|
551
|
+
borderColor,
|
|
552
|
+
`${FRAME_HORIZONTAL.repeat(remainingDashes)}${FRAME_ROUND_BOTTOM_RIGHT}`
|
|
553
|
+
)}`;
|
|
554
|
+
return [truncateToWidth2(bottom2, safeWidth)];
|
|
555
|
+
}
|
|
556
|
+
const bottom = `${theme.fg(
|
|
557
|
+
borderColor,
|
|
558
|
+
`${FRAME_ROUND_BOTTOM_LEFT}${FRAME_HORIZONTAL.repeat(innerWidth)}${FRAME_ROUND_BOTTOM_RIGHT}`
|
|
559
|
+
)}`;
|
|
560
|
+
return [truncateToWidth2(bottom, safeWidth)];
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/tools/ticker.ts
|
|
564
|
+
var tickerStates = /* @__PURE__ */ new Map();
|
|
565
|
+
var activeOwner;
|
|
566
|
+
function activateElapsedTickerOwner(owner) {
|
|
567
|
+
activeOwner = owner;
|
|
568
|
+
}
|
|
569
|
+
function releaseElapsedTickerOwner(owner) {
|
|
570
|
+
if (activeOwner === owner) activeOwner = void 0;
|
|
571
|
+
}
|
|
572
|
+
function legacyElapsedOverride(state) {
|
|
573
|
+
return typeof state?.completedElapsedMs === "number" && !Number.isFinite(state.completedElapsedMs) ? 0 : void 0;
|
|
574
|
+
}
|
|
575
|
+
function stopElapsedTicker(state) {
|
|
576
|
+
if (state.elapsedTicker !== void 0) clearInterval(state.elapsedTicker);
|
|
577
|
+
delete state.elapsedTicker;
|
|
578
|
+
tickerStates.delete(state);
|
|
579
|
+
}
|
|
580
|
+
function stopElapsedTickers(owner) {
|
|
581
|
+
for (const [state, stateOwner] of tickerStates) {
|
|
582
|
+
if (stateOwner !== owner) continue;
|
|
583
|
+
getToolElapsedMs({ status: "interrupted", context: { state } });
|
|
584
|
+
stopElapsedTicker(state);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
function syncElapsedTicker(context, owner = activeOwner, status) {
|
|
588
|
+
const state = context?.state;
|
|
589
|
+
if (!state) return;
|
|
590
|
+
const resolvedStatus = status ?? (context.isPartial ? "running" : context.isError ? "failed" : "completed");
|
|
591
|
+
getToolElapsedMs({
|
|
592
|
+
status: resolvedStatus,
|
|
593
|
+
context,
|
|
594
|
+
elapsedMs: status === void 0 ? legacyElapsedOverride(state) : void 0
|
|
595
|
+
});
|
|
596
|
+
if (!context.executionStarted || resolvedStatus !== "running" && resolvedStatus !== "in_progress" || state.completedElapsedMs !== void 0) {
|
|
597
|
+
stopElapsedTicker(state);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (owner === activeOwner && owner !== void 0 && context.invalidate && state.elapsedTicker === void 0) {
|
|
601
|
+
state.elapsedTicker = setInterval(context.invalidate, 1e3);
|
|
602
|
+
tickerStates.set(state, owner);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/render-kit/working.ts
|
|
607
|
+
function workingFrame(elapsedMs = 0, mode = "nerd") {
|
|
608
|
+
const elapsed = Number.isFinite(elapsedMs) ? Math.max(0, elapsedMs) : 0;
|
|
609
|
+
const working = frames("workingFrames", mode);
|
|
610
|
+
return working[Math.floor(elapsed / 1e3) % working.length];
|
|
611
|
+
}
|
|
612
|
+
function runningFooter(theme, elapsedMs, mode) {
|
|
613
|
+
return [
|
|
614
|
+
theme.fg("accent", workingFrame(elapsedMs, mode)),
|
|
615
|
+
elapsedMs === void 0 ? "" : theme.fg("dim", formatDuration(Math.floor(elapsedMs / 1e3) * 1e3))
|
|
616
|
+
].filter(Boolean).join(theme.fg("dim", ` ${icon("separator", mode)} `));
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/render-kit/index.ts
|
|
620
|
+
var STATUS_ROLE = {
|
|
621
|
+
pending: "dim",
|
|
622
|
+
queued: "dim",
|
|
623
|
+
in_progress: "accent",
|
|
624
|
+
running: "accent",
|
|
625
|
+
completed: "success",
|
|
626
|
+
failed: "error",
|
|
627
|
+
cancelled: "muted",
|
|
628
|
+
interrupted: "warning",
|
|
629
|
+
stopping: "warning",
|
|
630
|
+
deleted: "muted",
|
|
631
|
+
blocked: "warning",
|
|
632
|
+
unknown: "dim"
|
|
633
|
+
};
|
|
634
|
+
var TOOL_FOOTER_STATUS = {
|
|
635
|
+
completed: "completed",
|
|
636
|
+
deleted: "completed",
|
|
637
|
+
failed: "failed",
|
|
638
|
+
cancelled: "failed",
|
|
639
|
+
interrupted: "failed",
|
|
640
|
+
blocked: "failed"
|
|
641
|
+
};
|
|
642
|
+
function startsWithFooterGlyph(footer, mode) {
|
|
643
|
+
const plain = stripTerminalSequences(footer);
|
|
644
|
+
return ["completed", "failed"].some((status) => {
|
|
645
|
+
const glyph = statusIcon(status, mode);
|
|
646
|
+
return plain.startsWith(glyph) && (plain.length === glyph.length || /^\s/u.test(plain.slice(glyph.length)));
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
function isRunning(status) {
|
|
650
|
+
return status === "running" || status === "in_progress";
|
|
651
|
+
}
|
|
652
|
+
function statusGlyph(theme, status, mode) {
|
|
653
|
+
return theme.fg(STATUS_ROLE[status], statusIcon(status, mode));
|
|
654
|
+
}
|
|
655
|
+
function indicatorStatus(context) {
|
|
656
|
+
if (context.executionStarted && context.isPartial) return "running";
|
|
657
|
+
if (context.isError) return "failed";
|
|
658
|
+
return context.executionStarted ? "completed" : "pending";
|
|
659
|
+
}
|
|
660
|
+
function indicatorLabel(status) {
|
|
661
|
+
if (status === "completed") return "Done";
|
|
662
|
+
if (status === "failed") return "Error";
|
|
663
|
+
return status;
|
|
664
|
+
}
|
|
665
|
+
function card(theme, options, width, toolFooter2, mode) {
|
|
666
|
+
const safeWidth = Math.max(0, Math.floor(width));
|
|
667
|
+
if (safeWidth <= 0) return [];
|
|
668
|
+
const bodyWidth = safeWidth <= 4 ? safeWidth : Math.max(1, safeWidth - 4);
|
|
669
|
+
const rows = (source) => {
|
|
670
|
+
const resolved = typeof source === "function" ? source(bodyWidth) : source;
|
|
671
|
+
return (resolved ?? []).flatMap(
|
|
672
|
+
(row) => options.wrap ? wrapTextWithAnsi(row, bodyWidth) : [row]
|
|
673
|
+
);
|
|
674
|
+
};
|
|
675
|
+
let footer = options.footer ?? "";
|
|
676
|
+
if (options.footer === void 0 && options.status) {
|
|
677
|
+
footer = options.context ? toolFooter2(theme, {
|
|
678
|
+
status: options.status,
|
|
679
|
+
context: options.context,
|
|
680
|
+
summary: options.summary
|
|
681
|
+
}) : statusGlyph(theme, options.status, mode);
|
|
682
|
+
} else if (options.status && !isRunning(options.status) && !(TOOL_FOOTER_STATUS[options.status] && startsWithFooterGlyph(footer, mode))) {
|
|
683
|
+
footer = [statusGlyph(theme, options.status, mode), footer].filter(Boolean).join(" ");
|
|
684
|
+
}
|
|
685
|
+
const body = rows(options.body);
|
|
686
|
+
const borderState = {
|
|
687
|
+
isError: options.isError,
|
|
688
|
+
isSuccess: options.isSuccess === true
|
|
689
|
+
};
|
|
690
|
+
if (!options.sections?.length && (!options.part || options.part === "full")) {
|
|
691
|
+
return renderBox(theme, body, safeWidth, {
|
|
692
|
+
...options,
|
|
693
|
+
...borderState,
|
|
694
|
+
footer
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
const borderTone = getBorderTone(borderState);
|
|
698
|
+
return [
|
|
699
|
+
...options.part === "end" ? [] : options.title ? renderFrameTop(theme, options.title, safeWidth, borderTone) : renderBox(theme, [], safeWidth, borderState).slice(0, 1),
|
|
700
|
+
...body.flatMap((row) => renderFrameRow(theme, row, safeWidth, borderTone)),
|
|
701
|
+
...(options.sections ?? []).flatMap((section) => [
|
|
702
|
+
...renderFrameDivider(theme, section.title ?? "", safeWidth, borderTone),
|
|
703
|
+
...rows(section.rows).flatMap(
|
|
704
|
+
(row) => renderFrameRow(theme, row, safeWidth, borderTone)
|
|
705
|
+
)
|
|
706
|
+
]),
|
|
707
|
+
...options.part === "start" ? [] : renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
708
|
+
];
|
|
709
|
+
}
|
|
710
|
+
function toolFooter(theme, options, mode, owner) {
|
|
711
|
+
syncElapsedTicker(options.context ?? {}, owner, options.status);
|
|
712
|
+
const elapsedMs = getToolElapsedMs(options);
|
|
713
|
+
if (isRunning(options.status)) return runningFooter(theme, elapsedMs, mode);
|
|
714
|
+
const footerStatus = TOOL_FOOTER_STATUS[options.status];
|
|
715
|
+
const glyph = footerStatus ? statusIcon(footerStatus, mode) : void 0;
|
|
716
|
+
const summary = typeof options.summary === "string" ? [options.summary] : options.summary ?? [];
|
|
717
|
+
const parts = [
|
|
718
|
+
theme.fg(STATUS_ROLE[options.status], glyph ?? options.status)
|
|
719
|
+
];
|
|
720
|
+
if (elapsedMs !== void 0) {
|
|
721
|
+
parts.push(
|
|
722
|
+
theme.fg("dim", formatDuration(Math.floor(elapsedMs / 1e3) * 1e3))
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
if (glyph) {
|
|
726
|
+
parts.push(...summary.filter(Boolean).map((part) => theme.fg("dim", part)));
|
|
727
|
+
}
|
|
728
|
+
return parts.join(theme.fg("dim", ` ${icon("separator", mode)} `));
|
|
729
|
+
}
|
|
730
|
+
function createRenderKit(owner, resolveToolRenderers, mode = "nerd") {
|
|
731
|
+
const separator2 = icon("separator", mode);
|
|
732
|
+
const ellipsis = icon("ellipsis", mode);
|
|
733
|
+
const ownedToolFooter = (theme, options) => toolFooter(theme, options, mode, owner);
|
|
734
|
+
return {
|
|
735
|
+
version: 1,
|
|
736
|
+
...resolveToolRenderers ? { resolveToolRenderers } : {},
|
|
737
|
+
toolFooter: ownedToolFooter,
|
|
738
|
+
card(theme, options, width) {
|
|
739
|
+
if (options.footer !== void 0 && options.context && options.status) {
|
|
740
|
+
syncElapsedTicker(options.context, owner, options.status);
|
|
741
|
+
}
|
|
742
|
+
return card(theme, options, width, ownedToolFooter, mode);
|
|
743
|
+
},
|
|
744
|
+
collapse(theme, rows, options = {}) {
|
|
745
|
+
const budget = Math.max(0, Math.floor(options.budget ?? 8));
|
|
746
|
+
if (options.expanded || rows.length <= budget) return [...rows];
|
|
747
|
+
return [
|
|
748
|
+
...rows.slice(0, budget),
|
|
749
|
+
theme.fg(
|
|
750
|
+
"dim",
|
|
751
|
+
`${ellipsis} ${rows.length - budget} more lines ${separator2} ${options.expandHint ?? "ctrl+o to expand"}`
|
|
752
|
+
)
|
|
753
|
+
];
|
|
754
|
+
},
|
|
755
|
+
cachedComponent,
|
|
756
|
+
indicator(theme, context = {}, options = {}) {
|
|
757
|
+
const status = options.status ?? indicatorStatus(context);
|
|
758
|
+
syncElapsedTicker(context, owner, status);
|
|
759
|
+
const running = isRunning(status);
|
|
760
|
+
const elapsedMs = getToolElapsedMs({ ...options, status, context });
|
|
761
|
+
const elapsed = elapsedMs === void 0 ? "" : formatDuration(
|
|
762
|
+
running ? Math.floor(elapsedMs / 1e3) * 1e3 : elapsedMs
|
|
763
|
+
);
|
|
764
|
+
const frame = options.frame;
|
|
765
|
+
const spinner = frames("spinnerFrames", mode);
|
|
766
|
+
const workingGlyph = frame !== void 0 && Number.isFinite(frame) ? spinner[(Math.floor(frame) % spinner.length + spinner.length) % spinner.length] : workingFrame(elapsedMs, mode);
|
|
767
|
+
const glyph = running ? theme.fg("accent", workingGlyph) : statusGlyph(theme, status, mode);
|
|
768
|
+
const label = options.label ?? indicatorLabel(status);
|
|
769
|
+
const role = STATUS_ROLE[status];
|
|
770
|
+
return {
|
|
771
|
+
glyph,
|
|
772
|
+
elapsedMs,
|
|
773
|
+
elapsed,
|
|
774
|
+
text: running ? ownedToolFooter(theme, { status, elapsedMs }) : [theme.fg(role, label), elapsed ? theme.fg("dim", elapsed) : ""].filter(Boolean).join(theme.fg("dim", ` ${separator2} `))
|
|
775
|
+
};
|
|
776
|
+
},
|
|
777
|
+
icon(name) {
|
|
778
|
+
return name === "spinnerFrames" || name === "workingFrames" ? frames(name, mode) : icon(name, mode);
|
|
779
|
+
},
|
|
780
|
+
statusGlyph: (theme, status) => statusGlyph(theme, status, mode),
|
|
781
|
+
widgetHeading(theme, options, width) {
|
|
782
|
+
const title = theme.fg("toolTitle", options.title);
|
|
783
|
+
return truncateToWidth3(
|
|
784
|
+
[
|
|
785
|
+
options.status ? statusGlyph(theme, options.status, mode) : theme.fg("accent", icon("ready", mode)),
|
|
786
|
+
theme.bold ? theme.bold(title) : title,
|
|
787
|
+
options.counts ? theme.fg(
|
|
788
|
+
"dim",
|
|
789
|
+
`${options.counts.completed}/${options.counts.total}`
|
|
790
|
+
) : "",
|
|
791
|
+
options.suffix ? theme.fg("dim", options.suffix) : ""
|
|
792
|
+
].filter(Boolean).join(" "),
|
|
793
|
+
Math.max(0, Math.floor(width)),
|
|
794
|
+
ellipsis
|
|
795
|
+
);
|
|
796
|
+
},
|
|
797
|
+
treeRow(theme, options, width) {
|
|
798
|
+
const rail = `${" ".repeat(Math.max(0, Math.floor(options.depth ?? 0)))}${options.last ? "\u2514\u2500" : "\u251C\u2500"} `;
|
|
799
|
+
return truncateToWidth3(
|
|
800
|
+
[
|
|
801
|
+
theme.fg(
|
|
802
|
+
options.selected ? "accent" : "dim",
|
|
803
|
+
`${options.selected ? `${icon("selection", mode)} ` : " "}${rail}`
|
|
804
|
+
),
|
|
805
|
+
options.status ? `${statusGlyph(theme, options.status, mode)} ` : "",
|
|
806
|
+
options.text
|
|
807
|
+
].join(""),
|
|
808
|
+
Math.max(0, Math.floor(width)),
|
|
809
|
+
ellipsis
|
|
810
|
+
);
|
|
811
|
+
},
|
|
812
|
+
fg: (theme, role, text) => theme.fg(role, text)
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// src/shared/config.ts
|
|
817
|
+
import { readFileSync } from "node:fs";
|
|
818
|
+
import { homedir } from "node:os";
|
|
819
|
+
import { join } from "node:path";
|
|
820
|
+
var DEFAULT_RESPECT_PACKAGES = [
|
|
821
|
+
"thoth-agents",
|
|
822
|
+
"@thoth-agents/*",
|
|
823
|
+
"thoth-mem"
|
|
824
|
+
];
|
|
825
|
+
function asObject(value) {
|
|
826
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
827
|
+
}
|
|
828
|
+
function moduleEnabled(value) {
|
|
829
|
+
const enabled = asObject(value).enabled;
|
|
830
|
+
return typeof enabled === "boolean" ? enabled : true;
|
|
831
|
+
}
|
|
832
|
+
function parseSubscriptionProviders(value) {
|
|
833
|
+
const obj = asObject(value);
|
|
834
|
+
const providers = obj.subscriptionProviders;
|
|
835
|
+
if (Array.isArray(providers)) {
|
|
836
|
+
return providers.filter(
|
|
837
|
+
(item) => typeof item === "string" && item.trim().length > 0
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
return ["claude-bridge"];
|
|
841
|
+
}
|
|
842
|
+
function parseRespectPackages(value) {
|
|
843
|
+
const packages = asObject(value).respectPackages;
|
|
844
|
+
return Array.isArray(packages) && packages.every(
|
|
845
|
+
(item) => typeof item === "string" && item.trim().length > 0
|
|
846
|
+
) ? packages : [...DEFAULT_RESPECT_PACKAGES];
|
|
847
|
+
}
|
|
848
|
+
function loadConfig(configPath = join(
|
|
849
|
+
process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent"),
|
|
850
|
+
"pi-thoth-theme.json"
|
|
851
|
+
)) {
|
|
852
|
+
let config = {};
|
|
853
|
+
try {
|
|
854
|
+
config = asObject(JSON.parse(readFileSync(configPath, "utf8")));
|
|
855
|
+
} catch {
|
|
856
|
+
}
|
|
857
|
+
return {
|
|
858
|
+
icons: config.icons === "ascii" || config.icons === "unicode" ? config.icons : "nerd",
|
|
859
|
+
...config.inputBox === void 0 ? {} : { inputBox: { enabled: moduleEnabled(config.inputBox) } },
|
|
860
|
+
statusLine: {
|
|
861
|
+
enabled: moduleEnabled(config.statusLine),
|
|
862
|
+
subscriptionProviders: parseSubscriptionProviders(config.statusLine)
|
|
863
|
+
},
|
|
864
|
+
tools: {
|
|
865
|
+
enabled: moduleEnabled(config.tools),
|
|
866
|
+
respectPackages: parseRespectPackages(config.tools)
|
|
867
|
+
},
|
|
868
|
+
welcome: { enabled: moduleEnabled(config.welcome) }
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/input-box/decorate.ts
|
|
873
|
+
import {
|
|
874
|
+
truncateToWidth as truncateToWidth6,
|
|
875
|
+
visibleWidth as visibleWidth5
|
|
876
|
+
} from "@earendil-works/pi-tui";
|
|
877
|
+
|
|
878
|
+
// src/status-line/layout.ts
|
|
879
|
+
import { isAbsolute, relative as relative2, resolve as resolve2, sep } from "node:path";
|
|
880
|
+
import { truncateToWidth as truncateToWidth4, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
|
|
881
|
+
function themeFg(theme, token, text) {
|
|
882
|
+
return theme?.fg ? theme.fg(token, text) : text;
|
|
883
|
+
}
|
|
884
|
+
function formatCwd(cwd, home) {
|
|
885
|
+
if (!home) return cwd;
|
|
886
|
+
const relativeToHome = relative2(resolve2(home), resolve2(cwd));
|
|
887
|
+
if (relativeToHome === ".." || relativeToHome.startsWith(`..${sep}`) || isAbsolute(relativeToHome)) {
|
|
888
|
+
return cwd;
|
|
889
|
+
}
|
|
890
|
+
return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
|
|
891
|
+
}
|
|
892
|
+
function formatTokens(count) {
|
|
893
|
+
if (count < 1e3) return count.toString();
|
|
894
|
+
if (count < 1e6) {
|
|
895
|
+
const k = (count / 1e3).toFixed(1).replace(/\.0$/, "");
|
|
896
|
+
return `${k}K`;
|
|
897
|
+
}
|
|
898
|
+
const m = (count / 1e6).toFixed(1).replace(/\.0$/, "");
|
|
899
|
+
return `${m}M`;
|
|
900
|
+
}
|
|
901
|
+
function thinkingToken(level) {
|
|
902
|
+
const cap = level.charAt(0).toUpperCase() + level.slice(1);
|
|
903
|
+
return `thinking${cap}`;
|
|
904
|
+
}
|
|
905
|
+
function formatBranchSegment(data, { mode = "nerd", theme } = {}) {
|
|
906
|
+
if (!data.gitBranch) return "";
|
|
907
|
+
return `${themeFg(theme, "dim", icon("separator", mode))} ${themeFg(
|
|
908
|
+
theme,
|
|
909
|
+
"success",
|
|
910
|
+
`${icon("branch", mode)} ${data.gitBranch}`
|
|
911
|
+
)}`;
|
|
912
|
+
}
|
|
913
|
+
function formatModelSegments(data, { mode = "nerd", theme } = {}) {
|
|
914
|
+
const pieces = [];
|
|
915
|
+
const model = data.modelName || data.modelId;
|
|
916
|
+
if (model) {
|
|
917
|
+
pieces.push(themeFg(theme, "mdLink", `${icon("model", mode)} ${model}`));
|
|
918
|
+
}
|
|
919
|
+
const level = data.thinkingLevel;
|
|
920
|
+
if (level && level !== "off") {
|
|
921
|
+
const label = level === "minimal" ? "min" : level === "medium" ? "med" : level;
|
|
922
|
+
const effort = themeFg(
|
|
923
|
+
theme,
|
|
924
|
+
thinkingToken(level),
|
|
925
|
+
`${icon("effort", mode)} ${label}`
|
|
926
|
+
);
|
|
927
|
+
pieces.push(
|
|
928
|
+
model ? `${themeFg(theme, "dim", icon("separator", mode))} ${effort}` : effort
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
return pieces;
|
|
932
|
+
}
|
|
933
|
+
function formatCwdSegments(data, { mode = "nerd", theme } = {}) {
|
|
934
|
+
const cwd = data.cwd;
|
|
935
|
+
if (!cwd) return [];
|
|
936
|
+
const folder = icon("folder", mode);
|
|
937
|
+
const variants = [`${folder} ${cwd}`];
|
|
938
|
+
const leaf = cwd.split(/[/\\]/).filter(Boolean).pop();
|
|
939
|
+
const separator2 = cwd.includes("\\") && !cwd.includes("/") ? "\\" : "/";
|
|
940
|
+
const isDriveRoot = leaf !== void 0 && /^[A-Za-z]:$/.test(leaf);
|
|
941
|
+
if (leaf && leaf !== cwd && leaf !== "~" && !isDriveRoot) {
|
|
942
|
+
variants.push(`${folder} ${icon("ellipsis", mode)}${separator2}${leaf}`);
|
|
943
|
+
}
|
|
944
|
+
return variants.map((text) => themeFg(theme, "muted", text));
|
|
945
|
+
}
|
|
946
|
+
function contextBar(percent, isAscii, theme) {
|
|
947
|
+
let token = "success";
|
|
948
|
+
if (percent >= 90) token = "error";
|
|
949
|
+
else if (percent >= 70) token = "warning";
|
|
950
|
+
const filled = Math.min(10, Math.max(0, Math.round(percent / 100 * 10)));
|
|
951
|
+
const fillChar = isAscii ? "#" : "\u2588";
|
|
952
|
+
const emptyChar = isAscii ? "-" : "\u2591";
|
|
953
|
+
const dim = (text) => themeFg(theme, "dim", text);
|
|
954
|
+
return {
|
|
955
|
+
bar: `${dim("[")}${themeFg(theme, token, fillChar.repeat(filled))}${dim(
|
|
956
|
+
emptyChar.repeat(10 - filled)
|
|
957
|
+
)}${dim("]")}`,
|
|
958
|
+
pct: themeFg(theme, token, `${Math.round(percent)}%`)
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
function formatContextSegments(data, { mode = "nerd", theme } = {}) {
|
|
962
|
+
const contextIcon = themeFg(theme, "muted", icon("context", mode));
|
|
963
|
+
const windowText = data.contextWindow ? `/${formatTokens(data.contextWindow)}` : "";
|
|
964
|
+
const hasTokens = data.contextTokens !== null && data.contextTokens !== void 0;
|
|
965
|
+
const hasPercent = data.contextPercent !== null && data.contextPercent !== void 0;
|
|
966
|
+
const tokens = themeFg(
|
|
967
|
+
theme,
|
|
968
|
+
"muted",
|
|
969
|
+
`${hasTokens ? formatTokens(data.contextTokens) : "\u2014"}${windowText}`
|
|
970
|
+
);
|
|
971
|
+
if (!hasPercent) {
|
|
972
|
+
const hasAny = hasTokens || windowText !== "" || data.contextPercent !== void 0 || data.contextTokens !== void 0;
|
|
973
|
+
return hasAny ? [`${contextIcon} ${tokens}`] : [themeFg(theme, "muted", "\u2014")];
|
|
974
|
+
}
|
|
975
|
+
const { bar, pct } = contextBar(
|
|
976
|
+
data.contextPercent,
|
|
977
|
+
mode === "ascii",
|
|
978
|
+
theme
|
|
979
|
+
);
|
|
980
|
+
return [
|
|
981
|
+
`${contextIcon} ${bar} ${pct} ${tokens}`,
|
|
982
|
+
`${contextIcon} ${pct} ${tokens}`,
|
|
983
|
+
`${contextIcon} ${tokens}`
|
|
984
|
+
];
|
|
985
|
+
}
|
|
986
|
+
function formatRate(rate) {
|
|
987
|
+
if (rate === null || !Number.isFinite(rate)) return "\u2014";
|
|
988
|
+
return rate < 10 ? rate.toFixed(1) : Math.round(rate).toString();
|
|
989
|
+
}
|
|
990
|
+
function renderStatusLine(data, options) {
|
|
991
|
+
const { width, mode, theme } = options;
|
|
992
|
+
if (width <= 0) return "";
|
|
993
|
+
const sep2 = ` ${themeFg(theme, "border", icon("separator", mode))} `;
|
|
994
|
+
const segments = [];
|
|
995
|
+
if (data.cost !== void 0) {
|
|
996
|
+
const total = data.cost + (data.subagentCost ?? 0);
|
|
997
|
+
segments.push(
|
|
998
|
+
themeFg(
|
|
999
|
+
theme,
|
|
1000
|
+
"accent",
|
|
1001
|
+
`${icon("cost", mode)}${mode === "ascii" ? "" : " "}${total.toFixed(3)}${data.isSubscription ? " (sub)" : ""}`
|
|
1002
|
+
)
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
const totals = data.tokenTotals;
|
|
1006
|
+
if (totals) {
|
|
1007
|
+
const totalInput = totals.input + totals.cacheRead + totals.cacheWrite;
|
|
1008
|
+
const cacheHitRatio = totalInput === 0 ? "\u2014" : `${Math.round(totals.cacheRead / totalInput * 100)}%`;
|
|
1009
|
+
const up = icon("tokensIn", mode);
|
|
1010
|
+
const down = icon("tokensOut", mode);
|
|
1011
|
+
segments.push(
|
|
1012
|
+
themeFg(
|
|
1013
|
+
theme,
|
|
1014
|
+
"muted",
|
|
1015
|
+
`${up}${formatTokens(totalInput)} ${down}${formatTokens(totals.output)}`
|
|
1016
|
+
),
|
|
1017
|
+
themeFg(theme, "muted", `${icon("cache", mode)} ${cacheHitRatio}`)
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
if (data.tokensPerSecond !== void 0) {
|
|
1021
|
+
segments.push(
|
|
1022
|
+
themeFg(
|
|
1023
|
+
theme,
|
|
1024
|
+
"muted",
|
|
1025
|
+
mode === "ascii" ? `${formatRate(data.tokensPerSecond)} ${icon("throughput", mode)}` : `${icon("throughput", mode)} ${formatRate(data.tokensPerSecond)} tok/s`
|
|
1026
|
+
)
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
for (let count = segments.length; count >= 1; count--) {
|
|
1030
|
+
const line = segments.slice(0, count).join(sep2);
|
|
1031
|
+
if (visibleWidth3(line) <= width) return line;
|
|
1032
|
+
}
|
|
1033
|
+
return segments.length ? truncateToWidth4(segments[0], width, "") : "";
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// src/input-box/frame.ts
|
|
1037
|
+
import { truncateToWidth as truncateToWidth5, visibleWidth as visibleWidth4 } from "@earendil-works/pi-tui";
|
|
1038
|
+
|
|
1039
|
+
// src/input-box/gradient.ts
|
|
1040
|
+
import {
|
|
1041
|
+
foregroundAnsi,
|
|
1042
|
+
getTerminalColorMode,
|
|
1043
|
+
mixColors,
|
|
1044
|
+
parseColor,
|
|
1045
|
+
stripTerminalSequences as stripTerminalSequences2,
|
|
1046
|
+
styleText
|
|
1047
|
+
} from "@earendil-works/pi-tui";
|
|
1048
|
+
var GOLD = parseColor("#D4AF37");
|
|
1049
|
+
var BRIGHT_GOLD = parseColor("#F2C94C");
|
|
1050
|
+
var DIM = parseColor("#736850");
|
|
1051
|
+
var MUTED = parseColor("#A89A78");
|
|
1052
|
+
var BREATHING_CYCLE_MS = 2400;
|
|
1053
|
+
var SHIMMER_LAP_MS = 2400;
|
|
1054
|
+
var SHIMMER_BAND_CELLS = 3;
|
|
1055
|
+
function createBreathingFrame(now, mode = getTerminalColorMode()) {
|
|
1056
|
+
const phase = now % BREATHING_CYCLE_MS / BREATHING_CYCLE_MS * 2 * Math.PI;
|
|
1057
|
+
const intensity = (1 - Math.cos(phase)) / 2;
|
|
1058
|
+
const color2 = intensity <= 0.5 ? mixColors(DIM, GOLD, intensity * 2, "srgb") : mixColors(GOLD, BRIGHT_GOLD, (intensity - 0.5) * 2, "srgb");
|
|
1059
|
+
return { now, foreground: foregroundAnsi(color2, mode), mode };
|
|
1060
|
+
}
|
|
1061
|
+
function colorBreathingBorder(text, frame) {
|
|
1062
|
+
return `${frame.foreground}${text}\x1B[39m`;
|
|
1063
|
+
}
|
|
1064
|
+
function renderWorkingIndicator(native, frame, styleMuted, glyphs = frames("workingFrames", "nerd")) {
|
|
1065
|
+
const plain = stripTerminalSequences2(native);
|
|
1066
|
+
const glyph = glyphs.find(
|
|
1067
|
+
(candidate) => plain.startsWith(candidate) && (plain.length === candidate.length || /^\s/u.test(plain.slice(candidate.length)))
|
|
1068
|
+
);
|
|
1069
|
+
const spacing = glyph === void 0 ? "" : /^\s*/u.exec(plain.slice(glyph.length))?.[0] ?? "";
|
|
1070
|
+
const prefix = glyph === void 0 ? "" : `${frame.foreground}${glyph}\x1B[39m${spacing}`;
|
|
1071
|
+
const letters = Array.from(
|
|
1072
|
+
plain.slice(glyph === void 0 ? 0 : glyph.length + spacing.length)
|
|
1073
|
+
);
|
|
1074
|
+
const head = frame.now % SHIMMER_LAP_MS / SHIMMER_LAP_MS * (letters.length + SHIMMER_BAND_CELLS * 2) - SHIMMER_BAND_CELLS;
|
|
1075
|
+
const label = letters.map((letter, index) => {
|
|
1076
|
+
const intensity = Math.max(
|
|
1077
|
+
0,
|
|
1078
|
+
1 - Math.abs(index - head) / SHIMMER_BAND_CELLS
|
|
1079
|
+
);
|
|
1080
|
+
return intensity === 0 ? styleMuted(letter) : styleText(
|
|
1081
|
+
letter,
|
|
1082
|
+
{
|
|
1083
|
+
fg: mixColors(MUTED, BRIGHT_GOLD, intensity, "srgb"),
|
|
1084
|
+
bold: intensity >= 0.65
|
|
1085
|
+
},
|
|
1086
|
+
frame.mode
|
|
1087
|
+
);
|
|
1088
|
+
}).join("");
|
|
1089
|
+
return prefix + label;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// src/input-box/frame.ts
|
|
1093
|
+
function muted(theme, text) {
|
|
1094
|
+
return theme.fg?.("muted", text) ?? text;
|
|
1095
|
+
}
|
|
1096
|
+
function colorBorder(theme, text, frame) {
|
|
1097
|
+
return frame ? colorBreathingBorder(text, frame) : muted(theme, text);
|
|
1098
|
+
}
|
|
1099
|
+
function scrollLabel(direction, hiddenLineCount, mode = "nerd") {
|
|
1100
|
+
const arrow = icon(direction === "up" ? "scrollUp" : "scrollDown", mode);
|
|
1101
|
+
return `${arrow} ${hiddenLineCount} more`;
|
|
1102
|
+
}
|
|
1103
|
+
function inputLabelWidth(width, hiddenLineCount = 0, mode = "nerd") {
|
|
1104
|
+
const overflowWidth = hiddenLineCount > 0 ? visibleWidth4(scrollLabel("up", hiddenLineCount, mode)) + 3 : 0;
|
|
1105
|
+
return Math.max(0, Math.floor(width) - 6 - overflowWidth);
|
|
1106
|
+
}
|
|
1107
|
+
var LEFT_ONLY_OVERHEAD = 6;
|
|
1108
|
+
var LEFT_RIGHT_OVERHEAD = 9;
|
|
1109
|
+
var RIGHT_ONLY_OVERHEAD = 6;
|
|
1110
|
+
function renderBorder(width, theme, regions, overflow, capLeft, capRight, frame) {
|
|
1111
|
+
width = Math.max(0, Math.floor(width));
|
|
1112
|
+
const border = (text) => colorBorder(theme, text, frame);
|
|
1113
|
+
const pieces = (regions.left ?? []).filter((piece) => visibleWidth4(piece));
|
|
1114
|
+
const rights = (regions.right ?? []).filter((piece) => visibleWidth4(piece));
|
|
1115
|
+
const plain = () => truncateToWidth5(
|
|
1116
|
+
border(`${capLeft}${"\u2500".repeat(Math.max(0, width - 2))}${capRight}`),
|
|
1117
|
+
width,
|
|
1118
|
+
""
|
|
1119
|
+
);
|
|
1120
|
+
if (width <= 5 || !pieces.length && !overflow && !rights.length)
|
|
1121
|
+
return plain();
|
|
1122
|
+
const withOverflow = (text) => !overflow ? text : !text ? muted(theme, overflow) : frame ? `${text}${border(" \u2500 ")}${muted(theme, overflow)}` : `${text}${muted(theme, ` \u2500 ${overflow}`)}`;
|
|
1123
|
+
const leftWith = (count) => withOverflow(pieces.slice(0, count).join(" "));
|
|
1124
|
+
const full = leftWith(pieces.length);
|
|
1125
|
+
const fullWidth = visibleWidth4(full);
|
|
1126
|
+
for (const right of rights) {
|
|
1127
|
+
const rightWidth = visibleWidth4(right);
|
|
1128
|
+
if (!full) {
|
|
1129
|
+
if (rightWidth > width - RIGHT_ONLY_OVERHEAD) continue;
|
|
1130
|
+
const dashes3 = width - 5 - rightWidth;
|
|
1131
|
+
return `${border(`${capLeft}${"\u2500".repeat(dashes3)} `)}${right}${border(` \u2500${capRight}`)}`;
|
|
1132
|
+
}
|
|
1133
|
+
if (fullWidth + rightWidth > width - LEFT_RIGHT_OVERHEAD) continue;
|
|
1134
|
+
const dashes2 = width - 8 - fullWidth - rightWidth;
|
|
1135
|
+
return `${border(`${capLeft}\u2500 `)}${full}${border(` ${"\u2500".repeat(dashes2)} `)}${right}${border(` \u2500${capRight}`)}`;
|
|
1136
|
+
}
|
|
1137
|
+
if (!full) return plain();
|
|
1138
|
+
const labelWidth = width - LEFT_ONLY_OVERHEAD;
|
|
1139
|
+
let label;
|
|
1140
|
+
for (let count = pieces.length; count >= 1; count--) {
|
|
1141
|
+
const candidate = leftWith(count);
|
|
1142
|
+
if (visibleWidth4(candidate) <= labelWidth) {
|
|
1143
|
+
label = candidate;
|
|
1144
|
+
break;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (label === void 0) {
|
|
1148
|
+
if (overflow) {
|
|
1149
|
+
const statusWidth = labelWidth - visibleWidth4(overflow) - 3;
|
|
1150
|
+
const status = statusWidth > 0 && pieces.length ? truncateToWidth5(pieces[0], statusWidth, "") : "";
|
|
1151
|
+
label = withOverflow(visibleWidth4(status) ? status : "");
|
|
1152
|
+
} else {
|
|
1153
|
+
label = truncateToWidth5(pieces[0], labelWidth, "");
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
const fitted = truncateToWidth5(label, labelWidth, "");
|
|
1157
|
+
const dashes = width - visibleWidth4(fitted) - 5;
|
|
1158
|
+
return `${border(`${capLeft}\u2500 `)}${fitted}${border(` ${"\u2500".repeat(dashes)}${capRight}`)}`;
|
|
1159
|
+
}
|
|
1160
|
+
function renderInputTop(width, theme, regions, hiddenLineCount = 0, frame, mode = "nerd") {
|
|
1161
|
+
const overflow = hiddenLineCount > 0 ? scrollLabel("up", hiddenLineCount, mode) : "";
|
|
1162
|
+
return renderBorder(width, theme, regions, overflow, "\u256D", "\u256E", frame);
|
|
1163
|
+
}
|
|
1164
|
+
function renderInputBottom(width, theme, regions, hiddenLineCount = 0, frame, mode = "nerd") {
|
|
1165
|
+
const overflow = hiddenLineCount > 0 ? scrollLabel("down", hiddenLineCount, mode) : "";
|
|
1166
|
+
return renderBorder(width, theme, regions, overflow, "\u2570", "\u256F", frame);
|
|
1167
|
+
}
|
|
1168
|
+
function wrapContentRow(line, width, theme, frame) {
|
|
1169
|
+
width = Math.max(0, Math.floor(width));
|
|
1170
|
+
if (width < 2)
|
|
1171
|
+
return truncateToWidth5(colorBorder(theme, "\u2502\u2502", frame), width, "");
|
|
1172
|
+
const content = truncateToWidth5(line, width - 2, "", true);
|
|
1173
|
+
const side = colorBorder(theme, "\u2502", frame);
|
|
1174
|
+
return `${side}${content}${side}`;
|
|
1175
|
+
}
|
|
1176
|
+
function renderPlaceholder(line, width, theme, text) {
|
|
1177
|
+
if (text !== "") return line;
|
|
1178
|
+
const cursor = "\x1B[7m \x1B[0m";
|
|
1179
|
+
const cursorStart = line.indexOf(cursor);
|
|
1180
|
+
if (cursorStart < 0) return line;
|
|
1181
|
+
const prefix = line.slice(0, cursorStart + cursor.length);
|
|
1182
|
+
const available = Math.max(0, width - visibleWidth4(prefix));
|
|
1183
|
+
const placeholder = truncateToWidth5("type or / for commands", available, "");
|
|
1184
|
+
const padding = " ".repeat(available - visibleWidth4(placeholder));
|
|
1185
|
+
return prefix + (theme.fg?.("dim", placeholder) ?? placeholder) + padding;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// src/input-box/decorate.ts
|
|
1189
|
+
var MIN_INPUT_BOX_WIDTH = 16;
|
|
1190
|
+
var decoratedEditors = /* @__PURE__ */ new WeakSet();
|
|
1191
|
+
var decorations = /* @__PURE__ */ new WeakMap();
|
|
1192
|
+
var methodNames = [
|
|
1193
|
+
"render",
|
|
1194
|
+
"renderTopBorder",
|
|
1195
|
+
"renderBottomBorder",
|
|
1196
|
+
"getText",
|
|
1197
|
+
"getPaddingX",
|
|
1198
|
+
"handleMouse",
|
|
1199
|
+
"borderColor"
|
|
1200
|
+
];
|
|
1201
|
+
function method(editor, name) {
|
|
1202
|
+
for (let owner = editor; owner; owner = Reflect.getPrototypeOf(owner)) {
|
|
1203
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(owner, name);
|
|
1204
|
+
if (descriptor)
|
|
1205
|
+
return typeof descriptor.value === "function" ? descriptor.value : void 0;
|
|
1206
|
+
}
|
|
1207
|
+
return void 0;
|
|
1208
|
+
}
|
|
1209
|
+
function isEditorLike(editor) {
|
|
1210
|
+
if (!editor || typeof editor !== "object") return false;
|
|
1211
|
+
if (decoratedEditors.has(editor)) return true;
|
|
1212
|
+
try {
|
|
1213
|
+
return methodNames.every((name) => method(editor, name) !== void 0);
|
|
1214
|
+
} catch {
|
|
1215
|
+
return false;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
function decorateEditor(editor, deps) {
|
|
1219
|
+
if (!isEditorLike(editor)) return void 0;
|
|
1220
|
+
if (decoratedEditors.has(editor)) return decorations.get(editor);
|
|
1221
|
+
const names = [
|
|
1222
|
+
"render",
|
|
1223
|
+
"renderTopBorder",
|
|
1224
|
+
"renderBottomBorder",
|
|
1225
|
+
"handleMouse",
|
|
1226
|
+
"borderColor"
|
|
1227
|
+
];
|
|
1228
|
+
if (!Reflect.isExtensible(editor) || names.some(
|
|
1229
|
+
(name) => Reflect.getOwnPropertyDescriptor(editor, name)?.configurable === false
|
|
1230
|
+
))
|
|
1231
|
+
return void 0;
|
|
1232
|
+
const originals = Object.fromEntries(
|
|
1233
|
+
methodNames.map((name) => [name, method(editor, name)])
|
|
1234
|
+
);
|
|
1235
|
+
const descriptors = new Map(
|
|
1236
|
+
names.map((name) => [name, Reflect.getOwnPropertyDescriptor(editor, name)])
|
|
1237
|
+
);
|
|
1238
|
+
let active = true;
|
|
1239
|
+
let outerWidth;
|
|
1240
|
+
let fallback = false;
|
|
1241
|
+
let top;
|
|
1242
|
+
let bottom;
|
|
1243
|
+
let renderFrame;
|
|
1244
|
+
let boxRendered = false;
|
|
1245
|
+
const muted2 = (text) => deps.theme.fg?.("muted", text) ?? text;
|
|
1246
|
+
function nativeRender(receiver, width) {
|
|
1247
|
+
fallback = true;
|
|
1248
|
+
try {
|
|
1249
|
+
return originals.render.call(receiver, width);
|
|
1250
|
+
} finally {
|
|
1251
|
+
fallback = false;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
const patches = {
|
|
1255
|
+
render: {
|
|
1256
|
+
configurable: true,
|
|
1257
|
+
writable: true,
|
|
1258
|
+
value(width) {
|
|
1259
|
+
boxRendered = false;
|
|
1260
|
+
if (!active || width < MIN_INPUT_BOX_WIDTH)
|
|
1261
|
+
return nativeRender(this, width);
|
|
1262
|
+
const boxWidth = Math.floor(width);
|
|
1263
|
+
const innerWidth = boxWidth - 2;
|
|
1264
|
+
const maxPadding = Math.max(0, Math.floor((innerWidth - 1) / 2));
|
|
1265
|
+
const paddingX = Math.min(originals.getPaddingX.call(this), maxPadding);
|
|
1266
|
+
const contentWidth = Math.max(1, innerWidth - paddingX * 2);
|
|
1267
|
+
const layoutWidth = Math.max(1, contentWidth - (paddingX ? 0 : 1));
|
|
1268
|
+
if (layoutWidth < 2) return nativeRender(this, width);
|
|
1269
|
+
const frame = deps.working.isWorking ? createBreathingFrame(Date.now()) : void 0;
|
|
1270
|
+
renderFrame = frame;
|
|
1271
|
+
outerWidth = boxWidth;
|
|
1272
|
+
top = void 0;
|
|
1273
|
+
bottom = void 0;
|
|
1274
|
+
let lines;
|
|
1275
|
+
try {
|
|
1276
|
+
lines = originals.render.call(this, outerWidth - 2);
|
|
1277
|
+
} finally {
|
|
1278
|
+
outerWidth = void 0;
|
|
1279
|
+
renderFrame = void 0;
|
|
1280
|
+
}
|
|
1281
|
+
const bottomIndex = bottom === void 0 ? -1 : lines.indexOf(bottom, 1);
|
|
1282
|
+
if (top === void 0 || lines[0] !== top || bottomIndex < 2)
|
|
1283
|
+
return nativeRender(this, width);
|
|
1284
|
+
const text = originals.getText.call(this);
|
|
1285
|
+
lines = lines.map((line, index) => {
|
|
1286
|
+
if (index === 0 || index === bottomIndex) return line;
|
|
1287
|
+
if (index > bottomIndex)
|
|
1288
|
+
return ` ${truncateToWidth6(line, width - 2, "", true)} `;
|
|
1289
|
+
return wrapContentRow(
|
|
1290
|
+
renderPlaceholder(line, width - 2, deps.theme, text),
|
|
1291
|
+
width,
|
|
1292
|
+
deps.theme,
|
|
1293
|
+
frame
|
|
1294
|
+
);
|
|
1295
|
+
});
|
|
1296
|
+
boxRendered = true;
|
|
1297
|
+
return lines;
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
renderTopBorder: {
|
|
1301
|
+
configurable: true,
|
|
1302
|
+
writable: true,
|
|
1303
|
+
value(width, hidden) {
|
|
1304
|
+
if (outerWidth === void 0 || hidden > 0 && visibleWidth5(scrollLabel("up", hidden, deps.iconMode)) > outerWidth - 6)
|
|
1305
|
+
return originals.renderTopBorder.call(this, width, hidden);
|
|
1306
|
+
const status = deps.working.status(
|
|
1307
|
+
this.workingStatusIndicator,
|
|
1308
|
+
inputLabelWidth(outerWidth, hidden, deps.iconMode),
|
|
1309
|
+
muted2,
|
|
1310
|
+
renderFrame,
|
|
1311
|
+
deps.iconMode
|
|
1312
|
+
);
|
|
1313
|
+
const data = active ? deps.getStatusSnapshot?.() : void 0;
|
|
1314
|
+
const options = { mode: deps.iconMode, theme: deps.theme };
|
|
1315
|
+
top = renderInputTop(
|
|
1316
|
+
outerWidth,
|
|
1317
|
+
deps.theme,
|
|
1318
|
+
{
|
|
1319
|
+
left: [status, data ? formatBranchSegment(data, options) : ""],
|
|
1320
|
+
right: data ? formatCwdSegments(data, options) : []
|
|
1321
|
+
},
|
|
1322
|
+
hidden,
|
|
1323
|
+
renderFrame,
|
|
1324
|
+
deps.iconMode
|
|
1325
|
+
);
|
|
1326
|
+
return top;
|
|
1327
|
+
}
|
|
1328
|
+
},
|
|
1329
|
+
renderBottomBorder: {
|
|
1330
|
+
configurable: true,
|
|
1331
|
+
writable: true,
|
|
1332
|
+
value(width, hidden) {
|
|
1333
|
+
if (outerWidth === void 0 || hidden > 0 && visibleWidth5(scrollLabel("down", hidden, deps.iconMode)) > outerWidth - 6)
|
|
1334
|
+
return originals.renderBottomBorder.call(this, width, hidden);
|
|
1335
|
+
const data = active ? deps.getStatusSnapshot?.() : void 0;
|
|
1336
|
+
const options = { mode: deps.iconMode, theme: deps.theme };
|
|
1337
|
+
bottom = renderInputBottom(
|
|
1338
|
+
outerWidth,
|
|
1339
|
+
deps.theme,
|
|
1340
|
+
{
|
|
1341
|
+
left: data ? formatModelSegments(data, options) : [],
|
|
1342
|
+
right: data ? formatContextSegments(data, options) : []
|
|
1343
|
+
},
|
|
1344
|
+
hidden,
|
|
1345
|
+
renderFrame,
|
|
1346
|
+
deps.iconMode
|
|
1347
|
+
);
|
|
1348
|
+
return bottom;
|
|
1349
|
+
}
|
|
1350
|
+
},
|
|
1351
|
+
handleMouse: {
|
|
1352
|
+
configurable: true,
|
|
1353
|
+
writable: true,
|
|
1354
|
+
value(event) {
|
|
1355
|
+
const translated = active && boxRendered ? { ...event, x: event.x - 1, width: event.width - 2 } : event;
|
|
1356
|
+
return originals.handleMouse.call(this, translated);
|
|
1357
|
+
}
|
|
1358
|
+
},
|
|
1359
|
+
borderColor: {
|
|
1360
|
+
configurable: true,
|
|
1361
|
+
get: () => fallback ? originals.borderColor : muted2,
|
|
1362
|
+
set: () => {
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
Object.defineProperties(editor, patches);
|
|
1367
|
+
const decoration = {
|
|
1368
|
+
getStatusSnapshot: () => active ? deps.getStatusSnapshot?.() : void 0,
|
|
1369
|
+
dispose() {
|
|
1370
|
+
if (!active) return;
|
|
1371
|
+
active = false;
|
|
1372
|
+
for (const name of names) {
|
|
1373
|
+
const current = Reflect.getOwnPropertyDescriptor(editor, name);
|
|
1374
|
+
const patch = patches[name];
|
|
1375
|
+
if (current?.value !== patch.value || current?.get !== patch.get || current?.set !== patch.set)
|
|
1376
|
+
continue;
|
|
1377
|
+
const descriptor = descriptors.get(name);
|
|
1378
|
+
if (descriptor) Object.defineProperty(editor, name, descriptor);
|
|
1379
|
+
else Reflect.deleteProperty(editor, name);
|
|
1380
|
+
}
|
|
1381
|
+
decoratedEditors.delete(editor);
|
|
1382
|
+
decorations.delete(editor);
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
decoratedEditors.add(editor);
|
|
1386
|
+
decorations.set(editor, decoration);
|
|
1387
|
+
return decoration;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// src/input-box/state.ts
|
|
1391
|
+
import { truncateToWidth as truncateToWidth7, visibleWidth as visibleWidth6 } from "@earendil-works/pi-tui";
|
|
1392
|
+
function createWorkingState(requestRender) {
|
|
1393
|
+
let startedAt;
|
|
1394
|
+
let ticker;
|
|
1395
|
+
let disposed = false;
|
|
1396
|
+
function stop() {
|
|
1397
|
+
if (ticker !== void 0) clearInterval(ticker);
|
|
1398
|
+
ticker = void 0;
|
|
1399
|
+
startedAt = void 0;
|
|
1400
|
+
}
|
|
1401
|
+
return {
|
|
1402
|
+
get isWorking() {
|
|
1403
|
+
return startedAt !== void 0;
|
|
1404
|
+
},
|
|
1405
|
+
start() {
|
|
1406
|
+
if (disposed) return;
|
|
1407
|
+
stop();
|
|
1408
|
+
startedAt = Date.now();
|
|
1409
|
+
ticker = setInterval(requestRender, 50);
|
|
1410
|
+
ticker.unref?.();
|
|
1411
|
+
requestRender();
|
|
1412
|
+
},
|
|
1413
|
+
end() {
|
|
1414
|
+
if (disposed) return;
|
|
1415
|
+
stop();
|
|
1416
|
+
requestRender();
|
|
1417
|
+
},
|
|
1418
|
+
status(indicator, width, styleMuted = (text) => text, frame, mode = "nerd") {
|
|
1419
|
+
const ready = `${icon("ready", mode)} ready`;
|
|
1420
|
+
const separator2 = icon("separator", mode);
|
|
1421
|
+
if (!indicator || typeof indicator !== "object" || !("renderInBorder" in indicator) || typeof indicator.renderInBorder !== "function") {
|
|
1422
|
+
return styleMuted(truncateToWidth7(ready, width, ""));
|
|
1423
|
+
}
|
|
1424
|
+
const working = "kind" in indicator && indicator.kind === "working";
|
|
1425
|
+
const now = frame?.now ?? Date.now();
|
|
1426
|
+
const elapsed = working && startedAt !== void 0 ? ` ${separator2} ${formatDuration(Math.floor((now - startedAt) / 1e3) * 1e3)}` : "";
|
|
1427
|
+
const nativeWidth = Math.max(1, width - visibleWidth6(elapsed));
|
|
1428
|
+
const native = indicator.renderInBorder(nativeWidth);
|
|
1429
|
+
if (typeof native !== "string" || visibleWidth6(native) === 0) {
|
|
1430
|
+
return styleMuted(truncateToWidth7(ready, width, ""));
|
|
1431
|
+
}
|
|
1432
|
+
const fitted = truncateToWidth7(native, nativeWidth, "");
|
|
1433
|
+
const label = working && startedAt !== void 0 ? renderWorkingIndicator(
|
|
1434
|
+
fitted,
|
|
1435
|
+
frame ?? createBreathingFrame(now),
|
|
1436
|
+
styleMuted,
|
|
1437
|
+
frames("workingFrames", mode)
|
|
1438
|
+
) : fitted;
|
|
1439
|
+
return truncateToWidth7(
|
|
1440
|
+
`${label}${elapsed ? styleMuted(elapsed) : ""}`,
|
|
1441
|
+
width,
|
|
1442
|
+
""
|
|
1443
|
+
);
|
|
1444
|
+
},
|
|
1445
|
+
dispose() {
|
|
1446
|
+
disposed = true;
|
|
1447
|
+
stop();
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// src/status-line/cost.ts
|
|
1453
|
+
function calculateSessionCost(source) {
|
|
1454
|
+
if (!source) return 0;
|
|
1455
|
+
const entries = Array.isArray(source) ? source : typeof source.getEntries === "function" ? source.getEntries() : [];
|
|
1456
|
+
let totalCost = 0;
|
|
1457
|
+
for (const entry of entries) {
|
|
1458
|
+
if (!entry) continue;
|
|
1459
|
+
if (entry.type === "message") {
|
|
1460
|
+
const msg = entry.message;
|
|
1461
|
+
if ((msg?.role === "assistant" || msg?.role === "toolResult") && "usage" in msg && msg.usage?.cost?.total) {
|
|
1462
|
+
totalCost += msg.usage.cost.total;
|
|
1463
|
+
}
|
|
1464
|
+
} else if ((entry.type === "branch_summary" || entry.type === "compaction" || entry.type === "usage") && "usage" in entry && entry.usage?.cost?.total) {
|
|
1465
|
+
totalCost += entry.usage.cost.total;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return totalCost;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// src/status-line/throughput.ts
|
|
1472
|
+
function createThroughputTracker() {
|
|
1473
|
+
let pending;
|
|
1474
|
+
let outputTokens = 0;
|
|
1475
|
+
let generationMs = 0;
|
|
1476
|
+
const startedIds = /* @__PURE__ */ new Set();
|
|
1477
|
+
const endedIds = /* @__PURE__ */ new Set();
|
|
1478
|
+
let startedMessages = /* @__PURE__ */ new WeakSet();
|
|
1479
|
+
let endedMessages = /* @__PURE__ */ new WeakSet();
|
|
1480
|
+
return {
|
|
1481
|
+
observe(event, observedAt = Date.now()) {
|
|
1482
|
+
if (event.message.role !== "assistant") return;
|
|
1483
|
+
const id = event.message.id;
|
|
1484
|
+
if (event.type === "message_start") {
|
|
1485
|
+
if (startedMessages.has(event.message) || id && startedIds.has(id))
|
|
1486
|
+
return;
|
|
1487
|
+
startedMessages.add(event.message);
|
|
1488
|
+
if (id) startedIds.add(id);
|
|
1489
|
+
pending = { at: observedAt, id };
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
if (endedMessages.has(event.message) || id && endedIds.has(id)) return;
|
|
1493
|
+
const start = pending;
|
|
1494
|
+
if (!start || start.id && id && start.id !== id) return;
|
|
1495
|
+
pending = void 0;
|
|
1496
|
+
endedMessages.add(event.message);
|
|
1497
|
+
if (id) endedIds.add(id);
|
|
1498
|
+
const elapsed = observedAt - start.at;
|
|
1499
|
+
if (!Number.isFinite(elapsed) || elapsed <= 0) return;
|
|
1500
|
+
const output = event.message.usage?.output;
|
|
1501
|
+
if (typeof output !== "number" || !Number.isFinite(output) || output < 0)
|
|
1502
|
+
return;
|
|
1503
|
+
outputTokens += output;
|
|
1504
|
+
generationMs += elapsed;
|
|
1505
|
+
},
|
|
1506
|
+
reset() {
|
|
1507
|
+
pending = void 0;
|
|
1508
|
+
outputTokens = 0;
|
|
1509
|
+
generationMs = 0;
|
|
1510
|
+
startedIds.clear();
|
|
1511
|
+
endedIds.clear();
|
|
1512
|
+
startedMessages = /* @__PURE__ */ new WeakSet();
|
|
1513
|
+
endedMessages = /* @__PURE__ */ new WeakSet();
|
|
1514
|
+
},
|
|
1515
|
+
get tokensPerSecond() {
|
|
1516
|
+
if (!Number.isFinite(generationMs) || generationMs <= 0) return null;
|
|
1517
|
+
const speed = outputTokens * 1e3 / generationMs;
|
|
1518
|
+
return Number.isFinite(speed) ? speed : null;
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// src/status-line/tokens.ts
|
|
1524
|
+
function calculateSessionTokens(source) {
|
|
1525
|
+
const entries = Array.isArray(source) ? source : source && typeof source.getEntries === "function" ? source.getEntries() : [];
|
|
1526
|
+
const totals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
1527
|
+
for (const entry of entries) {
|
|
1528
|
+
if (!entry) continue;
|
|
1529
|
+
const usage = entry.type === "message" ? (entry.message?.role === "assistant" || entry.message?.role === "toolResult") && "usage" in entry.message ? entry.message.usage : void 0 : (entry.type === "branch_summary" || entry.type === "compaction" || entry.type === "usage") && "usage" in entry ? entry.usage : void 0;
|
|
1530
|
+
if (!usage) continue;
|
|
1531
|
+
for (const field of [
|
|
1532
|
+
"input",
|
|
1533
|
+
"output",
|
|
1534
|
+
"cacheRead",
|
|
1535
|
+
"cacheWrite"
|
|
1536
|
+
]) {
|
|
1537
|
+
const value = usage[field];
|
|
1538
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
1539
|
+
totals[field] += value;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
return totals;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// src/status-line/index.ts
|
|
1547
|
+
function registerStatusLine(pi, ctx, config) {
|
|
1548
|
+
const subscriptionProviders = config.statusLine.subscriptionProviders ?? [
|
|
1549
|
+
"claude-bridge"
|
|
1550
|
+
];
|
|
1551
|
+
const inputBoxEnabled = config.inputBox?.enabled !== false;
|
|
1552
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
1553
|
+
const unsubs = [];
|
|
1554
|
+
let disposed = false;
|
|
1555
|
+
const throughput = createThroughputTracker();
|
|
1556
|
+
let sessionLeafId = ctx.sessionManager.getLeafId();
|
|
1557
|
+
let session = readSession();
|
|
1558
|
+
let sessionDirty = false;
|
|
1559
|
+
let subagentCost = 0;
|
|
1560
|
+
let cachedKey = "";
|
|
1561
|
+
let cachedLines = [];
|
|
1562
|
+
let snapshotKey = "";
|
|
1563
|
+
let snapshot;
|
|
1564
|
+
const getStatusSnapshot = () => {
|
|
1565
|
+
if (!disposed) {
|
|
1566
|
+
const leafId = ctx.sessionManager.getLeafId();
|
|
1567
|
+
if (sessionDirty || leafId !== sessionLeafId) {
|
|
1568
|
+
session = readSession();
|
|
1569
|
+
sessionLeafId = leafId;
|
|
1570
|
+
sessionDirty = false;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
const data = {
|
|
1574
|
+
modelName: ctx.model?.name,
|
|
1575
|
+
modelId: ctx.model?.id,
|
|
1576
|
+
thinkingLevel: ctx.thinkingLevel,
|
|
1577
|
+
gitBranch: footerData?.getGitBranch?.() ?? null,
|
|
1578
|
+
cwd: formatCwd(ctx.cwd, process.env.HOME || process.env.USERPROFILE),
|
|
1579
|
+
...session,
|
|
1580
|
+
subagentCost,
|
|
1581
|
+
tokensPerSecond: throughput.tokensPerSecond
|
|
1582
|
+
};
|
|
1583
|
+
const key = JSON.stringify(data);
|
|
1584
|
+
if (key !== snapshotKey) {
|
|
1585
|
+
snapshotKey = key;
|
|
1586
|
+
snapshot = data;
|
|
1587
|
+
}
|
|
1588
|
+
return snapshot;
|
|
1589
|
+
};
|
|
1590
|
+
function readSession() {
|
|
1591
|
+
const usage = ctx.getContextUsage?.();
|
|
1592
|
+
const provider = ctx.model?.provider;
|
|
1593
|
+
const entries = ctx.sessionManager.getEntries();
|
|
1594
|
+
return {
|
|
1595
|
+
cost: calculateSessionCost(entries),
|
|
1596
|
+
tokenTotals: calculateSessionTokens(entries),
|
|
1597
|
+
isSubscription: provider !== void 0 && subscriptionProviders.includes(provider),
|
|
1598
|
+
contextTokens: usage?.tokens ?? null,
|
|
1599
|
+
contextPercent: usage?.percent ?? null,
|
|
1600
|
+
contextWindow: usage?.contextWindow
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
const requestRender = () => {
|
|
1604
|
+
if (!disposed) tui.requestRender?.();
|
|
1605
|
+
};
|
|
1606
|
+
const invalidateRender = () => {
|
|
1607
|
+
if (disposed) return;
|
|
1608
|
+
cachedKey = "";
|
|
1609
|
+
requestRender();
|
|
1610
|
+
};
|
|
1611
|
+
const working = createWorkingState(invalidateRender);
|
|
1612
|
+
const editorDecorations = /* @__PURE__ */ new Set();
|
|
1613
|
+
const refreshSession = () => {
|
|
1614
|
+
if (disposed) return;
|
|
1615
|
+
sessionDirty = true;
|
|
1616
|
+
invalidateRender();
|
|
1617
|
+
};
|
|
1618
|
+
unsubs.push(
|
|
1619
|
+
pi.events.on("thoth:subagent-usage", (data) => {
|
|
1620
|
+
if (disposed || !data || typeof data !== "object") return;
|
|
1621
|
+
const { parentSessionId, totalCost } = data;
|
|
1622
|
+
if (parentSessionId !== ctx.sessionManager.getSessionId() || typeof totalCost !== "number" || !Number.isFinite(totalCost) || totalCost < 0)
|
|
1623
|
+
return;
|
|
1624
|
+
subagentCost = totalCost;
|
|
1625
|
+
requestRender();
|
|
1626
|
+
})
|
|
1627
|
+
);
|
|
1628
|
+
if (footerData?.onBranchChange) {
|
|
1629
|
+
unsubs.push(footerData.onBranchChange(invalidateRender));
|
|
1630
|
+
}
|
|
1631
|
+
const requestSubagentUsage = () => {
|
|
1632
|
+
pi.events.emit("thoth:subagent-usage:request", {
|
|
1633
|
+
parentSessionId: ctx.sessionManager.getSessionId()
|
|
1634
|
+
});
|
|
1635
|
+
};
|
|
1636
|
+
if (pi?.on) {
|
|
1637
|
+
unsubs.push(
|
|
1638
|
+
pi.on("session_start", () => {
|
|
1639
|
+
if (disposed) return;
|
|
1640
|
+
subagentCost = 0;
|
|
1641
|
+
throughput.reset();
|
|
1642
|
+
if (inputBoxEnabled) working.end();
|
|
1643
|
+
refreshSession();
|
|
1644
|
+
requestSubagentUsage();
|
|
1645
|
+
})
|
|
1646
|
+
);
|
|
1647
|
+
if (inputBoxEnabled) {
|
|
1648
|
+
unsubs.push(pi.on("agent_start", working.start));
|
|
1649
|
+
unsubs.push(pi.on("agent_end", working.end));
|
|
1650
|
+
}
|
|
1651
|
+
unsubs.push(
|
|
1652
|
+
pi.on("message_start", (event) => {
|
|
1653
|
+
if (!disposed && event) throughput.observe(event);
|
|
1654
|
+
})
|
|
1655
|
+
);
|
|
1656
|
+
unsubs.push(
|
|
1657
|
+
pi.on("message_end", (event) => {
|
|
1658
|
+
if (disposed) return;
|
|
1659
|
+
if (event) throughput.observe(event);
|
|
1660
|
+
refreshSession();
|
|
1661
|
+
})
|
|
1662
|
+
);
|
|
1663
|
+
unsubs.push(pi.on("turn_end", refreshSession));
|
|
1664
|
+
unsubs.push(pi.on("agent_end", refreshSession));
|
|
1665
|
+
unsubs.push(pi.on("session_compact", refreshSession));
|
|
1666
|
+
unsubs.push(pi.on("model_select", refreshSession));
|
|
1667
|
+
unsubs.push(pi.on("thinking_level_select", invalidateRender));
|
|
1668
|
+
}
|
|
1669
|
+
requestSubagentUsage();
|
|
1670
|
+
return {
|
|
1671
|
+
getStatusSnapshot,
|
|
1672
|
+
render(width) {
|
|
1673
|
+
if (!disposed && inputBoxEnabled) {
|
|
1674
|
+
const focusedTui = tui;
|
|
1675
|
+
const editor = focusedTui.getFocusedComponent?.();
|
|
1676
|
+
const decoration = decorateEditor(editor, {
|
|
1677
|
+
theme,
|
|
1678
|
+
working,
|
|
1679
|
+
getStatusSnapshot,
|
|
1680
|
+
get iconMode() {
|
|
1681
|
+
return config.icons;
|
|
1682
|
+
}
|
|
1683
|
+
});
|
|
1684
|
+
if (decoration && !editorDecorations.has(decoration)) {
|
|
1685
|
+
editorDecorations.add(decoration);
|
|
1686
|
+
requestRender();
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (width <= 0) return [];
|
|
1690
|
+
const data = getStatusSnapshot();
|
|
1691
|
+
const key = `${width}\0${JSON.stringify(data)}`;
|
|
1692
|
+
if (key === cachedKey) return cachedLines;
|
|
1693
|
+
const line = renderStatusLine(data, {
|
|
1694
|
+
width,
|
|
1695
|
+
mode: config.icons,
|
|
1696
|
+
theme
|
|
1697
|
+
});
|
|
1698
|
+
cachedKey = key;
|
|
1699
|
+
if (line !== cachedLines[0]) cachedLines = line ? [line] : [];
|
|
1700
|
+
return cachedLines;
|
|
1701
|
+
},
|
|
1702
|
+
invalidate: invalidateRender,
|
|
1703
|
+
dispose() {
|
|
1704
|
+
disposed = true;
|
|
1705
|
+
working.dispose();
|
|
1706
|
+
for (const decoration of editorDecorations) decoration.dispose();
|
|
1707
|
+
editorDecorations.clear();
|
|
1708
|
+
for (const unsub of unsubs) {
|
|
1709
|
+
try {
|
|
1710
|
+
unsub();
|
|
1711
|
+
} catch {
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
unsubs.length = 0;
|
|
1715
|
+
}
|
|
1716
|
+
};
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// src/tools/file-icons.ts
|
|
1721
|
+
import { basename, extname } from "node:path";
|
|
1722
|
+
var NERD_FILE_ICONS = {
|
|
1723
|
+
ts: "\uE628",
|
|
1724
|
+
tsx: "\uE7BA",
|
|
1725
|
+
js: "\uE74E",
|
|
1726
|
+
jsx: "\uE7BA",
|
|
1727
|
+
mjs: "\uE74E",
|
|
1728
|
+
cjs: "\uE74E",
|
|
1729
|
+
py: "\uE73C",
|
|
1730
|
+
rs: "\uE7A8",
|
|
1731
|
+
go: "\uE724",
|
|
1732
|
+
java: "\uE738",
|
|
1733
|
+
c: "\uE61E",
|
|
1734
|
+
cpp: "\uE61D",
|
|
1735
|
+
h: "\uE61E",
|
|
1736
|
+
hpp: "\uE61D",
|
|
1737
|
+
cs: "\uE648",
|
|
1738
|
+
html: "\uE736",
|
|
1739
|
+
htm: "\uE736",
|
|
1740
|
+
css: "\uE749",
|
|
1741
|
+
scss: "\uE749",
|
|
1742
|
+
sass: "\uE749",
|
|
1743
|
+
less: "\uE749",
|
|
1744
|
+
vue: "\uE6A0",
|
|
1745
|
+
svelte: "\uE697",
|
|
1746
|
+
json: "\uE60B",
|
|
1747
|
+
yaml: "\uE6A8",
|
|
1748
|
+
yml: "\uE6A8",
|
|
1749
|
+
toml: "\uE6B2",
|
|
1750
|
+
xml: "\uE619",
|
|
1751
|
+
md: "\uE73E",
|
|
1752
|
+
mdx: "\uE73E",
|
|
1753
|
+
sql: "\uE706",
|
|
1754
|
+
sh: "\uE795",
|
|
1755
|
+
bash: "\uE795",
|
|
1756
|
+
zsh: "\uE795",
|
|
1757
|
+
ps1: "\uE70F",
|
|
1758
|
+
psm1: "\uE70F",
|
|
1759
|
+
lua: "\uE620",
|
|
1760
|
+
php: "\uE73D",
|
|
1761
|
+
dart: "\uE798",
|
|
1762
|
+
png: "\uF1C5",
|
|
1763
|
+
jpg: "\uF1C5",
|
|
1764
|
+
jpeg: "\uF1C5",
|
|
1765
|
+
svg: "\uF1C5",
|
|
1766
|
+
webp: "\uF1C5",
|
|
1767
|
+
gif: "\uF1C5",
|
|
1768
|
+
lock: "\uF023",
|
|
1769
|
+
env: "\uE615",
|
|
1770
|
+
dockerfile: "\uE7B0"
|
|
1771
|
+
};
|
|
1772
|
+
var NERD_NAME_ICONS = {
|
|
1773
|
+
"package.json": "\uE71E",
|
|
1774
|
+
"package-lock.json": "\uE71E",
|
|
1775
|
+
"tsconfig.json": "\uE628",
|
|
1776
|
+
".gitignore": "\uE702",
|
|
1777
|
+
".gitattributes": "\uE702",
|
|
1778
|
+
".env": "\uE615",
|
|
1779
|
+
dockerfile: "\uE7B0",
|
|
1780
|
+
makefile: "\uE615",
|
|
1781
|
+
"readme.md": "\uE73E",
|
|
1782
|
+
license: "\uE60A"
|
|
1783
|
+
};
|
|
1784
|
+
var ASCII_FILE_ICONS = {
|
|
1785
|
+
ts: "[ts]",
|
|
1786
|
+
tsx: "[ts]",
|
|
1787
|
+
js: "[js]",
|
|
1788
|
+
jsx: "[js]",
|
|
1789
|
+
mjs: "[js]",
|
|
1790
|
+
cjs: "[js]",
|
|
1791
|
+
py: "[py]",
|
|
1792
|
+
rs: "[rs]",
|
|
1793
|
+
go: "[go]",
|
|
1794
|
+
json: "{ }",
|
|
1795
|
+
yaml: "{ }",
|
|
1796
|
+
yml: "{ }",
|
|
1797
|
+
toml: "{ }",
|
|
1798
|
+
xml: "< >",
|
|
1799
|
+
md: "[md]",
|
|
1800
|
+
mdx: "[md]",
|
|
1801
|
+
html: "< >",
|
|
1802
|
+
htm: "< >",
|
|
1803
|
+
css: "{ }",
|
|
1804
|
+
sh: "$",
|
|
1805
|
+
bash: "$",
|
|
1806
|
+
zsh: "$",
|
|
1807
|
+
ps1: "PS",
|
|
1808
|
+
psm1: "PS",
|
|
1809
|
+
png: "[img]",
|
|
1810
|
+
jpg: "[img]",
|
|
1811
|
+
jpeg: "[img]",
|
|
1812
|
+
svg: "[img]",
|
|
1813
|
+
gif: "[img]",
|
|
1814
|
+
webp: "[img]",
|
|
1815
|
+
lock: "[lock]",
|
|
1816
|
+
env: "[env]"
|
|
1817
|
+
};
|
|
1818
|
+
function getFileIcon(filePath, mode) {
|
|
1819
|
+
if (filePath.endsWith("/") || filePath.endsWith("\\")) {
|
|
1820
|
+
return icon("folder", mode);
|
|
1821
|
+
}
|
|
1822
|
+
const name = basename(filePath).toLowerCase();
|
|
1823
|
+
const ext = extname(name).replace(/^\./, "").toLowerCase();
|
|
1824
|
+
if (mode === "ascii") {
|
|
1825
|
+
if (ASCII_FILE_ICONS[ext]) return ASCII_FILE_ICONS[ext];
|
|
1826
|
+
return icon("file", mode);
|
|
1827
|
+
}
|
|
1828
|
+
if (NERD_NAME_ICONS[name]) return NERD_NAME_ICONS[name];
|
|
1829
|
+
if (NERD_FILE_ICONS[ext]) return NERD_FILE_ICONS[ext];
|
|
1830
|
+
return icon("file", mode);
|
|
1831
|
+
}
|
|
1832
|
+
function getDirIcon(mode) {
|
|
1833
|
+
return icon("folder", mode);
|
|
1834
|
+
}
|
|
1835
|
+
function getToolIcon(tool, mode) {
|
|
1836
|
+
return icon(tool, mode);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// src/tools/bash.ts
|
|
1840
|
+
var COLLAPSED_OUTPUT_LINES = 5;
|
|
1841
|
+
var BASH_CONFIG = {
|
|
1842
|
+
toolName: "bash",
|
|
1843
|
+
title: "Bash",
|
|
1844
|
+
prompt: "$ "
|
|
1845
|
+
};
|
|
1846
|
+
var POWERSHELL_CONFIG = {
|
|
1847
|
+
toolName: "powershell",
|
|
1848
|
+
title: "PowerShell",
|
|
1849
|
+
prompt: "PS> "
|
|
1850
|
+
};
|
|
1851
|
+
function extractShellExitCode(result, textOutput, isErr) {
|
|
1852
|
+
const structured = result.structuredContent;
|
|
1853
|
+
if (typeof structured?.exit_code === "number") {
|
|
1854
|
+
return structured.exit_code;
|
|
1855
|
+
}
|
|
1856
|
+
if (!isErr) return 0;
|
|
1857
|
+
const lastLine = textOutput.trimEnd().split("\n").at(-1) ?? "";
|
|
1858
|
+
const match = /^Command exited with code (\d+)$/.exec(lastLine);
|
|
1859
|
+
return match ? Number.parseInt(match[1], 10) : 1;
|
|
1860
|
+
}
|
|
1861
|
+
function createCustomShellTool(shellConfig, config) {
|
|
1862
|
+
return {
|
|
1863
|
+
renderShell: "self",
|
|
1864
|
+
renderCall(rawArgs, theme, context) {
|
|
1865
|
+
syncElapsedTicker(context);
|
|
1866
|
+
const args = rawArgs ?? {};
|
|
1867
|
+
const command = String(args.command ?? "").trim();
|
|
1868
|
+
const icon2 = getToolIcon(shellConfig.toolName, config.icons);
|
|
1869
|
+
const borderTone = context?.isPartial ? "accent" : getToolBorderTone(context);
|
|
1870
|
+
const comp = createComponent((width) => {
|
|
1871
|
+
const safeWidth = Math.max(0, width);
|
|
1872
|
+
const cmdLines = command ? command.split("\n") : [""];
|
|
1873
|
+
const footer = context?.executionStarted && context.isPartial ? toolFooter(theme, { status: "running", context }, config.icons) : void 0;
|
|
1874
|
+
const title = `${theme.fg("accent", icon2)} ${theme.bold ? theme.bold(theme.fg("toolTitle", shellConfig.title)) : theme.fg("toolTitle", shellConfig.title)}`;
|
|
1875
|
+
const preview = cmdLines.map(
|
|
1876
|
+
(l) => `${theme.fg("dim", shellConfig.prompt)}${theme.fg("text", l)}`
|
|
1877
|
+
);
|
|
1878
|
+
if (hasToolResult(context)) {
|
|
1879
|
+
return [
|
|
1880
|
+
...renderFrameTop(theme, title, safeWidth, borderTone),
|
|
1881
|
+
...preview.flatMap(
|
|
1882
|
+
(l) => renderFrameRow(theme, l, safeWidth, borderTone)
|
|
1883
|
+
),
|
|
1884
|
+
...renderFrameDivider(theme, "Output", safeWidth, borderTone)
|
|
1885
|
+
];
|
|
1886
|
+
}
|
|
1887
|
+
return [
|
|
1888
|
+
...renderFrameTop(theme, title, safeWidth, borderTone),
|
|
1889
|
+
...preview.flatMap(
|
|
1890
|
+
(l) => renderFrameRow(theme, l, safeWidth, borderTone)
|
|
1891
|
+
),
|
|
1892
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
1893
|
+
];
|
|
1894
|
+
});
|
|
1895
|
+
if (context?.state) {
|
|
1896
|
+
context.state.callComponent = comp;
|
|
1897
|
+
}
|
|
1898
|
+
return comp;
|
|
1899
|
+
},
|
|
1900
|
+
renderResult(result, options, theme, context) {
|
|
1901
|
+
if (context?.state) {
|
|
1902
|
+
context.state.hasResult = true;
|
|
1903
|
+
if (context.state.callComponent && typeof context.state.callComponent.invalidate === "function") {
|
|
1904
|
+
context.state.callComponent.invalidate();
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
if (context?.lastComponent && typeof context.lastComponent.invalidate === "function") {
|
|
1908
|
+
context.lastComponent.invalidate();
|
|
1909
|
+
}
|
|
1910
|
+
const isErr = Boolean(context?.isError);
|
|
1911
|
+
const isPartial = Boolean(options?.isPartial);
|
|
1912
|
+
const borderTone = isPartial ? "accent" : getToolBorderTone({ isError: isErr, isPartial });
|
|
1913
|
+
syncElapsedTicker({ ...context, isPartial });
|
|
1914
|
+
const textOutput = getResultText(result);
|
|
1915
|
+
const allLines = textOutput ? textOutput.split("\n") : [];
|
|
1916
|
+
const lineCount = allLines.length;
|
|
1917
|
+
const exitNum = extractShellExitCode(result, textOutput, isErr);
|
|
1918
|
+
const exitStr = `Exit ${exitNum}`;
|
|
1919
|
+
const words = textOutput.trim() ? textOutput.trim().split(/\s+/).length : 0;
|
|
1920
|
+
const wordsStr = words > 0 ? `~${words} words` : "";
|
|
1921
|
+
return createComponent((width) => {
|
|
1922
|
+
const safeWidth = Math.max(0, width);
|
|
1923
|
+
const footer = toolFooter(
|
|
1924
|
+
theme,
|
|
1925
|
+
{
|
|
1926
|
+
status: isPartial ? "running" : isErr ? "failed" : "completed",
|
|
1927
|
+
context: { ...context, isPartial },
|
|
1928
|
+
summary: [
|
|
1929
|
+
exitStr,
|
|
1930
|
+
`${lineCount} ${lineCount === 1 ? "line" : "lines"}`,
|
|
1931
|
+
wordsStr
|
|
1932
|
+
]
|
|
1933
|
+
},
|
|
1934
|
+
config.icons
|
|
1935
|
+
);
|
|
1936
|
+
if (allLines.length === 0) {
|
|
1937
|
+
const emptyBody = [theme.fg("dim", "(no output)")];
|
|
1938
|
+
return [
|
|
1939
|
+
...emptyBody.flatMap(
|
|
1940
|
+
(l) => renderFrameRow(theme, l, safeWidth, borderTone)
|
|
1941
|
+
),
|
|
1942
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
1943
|
+
];
|
|
1944
|
+
}
|
|
1945
|
+
const visibleLines = options?.expanded ? allLines : allLines.slice(0, COLLAPSED_OUTPUT_LINES);
|
|
1946
|
+
const bodyLines = visibleLines.map((l) => {
|
|
1947
|
+
const sanitized = escapeOutputRow(l);
|
|
1948
|
+
return isErr ? theme.fg("error", sanitized) : theme.fg("toolOutput", sanitized);
|
|
1949
|
+
});
|
|
1950
|
+
if (!options?.expanded && allLines.length > COLLAPSED_OUTPUT_LINES) {
|
|
1951
|
+
const remaining = allLines.length - COLLAPSED_OUTPUT_LINES;
|
|
1952
|
+
bodyLines.push(
|
|
1953
|
+
theme.fg(
|
|
1954
|
+
"dim",
|
|
1955
|
+
`${icon("ellipsis", config.icons)} ${remaining} more lines ${icon("separator", config.icons)} ctrl+o to expand`
|
|
1956
|
+
)
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
return [
|
|
1960
|
+
...bodyLines.flatMap(
|
|
1961
|
+
(l) => renderFrameRow(theme, l, safeWidth, borderTone)
|
|
1962
|
+
),
|
|
1963
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
1964
|
+
];
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
function createCustomBashTool(_cwd, config) {
|
|
1970
|
+
return createCustomShellTool(BASH_CONFIG, config);
|
|
1971
|
+
}
|
|
1972
|
+
function createCustomPowerShellTool(_cwd, config) {
|
|
1973
|
+
return createCustomShellTool(POWERSHELL_CONFIG, config);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// src/tools/edit.ts
|
|
1977
|
+
var COLLAPSED_DIFF_LINES = 6;
|
|
1978
|
+
function parseDiffStats(diffText) {
|
|
1979
|
+
let additions = 0;
|
|
1980
|
+
let removals = 0;
|
|
1981
|
+
const lines = diffText.split("\n");
|
|
1982
|
+
for (const line of lines) {
|
|
1983
|
+
if (line.startsWith("+++") || line.startsWith("---")) continue;
|
|
1984
|
+
if (line.startsWith("+")) additions++;
|
|
1985
|
+
else if (line.startsWith("-")) removals++;
|
|
1986
|
+
}
|
|
1987
|
+
return { additions, removals };
|
|
1988
|
+
}
|
|
1989
|
+
function colorDiffLine(line, theme) {
|
|
1990
|
+
if (line.startsWith("+++") || line.startsWith("---") || line.startsWith("@@")) {
|
|
1991
|
+
return theme.fg("dim", line);
|
|
1992
|
+
}
|
|
1993
|
+
if (line.startsWith("+")) {
|
|
1994
|
+
return theme.fg("toolDiffAdded", line);
|
|
1995
|
+
}
|
|
1996
|
+
if (line.startsWith("-")) {
|
|
1997
|
+
return theme.fg("toolDiffRemoved", line);
|
|
1998
|
+
}
|
|
1999
|
+
return theme.fg("toolDiffContext", line);
|
|
2000
|
+
}
|
|
2001
|
+
function createCustomEditTool(cwd, config) {
|
|
2002
|
+
return {
|
|
2003
|
+
renderShell: "self",
|
|
2004
|
+
renderCall(rawArgs, theme, context) {
|
|
2005
|
+
syncElapsedTicker(context);
|
|
2006
|
+
const args = rawArgs ?? {};
|
|
2007
|
+
const rawPath = String(args.path ?? args.file_path ?? "");
|
|
2008
|
+
const filePath = formatDisplayPath(rawPath, context?.cwd ?? cwd);
|
|
2009
|
+
const icon2 = getFileIcon(filePath, config.icons);
|
|
2010
|
+
const editIcon = getToolIcon("edit", config.icons);
|
|
2011
|
+
const borderTone = getToolBorderTone(context);
|
|
2012
|
+
const comp = createComponent((width) => {
|
|
2013
|
+
const safeWidth = Math.max(0, width);
|
|
2014
|
+
const footer = context?.executionStarted && context.isPartial ? toolFooter(theme, { status: "running", context }, config.icons) : void 0;
|
|
2015
|
+
const title = `${theme.fg("accent", editIcon)} ${theme.bold ? theme.bold(theme.fg("toolTitle", "Edit")) : theme.fg("toolTitle", "Edit")} ${theme.fg("accent", icon2)} ${theme.fg("text", filePath)}`;
|
|
2016
|
+
if (hasToolResult(context)) {
|
|
2017
|
+
return renderFrameTop(theme, title, safeWidth, borderTone);
|
|
2018
|
+
}
|
|
2019
|
+
return [
|
|
2020
|
+
...renderFrameTop(theme, title, safeWidth, borderTone),
|
|
2021
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2022
|
+
];
|
|
2023
|
+
});
|
|
2024
|
+
if (context?.state) {
|
|
2025
|
+
context.state.callComponent = comp;
|
|
2026
|
+
}
|
|
2027
|
+
return comp;
|
|
2028
|
+
},
|
|
2029
|
+
renderResult(result, options, theme, context) {
|
|
2030
|
+
if (context?.state) {
|
|
2031
|
+
context.state.hasResult = true;
|
|
2032
|
+
if (context.state.callComponent && typeof context.state.callComponent.invalidate === "function") {
|
|
2033
|
+
context.state.callComponent.invalidate();
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
if (context?.lastComponent && typeof context.lastComponent.invalidate === "function") {
|
|
2037
|
+
context.lastComponent.invalidate();
|
|
2038
|
+
}
|
|
2039
|
+
const isErr = Boolean(context?.isError);
|
|
2040
|
+
const status = options?.isPartial ? "running" : isErr ? "failed" : "completed";
|
|
2041
|
+
const footerOptions = {
|
|
2042
|
+
status,
|
|
2043
|
+
context: { ...context, isPartial: options?.isPartial }
|
|
2044
|
+
};
|
|
2045
|
+
syncElapsedTicker(footerOptions.context);
|
|
2046
|
+
const borderTone = getToolBorderTone({
|
|
2047
|
+
isError: isErr,
|
|
2048
|
+
isPartial: options?.isPartial
|
|
2049
|
+
});
|
|
2050
|
+
const details = result.details;
|
|
2051
|
+
const diffText = details?.diff ?? getResultText(result);
|
|
2052
|
+
if (isErr) {
|
|
2053
|
+
return createComponent((width) => {
|
|
2054
|
+
const safeWidth = Math.max(0, width);
|
|
2055
|
+
const errText = `${theme.fg("error", `${statusIcon("warning", config.icons)} `)}${theme.fg("error", diffText || "Edit failed")}`;
|
|
2056
|
+
return [
|
|
2057
|
+
...renderFrameRow(theme, errText, safeWidth, borderTone),
|
|
2058
|
+
...renderFrameBottom(
|
|
2059
|
+
theme,
|
|
2060
|
+
toolFooter(theme, footerOptions, config.icons),
|
|
2061
|
+
safeWidth,
|
|
2062
|
+
borderTone
|
|
2063
|
+
)
|
|
2064
|
+
];
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
const { additions, removals } = parseDiffStats(diffText);
|
|
2068
|
+
const diffLines = diffText ? diffText.split("\n") : [];
|
|
2069
|
+
return createComponent((width) => {
|
|
2070
|
+
const safeWidth = Math.max(0, width);
|
|
2071
|
+
const addedStr = additions > 0 ? theme.fg("toolDiffAdded", `+${additions}`) : theme.fg("dim", "+0");
|
|
2072
|
+
const removedStr = removals > 0 ? theme.fg("toolDiffRemoved", `-${removals}`) : theme.fg("dim", "-0");
|
|
2073
|
+
const footer = toolFooter(
|
|
2074
|
+
theme,
|
|
2075
|
+
{
|
|
2076
|
+
...footerOptions,
|
|
2077
|
+
summary: [`${addedStr} ${removedStr}`, "1 file"]
|
|
2078
|
+
},
|
|
2079
|
+
config.icons
|
|
2080
|
+
);
|
|
2081
|
+
if (diffLines.length === 0) {
|
|
2082
|
+
return [
|
|
2083
|
+
...renderFrameRow(
|
|
2084
|
+
theme,
|
|
2085
|
+
theme.fg("dim", "(no changes)"),
|
|
2086
|
+
safeWidth,
|
|
2087
|
+
borderTone
|
|
2088
|
+
),
|
|
2089
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2090
|
+
];
|
|
2091
|
+
}
|
|
2092
|
+
const visibleLines = options?.expanded ? diffLines : diffLines.slice(0, COLLAPSED_DIFF_LINES);
|
|
2093
|
+
const bodyLines = visibleLines.map(
|
|
2094
|
+
(l) => colorDiffLine(l, theme)
|
|
2095
|
+
);
|
|
2096
|
+
if (!options?.expanded && diffLines.length > COLLAPSED_DIFF_LINES) {
|
|
2097
|
+
const remaining = diffLines.length - COLLAPSED_DIFF_LINES;
|
|
2098
|
+
bodyLines.push(
|
|
2099
|
+
theme.fg(
|
|
2100
|
+
"dim",
|
|
2101
|
+
`${icon("ellipsis", config.icons)} ${remaining} more lines ${icon("separator", config.icons)} ctrl+o to expand`
|
|
2102
|
+
)
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
return [
|
|
2106
|
+
...bodyLines.flatMap(
|
|
2107
|
+
(l) => renderFrameRow(theme, l, safeWidth, borderTone)
|
|
2108
|
+
),
|
|
2109
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2110
|
+
];
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// src/tools/find.ts
|
|
2117
|
+
import { truncateToWidth as truncateToWidth8 } from "@earendil-works/pi-tui";
|
|
2118
|
+
var COLLAPSED_ENTRY_LIMIT = 8;
|
|
2119
|
+
function parseFindOutput(text, details) {
|
|
2120
|
+
const output = splitResultNotice(text, details, "find");
|
|
2121
|
+
return {
|
|
2122
|
+
entries: output.text.split("\n").filter((line) => line.length > 0),
|
|
2123
|
+
notices: output.notices
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
function createCustomFindTool(cwd, config) {
|
|
2127
|
+
return {
|
|
2128
|
+
renderShell: "self",
|
|
2129
|
+
renderCall(rawArgs, theme, context) {
|
|
2130
|
+
syncElapsedTicker(context);
|
|
2131
|
+
const args = rawArgs ?? {};
|
|
2132
|
+
const pattern = escapeControlCharacters(String(args.pattern ?? "*"));
|
|
2133
|
+
const rawPath = String(args.path ?? ".");
|
|
2134
|
+
const searchPath = escapeControlCharacters(
|
|
2135
|
+
formatDisplayPath(rawPath, context?.cwd ?? cwd)
|
|
2136
|
+
);
|
|
2137
|
+
const icon2 = getToolIcon("search", config.icons);
|
|
2138
|
+
const borderTone = getToolBorderTone(context);
|
|
2139
|
+
const comp = createComponent((width) => {
|
|
2140
|
+
const safeWidth = Math.max(0, width);
|
|
2141
|
+
const footer = context?.executionStarted && context.isPartial ? toolFooter(theme, { status: "running", context }, config.icons) : void 0;
|
|
2142
|
+
const title = `${theme.fg("accent", icon2)} ${theme.bold ? theme.bold(theme.fg("toolTitle", "Find")) : theme.fg("toolTitle", "Find")} ${theme.fg("syntaxString", `"${pattern}"`)} ${theme.fg("dim", "in")} ${theme.fg("text", searchPath)}`;
|
|
2143
|
+
if (!isFramedContext(context)) {
|
|
2144
|
+
return renderBox(theme, [], safeWidth, {
|
|
2145
|
+
title,
|
|
2146
|
+
footer,
|
|
2147
|
+
isError: borderTone === "error",
|
|
2148
|
+
isSuccess: borderTone === "success"
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
if (hasToolResult(context)) {
|
|
2152
|
+
return renderFrameTop(theme, title, safeWidth, borderTone);
|
|
2153
|
+
}
|
|
2154
|
+
return [
|
|
2155
|
+
...renderFrameTop(theme, title, safeWidth, borderTone),
|
|
2156
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2157
|
+
];
|
|
2158
|
+
});
|
|
2159
|
+
if (context?.state) {
|
|
2160
|
+
context.state.callComponent = comp;
|
|
2161
|
+
}
|
|
2162
|
+
return comp;
|
|
2163
|
+
},
|
|
2164
|
+
renderResult(result, options, theme, context) {
|
|
2165
|
+
if (context?.state) {
|
|
2166
|
+
context.state.hasResult = true;
|
|
2167
|
+
if (context.state.callComponent && typeof context.state.callComponent.invalidate === "function") {
|
|
2168
|
+
context.state.callComponent.invalidate();
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
if (context?.lastComponent && typeof context.lastComponent.invalidate === "function") {
|
|
2172
|
+
context.lastComponent.invalidate();
|
|
2173
|
+
}
|
|
2174
|
+
const isErr = Boolean(context?.isError);
|
|
2175
|
+
const status = options?.isPartial ? "running" : isErr ? "failed" : "completed";
|
|
2176
|
+
const footerOptions = {
|
|
2177
|
+
status,
|
|
2178
|
+
context: { ...context, isPartial: options?.isPartial }
|
|
2179
|
+
};
|
|
2180
|
+
syncElapsedTicker(footerOptions.context);
|
|
2181
|
+
const borderTone = getToolBorderTone({
|
|
2182
|
+
isError: isErr,
|
|
2183
|
+
isPartial: options?.isPartial
|
|
2184
|
+
});
|
|
2185
|
+
const textOutput = getRawResultText(result);
|
|
2186
|
+
if (isErr) {
|
|
2187
|
+
return createComponent((width) => {
|
|
2188
|
+
const safeWidth = Math.max(0, width);
|
|
2189
|
+
const footer = toolFooter(theme, footerOptions, config.icons);
|
|
2190
|
+
const errText = `${theme.fg("error", `${statusIcon("warning", config.icons)} `)}${theme.fg("error", escapeControlCharacters(textOutput || "Find failed"))}`;
|
|
2191
|
+
if (!isFramedContext(context)) {
|
|
2192
|
+
return [
|
|
2193
|
+
truncateToWidth8(errText, safeWidth),
|
|
2194
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2195
|
+
];
|
|
2196
|
+
}
|
|
2197
|
+
return [
|
|
2198
|
+
...renderFrameRow(theme, errText, safeWidth, borderTone),
|
|
2199
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2200
|
+
];
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
const { entries, notices } = parseFindOutput(textOutput, result.details);
|
|
2204
|
+
const total = entries.length;
|
|
2205
|
+
return createComponent((width) => {
|
|
2206
|
+
const safeWidth = Math.max(0, width);
|
|
2207
|
+
const footer = toolFooter(theme, footerOptions, config.icons);
|
|
2208
|
+
if (safeWidth === 0) return [];
|
|
2209
|
+
if (total === 0 && notices.length === 0) {
|
|
2210
|
+
if (!isFramedContext(context)) {
|
|
2211
|
+
return [
|
|
2212
|
+
truncateToWidth8(theme.fg("dim", "no matches found"), safeWidth),
|
|
2213
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2214
|
+
];
|
|
2215
|
+
}
|
|
2216
|
+
return [
|
|
2217
|
+
...renderFrameRow(
|
|
2218
|
+
theme,
|
|
2219
|
+
theme.fg("dim", "no matches found"),
|
|
2220
|
+
safeWidth,
|
|
2221
|
+
borderTone
|
|
2222
|
+
),
|
|
2223
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2224
|
+
];
|
|
2225
|
+
}
|
|
2226
|
+
const rawLines = [];
|
|
2227
|
+
const visible = options?.expanded ? entries : entries.slice(0, COLLAPSED_ENTRY_LIMIT);
|
|
2228
|
+
const remaining = total - visible.length;
|
|
2229
|
+
for (let i = 0; i < visible.length; i++) {
|
|
2230
|
+
const entry = visible[i];
|
|
2231
|
+
const isLast = i === visible.length - 1 && remaining === 0;
|
|
2232
|
+
const branch = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
|
2233
|
+
const icon2 = getFileIcon(entry, config.icons);
|
|
2234
|
+
const line = `${theme.fg("dim", branch)} ${theme.fg("accent", icon2)} ${theme.fg("toolOutput", escapeControlCharacters(entry))}`;
|
|
2235
|
+
rawLines.push(line);
|
|
2236
|
+
}
|
|
2237
|
+
if (remaining > 0) {
|
|
2238
|
+
const more = `${theme.fg("dim", "\u2514\u2500")} ${theme.fg("dim", `${icon("ellipsis", config.icons)} ${remaining} more matches ${icon("separator", config.icons)} ctrl+o to expand`)}`;
|
|
2239
|
+
rawLines.push(more);
|
|
2240
|
+
}
|
|
2241
|
+
for (const notice of notices) {
|
|
2242
|
+
rawLines.push(theme.fg("warning", notice));
|
|
2243
|
+
}
|
|
2244
|
+
if (!isFramedContext(context)) {
|
|
2245
|
+
return [
|
|
2246
|
+
...rawLines.map((line) => truncateToWidth8(` ${line}`, safeWidth)),
|
|
2247
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2248
|
+
];
|
|
2249
|
+
}
|
|
2250
|
+
return [
|
|
2251
|
+
...rawLines.flatMap(
|
|
2252
|
+
(line) => renderFrameRow(theme, line, safeWidth, borderTone)
|
|
2253
|
+
),
|
|
2254
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2255
|
+
];
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// src/tools/generic.ts
|
|
2262
|
+
var COLLAPSED_OUTPUT_LINES2 = 8;
|
|
2263
|
+
var MAX_ARGS_SUMMARY_LENGTH = 500;
|
|
2264
|
+
var NESTED_KEYS_SHOWN = 4;
|
|
2265
|
+
function formatValue(value) {
|
|
2266
|
+
if (typeof value === "string") return JSON.stringify(value) ?? '""';
|
|
2267
|
+
try {
|
|
2268
|
+
return JSON.stringify(value) ?? String(value);
|
|
2269
|
+
} catch {
|
|
2270
|
+
return String(value);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
function summarizeValue(value, mode) {
|
|
2274
|
+
if (Array.isArray(value)) return `[${value.length} items]`;
|
|
2275
|
+
if (value !== null && typeof value === "object") {
|
|
2276
|
+
const keys = Object.keys(value);
|
|
2277
|
+
const shown = keys.slice(0, NESTED_KEYS_SHOWN).join(", ");
|
|
2278
|
+
return `{${keys.length > NESTED_KEYS_SHOWN ? `${shown}, ${icon("ellipsis", mode)}` : shown}}`;
|
|
2279
|
+
}
|
|
2280
|
+
return formatValue(value);
|
|
2281
|
+
}
|
|
2282
|
+
function prettifyJson(text) {
|
|
2283
|
+
const trimmed = text.trim();
|
|
2284
|
+
const first = trimmed[0];
|
|
2285
|
+
if (first !== "{" && first !== "[") return text;
|
|
2286
|
+
try {
|
|
2287
|
+
return JSON.stringify(JSON.parse(trimmed), null, 2);
|
|
2288
|
+
} catch {
|
|
2289
|
+
return text;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
function summarizeArgs(args, mode = "nerd") {
|
|
2293
|
+
if (args === void 0 || args === null) return "";
|
|
2294
|
+
let summary;
|
|
2295
|
+
if (typeof args === "object" && !Array.isArray(args)) {
|
|
2296
|
+
summary = Object.entries(args).map(([key, value]) => `${key}=${summarizeValue(value, mode)}`).join(" ");
|
|
2297
|
+
} else {
|
|
2298
|
+
summary = formatValue(args);
|
|
2299
|
+
}
|
|
2300
|
+
return escapeControlCharacters(summary.slice(0, MAX_ARGS_SUMMARY_LENGTH));
|
|
2301
|
+
}
|
|
2302
|
+
function createGenericTool(toolName, config) {
|
|
2303
|
+
const toolIcon = icon("tool", config.icons);
|
|
2304
|
+
const name = escapeControlCharacters(toolName);
|
|
2305
|
+
return {
|
|
2306
|
+
renderShell: "self",
|
|
2307
|
+
renderCall(rawArgs, theme, context) {
|
|
2308
|
+
syncElapsedTicker(context);
|
|
2309
|
+
const borderTone = context?.isPartial ? "accent" : getToolBorderTone(context);
|
|
2310
|
+
const argsSummary = summarizeArgs(rawArgs, config.icons);
|
|
2311
|
+
const comp = createComponent((width) => {
|
|
2312
|
+
const footer = context?.executionStarted && context.isPartial ? toolFooter(theme, { status: "running", context }, config.icons) : void 0;
|
|
2313
|
+
const title = `${theme.fg("accent", toolIcon)} ${theme.bold ? theme.bold(theme.fg("toolTitle", name)) : theme.fg("toolTitle", name)}`;
|
|
2314
|
+
const lines = [
|
|
2315
|
+
...renderFrameTop(theme, title, width, borderTone),
|
|
2316
|
+
...argsSummary ? renderFrameRow(
|
|
2317
|
+
theme,
|
|
2318
|
+
theme.fg("dim", argsSummary),
|
|
2319
|
+
width,
|
|
2320
|
+
borderTone
|
|
2321
|
+
) : []
|
|
2322
|
+
];
|
|
2323
|
+
if (hasToolResult(context)) return lines;
|
|
2324
|
+
return [
|
|
2325
|
+
...lines,
|
|
2326
|
+
...renderFrameBottom(theme, footer, width, borderTone)
|
|
2327
|
+
];
|
|
2328
|
+
});
|
|
2329
|
+
if (context?.state) context.state.callComponent = comp;
|
|
2330
|
+
return comp;
|
|
2331
|
+
},
|
|
2332
|
+
renderResult(result, options, theme, context) {
|
|
2333
|
+
if (context?.state) {
|
|
2334
|
+
context.state.hasResult = true;
|
|
2335
|
+
context.state.callComponent?.invalidate?.();
|
|
2336
|
+
}
|
|
2337
|
+
context?.lastComponent?.invalidate?.();
|
|
2338
|
+
const isErr = Boolean(context?.isError);
|
|
2339
|
+
const isPartial = Boolean(options?.isPartial);
|
|
2340
|
+
const borderTone = isPartial ? "accent" : getToolBorderTone({ isError: isErr, isPartial });
|
|
2341
|
+
syncElapsedTicker({ ...context, isPartial });
|
|
2342
|
+
const text = prettifyJson(getResultText(result));
|
|
2343
|
+
const allLines = text ? text.split("\n") : [];
|
|
2344
|
+
const lineCount = allLines.length;
|
|
2345
|
+
const hasImage = hasImageContent(result);
|
|
2346
|
+
return createComponent((width) => {
|
|
2347
|
+
const footer = toolFooter(
|
|
2348
|
+
theme,
|
|
2349
|
+
{
|
|
2350
|
+
status: isPartial ? "running" : isErr ? "failed" : "completed",
|
|
2351
|
+
context: { ...context, isPartial },
|
|
2352
|
+
summary: `${lineCount} ${lineCount === 1 ? "line" : "lines"}`
|
|
2353
|
+
},
|
|
2354
|
+
config.icons
|
|
2355
|
+
);
|
|
2356
|
+
const visible = options?.expanded ? allLines : allLines.slice(0, COLLAPSED_OUTPUT_LINES2);
|
|
2357
|
+
const body = visible.map((line) => {
|
|
2358
|
+
const sanitized = escapeControlCharacters(line);
|
|
2359
|
+
return isErr ? theme.fg("error", sanitized) : theme.fg("toolOutput", sanitized);
|
|
2360
|
+
});
|
|
2361
|
+
if (!options?.expanded && allLines.length > COLLAPSED_OUTPUT_LINES2) {
|
|
2362
|
+
body.push(
|
|
2363
|
+
theme.fg(
|
|
2364
|
+
"dim",
|
|
2365
|
+
`${icon("ellipsis", config.icons)} ${allLines.length - COLLAPSED_OUTPUT_LINES2} more lines ${icon("separator", config.icons)} ctrl+o to expand`
|
|
2366
|
+
)
|
|
2367
|
+
);
|
|
2368
|
+
}
|
|
2369
|
+
if (body.length === 0 && !hasImage) {
|
|
2370
|
+
body.push(theme.fg("dim", "(no output)"));
|
|
2371
|
+
}
|
|
2372
|
+
return [
|
|
2373
|
+
...body.flatMap(
|
|
2374
|
+
(line) => renderFrameRow(theme, line, width, borderTone)
|
|
2375
|
+
),
|
|
2376
|
+
...renderFrameBottom(theme, footer, width, borderTone)
|
|
2377
|
+
];
|
|
2378
|
+
});
|
|
2379
|
+
}
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
// src/tools/grep.ts
|
|
2384
|
+
import { truncateToWidth as truncateToWidth9 } from "@earendil-works/pi-tui";
|
|
2385
|
+
function getGrepCandidates(rawLine, isContext) {
|
|
2386
|
+
const candidates = [];
|
|
2387
|
+
const delimiterPattern = isContext ? /(?=-(\d+)-)/g : /(?=:(\d+):)/g;
|
|
2388
|
+
for (const delimiter of rawLine.matchAll(delimiterPattern)) {
|
|
2389
|
+
const file = rawLine.slice(0, delimiter.index);
|
|
2390
|
+
if (!file) continue;
|
|
2391
|
+
candidates.push({
|
|
2392
|
+
file,
|
|
2393
|
+
line: delimiter[1],
|
|
2394
|
+
content: rawLine.slice(delimiter.index + delimiter[1].length + 2),
|
|
2395
|
+
isContext
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
return candidates;
|
|
2399
|
+
}
|
|
2400
|
+
function parseGrepOutput(text, details) {
|
|
2401
|
+
const output = splitResultNotice(text, details, "grep");
|
|
2402
|
+
const lines = output.text || output.notices.length > 0 ? output.text.split("\n") : [];
|
|
2403
|
+
const metadata = details && typeof details === "object" ? details : void 0;
|
|
2404
|
+
const rawResult = {
|
|
2405
|
+
raw: true,
|
|
2406
|
+
rawLines: lines,
|
|
2407
|
+
notices: output.notices
|
|
2408
|
+
};
|
|
2409
|
+
if (typeof metadata?.matchLimitReached === "number" || metadata?.truncation?.truncated === true || metadata?.linesTruncated === true) {
|
|
2410
|
+
return rawResult;
|
|
2411
|
+
}
|
|
2412
|
+
const matches = [];
|
|
2413
|
+
for (const rawLine of lines) {
|
|
2414
|
+
const candidates = [
|
|
2415
|
+
...getGrepCandidates(rawLine, false),
|
|
2416
|
+
...getGrepCandidates(rawLine, true)
|
|
2417
|
+
];
|
|
2418
|
+
if (candidates.length !== 1) return rawResult;
|
|
2419
|
+
matches.push(candidates[0]);
|
|
2420
|
+
}
|
|
2421
|
+
return { raw: false, matches };
|
|
2422
|
+
}
|
|
2423
|
+
function groupMatches(matches) {
|
|
2424
|
+
const groups = [];
|
|
2425
|
+
for (const m of matches) {
|
|
2426
|
+
const previous = groups.at(-1);
|
|
2427
|
+
if (previous?.[0] === m.file) {
|
|
2428
|
+
previous[1].push(m);
|
|
2429
|
+
} else {
|
|
2430
|
+
groups.push([m.file, [m]]);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
return groups;
|
|
2434
|
+
}
|
|
2435
|
+
function createCustomGrepTool(cwd, config) {
|
|
2436
|
+
return {
|
|
2437
|
+
renderShell: "self",
|
|
2438
|
+
renderCall(rawArgs, theme, context) {
|
|
2439
|
+
syncElapsedTicker(context);
|
|
2440
|
+
const args = rawArgs ?? {};
|
|
2441
|
+
const pattern = escapeControlCharacters(String(args.pattern ?? ""));
|
|
2442
|
+
const rawPath = args.path ? String(args.path) : "";
|
|
2443
|
+
const searchPath = rawPath ? ` in ${escapeControlCharacters(formatDisplayPath(rawPath, context?.cwd ?? cwd))}` : "";
|
|
2444
|
+
const icon2 = getToolIcon("search", config.icons);
|
|
2445
|
+
const borderTone = getToolBorderTone(context);
|
|
2446
|
+
const comp = createComponent((width) => {
|
|
2447
|
+
const safeWidth = Math.max(0, width);
|
|
2448
|
+
const footer = context?.executionStarted && context.isPartial ? toolFooter(theme, { status: "running", context }, config.icons) : void 0;
|
|
2449
|
+
const title = `${theme.fg("accent", icon2)} ${theme.bold ? theme.bold(theme.fg("toolTitle", "Grep")) : theme.fg("toolTitle", "Grep")} ${theme.fg("syntaxString", `"${pattern}"`)}${theme.fg("dim", searchPath)}`;
|
|
2450
|
+
if (!isFramedContext(context)) {
|
|
2451
|
+
return renderBox(theme, [], safeWidth, {
|
|
2452
|
+
title,
|
|
2453
|
+
footer,
|
|
2454
|
+
isError: borderTone === "error",
|
|
2455
|
+
isSuccess: borderTone === "success"
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
if (hasToolResult(context)) {
|
|
2459
|
+
return renderFrameTop(theme, title, safeWidth, borderTone);
|
|
2460
|
+
}
|
|
2461
|
+
return [
|
|
2462
|
+
...renderFrameTop(theme, title, safeWidth, borderTone),
|
|
2463
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2464
|
+
];
|
|
2465
|
+
});
|
|
2466
|
+
if (context?.state) {
|
|
2467
|
+
context.state.callComponent = comp;
|
|
2468
|
+
}
|
|
2469
|
+
return comp;
|
|
2470
|
+
},
|
|
2471
|
+
renderResult(result, options, theme, context) {
|
|
2472
|
+
if (context?.state) {
|
|
2473
|
+
context.state.hasResult = true;
|
|
2474
|
+
if (context.state.callComponent && typeof context.state.callComponent.invalidate === "function") {
|
|
2475
|
+
context.state.callComponent.invalidate();
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
if (context?.lastComponent && typeof context.lastComponent.invalidate === "function") {
|
|
2479
|
+
context.lastComponent.invalidate();
|
|
2480
|
+
}
|
|
2481
|
+
const isErr = Boolean(context?.isError);
|
|
2482
|
+
const status = options?.isPartial ? "running" : isErr ? "failed" : "completed";
|
|
2483
|
+
const footerOptions = {
|
|
2484
|
+
status,
|
|
2485
|
+
context: { ...context, isPartial: options?.isPartial }
|
|
2486
|
+
};
|
|
2487
|
+
syncElapsedTicker(footerOptions.context);
|
|
2488
|
+
const borderTone = getToolBorderTone({
|
|
2489
|
+
isError: isErr,
|
|
2490
|
+
isPartial: options?.isPartial
|
|
2491
|
+
});
|
|
2492
|
+
const textOutput = getRawResultText(result);
|
|
2493
|
+
if (isErr) {
|
|
2494
|
+
return createComponent((width) => {
|
|
2495
|
+
const safeWidth = Math.max(0, width);
|
|
2496
|
+
const footer = toolFooter(theme, footerOptions, config.icons);
|
|
2497
|
+
const errText = `${theme.fg("error", `${statusIcon("warning", config.icons)} `)}${theme.fg("error", escapeControlCharacters(textOutput || "Grep failed"))}`;
|
|
2498
|
+
if (!isFramedContext(context)) {
|
|
2499
|
+
return [
|
|
2500
|
+
truncateToWidth9(errText, safeWidth),
|
|
2501
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2502
|
+
];
|
|
2503
|
+
}
|
|
2504
|
+
return [
|
|
2505
|
+
...renderFrameRow(theme, errText, safeWidth, borderTone),
|
|
2506
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2507
|
+
];
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
2510
|
+
const output = parseGrepOutput(textOutput, result.details);
|
|
2511
|
+
return createComponent((width) => {
|
|
2512
|
+
const safeWidth = Math.max(0, width);
|
|
2513
|
+
const footer = toolFooter(theme, footerOptions, config.icons);
|
|
2514
|
+
if (safeWidth === 0) return [];
|
|
2515
|
+
if (output.raw) {
|
|
2516
|
+
const { rawLines: rawLines2, notices } = output;
|
|
2517
|
+
const visibleRawLines = options?.expanded ? rawLines2 : rawLines2.slice(0, 8);
|
|
2518
|
+
const lines = visibleRawLines.map(
|
|
2519
|
+
(rawLine) => theme.fg("toolOutput", escapeControlCharacters(rawLine))
|
|
2520
|
+
);
|
|
2521
|
+
if (visibleRawLines.length < rawLines2.length) {
|
|
2522
|
+
lines.push(
|
|
2523
|
+
theme.fg(
|
|
2524
|
+
"dim",
|
|
2525
|
+
`${icon("ellipsis", config.icons)} ${rawLines2.length - visibleRawLines.length} more lines ${icon("separator", config.icons)} ctrl+o to expand`
|
|
2526
|
+
)
|
|
2527
|
+
);
|
|
2528
|
+
}
|
|
2529
|
+
if (notices.length > 0) lines.push("");
|
|
2530
|
+
for (const notice of notices) {
|
|
2531
|
+
lines.push(theme.fg("warning", notice));
|
|
2532
|
+
}
|
|
2533
|
+
if (!isFramedContext(context)) {
|
|
2534
|
+
const rawWithPrefix = lines.map((l) => l ? ` ${l}` : " ");
|
|
2535
|
+
return renderBox(theme, rawWithPrefix, safeWidth, {
|
|
2536
|
+
footer,
|
|
2537
|
+
isSuccess: borderTone === "success"
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
return [
|
|
2541
|
+
...lines.flatMap(
|
|
2542
|
+
(l) => renderFrameRow(theme, l ? ` ${l}` : " ", safeWidth, borderTone)
|
|
2543
|
+
),
|
|
2544
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2545
|
+
];
|
|
2546
|
+
}
|
|
2547
|
+
const { matches } = output;
|
|
2548
|
+
const totalItems = matches.length;
|
|
2549
|
+
if (totalItems === 0) {
|
|
2550
|
+
if (!isFramedContext(context)) {
|
|
2551
|
+
return [
|
|
2552
|
+
truncateToWidth9(theme.fg("dim", "no matches found"), safeWidth),
|
|
2553
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2554
|
+
];
|
|
2555
|
+
}
|
|
2556
|
+
return [
|
|
2557
|
+
...renderFrameRow(
|
|
2558
|
+
theme,
|
|
2559
|
+
theme.fg("dim", "no matches found"),
|
|
2560
|
+
safeWidth,
|
|
2561
|
+
borderTone
|
|
2562
|
+
),
|
|
2563
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2564
|
+
];
|
|
2565
|
+
}
|
|
2566
|
+
const groupEntries = groupMatches(matches);
|
|
2567
|
+
const rawLines = [];
|
|
2568
|
+
const maxGroups = options?.expanded ? groupEntries.length : 3;
|
|
2569
|
+
const visibleGroups = groupEntries.slice(0, maxGroups);
|
|
2570
|
+
let renderedItemCount = 0;
|
|
2571
|
+
for (let gi = 0; gi < visibleGroups.length; gi++) {
|
|
2572
|
+
const [file, fileMatches] = visibleGroups[gi];
|
|
2573
|
+
const isLastGroup = gi === visibleGroups.length - 1 && visibleGroups.length === groupEntries.length;
|
|
2574
|
+
const groupBranch = isLastGroup ? "\u2514\u2500" : "\u251C\u2500";
|
|
2575
|
+
const fileIcon = getFileIcon(file, config.icons);
|
|
2576
|
+
const actualMatches = fileMatches.filter((m) => !m.isContext).length;
|
|
2577
|
+
const matchCountLabel = `(${actualMatches} ${actualMatches === 1 ? "match" : "matches"})`;
|
|
2578
|
+
const fileHeader = `${theme.fg("dim", groupBranch)} ${theme.fg("accent", fileIcon)} ${theme.fg("toolTitle", escapeControlCharacters(file))} ${theme.fg("dim", matchCountLabel)}`;
|
|
2579
|
+
rawLines.push(fileHeader);
|
|
2580
|
+
const maxMatches = options?.expanded ? fileMatches.length : 2;
|
|
2581
|
+
const visibleMatches = fileMatches.slice(0, maxMatches);
|
|
2582
|
+
renderedItemCount += visibleMatches.length;
|
|
2583
|
+
const matchBranchPrefix = isLastGroup ? " " : ` ${theme.fg("dim", "\u2502")} `;
|
|
2584
|
+
for (const m of visibleMatches) {
|
|
2585
|
+
const separator2 = m.isContext ? "- " : ": ";
|
|
2586
|
+
const lineNum = theme.fg(
|
|
2587
|
+
"dim",
|
|
2588
|
+
`${m.line.padStart(4, " ")}${separator2}`
|
|
2589
|
+
);
|
|
2590
|
+
const content = theme.fg(
|
|
2591
|
+
m.isContext ? "dim" : "toolOutput",
|
|
2592
|
+
escapeControlCharacters(m.content)
|
|
2593
|
+
);
|
|
2594
|
+
rawLines.push(`${matchBranchPrefix}${lineNum}${content}`);
|
|
2595
|
+
}
|
|
2596
|
+
if (fileMatches.length > visibleMatches.length && !options?.expanded) {
|
|
2597
|
+
const moreFileMatches = fileMatches.length - visibleMatches.length;
|
|
2598
|
+
rawLines.push(
|
|
2599
|
+
`${matchBranchPrefix}${theme.fg("dim", `${icon("ellipsis", config.icons)} ${moreFileMatches} more`)}`
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
const remainingItems = totalItems - renderedItemCount;
|
|
2604
|
+
if (!options?.expanded && (groupEntries.length > maxGroups || remainingItems > 0)) {
|
|
2605
|
+
const hiddenFiles = groupEntries.length - visibleGroups.length;
|
|
2606
|
+
const summary = `${theme.fg("dim", "\u2514\u2500")} ${theme.fg("dim", `${icon("ellipsis", config.icons)} ${remainingItems} more matches across ${hiddenFiles} files ${icon("separator", config.icons)} ctrl+o to expand`)}`;
|
|
2607
|
+
rawLines.push(summary);
|
|
2608
|
+
}
|
|
2609
|
+
if (!isFramedContext(context)) {
|
|
2610
|
+
return [
|
|
2611
|
+
...rawLines.map((line) => truncateToWidth9(` ${line}`, safeWidth)),
|
|
2612
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2613
|
+
];
|
|
2614
|
+
}
|
|
2615
|
+
return [
|
|
2616
|
+
...rawLines.flatMap(
|
|
2617
|
+
(line) => renderFrameRow(theme, line, safeWidth, borderTone)
|
|
2618
|
+
),
|
|
2619
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2620
|
+
];
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
// src/tools/ls.ts
|
|
2627
|
+
import { truncateToWidth as truncateToWidth10 } from "@earendil-works/pi-tui";
|
|
2628
|
+
var COLLAPSED_ENTRY_LIMIT2 = 8;
|
|
2629
|
+
function parseLsOutput(text, details) {
|
|
2630
|
+
if (text === "(empty directory)") return { entries: [], notices: [] };
|
|
2631
|
+
const output = splitResultNotice(text, details, "ls");
|
|
2632
|
+
return {
|
|
2633
|
+
entries: output.text.split("\n").filter((line) => line.length > 0),
|
|
2634
|
+
notices: output.notices
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
function createCustomLsTool(cwd, config) {
|
|
2638
|
+
return {
|
|
2639
|
+
renderShell: "self",
|
|
2640
|
+
renderCall(rawArgs, theme, context) {
|
|
2641
|
+
syncElapsedTicker(context);
|
|
2642
|
+
const args = rawArgs ?? {};
|
|
2643
|
+
const rawPath = String(args.path ?? ".");
|
|
2644
|
+
const dirPath = escapeControlCharacters(
|
|
2645
|
+
formatDisplayPath(rawPath, context?.cwd ?? cwd)
|
|
2646
|
+
);
|
|
2647
|
+
const icon2 = getDirIcon(config.icons);
|
|
2648
|
+
const borderTone = getToolBorderTone(context);
|
|
2649
|
+
const comp = createComponent((width) => {
|
|
2650
|
+
const safeWidth = Math.max(0, width);
|
|
2651
|
+
const footer = context?.executionStarted && context.isPartial ? toolFooter(theme, { status: "running", context }, config.icons) : void 0;
|
|
2652
|
+
const title = `${theme.fg("accent", icon2)} ${theme.bold ? theme.bold(theme.fg("toolTitle", "List")) : theme.fg("toolTitle", "List")} ${theme.fg("text", dirPath)}`;
|
|
2653
|
+
if (!isFramedContext(context)) {
|
|
2654
|
+
return renderBox(theme, [], safeWidth, {
|
|
2655
|
+
title,
|
|
2656
|
+
footer,
|
|
2657
|
+
isError: borderTone === "error",
|
|
2658
|
+
isSuccess: borderTone === "success"
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
if (hasToolResult(context)) {
|
|
2662
|
+
return renderFrameTop(theme, title, safeWidth, borderTone);
|
|
2663
|
+
}
|
|
2664
|
+
return [
|
|
2665
|
+
...renderFrameTop(theme, title, safeWidth, borderTone),
|
|
2666
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2667
|
+
];
|
|
2668
|
+
});
|
|
2669
|
+
if (context?.state) {
|
|
2670
|
+
context.state.callComponent = comp;
|
|
2671
|
+
}
|
|
2672
|
+
return comp;
|
|
2673
|
+
},
|
|
2674
|
+
renderResult(result, options, theme, context) {
|
|
2675
|
+
if (context?.state) {
|
|
2676
|
+
context.state.hasResult = true;
|
|
2677
|
+
if (context.state.callComponent && typeof context.state.callComponent.invalidate === "function") {
|
|
2678
|
+
context.state.callComponent.invalidate();
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
if (context?.lastComponent && typeof context.lastComponent.invalidate === "function") {
|
|
2682
|
+
context.lastComponent.invalidate();
|
|
2683
|
+
}
|
|
2684
|
+
const isErr = Boolean(context?.isError);
|
|
2685
|
+
const status = options?.isPartial ? "running" : isErr ? "failed" : "completed";
|
|
2686
|
+
const footerOptions = {
|
|
2687
|
+
status,
|
|
2688
|
+
context: { ...context, isPartial: options?.isPartial }
|
|
2689
|
+
};
|
|
2690
|
+
syncElapsedTicker(footerOptions.context);
|
|
2691
|
+
const borderTone = getToolBorderTone({
|
|
2692
|
+
isError: isErr,
|
|
2693
|
+
isPartial: options?.isPartial
|
|
2694
|
+
});
|
|
2695
|
+
const textOutput = getRawResultText(result);
|
|
2696
|
+
if (isErr) {
|
|
2697
|
+
return createComponent((width) => {
|
|
2698
|
+
const safeWidth = Math.max(0, width);
|
|
2699
|
+
const footer = toolFooter(theme, footerOptions, config.icons);
|
|
2700
|
+
const errText = `${theme.fg("error", `${statusIcon("warning", config.icons)} `)}${theme.fg("error", escapeControlCharacters(textOutput || "Failed to list directory"))}`;
|
|
2701
|
+
if (!isFramedContext(context)) {
|
|
2702
|
+
return [
|
|
2703
|
+
truncateToWidth10(errText, safeWidth),
|
|
2704
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2705
|
+
];
|
|
2706
|
+
}
|
|
2707
|
+
return [
|
|
2708
|
+
...renderFrameRow(theme, errText, safeWidth, borderTone),
|
|
2709
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2710
|
+
];
|
|
2711
|
+
});
|
|
2712
|
+
}
|
|
2713
|
+
const { entries, notices } = parseLsOutput(textOutput, result.details);
|
|
2714
|
+
const total = entries.length;
|
|
2715
|
+
return createComponent((width) => {
|
|
2716
|
+
const safeWidth = Math.max(0, width);
|
|
2717
|
+
const footer = toolFooter(theme, footerOptions, config.icons);
|
|
2718
|
+
if (safeWidth === 0) return [];
|
|
2719
|
+
if (total === 0 && notices.length === 0) {
|
|
2720
|
+
if (!isFramedContext(context)) {
|
|
2721
|
+
return [
|
|
2722
|
+
truncateToWidth10(theme.fg("dim", "empty directory"), safeWidth),
|
|
2723
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2724
|
+
];
|
|
2725
|
+
}
|
|
2726
|
+
return [
|
|
2727
|
+
...renderFrameRow(
|
|
2728
|
+
theme,
|
|
2729
|
+
theme.fg("dim", "empty directory"),
|
|
2730
|
+
safeWidth,
|
|
2731
|
+
borderTone
|
|
2732
|
+
),
|
|
2733
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2734
|
+
];
|
|
2735
|
+
}
|
|
2736
|
+
const rawLines = [];
|
|
2737
|
+
const visible = options?.expanded ? entries : entries.slice(0, COLLAPSED_ENTRY_LIMIT2);
|
|
2738
|
+
const remaining = total - visible.length;
|
|
2739
|
+
for (let i = 0; i < visible.length; i++) {
|
|
2740
|
+
const entry = visible[i];
|
|
2741
|
+
const isLast = i === visible.length - 1 && remaining === 0;
|
|
2742
|
+
const branch = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
|
2743
|
+
const isDir = entry.endsWith("/") || entry.endsWith("\\");
|
|
2744
|
+
const icon2 = isDir ? getDirIcon(config.icons) : getFileIcon(entry, config.icons);
|
|
2745
|
+
const line = `${theme.fg("dim", branch)} ${theme.fg("accent", icon2)} ${theme.fg("toolOutput", escapeControlCharacters(entry))}`;
|
|
2746
|
+
rawLines.push(line);
|
|
2747
|
+
}
|
|
2748
|
+
if (remaining > 0) {
|
|
2749
|
+
const more = `${theme.fg("dim", "\u2514\u2500")} ${theme.fg("dim", `${icon("ellipsis", config.icons)} ${remaining} more files ${icon("separator", config.icons)} ctrl+o to expand`)}`;
|
|
2750
|
+
rawLines.push(more);
|
|
2751
|
+
}
|
|
2752
|
+
for (const notice of notices) {
|
|
2753
|
+
rawLines.push(theme.fg("warning", notice));
|
|
2754
|
+
}
|
|
2755
|
+
if (!isFramedContext(context)) {
|
|
2756
|
+
return [
|
|
2757
|
+
...rawLines.map((line) => truncateToWidth10(` ${line}`, safeWidth)),
|
|
2758
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2759
|
+
];
|
|
2760
|
+
}
|
|
2761
|
+
return [
|
|
2762
|
+
...rawLines.flatMap(
|
|
2763
|
+
(line) => renderFrameRow(theme, line, safeWidth, borderTone)
|
|
2764
|
+
),
|
|
2765
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2766
|
+
];
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// src/tools/ownership.ts
|
|
2773
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
2774
|
+
import { dirname, join as join2 } from "node:path";
|
|
2775
|
+
function isOwnedName(name, respectPackages) {
|
|
2776
|
+
return typeof name === "string" && respectPackages.some((pattern) => {
|
|
2777
|
+
if (name === pattern) return true;
|
|
2778
|
+
if (!pattern.endsWith("/*") || pattern.indexOf("*") !== pattern.length - 1) {
|
|
2779
|
+
return false;
|
|
2780
|
+
}
|
|
2781
|
+
const prefix = pattern.slice(0, -1);
|
|
2782
|
+
return name.startsWith(prefix) && name.length > prefix.length;
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2785
|
+
function isMissing(error) {
|
|
2786
|
+
const code = error?.code;
|
|
2787
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
2788
|
+
}
|
|
2789
|
+
function createOwnershipResolver(respectPackages, read = (path) => readFileSync2(path, "utf8")) {
|
|
2790
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2791
|
+
return (baseDir) => {
|
|
2792
|
+
const visited = [];
|
|
2793
|
+
let dir = baseDir;
|
|
2794
|
+
let owned = false;
|
|
2795
|
+
for (; ; ) {
|
|
2796
|
+
const hit = cache.get(dir);
|
|
2797
|
+
if (hit !== void 0) {
|
|
2798
|
+
owned = hit;
|
|
2799
|
+
break;
|
|
2800
|
+
}
|
|
2801
|
+
visited.push(dir);
|
|
2802
|
+
try {
|
|
2803
|
+
const manifest = JSON.parse(read(join2(dir, "package.json")));
|
|
2804
|
+
owned = isOwnedName(
|
|
2805
|
+
manifest?.name,
|
|
2806
|
+
respectPackages
|
|
2807
|
+
);
|
|
2808
|
+
break;
|
|
2809
|
+
} catch (error) {
|
|
2810
|
+
if (!(error instanceof Error) || !isMissing(error)) break;
|
|
2811
|
+
}
|
|
2812
|
+
const parent = dirname(dir);
|
|
2813
|
+
if (parent === dir) break;
|
|
2814
|
+
dir = parent;
|
|
2815
|
+
}
|
|
2816
|
+
for (const entry of visited) cache.set(entry, owned);
|
|
2817
|
+
return owned;
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// src/tools/read.ts
|
|
2822
|
+
function createCustomReadTool(cwd, config) {
|
|
2823
|
+
return {
|
|
2824
|
+
renderShell: "self",
|
|
2825
|
+
renderCall(rawArgs, theme, context) {
|
|
2826
|
+
syncElapsedTicker(context);
|
|
2827
|
+
const args = rawArgs ?? {};
|
|
2828
|
+
const rawPath = String(args.path ?? args.file_path ?? "");
|
|
2829
|
+
const filePath = formatDisplayPath(rawPath, context?.cwd ?? cwd);
|
|
2830
|
+
const icon2 = getFileIcon(filePath, config.icons);
|
|
2831
|
+
let range = "";
|
|
2832
|
+
if (args.offset !== void 0 || args.limit !== void 0) {
|
|
2833
|
+
const start = args.offset ?? 1;
|
|
2834
|
+
const end = args.limit !== void 0 ? Number(start) + Number(args.limit) - 1 : "";
|
|
2835
|
+
range = `:${start}${end ? `-${end}` : ""}`;
|
|
2836
|
+
}
|
|
2837
|
+
const borderTone = getToolBorderTone(context);
|
|
2838
|
+
const comp = createComponent((width) => {
|
|
2839
|
+
const safeWidth = Math.max(0, width);
|
|
2840
|
+
const footer = context?.executionStarted && context.isPartial ? toolFooter(theme, { status: "running", context }, config.icons) : void 0;
|
|
2841
|
+
const title = `${theme.fg("accent", icon2)} ${theme.bold ? theme.bold(theme.fg("toolTitle", "Read")) : theme.fg("toolTitle", "Read")} ${theme.fg("text", `${filePath}${range}`)}`;
|
|
2842
|
+
if (hasToolResult(context)) {
|
|
2843
|
+
return renderFrameTop(theme, title, safeWidth, borderTone);
|
|
2844
|
+
}
|
|
2845
|
+
return [
|
|
2846
|
+
...renderFrameTop(theme, title, safeWidth, borderTone),
|
|
2847
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2848
|
+
];
|
|
2849
|
+
});
|
|
2850
|
+
if (context?.state) {
|
|
2851
|
+
context.state.callComponent = comp;
|
|
2852
|
+
}
|
|
2853
|
+
return comp;
|
|
2854
|
+
},
|
|
2855
|
+
renderResult(result, options, theme, context) {
|
|
2856
|
+
if (context?.state) {
|
|
2857
|
+
context.state.hasResult = true;
|
|
2858
|
+
if (context.state.callComponent && typeof context.state.callComponent.invalidate === "function") {
|
|
2859
|
+
context.state.callComponent.invalidate();
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
if (context?.lastComponent && typeof context.lastComponent.invalidate === "function") {
|
|
2863
|
+
context.lastComponent.invalidate();
|
|
2864
|
+
}
|
|
2865
|
+
const isErr = Boolean(context?.isError);
|
|
2866
|
+
const status = options?.isPartial ? "running" : isErr ? "failed" : "completed";
|
|
2867
|
+
const footerOptions = {
|
|
2868
|
+
status,
|
|
2869
|
+
context: { ...context, isPartial: options?.isPartial }
|
|
2870
|
+
};
|
|
2871
|
+
syncElapsedTicker(footerOptions.context);
|
|
2872
|
+
const borderTone = getToolBorderTone({
|
|
2873
|
+
isError: isErr,
|
|
2874
|
+
isPartial: options?.isPartial
|
|
2875
|
+
});
|
|
2876
|
+
const textOutput = getResultText(result);
|
|
2877
|
+
if (isErr) {
|
|
2878
|
+
return createComponent((width) => {
|
|
2879
|
+
const safeWidth = Math.max(0, width);
|
|
2880
|
+
const footer = toolFooter(theme, footerOptions, config.icons);
|
|
2881
|
+
const errText = `${theme.fg("error", `${statusIcon("warning", config.icons)} `)}${theme.fg("error", textOutput || "Failed to read file")}`;
|
|
2882
|
+
return [
|
|
2883
|
+
...renderFrameRow(theme, errText, safeWidth, borderTone),
|
|
2884
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2885
|
+
];
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
if (hasImageContent(result)) {
|
|
2889
|
+
const firstTextBlock = result.content?.find(
|
|
2890
|
+
(c) => c && typeof c === "object" && c.type === "text"
|
|
2891
|
+
);
|
|
2892
|
+
const note = firstTextBlock?.text ?? "Read image file";
|
|
2893
|
+
return createComponent((width) => {
|
|
2894
|
+
const safeWidth = Math.max(0, width);
|
|
2895
|
+
const footer = toolFooter(theme, footerOptions, config.icons);
|
|
2896
|
+
return [
|
|
2897
|
+
...renderFrameRow(
|
|
2898
|
+
theme,
|
|
2899
|
+
theme.fg("dim", note),
|
|
2900
|
+
safeWidth,
|
|
2901
|
+
borderTone
|
|
2902
|
+
),
|
|
2903
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2904
|
+
];
|
|
2905
|
+
});
|
|
2906
|
+
}
|
|
2907
|
+
const lines = textOutput ? textOutput.split("\n") : [];
|
|
2908
|
+
const lineCount = lines.length;
|
|
2909
|
+
return createComponent((width) => {
|
|
2910
|
+
const safeWidth = Math.max(0, width);
|
|
2911
|
+
const footer = toolFooter(theme, footerOptions, config.icons);
|
|
2912
|
+
if (!options?.expanded) {
|
|
2913
|
+
const summary = `${lineCount} ${lineCount === 1 ? "line" : "lines"} ${icon("separator", config.icons)} ctrl+o to expand`;
|
|
2914
|
+
return [
|
|
2915
|
+
...renderFrameRow(
|
|
2916
|
+
theme,
|
|
2917
|
+
theme.fg("dim", summary),
|
|
2918
|
+
safeWidth,
|
|
2919
|
+
borderTone
|
|
2920
|
+
),
|
|
2921
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2922
|
+
];
|
|
2923
|
+
}
|
|
2924
|
+
if (lines.length === 0) {
|
|
2925
|
+
return [
|
|
2926
|
+
...renderFrameRow(
|
|
2927
|
+
theme,
|
|
2928
|
+
theme.fg("dim", "(empty file)"),
|
|
2929
|
+
safeWidth,
|
|
2930
|
+
borderTone
|
|
2931
|
+
),
|
|
2932
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2933
|
+
];
|
|
2934
|
+
}
|
|
2935
|
+
return [
|
|
2936
|
+
...lines.flatMap(
|
|
2937
|
+
(line) => renderFrameRow(theme, line, safeWidth, borderTone)
|
|
2938
|
+
),
|
|
2939
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2940
|
+
];
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
};
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
// src/tools/write.ts
|
|
2947
|
+
var COLLAPSED_WRITE_LINES = 6;
|
|
2948
|
+
function createCustomWriteTool(cwd, config) {
|
|
2949
|
+
return {
|
|
2950
|
+
renderShell: "self",
|
|
2951
|
+
renderCall(rawArgs, theme, context) {
|
|
2952
|
+
syncElapsedTicker(context);
|
|
2953
|
+
const args = rawArgs ?? {};
|
|
2954
|
+
const rawPath = String(args.path ?? args.file_path ?? "");
|
|
2955
|
+
const filePath = formatDisplayPath(rawPath, context?.cwd ?? cwd);
|
|
2956
|
+
const icon2 = getFileIcon(filePath, config.icons);
|
|
2957
|
+
const writeIcon = getToolIcon("write", config.icons);
|
|
2958
|
+
const borderTone = getToolBorderTone(context);
|
|
2959
|
+
const comp = createComponent((width) => {
|
|
2960
|
+
const safeWidth = Math.max(0, width);
|
|
2961
|
+
const footer = context?.executionStarted && context.isPartial ? toolFooter(theme, { status: "running", context }, config.icons) : void 0;
|
|
2962
|
+
const title = `${theme.fg("accent", writeIcon)} ${theme.bold ? theme.bold(theme.fg("toolTitle", "Write")) : theme.fg("toolTitle", "Write")} ${theme.fg("accent", icon2)} ${theme.fg("text", filePath)}`;
|
|
2963
|
+
if (hasToolResult(context)) {
|
|
2964
|
+
return renderFrameTop(theme, title, safeWidth, borderTone);
|
|
2965
|
+
}
|
|
2966
|
+
return [
|
|
2967
|
+
...renderFrameTop(theme, title, safeWidth, borderTone),
|
|
2968
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
2969
|
+
];
|
|
2970
|
+
});
|
|
2971
|
+
if (context?.state) {
|
|
2972
|
+
context.state.callComponent = comp;
|
|
2973
|
+
}
|
|
2974
|
+
return comp;
|
|
2975
|
+
},
|
|
2976
|
+
renderResult(result, options, theme, context) {
|
|
2977
|
+
if (context?.state) {
|
|
2978
|
+
context.state.hasResult = true;
|
|
2979
|
+
if (context.state.callComponent && typeof context.state.callComponent.invalidate === "function") {
|
|
2980
|
+
context.state.callComponent.invalidate();
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
if (context?.lastComponent && typeof context.lastComponent.invalidate === "function") {
|
|
2984
|
+
context.lastComponent.invalidate();
|
|
2985
|
+
}
|
|
2986
|
+
const isErr = Boolean(context?.isError);
|
|
2987
|
+
const status = options?.isPartial ? "running" : isErr ? "failed" : "completed";
|
|
2988
|
+
const footerOptions = {
|
|
2989
|
+
status,
|
|
2990
|
+
context: { ...context, isPartial: options?.isPartial }
|
|
2991
|
+
};
|
|
2992
|
+
syncElapsedTicker(footerOptions.context);
|
|
2993
|
+
const borderTone = getToolBorderTone({
|
|
2994
|
+
isError: isErr,
|
|
2995
|
+
isPartial: options?.isPartial
|
|
2996
|
+
});
|
|
2997
|
+
const textOutput = getResultText(result);
|
|
2998
|
+
if (isErr) {
|
|
2999
|
+
return createComponent((width) => {
|
|
3000
|
+
const safeWidth = Math.max(0, width);
|
|
3001
|
+
const errText = `${theme.fg("error", `${statusIcon("warning", config.icons)} `)}${theme.fg("error", textOutput || "Write failed")}`;
|
|
3002
|
+
return [
|
|
3003
|
+
...renderFrameRow(theme, errText, safeWidth, borderTone),
|
|
3004
|
+
...renderFrameBottom(
|
|
3005
|
+
theme,
|
|
3006
|
+
toolFooter(theme, footerOptions, config.icons),
|
|
3007
|
+
safeWidth,
|
|
3008
|
+
borderTone
|
|
3009
|
+
)
|
|
3010
|
+
];
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
const writeArgs = context?.args;
|
|
3014
|
+
const content = String(writeArgs?.content ?? "");
|
|
3015
|
+
const contentLines = content ? content.split("\n") : [];
|
|
3016
|
+
const lineCount = contentLines.length || 1;
|
|
3017
|
+
return createComponent((width) => {
|
|
3018
|
+
const safeWidth = Math.max(0, width);
|
|
3019
|
+
const addedStr = theme.fg("toolDiffAdded", `+${lineCount} lines`);
|
|
3020
|
+
const footer = toolFooter(
|
|
3021
|
+
theme,
|
|
3022
|
+
{
|
|
3023
|
+
...footerOptions,
|
|
3024
|
+
summary: [addedStr, "1 file"]
|
|
3025
|
+
},
|
|
3026
|
+
config.icons
|
|
3027
|
+
);
|
|
3028
|
+
if (contentLines.length === 0) {
|
|
3029
|
+
const defaultMsg = textOutput || "File written";
|
|
3030
|
+
return [
|
|
3031
|
+
...renderFrameRow(
|
|
3032
|
+
theme,
|
|
3033
|
+
theme.fg("toolOutput", defaultMsg),
|
|
3034
|
+
safeWidth,
|
|
3035
|
+
borderTone
|
|
3036
|
+
),
|
|
3037
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
3038
|
+
];
|
|
3039
|
+
}
|
|
3040
|
+
const visibleLines = options?.expanded ? contentLines : contentLines.slice(0, COLLAPSED_WRITE_LINES);
|
|
3041
|
+
const bodyLines = visibleLines.map(
|
|
3042
|
+
(l) => theme.fg("toolOutput", l)
|
|
3043
|
+
);
|
|
3044
|
+
if (!options?.expanded && contentLines.length > COLLAPSED_WRITE_LINES) {
|
|
3045
|
+
const remaining = contentLines.length - COLLAPSED_WRITE_LINES;
|
|
3046
|
+
bodyLines.push(
|
|
3047
|
+
theme.fg(
|
|
3048
|
+
"dim",
|
|
3049
|
+
`${icon("ellipsis", config.icons)} ${remaining} more lines ${icon("separator", config.icons)} ctrl+o to expand`
|
|
3050
|
+
)
|
|
3051
|
+
);
|
|
3052
|
+
}
|
|
3053
|
+
return [
|
|
3054
|
+
...bodyLines.flatMap(
|
|
3055
|
+
(l) => renderFrameRow(theme, l, safeWidth, borderTone)
|
|
3056
|
+
),
|
|
3057
|
+
...renderFrameBottom(theme, footer, safeWidth, borderTone)
|
|
3058
|
+
];
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
// src/tools/index.ts
|
|
3065
|
+
function createToolRendererResolver(pi, config, cwd = process.cwd()) {
|
|
3066
|
+
const renderers = /* @__PURE__ */ new Map([
|
|
3067
|
+
["read", createCustomReadTool(cwd, config)],
|
|
3068
|
+
["bash", createCustomBashTool(cwd, config)],
|
|
3069
|
+
["powershell", createCustomPowerShellTool(cwd, config)],
|
|
3070
|
+
["ls", createCustomLsTool(cwd, config)],
|
|
3071
|
+
["grep", createCustomGrepTool(cwd, config)],
|
|
3072
|
+
["find", createCustomFindTool(cwd, config)],
|
|
3073
|
+
["edit", createCustomEditTool(cwd, config)],
|
|
3074
|
+
["write", createCustomWriteTool(cwd, config)]
|
|
3075
|
+
]);
|
|
3076
|
+
const genericRenderers = /* @__PURE__ */ new Map();
|
|
3077
|
+
const isOwnedBaseDir = createOwnershipResolver(
|
|
3078
|
+
config.tools.respectPackages ?? DEFAULT_RESPECT_PACKAGES
|
|
3079
|
+
);
|
|
3080
|
+
const canRespectDownstream = (toolName) => {
|
|
3081
|
+
const tool = typeof pi.getAllTools === "function" ? pi.getAllTools().find((t) => t.name === toolName) : void 0;
|
|
3082
|
+
if (!tool) return true;
|
|
3083
|
+
const baseDir = tool.sourceInfo?.baseDir;
|
|
3084
|
+
return baseDir ? isOwnedBaseDir(baseDir) : false;
|
|
3085
|
+
};
|
|
3086
|
+
return (toolName, next) => {
|
|
3087
|
+
const builtIn = renderers.get(toolName);
|
|
3088
|
+
if (builtIn) return builtIn;
|
|
3089
|
+
const downstream = next();
|
|
3090
|
+
if ((downstream?.renderCall || downstream?.renderResult) && canRespectDownstream(toolName)) {
|
|
3091
|
+
return downstream;
|
|
3092
|
+
}
|
|
3093
|
+
let generic = genericRenderers.get(toolName);
|
|
3094
|
+
if (!generic) {
|
|
3095
|
+
generic = createGenericTool(toolName, config);
|
|
3096
|
+
genericRenderers.set(toolName, generic);
|
|
3097
|
+
}
|
|
3098
|
+
return generic;
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
function registerTools(pi, config, cwd = process.cwd(), owner = {}, resolveToolRenderers) {
|
|
3102
|
+
if (!config.tools.enabled || typeof pi?.registerToolRenderer !== "function") {
|
|
3103
|
+
return () => {
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
pi.registerToolRenderer(
|
|
3107
|
+
resolveToolRenderers ?? createToolRendererResolver(pi, config, cwd)
|
|
3108
|
+
);
|
|
3109
|
+
const unsubs = [];
|
|
3110
|
+
if (typeof pi?.on === "function") {
|
|
3111
|
+
unsubs.push(
|
|
3112
|
+
pi.on("agent_end", (_event, ctx) => {
|
|
3113
|
+
if (ctx.hasUI) stopElapsedTickers(owner);
|
|
3114
|
+
})
|
|
3115
|
+
);
|
|
3116
|
+
unsubs.push(
|
|
3117
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
3118
|
+
if (!ctx.hasUI) return;
|
|
3119
|
+
stopElapsedTickers(owner);
|
|
3120
|
+
releaseElapsedTickerOwner(owner);
|
|
3121
|
+
})
|
|
3122
|
+
);
|
|
3123
|
+
unsubs.push(
|
|
3124
|
+
pi.on("session_start", (_event, ctx) => {
|
|
3125
|
+
if (!ctx.hasUI) return;
|
|
3126
|
+
stopElapsedTickers(owner);
|
|
3127
|
+
activateElapsedTickerOwner(owner);
|
|
3128
|
+
})
|
|
3129
|
+
);
|
|
3130
|
+
}
|
|
3131
|
+
let disposed = false;
|
|
3132
|
+
return () => {
|
|
3133
|
+
if (disposed) return;
|
|
3134
|
+
disposed = true;
|
|
3135
|
+
stopElapsedTickers(owner);
|
|
3136
|
+
releaseElapsedTickerOwner(owner);
|
|
3137
|
+
for (const unsub of unsubs) unsub();
|
|
3138
|
+
unsubs.length = 0;
|
|
3139
|
+
};
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
// src/welcome/index.ts
|
|
3143
|
+
import { VERSION as PI_VERSION2 } from "@earendil-works/pi-coding-agent";
|
|
3144
|
+
|
|
3145
|
+
// src/welcome/render.ts
|
|
3146
|
+
import { truncateToWidth as truncateToWidth11, visibleWidth as visibleWidth7 } from "@earendil-works/pi-tui";
|
|
3147
|
+
|
|
3148
|
+
// src/welcome/logo.ts
|
|
3149
|
+
var THOTH_LOGO_NERD = [
|
|
3150
|
+
" \u2584\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2584 ",
|
|
3151
|
+
" \u2584\u2588\u2588\u2580\u2580 \u2580\u2580\u2588\u2588\u2584 ",
|
|
3152
|
+
" \u2588\u2588\u2588 \u2584\u2588\u2588\u2588\u2588\u2584 \u2588\u2588\u2588 ",
|
|
3153
|
+
" \u2590\u2588\u2588\u258C \u2590\u2588\u2588\u2588\u2588\u2588\u2588\u258C \u2590\u2588\u2588\u258C ",
|
|
3154
|
+
" \u2588\u2588\u2588 \u2580\u2588\u2588\u2588\u2588\u2580 \u2588\u2588\u2588 ",
|
|
3155
|
+
" \u2580\u2588\u2588\u2584\u2584 \u2580\u2580 \u2584\u2584\u2588\u2588\u2580 ",
|
|
3156
|
+
" \u2580\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2580\u2580 ",
|
|
3157
|
+
" \u2588\u2588 \u2580\u2584 ",
|
|
3158
|
+
" \u2588\u2588 \u2580\u2584 "
|
|
3159
|
+
];
|
|
3160
|
+
var THOTH_LOGO_ASCII = [
|
|
3161
|
+
" .-------. ",
|
|
3162
|
+
" / .---. \\ ",
|
|
3163
|
+
" ,-' ( (o) ) `-. ",
|
|
3164
|
+
" <____ `---' ____>",
|
|
3165
|
+
" `-------' ",
|
|
3166
|
+
" | \\ ",
|
|
3167
|
+
" | \\ "
|
|
3168
|
+
];
|
|
3169
|
+
function getThothLogoLines(mode) {
|
|
3170
|
+
return mode === "ascii" ? THOTH_LOGO_ASCII : THOTH_LOGO_NERD;
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
// src/welcome/render.ts
|
|
3174
|
+
function color(theme, token, text) {
|
|
3175
|
+
if (!text) return "";
|
|
3176
|
+
if (theme?.fg) {
|
|
3177
|
+
try {
|
|
3178
|
+
return theme.fg(token, text);
|
|
3179
|
+
} catch {
|
|
3180
|
+
return text;
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
return text;
|
|
3184
|
+
}
|
|
3185
|
+
function bold(theme, text) {
|
|
3186
|
+
if (!text) return "";
|
|
3187
|
+
if (theme?.bold) {
|
|
3188
|
+
try {
|
|
3189
|
+
return theme.bold(text);
|
|
3190
|
+
} catch {
|
|
3191
|
+
return text;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
return text;
|
|
3195
|
+
}
|
|
3196
|
+
function separator(mode) {
|
|
3197
|
+
const pad = mode === "ascii" ? " " : " ";
|
|
3198
|
+
return `${pad}${icon("separator", mode)}${pad}`;
|
|
3199
|
+
}
|
|
3200
|
+
function padEnd(text, width, ellipsis) {
|
|
3201
|
+
const current = visibleWidth7(text);
|
|
3202
|
+
if (current > width) return truncateToWidth11(text, width, ellipsis);
|
|
3203
|
+
return text + " ".repeat(Math.max(0, width - current));
|
|
3204
|
+
}
|
|
3205
|
+
function center(text, width, ellipsis) {
|
|
3206
|
+
const current = visibleWidth7(text);
|
|
3207
|
+
if (current > width) return truncateToWidth11(text, width, ellipsis);
|
|
3208
|
+
const left = Math.floor((width - current) / 2);
|
|
3209
|
+
const right = width - current - left;
|
|
3210
|
+
return " ".repeat(left) + text + " ".repeat(right);
|
|
3211
|
+
}
|
|
3212
|
+
function formatResourceSummary(theme, resources, sep2) {
|
|
3213
|
+
const parts = [];
|
|
3214
|
+
const cText = (s) => color(theme, "text", s);
|
|
3215
|
+
const cSuccess = (s) => color(theme, "success", s);
|
|
3216
|
+
const cDim = (s) => color(theme, "dim", s);
|
|
3217
|
+
if (typeof resources.tools === "number") {
|
|
3218
|
+
parts.push(cText("tools ") + cSuccess(String(resources.tools)));
|
|
3219
|
+
}
|
|
3220
|
+
if (typeof resources.skills === "number") {
|
|
3221
|
+
parts.push(cText("skills ") + cSuccess(String(resources.skills)));
|
|
3222
|
+
}
|
|
3223
|
+
if (typeof resources.extensions === "number") {
|
|
3224
|
+
parts.push(cText("exts ") + cSuccess(String(resources.extensions)));
|
|
3225
|
+
}
|
|
3226
|
+
return parts.join(cDim(sep2));
|
|
3227
|
+
}
|
|
3228
|
+
function buildTips(theme, mode) {
|
|
3229
|
+
const bullet = mode === "ascii" ? "*" : "\u2022";
|
|
3230
|
+
const sep2 = separator(mode);
|
|
3231
|
+
const cDim = (s) => color(theme, "dim", s);
|
|
3232
|
+
const cMuted = (s) => color(theme, "muted", s);
|
|
3233
|
+
return [
|
|
3234
|
+
` ${cDim(bullet)} ${cMuted("/ for commands")}${cDim(sep2)}${cMuted("! for bash")}`
|
|
3235
|
+
];
|
|
3236
|
+
}
|
|
3237
|
+
function buildProviderRows(theme, providers, mode, maxWidth) {
|
|
3238
|
+
const bullet = mode === "ascii" ? "*" : "\u25CF";
|
|
3239
|
+
const cSuccess = (s) => color(theme, "success", s);
|
|
3240
|
+
const cMuted = (s) => color(theme, "muted", s);
|
|
3241
|
+
const cDim = (s) => color(theme, "dim", s);
|
|
3242
|
+
if (providers.length === 0) {
|
|
3243
|
+
return [` ${cDim("No tool providers")}`];
|
|
3244
|
+
}
|
|
3245
|
+
return providers.slice(0, 4).map((p) => {
|
|
3246
|
+
const mark = cSuccess(bullet);
|
|
3247
|
+
const name = cMuted(p.name);
|
|
3248
|
+
const prefixWidth = visibleWidth7(` ${bullet} ${p.name}: `);
|
|
3249
|
+
const detailWidth = Math.max(1, maxWidth - prefixWidth);
|
|
3250
|
+
const detail = cDim(
|
|
3251
|
+
truncateToWidth11(p.detail, detailWidth, icon("ellipsis", mode))
|
|
3252
|
+
);
|
|
3253
|
+
return ` ${mark} ${name} ${detail}`;
|
|
3254
|
+
});
|
|
3255
|
+
}
|
|
3256
|
+
function buildSessionRows(theme, sessions, mode, maxWidth) {
|
|
3257
|
+
const bullet = mode === "ascii" ? "*" : "\u2022";
|
|
3258
|
+
const cDim = (s) => color(theme, "dim", s);
|
|
3259
|
+
const cMuted = (s) => color(theme, "muted", s);
|
|
3260
|
+
if (sessions.length === 0) {
|
|
3261
|
+
return [` ${cDim("No recent sessions")}`];
|
|
3262
|
+
}
|
|
3263
|
+
return sessions.slice(0, 4).map((s) => {
|
|
3264
|
+
const age = ` (${s.timeAgo})`;
|
|
3265
|
+
const ageWidth = visibleWidth7(age);
|
|
3266
|
+
const bulletPrefix = ` ${bullet} `;
|
|
3267
|
+
const maxNameWidth = Math.max(
|
|
3268
|
+
1,
|
|
3269
|
+
maxWidth - visibleWidth7(bulletPrefix) - ageWidth
|
|
3270
|
+
);
|
|
3271
|
+
const name = cMuted(
|
|
3272
|
+
truncateToWidth11(s.name, maxNameWidth, icon("ellipsis", mode))
|
|
3273
|
+
);
|
|
3274
|
+
return ` ${cDim(bullet)} ${name}${cDim(age)}`;
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
function renderWelcomeHeader(theme, data, width, mode = "nerd") {
|
|
3278
|
+
if (width <= 0) return [];
|
|
3279
|
+
const isAscii = mode === "ascii";
|
|
3280
|
+
const cAccent = (s) => color(theme, "accent", s);
|
|
3281
|
+
const cDim = (s) => color(theme, "dim", s);
|
|
3282
|
+
const cMuted = (s) => color(theme, "muted", s);
|
|
3283
|
+
const cText = (s) => color(theme, "text", s);
|
|
3284
|
+
const bTl = isAscii ? "+" : "\u256D";
|
|
3285
|
+
const bTr = isAscii ? "+" : "\u256E";
|
|
3286
|
+
const bBl = isAscii ? "+" : "\u2570";
|
|
3287
|
+
const bBr = isAscii ? "+" : "\u256F";
|
|
3288
|
+
const bV = isAscii ? "|" : "\u2502";
|
|
3289
|
+
const bH = isAscii ? "-" : "\u2500";
|
|
3290
|
+
const sep2 = separator(mode);
|
|
3291
|
+
const ellipsis = icon("ellipsis", mode);
|
|
3292
|
+
const mark = isAscii ? "*" : "\u25C6";
|
|
3293
|
+
if (width >= 64) {
|
|
3294
|
+
const boxWidth = width;
|
|
3295
|
+
const contentWidth = boxWidth - 3;
|
|
3296
|
+
const logoLines = getThothLogoLines(mode);
|
|
3297
|
+
const logoWidth = Math.max(...logoLines.map((l) => visibleWidth7(l)));
|
|
3298
|
+
const leftColumn = Math.max(logoWidth + 2, 24);
|
|
3299
|
+
const rightColumn = contentWidth - leftColumn;
|
|
3300
|
+
if (rightColumn >= 30) {
|
|
3301
|
+
const leftRows = [
|
|
3302
|
+
"",
|
|
3303
|
+
center(bold(theme, cAccent("T H O T H")), leftColumn, ellipsis),
|
|
3304
|
+
"",
|
|
3305
|
+
...logoLines.map((line) => center(cAccent(line), leftColumn, ellipsis)),
|
|
3306
|
+
"",
|
|
3307
|
+
center(cText(data.model || "pi-coding-agent"), leftColumn, ellipsis),
|
|
3308
|
+
center(cDim(data.provider || ""), leftColumn, ellipsis)
|
|
3309
|
+
];
|
|
3310
|
+
const resourceSummary = formatResourceSummary(theme, data.resources, sep2);
|
|
3311
|
+
const rightDivider = ` ${cDim(bH.repeat(Math.max(1, rightColumn - 2)))}`;
|
|
3312
|
+
const resourceHeader = resourceSummary ? ` ${cAccent(mark)} ${bold(theme, cAccent("Resources"))} ${resourceSummary}` : ` ${cAccent(mark)} ${bold(theme, cAccent("Resources"))}`;
|
|
3313
|
+
const rightRows = [
|
|
3314
|
+
resourceHeader,
|
|
3315
|
+
rightDivider,
|
|
3316
|
+
` ${cAccent(mark)} ${bold(theme, cAccent("Tool Providers"))}`,
|
|
3317
|
+
...buildProviderRows(theme, data.providers, mode, rightColumn),
|
|
3318
|
+
rightDivider,
|
|
3319
|
+
` ${cAccent(mark)} ${bold(theme, cAccent("Recent Sessions"))}`,
|
|
3320
|
+
...buildSessionRows(theme, data.sessions, mode, rightColumn),
|
|
3321
|
+
rightDivider,
|
|
3322
|
+
...buildTips(theme, mode)
|
|
3323
|
+
];
|
|
3324
|
+
const maxRows = Math.max(leftRows.length, rightRows.length);
|
|
3325
|
+
while (leftRows.length < maxRows) leftRows.push("");
|
|
3326
|
+
while (rightRows.length < maxRows) rightRows.push("");
|
|
3327
|
+
const label = ` v${data.version} `;
|
|
3328
|
+
const lead = bH.repeat(3);
|
|
3329
|
+
const remainingFill = Math.max(
|
|
3330
|
+
0,
|
|
3331
|
+
boxWidth - 2 - visibleWidth7(lead) - visibleWidth7(label)
|
|
3332
|
+
);
|
|
3333
|
+
const topBorder = `${cDim(bTl)}${cDim(lead)}${cAccent(label)}${cDim(bH.repeat(remainingFill))}${cDim(bTr)}`;
|
|
3334
|
+
const bottomBorder = `${cDim(bBl)}${cDim(bH.repeat(boxWidth - 2))}${cDim(bBr)}`;
|
|
3335
|
+
const out = [topBorder];
|
|
3336
|
+
for (let i = 0; i < maxRows; i++) {
|
|
3337
|
+
const leftCell = padEnd(leftRows[i] ?? "", leftColumn, ellipsis);
|
|
3338
|
+
const rightCell = padEnd(rightRows[i] ?? "", rightColumn, ellipsis);
|
|
3339
|
+
out.push(`${cDim(bV)}${leftCell}${cDim(bV)}${rightCell}${cDim(bV)}`);
|
|
3340
|
+
}
|
|
3341
|
+
out.push(bottomBorder);
|
|
3342
|
+
return out.map(
|
|
3343
|
+
(line) => visibleWidth7(line) <= width ? line : truncateToWidth11(line, width, ellipsis)
|
|
3344
|
+
);
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
if (width >= 42) {
|
|
3348
|
+
const boxWidth = width;
|
|
3349
|
+
const innerWidth = boxWidth - 2;
|
|
3350
|
+
const label = ` Thoth v${data.version} `;
|
|
3351
|
+
const lead = bH.repeat(2);
|
|
3352
|
+
const remainingFill = Math.max(
|
|
3353
|
+
0,
|
|
3354
|
+
boxWidth - 2 - visibleWidth7(lead) - visibleWidth7(label)
|
|
3355
|
+
);
|
|
3356
|
+
const topBorder = `${cDim(bTl)}${cDim(lead)}${cAccent(label)}${cDim(bH.repeat(remainingFill))}${cDim(bTr)}`;
|
|
3357
|
+
const bottomBorder = `${cDim(bBl)}${cDim(bH.repeat(boxWidth - 2))}${cDim(bBr)}`;
|
|
3358
|
+
const divider = `${cDim(bV)} ${cDim(bH.repeat(Math.max(1, innerWidth - 2)))} ${cDim(bV)}`;
|
|
3359
|
+
const resourceSummary = formatResourceSummary(theme, data.resources, sep2);
|
|
3360
|
+
const content = [
|
|
3361
|
+
center(bold(theme, cAccent("T H O T H")), innerWidth, ellipsis),
|
|
3362
|
+
center(cText(data.model || "pi-coding-agent"), innerWidth, ellipsis),
|
|
3363
|
+
divider,
|
|
3364
|
+
...resourceSummary ? [` ${cAccent(mark)} ${resourceSummary}`, divider] : [],
|
|
3365
|
+
` ${cAccent(mark)} ${bold(theme, cAccent("Recent Sessions"))}`,
|
|
3366
|
+
...buildSessionRows(theme, data.sessions, mode, innerWidth)
|
|
3367
|
+
];
|
|
3368
|
+
const out = [topBorder];
|
|
3369
|
+
for (const row of content) {
|
|
3370
|
+
if (row === divider) {
|
|
3371
|
+
out.push(divider);
|
|
3372
|
+
} else {
|
|
3373
|
+
out.push(`${cDim(bV)}${padEnd(row, innerWidth, ellipsis)}${cDim(bV)}`);
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
out.push(bottomBorder);
|
|
3377
|
+
return out.map(
|
|
3378
|
+
(line) => visibleWidth7(line) <= width ? line : truncateToWidth11(line, width, ellipsis)
|
|
3379
|
+
);
|
|
3380
|
+
}
|
|
3381
|
+
const lines = [
|
|
3382
|
+
cAccent(bold(theme, `Thoth v${data.version}`)),
|
|
3383
|
+
cText(truncateToWidth11(data.model || "ready", width, ellipsis))
|
|
3384
|
+
];
|
|
3385
|
+
const narrowParts = [];
|
|
3386
|
+
if (typeof data.resources.tools === "number") {
|
|
3387
|
+
narrowParts.push(`tools:${data.resources.tools}`);
|
|
3388
|
+
}
|
|
3389
|
+
if (typeof data.resources.skills === "number") {
|
|
3390
|
+
narrowParts.push(`skills:${data.resources.skills}`);
|
|
3391
|
+
}
|
|
3392
|
+
if (typeof data.resources.extensions === "number") {
|
|
3393
|
+
narrowParts.push(`exts:${data.resources.extensions}`);
|
|
3394
|
+
}
|
|
3395
|
+
if (narrowParts.length > 0) {
|
|
3396
|
+
lines.push(cDim(narrowParts.join(" ")));
|
|
3397
|
+
}
|
|
3398
|
+
if (data.sessions.length > 0) {
|
|
3399
|
+
const s = data.sessions[0];
|
|
3400
|
+
if (s) {
|
|
3401
|
+
lines.push(cMuted(truncateToWidth11(`* ${s.name}`, width, ellipsis)));
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
return lines.map(
|
|
3405
|
+
(line) => visibleWidth7(line) <= width ? line : truncateToWidth11(line, width, ellipsis)
|
|
3406
|
+
);
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
// src/welcome/resources.ts
|
|
3410
|
+
import {
|
|
3411
|
+
VERSION as PI_VERSION,
|
|
3412
|
+
SessionManager
|
|
3413
|
+
} from "@earendil-works/pi-coding-agent";
|
|
3414
|
+
|
|
3415
|
+
// src/welcome/age.ts
|
|
3416
|
+
function formatAge(fromMs, nowMs = Date.now()) {
|
|
3417
|
+
const seconds = Math.max(0, Math.round((nowMs - fromMs) / 1e3));
|
|
3418
|
+
if (seconds < 60) return "just now";
|
|
3419
|
+
const minutes = Math.round(seconds / 60);
|
|
3420
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
3421
|
+
const hours = Math.round(minutes / 60);
|
|
3422
|
+
if (hours < 24) return `${hours}h ago`;
|
|
3423
|
+
const days = Math.round(hours / 24);
|
|
3424
|
+
if (days < 30) return `${days}d ago`;
|
|
3425
|
+
return `${Math.round(days / 30)}mo ago`;
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
// src/welcome/resources.ts
|
|
3429
|
+
function toolSourceLabel(sourceInfo) {
|
|
3430
|
+
if (!sourceInfo || typeof sourceInfo !== "object") return "core";
|
|
3431
|
+
const source = typeof sourceInfo.source === "string" ? sourceInfo.source : "";
|
|
3432
|
+
if (source === "builtin" || source === "core") return "core";
|
|
3433
|
+
if (source === "sdk") return "sdk";
|
|
3434
|
+
if (source.startsWith("npm:")) return source.slice("npm:".length) || source;
|
|
3435
|
+
if (source.startsWith("git:")) return source.replace(/\.git(?:#.*)?$/i, "");
|
|
3436
|
+
const baseDir = typeof sourceInfo.baseDir === "string" ? sourceInfo.baseDir : "";
|
|
3437
|
+
if (baseDir) {
|
|
3438
|
+
const parts = baseDir.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
3439
|
+
return parts[parts.length - 1] || "extension";
|
|
3440
|
+
}
|
|
3441
|
+
const path = typeof sourceInfo.path === "string" ? sourceInfo.path : "";
|
|
3442
|
+
if (path) {
|
|
3443
|
+
const parts = path.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
3444
|
+
const last = parts[parts.length - 1] || "extension";
|
|
3445
|
+
return last.replace(/\.(?:ts|js|mjs|cjs)$/i, "");
|
|
3446
|
+
}
|
|
3447
|
+
return source || "extension";
|
|
3448
|
+
}
|
|
3449
|
+
function groupToolDetails(tools) {
|
|
3450
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3451
|
+
for (const tool of tools) {
|
|
3452
|
+
if (!tool || typeof tool.name !== "string") continue;
|
|
3453
|
+
const name = tool.name.trim();
|
|
3454
|
+
if (!name) continue;
|
|
3455
|
+
const label = toolSourceLabel(tool.sourceInfo);
|
|
3456
|
+
let list = groups.get(label);
|
|
3457
|
+
if (!list) {
|
|
3458
|
+
list = [];
|
|
3459
|
+
groups.set(label, list);
|
|
3460
|
+
}
|
|
3461
|
+
list.push(name);
|
|
3462
|
+
}
|
|
3463
|
+
const result = [];
|
|
3464
|
+
const sortedKeys = Array.from(groups.keys()).sort((a, b) => {
|
|
3465
|
+
if (a === "core") return -1;
|
|
3466
|
+
if (b === "core") return 1;
|
|
3467
|
+
return a.localeCompare(b);
|
|
3468
|
+
});
|
|
3469
|
+
for (const key of sortedKeys) {
|
|
3470
|
+
const toolNames = (groups.get(key) || []).sort();
|
|
3471
|
+
const sample = toolNames.slice(0, 3).join(" ");
|
|
3472
|
+
const remaining = toolNames.length > 3 ? ` +${toolNames.length - 3}` : "";
|
|
3473
|
+
result.push({
|
|
3474
|
+
name: key,
|
|
3475
|
+
detail: `${sample}${remaining}`
|
|
3476
|
+
});
|
|
3477
|
+
}
|
|
3478
|
+
return result.slice(0, 4);
|
|
3479
|
+
}
|
|
3480
|
+
async function fetchRecentSessions(cwd, limit = 4, mode = "nerd") {
|
|
3481
|
+
try {
|
|
3482
|
+
const sessions = await SessionManager.list(cwd);
|
|
3483
|
+
const sorted = [...sessions].sort(
|
|
3484
|
+
(a, b) => (b.modified?.getTime?.() ?? 0) - (a.modified?.getTime?.() ?? 0)
|
|
3485
|
+
);
|
|
3486
|
+
const out = [];
|
|
3487
|
+
for (const session of sorted) {
|
|
3488
|
+
if (out.length >= limit) break;
|
|
3489
|
+
const rawTitle = session.name?.trim() || session.firstMessage?.trim() || "Untitled session";
|
|
3490
|
+
const cleanTitle = Array.from(rawTitle).map((c) => {
|
|
3491
|
+
const code = c.charCodeAt(0);
|
|
3492
|
+
return code < 32 || code === 127 ? " " : c;
|
|
3493
|
+
}).join("").replace(/\s+/g, " ").trim();
|
|
3494
|
+
const title = cleanTitle.length > 45 ? `${cleanTitle.slice(0, 44)}${icon("ellipsis", mode)}` : cleanTitle;
|
|
3495
|
+
const timeAgo = formatAge(
|
|
3496
|
+
session.modified ? session.modified.getTime() : Date.now()
|
|
3497
|
+
);
|
|
3498
|
+
out.push({ name: title, timeAgo });
|
|
3499
|
+
}
|
|
3500
|
+
return out;
|
|
3501
|
+
} catch {
|
|
3502
|
+
return [];
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
async function collectStartupResources(pi, ctx, options) {
|
|
3506
|
+
let tools = [];
|
|
3507
|
+
let toolsCount;
|
|
3508
|
+
if (typeof pi.getAllTools === "function") {
|
|
3509
|
+
try {
|
|
3510
|
+
tools = pi.getAllTools() ?? [];
|
|
3511
|
+
toolsCount = tools.length;
|
|
3512
|
+
} catch {
|
|
3513
|
+
}
|
|
3514
|
+
} else if (typeof pi.getActiveTools === "function") {
|
|
3515
|
+
try {
|
|
3516
|
+
const active = pi.getActiveTools() ?? [];
|
|
3517
|
+
toolsCount = active.length;
|
|
3518
|
+
} catch {
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
const providers = groupToolDetails(tools);
|
|
3522
|
+
const loader = options?.resourceLoader ?? ctx?.resourceLoader ?? pi?.resourceLoader ?? ctx?.session?.resourceLoader;
|
|
3523
|
+
let skillsCount;
|
|
3524
|
+
if (options?.skillCountLister) {
|
|
3525
|
+
skillsCount = options.skillCountLister(ctx.cwd);
|
|
3526
|
+
} else if (loader?.getSkills) {
|
|
3527
|
+
try {
|
|
3528
|
+
const skillsRes = loader.getSkills();
|
|
3529
|
+
if (skillsRes && Array.isArray(skillsRes.skills)) {
|
|
3530
|
+
skillsCount = skillsRes.skills.length;
|
|
3531
|
+
}
|
|
3532
|
+
} catch {
|
|
3533
|
+
}
|
|
3534
|
+
} else if (typeof pi.getCommands === "function") {
|
|
3535
|
+
try {
|
|
3536
|
+
const commands = pi.getCommands();
|
|
3537
|
+
if (Array.isArray(commands)) {
|
|
3538
|
+
const skillCommands = commands.filter(
|
|
3539
|
+
(c) => c?.source === "skill" || typeof c?.name === "string" && c.name.startsWith("skill:")
|
|
3540
|
+
);
|
|
3541
|
+
skillsCount = skillCommands.length;
|
|
3542
|
+
}
|
|
3543
|
+
} catch {
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
let extensionsCount;
|
|
3547
|
+
if (options?.extensionCountLister) {
|
|
3548
|
+
extensionsCount = options.extensionCountLister(ctx.cwd);
|
|
3549
|
+
} else if (loader?.getExtensions) {
|
|
3550
|
+
try {
|
|
3551
|
+
const extRes = loader.getExtensions();
|
|
3552
|
+
if (extRes && Array.isArray(extRes.extensions)) {
|
|
3553
|
+
extensionsCount = extRes.extensions.length;
|
|
3554
|
+
}
|
|
3555
|
+
} catch {
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
let sessions = [];
|
|
3559
|
+
if (options?.sessionLister) {
|
|
3560
|
+
sessions = await options.sessionLister(ctx.cwd);
|
|
3561
|
+
} else {
|
|
3562
|
+
sessions = await fetchRecentSessions(ctx.cwd, 4, options?.iconMode);
|
|
3563
|
+
}
|
|
3564
|
+
const modelName = ctx.model?.name || ctx.model?.id;
|
|
3565
|
+
const provider = ctx.model?.provider;
|
|
3566
|
+
return {
|
|
3567
|
+
version: PI_VERSION,
|
|
3568
|
+
model: modelName,
|
|
3569
|
+
provider,
|
|
3570
|
+
resources: {
|
|
3571
|
+
tools: toolsCount,
|
|
3572
|
+
skills: skillsCount,
|
|
3573
|
+
extensions: extensionsCount
|
|
3574
|
+
},
|
|
3575
|
+
providers,
|
|
3576
|
+
sessions
|
|
3577
|
+
};
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3580
|
+
// src/welcome/index.ts
|
|
3581
|
+
var WelcomeComponent = class {
|
|
3582
|
+
data;
|
|
3583
|
+
renderCache;
|
|
3584
|
+
_disposed = false;
|
|
3585
|
+
constructor(theme, config, initialData) {
|
|
3586
|
+
this.data = initialData;
|
|
3587
|
+
this.renderCache = cachedComponent(
|
|
3588
|
+
(width) => renderWelcomeHeader(theme, this.data, width, config.icons)
|
|
3589
|
+
);
|
|
3590
|
+
}
|
|
3591
|
+
get isDisposed() {
|
|
3592
|
+
return this._disposed;
|
|
3593
|
+
}
|
|
3594
|
+
updateData(newData) {
|
|
3595
|
+
if (this._disposed) return;
|
|
3596
|
+
this.data = newData;
|
|
3597
|
+
this.invalidate();
|
|
3598
|
+
}
|
|
3599
|
+
invalidate() {
|
|
3600
|
+
this.renderCache.invalidate();
|
|
3601
|
+
}
|
|
3602
|
+
dispose() {
|
|
3603
|
+
this._disposed = true;
|
|
3604
|
+
}
|
|
3605
|
+
render(width) {
|
|
3606
|
+
return this.renderCache.render(width);
|
|
3607
|
+
}
|
|
3608
|
+
};
|
|
3609
|
+
function registerWelcome(pi, ctx, config, options) {
|
|
3610
|
+
if (!ctx.hasUI || !ctx.ui?.setHeader) return;
|
|
3611
|
+
const initialData = {
|
|
3612
|
+
version: PI_VERSION2,
|
|
3613
|
+
model: ctx.model?.name || ctx.model?.id,
|
|
3614
|
+
provider: ctx.model?.provider,
|
|
3615
|
+
resources: {
|
|
3616
|
+
tools: typeof pi.getAllTools === "function" ? pi.getAllTools()?.length ?? 0 : typeof pi.getActiveTools === "function" ? pi.getActiveTools()?.length ?? 0 : void 0,
|
|
3617
|
+
skills: typeof pi.getCommands === "function" ? pi.getCommands()?.filter(
|
|
3618
|
+
(c) => c?.source === "skill" || typeof c?.name === "string" && c.name.startsWith("skill:")
|
|
3619
|
+
)?.length ?? void 0 : void 0,
|
|
3620
|
+
extensions: void 0
|
|
3621
|
+
},
|
|
3622
|
+
providers: [],
|
|
3623
|
+
sessions: []
|
|
3624
|
+
};
|
|
3625
|
+
let activeComponent;
|
|
3626
|
+
ctx.ui.setHeader((tui, theme) => {
|
|
3627
|
+
if (activeComponent && !activeComponent.isDisposed) {
|
|
3628
|
+
activeComponent.dispose();
|
|
3629
|
+
}
|
|
3630
|
+
const activeTheme = theme;
|
|
3631
|
+
const component = new WelcomeComponent(activeTheme, config, initialData);
|
|
3632
|
+
activeComponent = component;
|
|
3633
|
+
void collectStartupResources(pi, ctx, {
|
|
3634
|
+
...options,
|
|
3635
|
+
iconMode: options?.iconMode ?? config.icons
|
|
3636
|
+
}).then((fullData) => {
|
|
3637
|
+
if (!component.isDisposed && activeComponent === component) {
|
|
3638
|
+
component.updateData(fullData);
|
|
3639
|
+
tui?.requestRender?.();
|
|
3640
|
+
}
|
|
3641
|
+
}).catch(() => {
|
|
3642
|
+
});
|
|
3643
|
+
return component;
|
|
3644
|
+
});
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
// src/index.ts
|
|
3648
|
+
function thothTheme(pi) {
|
|
3649
|
+
const config = loadConfig();
|
|
3650
|
+
const owner = {};
|
|
3651
|
+
const cwd = process.cwd();
|
|
3652
|
+
const resolveToolRenderers = config.tools.enabled ? createToolRendererResolver(pi, config, cwd) : void 0;
|
|
3653
|
+
let kitToken;
|
|
3654
|
+
if (resolveToolRenderers) {
|
|
3655
|
+
registerTools(pi, config, cwd, owner, resolveToolRenderers);
|
|
3656
|
+
}
|
|
3657
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
3658
|
+
if (!ctx.hasUI || kitToken === void 0) return;
|
|
3659
|
+
withdrawRenderKit(kitToken);
|
|
3660
|
+
kitToken = void 0;
|
|
3661
|
+
});
|
|
3662
|
+
pi.on("session_start", (_event, ctx) => {
|
|
3663
|
+
if (!ctx.hasUI) return;
|
|
3664
|
+
if (config.tools.enabled) {
|
|
3665
|
+
kitToken = registerRenderKit(
|
|
3666
|
+
createRenderKit(owner, resolveToolRenderers, config.icons),
|
|
3667
|
+
owner
|
|
3668
|
+
);
|
|
3669
|
+
}
|
|
3670
|
+
if (!ctx.ui) return;
|
|
3671
|
+
if (config.statusLine.enabled) {
|
|
3672
|
+
if (config.inputBox?.enabled !== false) {
|
|
3673
|
+
ctx.ui.setWorkingIndicator?.({
|
|
3674
|
+
frames: [...frames("workingFrames", config.icons)],
|
|
3675
|
+
intervalMs: 200
|
|
3676
|
+
});
|
|
3677
|
+
ctx.ui.setWorkingMessage?.(`working${icon("ellipsis", config.icons)}`);
|
|
3678
|
+
}
|
|
3679
|
+
registerStatusLine(pi, ctx, config);
|
|
3680
|
+
}
|
|
3681
|
+
if (config.welcome.enabled) registerWelcome(pi, ctx, config);
|
|
3682
|
+
});
|
|
3683
|
+
}
|
|
3684
|
+
export {
|
|
3685
|
+
thothTheme as default
|
|
3686
|
+
};
|