pilotswarm-web 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.
Files changed (57) hide show
  1. package/README.md +144 -0
  2. package/auth/authz/engine.js +139 -0
  3. package/auth/config.js +110 -0
  4. package/auth/index.js +153 -0
  5. package/auth/normalize/entra.js +22 -0
  6. package/auth/providers/entra.js +76 -0
  7. package/auth/providers/none.js +24 -0
  8. package/auth.js +10 -0
  9. package/bin/serve.js +53 -0
  10. package/config.js +20 -0
  11. package/dist/app.js +469 -0
  12. package/dist/assets/index-BSVg-lGb.css +1 -0
  13. package/dist/assets/index-BXD5YP7A.js +24 -0
  14. package/dist/assets/msal-CytV9RFv.js +7 -0
  15. package/dist/assets/pilotswarm-WX3NED6m.js +40 -0
  16. package/dist/assets/react-jg0oazEi.js +1 -0
  17. package/dist/index.html +16 -0
  18. package/node_modules/pilotswarm-ui-core/README.md +6 -0
  19. package/node_modules/pilotswarm-ui-core/package.json +32 -0
  20. package/node_modules/pilotswarm-ui-core/src/commands.js +72 -0
  21. package/node_modules/pilotswarm-ui-core/src/context-usage.js +212 -0
  22. package/node_modules/pilotswarm-ui-core/src/controller.js +3613 -0
  23. package/node_modules/pilotswarm-ui-core/src/formatting.js +872 -0
  24. package/node_modules/pilotswarm-ui-core/src/history.js +571 -0
  25. package/node_modules/pilotswarm-ui-core/src/index.js +13 -0
  26. package/node_modules/pilotswarm-ui-core/src/layout.js +196 -0
  27. package/node_modules/pilotswarm-ui-core/src/reducer.js +1027 -0
  28. package/node_modules/pilotswarm-ui-core/src/selectors.js +2786 -0
  29. package/node_modules/pilotswarm-ui-core/src/session-tree.js +109 -0
  30. package/node_modules/pilotswarm-ui-core/src/state.js +80 -0
  31. package/node_modules/pilotswarm-ui-core/src/store.js +23 -0
  32. package/node_modules/pilotswarm-ui-core/src/system-titles.js +24 -0
  33. package/node_modules/pilotswarm-ui-core/src/themes/catppuccin-mocha.js +56 -0
  34. package/node_modules/pilotswarm-ui-core/src/themes/cobalt2.js +56 -0
  35. package/node_modules/pilotswarm-ui-core/src/themes/dark-high-contrast.js +56 -0
  36. package/node_modules/pilotswarm-ui-core/src/themes/dracula.js +56 -0
  37. package/node_modules/pilotswarm-ui-core/src/themes/github-dark.js +56 -0
  38. package/node_modules/pilotswarm-ui-core/src/themes/gruvbox-dark.js +56 -0
  39. package/node_modules/pilotswarm-ui-core/src/themes/hacker-x-matrix.js +56 -0
  40. package/node_modules/pilotswarm-ui-core/src/themes/hacker-x-orion-prime.js +56 -0
  41. package/node_modules/pilotswarm-ui-core/src/themes/helpers.js +77 -0
  42. package/node_modules/pilotswarm-ui-core/src/themes/index.js +42 -0
  43. package/node_modules/pilotswarm-ui-core/src/themes/noctis-viola.js +56 -0
  44. package/node_modules/pilotswarm-ui-core/src/themes/noctis.js +56 -0
  45. package/node_modules/pilotswarm-ui-core/src/themes/nord.js +56 -0
  46. package/node_modules/pilotswarm-ui-core/src/themes/solarized-dark.js +56 -0
  47. package/node_modules/pilotswarm-ui-core/src/themes/tokyo-night.js +56 -0
  48. package/node_modules/pilotswarm-ui-react/README.md +5 -0
  49. package/node_modules/pilotswarm-ui-react/package.json +36 -0
  50. package/node_modules/pilotswarm-ui-react/src/components.js +1316 -0
  51. package/node_modules/pilotswarm-ui-react/src/index.js +4 -0
  52. package/node_modules/pilotswarm-ui-react/src/platform.js +15 -0
  53. package/node_modules/pilotswarm-ui-react/src/use-controller-state.js +38 -0
  54. package/node_modules/pilotswarm-ui-react/src/web-app.js +2661 -0
  55. package/package.json +64 -0
  56. package/runtime.js +146 -0
  57. package/server.js +311 -0
@@ -0,0 +1,2786 @@
1
+ import { INSPECTOR_TABS, FOCUS_REGIONS } from "./commands.js";
2
+ import { createSplashCard, parseAskedAndAnsweredExchange } from "./history.js";
3
+ import {
4
+ buildMessageCardLines,
5
+ decorateArtifactLinksForChat,
6
+ extractArtifactLinks,
7
+ formatDisplayDateTime,
8
+ formatHumanDurationSeconds,
9
+ formatTimestamp,
10
+ parseMarkdownLines,
11
+ shortModelName,
12
+ shortSessionId,
13
+ } from "./formatting.js";
14
+ import {
15
+ getContextCompactionBadge,
16
+ getContextHeaderBadge,
17
+ getContextListBadge,
18
+ } from "./context-usage.js";
19
+ import { canonicalSystemTitle } from "./system-titles.js";
20
+
21
+ export const ACTIVE_HIGHLIGHT_BACKGROUND = "activeHighlightBackground";
22
+ export const ACTIVE_HIGHLIGHT_FOREGROUND = "activeHighlightForeground";
23
+ const USER_CHAT_COLOR = "#b392f0";
24
+ const USER_CHAT_LABEL_COLOR = "#d2b9ff";
25
+
26
+ const totalDescendantCountsCache = new WeakMap();
27
+ const visibleDescendantCountsCache = new WeakMap();
28
+
29
+ export function resolveActiveHighlightColor(color) {
30
+ return color || ACTIVE_HIGHLIGHT_FOREGROUND;
31
+ }
32
+
33
+ export function applyActiveHighlightRuns(runs, { preserveColors = false } = {}) {
34
+ return (runs || []).map((run) => ({
35
+ ...run,
36
+ color: preserveColors ? resolveActiveHighlightColor(run?.color) : ACTIVE_HIGHLIGHT_FOREGROUND,
37
+ backgroundColor: ACTIVE_HIGHLIGHT_BACKGROUND,
38
+ bold: run?.bold ?? true,
39
+ }));
40
+ }
41
+
42
+ export function buildActiveHighlightLine(text, { color = ACTIVE_HIGHLIGHT_FOREGROUND, bold = true } = {}) {
43
+ return {
44
+ text,
45
+ color,
46
+ backgroundColor: ACTIVE_HIGHLIGHT_BACKGROUND,
47
+ bold,
48
+ };
49
+ }
50
+
51
+ function getSessionVisualStatus(session) {
52
+ if (!session) return "unknown";
53
+ const status = session.status || "unknown";
54
+ if (
55
+ session.cronActive === true
56
+ && (status === "waiting" || status === "idle" || status === "unknown")
57
+ ) {
58
+ return "cron_waiting";
59
+ }
60
+ return status;
61
+ }
62
+
63
+ function isTerminalOrchestrationStatus(status) {
64
+ return status === "Completed" || status === "Failed" || status === "Terminated";
65
+ }
66
+
67
+ function getSessionErrorVisualKind(session) {
68
+ const status = getSessionVisualStatus(session);
69
+ if (session?.orchestrationStatus === "Failed" || status === "failed") return "failed";
70
+ if (status === "error") return "warning";
71
+ return null;
72
+ }
73
+
74
+ function getSessionVisualKind(session, mode = "local") {
75
+ const errorKind = getSessionErrorVisualKind(session);
76
+ if (errorKind) return errorKind;
77
+
78
+ const status = getSessionVisualStatus(session);
79
+ if (
80
+ mode === "remote"
81
+ && isTerminalOrchestrationStatus(session?.orchestrationStatus)
82
+ && status !== "cron_waiting"
83
+ && status !== "waiting"
84
+ && status !== "input_required"
85
+ ) {
86
+ if (session?.orchestrationStatus === "Completed") return "completed";
87
+ if (session?.orchestrationStatus === "Terminated") return "terminated";
88
+ }
89
+ if (status === "terminated") return "terminated";
90
+ return status;
91
+ }
92
+
93
+ function sessionStatusColor(session, mode = "local") {
94
+ switch (getSessionVisualKind(session, mode)) {
95
+ case "running": return "green";
96
+ case "cron_waiting": return "yellow";
97
+ case "waiting": return "yellow";
98
+ case "input_required": return "cyan";
99
+ case "cancelled": return "gray";
100
+ case "warning": return "yellow";
101
+ case "failed": return "red";
102
+ case "terminated": return "gray";
103
+ case "completed": return "gray";
104
+ case "idle": return "white";
105
+ default: return "white";
106
+ }
107
+ }
108
+
109
+ function sessionStatusIcon(session, mode = "local") {
110
+ switch (getSessionVisualKind(session, mode)) {
111
+ case "running": return "*";
112
+ case "cron_waiting": return "~";
113
+ case "waiting": return "~";
114
+ case "input_required": return "?";
115
+ case "cancelled": return "x";
116
+ case "warning": return "!";
117
+ case "failed":
118
+ case "terminated": return "x";
119
+ case "idle": return ".";
120
+ default: return "";
121
+ }
122
+ }
123
+
124
+ function buildSessionTitle(session, brandingTitle) {
125
+ const shortId = shortSessionId(session?.sessionId);
126
+
127
+ if (session?.isSystem) {
128
+ return `${canonicalSystemTitle(session, brandingTitle)} (${shortId})`;
129
+ }
130
+
131
+ const title = String(session?.title || "");
132
+ if (!title) return `(${shortId})`;
133
+ return title.includes(shortId) ? title : `${title} (${shortId})`;
134
+ }
135
+
136
+ function flattenRunsText(runs) {
137
+ return (runs || []).map((run) => run?.text || "").join("");
138
+ }
139
+
140
+ function buildChildMaps(byId) {
141
+ const childMap = new Map();
142
+ const parentMap = new Map();
143
+
144
+ for (const session of Object.values(byId || {})) {
145
+ const parentId = session?.parentSessionId;
146
+ if (!parentId) continue;
147
+ parentMap.set(session.sessionId, parentId);
148
+ if (!childMap.has(parentId)) childMap.set(parentId, []);
149
+ childMap.get(parentId).push(session.sessionId);
150
+ }
151
+
152
+ return { childMap, parentMap };
153
+ }
154
+
155
+ function buildTotalDescendantCounts(byId) {
156
+ const { childMap } = buildChildMaps(byId);
157
+ const counts = new Map();
158
+
159
+ function countFor(sessionId) {
160
+ if (counts.has(sessionId)) return counts.get(sessionId);
161
+ const children = childMap.get(sessionId) || [];
162
+ const total = children.reduce((sum, childId) => sum + 1 + countFor(childId), 0);
163
+ counts.set(sessionId, total);
164
+ return total;
165
+ }
166
+
167
+ for (const sessionId of Object.keys(byId || {})) {
168
+ countFor(sessionId);
169
+ }
170
+
171
+ return counts;
172
+ }
173
+
174
+ function buildVisibleDescendantCounts(flat = [], byId = {}) {
175
+ const { parentMap } = buildChildMaps(byId);
176
+ const counts = new Map();
177
+
178
+ for (const entry of flat) {
179
+ let currentParentId = parentMap.get(entry.sessionId);
180
+ while (currentParentId) {
181
+ counts.set(currentParentId, (counts.get(currentParentId) || 0) + 1);
182
+ currentParentId = parentMap.get(currentParentId);
183
+ }
184
+ }
185
+
186
+ return counts;
187
+ }
188
+
189
+ function getTotalDescendantCounts(byId = {}) {
190
+ if (!byId || typeof byId !== "object") {
191
+ return buildTotalDescendantCounts(byId);
192
+ }
193
+ const cached = totalDescendantCountsCache.get(byId);
194
+ if (cached) return cached;
195
+ const counts = buildTotalDescendantCounts(byId);
196
+ totalDescendantCountsCache.set(byId, counts);
197
+ return counts;
198
+ }
199
+
200
+ function getVisibleDescendantCounts(flat = [], byId = {}) {
201
+ if (!Array.isArray(flat)) {
202
+ return buildVisibleDescendantCounts(flat, byId);
203
+ }
204
+ const cached = visibleDescendantCountsCache.get(flat);
205
+ if (cached) return cached;
206
+ const counts = buildVisibleDescendantCounts(flat, byId);
207
+ visibleDescendantCountsCache.set(flat, counts);
208
+ return counts;
209
+ }
210
+
211
+ function getCollapseBadge(sessionId, entry, totalDescendantCounts, visibleDescendantCounts) {
212
+ const totalDescendants = totalDescendantCounts.get(sessionId) || 0;
213
+ const visibleDescendants = visibleDescendantCounts.get(sessionId) || 0;
214
+ const hiddenDescendants = Math.max(0, totalDescendants - visibleDescendants);
215
+ const badgeCount = entry?.collapsed ? totalDescendants : hiddenDescendants;
216
+ if (!badgeCount) return null;
217
+ return { text: `[+${badgeCount}]`, color: "cyan" };
218
+ }
219
+
220
+ function getCronBadge(session) {
221
+ if (!(session?.cronActive === true && typeof session?.cronInterval === "number")) {
222
+ return null;
223
+ }
224
+ return {
225
+ text: `[cron ${formatHumanDurationSeconds(session.cronInterval)}]`,
226
+ color: "magenta",
227
+ };
228
+ }
229
+
230
+ function buildSessionRowRuns(entry, session, state, totalDescendantCounts, visibleDescendantCounts) {
231
+ const runs = [];
232
+ const mode = state.connection?.mode || "local";
233
+ const depthPrefix = entry.depth > 0
234
+ ? `${" ".repeat(Math.max(0, entry.depth - 1))}└ `
235
+ : "";
236
+
237
+ if (depthPrefix) {
238
+ runs.push({ text: depthPrefix, color: "gray" });
239
+ }
240
+
241
+ if (session?.isSystem) {
242
+ runs.push({ text: "≈ ", color: "yellow", bold: true });
243
+ } else {
244
+ const icon = sessionStatusIcon(session, mode);
245
+ runs.push({
246
+ text: icon ? `${icon} ` : " ",
247
+ color: sessionStatusColor(session, mode),
248
+ });
249
+ }
250
+
251
+ const mainColor = session?.isSystem ? "yellow" : sessionStatusColor(session, mode);
252
+ const titleText = buildSessionTitle(session, state.branding?.title || "PilotSwarm");
253
+ const createdAtText = session?.createdAt ? ` ${formatDisplayDateTime(session.createdAt)}` : "";
254
+ runs.push({
255
+ text: `${titleText}${createdAtText}`,
256
+ color: mainColor,
257
+ bold: Boolean(session?.isSystem),
258
+ });
259
+
260
+ for (const badge of [
261
+ getCronBadge(session),
262
+ getContextListBadge(session?.contextUsage),
263
+ getCollapseBadge(session?.sessionId, entry, totalDescendantCounts, visibleDescendantCounts),
264
+ ]) {
265
+ if (!badge) continue;
266
+ runs.push({ text: ` ${badge.text}`, color: badge.color, bold: badge.bold });
267
+ }
268
+
269
+ return runs;
270
+ }
271
+
272
+ function normalizeSearchQuery(value) {
273
+ return String(value || "").trim().toLowerCase();
274
+ }
275
+
276
+ function matchesSearchQuery(value, query) {
277
+ const normalizedQuery = normalizeSearchQuery(query);
278
+ if (!normalizedQuery) return true;
279
+ return String(value || "").toLowerCase().includes(normalizedQuery);
280
+ }
281
+
282
+ export function selectSessionRows(state) {
283
+ const totalDescendantCounts = getTotalDescendantCounts(state.sessions.byId);
284
+ const visibleDescendantCounts = getVisibleDescendantCounts(state.sessions.flat, state.sessions.byId);
285
+ const query = state.sessions?.filterQuery || "";
286
+
287
+ return state.sessions.flat.map((entry) => {
288
+ const session = state.sessions.byId[entry.sessionId];
289
+ const runs = buildSessionRowRuns(entry, session, state, totalDescendantCounts, visibleDescendantCounts);
290
+ return {
291
+ sessionId: entry.sessionId,
292
+ text: flattenRunsText(runs),
293
+ runs,
294
+ depth: entry.depth,
295
+ status: session?.status,
296
+ statusColor: sessionStatusColor(session, state.connection?.mode || "local"),
297
+ active: entry.sessionId === state.sessions.activeSessionId,
298
+ isSystem: Boolean(session?.isSystem),
299
+ hasChildren: entry.hasChildren,
300
+ collapsed: entry.collapsed,
301
+ };
302
+ }).filter((row) => matchesSearchQuery(row.text, query) || matchesSearchQuery(row.sessionId, query));
303
+ }
304
+
305
+ export function selectVisibleSessionRows(state, maxRows = 8) {
306
+ const rows = selectSessionRows(state);
307
+ if (rows.length === 0) return [];
308
+
309
+ const filteredSessionIds = new Set(rows.map((row) => row.sessionId));
310
+ const flat = (Array.isArray(state.sessions?.flat) ? state.sessions.flat : [])
311
+ .filter((entry) => filteredSessionIds.has(entry.sessionId));
312
+
313
+ const activeIndexRaw = flat.findIndex((entry) => entry.sessionId === state.sessions.activeSessionId);
314
+ const activeIndex = Math.max(0, activeIndexRaw);
315
+ if (flat.length <= maxRows) {
316
+ return rows;
317
+ }
318
+
319
+ const half = Math.floor(maxRows / 2);
320
+ let start = Math.max(0, activeIndex - half);
321
+ let end = Math.min(flat.length, start + maxRows);
322
+
323
+ if (end > flat.length) {
324
+ end = flat.length;
325
+ start = Math.max(0, end - maxRows);
326
+ }
327
+
328
+ const visibleEntries = flat.slice(start, end);
329
+ const totalDescendantCounts = getTotalDescendantCounts(state.sessions.byId);
330
+ const visibleDescendantCounts = getVisibleDescendantCounts(flat, state.sessions.byId);
331
+
332
+ return rows.filter((row) => visibleEntries.some((entry) => entry.sessionId === row.sessionId));
333
+ }
334
+
335
+ export function selectActiveSession(state) {
336
+ const sessionId = state.sessions.activeSessionId;
337
+ return sessionId ? state.sessions.byId[sessionId] || null : null;
338
+ }
339
+
340
+ function buildPendingQuestionMessage(session) {
341
+ const pendingQuestion = session?.pendingQuestion;
342
+ if (!pendingQuestion?.question) return null;
343
+
344
+ const body = [String(pendingQuestion.question).trim()];
345
+ const choices = Array.isArray(pendingQuestion.choices)
346
+ ? pendingQuestion.choices.filter((choice) => typeof choice === "string" && choice.trim())
347
+ : [];
348
+
349
+ if (choices.length > 0) {
350
+ body.push("", "Choices:");
351
+ for (const choice of choices) {
352
+ body.push(`- ${choice}`);
353
+ }
354
+ }
355
+
356
+ if (choices.length > 0 && pendingQuestion.allowFreeform === false) {
357
+ body.push("", "Reply with one of the choices above in the prompt below.");
358
+ } else if (choices.length > 0) {
359
+ body.push("", "Reply with one of the choices above, or type a free-form answer below.");
360
+ } else {
361
+ body.push("", "Type your answer in the prompt below and press Enter.");
362
+ }
363
+
364
+ return {
365
+ id: `pending-question:${session.sessionId}:${pendingQuestion.question}`,
366
+ role: "system",
367
+ text: body.join("\n"),
368
+ time: "",
369
+ createdAt: session.updatedAt || Date.now(),
370
+ cardTitle: "Question",
371
+ cardTitleColor: "cyan",
372
+ cardBorderColor: "cyan",
373
+ };
374
+ }
375
+
376
+ function chatAlreadyContainsPendingQuestion(chat, question) {
377
+ const normalizedQuestion = String(question || "").trim();
378
+ if (!normalizedQuestion) return false;
379
+
380
+ return (chat || []).some((message) => {
381
+ const parsedExchange = parseAskedAndAnsweredExchange(message?.text || "");
382
+ if (parsedExchange?.question?.trim() === normalizedQuestion) return true;
383
+ return false;
384
+ });
385
+ }
386
+
387
+ function buildSessionErrorMessage(session) {
388
+ const errorText = String(session?.error || "").trim();
389
+ if (!errorText) return null;
390
+
391
+ const errorKind = getSessionErrorVisualKind(session);
392
+ if (!errorKind) return null;
393
+
394
+ const isFailed = errorKind === "failed";
395
+ const body = isFailed
396
+ ? errorText
397
+ : `${errorText}\n\nThe orchestration is still running, so this may be transient.`;
398
+
399
+ return {
400
+ id: `session-error:${session.sessionId}:${session.updatedAt || ""}:${errorText}`,
401
+ role: "system",
402
+ text: body,
403
+ time: "",
404
+ createdAt: session.updatedAt || Date.now(),
405
+ cardTitle: isFailed ? "Error" : "Warning",
406
+ cardTitleColor: isFailed ? "red" : "yellow",
407
+ cardBorderColor: isFailed ? "red" : "yellow",
408
+ };
409
+ }
410
+
411
+ export function selectActiveChat(state) {
412
+ const sessionId = state.sessions.activeSessionId;
413
+ const session = sessionId ? state.sessions.byId[sessionId] || null : null;
414
+ if (!sessionId) return createSplashCard(state.branding);
415
+ const history = state.history.bySessionId.get(sessionId);
416
+ const chat = history?.chat || [];
417
+ const pendingQuestionMessage = session?.pendingQuestion?.question
418
+ && !chatAlreadyContainsPendingQuestion(chat, session.pendingQuestion.question)
419
+ ? buildPendingQuestionMessage(session)
420
+ : null;
421
+ const sessionErrorMessage = buildSessionErrorMessage(session);
422
+
423
+ if ((!history || chat.length === 0) && !pendingQuestionMessage && !sessionErrorMessage) {
424
+ return createSplashCard(state.branding, session);
425
+ }
426
+
427
+ const messages = chat.length > 0 ? [...chat] : createSplashCard(state.branding, session);
428
+ if (pendingQuestionMessage) {
429
+ messages.push(pendingQuestionMessage);
430
+ }
431
+ if (sessionErrorMessage) {
432
+ messages.push(sessionErrorMessage);
433
+ }
434
+ return messages;
435
+ }
436
+
437
+ function prefixRuns(text, color = "gray", options = {}) {
438
+ return [{
439
+ text,
440
+ color,
441
+ bold: Boolean(options.bold),
442
+ underline: Boolean(options.underline),
443
+ }];
444
+ }
445
+
446
+ function buildChatMessagePrefix(message) {
447
+ const time = formatTimestamp(message?.createdAt || message?.time);
448
+ const roleLabel = message?.role === "user"
449
+ ? "You"
450
+ : message?.role === "assistant"
451
+ ? "Agent"
452
+ : message?.role === "system"
453
+ ? "System"
454
+ : "PilotSwarm";
455
+ const roleColor = message?.role === "user"
456
+ ? USER_CHAT_LABEL_COLOR
457
+ : message?.role === "assistant"
458
+ ? "green"
459
+ : message?.role === "system"
460
+ ? "yellow"
461
+ : "white";
462
+ const prefix = time ? `[${time}] ` : "";
463
+ return [
464
+ ...prefixRuns(prefix, "gray"),
465
+ ...prefixRuns(`${roleLabel}: `, roleColor, { bold: true }),
466
+ ];
467
+ }
468
+
469
+ function flattenLineText(lineRuns) {
470
+ if (!Array.isArray(lineRuns)) return String(lineRuns?.text || "");
471
+ return (lineRuns || []).map((run) => run?.text || "").join("");
472
+ }
473
+
474
+ function createBlankLine() {
475
+ return [{ text: "", color: null }];
476
+ }
477
+
478
+ function startsWithStructuredBlock(lines) {
479
+ const firstVisibleLine = (lines || []).find((line) => flattenLineText(line).trim().length > 0);
480
+ const text = flattenLineText(firstVisibleLine).trimStart();
481
+ return /^[┌│└]/.test(text);
482
+ }
483
+
484
+ function trimLeadingBlankLines(lines) {
485
+ const source = Array.isArray(lines) ? [...lines] : [];
486
+ while (source.length > 0) {
487
+ const firstLine = source[0];
488
+ if (flattenLineText(firstLine).trim().length > 0) break;
489
+ source.shift();
490
+ }
491
+ return source;
492
+ }
493
+
494
+ function splitSystemNoticeSegments(text) {
495
+ const lines = String(text || "").replace(/\r\n/g, "\n").split("\n");
496
+ const segments = [];
497
+ let textLines = [];
498
+
499
+ function flushText() {
500
+ if (textLines.length === 0) return;
501
+ segments.push({
502
+ kind: "text",
503
+ text: textLines.join("\n"),
504
+ });
505
+ textLines = [];
506
+ }
507
+
508
+ for (let index = 0; index < lines.length;) {
509
+ const line = lines[index];
510
+ if (!/^\s*\[SYSTEM:/i.test(line)) {
511
+ textLines.push(line);
512
+ index += 1;
513
+ continue;
514
+ }
515
+
516
+ const singleLineMatch = /^\s*\[SYSTEM:\s*(.*?)\]\s*$/i.exec(line);
517
+ if (singleLineMatch) {
518
+ flushText();
519
+ segments.push({
520
+ kind: "system",
521
+ text: singleLineMatch[1].trim(),
522
+ });
523
+ index += 1;
524
+ continue;
525
+ }
526
+
527
+ const noticeLines = [line.replace(/^\s*\[SYSTEM:\s*/i, "")];
528
+ let closingIndex = -1;
529
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
530
+ const closingLine = lines[cursor];
531
+ if (closingLine.trim() === "]") {
532
+ closingIndex = cursor;
533
+ break;
534
+ }
535
+ noticeLines.push(closingLine);
536
+ }
537
+
538
+ if (closingIndex === -1) {
539
+ textLines.push(line);
540
+ index += 1;
541
+ continue;
542
+ }
543
+
544
+ flushText();
545
+ segments.push({
546
+ kind: "system",
547
+ text: noticeLines.join("\n").trim(),
548
+ });
549
+ index = closingIndex + 1;
550
+ }
551
+
552
+ flushText();
553
+ return segments;
554
+ }
555
+
556
+ function summarizeSystemNoticeText(text) {
557
+ const normalized = String(text || "")
558
+ .replace(/\s+/g, " ")
559
+ .trim();
560
+ if (!normalized) return "System notice.";
561
+ if (normalized.startsWith("The session was dehydrated and has been rehydrated on a new worker.")) {
562
+ return "Session rehydrated on a new worker.";
563
+ }
564
+
565
+ const sentenceMatch = /^(.{1,160}?[.!?])(?:\s|$)/.exec(normalized);
566
+ const firstSentence = sentenceMatch?.[1]?.trim() || normalized;
567
+ if (firstSentence.length >= normalized.length) {
568
+ return firstSentence.length > 160
569
+ ? `${firstSentence.slice(0, 159).trimEnd()}…`
570
+ : firstSentence;
571
+ }
572
+ return `${firstSentence}…`;
573
+ }
574
+
575
+ function buildCollapsedSystemNoticeLine(text, timestamp = "") {
576
+ const summary = summarizeSystemNoticeText(text);
577
+ return {
578
+ kind: "systemNotice",
579
+ text: `${timestamp ? `[${timestamp}] ` : ""}System: ${summary}`,
580
+ summary,
581
+ body: decorateArtifactLinksForChat(text),
582
+ color: "gray",
583
+ };
584
+ }
585
+
586
+ function tintRunsIfUnset(lines, color) {
587
+ if (!color) return lines;
588
+ return (lines || []).map((lineRuns) => (lineRuns || []).map((run) => ({
589
+ ...run,
590
+ color: run?.color || color,
591
+ })));
592
+ }
593
+
594
+ function startsWithCardBlock(lines) {
595
+ const firstVisibleLine = (lines || []).find((line) => flattenLineText(line).trim().length > 0);
596
+ if (!firstVisibleLine) return false;
597
+ return flattenLineText(firstVisibleLine).trimStart().startsWith("┌");
598
+ }
599
+
600
+ function appendChatBlockLines(targetLines, nextLines) {
601
+ if (!Array.isArray(nextLines) || nextLines.length === 0) return;
602
+ if (
603
+ targetLines.length > 0
604
+ && startsWithCardBlock(nextLines)
605
+ && flattenLineText(targetLines[targetLines.length - 1]).trim().length > 0
606
+ ) {
607
+ targetLines.push([{ text: "", color: null }]);
608
+ }
609
+ targetLines.push(...nextLines);
610
+ }
611
+
612
+ function buildChatMessageLines(message, maxWidth, options = {}) {
613
+ if (message?.splash) {
614
+ return [{ kind: "markup", value: message.text }];
615
+ }
616
+
617
+ if (message?.role === "user") {
618
+ const askedAndAnswered = parseAskedAndAnsweredExchange(message?.text || "");
619
+ if (askedAndAnswered) {
620
+ return [
621
+ ...buildMessageCardLines({
622
+ title: "Question",
623
+ timestamp: formatTimestamp(message?.createdAt || message?.time),
624
+ body: askedAndAnswered.question,
625
+ width: Math.max(20, maxWidth),
626
+ titleColor: USER_CHAT_COLOR,
627
+ borderColor: USER_CHAT_COLOR,
628
+ }),
629
+ ...buildChatMessageLines({
630
+ ...message,
631
+ text: askedAndAnswered.answer,
632
+ }, maxWidth),
633
+ ];
634
+ }
635
+ }
636
+
637
+ if (options.allowLeadingSystemNotices !== false && (message?.role === "user" || message?.role === "assistant")) {
638
+ const segments = splitSystemNoticeSegments(message?.text || "");
639
+ if (segments.some((segment) => segment.kind === "system")) {
640
+ const rendered = [];
641
+ let renderedSpeakerText = false;
642
+ for (const [segmentIndex, segment] of segments.entries()) {
643
+ if (segment.kind === "system") {
644
+ if (rendered.length > 0 && flattenLineText(rendered[rendered.length - 1]).trim().length > 0) {
645
+ rendered.push(createBlankLine());
646
+ }
647
+ rendered.push(buildCollapsedSystemNoticeLine(
648
+ segment.text,
649
+ formatTimestamp(message?.createdAt || message?.time),
650
+ ));
651
+ const hasLaterContent = segments.slice(segmentIndex + 1).some((candidate) => candidate.kind !== "text" || candidate.text.trim());
652
+ if (hasLaterContent) {
653
+ rendered.push(createBlankLine());
654
+ }
655
+ continue;
656
+ }
657
+
658
+ if (!segment.text.trim()) continue;
659
+ appendChatBlockLines(rendered, buildChatMessageLines({
660
+ ...message,
661
+ text: segment.text,
662
+ }, maxWidth, {
663
+ allowLeadingSystemNotices: false,
664
+ skipPrefix: renderedSpeakerText,
665
+ }));
666
+ renderedSpeakerText = true;
667
+ }
668
+
669
+ if (rendered.length > 0) {
670
+ return rendered;
671
+ }
672
+ }
673
+ }
674
+
675
+ if (message?.role !== "user" && message?.role !== "assistant") {
676
+ if (message?.role === "system") {
677
+ return [buildCollapsedSystemNoticeLine(
678
+ message?.text || "",
679
+ formatTimestamp(message?.createdAt || message?.time),
680
+ )];
681
+ }
682
+ const isSystemCard = message?.role === "system"
683
+ && (!message?.cardTitle || String(message.cardTitle).toLowerCase() === "system");
684
+ return buildMessageCardLines({
685
+ title: message?.cardTitle || (message?.role === "system" ? "System" : "PilotSwarm"),
686
+ timestamp: formatTimestamp(message?.createdAt || message?.time),
687
+ body: decorateArtifactLinksForChat(message?.text || ""),
688
+ width: Math.max(20, maxWidth),
689
+ titleColor: message?.cardTitleColor || (message?.role === "system" ? "yellow" : "cyan"),
690
+ borderColor: message?.cardBorderColor || "gray",
691
+ ...(isSystemCard ? { bodyColor: "gray", fitToContent: true } : {}),
692
+ });
693
+ }
694
+
695
+ const markdownLines = trimLeadingBlankLines(parseMarkdownLines(
696
+ decorateArtifactLinksForChat(message?.text || ""),
697
+ { width: maxWidth },
698
+ ));
699
+ const tintedMarkdownLines = tintRunsIfUnset(
700
+ markdownLines,
701
+ message?.role === "user" ? USER_CHAT_COLOR : null,
702
+ );
703
+ const prefix = options.skipPrefix ? [] : buildChatMessagePrefix(message);
704
+
705
+ if (tintedMarkdownLines.length === 0) {
706
+ return prefix.length > 0 ? [prefix] : [];
707
+ }
708
+
709
+ if (startsWithStructuredBlock(tintedMarkdownLines)) {
710
+ return prefix.length > 0 ? [prefix, ...tintedMarkdownLines] : tintedMarkdownLines;
711
+ }
712
+
713
+ return tintedMarkdownLines.map((lineRuns, index) => {
714
+ if (index === 0 && prefix.length > 0) {
715
+ return [...prefix, ...lineRuns];
716
+ }
717
+ return lineRuns;
718
+ });
719
+ }
720
+
721
+ export function selectChatLines(state, maxWidth = 80) {
722
+ const messages = selectActiveChat(state);
723
+ if (!messages || messages.length === 0) {
724
+ return [{ text: "No messages yet.", color: "gray" }];
725
+ }
726
+
727
+ const lines = [];
728
+ for (const [index, message] of messages.entries()) {
729
+ const messageLines = buildChatMessageLines(message, maxWidth);
730
+ appendChatBlockLines(lines, messageLines);
731
+ const nextMessage = messages[index + 1];
732
+ if (nextMessage && flattenLineText(lines[lines.length - 1]).trim().length > 0) {
733
+ lines.push(createBlankLine());
734
+ }
735
+ }
736
+ return lines.length > 0 ? lines : [{ text: "No messages yet.", color: "gray" }];
737
+ }
738
+
739
+ export function selectActiveArtifactLinks(state) {
740
+ const messages = selectActiveChat(state);
741
+ const links = [];
742
+ const seen = new Set();
743
+
744
+ for (const message of messages || []) {
745
+ for (const link of extractArtifactLinks(message?.text || "")) {
746
+ const key = `${link.sessionId}/${link.filename}`;
747
+ if (seen.has(key)) continue;
748
+ seen.add(key);
749
+ links.push(link);
750
+ }
751
+ }
752
+
753
+ return links;
754
+ }
755
+
756
+ export function selectChatPaneChrome(state) {
757
+ const session = selectActiveSession(state);
758
+ const totalDescendantCounts = getTotalDescendantCounts(state.sessions.byId);
759
+ const visibleDescendantCounts = getVisibleDescendantCounts(state.sessions.flat, state.sessions.byId);
760
+
761
+ if (!session) {
762
+ return {
763
+ color: "cyan",
764
+ title: [{ text: "Chat", color: "cyan", bold: true }],
765
+ };
766
+ }
767
+
768
+ const shortId = shortSessionId(session.sessionId);
769
+ const mainColor = session.isSystem ? "yellow" : "cyan";
770
+ const title = [{
771
+ text: session.isSystem
772
+ ? `≈ ${canonicalSystemTitle(session, state.branding?.title || "PilotSwarm")}`
773
+ : (session.title || "Chat"),
774
+ color: mainColor,
775
+ bold: true,
776
+ }];
777
+
778
+ const activeEntry = state.sessions.flat.find((entry) => entry.sessionId === session.sessionId);
779
+ const collapseBadge = getCollapseBadge(session.sessionId, activeEntry, totalDescendantCounts, visibleDescendantCounts);
780
+ if (collapseBadge) {
781
+ title.push({ text: ` ${collapseBadge.text}`, color: collapseBadge.color });
782
+ }
783
+
784
+ title.push({ text: ` [${shortId}]`, color: "gray" });
785
+
786
+ const modelName = shortModelName(session.model);
787
+ if (modelName) {
788
+ title.push({ text: ` ${modelName}`, color: "cyan" });
789
+ }
790
+
791
+ const contextBadge = getContextHeaderBadge(session.contextUsage);
792
+ if (contextBadge) {
793
+ title.push({ text: ` ${contextBadge.text}`, color: "gray" });
794
+ }
795
+
796
+ const compactionBadge = getContextCompactionBadge(session.contextUsage);
797
+ if (compactionBadge) {
798
+ title.push({ text: ` ${compactionBadge.text}`, color: "gray" });
799
+ }
800
+
801
+ if (state.ui.inspectorTab === "sequence" || state.ui.inspectorTab === "nodes") {
802
+ title.push({ text: " [last 5m window]", color: "gray" });
803
+ }
804
+
805
+ return {
806
+ color: session.isSystem ? "yellow" : "cyan",
807
+ title,
808
+ };
809
+ }
810
+
811
+ export function selectActiveActivity(state) {
812
+ const sessionId = state.sessions.activeSessionId;
813
+ if (!sessionId) return [];
814
+ const history = state.history.bySessionId.get(sessionId);
815
+ return history?.activity || [];
816
+ }
817
+
818
+ export function selectActivityPane(state, maxLines = 12) {
819
+ const activity = selectActiveActivity(state);
820
+ const session = selectActiveSession(state);
821
+ const title = [{ text: "Activity", color: "gray", bold: true }];
822
+
823
+ if (session?.statusVersion != null) {
824
+ title.push({
825
+ text: ` [current session v${session.statusVersion}]`,
826
+ color: "gray",
827
+ });
828
+ }
829
+
830
+ return {
831
+ title,
832
+ lines: activity.length > 0
833
+ ? activity.map((item) => item.line || [{ text: item.text, color: "white" }])
834
+ : [{ text: "No activity yet", color: "gray" }],
835
+ };
836
+ }
837
+
838
+ function logLevelColor(level) {
839
+ switch (String(level || "").toLowerCase()) {
840
+ case "error": return "red";
841
+ case "warn": return "yellow";
842
+ case "debug": return "blue";
843
+ case "trace": return "magenta";
844
+ case "info":
845
+ default:
846
+ return "green";
847
+ }
848
+ }
849
+
850
+ function logCategoryColor(category, level) {
851
+ if (category === "orchestration") return "magenta";
852
+ if (category === "activity") return "cyan";
853
+ return logLevelColor(level);
854
+ }
855
+
856
+ function formatLogFormatLabel(format) {
857
+ return format === "raw" ? "raw summary" : "pretty text";
858
+ }
859
+
860
+ function currentOrchestrationIdForSession(session) {
861
+ if (!session?.sessionId) return null;
862
+ return `session-${session.sessionId}`;
863
+ }
864
+
865
+ function filterLogEntries(state, session) {
866
+ const entries = Array.isArray(state.logs?.entries) ? state.logs.entries : [];
867
+ const filter = state.logs?.filter || {};
868
+ const activeOrchestrationId = currentOrchestrationIdForSession(session);
869
+
870
+ return entries.filter((entry) => {
871
+ if (!entry) return false;
872
+ if (filter.source === "currentOrchestration" && activeOrchestrationId) {
873
+ if (entry.orchId !== activeOrchestrationId) return false;
874
+ }
875
+ if (filter.level && filter.level !== "all") {
876
+ if (String(entry.level || "").toLowerCase() !== filter.level) return false;
877
+ }
878
+ return true;
879
+ });
880
+ }
881
+
882
+ function buildRawLogLine(entry) {
883
+ const podLabel = shortNodeLabel(entry?.podName) || entry?.podName || "node";
884
+ const sessionId = entry?.sessionId
885
+ || (String(entry?.orchId || "").startsWith("session-") ? String(entry.orchId).slice("session-".length) : "");
886
+ const level = String(entry?.level || "info").toUpperCase();
887
+ const categoryMarker = entry?.category === "orchestration"
888
+ ? "◆"
889
+ : entry?.category === "activity"
890
+ ? "●"
891
+ : "•";
892
+ return [
893
+ { text: `[${entry?.time || "--:--:--"}] `, color: "gray" },
894
+ { text: `${categoryMarker} `, color: logCategoryColor(entry?.category, entry?.level) },
895
+ { text: `${podLabel} `, color: "white", bold: true },
896
+ ...(sessionId ? [{ text: `[${shortSessionId(sessionId)}] `, color: "gray" }] : []),
897
+ { text: `${level} `, color: logLevelColor(entry?.level), bold: true },
898
+ { text: entry?.rawLine || entry?.message || "", color: "white" },
899
+ ];
900
+ }
901
+
902
+ function buildPrettyLogLine(entry) {
903
+ const sessionId = entry?.sessionId
904
+ || (String(entry?.orchId || "").startsWith("session-") ? String(entry.orchId).slice("session-".length) : "");
905
+ return [{
906
+ text: `${sessionId ? `[${shortSessionId(sessionId)}] ` : ""}${entry?.prettyMessage || entry?.message || entry?.rawLine || ""}`,
907
+ color: logCategoryColor(entry?.category, entry?.level),
908
+ bold: String(entry?.level || "").toLowerCase() === "warn" || String(entry?.level || "").toLowerCase() === "error",
909
+ }];
910
+ }
911
+
912
+ function selectLogPane(state, session) {
913
+ const logs = state.logs || {};
914
+ const filter = logs.filter || {};
915
+ const summaryRuns = [
916
+ { text: "Scope: ", color: "gray" },
917
+ { text: filter.source === "currentOrchestration" ? "current orchestration" : "all nodes", color: "white" },
918
+ { text: " Level: ", color: "gray" },
919
+ { text: filter.level || "all", color: "white" },
920
+ { text: " Format: ", color: "gray" },
921
+ { text: formatLogFormatLabel(filter.format), color: "white" },
922
+ ];
923
+
924
+ if (!logs.available) {
925
+ return [
926
+ { text: logs.availabilityReason || "Log tailing disabled: no K8S_CONTEXT configured in the env file.", color: "yellow" },
927
+ { text: "", color: "gray" },
928
+ summaryRuns,
929
+ ];
930
+ }
931
+
932
+ if (!logs.tailing) {
933
+ return [
934
+ { text: "Press t to start log tailing.", color: "cyan", bold: true },
935
+ { text: "Press f to open log filters.", color: "gray" },
936
+ { text: "", color: "gray" },
937
+ summaryRuns,
938
+ ];
939
+ }
940
+
941
+ const entries = filterLogEntries(state, session);
942
+ if (entries.length === 0) {
943
+ return [
944
+ { text: "Tailing logs…", color: "cyan" },
945
+ { text: "No logs match the current filter yet.", color: "gray" },
946
+ { text: "", color: "gray" },
947
+ summaryRuns,
948
+ ];
949
+ }
950
+
951
+ return [
952
+ summaryRuns,
953
+ { text: "", color: "gray" },
954
+ ...entries.map((entry) => filter.format === "raw" ? buildRawLogLine(entry) : buildPrettyLogLine(entry)),
955
+ ];
956
+ }
957
+
958
+ function isMarkdownFilename(filename) {
959
+ return /\.(md|markdown|mdown|mkd|mdx)$/i.test(String(filename || ""));
960
+ }
961
+
962
+ function isJsonFilename(filename) {
963
+ return /\.(json|jsonl)$/i.test(String(filename || ""));
964
+ }
965
+
966
+ function buildFileTabRuns(activeTab) {
967
+ return INSPECTOR_TABS.map((tab) => ({
968
+ text: tab === activeTab ? `[${tab}] ` : `${tab} `,
969
+ color: tab === activeTab ? "magenta" : "gray",
970
+ bold: tab === activeTab,
971
+ }));
972
+ }
973
+
974
+ function buildPlainFilePreviewLines(content = "") {
975
+ const lines = String(content || "").split("\n");
976
+ return lines.length > 0
977
+ ? lines.map((line) => ({ text: line, color: "white" }))
978
+ : [{ text: "", color: "white" }];
979
+ }
980
+
981
+ function buildFileListEntry(filename, { selected = false, width = 24, label = null } = {}) {
982
+ const safeWidth = Math.max(8, width);
983
+ const prefix = isMarkdownFilename(filename)
984
+ ? "# "
985
+ : isJsonFilename(filename)
986
+ ? "{ "
987
+ : "• ";
988
+ const text = fitDisplayText(`${prefix}${label || filename}`, safeWidth).padEnd(safeWidth, " ");
989
+ if (selected) {
990
+ return buildActiveHighlightLine(text);
991
+ }
992
+ return {
993
+ text,
994
+ color: isMarkdownFilename(filename) ? "cyan" : "white",
995
+ bold: false,
996
+ };
997
+ }
998
+
999
+ export function selectFilesScope(state) {
1000
+ return state.files?.filter?.scope === "allSessions" ? "allSessions" : "selectedSession";
1001
+ }
1002
+
1003
+ export function selectFileBrowserItems(state) {
1004
+ const scope = selectFilesScope(state);
1005
+ const activeSession = selectActiveSession(state);
1006
+ const activeSessionId = activeSession?.sessionId || null;
1007
+ const query = state.files?.filter?.query || "";
1008
+ if (scope !== "allSessions") {
1009
+ const entries = Array.isArray(state.files?.bySessionId?.[activeSessionId]?.entries)
1010
+ ? state.files.bySessionId[activeSessionId].entries
1011
+ : [];
1012
+ return entries.map((filename) => ({
1013
+ id: `${activeSessionId || "none"}/${filename}`,
1014
+ sessionId: activeSessionId,
1015
+ filename,
1016
+ label: filename,
1017
+ })).filter((item) => matchesSearchQuery(item.filename, query));
1018
+ }
1019
+
1020
+ const orderedSessionIds = [
1021
+ ...new Set([
1022
+ ...(Array.isArray(state.sessions?.flat) ? state.sessions.flat.map((entry) => entry?.sessionId || entry) : []),
1023
+ ...Object.keys(state.files?.bySessionId || {}),
1024
+ ]),
1025
+ ].filter(Boolean);
1026
+
1027
+ const items = [];
1028
+ for (const sessionId of orderedSessionIds) {
1029
+ const entries = Array.isArray(state.files?.bySessionId?.[sessionId]?.entries)
1030
+ ? state.files.bySessionId[sessionId].entries
1031
+ : [];
1032
+ for (const filename of entries) {
1033
+ items.push({
1034
+ id: `${sessionId}/${filename}`,
1035
+ sessionId,
1036
+ filename,
1037
+ label: `[${shortSessionId(sessionId)}] ${filename}`,
1038
+ });
1039
+ }
1040
+ }
1041
+ return items.filter((item) => (
1042
+ matchesSearchQuery(item.filename, query)
1043
+ || matchesSearchQuery(item.sessionId, query)
1044
+ || matchesSearchQuery(item.label, query)
1045
+ ));
1046
+ }
1047
+
1048
+ export function selectSelectedFileBrowserItem(state) {
1049
+ const items = selectFileBrowserItems(state);
1050
+ if (items.length === 0) return null;
1051
+
1052
+ const scope = selectFilesScope(state);
1053
+ if (scope === "allSessions") {
1054
+ const preferredId = state.files?.selectedArtifactId || null;
1055
+ if (preferredId) {
1056
+ const selected = items.find((item) => item.id === preferredId);
1057
+ if (selected) return selected;
1058
+ }
1059
+ const activeSessionId = selectActiveSession(state)?.sessionId || null;
1060
+ const activeFilename = activeSessionId
1061
+ ? state.files?.bySessionId?.[activeSessionId]?.selectedFilename || null
1062
+ : null;
1063
+ if (activeSessionId && activeFilename) {
1064
+ const activeSelected = items.find((item) => item.sessionId === activeSessionId && item.filename === activeFilename);
1065
+ if (activeSelected) return activeSelected;
1066
+ }
1067
+ return items[0];
1068
+ }
1069
+
1070
+ const activeSessionId = selectActiveSession(state)?.sessionId || null;
1071
+ const selectedFilename = activeSessionId
1072
+ ? state.files?.bySessionId?.[activeSessionId]?.selectedFilename || null
1073
+ : null;
1074
+ return items.find((item) => item.filename === selectedFilename) || items[0];
1075
+ }
1076
+
1077
+ export function selectFilesView(state, options = {}) {
1078
+ const session = selectActiveSession(state);
1079
+ const listWidth = Math.max(12, Number(options?.listWidth) || Number(options?.width) || 24);
1080
+ const previewWidth = Math.max(18, Number(options?.previewWidth) || Number(options?.width) || 36);
1081
+ const showHints = options?.showHints !== false;
1082
+ const sessionId = session?.sessionId || null;
1083
+ const scope = selectFilesScope(state);
1084
+ const query = state.files?.filter?.query || "";
1085
+ const fileItems = selectFileBrowserItems(state);
1086
+ const selectedItem = selectSelectedFileBrowserItem(state);
1087
+ const selectedFilename = selectedItem?.filename || null;
1088
+ const selectedIndex = Math.max(0, fileItems.findIndex((item) => item.id === selectedItem?.id));
1089
+ const previewState = selectedItem?.sessionId && selectedFilename
1090
+ ? state.files?.bySessionId?.[selectedItem.sessionId]?.previews?.[selectedFilename] || null
1091
+ : null;
1092
+ const shortId = session ? shortSessionId(session.sessionId) : "";
1093
+ const allSessionIds = [...new Set([
1094
+ ...(Array.isArray(state.sessions?.flat) ? state.sessions.flat.map((entry) => entry?.sessionId || entry) : []),
1095
+ ...Object.keys(state.files?.bySessionId || {}),
1096
+ ])].filter(Boolean);
1097
+ const allSessionsLoading = scope === "allSessions" && allSessionIds.some((id) => !state.files?.bySessionId?.[id]?.loaded || state.files?.bySessionId?.[id]?.loading);
1098
+ const allSessionsError = scope === "allSessions"
1099
+ ? allSessionIds.map((id) => state.files?.bySessionId?.[id]?.error).find(Boolean) || null
1100
+ : null;
1101
+
1102
+ const listLines = [
1103
+ buildFileTabRuns("files"),
1104
+ ...((session || scope === "allSessions")
1105
+ ? []
1106
+ : [{ text: "No session selected.", color: "gray" }]),
1107
+ ];
1108
+
1109
+ if (scope === "allSessions") {
1110
+ if (allSessionsLoading && fileItems.length === 0) {
1111
+ listLines.push({ text: "Loading exported files across all sessions…", color: "gray" });
1112
+ } else if (allSessionsError && fileItems.length === 0) {
1113
+ listLines.push({ text: allSessionsError, color: "red" });
1114
+ } else if (fileItems.length === 0) {
1115
+ listLines.push({
1116
+ text: query
1117
+ ? `No artifacts matched "${query}" across any session.`
1118
+ : "No exported files across any session yet.",
1119
+ color: "gray",
1120
+ });
1121
+ listLines.push({
1122
+ text: query
1123
+ ? "Clear the query or switch back to the selected session."
1124
+ : "Switch the filter back to the selected session or wait for agents to export artifacts.",
1125
+ color: "gray",
1126
+ });
1127
+ } else {
1128
+ listLines.push(...fileItems.map((item, index) => buildFileListEntry(item.filename, {
1129
+ selected: index === selectedIndex,
1130
+ width: listWidth,
1131
+ label: item.label,
1132
+ })));
1133
+ }
1134
+ } else if (session) {
1135
+ const fileState = sessionId ? state.files?.bySessionId?.[sessionId] : null;
1136
+ const entries = Array.isArray(fileState?.entries) ? fileState.entries : [];
1137
+ const scopedItems = fileItems.filter((item) => item.sessionId === sessionId);
1138
+ if (fileState?.loading) {
1139
+ listLines.push({ text: "Loading exported files…", color: "gray" });
1140
+ } else if (fileState?.error) {
1141
+ listLines.push({ text: fileState.error, color: "red" });
1142
+ } else if (entries.length === 0 || scopedItems.length === 0) {
1143
+ listLines.push({
1144
+ text: query
1145
+ ? `No artifacts matched "${query}" for this session.`
1146
+ : "No exported files for this session yet.",
1147
+ color: "gray",
1148
+ });
1149
+ listLines.push({
1150
+ text: query
1151
+ ? "Clear the query or upload an artifact to this session."
1152
+ : "Agents must write/export artifacts before they appear here.",
1153
+ color: "gray",
1154
+ });
1155
+ } else {
1156
+ listLines.push(...scopedItems.map((item, index) => buildFileListEntry(item.filename, {
1157
+ selected: index === selectedIndex,
1158
+ width: listWidth,
1159
+ })));
1160
+ }
1161
+ }
1162
+
1163
+ let previewLines;
1164
+ let previewTitle;
1165
+ if (!session && scope !== "allSessions") {
1166
+ previewTitle = [{ text: "Preview", color: "cyan", bold: true }];
1167
+ previewLines = [{ text: "No session selected.", color: "gray" }];
1168
+ } else if (!selectedFilename) {
1169
+ previewTitle = [{ text: "Preview", color: "cyan", bold: true }];
1170
+ previewLines = [{ text: "Select a file to preview it here.", color: "gray" }];
1171
+ } else if (previewState?.loading) {
1172
+ previewTitle = [
1173
+ { text: `Preview: ${selectedFilename}`, color: "cyan", bold: true },
1174
+ ...(scope === "allSessions" && selectedItem?.sessionId
1175
+ ? [{ text: ` · ${shortSessionId(selectedItem.sessionId)}`, color: "gray" }]
1176
+ : []),
1177
+ ];
1178
+ previewLines = [{ text: "Loading file preview…", color: "gray" }];
1179
+ } else if (previewState?.error) {
1180
+ previewTitle = [
1181
+ { text: `Preview: ${selectedFilename}`, color: "cyan", bold: true },
1182
+ ...(scope === "allSessions" && selectedItem?.sessionId
1183
+ ? [{ text: ` · ${shortSessionId(selectedItem.sessionId)}`, color: "gray" }]
1184
+ : []),
1185
+ ];
1186
+ previewLines = [{ text: previewState.error, color: "red" }];
1187
+ } else {
1188
+ previewTitle = [
1189
+ { text: `Preview: ${selectedFilename}`, color: "cyan", bold: true },
1190
+ ...(scope === "allSessions" && selectedItem?.sessionId
1191
+ ? [{ text: ` · ${shortSessionId(selectedItem.sessionId)}`, color: "gray" }]
1192
+ : []),
1193
+ ...(previewState?.renderMode === "markdown"
1194
+ ? [{ text: " [md]", color: "gray" }]
1195
+ : previewState?.renderMode === "note"
1196
+ ? [{ text: " [note]", color: "gray" }]
1197
+ : []),
1198
+ ];
1199
+ previewLines = previewState?.renderMode === "markdown"
1200
+ ? trimLeadingBlankLines(parseMarkdownLines(previewState?.content || "", { width: previewWidth }))
1201
+ : buildPlainFilePreviewLines(previewState?.content || "");
1202
+ }
1203
+
1204
+ const panelTitleLabel = scope === "allSessions"
1205
+ ? `Files: all sessions${fileItems.length > 0 ? ` [${fileItems.length}]` : ""}`
1206
+ : session
1207
+ ? `Files: ${shortId}${fileItems.length > 0 ? ` [${fileItems.length}]` : ""}`
1208
+ : "Files";
1209
+ const listTitleLabel = scope === "allSessions"
1210
+ ? `Artifacts: all sessions${fileItems.length > 0 ? ` [${fileItems.length}]` : ""}`
1211
+ : session
1212
+ ? `Artifacts: ${shortId}${fileItems.length > 0 ? ` [${fileItems.length}]` : ""}`
1213
+ : "Artifacts";
1214
+ const querySuffix = query ? ` · @${query}` : "";
1215
+
1216
+ return {
1217
+ panelTitle: [{ text: `${panelTitleLabel}${querySuffix}`, color: "magenta", bold: true }],
1218
+ listTitle: [{ text: `${listTitleLabel}${querySuffix}`, color: "cyan", bold: true }],
1219
+ listLines,
1220
+ listBodyLines: listLines.slice(1),
1221
+ selectedIndex,
1222
+ selectedFilename,
1223
+ selectedSessionId: selectedItem?.sessionId || null,
1224
+ scope,
1225
+ previewTitle,
1226
+ previewLines,
1227
+ previewContent: previewState?.content || "",
1228
+ previewContentType: previewState?.contentType || "",
1229
+ previewRenderMode: previewState?.renderMode || null,
1230
+ previewError: previewState?.error || null,
1231
+ previewLoading: Boolean(previewState?.loading),
1232
+ previewScrollOffset: state.ui.scroll.filePreview || 0,
1233
+ fullscreen: Boolean(state.files?.fullscreen),
1234
+ fullscreenTitle: [
1235
+ { text: panelTitleLabel, color: "magenta", bold: true },
1236
+ ...(selectedFilename
1237
+ ? [
1238
+ { text: " · ", color: "gray" },
1239
+ { text: selectedFilename, color: "cyan", bold: true },
1240
+ ]
1241
+ : []),
1242
+ ...(showHints
1243
+ ? [{ text: " [f filter] [u upload] [a download] [o open] [v/esc close fullscreen]", color: "gray" }]
1244
+ : []),
1245
+ ],
1246
+ };
1247
+ }
1248
+
1249
+ export function selectStatusBar(state) {
1250
+ const focus = state.ui.focusRegion;
1251
+ const paneFullscreen = state.ui.fullscreenPane || null;
1252
+ const hasPendingQuestion = Boolean(selectActiveSession(state)?.pendingQuestion?.question);
1253
+ const fullscreenHint = paneFullscreen === focus ? "v/esc close fullscreen" : "v fullscreen";
1254
+ if (state.ui.modal?.type === "artifactUpload") {
1255
+ return {
1256
+ left: "Upload a local file into this session's artifact store",
1257
+ right: "type path · left/right move · enter upload · esc cancel",
1258
+ };
1259
+ }
1260
+ if (state.ui.modal?.type === "renameSession") {
1261
+ return {
1262
+ left: "Rename the selected session title",
1263
+ right: "type title · left/right move · enter save · esc cancel",
1264
+ };
1265
+ }
1266
+ if (state.ui.modal?.type === "artifactPicker") {
1267
+ return {
1268
+ left: "Select a linked artifact to download",
1269
+ right: "up/down move · enter download · a/esc close",
1270
+ };
1271
+ }
1272
+ if (state.ui.modal?.type === "modelPicker") {
1273
+ return {
1274
+ left: "Select a model for the new session",
1275
+ right: "up/down move · enter create · esc cancel",
1276
+ };
1277
+ }
1278
+ if (state.ui.modal?.type === "themePicker") {
1279
+ return {
1280
+ left: "Select a shared portal/TUI theme",
1281
+ right: "up/down move · enter apply · esc close",
1282
+ };
1283
+ }
1284
+ if (state.ui.modal?.type === "sessionAgentPicker") {
1285
+ return {
1286
+ left: "Select an agent for the new session",
1287
+ right: "up/down move · enter create · esc cancel",
1288
+ };
1289
+ }
1290
+ if (state.ui.modal?.type === "logFilter") {
1291
+ return {
1292
+ left: "Adjust log filters",
1293
+ right: "tab/shift-tab filter · up/down change · enter close · esc close",
1294
+ };
1295
+ }
1296
+ if (state.ui.modal?.type === "filesFilter") {
1297
+ return {
1298
+ left: "Adjust files browser filters",
1299
+ right: "tab/shift-tab filter · up/down change · enter close · esc close",
1300
+ };
1301
+ }
1302
+ const hints = {
1303
+ [FOCUS_REGIONS.SESSIONS]: `up/down switch · ctrl-u/ctrl-d page · d done · D delete · r refresh · t title · ${fullscreenHint} · {/} session pane · [/] side pane · T themes · a linked artifacts · drag copy · tab next pane · p prompt`,
1304
+ [FOCUS_REGIONS.CHAT]: `j/k scroll · ctrl-u/ctrl-d page · e older history · g/G top/bottom · d done · ${fullscreenHint} · {/} session pane · [/] side pane · T themes · a linked artifacts · drag copy · tab next pane · p prompt`,
1305
+ [FOCUS_REGIONS.INSPECTOR]: state.ui.inspectorTab === "logs"
1306
+ ? `j/k scroll · ctrl-u/ctrl-d page · g/G top/bottom · d done · t tail · f filter · ${fullscreenHint} · left/right tab · [/] side pane · T themes · a linked artifacts · drag copy · tab next pane`
1307
+ : state.ui.inspectorTab === "files"
1308
+ ? state.files?.fullscreen
1309
+ ? "j/k scroll · ctrl-u/ctrl-d page · g/G top/bottom · f filter · u/ctrl-a upload · a download · o open · d done · v/esc close fullscreen · left/right tab · {/} session pane · [/] side pane · T themes · tab next pane"
1310
+ : "j/k files · ctrl-u/ctrl-d page preview · g/G preview top/bottom · f filter · u/ctrl-a upload · a download · o open · d done · v fullscreen · left/right tab · {/} session pane · [/] side pane · T themes · tab next pane"
1311
+ : state.ui.inspectorTab === "history"
1312
+ ? `j/k scroll · ctrl-u/ctrl-d page · g/G top/bottom · f format · r refresh · a save artifact · d done · ${fullscreenHint} · left/right tab · [/] side pane · T themes · m next tab · tab next pane`
1313
+ : `j/k scroll · ctrl-u/ctrl-d page · g/G top/bottom · d done · ${fullscreenHint} · left/right tab · [/] side pane · T themes · h/l focus · a linked artifacts · drag copy · m next tab · tab next pane`,
1314
+ [FOCUS_REGIONS.ACTIVITY]: `j/k scroll · ctrl-u/ctrl-d page · g/G top/bottom · d done · ${fullscreenHint} · {/} session pane · [/] side pane · T themes · a linked artifacts · drag copy · h left · tab next pane`,
1315
+ [FOCUS_REGIONS.PROMPT]: hasPendingQuestion
1316
+ ? `type answer · enter reply · alt-enter newline · T themes · arrows move · alt-left/right word · alt-delete word · @ artifacts · @@ sessions · ${paneFullscreen ? "esc pane" : "esc sessions"}`
1317
+ : `type message · enter send · alt-enter newline · T themes · arrows move · alt-left/right word · alt-delete word · @ artifacts · @@ sessions · ${paneFullscreen ? "esc pane" : "esc sessions"}`,
1318
+ };
1319
+
1320
+ return {
1321
+ left: state.ui.statusText,
1322
+ right: hints[focus] || hints[FOCUS_REGIONS.SESSIONS],
1323
+ };
1324
+ }
1325
+
1326
+ function flattenRunsLength(runs) {
1327
+ return (runs || []).reduce((sum, run) => sum + String(run?.text || "").length, 0);
1328
+ }
1329
+
1330
+ function fitRuns(runs, maxWidth) {
1331
+ if (maxWidth <= 0) return [];
1332
+ const output = [];
1333
+ let remaining = maxWidth;
1334
+
1335
+ for (const run of runs || []) {
1336
+ if (remaining <= 0) break;
1337
+ const text = String(run?.text || "");
1338
+ if (!text) continue;
1339
+ const chunk = text.length > remaining && remaining > 1
1340
+ ? `${text.slice(0, remaining - 1)}…`
1341
+ : text.slice(0, remaining);
1342
+ if (!chunk) continue;
1343
+ output.push({ ...run, text: chunk });
1344
+ remaining -= chunk.length;
1345
+ }
1346
+
1347
+ return output;
1348
+ }
1349
+
1350
+ function displayLength(value) {
1351
+ return Array.from(String(value || "")).length;
1352
+ }
1353
+
1354
+ function fitDisplayText(value, maxWidth) {
1355
+ const text = String(value || "");
1356
+ if (maxWidth <= 0) return "";
1357
+ if (displayLength(text) <= maxWidth) return text;
1358
+ if (maxWidth === 1) return Array.from(text)[0] || "";
1359
+ return `${Array.from(text).slice(0, maxWidth - 1).join("")}…`;
1360
+ }
1361
+
1362
+ function padDisplayText(value, width) {
1363
+ const text = fitDisplayText(value, width);
1364
+ const padding = Math.max(0, width - displayLength(text));
1365
+ return text + " ".repeat(padding);
1366
+ }
1367
+
1368
+ function plainInspectorLine(text, color = "white", extra = {}) {
1369
+ return {
1370
+ text: String(text || ""),
1371
+ color,
1372
+ ...extra,
1373
+ };
1374
+ }
1375
+
1376
+ function formatCompactBytes(value) {
1377
+ const bytes = Number(value);
1378
+ if (!Number.isFinite(bytes) || bytes < 0) return "?";
1379
+ if (bytes < 1024) return `${Math.round(bytes)} B`;
1380
+ if (bytes < 1024 * 1024) {
1381
+ const kb = bytes / 1024;
1382
+ return `${kb >= 10 ? Math.round(kb) : kb.toFixed(1)} KB`;
1383
+ }
1384
+ const mb = bytes / (1024 * 1024);
1385
+ return `${mb >= 10 ? Math.round(mb) : mb.toFixed(1)} MB`;
1386
+ }
1387
+
1388
+ function summarizeEventPreview(text, maxLength = 18) {
1389
+ const normalized = String(text || "")
1390
+ .replace(/\s+/g, " ")
1391
+ .trim();
1392
+ if (!normalized) return "";
1393
+ return displayLength(normalized) > maxLength
1394
+ ? `${Array.from(normalized).slice(0, Math.max(1, maxLength - 1)).join("")}…`
1395
+ : normalized;
1396
+ }
1397
+
1398
+ function eventMessageText(event) {
1399
+ const data = event?.data;
1400
+ if (typeof data === "string") return data;
1401
+ if (data && typeof data === "object") {
1402
+ if (typeof data.content === "string") return data.content;
1403
+ if (typeof data.text === "string") return data.text;
1404
+ if (typeof data.message === "string") return data.message;
1405
+ if (typeof data.question === "string") return data.question;
1406
+ }
1407
+ return "";
1408
+ }
1409
+
1410
+ function joinUniqueSequenceDetail(parts = []) {
1411
+ const seen = new Set();
1412
+ const normalized = [];
1413
+ for (const part of parts) {
1414
+ const text = String(part || "").trim();
1415
+ if (!text) continue;
1416
+ const key = text.toLowerCase();
1417
+ if (seen.has(key)) continue;
1418
+ seen.add(key);
1419
+ normalized.push(text);
1420
+ }
1421
+ return normalized.join(" | ");
1422
+ }
1423
+
1424
+ function formatDehydrateSequenceDetail(event, preview = "") {
1425
+ return joinUniqueSequenceDetail([
1426
+ event?.data?.reason,
1427
+ event?.data?.detail,
1428
+ event?.data?.message,
1429
+ event?.data?.error,
1430
+ preview,
1431
+ ]);
1432
+ }
1433
+
1434
+ function formatLossyHandoffSequenceDetail(event, preview = "") {
1435
+ return joinUniqueSequenceDetail([
1436
+ event?.data?.message,
1437
+ event?.data?.detail,
1438
+ event?.data?.error,
1439
+ preview,
1440
+ ]);
1441
+ }
1442
+
1443
+ function shortNodeLabel(nodeId) {
1444
+ const raw = String(nodeId || "").trim();
1445
+ if (!raw || raw === "(unknown)") return null;
1446
+ const tail = raw.split(/[/:]/).pop() || raw;
1447
+ const short = tail.length <= 5 ? tail : tail.slice(-5);
1448
+ return short.replace(/^[^a-zA-Z0-9]+/, "") || short;
1449
+ }
1450
+
1451
+ const RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1000;
1452
+ const RECENT_ACTIVITY_WINDOW_LABEL = "last 5m";
1453
+
1454
+ function getRecentActivityWindow(state) {
1455
+ let endMs = 0;
1456
+
1457
+ for (const history of state.history.bySessionId.values()) {
1458
+ for (const event of history?.events || []) {
1459
+ const createdAtMs = event?.createdAt instanceof Date
1460
+ ? event.createdAt.getTime()
1461
+ : new Date(event?.createdAt || 0).getTime();
1462
+ if (Number.isFinite(createdAtMs)) {
1463
+ endMs = Math.max(endMs, createdAtMs);
1464
+ }
1465
+ }
1466
+ }
1467
+
1468
+ if (!Number.isFinite(endMs) || endMs <= 0) {
1469
+ endMs = Date.now();
1470
+ }
1471
+
1472
+ return {
1473
+ startMs: endMs - RECENT_ACTIVITY_WINDOW_MS,
1474
+ endMs,
1475
+ label: RECENT_ACTIVITY_WINDOW_LABEL,
1476
+ };
1477
+ }
1478
+
1479
+ function entryFallsWithinWindow(entry, window) {
1480
+ if (!entry || !window) return false;
1481
+ const createdAtMs = Number(entry.createdAtMs || 0);
1482
+ if (!Number.isFinite(createdAtMs)) return false;
1483
+ return createdAtMs >= window.startMs && createdAtMs <= window.endMs;
1484
+ }
1485
+
1486
+ function eventFallsWithinWindow(event, window) {
1487
+ if (!event || !window) return false;
1488
+ const createdAtMs = event?.createdAt instanceof Date
1489
+ ? event.createdAt.getTime()
1490
+ : new Date(event?.createdAt || 0).getTime();
1491
+ if (!Number.isFinite(createdAtMs)) return false;
1492
+ return createdAtMs >= window.startMs && createdAtMs <= window.endMs;
1493
+ }
1494
+
1495
+ const SEQUENCE_ORCHESTRATOR_TYPES = new Set([
1496
+ "wait",
1497
+ "timer",
1498
+ "cron_start",
1499
+ "cron_fire",
1500
+ "cron_cancel",
1501
+ "spawn",
1502
+ "cmd_recv",
1503
+ "cmd_done",
1504
+ ]);
1505
+
1506
+ function isSequenceOrchestratorType(type) {
1507
+ return SEQUENCE_ORCHESTRATOR_TYPES.has(type);
1508
+ }
1509
+
1510
+ function mapEventToSequenceEntry(event) {
1511
+ const time = formatTimestamp(event?.createdAt);
1512
+ const nodeLabel = shortNodeLabel(event?.workerNodeId);
1513
+ const detailText = eventMessageText(event);
1514
+ const preview = summarizeEventPreview(detailText, 20);
1515
+ const createdAtMs = event?.createdAt instanceof Date
1516
+ ? event.createdAt.getTime()
1517
+ : new Date(event?.createdAt || 0).getTime();
1518
+ const base = {
1519
+ time,
1520
+ createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : 0,
1521
+ nodeLabel,
1522
+ color: "white",
1523
+ detail: "",
1524
+ type: "other",
1525
+ };
1526
+
1527
+ switch (event?.eventType) {
1528
+ case "session.turn_started":
1529
+ return { ...base, type: "turn_start", color: "gray", detail: `turn ${event?.data?.iteration ?? "?"}` };
1530
+ case "session.turn_completed":
1531
+ return { ...base, type: "turn_end", color: "gray", detail: `turn ${event?.data?.iteration ?? "?"} done` };
1532
+ case "user.message":
1533
+ return { ...base, type: "user_msg", color: "white", detail: preview ? `>> ${preview}` : ">> user" };
1534
+ case "assistant.message":
1535
+ return { ...base, type: "response", color: "green", detail: preview ? `< ${preview}` : "< response" };
1536
+ case "system.message":
1537
+ return { ...base, type: "system", color: "yellow", detail: preview || "system" };
1538
+ case "session.wait_started":
1539
+ return {
1540
+ ...base,
1541
+ type: "wait",
1542
+ color: "yellow",
1543
+ detail: `wait ${formatHumanDurationSeconds(event?.data?.seconds ?? 0)}`,
1544
+ };
1545
+ case "session.wait_completed":
1546
+ return {
1547
+ ...base,
1548
+ type: "timer",
1549
+ color: "yellow",
1550
+ detail: `${formatHumanDurationSeconds(event?.data?.seconds ?? 0)} up`,
1551
+ };
1552
+ case "session.lossy_handoff": {
1553
+ const detail = formatLossyHandoffSequenceDetail(event, preview);
1554
+ return {
1555
+ ...base,
1556
+ type: "dehydrate",
1557
+ color: "yellow",
1558
+ detail: detail ? `lossy ${detail}` : "lossy handoff",
1559
+ };
1560
+ }
1561
+ case "session.dehydrated":
1562
+ return {
1563
+ ...base,
1564
+ type: "dehydrate",
1565
+ color: "cyan",
1566
+ detail: `ZZ ${formatDehydrateSequenceDetail(event, preview)}`.trim(),
1567
+ };
1568
+ case "session.rehydrated":
1569
+ case "session.hydrated":
1570
+ return { ...base, type: "hydrate", color: "green", detail: "rehydrated" };
1571
+ case "session.agent_spawned":
1572
+ return {
1573
+ ...base,
1574
+ type: "spawn",
1575
+ color: "cyan",
1576
+ detail: `spawn ${event?.data?.agentId || shortSessionId(event?.data?.childSessionId) || "agent"}`,
1577
+ };
1578
+ case "session.cron_started":
1579
+ return {
1580
+ ...base,
1581
+ type: "cron_start",
1582
+ color: "magenta",
1583
+ detail: `cron ${formatHumanDurationSeconds(event?.data?.intervalSeconds ?? 0)}`,
1584
+ };
1585
+ case "session.cron_fired":
1586
+ return { ...base, type: "cron_fire", color: "magenta", detail: "cron fired" };
1587
+ case "session.cron_cancelled":
1588
+ return { ...base, type: "cron_cancel", color: "magenta", detail: "cron off" };
1589
+ case "session.command_received":
1590
+ return {
1591
+ ...base,
1592
+ type: "cmd_recv",
1593
+ color: "magenta",
1594
+ detail: `/${event?.data?.cmd || "?"}`,
1595
+ };
1596
+ case "session.command_completed":
1597
+ return {
1598
+ ...base,
1599
+ type: "cmd_done",
1600
+ color: "magenta",
1601
+ detail: `/${event?.data?.cmd || "?"} ok`,
1602
+ };
1603
+ case "session.compaction_start":
1604
+ return { ...base, type: "compaction", color: "gray", detail: "compaction…" };
1605
+ case "session.compaction_complete":
1606
+ return { ...base, type: "compaction", color: "gray", detail: "compacted" };
1607
+ case "session.error":
1608
+ return { ...base, type: "error", color: "red", detail: preview || "error" };
1609
+ default:
1610
+ return null;
1611
+ }
1612
+ }
1613
+
1614
+ function buildSequenceEntries(events = []) {
1615
+ const entries = [];
1616
+
1617
+ for (const event of events || []) {
1618
+ const entry = mapEventToSequenceEntry(event);
1619
+ if (!entry) continue;
1620
+
1621
+ entries.push({
1622
+ ...entry,
1623
+ nodeLabel: isSequenceOrchestratorType(entry.type)
1624
+ ? "orch"
1625
+ : (entry.nodeLabel || "orch"),
1626
+ });
1627
+ }
1628
+
1629
+ return entries;
1630
+ }
1631
+
1632
+ function collapseContiguousSpawnEntries(entries = []) {
1633
+ const collapsed = [];
1634
+
1635
+ for (let index = 0; index < entries.length; index += 1) {
1636
+ const entry = entries[index];
1637
+ if (entry?.type !== "spawn" || entry?.nodeLabel !== "orch") {
1638
+ collapsed.push(entry);
1639
+ continue;
1640
+ }
1641
+
1642
+ let runLength = 1;
1643
+ while (index + runLength < entries.length) {
1644
+ const nextEntry = entries[index + runLength];
1645
+ if (nextEntry?.type !== "spawn" || nextEntry?.nodeLabel !== "orch") break;
1646
+ if (nextEntry?.time !== entry.time) break;
1647
+ runLength += 1;
1648
+ }
1649
+
1650
+ if (runLength === 1) {
1651
+ collapsed.push(entry);
1652
+ continue;
1653
+ }
1654
+
1655
+ collapsed.push({
1656
+ ...entry,
1657
+ detail: `spawn x${runLength}`,
1658
+ });
1659
+ index += runLength - 1;
1660
+ }
1661
+
1662
+ return collapsed;
1663
+ }
1664
+
1665
+ function buildSessionStatusSequenceEntry(session) {
1666
+ const errorText = String(session?.error || "").trim();
1667
+ if (!errorText) return null;
1668
+
1669
+ const errorKind = getSessionErrorVisualKind(session);
1670
+ if (!errorKind) return null;
1671
+
1672
+ const createdAtMs = session?.updatedAt ? Number(session.updatedAt) : Date.now();
1673
+ const safeCreatedAtMs = Number.isFinite(createdAtMs) ? createdAtMs : Date.now();
1674
+
1675
+ return {
1676
+ time: formatTimestamp(safeCreatedAtMs),
1677
+ createdAtMs: safeCreatedAtMs,
1678
+ nodeLabel: "orch",
1679
+ color: errorKind === "failed" ? "red" : "yellow",
1680
+ detail: `${errorKind === "failed" ? "ERR" : "WARN"} ${summarizeEventPreview(errorText, 20) || (errorKind === "failed" ? "error" : "warning")}`,
1681
+ type: errorKind === "failed" ? "error" : "warning",
1682
+ };
1683
+ }
1684
+
1685
+ function appendCurrentSessionStatusEntry(entries, session) {
1686
+ const statusEntry = buildSessionStatusSequenceEntry(session);
1687
+ if (!statusEntry) return entries;
1688
+
1689
+ const hasEquivalentEntry = (entries || []).some((entry) => {
1690
+ if (!entry || entry.nodeLabel !== "orch") return false;
1691
+ if (entry.detail !== statusEntry.detail) return false;
1692
+ return Math.abs(Number(entry.createdAtMs || 0) - statusEntry.createdAtMs) <= 60_000;
1693
+ });
1694
+ if (hasEquivalentEntry) return entries;
1695
+
1696
+ return [...(entries || []), statusEntry];
1697
+ }
1698
+
1699
+ function buildSequenceNodeUnionForWindow(state, startMs, endMs) {
1700
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs) {
1701
+ return [];
1702
+ }
1703
+
1704
+ const labels = new Set();
1705
+
1706
+ for (const history of state.history.bySessionId.values()) {
1707
+ const entries = buildSequenceEntries(history?.events || []);
1708
+ for (const entry of entries) {
1709
+ if (!entry?.nodeLabel || entry.nodeLabel === "orch") continue;
1710
+ const createdAtMs = Number(entry.createdAtMs || 0);
1711
+ if (!Number.isFinite(createdAtMs)) continue;
1712
+ if (createdAtMs < startMs || createdAtMs > endMs) continue;
1713
+ labels.add(entry.nodeLabel);
1714
+ }
1715
+ }
1716
+
1717
+ return Array.from(labels).sort((left, right) => left.localeCompare(right));
1718
+ }
1719
+
1720
+ function buildNodeMapNodeUnionForWindow(state, window) {
1721
+ const labels = new Set();
1722
+
1723
+ for (const history of state.history.bySessionId.values()) {
1724
+ for (const event of history?.events || []) {
1725
+ if (!eventFallsWithinWindow(event, window)) continue;
1726
+ const nodeLabel = shortNodeLabel(event?.workerNodeId);
1727
+ if (!nodeLabel) continue;
1728
+ labels.add(nodeLabel);
1729
+ }
1730
+ }
1731
+
1732
+ return Array.from(labels).sort((left, right) => left.localeCompare(right));
1733
+ }
1734
+
1735
+ function buildSequenceHeaderLine(nodeLabels, timeWidth, colWidth) {
1736
+ const runs = [
1737
+ { text: padDisplayText("TIME", timeWidth), color: "white", bold: true },
1738
+ { text: " ", color: null },
1739
+ ];
1740
+ nodeLabels.forEach((nodeLabel, index) => {
1741
+ if (index > 0) runs.push({ text: " ", color: null });
1742
+ runs.push({ text: padDisplayText(nodeLabel, colWidth), color: "white", bold: true });
1743
+ });
1744
+ return runs;
1745
+ }
1746
+
1747
+ function buildSequenceDividerLine(nodeLabels, timeWidth, colWidth) {
1748
+ return plainInspectorLine(
1749
+ `${"-".repeat(timeWidth)} ${nodeLabels.map(() => "─".repeat(colWidth)).join(" ")}`,
1750
+ "gray",
1751
+ );
1752
+ }
1753
+
1754
+ function buildSequenceStatsLines(state, session, maxWidth) {
1755
+ const statsEntry = state?.orchestration?.bySessionId?.[session?.sessionId] || null;
1756
+ let body = "loading orchestration stats...";
1757
+ if (!statsEntry) {
1758
+ } else if (statsEntry.loading && !statsEntry.stats) {
1759
+ body = "loading orchestration stats...";
1760
+ } else if (!statsEntry.stats) {
1761
+ body = statsEntry.error ? "orchestration stats unavailable" : "loading orchestration stats...";
1762
+ } else {
1763
+ const stats = statsEntry.stats;
1764
+ const fullParts = [
1765
+ `hist ${Number(stats.historyEventCount) || 0} ev`,
1766
+ formatCompactBytes(stats.historySizeBytes),
1767
+ `q ${Number(stats.queuePendingCount) || 0}`,
1768
+ `kv ${Number(stats.kvUserKeyCount) || 0} keys`,
1769
+ formatCompactBytes(stats.kvTotalValueBytes),
1770
+ ];
1771
+ const compactParts = [
1772
+ `h${Number(stats.historyEventCount) || 0}`,
1773
+ formatCompactBytes(stats.historySizeBytes),
1774
+ `q${Number(stats.queuePendingCount) || 0}`,
1775
+ `kv${Number(stats.kvUserKeyCount) || 0}`,
1776
+ ];
1777
+ body = maxWidth <= 40 ? compactParts.join(" · ") : fullParts.join(" | ");
1778
+ }
1779
+
1780
+ if (maxWidth <= 52) {
1781
+ return [fitRuns([
1782
+ { text: "Stats ", color: "cyan", bold: true },
1783
+ { text: body, color: "white" },
1784
+ ], Math.max(18, maxWidth))];
1785
+ }
1786
+
1787
+ return buildMessageCardLines({
1788
+ title: "Stats",
1789
+ body,
1790
+ width: Math.max(24, maxWidth),
1791
+ titleColor: "cyan",
1792
+ borderColor: "gray",
1793
+ fitToContent: true,
1794
+ }).slice(0, -1);
1795
+ }
1796
+
1797
+ function buildSequenceEventLine(entry, nodeLabels, timeWidth, colWidth) {
1798
+ const targetNode = nodeLabels.includes(entry.nodeLabel)
1799
+ ? entry.nodeLabel
1800
+ : (nodeLabels.includes("…") ? "…" : nodeLabels[nodeLabels.length - 1]);
1801
+ const runs = [
1802
+ { text: padDisplayText(entry.time || "", timeWidth), color: "white" },
1803
+ { text: " ", color: null },
1804
+ ];
1805
+
1806
+ nodeLabels.forEach((nodeLabel, index) => {
1807
+ if (index > 0) runs.push({ text: " ", color: null });
1808
+ if (nodeLabel === targetNode) {
1809
+ runs.push({
1810
+ text: padDisplayText(entry.detail || "", colWidth),
1811
+ color: entry.color || "white",
1812
+ bold: Boolean(entry.bold),
1813
+ underline: Boolean(entry.underline),
1814
+ });
1815
+ } else {
1816
+ runs.push({
1817
+ text: padDisplayText("│", colWidth),
1818
+ color: "gray",
1819
+ });
1820
+ }
1821
+ });
1822
+
1823
+ return runs;
1824
+ }
1825
+
1826
+ function buildNodeMapHeaderLine(nodeLabels, colWidth) {
1827
+ const runs = [];
1828
+ nodeLabels.forEach((nodeLabel, index) => {
1829
+ if (index > 0) runs.push({ text: " ", color: null });
1830
+ runs.push({ text: padDisplayText(nodeLabel, colWidth), color: "white", bold: true });
1831
+ });
1832
+ return runs;
1833
+ }
1834
+
1835
+ function buildSequenceViewForSession(state, session, maxWidth, options = {}) {
1836
+ const allowWideColumns = Boolean(options?.allowWideColumns);
1837
+ const statsLines = buildSequenceStatsLines(state, session, maxWidth);
1838
+ const history = state.history.bySessionId.get(session.sessionId);
1839
+ const entries = appendCurrentSessionStatusEntry(
1840
+ collapseContiguousSpawnEntries(buildSequenceEntries(history?.events || [])),
1841
+ session,
1842
+ );
1843
+ if (entries.length === 0) {
1844
+ return {
1845
+ stickyLines: statsLines,
1846
+ lines: [plainInspectorLine("No events yet - interact with this session to populate the sequence diagram.")],
1847
+ };
1848
+ }
1849
+
1850
+ const recentWindow = getRecentActivityWindow(state);
1851
+ const windowedEntries = entries.filter((entry) => entryFallsWithinWindow(entry, recentWindow));
1852
+ const visibleEntries = (windowedEntries.length > 0 ? windowedEntries : entries).slice(-48);
1853
+ const unionNodes = buildSequenceNodeUnionForWindow(state, recentWindow.startMs, recentWindow.endMs);
1854
+ const activeSessionNodes = Array.from(new Set(visibleEntries
1855
+ .map((entry) => entry.nodeLabel)
1856
+ .filter((nodeLabel) => nodeLabel && nodeLabel !== "orch"))).sort((left, right) => left.localeCompare(right));
1857
+ const uniqueNodes = unionNodes.length > 0 ? unionNodes : activeSessionNodes;
1858
+ const timeWidth = 8;
1859
+ const availableWidth = Math.max(18, maxWidth);
1860
+ const maxNodes = Math.max(1, Math.floor((availableWidth - timeWidth - 1) / 6));
1861
+ let nodeLabels = ["orch", ...uniqueNodes];
1862
+ if (!allowWideColumns && nodeLabels.length > maxNodes) {
1863
+ const visibleCount = Math.max(1, maxNodes - 1);
1864
+ nodeLabels = [
1865
+ ...nodeLabels.slice(0, visibleCount),
1866
+ "…",
1867
+ ];
1868
+ }
1869
+
1870
+ const gapWidth = Math.max(0, nodeLabels.length - 1);
1871
+ const colWidth = Math.max(
1872
+ 4,
1873
+ Math.floor((availableWidth - timeWidth - 1 - gapWidth) / Math.max(1, nodeLabels.length)),
1874
+ );
1875
+
1876
+ return {
1877
+ stickyLines: [
1878
+ ...statsLines,
1879
+ plainInspectorLine(`Window: ${recentWindow.label}`, "gray"),
1880
+ buildSequenceHeaderLine(nodeLabels, timeWidth, colWidth),
1881
+ buildSequenceDividerLine(nodeLabels, timeWidth, colWidth),
1882
+ ],
1883
+ lines: visibleEntries.map((entry) => buildSequenceEventLine(entry, nodeLabels, timeWidth, colWidth)),
1884
+ };
1885
+ }
1886
+
1887
+ function buildOrderedSessionIds(state) {
1888
+ const orderedIds = [];
1889
+ const seen = new Set();
1890
+
1891
+ for (const entry of state.sessions.flat || []) {
1892
+ if (!entry?.sessionId || seen.has(entry.sessionId)) continue;
1893
+ seen.add(entry.sessionId);
1894
+ orderedIds.push(entry.sessionId);
1895
+ }
1896
+
1897
+ for (const sessionId of Object.keys(state.sessions.byId || {})) {
1898
+ if (seen.has(sessionId)) continue;
1899
+ seen.add(sessionId);
1900
+ orderedIds.push(sessionId);
1901
+ }
1902
+
1903
+ return orderedIds;
1904
+ }
1905
+
1906
+ function getLastKnownSessionNode(history, window = null) {
1907
+ const events = history?.events || [];
1908
+ for (let index = events.length - 1; index >= 0; index -= 1) {
1909
+ if (window && !eventFallsWithinWindow(events[index], window)) continue;
1910
+ const nodeLabel = shortNodeLabel(events[index]?.workerNodeId);
1911
+ if (nodeLabel) return nodeLabel;
1912
+ }
1913
+ return null;
1914
+ }
1915
+
1916
+ function buildNodeMapCell(session, brandingTitle, width, active) {
1917
+ const label = width >= 16
1918
+ ? (session?.isSystem
1919
+ ? canonicalSystemTitle(session, brandingTitle)
1920
+ : (session?.title || shortSessionId(session?.sessionId)))
1921
+ : shortSessionId(session?.sessionId);
1922
+ const prefix = session?.isSystem ? "≈ " : `${sessionStatusIcon(session) || "."} `;
1923
+ const text = padDisplayText(`${prefix}${label}`, width);
1924
+
1925
+ if (active) {
1926
+ return buildActiveHighlightLine(text);
1927
+ }
1928
+
1929
+ return {
1930
+ text,
1931
+ color: session?.isSystem ? "yellow" : sessionStatusColor(session),
1932
+ bold: Boolean(session?.isSystem),
1933
+ };
1934
+ }
1935
+
1936
+ function buildNodeMapLines(state, maxWidth, options = {}) {
1937
+ const allowWideColumns = Boolean(options?.allowWideColumns);
1938
+ const orderedSessionIds = buildOrderedSessionIds(state);
1939
+ if (orderedSessionIds.length === 0) {
1940
+ return [plainInspectorLine("No sessions available for the node map.", "gray")];
1941
+ }
1942
+
1943
+ const recentWindow = getRecentActivityWindow(state);
1944
+ const nodeSessionMap = new Map();
1945
+ const knownNodes = buildNodeMapNodeUnionForWindow(state, recentWindow);
1946
+ let missingHistoryCount = 0;
1947
+ let inWindowSessionCount = 0;
1948
+
1949
+ for (const sessionId of orderedSessionIds) {
1950
+ const session = state.sessions.byId[sessionId];
1951
+ if (!session) continue;
1952
+ const history = state.history.bySessionId.get(sessionId);
1953
+ if (!history?.events) missingHistoryCount += 1;
1954
+ const nodeLabel = getLastKnownSessionNode(history, recentWindow);
1955
+ if (!nodeLabel || !knownNodes.includes(nodeLabel)) continue;
1956
+ inWindowSessionCount += 1;
1957
+ if (!nodeSessionMap.has(nodeLabel)) {
1958
+ nodeSessionMap.set(nodeLabel, []);
1959
+ }
1960
+ nodeSessionMap.get(nodeLabel).push(session);
1961
+ }
1962
+
1963
+ if (knownNodes.length === 0) {
1964
+ return [plainInspectorLine(`No worker activity in the ${recentWindow.label} window.`, "gray")];
1965
+ }
1966
+
1967
+ const availableWidth = Math.max(18, maxWidth);
1968
+ const maxColumns = Math.max(1, Math.floor((availableWidth + 1) / 10));
1969
+ let nodeLabels = knownNodes;
1970
+ if (!allowWideColumns && knownNodes.length > maxColumns) {
1971
+ const visibleCount = Math.max(0, maxColumns - 1);
1972
+ const overflowSessions = [];
1973
+ const visibleLabels = knownNodes.slice(0, visibleCount);
1974
+ for (const hiddenLabel of knownNodes.slice(visibleCount)) {
1975
+ overflowSessions.push(...(nodeSessionMap.get(hiddenLabel) || []));
1976
+ }
1977
+ nodeSessionMap.set("…", overflowSessions);
1978
+ nodeLabels = [...visibleLabels, "…"];
1979
+ }
1980
+
1981
+ const gapWidth = Math.max(0, nodeLabels.length - 1);
1982
+ const colWidth = Math.max(8, Math.floor((availableWidth - gapWidth) / Math.max(1, nodeLabels.length)));
1983
+ const maxRows = nodeLabels.reduce(
1984
+ (max, nodeLabel) => Math.max(max, (nodeSessionMap.get(nodeLabel) || []).length),
1985
+ 0,
1986
+ );
1987
+
1988
+ const lines = [
1989
+ plainInspectorLine(`Window: ${recentWindow.label}`, "gray"),
1990
+ buildNodeMapHeaderLine(nodeLabels, colWidth),
1991
+ plainInspectorLine(nodeLabels.map(() => "─".repeat(colWidth)).join(" "), "gray"),
1992
+ ];
1993
+
1994
+ for (let rowIndex = 0; rowIndex < maxRows; rowIndex += 1) {
1995
+ const rowRuns = [];
1996
+ nodeLabels.forEach((nodeLabel, columnIndex) => {
1997
+ if (columnIndex > 0) rowRuns.push({ text: " ", color: null });
1998
+ const session = (nodeSessionMap.get(nodeLabel) || [])[rowIndex];
1999
+ if (!session) {
2000
+ rowRuns.push({ text: " ".repeat(colWidth), color: null });
2001
+ return;
2002
+ }
2003
+ rowRuns.push(buildNodeMapCell(
2004
+ session,
2005
+ state.branding?.title || "PilotSwarm",
2006
+ colWidth,
2007
+ session.sessionId === state.sessions.activeSessionId,
2008
+ ));
2009
+ });
2010
+ lines.push(rowRuns);
2011
+ }
2012
+
2013
+ if (missingHistoryCount > 0) {
2014
+ lines.push(plainInspectorLine("", "gray"));
2015
+ lines.push(plainInspectorLine(`Loading worker history for ${missingHistoryCount} session(s)…`, "gray"));
2016
+ }
2017
+ if (inWindowSessionCount === 0) {
2018
+ lines.push(plainInspectorLine("", "gray"));
2019
+ lines.push(plainInspectorLine(`No sessions mapped onto worker nodes in the ${recentWindow.label} window.`, "gray"));
2020
+ }
2021
+
2022
+ return lines;
2023
+ }
2024
+
2025
+ export function selectModelPickerModal(state, maxWidth = 72) {
2026
+ const modal = state.ui.modal;
2027
+ if (!modal || modal.type !== "modelPicker") return null;
2028
+
2029
+ const groups = Array.isArray(modal.groups) ? modal.groups : [];
2030
+ const selectedIndex = Math.max(0, Number(modal.selectedIndex) || 0);
2031
+ const contentWidth = Math.max(24, maxWidth - 4);
2032
+ const rows = [];
2033
+ let selectedRowIndex = 0;
2034
+
2035
+ for (const group of groups) {
2036
+ const headerRuns = fitRuns([
2037
+ { text: `${group.providerId}`, color: "cyan", bold: true },
2038
+ { text: ` (${group.providerType || "provider"})`, color: "gray" },
2039
+ ], contentWidth);
2040
+ rows.push(headerRuns);
2041
+
2042
+ for (const model of group.models || []) {
2043
+ const itemIndex = Array.isArray(modal.items)
2044
+ ? modal.items.findIndex((item) => item.id === model.id)
2045
+ : -1;
2046
+ const isSelected = itemIndex === selectedIndex;
2047
+ const labelRuns = fitRuns([
2048
+ { text: " · ", color: "gray" },
2049
+ { text: model.modelName || model.qualifiedName || model.id, color: "white", bold: Boolean(model.isDefault) },
2050
+ ...(model.cost ? [{ text: ` [${model.cost}]`, color: "gray" }] : []),
2051
+ ...(model.isDefault ? [{ text: " ← current default", color: "gray" }] : []),
2052
+ ], contentWidth);
2053
+
2054
+ const line = isSelected
2055
+ ? buildActiveHighlightLine(labelRuns.map((run) => run.text).join("").padEnd(contentWidth, " "))
2056
+ : labelRuns;
2057
+
2058
+ if (isSelected) selectedRowIndex = rows.length;
2059
+ rows.push(line);
2060
+ }
2061
+ }
2062
+
2063
+ const selectedItem = Array.isArray(modal.items) ? modal.items[selectedIndex] || null : null;
2064
+ const detailsLines = selectedItem
2065
+ ? [
2066
+ [{
2067
+ text: selectedItem.modelName || selectedItem.qualifiedName || selectedItem.id,
2068
+ color: "white",
2069
+ bold: true,
2070
+ }],
2071
+ [{
2072
+ text: `${selectedItem.providerId} (${selectedItem.providerType || "provider"})`,
2073
+ color: "gray",
2074
+ }],
2075
+ ...(selectedItem.cost ? [[{ text: `Cost: ${selectedItem.cost}`, color: "gray" }]] : []),
2076
+ [{ text: "", color: "gray" }],
2077
+ [{
2078
+ text: selectedItem.description || "No description available for this model.",
2079
+ color: selectedItem.description ? "white" : "gray",
2080
+ }],
2081
+ ]
2082
+ : [[{ text: "No model selected.", color: "gray" }]];
2083
+
2084
+ return {
2085
+ title: modal.title || "Select model for new session",
2086
+ rows,
2087
+ selectedRowIndex,
2088
+ detailsTitle: "Model Details",
2089
+ detailsLines,
2090
+ idealWidth: Math.min(
2091
+ Math.max(
2092
+ 46,
2093
+ rows.reduce((max, row) => {
2094
+ if (Array.isArray(row)) return Math.max(max, flattenRunsLength(row));
2095
+ return Math.max(max, String(row?.text || "").length);
2096
+ }, 0) + 4,
2097
+ ),
2098
+ maxWidth,
2099
+ ),
2100
+ };
2101
+ }
2102
+
2103
+ export function selectSessionAgentPickerModal(state, maxWidth = 76) {
2104
+ const modal = state.ui.modal;
2105
+ if (!modal || modal.type !== "sessionAgentPicker") return null;
2106
+
2107
+ const items = Array.isArray(modal.items) ? modal.items : [];
2108
+ const selectedIndex = Math.max(0, Number(modal.selectedIndex) || 0);
2109
+ const contentWidth = Math.max(24, maxWidth - 4);
2110
+ const rows = items.map((item, index) => {
2111
+ const isSelected = index === selectedIndex;
2112
+ const labelRuns = fitRuns([
2113
+ { text: item?.kind === "generic" ? " ○ " : " · ", color: "gray" },
2114
+ { text: item?.title || item?.agentName || item?.id || "Agent", color: "white", bold: true },
2115
+ ...(item?.kind === "generic"
2116
+ ? [{ text: " [generic]", color: "gray" }]
2117
+ : [{ text: ` (${item?.agentName || item?.id || "agent"})`, color: "gray" }]),
2118
+ ], contentWidth);
2119
+ return isSelected
2120
+ ? buildActiveHighlightLine(labelRuns.map((run) => run.text).join("").padEnd(contentWidth, " "))
2121
+ : labelRuns;
2122
+ });
2123
+
2124
+ const selectedItem = items[selectedIndex] || null;
2125
+ const selectedModel = modal.sessionOptions?.model || null;
2126
+ const detailsLines = selectedItem
2127
+ ? [
2128
+ [{
2129
+ text: selectedItem.title || selectedItem.agentName || selectedItem.id || "Agent",
2130
+ color: "white",
2131
+ bold: true,
2132
+ }],
2133
+ ...(selectedItem.kind === "generic"
2134
+ ? [[{ text: "Open-ended session", color: "gray" }]]
2135
+ : [[{ text: selectedItem.agentName || selectedItem.id || "agent", color: "gray" }]]),
2136
+ ...(selectedModel ? [[{ text: `Model: ${selectedModel}`, color: "gray" }]] : []),
2137
+ [{ text: "", color: "gray" }],
2138
+ [{
2139
+ text: selectedItem.description || (
2140
+ selectedItem.kind === "generic"
2141
+ ? "Create a general-purpose session without a specialized named agent."
2142
+ : "No description available for this agent."
2143
+ ),
2144
+ color: selectedItem.description ? "white" : "gray",
2145
+ }],
2146
+ [{ text: "", color: "gray" }],
2147
+ [{
2148
+ text: selectedItem.tools?.length
2149
+ ? `Tools: ${selectedItem.tools.join(", ")}`
2150
+ : "Tools: system defaults only",
2151
+ color: "gray",
2152
+ }],
2153
+ ]
2154
+ : [[{ text: "No agent selected.", color: "gray" }]];
2155
+
2156
+ return {
2157
+ title: modal.title || "Select agent for new session",
2158
+ rows,
2159
+ selectedRowIndex: selectedIndex,
2160
+ detailsTitle: "Agent Details",
2161
+ detailsLines,
2162
+ idealWidth: Math.min(
2163
+ Math.max(
2164
+ 52,
2165
+ rows.reduce((max, row) => {
2166
+ if (Array.isArray(row)) return Math.max(max, flattenRunsLength(row));
2167
+ return Math.max(max, String(row?.text || "").length);
2168
+ }, 0) + 4,
2169
+ ),
2170
+ maxWidth,
2171
+ ),
2172
+ };
2173
+ }
2174
+
2175
+ export function selectRenameSessionModal(state, maxWidth = 76) {
2176
+ const modal = state.ui.modal;
2177
+ if (!modal || modal.type !== "renameSession") return null;
2178
+
2179
+ const value = String(modal.value || "");
2180
+ const agentTitlePrefix = typeof modal.agentTitlePrefix === "string" && modal.agentTitlePrefix.trim()
2181
+ ? modal.agentTitlePrefix.trim()
2182
+ : null;
2183
+ const currentTitle = String(modal.currentTitle || "").trim();
2184
+ const previewTitle = value.trim()
2185
+ ? (agentTitlePrefix ? `${agentTitlePrefix}: ${value.trim()}` : value.trim())
2186
+ : (agentTitlePrefix ? `${agentTitlePrefix}: …` : "…");
2187
+
2188
+ const detailsLines = [
2189
+ [{
2190
+ text: "Current: ",
2191
+ color: "gray",
2192
+ }, {
2193
+ text: currentTitle || "(untitled session)",
2194
+ color: currentTitle ? "white" : "gray",
2195
+ }],
2196
+ [{
2197
+ text: "Saved as: ",
2198
+ color: "gray",
2199
+ }, {
2200
+ text: previewTitle,
2201
+ color: "white",
2202
+ bold: true,
2203
+ }],
2204
+ ...(agentTitlePrefix
2205
+ ? [[{
2206
+ text: "Named-agent prefix stays fixed.",
2207
+ color: "gray",
2208
+ }]]
2209
+ : []),
2210
+ ];
2211
+
2212
+ return {
2213
+ title: modal.title || "Rename Session",
2214
+ value,
2215
+ cursorIndex: Math.max(0, Math.min(Number(modal.cursorIndex) || 0, value.length)),
2216
+ placeholder: agentTitlePrefix
2217
+ ? "Type the title after the fixed agent name"
2218
+ : "Type a session title",
2219
+ helpTitle: "Rename Rules",
2220
+ helpLines: [
2221
+ [{
2222
+ text: "Enter",
2223
+ color: "cyan",
2224
+ bold: true,
2225
+ }, {
2226
+ text: " save ",
2227
+ color: "gray",
2228
+ }, {
2229
+ text: "Esc",
2230
+ color: "cyan",
2231
+ bold: true,
2232
+ }, {
2233
+ text: " cancel",
2234
+ color: "gray",
2235
+ }],
2236
+ [{ text: "", color: "gray" }],
2237
+ [{
2238
+ text: "Manual titles stop future automatic LLM title changes for this session.",
2239
+ color: "gray",
2240
+ }],
2241
+ ],
2242
+ detailsLines,
2243
+ idealWidth: Math.min(
2244
+ Math.max(
2245
+ 56,
2246
+ displayLength(currentTitle || "(untitled session)") + 18,
2247
+ displayLength(previewTitle) + 18,
2248
+ ),
2249
+ maxWidth,
2250
+ ),
2251
+ };
2252
+ }
2253
+
2254
+ export function selectArtifactUploadModal(state, maxWidth = 82) {
2255
+ const modal = state.ui.modal;
2256
+ if (!modal || modal.type !== "artifactUpload") return null;
2257
+
2258
+ const value = String(modal.value || "");
2259
+ const sessionId = modal.sessionId || state.ui.promptAttachments?.[0]?.sessionId || state.sessions?.activeSessionId || null;
2260
+ const targetSession = sessionId ? state.sessions?.byId?.[sessionId] || null : null;
2261
+ const targetLabel = sessionId
2262
+ ? (targetSession ? buildSessionTitle(targetSession, state.branding?.title) : shortSessionId(sessionId))
2263
+ : "A new session will be created on upload";
2264
+
2265
+ const detailsLines = [
2266
+ [{
2267
+ text: "Target: ",
2268
+ color: "gray",
2269
+ }, {
2270
+ text: targetLabel,
2271
+ color: sessionId ? "white" : "gray",
2272
+ bold: Boolean(sessionId),
2273
+ }],
2274
+ ];
2275
+
2276
+ return {
2277
+ title: modal.title || "Upload Artifact",
2278
+ value,
2279
+ cursorIndex: Math.max(0, Math.min(Number(modal.cursorIndex) || 0, value.length)),
2280
+ placeholder: "~/path/to/file.md",
2281
+ helpTitle: "Upload Rules",
2282
+ helpLines: [
2283
+ [{
2284
+ text: "Enter",
2285
+ color: "cyan",
2286
+ bold: true,
2287
+ }, {
2288
+ text: " upload ",
2289
+ color: "gray",
2290
+ }, {
2291
+ text: "Esc",
2292
+ color: "cyan",
2293
+ bold: true,
2294
+ }, {
2295
+ text: " cancel",
2296
+ color: "gray",
2297
+ }],
2298
+ [{ text: "", color: "gray" }],
2299
+ [{
2300
+ text: "The file is uploaded into this session's artifact store immediately.",
2301
+ color: "gray",
2302
+ }],
2303
+ [{
2304
+ text: "Use the files browser or @-driven browsing in the prompt to reference it after upload.",
2305
+ color: "gray",
2306
+ }],
2307
+ ],
2308
+ detailsLines,
2309
+ idealWidth: Math.min(
2310
+ Math.max(
2311
+ 60,
2312
+ displayLength(value || "~/path/to/file.md") + 20,
2313
+ displayLength(targetLabel) + 20,
2314
+ ),
2315
+ maxWidth,
2316
+ ),
2317
+ };
2318
+ }
2319
+
2320
+ function buildFilterModalPresentation(modal, currentValues = {}, maxWidth = 96, fallbackTitle = "Filters", fallbackDescription = "Choose how this pane is filtered and rendered.") {
2321
+ const items = Array.isArray(modal.items) ? modal.items : [];
2322
+ const selectedIndex = Math.max(0, Number(modal.selectedIndex) || 0);
2323
+ const selectedItem = items[selectedIndex] || null;
2324
+ const panes = items.map((item, index) => {
2325
+ const currentValue = currentValues?.[item.id] || item.options?.[0]?.id;
2326
+ const lines = (item.options || []).map((option) => (option.id === currentValue
2327
+ ? buildActiveHighlightLine(option.label)
2328
+ : {
2329
+ text: option.label,
2330
+ color: "white",
2331
+ }));
2332
+
2333
+ return {
2334
+ id: item.id,
2335
+ title: item.label,
2336
+ focused: index === selectedIndex,
2337
+ description: item.description || "",
2338
+ lines: lines.length > 0
2339
+ ? lines
2340
+ : [{ text: "No options available.", color: "gray" }],
2341
+ idealWidth: Math.max(
2342
+ 20,
2343
+ String(item.label || "").length + 4,
2344
+ ...(item.options || []).map((option) => String(option?.label || "").length + 4),
2345
+ ),
2346
+ };
2347
+ });
2348
+
2349
+ return {
2350
+ title: modal.title || fallbackTitle,
2351
+ panes,
2352
+ helpTitle: selectedItem?.label || fallbackTitle,
2353
+ helpLines: [
2354
+ [{
2355
+ text: selectedItem?.description || fallbackDescription,
2356
+ color: "white",
2357
+ }],
2358
+ [{ text: "", color: "gray" }],
2359
+ [{
2360
+ text: "Tab/Shift-Tab",
2361
+ color: "cyan",
2362
+ bold: true,
2363
+ }, {
2364
+ text: " switch filter ",
2365
+ color: "gray",
2366
+ }, {
2367
+ text: "Up/Down",
2368
+ color: "cyan",
2369
+ bold: true,
2370
+ }, {
2371
+ text: " change value ",
2372
+ color: "gray",
2373
+ }, {
2374
+ text: "Enter",
2375
+ color: "cyan",
2376
+ bold: true,
2377
+ }, {
2378
+ text: " close ",
2379
+ color: "gray",
2380
+ }, {
2381
+ text: "Esc",
2382
+ color: "cyan",
2383
+ bold: true,
2384
+ }, {
2385
+ text: " cancel",
2386
+ color: "gray",
2387
+ }],
2388
+ ],
2389
+ footerRuns: [
2390
+ { text: "Tab/Shift-Tab", color: "cyan", bold: true },
2391
+ { text: " switch filter · ", color: "gray" },
2392
+ { text: "Up/Down", color: "cyan", bold: true },
2393
+ { text: " change value · ", color: "gray" },
2394
+ { text: "Enter", color: "cyan", bold: true },
2395
+ { text: " close · ", color: "gray" },
2396
+ { text: "Esc", color: "cyan", bold: true },
2397
+ { text: " cancel", color: "gray" },
2398
+ ],
2399
+ idealWidth: Math.min(
2400
+ Math.max(
2401
+ 72,
2402
+ panes.reduce((sum, pane) => sum + pane.idealWidth, 0) + Math.max(0, panes.length - 1) * 2 + 4,
2403
+ ),
2404
+ maxWidth,
2405
+ ),
2406
+ };
2407
+ }
2408
+
2409
+ export function selectLogFilterModal(state, maxWidth = 96) {
2410
+ const modal = state.ui.modal;
2411
+ if (!modal || modal.type !== "logFilter") return null;
2412
+ return buildFilterModalPresentation(
2413
+ modal,
2414
+ state.logs?.filter || {},
2415
+ maxWidth,
2416
+ "Log Filters",
2417
+ "Choose how the log pane is filtered and rendered.",
2418
+ );
2419
+ }
2420
+
2421
+ export function selectFilesFilterModal(state, maxWidth = 88) {
2422
+ const modal = state.ui.modal;
2423
+ if (!modal || modal.type !== "filesFilter") return null;
2424
+ return buildFilterModalPresentation(
2425
+ modal,
2426
+ state.files?.filter || {},
2427
+ maxWidth,
2428
+ "Files Filter",
2429
+ "Choose whether the files browser shows only the selected session or aggregates artifacts across all sessions.",
2430
+ );
2431
+ }
2432
+
2433
+ export function selectArtifactPickerModal(state, maxWidth = 88) {
2434
+ const modal = state.ui.modal;
2435
+ if (!modal || modal.type !== "artifactPicker") return null;
2436
+
2437
+ const items = Array.isArray(modal.items) ? modal.items : [];
2438
+ const selectedIndex = Math.max(0, Number(modal.selectedIndex) || 0);
2439
+ const contentWidth = Math.max(28, maxWidth - 4);
2440
+ const artifactItems = items.filter((item) => item.kind === "artifact");
2441
+ const downloadedCount = artifactItems.reduce((count, item) => {
2442
+ const download = state.files?.bySessionId?.[item.sessionId]?.downloads?.[item.filename];
2443
+ return count + (download?.localPath ? 1 : 0);
2444
+ }, 0);
2445
+ const pendingCount = Math.max(0, artifactItems.length - downloadedCount);
2446
+
2447
+ const rows = items.map((item, index) => {
2448
+ let runs;
2449
+ if (item.kind === "downloadAll") {
2450
+ runs = fitRuns([
2451
+ { text: "dl ", color: "cyan", bold: true },
2452
+ { text: "Download All", color: "white", bold: true },
2453
+ { text: ` [${pendingCount} pending]`, color: "gray" },
2454
+ ], contentWidth);
2455
+ } else {
2456
+ const download = state.files?.bySessionId?.[item.sessionId]?.downloads?.[item.filename];
2457
+ runs = fitRuns([
2458
+ {
2459
+ text: download?.localPath ? "ok " : "dl ",
2460
+ color: download?.localPath ? "green" : "cyan",
2461
+ bold: true,
2462
+ },
2463
+ { text: `${shortSessionId(item.sessionId)}/`, color: "gray" },
2464
+ { text: item.filename, color: "white" },
2465
+ ], contentWidth);
2466
+ }
2467
+
2468
+ if (index !== selectedIndex) return runs;
2469
+ return buildActiveHighlightLine(runs.map((run) => run.text).join("").padEnd(contentWidth, " "));
2470
+ });
2471
+
2472
+ const selectedItem = items[selectedIndex] || null;
2473
+ let detailsLines = [[{ text: "No artifact selected.", color: "gray" }]];
2474
+ if (selectedItem?.kind === "downloadAll") {
2475
+ detailsLines = [
2476
+ [{ text: `${artifactItems.length} artifacts available`, color: "white", bold: true }],
2477
+ [{ text: `${downloadedCount} already downloaded`, color: "gray" }],
2478
+ [{ text: `${pendingCount} pending download`, color: "gray" }],
2479
+ ...(modal.exportDirectory ? [[{ text: `Save location: ${modal.exportDirectory}`, color: "white" }]] : []),
2480
+ [{ text: "", color: "gray" }],
2481
+ [{ text: "Press Enter to download all pending artifacts.", color: "white" }],
2482
+ [{ text: "Press a or Esc to close the picker.", color: "gray" }],
2483
+ ];
2484
+ } else if (selectedItem?.kind === "artifact") {
2485
+ const session = state.sessions?.byId?.[selectedItem.sessionId] || null;
2486
+ const download = state.files?.bySessionId?.[selectedItem.sessionId]?.downloads?.[selectedItem.filename] || null;
2487
+ detailsLines = [
2488
+ [{ text: selectedItem.filename, color: "white", bold: true }],
2489
+ [{ text: session ? buildSessionTitle(session, state.branding?.title) : shortSessionId(selectedItem.sessionId), color: "gray" }],
2490
+ ...(download?.localPath
2491
+ ? [[{ text: `Saved to: ${download.localPath}`, color: "white" }]]
2492
+ : modal.exportDirectory
2493
+ ? [[{ text: `Save location: ${modal.exportDirectory}`, color: "white" }]]
2494
+ : []),
2495
+ [{ text: "", color: "gray" }],
2496
+ [{
2497
+ text: download?.localPath
2498
+ ? "Press Enter to download this artifact again."
2499
+ : "Press Enter to download this artifact.",
2500
+ color: "white",
2501
+ }],
2502
+ [{ text: "Press a or Esc to close the picker.", color: "gray" }],
2503
+ ];
2504
+ }
2505
+
2506
+ return {
2507
+ title: modal.title || "Artifact Downloads",
2508
+ rows: rows.length > 0 ? rows : [{ text: "No artifacts available.", color: "gray" }],
2509
+ selectedRowIndex: selectedIndex,
2510
+ detailsTitle: "Artifact Details",
2511
+ detailsLines,
2512
+ idealWidth: Math.min(
2513
+ Math.max(
2514
+ 54,
2515
+ rows.reduce((max, row) => {
2516
+ if (Array.isArray(row)) return Math.max(max, flattenRunsLength(row));
2517
+ return Math.max(max, String(row?.text || "").length);
2518
+ }, 0) + 4,
2519
+ ),
2520
+ maxWidth,
2521
+ ),
2522
+ };
2523
+ }
2524
+
2525
+ export function selectInspector(state, options = {}) {
2526
+ const session = selectActiveSession(state);
2527
+ const activeTab = state.ui.inspectorTab;
2528
+ const maxWidth = Math.max(18, Number(options?.width) || 36);
2529
+ const allowWideColumns = Boolean(options?.allowWideColumns);
2530
+ const shortId = session ? shortSessionId(session.sessionId) : "";
2531
+ const recentWindow = activeTab === "sequence" || activeTab === "nodes"
2532
+ ? getRecentActivityWindow(state)
2533
+ : null;
2534
+ const title = activeTab === "nodes"
2535
+ ? [
2536
+ { text: "Node Map", color: "magenta", bold: true },
2537
+ { text: ` [${recentWindow.label}]`, color: "gray" },
2538
+ ]
2539
+ : !session
2540
+ ? "No session selected"
2541
+ : activeTab === "sequence"
2542
+ ? [
2543
+ { text: `Sequence: ${shortId}`, color: "magenta", bold: true },
2544
+ { text: ` [${recentWindow.label}]`, color: "gray" },
2545
+ ]
2546
+ : activeTab === "logs"
2547
+ ? `Logs: ${shortId}`
2548
+ : activeTab === "history"
2549
+ ? [
2550
+ { text: `History: ${shortId}`, color: "magenta", bold: true },
2551
+ ]
2552
+ : `Files: ${shortId}`;
2553
+
2554
+ let lines;
2555
+ let stickyLines = [];
2556
+ switch (activeTab) {
2557
+ case "sequence": {
2558
+ const sequenceView = session
2559
+ ? buildSequenceViewForSession(state, session, maxWidth, { allowWideColumns })
2560
+ : { stickyLines: [], lines: ["No session selected."] };
2561
+ stickyLines = sequenceView.stickyLines || [];
2562
+ lines = sequenceView.lines;
2563
+ break;
2564
+ }
2565
+ case "logs":
2566
+ lines = session
2567
+ ? selectLogPane(state, session)
2568
+ : ["No session selected."];
2569
+ break;
2570
+ case "nodes":
2571
+ lines = buildNodeMapLines(state, maxWidth, { allowWideColumns });
2572
+ break;
2573
+ case "files":
2574
+ lines = session
2575
+ ? ["Files view is rendered in the shared host layout."]
2576
+ : ["No session selected."];
2577
+ break;
2578
+ case "history":
2579
+ lines = session
2580
+ ? selectExecutionHistoryPane(state, session)
2581
+ : ["No session selected."];
2582
+ break;
2583
+ default:
2584
+ lines = ["Inspector view is scaffolded in the new architecture."];
2585
+ break;
2586
+ }
2587
+
2588
+ return {
2589
+ title,
2590
+ activeTab,
2591
+ tabs: INSPECTOR_TABS,
2592
+ stickyLines,
2593
+ lines,
2594
+ };
2595
+ }
2596
+
2597
+ // ── Execution History Pane ──────────────────────────────────────────
2598
+
2599
+ const HISTORY_EVENT_KIND_COLORS = {
2600
+ OrchestratorStarted: "green",
2601
+ OrchestratorCompleted: "green",
2602
+ ExecutionStarted: "cyan",
2603
+ ExecutionCompleted: "cyan",
2604
+ TaskScheduled: "yellow",
2605
+ TaskCompleted: "yellow",
2606
+ TaskFailed: "red",
2607
+ SubOrchestrationCreated: "magenta",
2608
+ SubOrchestrationCompleted: "magenta",
2609
+ SubOrchestrationFailed: "red",
2610
+ TimerCreated: "blue",
2611
+ TimerFired: "blue",
2612
+ EventRaised: "white",
2613
+ EventSent: "white",
2614
+ CustomStatusUpdated: "gray",
2615
+ };
2616
+
2617
+ function formatHistoryEventPretty(event) {
2618
+ const ts = event.timestampMs
2619
+ ? new Date(event.timestampMs).toISOString().slice(11, 23)
2620
+ : "???";
2621
+ const color = HISTORY_EVENT_KIND_COLORS[event.kind] || "gray";
2622
+ const lines = [];
2623
+ const header = [
2624
+ { text: `#${event.eventId}`, color: "white", bold: true },
2625
+ { text: ` ${ts}`, color: "gray" },
2626
+ { text: ` ${event.kind}`, color, bold: event.kind.includes("Failed") },
2627
+ ];
2628
+ if (event.sourceEventId != null) {
2629
+ header.push({ text: ` ←#${event.sourceEventId}`, color: "cyan" });
2630
+ }
2631
+ lines.push(header);
2632
+ if (event.data) {
2633
+ try {
2634
+ const parsed = JSON.parse(event.data);
2635
+ if (typeof parsed === "object" && parsed !== null) {
2636
+ for (const [k, v] of Object.entries(parsed)) {
2637
+ const s = typeof v === "string" ? v : JSON.stringify(v);
2638
+ const display = s.length > 100 ? s.slice(0, 97) + "..." : s;
2639
+ lines.push([
2640
+ { text: ` ${k}: `, color: "yellow" },
2641
+ { text: display, color: "white" },
2642
+ ]);
2643
+ }
2644
+ } else {
2645
+ lines.push({ text: ` ${String(parsed).slice(0, 120)}`, color: "white" });
2646
+ }
2647
+ } catch {
2648
+ const display = event.data.length > 120 ? event.data.slice(0, 117) + "..." : event.data;
2649
+ lines.push({ text: ` ${display}`, color: "white" });
2650
+ }
2651
+ }
2652
+ return lines;
2653
+ }
2654
+
2655
+ function formatHistoryEventRaw(event) {
2656
+ const clone = { ...event };
2657
+ if (clone.data) {
2658
+ try { clone.data = JSON.parse(clone.data); } catch { /* keep raw */ }
2659
+ }
2660
+ return JSON.stringify(clone, null, 2);
2661
+ }
2662
+
2663
+ function selectExecutionHistoryPane(state, session) {
2664
+ const entry = state.executionHistory?.bySessionId?.[session.sessionId];
2665
+ if (!entry) return ["No execution history loaded. Press r to refresh."];
2666
+ if (entry.loading) return ["Loading execution history..."];
2667
+ if (entry.error) return [`Error: ${entry.error}`];
2668
+ const events = entry.events;
2669
+ if (!Array.isArray(events) || events.length === 0) return ["No history events found."];
2670
+
2671
+ const format = state.executionHistory?.format || "pretty";
2672
+ const lines = [];
2673
+ lines.push({
2674
+ text: `${events.length} event(s) · format: ${format}`,
2675
+ color: "gray",
2676
+ });
2677
+ lines.push("");
2678
+
2679
+ if (format === "raw") {
2680
+ for (let i = 0; i < events.length; i++) {
2681
+ const rawLines = formatHistoryEventRaw(events[i]).split("\n");
2682
+ for (const line of rawLines) {
2683
+ lines.push({ text: line, color: "gray" });
2684
+ }
2685
+ if (i < events.length - 1) {
2686
+ lines.push({ text: "────────────────────────────────", color: "gray" });
2687
+ }
2688
+ }
2689
+ } else {
2690
+ for (let i = 0; i < events.length; i++) {
2691
+ const eventLines = formatHistoryEventPretty(events[i]);
2692
+ for (const line of eventLines) {
2693
+ lines.push(line);
2694
+ }
2695
+ if (i < events.length - 1) {
2696
+ lines.push({ text: "────────────────────────────────", color: "gray" });
2697
+ }
2698
+ }
2699
+ }
2700
+ return lines;
2701
+ }
2702
+
2703
+ export function selectHistoryFormatModal(state, maxWidth = 88) {
2704
+ const modal = state.ui.modal;
2705
+ if (!modal || modal.type !== "historyFormat") return null;
2706
+ return buildFilterModalPresentation(
2707
+ modal,
2708
+ { format: state.executionHistory?.format || "pretty" },
2709
+ maxWidth,
2710
+ "Execution History Format",
2711
+ "Choose the display format for duroxide execution history events.",
2712
+ );
2713
+ }
2714
+
2715
+ function buildThemeSwatchRuns(entries = []) {
2716
+ const runs = [];
2717
+ for (const entry of entries) {
2718
+ if (runs.length > 0) runs.push({ text: " ", color: "gray" });
2719
+ runs.push({ text: `${entry.label} `, color: "gray" });
2720
+ runs.push({
2721
+ text: "██",
2722
+ color: entry.color,
2723
+ backgroundColor: entry.color,
2724
+ });
2725
+ }
2726
+ return runs;
2727
+ }
2728
+
2729
+ export function selectThemePickerModal(state, maxWidth = 80) {
2730
+ const modal = state.ui.modal;
2731
+ if (!modal || modal.type !== "themePicker") return null;
2732
+
2733
+ const items = Array.isArray(modal.items) ? modal.items : [];
2734
+ const selectedIndex = Math.max(0, Number(modal.selectedIndex) || 0);
2735
+ const selectedItem = items[selectedIndex] || null;
2736
+ const currentThemeId = modal.currentThemeId || state.ui.themeId || null;
2737
+ const currentTheme = items.find((item) => item.id === currentThemeId) || null;
2738
+ const contentWidth = Math.max(24, maxWidth - 4);
2739
+ const rows = items.map((item, index) => {
2740
+ const suffix = item.id === currentThemeId ? " [current]" : "";
2741
+ const text = `${item.label}${suffix}`.slice(0, contentWidth);
2742
+ if (index === selectedIndex) {
2743
+ return buildActiveHighlightLine(text.padEnd(contentWidth, " "));
2744
+ }
2745
+ return [{
2746
+ text,
2747
+ color: item.id === currentThemeId ? "cyan" : "white",
2748
+ bold: item.id === currentThemeId,
2749
+ }];
2750
+ });
2751
+
2752
+ const detailsLines = selectedItem
2753
+ ? [
2754
+ [{ text: `theme: ${selectedItem.description || "Shared theme for the portal and native TUI."}`, color: "white" }],
2755
+ { text: "", color: "gray" },
2756
+ [{
2757
+ text: currentTheme?.id === selectedItem.id
2758
+ ? "Currently active in this TUI session."
2759
+ : `Current theme: ${currentTheme?.label || "Unknown"}`,
2760
+ color: currentTheme?.id === selectedItem.id ? "green" : "gray",
2761
+ }],
2762
+ buildThemeSwatchRuns([
2763
+ { label: "bg", color: selectedItem.tui?.background || selectedItem.terminal?.background || "#000000" },
2764
+ { label: "surface", color: selectedItem.tui?.surface || selectedItem.terminal?.background || "#000000" },
2765
+ { label: "fg", color: selectedItem.tui?.white || selectedItem.terminal?.foreground || "#ffffff" },
2766
+ ]),
2767
+ buildThemeSwatchRuns([
2768
+ { label: "blue", color: selectedItem.tui?.blue || selectedItem.terminal?.blue || "#5555ff" },
2769
+ { label: "green", color: selectedItem.tui?.green || selectedItem.terminal?.green || "#55ff55" },
2770
+ { label: "magenta", color: selectedItem.tui?.magenta || selectedItem.terminal?.magenta || "#ff55ff" },
2771
+ { label: "yellow", color: selectedItem.tui?.yellow || selectedItem.terminal?.yellow || "#ffff55" },
2772
+ ]),
2773
+ { text: "", color: "gray" },
2774
+ [{ text: "Press Enter to apply. The portal browser picker uses this same shared registry.", color: "gray" }],
2775
+ ]
2776
+ : [{ text: "No themes available.", color: "gray" }];
2777
+
2778
+ return {
2779
+ title: modal.title || "Theme Picker",
2780
+ idealWidth: Math.max(60, Math.min(maxWidth, 80)),
2781
+ rows,
2782
+ selectedRowIndex: selectedIndex,
2783
+ detailsTitle: "Theme Details",
2784
+ detailsLines,
2785
+ };
2786
+ }