pi-gentic 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +499 -0
- package/dist/commands.d.ts +93 -0
- package/dist/commands.js +422 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.js +407 -0
- package/dist/core.d.ts +19 -0
- package/dist/core.js +125 -0
- package/dist/extension.d.ts +2 -0
- package/dist/extension.js +476 -0
- package/dist/orchestrator.d.ts +277 -0
- package/dist/orchestrator.js +633 -0
- package/dist/policy.d.ts +43 -0
- package/dist/policy.js +136 -0
- package/dist/prompt.d.ts +12 -0
- package/dist/prompt.js +226 -0
- package/dist/runs.d.ts +99 -0
- package/dist/runs.js +540 -0
- package/dist/runtime.d.ts +127 -0
- package/dist/runtime.js +360 -0
- package/dist/sessions.d.ts +56 -0
- package/dist/sessions.js +487 -0
- package/dist/ui.d.ts +108 -0
- package/dist/ui.js +957 -0
- package/dist/worktrees.d.ts +1 -0
- package/dist/worktrees.js +86 -0
- package/docs/assets/error-card.png +0 -0
- package/docs/assets/load-agent.png +0 -0
- package/docs/assets/orchestration-tree.png +0 -0
- package/docs/assets/send-background.png +0 -0
- package/docs/assets/send-foreground.png +0 -0
- package/package.json +58 -0
package/dist/ui.js
ADDED
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal UI rendering for pi-gentic.
|
|
3
|
+
*
|
|
4
|
+
* Cards and tree pickers render in plain terminal cells, so width calculations
|
|
5
|
+
* must account for ANSI colors, emoji, combining marks, and wide characters.
|
|
6
|
+
*/
|
|
7
|
+
import { formatDuration, isRecord, shortSessionId } from "./core.js";
|
|
8
|
+
const RUNNING_CARD_TTL_MS = 10 * 60_000;
|
|
9
|
+
const COMPLETED_CARD_TTL_MS = 60_000;
|
|
10
|
+
const liveCards = new Map();
|
|
11
|
+
export function liveCardKey(details) {
|
|
12
|
+
if (!details || typeof details !== "object")
|
|
13
|
+
return undefined;
|
|
14
|
+
return details.cardId ?? details.sessionId;
|
|
15
|
+
}
|
|
16
|
+
export function setLiveCardDetails(details, options = {}) {
|
|
17
|
+
const key = liveCardKey(details);
|
|
18
|
+
if (!key)
|
|
19
|
+
return undefined;
|
|
20
|
+
const existing = liveCards.get(key);
|
|
21
|
+
if (existing?.timer)
|
|
22
|
+
clearTimeout(existing.timer);
|
|
23
|
+
const nextDetails = { ...(existing?.details ?? {}), ...details };
|
|
24
|
+
const ttlMs = Math.max(100, Number(options.ttlMs ?? defaultTtl(nextDetails)));
|
|
25
|
+
const timer = setTimeout(() => liveCards.delete(key), ttlMs);
|
|
26
|
+
timer.unref?.();
|
|
27
|
+
liveCards.set(key, { details: nextDetails, timer });
|
|
28
|
+
return nextDetails;
|
|
29
|
+
}
|
|
30
|
+
export function getLiveCardDetails(details) {
|
|
31
|
+
const key = liveCardKey(details);
|
|
32
|
+
return key ? liveCards.get(key)?.details : undefined;
|
|
33
|
+
}
|
|
34
|
+
export function clearLiveCardDetails(details) {
|
|
35
|
+
const key = liveCardKey(details);
|
|
36
|
+
const entry = key ? liveCards.get(key) : undefined;
|
|
37
|
+
if (entry?.timer)
|
|
38
|
+
clearTimeout(entry.timer);
|
|
39
|
+
if (key)
|
|
40
|
+
liveCards.delete(key);
|
|
41
|
+
}
|
|
42
|
+
function defaultTtl(details) {
|
|
43
|
+
return details.completedAt ||
|
|
44
|
+
["done", "error", "aborted", "stopped"].includes(details.status)
|
|
45
|
+
? COMPLETED_CARD_TTL_MS
|
|
46
|
+
: RUNNING_CARD_TTL_MS;
|
|
47
|
+
}
|
|
48
|
+
const COMBINING_MARK = /\p{Mark}/u;
|
|
49
|
+
const EMOJI_MODIFIER = /\p{Emoji_Modifier}/u;
|
|
50
|
+
const EMOJI_PRESENTATION = /\p{Emoji_Presentation}/u;
|
|
51
|
+
const EXTENDED_PICTOGRAPHIC = /\p{Extended_Pictographic}/u;
|
|
52
|
+
const REGIONAL_INDICATOR_START = 0x1f1e6;
|
|
53
|
+
const REGIONAL_INDICATOR_END = 0x1f1ff;
|
|
54
|
+
export function center(text, width) {
|
|
55
|
+
const padding = Math.max(0, Math.floor((width - visibleLength(text)) / 2));
|
|
56
|
+
return fit(`${" ".repeat(padding)}${text}`, width);
|
|
57
|
+
}
|
|
58
|
+
export function joinWithRight(left, right, width) {
|
|
59
|
+
if (!right)
|
|
60
|
+
return fit(left, width);
|
|
61
|
+
const rightWidth = visibleLength(right);
|
|
62
|
+
const leftWidth = Math.max(0, width - rightWidth - 1);
|
|
63
|
+
const fittedLeft = fit(left, leftWidth);
|
|
64
|
+
return `${fittedLeft}${" ".repeat(Math.max(1, width - visibleLength(fittedLeft) - rightWidth))}${right}`;
|
|
65
|
+
}
|
|
66
|
+
export function joinWithMiddle(left, middle, right, width) {
|
|
67
|
+
const rightWidth = visibleLength(right);
|
|
68
|
+
const leftAreaWidth = Math.max(0, width - rightWidth - 1);
|
|
69
|
+
const middleWidth = Math.max(0, leftAreaWidth - visibleLength(left));
|
|
70
|
+
const fittedLeft = middleWidth > 0
|
|
71
|
+
? `${left}${fit(middle, middleWidth)}`
|
|
72
|
+
: fit(left, leftAreaWidth);
|
|
73
|
+
return `${fittedLeft}${" ".repeat(Math.max(1, width - visibleLength(fittedLeft) - rightWidth))}${right}`;
|
|
74
|
+
}
|
|
75
|
+
export function normalizeInline(text) {
|
|
76
|
+
return String(text ?? "")
|
|
77
|
+
.replace(/\s+/g, " ")
|
|
78
|
+
.trim();
|
|
79
|
+
}
|
|
80
|
+
export function wrap(text, width) {
|
|
81
|
+
const clean = String(text ?? "");
|
|
82
|
+
if (!clean)
|
|
83
|
+
return [];
|
|
84
|
+
const lines = [];
|
|
85
|
+
for (const rawLine of clean.split(/\r?\n/)) {
|
|
86
|
+
if (!rawLine) {
|
|
87
|
+
lines.push("");
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
let line = rawLine;
|
|
91
|
+
while (line.length > 0) {
|
|
92
|
+
const chunk = takeVisiblePrefix(line, width);
|
|
93
|
+
if (!chunk.text || chunk.end >= line.length) {
|
|
94
|
+
lines.push(line);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
lines.push(chunk.text);
|
|
98
|
+
line = line.slice(chunk.end);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return lines;
|
|
102
|
+
}
|
|
103
|
+
export function fit(text, width) {
|
|
104
|
+
if (width <= 0)
|
|
105
|
+
return "";
|
|
106
|
+
const value = String(text ?? "");
|
|
107
|
+
const fitted = takeVisiblePrefix(value, width);
|
|
108
|
+
if (fitted.end >= value.length)
|
|
109
|
+
return value + " ".repeat(width - fitted.width);
|
|
110
|
+
return `${takeVisiblePrefix(value, Math.max(0, width - 1), true).text}…`;
|
|
111
|
+
}
|
|
112
|
+
/** Measures terminal cell width after stripping ANSI control sequences. */
|
|
113
|
+
export function visibleLength(text) {
|
|
114
|
+
const value = String(text ?? "");
|
|
115
|
+
let width = 0;
|
|
116
|
+
let index = 0;
|
|
117
|
+
while (index < value.length) {
|
|
118
|
+
const unit = readDisplayUnit(value, index, width);
|
|
119
|
+
width += unit.width;
|
|
120
|
+
index = unit.end;
|
|
121
|
+
}
|
|
122
|
+
return width;
|
|
123
|
+
}
|
|
124
|
+
function takeVisiblePrefix(text, maxWidth, closeAnsi = false) {
|
|
125
|
+
const value = String(text ?? "");
|
|
126
|
+
let output = "";
|
|
127
|
+
let width = 0;
|
|
128
|
+
let index = 0;
|
|
129
|
+
let sawAnsi = false;
|
|
130
|
+
while (index < value.length) {
|
|
131
|
+
const unit = readDisplayUnit(value, index, width);
|
|
132
|
+
if (unit.control) {
|
|
133
|
+
output += value.slice(index, unit.end);
|
|
134
|
+
sawAnsi = true;
|
|
135
|
+
index = unit.end;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (width >= maxWidth || width + unit.width > maxWidth)
|
|
139
|
+
break;
|
|
140
|
+
output += value.slice(index, unit.end);
|
|
141
|
+
width += unit.width;
|
|
142
|
+
index = unit.end;
|
|
143
|
+
}
|
|
144
|
+
while (index < value.length) {
|
|
145
|
+
const sequence = controlSequenceAt(value, index);
|
|
146
|
+
if (!sequence)
|
|
147
|
+
break;
|
|
148
|
+
output += sequence;
|
|
149
|
+
sawAnsi = true;
|
|
150
|
+
index += sequence.length;
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
text: closeAnsi && sawAnsi ? `${output}\x1b[0m` : output,
|
|
154
|
+
width,
|
|
155
|
+
end: index,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function readDisplayUnit(text, index, column) {
|
|
159
|
+
const sequence = controlSequenceAt(text, index);
|
|
160
|
+
if (sequence)
|
|
161
|
+
return { end: index + sequence.length, width: 0, control: true };
|
|
162
|
+
const codePoint = text.codePointAt(index);
|
|
163
|
+
if (codePoint === undefined)
|
|
164
|
+
return { end: index + 1, width: 0, control: false };
|
|
165
|
+
let end = index + codePointSize(codePoint);
|
|
166
|
+
if (codePoint === 9)
|
|
167
|
+
return { end, width: 4 - (column % 4), control: false };
|
|
168
|
+
if (isControlCodePoint(codePoint))
|
|
169
|
+
return { end, width: 0, control: false };
|
|
170
|
+
if (isRegionalIndicator(codePoint)) {
|
|
171
|
+
const next = text.codePointAt(end);
|
|
172
|
+
if (next !== undefined && isRegionalIndicator(next))
|
|
173
|
+
end += codePointSize(next);
|
|
174
|
+
return { end, width: 2, control: false };
|
|
175
|
+
}
|
|
176
|
+
const keycapBase = isKeycapBase(codePoint);
|
|
177
|
+
let width = baseDisplayWidth(codePoint, text.codePointAt(end));
|
|
178
|
+
while (end < text.length) {
|
|
179
|
+
const next = text.codePointAt(end);
|
|
180
|
+
if (next === undefined)
|
|
181
|
+
break;
|
|
182
|
+
const nextSize = codePointSize(next);
|
|
183
|
+
if (isVariationSelector(next) ||
|
|
184
|
+
isCombiningCodePoint(next) ||
|
|
185
|
+
isEmojiModifierCodePoint(next)) {
|
|
186
|
+
end += nextSize;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (keycapBase && next === 0x20e3) {
|
|
190
|
+
end += nextSize;
|
|
191
|
+
width = 2;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (next !== 0x200d)
|
|
195
|
+
break;
|
|
196
|
+
end += nextSize;
|
|
197
|
+
const joined = text.codePointAt(end);
|
|
198
|
+
if (joined === undefined)
|
|
199
|
+
break;
|
|
200
|
+
end += codePointSize(joined);
|
|
201
|
+
width = 2;
|
|
202
|
+
}
|
|
203
|
+
return { end, width, control: false };
|
|
204
|
+
}
|
|
205
|
+
function controlSequenceAt(text, index) {
|
|
206
|
+
if (text[index] !== "\x1b")
|
|
207
|
+
return "";
|
|
208
|
+
const rest = text.slice(index);
|
|
209
|
+
return (rest.match(/^\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/)?.[0] ??
|
|
210
|
+
rest.match(/^\x1b\[[0-9;?]*[ -/]*[@-~]/)?.[0] ??
|
|
211
|
+
"");
|
|
212
|
+
}
|
|
213
|
+
function codePointSize(codePoint) {
|
|
214
|
+
return codePoint > 0xffff ? 2 : 1;
|
|
215
|
+
}
|
|
216
|
+
function baseDisplayWidth(codePoint, nextCodePoint) {
|
|
217
|
+
if (isTextVariationSelector(nextCodePoint))
|
|
218
|
+
return 1;
|
|
219
|
+
if (isEmojiVariationSelector(nextCodePoint))
|
|
220
|
+
return 2;
|
|
221
|
+
if (isWideCodePoint(codePoint))
|
|
222
|
+
return 2;
|
|
223
|
+
if (isEmojiCodePoint(codePoint))
|
|
224
|
+
return isEmojiPresentationCodePoint(codePoint) ? 2 : 1;
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
function isControlCodePoint(codePoint) {
|
|
228
|
+
return ((codePoint >= 0 && codePoint < 0x20) ||
|
|
229
|
+
(codePoint >= 0x7f && codePoint < 0xa0));
|
|
230
|
+
}
|
|
231
|
+
function isCombiningCodePoint(codePoint) {
|
|
232
|
+
return COMBINING_MARK.test(String.fromCodePoint(codePoint));
|
|
233
|
+
}
|
|
234
|
+
function isEmojiCodePoint(codePoint) {
|
|
235
|
+
return EXTENDED_PICTOGRAPHIC.test(String.fromCodePoint(codePoint));
|
|
236
|
+
}
|
|
237
|
+
function isEmojiPresentationCodePoint(codePoint) {
|
|
238
|
+
return EMOJI_PRESENTATION.test(String.fromCodePoint(codePoint));
|
|
239
|
+
}
|
|
240
|
+
function isTextVariationSelector(codePoint) {
|
|
241
|
+
return codePoint === 0xfe0e;
|
|
242
|
+
}
|
|
243
|
+
function isEmojiVariationSelector(codePoint) {
|
|
244
|
+
return codePoint === 0xfe0f;
|
|
245
|
+
}
|
|
246
|
+
function isEmojiModifierCodePoint(codePoint) {
|
|
247
|
+
return EMOJI_MODIFIER.test(String.fromCodePoint(codePoint));
|
|
248
|
+
}
|
|
249
|
+
function isKeycapBase(codePoint) {
|
|
250
|
+
return ((codePoint >= 0x30 && codePoint <= 0x39) ||
|
|
251
|
+
codePoint === 0x23 ||
|
|
252
|
+
codePoint === 0x2a);
|
|
253
|
+
}
|
|
254
|
+
function isRegionalIndicator(codePoint) {
|
|
255
|
+
return (codePoint >= REGIONAL_INDICATOR_START && codePoint <= REGIONAL_INDICATOR_END);
|
|
256
|
+
}
|
|
257
|
+
function isVariationSelector(codePoint) {
|
|
258
|
+
return ((codePoint >= 0xfe00 && codePoint <= 0xfe0f) ||
|
|
259
|
+
(codePoint >= 0xe0100 && codePoint <= 0xe01ef));
|
|
260
|
+
}
|
|
261
|
+
function isWideCodePoint(codePoint) {
|
|
262
|
+
return (codePoint >= 0x1100 &&
|
|
263
|
+
(codePoint <= 0x115f ||
|
|
264
|
+
codePoint === 0x2329 ||
|
|
265
|
+
codePoint === 0x232a ||
|
|
266
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
|
|
267
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
268
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
269
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
270
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
271
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
272
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
273
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
|
|
274
|
+
(codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
|
|
275
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd)));
|
|
276
|
+
}
|
|
277
|
+
export const AGENT_WIDGET_KEY = "pi-gentic-agent";
|
|
278
|
+
export const CARD_MESSAGE_TYPE = "pi-gentic:card";
|
|
279
|
+
export const LIVE_REFRESH_WIDGET_KEY = "pi-gentic-live-refresh";
|
|
280
|
+
const AGENT_COLORS = [36, 92, 95, 93, 91, 94, 96, 33];
|
|
281
|
+
export function setAgentLabel(ctx, agentName) {
|
|
282
|
+
if (ctx.mode !== "tui" || typeof ctx.ui?.setWidget !== "function")
|
|
283
|
+
return;
|
|
284
|
+
const content = agentName ? () => createAgentLabel(agentName) : undefined;
|
|
285
|
+
ctx.ui.setWidget(AGENT_WIDGET_KEY, content, { placement: "belowEditor" });
|
|
286
|
+
}
|
|
287
|
+
export function showCard(pi, text, details) {
|
|
288
|
+
pi.sendMessage({
|
|
289
|
+
customType: CARD_MESSAGE_TYPE,
|
|
290
|
+
content: text,
|
|
291
|
+
display: true,
|
|
292
|
+
details,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
export function startLiveRefresh(ctx, key = "default", options = {}) {
|
|
296
|
+
const noop = (() => { });
|
|
297
|
+
noop.refresh = () => { };
|
|
298
|
+
if (ctx.mode !== "tui" || typeof ctx.ui?.setWidget !== "function")
|
|
299
|
+
return noop;
|
|
300
|
+
const widgetKey = `${LIVE_REFRESH_WIDGET_KEY}:${key}`;
|
|
301
|
+
const minIntervalMs = Math.max(16, Number(options.intervalMs ?? 100));
|
|
302
|
+
let stopped = false;
|
|
303
|
+
let pending = false;
|
|
304
|
+
let lastRefreshAt = 0;
|
|
305
|
+
let refreshTimer;
|
|
306
|
+
let pulseTimer;
|
|
307
|
+
let timeout;
|
|
308
|
+
const clearRefreshTimer = () => {
|
|
309
|
+
if (!refreshTimer)
|
|
310
|
+
return;
|
|
311
|
+
clearTimeout(refreshTimer);
|
|
312
|
+
refreshTimer = undefined;
|
|
313
|
+
};
|
|
314
|
+
const clearPulseTimer = () => {
|
|
315
|
+
if (!pulseTimer)
|
|
316
|
+
return;
|
|
317
|
+
clearInterval(pulseTimer);
|
|
318
|
+
pulseTimer = undefined;
|
|
319
|
+
};
|
|
320
|
+
const stop = () => {
|
|
321
|
+
if (stopped)
|
|
322
|
+
return;
|
|
323
|
+
stopped = true;
|
|
324
|
+
clearRefreshTimer();
|
|
325
|
+
clearPulseTimer();
|
|
326
|
+
if (timeout)
|
|
327
|
+
clearTimeout(timeout);
|
|
328
|
+
try {
|
|
329
|
+
ctx.ui.setWidget(widgetKey, undefined, { placement: "belowEditor" });
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
// Stale command contexts are expected after session switches.
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
const renderPulse = () => {
|
|
336
|
+
pending = false;
|
|
337
|
+
if (stopped)
|
|
338
|
+
return;
|
|
339
|
+
try {
|
|
340
|
+
lastRefreshAt = Date.now();
|
|
341
|
+
ctx.ui.setWidget(widgetKey, () => invisibleComponent(), {
|
|
342
|
+
placement: "belowEditor",
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
stop();
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
stop.refresh = () => {
|
|
350
|
+
if (stopped || pending)
|
|
351
|
+
return;
|
|
352
|
+
const delay = Math.max(0, minIntervalMs - (Date.now() - lastRefreshAt));
|
|
353
|
+
pending = true;
|
|
354
|
+
refreshTimer = setTimeout(renderPulse, delay);
|
|
355
|
+
refreshTimer.unref?.();
|
|
356
|
+
};
|
|
357
|
+
if (options.autoPulse !== false) {
|
|
358
|
+
pulseTimer = setInterval(renderPulse, Math.max(250, Number(options.pulseIntervalMs ?? 1000)));
|
|
359
|
+
pulseTimer.unref?.();
|
|
360
|
+
}
|
|
361
|
+
timeout = setTimeout(() => stop(), Math.max(1000, Number(options.ttlMs ?? 10 * 60_000)));
|
|
362
|
+
timeout.unref?.();
|
|
363
|
+
return stop;
|
|
364
|
+
}
|
|
365
|
+
export function styleAgentName(agentName, { bracketed = false } = {}) {
|
|
366
|
+
const text = bracketed ? `[${agentName}]` : agentName;
|
|
367
|
+
return `\x1b[${agentColorCode(agentName)}m${text}\x1b[39m`;
|
|
368
|
+
}
|
|
369
|
+
export function agentColorCode(agentName) {
|
|
370
|
+
return AGENT_COLORS[hashString(String(agentName ?? "")) % AGENT_COLORS.length];
|
|
371
|
+
}
|
|
372
|
+
function createAgentLabel(agentName) {
|
|
373
|
+
return {
|
|
374
|
+
invalidate() { },
|
|
375
|
+
render(width) {
|
|
376
|
+
return [rightAlign(styleAgentName(agentName), width)];
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function invisibleComponent() {
|
|
381
|
+
return {
|
|
382
|
+
invalidate() { },
|
|
383
|
+
render() {
|
|
384
|
+
return [];
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function rightAlign(text, width) {
|
|
389
|
+
return `${" ".repeat(Math.max(0, width - ansiVisibleLength(text)))}${text}`;
|
|
390
|
+
}
|
|
391
|
+
function ansiVisibleLength(text) {
|
|
392
|
+
return String(text).replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
393
|
+
}
|
|
394
|
+
function hashString(text) {
|
|
395
|
+
let hash = 0;
|
|
396
|
+
for (const char of text)
|
|
397
|
+
hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
|
|
398
|
+
return hash;
|
|
399
|
+
}
|
|
400
|
+
export const SESSION_TREE_VISIBLE_ITEMS = 12;
|
|
401
|
+
export function renderSessionTree(details, theme) {
|
|
402
|
+
return new SessionTreeCard(details.sessions ?? [], theme);
|
|
403
|
+
}
|
|
404
|
+
export function createSessionTreePicker(sessions, theme, done, requestRender = () => { }, options = {}) {
|
|
405
|
+
return new SessionTreeCard(sessions, theme, {
|
|
406
|
+
...options,
|
|
407
|
+
onSelect: done,
|
|
408
|
+
requestRender,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
/** Interactive orchestration tree used by the orchestration-tree picker. */
|
|
412
|
+
export class SessionTreeCard {
|
|
413
|
+
sessions;
|
|
414
|
+
theme;
|
|
415
|
+
selectedIndex;
|
|
416
|
+
maxVisible;
|
|
417
|
+
onSelect;
|
|
418
|
+
requestRender;
|
|
419
|
+
refreshSessions;
|
|
420
|
+
refreshing;
|
|
421
|
+
refreshIntervalMs;
|
|
422
|
+
repaintIntervalMs;
|
|
423
|
+
refreshTimer;
|
|
424
|
+
repaintTimer;
|
|
425
|
+
constructor(sessions, theme, options = {}) {
|
|
426
|
+
this.sessions = sessions;
|
|
427
|
+
this.theme = theme;
|
|
428
|
+
this.selectedIndex = 0;
|
|
429
|
+
this.maxVisible = Math.max(1, Number(options.maxVisible ?? SESSION_TREE_VISIBLE_ITEMS));
|
|
430
|
+
this.onSelect =
|
|
431
|
+
typeof options.onSelect === "function"
|
|
432
|
+
? options.onSelect
|
|
433
|
+
: undefined;
|
|
434
|
+
this.requestRender =
|
|
435
|
+
typeof options.requestRender === "function"
|
|
436
|
+
? options.requestRender
|
|
437
|
+
: () => { };
|
|
438
|
+
this.refreshSessions =
|
|
439
|
+
typeof options.refreshSessions === "function"
|
|
440
|
+
? options.refreshSessions
|
|
441
|
+
: undefined;
|
|
442
|
+
this.refreshing = false;
|
|
443
|
+
this.refreshIntervalMs = Math.max(1000, Number(options.refreshIntervalMs ?? 5000));
|
|
444
|
+
this.repaintIntervalMs = Math.max(250, Number(options.repaintIntervalMs ?? 1000));
|
|
445
|
+
this.ensureRefreshTimer();
|
|
446
|
+
}
|
|
447
|
+
invalidate() { }
|
|
448
|
+
dispose() {
|
|
449
|
+
this.clearRefreshTimer();
|
|
450
|
+
}
|
|
451
|
+
updateSessions(sessions) {
|
|
452
|
+
const selected = this.sessions[this.clampedSelectedIndex()];
|
|
453
|
+
this.sessions = sessions ?? [];
|
|
454
|
+
const selectedIndex = selected
|
|
455
|
+
? this.sessions.findIndex((session) => sameSession(session, selected))
|
|
456
|
+
: -1;
|
|
457
|
+
this.selectedIndex =
|
|
458
|
+
selectedIndex >= 0 ? selectedIndex : this.clampedSelectedIndex();
|
|
459
|
+
this.ensureRefreshTimer();
|
|
460
|
+
}
|
|
461
|
+
async refresh() {
|
|
462
|
+
if (!this.refreshSessions || this.refreshing) {
|
|
463
|
+
this.requestRender();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
this.refreshing = true;
|
|
467
|
+
try {
|
|
468
|
+
const sessions = await this.refreshSessions();
|
|
469
|
+
if (Array.isArray(sessions))
|
|
470
|
+
this.updateSessions(sessions);
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
this.ensureRefreshTimer();
|
|
474
|
+
}
|
|
475
|
+
finally {
|
|
476
|
+
this.refreshing = false;
|
|
477
|
+
this.requestRender();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
ensureRefreshTimer() {
|
|
481
|
+
if (!this.sessions.some((session) => session.running)) {
|
|
482
|
+
this.clearRefreshTimer();
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (!this.repaintTimer) {
|
|
486
|
+
this.repaintTimer = setInterval(() => this.requestRender(), this.repaintIntervalMs);
|
|
487
|
+
this.repaintTimer.unref?.();
|
|
488
|
+
}
|
|
489
|
+
if (this.refreshTimer || !this.refreshSessions)
|
|
490
|
+
return;
|
|
491
|
+
this.refreshTimer = setInterval(() => void this.refresh(), this.refreshIntervalMs);
|
|
492
|
+
this.refreshTimer.unref?.();
|
|
493
|
+
}
|
|
494
|
+
clearRefreshTimer() {
|
|
495
|
+
if (this.refreshTimer)
|
|
496
|
+
clearInterval(this.refreshTimer);
|
|
497
|
+
if (this.repaintTimer)
|
|
498
|
+
clearInterval(this.repaintTimer);
|
|
499
|
+
this.refreshTimer = undefined;
|
|
500
|
+
this.repaintTimer = undefined;
|
|
501
|
+
}
|
|
502
|
+
handleInput(data) {
|
|
503
|
+
if (!this.onSelect)
|
|
504
|
+
return;
|
|
505
|
+
if (data === "\x1b") {
|
|
506
|
+
this.onSelect(undefined);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (data === "\r" || data === "\n") {
|
|
510
|
+
this.onSelect(this.sessions[this.clampedSelectedIndex()]);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const lastIndex = Math.max(0, this.sessions.length - 1);
|
|
514
|
+
if (data === "\x1b[A")
|
|
515
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
516
|
+
else if (data === "\x1b[B")
|
|
517
|
+
this.selectedIndex = Math.min(lastIndex, this.selectedIndex + 1);
|
|
518
|
+
else if (data === "\x1b[5~")
|
|
519
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible);
|
|
520
|
+
else if (data === "\x1b[6~")
|
|
521
|
+
this.selectedIndex = Math.min(lastIndex, this.selectedIndex + this.maxVisible);
|
|
522
|
+
else if (data === "\x1b[H" || data === "\x1b[1~")
|
|
523
|
+
this.selectedIndex = 0;
|
|
524
|
+
else if (data === "\x1b[F" || data === "\x1b[4~")
|
|
525
|
+
this.selectedIndex = lastIndex;
|
|
526
|
+
else
|
|
527
|
+
return;
|
|
528
|
+
this.requestRender();
|
|
529
|
+
}
|
|
530
|
+
render(width) {
|
|
531
|
+
const innerWidth = Math.max(10, width - 4);
|
|
532
|
+
const lines = this.lines(innerWidth);
|
|
533
|
+
return [
|
|
534
|
+
this.colorBorder(`╭${"─".repeat(Math.max(0, width - 2))}╮`),
|
|
535
|
+
...lines.map((line) => this.colorBorder("│ ") +
|
|
536
|
+
fit(line, innerWidth) +
|
|
537
|
+
this.colorBorder(" │")),
|
|
538
|
+
this.colorBorder(`╰${"─".repeat(Math.max(0, width - 2))}╯`),
|
|
539
|
+
];
|
|
540
|
+
}
|
|
541
|
+
lines(width) {
|
|
542
|
+
if (this.sessions.length === 0) {
|
|
543
|
+
return [
|
|
544
|
+
center(this.bold("Orchestration Tree"), width),
|
|
545
|
+
this.muted("─".repeat(width)),
|
|
546
|
+
"",
|
|
547
|
+
this.muted("No related sessions found."),
|
|
548
|
+
];
|
|
549
|
+
}
|
|
550
|
+
const { start, end } = this.visibleRange();
|
|
551
|
+
const visible = this.sessions.slice(start, end);
|
|
552
|
+
return [
|
|
553
|
+
center(this.bold("Orchestration Tree"), width),
|
|
554
|
+
this.muted("─".repeat(width)),
|
|
555
|
+
"",
|
|
556
|
+
...visible.map((session, index) => this.sessionLine(session, start + index, width)),
|
|
557
|
+
...this.scrollLines(start, end, width),
|
|
558
|
+
];
|
|
559
|
+
}
|
|
560
|
+
visibleRange() {
|
|
561
|
+
const selectedIndex = this.clampedSelectedIndex();
|
|
562
|
+
const start = Math.max(0, Math.min(selectedIndex - Math.floor(this.maxVisible / 2), this.sessions.length - this.maxVisible));
|
|
563
|
+
return {
|
|
564
|
+
start,
|
|
565
|
+
end: Math.min(start + this.maxVisible, this.sessions.length),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
scrollLines(start, end, width) {
|
|
569
|
+
if (this.sessions.length <= this.maxVisible)
|
|
570
|
+
return [];
|
|
571
|
+
const text = this.onSelect
|
|
572
|
+
? ` (${this.clampedSelectedIndex() + 1}/${this.sessions.length})`
|
|
573
|
+
: ` Showing ${start + 1}-${end} of ${this.sessions.length}`;
|
|
574
|
+
return [this.muted(""), this.muted(fit(text, width))];
|
|
575
|
+
}
|
|
576
|
+
clampedSelectedIndex() {
|
|
577
|
+
return Math.min(Math.max(0, this.selectedIndex), Math.max(0, this.sessions.length - 1));
|
|
578
|
+
}
|
|
579
|
+
sessionLine(session, index, width) {
|
|
580
|
+
const depth = Math.max(0, Number(session.depth ?? 0));
|
|
581
|
+
const isLast = session.isLast === true;
|
|
582
|
+
const connector = depth === 0
|
|
583
|
+
? ""
|
|
584
|
+
: `${"│ ".repeat(Math.max(0, depth - 1))}${isLast ? "└─" : "├─"} `;
|
|
585
|
+
const indicator = session.running ? this.green("●") : this.dim("○");
|
|
586
|
+
const agent = session.agentName
|
|
587
|
+
? `${this.agentName(session.agentName)} `
|
|
588
|
+
: "";
|
|
589
|
+
const message = normalizeInline(session.lastMessage ??
|
|
590
|
+
session.firstMessage ??
|
|
591
|
+
session.name ??
|
|
592
|
+
"Untitled session");
|
|
593
|
+
const id = this.dim(`(${shortSessionId(session.sessionId ?? session.id)})`);
|
|
594
|
+
const isSelected = this.onSelect && index === this.clampedSelectedIndex();
|
|
595
|
+
const selectMarker = this.onSelect
|
|
596
|
+
? isSelected
|
|
597
|
+
? `${this.green(">")} `
|
|
598
|
+
: " "
|
|
599
|
+
: "";
|
|
600
|
+
const left = `${selectMarker}${this.dim(connector)}${indicator} ${agent}`;
|
|
601
|
+
const timerText = formatDuration(sessionInactiveMs(session));
|
|
602
|
+
const inactive = session.running
|
|
603
|
+
? ` ${this.dim("Inactive:")} ${this.timer(timerText)}${" ".repeat(Math.max(0, 8 - timerText.length))}`
|
|
604
|
+
: "";
|
|
605
|
+
const right = `${id}${inactive}`;
|
|
606
|
+
const line = joinWithMiddle(left, message, right, width);
|
|
607
|
+
return isSelected ? this.selected(line) : line;
|
|
608
|
+
}
|
|
609
|
+
colorBorder(text) {
|
|
610
|
+
return this.theme.fg("dim", text);
|
|
611
|
+
}
|
|
612
|
+
bold(text) {
|
|
613
|
+
return this.theme.bold(text);
|
|
614
|
+
}
|
|
615
|
+
muted(text) {
|
|
616
|
+
return this.theme.fg("muted", text);
|
|
617
|
+
}
|
|
618
|
+
dim(text) {
|
|
619
|
+
return this.theme.fg("dim", text);
|
|
620
|
+
}
|
|
621
|
+
green(text) {
|
|
622
|
+
return this.theme.fg("success", text);
|
|
623
|
+
}
|
|
624
|
+
timer(text) {
|
|
625
|
+
return `\x1b[95m${text}\x1b[39m`;
|
|
626
|
+
}
|
|
627
|
+
selected(text) {
|
|
628
|
+
return `\x1b[48;5;236m${text}\x1b[49m`;
|
|
629
|
+
}
|
|
630
|
+
agentName(text) {
|
|
631
|
+
return this.theme.bold(styleAgentName(text, { bracketed: true }));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
export function sessionInactiveMs(session) {
|
|
635
|
+
if (!session.running)
|
|
636
|
+
return Number(session.inactiveMs ?? 0);
|
|
637
|
+
const timestamp = session.lastActivityAt ?? session.modified ?? 0;
|
|
638
|
+
const time = new Date(typeof timestamp === "string" ||
|
|
639
|
+
typeof timestamp === "number" ||
|
|
640
|
+
timestamp instanceof Date
|
|
641
|
+
? timestamp
|
|
642
|
+
: 0).getTime();
|
|
643
|
+
return Number.isFinite(time) && time > 0
|
|
644
|
+
? Date.now() - time
|
|
645
|
+
: Number(session.inactiveMs ?? 0);
|
|
646
|
+
}
|
|
647
|
+
function sameSession(a, b) {
|
|
648
|
+
return Boolean((a.sessionId && a.sessionId === b.sessionId) ||
|
|
649
|
+
(a.id && a.id === b.id) ||
|
|
650
|
+
(a.path && a.path === b.path));
|
|
651
|
+
}
|
|
652
|
+
export function renderAgentsCall() {
|
|
653
|
+
return new InvisibleComponent();
|
|
654
|
+
}
|
|
655
|
+
/** Reuses card instances during streaming updates so live details stay smooth. */
|
|
656
|
+
export function renderAgentsResult(result, options, theme, context) {
|
|
657
|
+
const previous = context.lastComponent;
|
|
658
|
+
const previousCard = previous instanceof AgentsCard ? previous : undefined;
|
|
659
|
+
const card = previousCard ?? new AgentsCard(theme);
|
|
660
|
+
const originalDetails = result.details && typeof result.details === "object" ? result.details : {};
|
|
661
|
+
const liveDetails = getLiveCardDetails(originalDetails);
|
|
662
|
+
const details = { ...originalDetails, ...(liveDetails ?? {}) };
|
|
663
|
+
const restoredRunning = details.status === "running" && !options.isPartial && !liveDetails;
|
|
664
|
+
card.update({
|
|
665
|
+
cardId: details.cardId,
|
|
666
|
+
kind: details.kind ?? context.args.action ?? "agents",
|
|
667
|
+
restored: restoredRunning,
|
|
668
|
+
status: restoredRunning
|
|
669
|
+
? "restored"
|
|
670
|
+
: options.isPartial
|
|
671
|
+
? (details.status ?? "running")
|
|
672
|
+
: (details.status ?? (context.isError ? "error" : "done")),
|
|
673
|
+
async: details.async ?? context.args.async === true,
|
|
674
|
+
agentName: details.agentName ?? context.args.agent,
|
|
675
|
+
sessionId: details.sessionId ?? context.args.sessionId,
|
|
676
|
+
message: details.message ?? context.args.message ?? firstText(result.content),
|
|
677
|
+
activities: details.activities ?? [],
|
|
678
|
+
startedAt: details.startedAt ??
|
|
679
|
+
previousCard?.data?.startedAt ??
|
|
680
|
+
(details.kind === "send" && details.status === "running"
|
|
681
|
+
? Date.now()
|
|
682
|
+
: undefined),
|
|
683
|
+
updatedAt: details.updatedAt ?? previousCard?.data?.updatedAt,
|
|
684
|
+
completedAt: restoredRunning
|
|
685
|
+
? (details.completedAt ?? details.updatedAt ?? details.startedAt)
|
|
686
|
+
: details.completedAt,
|
|
687
|
+
error: details.error,
|
|
688
|
+
configuration: details.configuration,
|
|
689
|
+
sessions: details.sessions ?? details.configuration?.sessions,
|
|
690
|
+
systemPrompt: details.systemPrompt,
|
|
691
|
+
}, options.expanded);
|
|
692
|
+
return card;
|
|
693
|
+
}
|
|
694
|
+
function firstText(content) {
|
|
695
|
+
return Array.isArray(content)
|
|
696
|
+
? content.find((item) => item.type === "text")?.text
|
|
697
|
+
: undefined;
|
|
698
|
+
}
|
|
699
|
+
class InvisibleComponent {
|
|
700
|
+
invalidate() { }
|
|
701
|
+
render() {
|
|
702
|
+
return [];
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
/** Chat card renderer for load, send, status, discovery, and error results. */
|
|
706
|
+
class AgentsCard {
|
|
707
|
+
theme;
|
|
708
|
+
data;
|
|
709
|
+
expanded;
|
|
710
|
+
constructor(theme) {
|
|
711
|
+
this.theme = theme;
|
|
712
|
+
this.data = {};
|
|
713
|
+
this.expanded = false;
|
|
714
|
+
}
|
|
715
|
+
update(data, expanded) {
|
|
716
|
+
this.data = data;
|
|
717
|
+
this.expanded = expanded;
|
|
718
|
+
}
|
|
719
|
+
invalidate() { }
|
|
720
|
+
render(width) {
|
|
721
|
+
const liveDetails = getLiveCardDetails(this.data);
|
|
722
|
+
this.data = {
|
|
723
|
+
...this.data,
|
|
724
|
+
...(liveDetails ?? {}),
|
|
725
|
+
restored: liveDetails ? false : this.data.restored,
|
|
726
|
+
};
|
|
727
|
+
const innerWidth = Math.max(10, width - 4);
|
|
728
|
+
const lines = this.buildLines(innerWidth);
|
|
729
|
+
return [
|
|
730
|
+
this.colorBorder(`╭${"─".repeat(Math.max(0, width - 2))}╮`),
|
|
731
|
+
...lines.map((line) => this.colorBorder("│ ") +
|
|
732
|
+
fit(line, innerWidth) +
|
|
733
|
+
this.colorBorder(" │")),
|
|
734
|
+
this.colorBorder(`╰${"─".repeat(Math.max(0, width - 2))}╯`),
|
|
735
|
+
];
|
|
736
|
+
}
|
|
737
|
+
buildLines(width) {
|
|
738
|
+
const header = this.header(width);
|
|
739
|
+
const body = this.expanded
|
|
740
|
+
? this.body(width).flatMap((line) => wrap(line, width))
|
|
741
|
+
: this.body(width);
|
|
742
|
+
const footer = this.footer(width);
|
|
743
|
+
const maxBodyLines = Math.max(1, 13 - 2);
|
|
744
|
+
const visibleBody = !this.expanded && body.length > maxBodyLines
|
|
745
|
+
? [
|
|
746
|
+
...body.slice(0, maxBodyLines - 1),
|
|
747
|
+
this.muted(`… ${body.length - maxBodyLines + 1} more`),
|
|
748
|
+
]
|
|
749
|
+
: body;
|
|
750
|
+
return [header, "", ...visibleBody, "", footer];
|
|
751
|
+
}
|
|
752
|
+
header(width) {
|
|
753
|
+
const icon = this.statusIcon();
|
|
754
|
+
const async = this.data.async ? `${this.purple("[ASYNC]")} ` : "";
|
|
755
|
+
const title = this.title();
|
|
756
|
+
const agent = this.data.agentName && this.data.agentName !== "agentless"
|
|
757
|
+
? ` ${this.agent(this.data.agentName)}`
|
|
758
|
+
: "";
|
|
759
|
+
const session = this.data.sessionId
|
|
760
|
+
? ` ${this.dim(`(${shortSessionId(this.data.sessionId)})`)}`
|
|
761
|
+
: "";
|
|
762
|
+
const inactive = this.data.status === "running" && this.data.updatedAt
|
|
763
|
+
? `${this.dim("Inactive:")} ${this.timer(formatDuration(Date.now() - this.data.updatedAt))}`
|
|
764
|
+
: "";
|
|
765
|
+
return joinWithRight(`${icon} ${async}${this.bold(title)}${agent}${session}`, inactive, width);
|
|
766
|
+
}
|
|
767
|
+
title() {
|
|
768
|
+
if (this.data.status === "error")
|
|
769
|
+
return "Agent call failed.";
|
|
770
|
+
if (this.data.status === "stopped")
|
|
771
|
+
return "Agent stopped before answering.";
|
|
772
|
+
if (this.data.status === "aborted")
|
|
773
|
+
return "Agent got aborted.";
|
|
774
|
+
if (this.data.status === "queued")
|
|
775
|
+
return "Message queued.";
|
|
776
|
+
if (this.data.restored && this.data.kind === "send")
|
|
777
|
+
return "Sent a message to";
|
|
778
|
+
if (this.data.status === "done" && this.data.kind === "send")
|
|
779
|
+
return "Agent answered.";
|
|
780
|
+
if (this.data.kind === "load" && this.data.agentName === "agentless")
|
|
781
|
+
return "Cleared active agent";
|
|
782
|
+
if (this.data.kind === "load")
|
|
783
|
+
return "Loaded";
|
|
784
|
+
if (this.data.kind === "send")
|
|
785
|
+
return "Sent a message to";
|
|
786
|
+
return String(this.data.kind ?? "agents");
|
|
787
|
+
}
|
|
788
|
+
body(width) {
|
|
789
|
+
if (this.data.error)
|
|
790
|
+
return wrap(this.data.error, width).map((line) => this.red(line));
|
|
791
|
+
if (this.data.kind === "discoverSessions")
|
|
792
|
+
return this.sessionTreeLines(width);
|
|
793
|
+
if (this.data.kind === "load")
|
|
794
|
+
return this.configurationLines(width);
|
|
795
|
+
const message = wrap(this.data.message || "", width);
|
|
796
|
+
const activityLines = this.activityLines(width);
|
|
797
|
+
return [...message, ...activityLines];
|
|
798
|
+
}
|
|
799
|
+
sessionTreeLines(width) {
|
|
800
|
+
const sessions = Array.isArray(this.data.sessions)
|
|
801
|
+
? this.data.sessions
|
|
802
|
+
: [];
|
|
803
|
+
const title = center(this.bold("Orchestration Tree"), width);
|
|
804
|
+
if (sessions.length === 0)
|
|
805
|
+
return [
|
|
806
|
+
title,
|
|
807
|
+
this.muted("─".repeat(width)),
|
|
808
|
+
"",
|
|
809
|
+
this.muted("No related sessions found."),
|
|
810
|
+
];
|
|
811
|
+
const end = Math.min(SESSION_TREE_VISIBLE_ITEMS, sessions.length);
|
|
812
|
+
const scroll = sessions.length > SESSION_TREE_VISIBLE_ITEMS
|
|
813
|
+
? [
|
|
814
|
+
"",
|
|
815
|
+
this.muted(fit(` Showing 1-${end} of ${sessions.length}`, width)),
|
|
816
|
+
]
|
|
817
|
+
: [];
|
|
818
|
+
return [
|
|
819
|
+
title,
|
|
820
|
+
this.muted("─".repeat(width)),
|
|
821
|
+
"",
|
|
822
|
+
...sessions
|
|
823
|
+
.slice(0, end)
|
|
824
|
+
.map((session, index) => this.sessionTreeLine(session, index, width)),
|
|
825
|
+
...scroll,
|
|
826
|
+
];
|
|
827
|
+
}
|
|
828
|
+
sessionTreeLine(session, index, width) {
|
|
829
|
+
const depth = Math.max(0, Number(session.depth ?? 0));
|
|
830
|
+
const isLast = session.isLast === true;
|
|
831
|
+
const connector = depth === 0
|
|
832
|
+
? ""
|
|
833
|
+
: `${"│ ".repeat(Math.max(0, depth - 1))}${isLast ? "└─" : "├─"} `;
|
|
834
|
+
const indicator = session.running ? this.green("●") : this.dim("○");
|
|
835
|
+
const agent = session.agentName
|
|
836
|
+
? `${this.agentName(session.agentName)} `
|
|
837
|
+
: "";
|
|
838
|
+
const message = this.sessionMessage(session);
|
|
839
|
+
const id = this.dim(`(${shortSessionId(session.sessionId ?? session.id)})`);
|
|
840
|
+
const left = `${this.dim(connector)}${indicator} ${agent}`;
|
|
841
|
+
const inactive = session.running
|
|
842
|
+
? ` ${this.dim("Inactive:")} ${this.timer(formatDuration(sessionInactiveMs(session)))}`
|
|
843
|
+
: "";
|
|
844
|
+
const right = `${id}${inactive}`;
|
|
845
|
+
return joinWithMiddle(left, message, right, width);
|
|
846
|
+
}
|
|
847
|
+
sessionMessage(session) {
|
|
848
|
+
const text = session.lastMessage ??
|
|
849
|
+
session.firstMessage ??
|
|
850
|
+
session.name ??
|
|
851
|
+
"Untitled session";
|
|
852
|
+
return normalizeInline(text);
|
|
853
|
+
}
|
|
854
|
+
configurationLines(width) {
|
|
855
|
+
const configuration = this.data.configuration ?? {};
|
|
856
|
+
const lines = Object.entries(configuration).map(([key, value]) => `${this.muted(`${key}:`)} ${formatValue(value)}`);
|
|
857
|
+
if (this.expanded && this.data.systemPrompt) {
|
|
858
|
+
lines.push("", this.bold("Resolved system prompt"), ...wrap(this.data.systemPrompt, width));
|
|
859
|
+
}
|
|
860
|
+
return lines.length ? lines : [this.muted("No configuration changes.")];
|
|
861
|
+
}
|
|
862
|
+
activityLines(width) {
|
|
863
|
+
const activities = Array.isArray(this.data.activities)
|
|
864
|
+
? this.data.activities
|
|
865
|
+
: [];
|
|
866
|
+
if (activities.length === 0)
|
|
867
|
+
return [];
|
|
868
|
+
const visible = this.expanded
|
|
869
|
+
? activities.slice(-13)
|
|
870
|
+
: activities.slice(-3);
|
|
871
|
+
const hidden = activities.length - visible.length;
|
|
872
|
+
const lines = hidden > 0 ? [this.muted(`├─ [+${hidden} activities]`)] : [];
|
|
873
|
+
for (const activity of visible) {
|
|
874
|
+
lines.push(fit(`${this.muted("├─")} ${formatActivity(activity)}`, width));
|
|
875
|
+
}
|
|
876
|
+
return lines;
|
|
877
|
+
}
|
|
878
|
+
footer(width) {
|
|
879
|
+
const collapse = this.expanded ? "Ctrl+O to collapse" : "Ctrl+O to expand";
|
|
880
|
+
const end = this.totalDurationText();
|
|
881
|
+
return joinWithRight(this.muted(collapse), this.dim(end), width);
|
|
882
|
+
}
|
|
883
|
+
totalDurationText() {
|
|
884
|
+
if (this.data.kind !== "send" || !this.data.startedAt)
|
|
885
|
+
return "";
|
|
886
|
+
const endAt = this.data.completedAt ??
|
|
887
|
+
(this.data.status === "running"
|
|
888
|
+
? Date.now()
|
|
889
|
+
: (this.data.updatedAt ?? this.data.startedAt));
|
|
890
|
+
return formatDuration(Math.max(0, endAt - this.data.startedAt));
|
|
891
|
+
}
|
|
892
|
+
statusIcon() {
|
|
893
|
+
if (this.data.kind === "load")
|
|
894
|
+
return this.pink("→");
|
|
895
|
+
if (this.data.status === "done")
|
|
896
|
+
return this.green("✓");
|
|
897
|
+
if (["error", "aborted", "stopped"].includes(this.data.status))
|
|
898
|
+
return this.red("!");
|
|
899
|
+
if (this.data.status === "queued")
|
|
900
|
+
return this.pink("○");
|
|
901
|
+
if (this.data.status === "running")
|
|
902
|
+
return this.green("●");
|
|
903
|
+
if (this.data.status === "restored")
|
|
904
|
+
return this.muted("○");
|
|
905
|
+
return this.muted("○");
|
|
906
|
+
}
|
|
907
|
+
colorBorder(text) {
|
|
908
|
+
return this.theme.fg("dim", text);
|
|
909
|
+
}
|
|
910
|
+
bold(text) {
|
|
911
|
+
return this.theme.bold(text);
|
|
912
|
+
}
|
|
913
|
+
muted(text) {
|
|
914
|
+
return this.theme.fg("muted", text);
|
|
915
|
+
}
|
|
916
|
+
dim(text) {
|
|
917
|
+
return this.theme.fg("dim", text);
|
|
918
|
+
}
|
|
919
|
+
green(text) {
|
|
920
|
+
return this.theme.fg("success", text);
|
|
921
|
+
}
|
|
922
|
+
red(text) {
|
|
923
|
+
return this.theme.fg("error", text);
|
|
924
|
+
}
|
|
925
|
+
purple(text) {
|
|
926
|
+
return this.theme.fg("accent", text);
|
|
927
|
+
}
|
|
928
|
+
brightPurple(text) {
|
|
929
|
+
return `\x1b[95m${text}\x1b[39m`;
|
|
930
|
+
}
|
|
931
|
+
pink(text) {
|
|
932
|
+
return this.theme.fg("warning", text);
|
|
933
|
+
}
|
|
934
|
+
timer(text) {
|
|
935
|
+
return this.brightPurple(text);
|
|
936
|
+
}
|
|
937
|
+
agent(text) {
|
|
938
|
+
return this.theme.bold(styleAgentName(text));
|
|
939
|
+
}
|
|
940
|
+
agentName(text) {
|
|
941
|
+
return this.theme.bold(styleAgentName(text, { bracketed: true }));
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
function formatActivity(activity) {
|
|
945
|
+
if (!isRecord(activity))
|
|
946
|
+
return normalizeInline(activity);
|
|
947
|
+
if (activity.type === "tool")
|
|
948
|
+
return normalizeInline(`[${activity.name}] ${activity.summary ?? ""} ${activity.status ? `(${activity.status})` : ""}`);
|
|
949
|
+
return normalizeInline(activity.text ?? activity.summary ?? JSON.stringify(activity));
|
|
950
|
+
}
|
|
951
|
+
function formatValue(value) {
|
|
952
|
+
if (Array.isArray(value))
|
|
953
|
+
return value.join(", ");
|
|
954
|
+
if (value && typeof value === "object")
|
|
955
|
+
return JSON.stringify(value);
|
|
956
|
+
return String(value ?? "");
|
|
957
|
+
}
|