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