@agentuity/coder-tui 2.0.8

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 (117) hide show
  1. package/README.md +57 -0
  2. package/dist/chain-preview.d.ts +55 -0
  3. package/dist/chain-preview.d.ts.map +1 -0
  4. package/dist/chain-preview.js +472 -0
  5. package/dist/chain-preview.js.map +1 -0
  6. package/dist/client.d.ts +44 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +411 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/commands.d.ts +22 -0
  11. package/dist/commands.d.ts.map +1 -0
  12. package/dist/commands.js +99 -0
  13. package/dist/commands.js.map +1 -0
  14. package/dist/footer.d.ts +34 -0
  15. package/dist/footer.d.ts.map +1 -0
  16. package/dist/footer.js +249 -0
  17. package/dist/footer.js.map +1 -0
  18. package/dist/handlers.d.ts +24 -0
  19. package/dist/handlers.d.ts.map +1 -0
  20. package/dist/handlers.js +83 -0
  21. package/dist/handlers.js.map +1 -0
  22. package/dist/hub-overlay-state.d.ts +31 -0
  23. package/dist/hub-overlay-state.d.ts.map +1 -0
  24. package/dist/hub-overlay-state.js +78 -0
  25. package/dist/hub-overlay-state.js.map +1 -0
  26. package/dist/hub-overlay.d.ts +146 -0
  27. package/dist/hub-overlay.d.ts.map +1 -0
  28. package/dist/hub-overlay.js +2354 -0
  29. package/dist/hub-overlay.js.map +1 -0
  30. package/dist/inbound-rpc.d.ts +3 -0
  31. package/dist/inbound-rpc.d.ts.map +1 -0
  32. package/dist/inbound-rpc.js +29 -0
  33. package/dist/inbound-rpc.js.map +1 -0
  34. package/dist/index.d.ts +4 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +1641 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/native-remote-ui-context.d.ts +5 -0
  39. package/dist/native-remote-ui-context.d.ts.map +1 -0
  40. package/dist/native-remote-ui-context.js +30 -0
  41. package/dist/native-remote-ui-context.js.map +1 -0
  42. package/dist/output-viewer.d.ts +49 -0
  43. package/dist/output-viewer.d.ts.map +1 -0
  44. package/dist/output-viewer.js +389 -0
  45. package/dist/output-viewer.js.map +1 -0
  46. package/dist/overlay.d.ts +40 -0
  47. package/dist/overlay.d.ts.map +1 -0
  48. package/dist/overlay.js +225 -0
  49. package/dist/overlay.js.map +1 -0
  50. package/dist/protocol.d.ts +605 -0
  51. package/dist/protocol.d.ts.map +1 -0
  52. package/dist/protocol.js +4 -0
  53. package/dist/protocol.js.map +1 -0
  54. package/dist/remote-lifecycle.d.ts +61 -0
  55. package/dist/remote-lifecycle.d.ts.map +1 -0
  56. package/dist/remote-lifecycle.js +190 -0
  57. package/dist/remote-lifecycle.js.map +1 -0
  58. package/dist/remote-session.d.ts +130 -0
  59. package/dist/remote-session.d.ts.map +1 -0
  60. package/dist/remote-session.js +896 -0
  61. package/dist/remote-session.js.map +1 -0
  62. package/dist/remote-tui.d.ts +42 -0
  63. package/dist/remote-tui.d.ts.map +1 -0
  64. package/dist/remote-tui.js +868 -0
  65. package/dist/remote-tui.js.map +1 -0
  66. package/dist/remote-ui-handler.d.ts +5 -0
  67. package/dist/remote-ui-handler.d.ts.map +1 -0
  68. package/dist/remote-ui-handler.js +53 -0
  69. package/dist/remote-ui-handler.js.map +1 -0
  70. package/dist/renderers.d.ts +34 -0
  71. package/dist/renderers.d.ts.map +1 -0
  72. package/dist/renderers.js +669 -0
  73. package/dist/renderers.js.map +1 -0
  74. package/dist/review.d.ts +15 -0
  75. package/dist/review.d.ts.map +1 -0
  76. package/dist/review.js +154 -0
  77. package/dist/review.js.map +1 -0
  78. package/dist/titlebar.d.ts +3 -0
  79. package/dist/titlebar.d.ts.map +1 -0
  80. package/dist/titlebar.js +59 -0
  81. package/dist/titlebar.js.map +1 -0
  82. package/dist/todo/index.d.ts +3 -0
  83. package/dist/todo/index.d.ts.map +1 -0
  84. package/dist/todo/index.js +3 -0
  85. package/dist/todo/index.js.map +1 -0
  86. package/dist/todo/store.d.ts +6 -0
  87. package/dist/todo/store.d.ts.map +1 -0
  88. package/dist/todo/store.js +43 -0
  89. package/dist/todo/store.js.map +1 -0
  90. package/dist/todo/types.d.ts +13 -0
  91. package/dist/todo/types.d.ts.map +1 -0
  92. package/dist/todo/types.js +2 -0
  93. package/dist/todo/types.js.map +1 -0
  94. package/package.json +42 -0
  95. package/src/chain-preview.ts +621 -0
  96. package/src/client.ts +527 -0
  97. package/src/commands.ts +132 -0
  98. package/src/footer.ts +305 -0
  99. package/src/handlers.ts +113 -0
  100. package/src/hub-overlay-state.ts +127 -0
  101. package/src/hub-overlay.ts +3037 -0
  102. package/src/inbound-rpc.ts +35 -0
  103. package/src/index.ts +1963 -0
  104. package/src/native-remote-ui-context.ts +41 -0
  105. package/src/output-viewer.ts +480 -0
  106. package/src/overlay.ts +294 -0
  107. package/src/protocol.ts +758 -0
  108. package/src/remote-lifecycle.ts +270 -0
  109. package/src/remote-session.ts +1100 -0
  110. package/src/remote-tui.ts +1023 -0
  111. package/src/remote-ui-handler.ts +86 -0
  112. package/src/renderers.ts +740 -0
  113. package/src/review.ts +201 -0
  114. package/src/titlebar.ts +63 -0
  115. package/src/todo/index.ts +2 -0
  116. package/src/todo/store.ts +49 -0
  117. package/src/todo/types.ts +14 -0
@@ -0,0 +1,2354 @@
1
+ import { getMarkdownTheme } from '@mariozechner/pi-coding-agent';
2
+ import { matchesKey, Markdown as MdComponent } from '@mariozechner/pi-tui';
3
+ import { buildProjectionFromEntries, normalizeStreamProjection, shouldReplaceStreamProjection, } from "./hub-overlay-state.js";
4
+ import { truncateToWidth } from "./renderers.js";
5
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
6
+ const POLL_MS = 4_000;
7
+ const REQUEST_TIMEOUT_MS = 5_000;
8
+ const HISTORY_FETCH_FAILURE_COOLDOWN_MS = 15_000;
9
+ const MAX_FEED_ITEMS = 80;
10
+ const STREAM_SESSION_LIMIT = 8;
11
+ class HubRequestError extends Error {
12
+ _tag = 'HubRequestError';
13
+ path;
14
+ status;
15
+ statusText;
16
+ body;
17
+ plainArgs;
18
+ constructor(args) {
19
+ super(args.message, args.cause === undefined ? undefined : { cause: args.cause });
20
+ this.name = 'HubRequestError';
21
+ this.path = args.path;
22
+ this.status = args.status;
23
+ this.statusText = args.statusText;
24
+ this.body = args.body;
25
+ this.plainArgs = {
26
+ path: args.path,
27
+ status: args.status,
28
+ statusText: args.statusText,
29
+ body: args.body,
30
+ };
31
+ }
32
+ }
33
+ function visibleWidth(text) {
34
+ return text.replace(ANSI_RE, '').replace(/\t/g, ' ').length;
35
+ }
36
+ function padRight(text, width) {
37
+ if (width <= 0)
38
+ return '';
39
+ const normalized = text.replace(/\t/g, ' ');
40
+ const truncated = truncateToWidth(normalized, width);
41
+ const remaining = width - visibleWidth(truncated);
42
+ return remaining > 0 ? truncated + ' '.repeat(remaining) : truncated;
43
+ }
44
+ function hLine(width) {
45
+ return width > 0 ? '─'.repeat(width) : '';
46
+ }
47
+ function buildTopBorder(width, title) {
48
+ if (width <= 0)
49
+ return '';
50
+ if (width === 1)
51
+ return '╭';
52
+ if (width === 2)
53
+ return '╭╮';
54
+ const inner = width - 2;
55
+ const titleText = ` ${title} `;
56
+ if (titleText.length >= inner)
57
+ return `╭${hLine(inner)}╮`;
58
+ const left = Math.floor((inner - titleText.length) / 2);
59
+ const right = inner - titleText.length - left;
60
+ return `╭${hLine(left)}${titleText}${hLine(right)}╮`;
61
+ }
62
+ function buildBottomBorder(width) {
63
+ if (width <= 0)
64
+ return '';
65
+ if (width === 1)
66
+ return '╰';
67
+ if (width === 2)
68
+ return '╰╯';
69
+ return `╰${hLine(width - 2)}╯`;
70
+ }
71
+ function formatClock(ms) {
72
+ const d = new Date(ms);
73
+ return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
74
+ }
75
+ function formatRelative(value) {
76
+ const ts = typeof value === 'number' ? value : Date.parse(value);
77
+ if (Number.isNaN(ts))
78
+ return '-';
79
+ const seconds = Math.max(0, Math.floor((Date.now() - ts) / 1000));
80
+ if (seconds < 60)
81
+ return `${seconds}s ago`;
82
+ const minutes = Math.floor(seconds / 60);
83
+ if (minutes < 60)
84
+ return `${minutes}m ago`;
85
+ const hours = Math.floor(minutes / 60);
86
+ if (hours < 24)
87
+ return `${hours}h ago`;
88
+ const days = Math.floor(hours / 24);
89
+ return `${days}d ago`;
90
+ }
91
+ function formatElapsedCompact(ms) {
92
+ if (ms < 1_000)
93
+ return `${ms}ms`;
94
+ if (ms < 60_000)
95
+ return `${Math.round(ms / 1_000)}s`;
96
+ if (ms < 3_600_000)
97
+ return `${Math.round(ms / 60_000)}m`;
98
+ return `${Math.round(ms / 3_600_000)}h`;
99
+ }
100
+ function shortId(id) {
101
+ if (id.length <= 12)
102
+ return id;
103
+ return id.slice(0, 12);
104
+ }
105
+ function getVisibleRange(total, selected, windowSize) {
106
+ if (total <= windowSize)
107
+ return [0, total];
108
+ const half = Math.floor(windowSize / 2);
109
+ let start = Math.max(0, selected - half);
110
+ let end = start + windowSize;
111
+ if (end > total) {
112
+ end = total;
113
+ start = end - windowSize;
114
+ }
115
+ return [start, end];
116
+ }
117
+ function wrapText(text, width) {
118
+ if (width <= 0)
119
+ return [''];
120
+ if (!text)
121
+ return [''];
122
+ const lines = [];
123
+ const paragraphs = text.split(/\r?\n/);
124
+ for (const paragraph of paragraphs) {
125
+ const words = paragraph.split(/\s+/).filter(Boolean);
126
+ if (words.length === 0) {
127
+ lines.push('');
128
+ continue;
129
+ }
130
+ let current = words[0];
131
+ for (let i = 1; i < words.length; i++) {
132
+ const word = words[i];
133
+ const candidate = `${current} ${word}`;
134
+ if (candidate.length <= width) {
135
+ current = candidate;
136
+ }
137
+ else {
138
+ lines.push(current);
139
+ current = word.length > width ? word.slice(0, width) : word;
140
+ }
141
+ }
142
+ lines.push(current);
143
+ }
144
+ return lines;
145
+ }
146
+ function toSingleLine(text) {
147
+ return text
148
+ .replace(/[\r\n\t]+/g, ' ')
149
+ .replace(/[\x00-\x1f\x7f]/g, '')
150
+ .replace(/\s+/g, ' ')
151
+ .trim();
152
+ }
153
+ function summarizeArgs(value, maxWidth = 100) {
154
+ if (!value || typeof value !== 'object')
155
+ return '';
156
+ const args = value;
157
+ if (typeof args.command === 'string')
158
+ return truncateToWidth(toSingleLine(args.command), maxWidth);
159
+ if (typeof args.path === 'string')
160
+ return truncateToWidth(toSingleLine(args.path), maxWidth);
161
+ if (typeof args.filePath === 'string')
162
+ return truncateToWidth(toSingleLine(args.filePath), maxWidth);
163
+ if (typeof args.pattern === 'string')
164
+ return truncateToWidth(toSingleLine(args.pattern), maxWidth);
165
+ try {
166
+ const raw = JSON.stringify(value);
167
+ return truncateToWidth(toSingleLine(raw), 100);
168
+ }
169
+ catch {
170
+ return '';
171
+ }
172
+ }
173
+ function summarizeToolCall(name, argsRaw) {
174
+ const args = argsRaw && typeof argsRaw === 'object' ? argsRaw : undefined;
175
+ if (name === 'task') {
176
+ const agent = typeof args?.subagent_type === 'string' ? args.subagent_type : '?';
177
+ const description = typeof args?.description === 'string'
178
+ ? truncateToWidth(toSingleLine(args.description), 80)
179
+ : '';
180
+ return description ? `${agent} - ${description}` : `${agent} - delegated task`;
181
+ }
182
+ if (name === 'parallel_tasks') {
183
+ const tasks = Array.isArray(args?.tasks) ? args.tasks : [];
184
+ const agents = tasks
185
+ .map((task) => task && typeof task === 'object'
186
+ ? task.subagent_type
187
+ : undefined)
188
+ .filter((agent) => typeof agent === 'string');
189
+ if (agents.length > 0)
190
+ return agents.join(' + ');
191
+ return 'parallel delegated tasks';
192
+ }
193
+ return null;
194
+ }
195
+ function extractToolResultText(contentRaw) {
196
+ if (typeof contentRaw === 'string')
197
+ return contentRaw;
198
+ if (!Array.isArray(contentRaw))
199
+ return '';
200
+ const parts = [];
201
+ for (const item of contentRaw) {
202
+ if (!item || typeof item !== 'object')
203
+ continue;
204
+ const block = item;
205
+ if (typeof block.text === 'string') {
206
+ parts.push(block.text);
207
+ }
208
+ }
209
+ return parts.join('\n');
210
+ }
211
+ function formatDuration(ms) {
212
+ if (ms >= 1000)
213
+ return `${(ms / 1000).toFixed(1)}s`;
214
+ return `${ms}ms`;
215
+ }
216
+ function extractMessageSegments(data) {
217
+ const fallback = typeof data?.text === 'string' ? data.text : '';
218
+ const messageRaw = data?.message;
219
+ if (!messageRaw || typeof messageRaw !== 'object') {
220
+ return { output: fallback, thinking: '' };
221
+ }
222
+ const message = messageRaw;
223
+ const role = typeof message.role === 'string' ? message.role : '';
224
+ if (role && role !== 'assistant') {
225
+ return { output: '', thinking: '' };
226
+ }
227
+ const content = message.content;
228
+ const outputParts = [];
229
+ const thinkingParts = [];
230
+ if (typeof content === 'string') {
231
+ outputParts.push(content);
232
+ }
233
+ else if (Array.isArray(content)) {
234
+ for (const blockRaw of content) {
235
+ if (!blockRaw || typeof blockRaw !== 'object')
236
+ continue;
237
+ const block = blockRaw;
238
+ const type = typeof block.type === 'string' ? block.type : '';
239
+ if (type === 'text' && typeof block.text === 'string') {
240
+ outputParts.push(block.text);
241
+ continue;
242
+ }
243
+ if (type === 'thinking' && typeof block.thinking === 'string') {
244
+ thinkingParts.push(block.thinking);
245
+ continue;
246
+ }
247
+ }
248
+ }
249
+ if (outputParts.length === 0 && fallback) {
250
+ outputParts.push(fallback);
251
+ }
252
+ return {
253
+ output: outputParts.join('\n\n').trim(),
254
+ thinking: thinkingParts.join('\n\n').trim(),
255
+ };
256
+ }
257
+ export class HubOverlay {
258
+ focused = true;
259
+ tui;
260
+ theme;
261
+ done;
262
+ baseUrl;
263
+ currentSessionId;
264
+ screen;
265
+ selectedIndex = 0;
266
+ detailSessionId;
267
+ detailScrollOffset = 0;
268
+ detailMaxScroll = 0;
269
+ feedScrollOffset = 0;
270
+ feedMaxScroll = 0;
271
+ feedScope = 'global';
272
+ feedViewMode = 'stream';
273
+ showFeedThinking = true;
274
+ feedFollowing = true;
275
+ taskScrollOffset = 0;
276
+ taskMaxScroll = 0;
277
+ selectedTaskIndex = 0;
278
+ showTaskThinking = true;
279
+ taskFollowing = true;
280
+ sessions = [];
281
+ detail = null;
282
+ cachedTodos = null;
283
+ feed = [];
284
+ sessionFeed = new Map();
285
+ sessionHistoryFeed = new Map();
286
+ sessionBuffers = new Map();
287
+ taskBuffers = new Map();
288
+ streamSources = new Map();
289
+ replayLoadedSessions = new Set();
290
+ replayInFlight = new Set();
291
+ replayRequestControllers = new Map();
292
+ replayRequestGenerations = new Map();
293
+ replayFailureUntil = new Map();
294
+ eventHistoryLoadedSessions = new Set();
295
+ eventHistoryInFlight = new Set();
296
+ eventHistoryRequestControllers = new Map();
297
+ eventHistoryRequestGenerations = new Map();
298
+ eventHistoryFailureUntil = new Map();
299
+ previousDigests = new Map();
300
+ loadingList = true;
301
+ loadingDetail = false;
302
+ listError = '';
303
+ detailError = '';
304
+ detailTodoRequestId = 0;
305
+ resumeBusySessionId = null;
306
+ lastUpdatedAt = 0;
307
+ listInFlight = false;
308
+ detailInFlight = false;
309
+ sseControllers = new Map();
310
+ disposed = false;
311
+ pollTimer = null;
312
+ mdRenderer = null;
313
+ constructor(tui, theme, options) {
314
+ this.tui = tui;
315
+ this.theme = theme;
316
+ this.done = options.done;
317
+ this.baseUrl = options.baseUrl;
318
+ this.currentSessionId = options.currentSessionId;
319
+ this.detailSessionId = options.initialSessionId ?? null;
320
+ this.screen = options.startInDetail && options.initialSessionId ? 'detail' : 'list';
321
+ try {
322
+ const mdTheme = getMarkdownTheme?.();
323
+ if (mdTheme) {
324
+ this.mdRenderer = new MdComponent('', 0, 0, mdTheme);
325
+ }
326
+ }
327
+ catch {
328
+ this.mdRenderer = null;
329
+ }
330
+ void this.refreshList(true);
331
+ if (this.detailSessionId) {
332
+ void this.refreshDetail(this.detailSessionId, true);
333
+ }
334
+ this.pollTimer = setInterval(() => {
335
+ if (this.disposed)
336
+ return;
337
+ void this.refreshList();
338
+ if (this.detailSessionId) {
339
+ void this.refreshDetail(this.detailSessionId);
340
+ }
341
+ }, POLL_MS);
342
+ }
343
+ handleInput(data) {
344
+ if (this.disposed)
345
+ return;
346
+ const inSessionContext = this.isSessionContext();
347
+ if (data === '1') {
348
+ this.screen = inSessionContext ? 'detail' : 'list';
349
+ void this.syncSseStreams(this.sessions);
350
+ this.requestRender();
351
+ return;
352
+ }
353
+ if (data === '2') {
354
+ // Session/task views use task drill-down, not direct feed jumps from task screen.
355
+ if (this.screen === 'task')
356
+ return;
357
+ if (inSessionContext && this.detailSessionId) {
358
+ this.feedScope = 'session';
359
+ this.feedViewMode = 'stream';
360
+ }
361
+ else {
362
+ this.feedScope = 'global';
363
+ this.feedViewMode = 'events';
364
+ }
365
+ this.feedScrollOffset = 0;
366
+ this.feedFollowing = true;
367
+ this.screen = 'feed';
368
+ void this.syncSseStreams(this.sessions);
369
+ this.requestRender();
370
+ return;
371
+ }
372
+ if (data === '3') {
373
+ const atSessionLevel = !!this.detailSessionId &&
374
+ (this.screen === 'detail' || (this.screen === 'feed' && this.feedScope === 'session'));
375
+ if (!atSessionLevel)
376
+ return;
377
+ this.feedScope = 'session';
378
+ this.feedViewMode = 'events';
379
+ this.feedScrollOffset = 0;
380
+ this.feedFollowing = true;
381
+ this.screen = 'feed';
382
+ void this.syncSseStreams(this.sessions);
383
+ this.requestRender();
384
+ return;
385
+ }
386
+ if (matchesKey(data, 'escape')) {
387
+ if (this.screen === 'task') {
388
+ this.screen = 'detail';
389
+ void this.syncSseStreams(this.sessions);
390
+ this.requestRender();
391
+ return;
392
+ }
393
+ if (this.screen === 'feed') {
394
+ this.screen = this.feedScope === 'session' ? 'detail' : 'list';
395
+ void this.syncSseStreams(this.sessions);
396
+ this.requestRender();
397
+ return;
398
+ }
399
+ if (this.screen === 'detail') {
400
+ this.screen = 'list';
401
+ void this.syncSseStreams(this.sessions);
402
+ this.requestRender();
403
+ return;
404
+ }
405
+ this.close();
406
+ return;
407
+ }
408
+ if (matchesKey(data, 'r') || data.toLowerCase() === 'r') {
409
+ void this.refreshList();
410
+ if ((this.screen === 'detail' || this.screen === 'task' || this.screen === 'feed') &&
411
+ this.detailSessionId) {
412
+ void this.refreshDetail(this.detailSessionId);
413
+ }
414
+ return;
415
+ }
416
+ if (this.screen === 'list') {
417
+ this.handleListInput(data);
418
+ return;
419
+ }
420
+ if (this.screen === 'feed') {
421
+ this.handleFeedInput(data);
422
+ return;
423
+ }
424
+ if (this.screen === 'task') {
425
+ this.handleTaskInput(data);
426
+ return;
427
+ }
428
+ this.handleDetailInput(data);
429
+ }
430
+ render(width) {
431
+ const safeWidth = Math.max(6, width);
432
+ const termHeight = process.stdout.rows || 40;
433
+ const maxLines = Math.max(12, Math.floor(termHeight * 0.95) - 2);
434
+ const lines = this.screen === 'detail'
435
+ ? this.renderDetailScreen(safeWidth, maxLines)
436
+ : this.screen === 'feed'
437
+ ? this.renderFeedScreen(safeWidth, maxLines)
438
+ : this.screen === 'task'
439
+ ? this.renderTaskScreen(safeWidth, maxLines)
440
+ : this.renderListScreen(safeWidth, maxLines);
441
+ return lines.map((line) => truncateToWidth(line, safeWidth));
442
+ }
443
+ invalidate() {
444
+ this.requestRender();
445
+ }
446
+ dispose() {
447
+ if (this.disposed)
448
+ return;
449
+ this.disposed = true;
450
+ if (this.pollTimer) {
451
+ clearInterval(this.pollTimer);
452
+ this.pollTimer = null;
453
+ }
454
+ for (const stream of this.sseControllers.values()) {
455
+ stream.controller.abort();
456
+ }
457
+ this.sseControllers.clear();
458
+ for (const controller of this.replayRequestControllers.values()) {
459
+ controller.abort();
460
+ }
461
+ this.replayRequestControllers.clear();
462
+ for (const controller of this.eventHistoryRequestControllers.values()) {
463
+ controller.abort();
464
+ }
465
+ this.eventHistoryRequestControllers.clear();
466
+ }
467
+ requestRender() {
468
+ try {
469
+ this.tui.requestRender();
470
+ }
471
+ catch {
472
+ // Best effort render invalidation.
473
+ }
474
+ }
475
+ nextRequestGeneration(generations, sessionId) {
476
+ const next = (generations.get(sessionId) ?? 0) + 1;
477
+ generations.set(sessionId, next);
478
+ return next;
479
+ }
480
+ invalidateSessionRequest(generations, controllers, inFlight, sessionId) {
481
+ this.nextRequestGeneration(generations, sessionId);
482
+ inFlight.delete(sessionId);
483
+ const controller = controllers.get(sessionId);
484
+ if (controller)
485
+ controller.abort();
486
+ controllers.delete(sessionId);
487
+ }
488
+ isCurrentSessionRequest(generations, controllers, sessionId, generation, controller) {
489
+ return (!controller.signal.aborted &&
490
+ generations.get(sessionId) === generation &&
491
+ controllers.get(sessionId) === controller);
492
+ }
493
+ isFailureCoolingDown(failureUntil, sessionId) {
494
+ const until = failureUntil.get(sessionId);
495
+ if (!until)
496
+ return false;
497
+ if (until <= Date.now()) {
498
+ failureUntil.delete(sessionId);
499
+ return false;
500
+ }
501
+ return true;
502
+ }
503
+ markFailureCooldown(failureUntil, sessionId) {
504
+ failureUntil.set(sessionId, Date.now() + HISTORY_FETCH_FAILURE_COOLDOWN_MS);
505
+ }
506
+ clearReplayFailureCooldown(sessionId) {
507
+ this.replayFailureUntil.delete(sessionId);
508
+ }
509
+ clearEventHistoryFailureCooldown(sessionId) {
510
+ this.eventHistoryFailureUntil.delete(sessionId);
511
+ }
512
+ resetHistoricalSessionState(sessionId) {
513
+ this.invalidateSessionRequest(this.replayRequestGenerations, this.replayRequestControllers, this.replayInFlight, sessionId);
514
+ this.invalidateSessionRequest(this.eventHistoryRequestGenerations, this.eventHistoryRequestControllers, this.eventHistoryInFlight, sessionId);
515
+ this.replayLoadedSessions.delete(sessionId);
516
+ this.eventHistoryLoadedSessions.delete(sessionId);
517
+ this.sessionHistoryFeed.delete(sessionId);
518
+ this.clearReplayFailureCooldown(sessionId);
519
+ this.clearEventHistoryFailureCooldown(sessionId);
520
+ }
521
+ close() {
522
+ this.dispose();
523
+ this.done(undefined);
524
+ }
525
+ handleListInput(data) {
526
+ const count = this.sessions.length;
527
+ if (matchesKey(data, 'up') || data.toLowerCase() === 'k') {
528
+ if (count > 0) {
529
+ this.selectedIndex = (this.selectedIndex - 1 + count) % count;
530
+ this.requestRender();
531
+ }
532
+ return;
533
+ }
534
+ if (matchesKey(data, 'down') || data.toLowerCase() === 'j') {
535
+ if (count > 0) {
536
+ this.selectedIndex = (this.selectedIndex + 1) % count;
537
+ this.requestRender();
538
+ }
539
+ return;
540
+ }
541
+ if (matchesKey(data, 'enter')) {
542
+ const selected = this.sessions[this.selectedIndex];
543
+ if (!selected)
544
+ return;
545
+ this.detailSessionId = selected.sessionId;
546
+ this.detailScrollOffset = 0;
547
+ this.selectedTaskIndex = 0;
548
+ this.showTaskThinking = true;
549
+ this.taskFollowing = true;
550
+ this.screen = 'detail';
551
+ void this.syncSseStreams(this.sessions);
552
+ void this.refreshDetail(selected.sessionId, true);
553
+ this.requestRender();
554
+ }
555
+ }
556
+ isSessionContext() {
557
+ return (this.screen === 'detail' ||
558
+ this.screen === 'task' ||
559
+ (this.screen === 'feed' && this.feedScope === 'session'));
560
+ }
561
+ handleDetailInput(data) {
562
+ const tasks = this.getDetailTasks();
563
+ if ((matchesKey(data, 'w') || data.toLowerCase() === 'w') && this.detailSessionId) {
564
+ if (this.canResumeSession(this.detail)) {
565
+ void this.resumeSession(this.detailSessionId);
566
+ }
567
+ return;
568
+ }
569
+ if (matchesKey(data, 'up')) {
570
+ if (tasks.length > 0) {
571
+ this.selectedTaskIndex = (this.selectedTaskIndex - 1 + tasks.length) % tasks.length;
572
+ this.requestRender();
573
+ }
574
+ return;
575
+ }
576
+ if (matchesKey(data, 'down')) {
577
+ if (tasks.length > 0) {
578
+ this.selectedTaskIndex = (this.selectedTaskIndex + 1) % tasks.length;
579
+ this.requestRender();
580
+ }
581
+ return;
582
+ }
583
+ if (matchesKey(data, 'enter')) {
584
+ if (tasks.length > 0) {
585
+ this.selectedTaskIndex = Math.min(this.selectedTaskIndex, tasks.length - 1);
586
+ this.taskScrollOffset = 0;
587
+ this.showTaskThinking = true;
588
+ this.taskFollowing = true;
589
+ this.screen = 'task';
590
+ this.requestRender();
591
+ }
592
+ return;
593
+ }
594
+ if (data.toLowerCase() === 'k') {
595
+ if (this.detailScrollOffset > 0) {
596
+ this.detailScrollOffset -= 1;
597
+ this.requestRender();
598
+ }
599
+ return;
600
+ }
601
+ if (data.toLowerCase() === 'j') {
602
+ if (this.detailScrollOffset < this.detailMaxScroll) {
603
+ this.detailScrollOffset += 1;
604
+ this.requestRender();
605
+ }
606
+ return;
607
+ }
608
+ if (matchesKey(data, 'pageUp') || matchesKey(data, 'shift+up')) {
609
+ const jump = Math.max(1, Math.floor((process.stdout.rows || 40) / 3));
610
+ this.detailScrollOffset = Math.max(0, this.detailScrollOffset - jump);
611
+ this.requestRender();
612
+ return;
613
+ }
614
+ if (matchesKey(data, 'pageDown') || matchesKey(data, 'shift+down')) {
615
+ const jump = Math.max(1, Math.floor((process.stdout.rows || 40) / 3));
616
+ this.detailScrollOffset = Math.min(this.detailMaxScroll, this.detailScrollOffset + jump);
617
+ this.requestRender();
618
+ }
619
+ }
620
+ handleFeedInput(data) {
621
+ const maxScroll = this.feedMaxScroll;
622
+ const jump = Math.max(1, Math.floor((process.stdout.rows || 40) / 3));
623
+ if (matchesKey(data, 't') || data.toLowerCase() === 't') {
624
+ if (this.feedScope === 'session' && this.feedViewMode === 'stream') {
625
+ this.showFeedThinking = !this.showFeedThinking;
626
+ this.requestRender();
627
+ }
628
+ return;
629
+ }
630
+ if (this.feedScope === 'session' &&
631
+ this.feedViewMode === 'stream' &&
632
+ (matchesKey(data, 'enter') || matchesKey(data, 'v') || data.toLowerCase() === 'v')) {
633
+ const tasks = this.getDetailTasks();
634
+ if (tasks.length > 0) {
635
+ this.selectedTaskIndex = Math.min(this.selectedTaskIndex, tasks.length - 1);
636
+ this.taskScrollOffset = 0;
637
+ this.showTaskThinking = true;
638
+ this.taskFollowing = true;
639
+ this.screen = 'task';
640
+ this.requestRender();
641
+ }
642
+ return;
643
+ }
644
+ if (matchesKey(data, 'f') || data.toLowerCase() === 'f') {
645
+ this.feedFollowing = !this.feedFollowing;
646
+ if (this.feedFollowing) {
647
+ this.feedScrollOffset = maxScroll;
648
+ }
649
+ this.requestRender();
650
+ return;
651
+ }
652
+ if (matchesKey(data, 'up') || data.toLowerCase() === 'k') {
653
+ if (this.feedScrollOffset > 0) {
654
+ this.feedFollowing = false;
655
+ this.feedScrollOffset -= 1;
656
+ this.requestRender();
657
+ }
658
+ return;
659
+ }
660
+ if (matchesKey(data, 'down') || data.toLowerCase() === 'j') {
661
+ if (this.feedScrollOffset < this.feedMaxScroll) {
662
+ this.feedFollowing = false;
663
+ this.feedScrollOffset += 1;
664
+ this.requestRender();
665
+ }
666
+ return;
667
+ }
668
+ if (matchesKey(data, 'pageUp') || matchesKey(data, 'shift+up')) {
669
+ this.feedFollowing = false;
670
+ this.feedScrollOffset = Math.max(0, this.feedScrollOffset - jump);
671
+ this.requestRender();
672
+ return;
673
+ }
674
+ if (matchesKey(data, 'pageDown') || matchesKey(data, 'shift+down')) {
675
+ this.feedFollowing = false;
676
+ this.feedScrollOffset = Math.min(this.feedMaxScroll, this.feedScrollOffset + jump);
677
+ this.requestRender();
678
+ return;
679
+ }
680
+ if (data === 'g') {
681
+ this.feedFollowing = false;
682
+ this.feedScrollOffset = 0;
683
+ this.requestRender();
684
+ return;
685
+ }
686
+ if (data === 'G') {
687
+ this.feedFollowing = false;
688
+ this.feedScrollOffset = this.feedMaxScroll;
689
+ this.requestRender();
690
+ return;
691
+ }
692
+ if (data === '{') {
693
+ this.feedFollowing = false;
694
+ const segment = Math.max(1, Math.floor((this.feedMaxScroll || 1) * 0.25));
695
+ this.feedScrollOffset = Math.max(0, this.feedScrollOffset - segment);
696
+ this.requestRender();
697
+ return;
698
+ }
699
+ if (data === '}') {
700
+ this.feedFollowing = false;
701
+ const segment = Math.max(1, Math.floor((this.feedMaxScroll || 1) * 0.25));
702
+ this.feedScrollOffset = Math.min(this.feedMaxScroll, this.feedScrollOffset + segment);
703
+ this.requestRender();
704
+ return;
705
+ }
706
+ }
707
+ handleTaskInput(data) {
708
+ const tasks = this.getDetailTasks();
709
+ const maxScroll = this.taskMaxScroll;
710
+ const jump = Math.max(1, Math.floor((process.stdout.rows || 40) / 3));
711
+ if (matchesKey(data, 't') || data.toLowerCase() === 't') {
712
+ this.showTaskThinking = !this.showTaskThinking;
713
+ this.requestRender();
714
+ return;
715
+ }
716
+ if (matchesKey(data, 'f') || data.toLowerCase() === 'f') {
717
+ this.taskFollowing = !this.taskFollowing;
718
+ if (this.taskFollowing) {
719
+ this.taskScrollOffset = maxScroll;
720
+ }
721
+ this.requestRender();
722
+ return;
723
+ }
724
+ if (data === '[') {
725
+ if (tasks.length > 0) {
726
+ this.selectedTaskIndex = (this.selectedTaskIndex - 1 + tasks.length) % tasks.length;
727
+ this.taskScrollOffset = 0;
728
+ this.taskFollowing = true;
729
+ this.requestRender();
730
+ }
731
+ return;
732
+ }
733
+ if (data === ']') {
734
+ if (tasks.length > 0) {
735
+ this.selectedTaskIndex = (this.selectedTaskIndex + 1) % tasks.length;
736
+ this.taskScrollOffset = 0;
737
+ this.taskFollowing = true;
738
+ this.requestRender();
739
+ }
740
+ return;
741
+ }
742
+ if (matchesKey(data, 'up') || data.toLowerCase() === 'k') {
743
+ if (this.taskScrollOffset > 0) {
744
+ this.taskFollowing = false;
745
+ this.taskScrollOffset -= 1;
746
+ this.requestRender();
747
+ }
748
+ return;
749
+ }
750
+ if (matchesKey(data, 'down') || data.toLowerCase() === 'j') {
751
+ if (this.taskScrollOffset < this.taskMaxScroll) {
752
+ this.taskFollowing = false;
753
+ this.taskScrollOffset += 1;
754
+ this.requestRender();
755
+ }
756
+ return;
757
+ }
758
+ if (matchesKey(data, 'pageUp') || matchesKey(data, 'shift+up')) {
759
+ this.taskFollowing = false;
760
+ this.taskScrollOffset = Math.max(0, this.taskScrollOffset - jump);
761
+ this.requestRender();
762
+ return;
763
+ }
764
+ if (matchesKey(data, 'pageDown') || matchesKey(data, 'shift+down')) {
765
+ this.taskFollowing = false;
766
+ this.taskScrollOffset = Math.min(this.taskMaxScroll, this.taskScrollOffset + jump);
767
+ this.requestRender();
768
+ return;
769
+ }
770
+ if (data === 'g') {
771
+ this.taskFollowing = false;
772
+ this.taskScrollOffset = 0;
773
+ this.requestRender();
774
+ return;
775
+ }
776
+ if (data === 'G') {
777
+ this.taskFollowing = false;
778
+ this.taskScrollOffset = this.taskMaxScroll;
779
+ this.requestRender();
780
+ return;
781
+ }
782
+ if (data === '{') {
783
+ this.taskFollowing = false;
784
+ const segment = Math.max(1, Math.floor((this.taskMaxScroll || 1) * 0.25));
785
+ this.taskScrollOffset = Math.max(0, this.taskScrollOffset - segment);
786
+ this.requestRender();
787
+ return;
788
+ }
789
+ if (data === '}') {
790
+ this.taskFollowing = false;
791
+ const segment = Math.max(1, Math.floor((this.taskMaxScroll || 1) * 0.25));
792
+ this.taskScrollOffset = Math.min(this.taskMaxScroll, this.taskScrollOffset + segment);
793
+ this.requestRender();
794
+ return;
795
+ }
796
+ }
797
+ getDetailTasks() {
798
+ return this.detail?.tasks ?? [];
799
+ }
800
+ getSessionBuffer(sessionId) {
801
+ let buffer = this.sessionBuffers.get(sessionId);
802
+ if (!buffer) {
803
+ buffer = { output: '', thinking: '' };
804
+ this.sessionBuffers.set(sessionId, buffer);
805
+ }
806
+ return buffer;
807
+ }
808
+ getTaskBuffer(sessionId, taskId) {
809
+ const key = this.getTaskFeedKey(sessionId, taskId);
810
+ let buffer = this.taskBuffers.get(key);
811
+ if (!buffer) {
812
+ buffer = { output: '', thinking: '' };
813
+ this.taskBuffers.set(key, buffer);
814
+ }
815
+ return buffer;
816
+ }
817
+ getStreamSource(sessionId) {
818
+ return this.streamSources.get(sessionId) ?? 'none';
819
+ }
820
+ setStreamSource(sessionId, source) {
821
+ this.streamSources.set(sessionId, source);
822
+ }
823
+ clearTaskBuffers(sessionId) {
824
+ for (const key of this.taskBuffers.keys()) {
825
+ if (key.startsWith(`${sessionId}:`)) {
826
+ this.taskBuffers.delete(key);
827
+ }
828
+ }
829
+ }
830
+ replaceStreamProjection(sessionId, projectionRaw, source) {
831
+ if (!projectionRaw)
832
+ return false;
833
+ const currentSource = this.getStreamSource(sessionId);
834
+ if (!shouldReplaceStreamProjection(currentSource, source))
835
+ return false;
836
+ const projection = normalizeStreamProjection(projectionRaw);
837
+ const sessionBuffer = this.getSessionBuffer(sessionId);
838
+ sessionBuffer.output = projection.output;
839
+ sessionBuffer.thinking = projection.thinking;
840
+ this.clearTaskBuffers(sessionId);
841
+ for (const [taskId, block] of Object.entries(projection.tasks)) {
842
+ const taskBuffer = this.getTaskBuffer(sessionId, taskId);
843
+ taskBuffer.output = block.output;
844
+ taskBuffer.thinking = block.thinking;
845
+ }
846
+ this.setStreamSource(sessionId, source);
847
+ return true;
848
+ }
849
+ applyReplayEntries(sessionId, entries) {
850
+ const projection = buildProjectionFromEntries(entries);
851
+ this.clearReplayFailureCooldown(sessionId);
852
+ if (this.replaceStreamProjection(sessionId, projection, 'replay')) {
853
+ this.replayLoadedSessions.add(sessionId);
854
+ }
855
+ }
856
+ appendBufferText(sessionId, kind, chunk, taskId) {
857
+ if (!chunk)
858
+ return;
859
+ this.clearReplayFailureCooldown(sessionId);
860
+ if (this.replayInFlight.has(sessionId)) {
861
+ this.invalidateSessionRequest(this.replayRequestGenerations, this.replayRequestControllers, this.replayInFlight, sessionId);
862
+ }
863
+ this.setStreamSource(sessionId, 'live');
864
+ // Session stream is lead/top-level only; task-scoped output lives in task buffers.
865
+ if (!taskId) {
866
+ const sessionBuffer = this.getSessionBuffer(sessionId);
867
+ if (kind === 'output')
868
+ sessionBuffer.output += chunk;
869
+ else
870
+ sessionBuffer.thinking += chunk;
871
+ }
872
+ if (taskId) {
873
+ const taskBuffer = this.getTaskBuffer(sessionId, taskId);
874
+ if (kind === 'output')
875
+ taskBuffer.output += chunk;
876
+ else
877
+ taskBuffer.thinking += chunk;
878
+ }
879
+ }
880
+ renderMarkdownLines(text, width) {
881
+ if (!text)
882
+ return [];
883
+ const safeWidth = Math.max(20, width);
884
+ if (this.mdRenderer) {
885
+ try {
886
+ this.mdRenderer.setText(text);
887
+ return this.mdRenderer.render(safeWidth);
888
+ }
889
+ catch {
890
+ return text.split(/\r?\n/);
891
+ }
892
+ }
893
+ return text.split(/\r?\n/);
894
+ }
895
+ renderStreamLines(output, thinking, showThinking, width) {
896
+ const lines = [];
897
+ if (showThinking && thinking.trim()) {
898
+ for (const line of thinking.split(/\r?\n/)) {
899
+ lines.push(this.theme.fg('dim', line));
900
+ }
901
+ lines.push(this.theme.fg('muted', '--- end thinking ---'));
902
+ lines.push('');
903
+ }
904
+ if (output.trim()) {
905
+ lines.push(...this.renderMarkdownLines(output, width));
906
+ }
907
+ return lines;
908
+ }
909
+ buildTopTabs(active, sessionTabs) {
910
+ const divider = this.theme.fg('dim', ' | ');
911
+ const tab = (key, label, isActive) => {
912
+ const text = `[${key}] ${label}`;
913
+ return isActive
914
+ ? this.theme.bold(this.theme.fg('accent', text))
915
+ : this.theme.fg('dim', text);
916
+ };
917
+ if (sessionTabs) {
918
+ return ` ${tab('1', 'Detail', active === 'detail')}${divider}${tab('2', 'Feed', active === 'feed')}${divider}${tab('3', 'Events', active === 'events')}`;
919
+ }
920
+ return ` ${tab('1', 'List', active === 'list')}${divider}${tab('2', 'Feed', active === 'feed')}`;
921
+ }
922
+ async fetchJson(path, timeoutMs = REQUEST_TIMEOUT_MS, init) {
923
+ const controller = new AbortController();
924
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
925
+ try {
926
+ // TODO: Remove/Change when we get Agentuity service level auth enabled, this is just temporary
927
+ const apiKey = process.env.AGENTUITY_CODER_API_KEY;
928
+ const headers = {
929
+ accept: 'application/json',
930
+ ...(init?.headers && typeof init.headers === 'object'
931
+ ? init.headers
932
+ : {}),
933
+ };
934
+ if (apiKey)
935
+ headers['x-agentuity-auth-api-key'] = apiKey;
936
+ const signal = init?.signal
937
+ ? AbortSignal.any([controller.signal, init.signal])
938
+ : controller.signal;
939
+ const response = await fetch(`${this.baseUrl}${path}`, {
940
+ ...init,
941
+ headers,
942
+ signal,
943
+ });
944
+ if (!response.ok) {
945
+ let message = `Hub returned ${response.status}`;
946
+ let rawBody = '';
947
+ try {
948
+ const text = await response.text();
949
+ if (text) {
950
+ rawBody = text;
951
+ try {
952
+ const parsed = JSON.parse(text);
953
+ if (typeof parsed.error === 'string' && parsed.error.trim()) {
954
+ message = parsed.error;
955
+ }
956
+ else if (typeof parsed.message === 'string' && parsed.message.trim()) {
957
+ message = parsed.message;
958
+ }
959
+ else {
960
+ message = text;
961
+ }
962
+ }
963
+ catch {
964
+ message = text;
965
+ }
966
+ }
967
+ }
968
+ catch {
969
+ // Fall back to the status-only message.
970
+ }
971
+ throw new HubRequestError({
972
+ message,
973
+ path,
974
+ status: response.status,
975
+ statusText: response.statusText || undefined,
976
+ body: rawBody || undefined,
977
+ });
978
+ }
979
+ return (await response.json());
980
+ }
981
+ finally {
982
+ clearTimeout(timeout);
983
+ }
984
+ }
985
+ getSessionSummary(sessionId) {
986
+ return this.sessions.find((item) => item.sessionId === sessionId);
987
+ }
988
+ isRuntimeAvailable(sessionId) {
989
+ const detailRuntime = this.detail?.sessionId === sessionId ? this.detail.runtimeAvailable : undefined;
990
+ if (typeof detailRuntime === 'boolean')
991
+ return detailRuntime;
992
+ const session = this.getSessionSummary(sessionId);
993
+ if (typeof session?.runtimeAvailable === 'boolean')
994
+ return session.runtimeAvailable;
995
+ if (session?.historyOnly)
996
+ return false;
997
+ if (session?.bucket === 'history')
998
+ return false;
999
+ return true;
1000
+ }
1001
+ isHistoryOnly(sessionId) {
1002
+ if (this.detail?.sessionId === sessionId && typeof this.detail.historyOnly === 'boolean') {
1003
+ return this.detail.historyOnly;
1004
+ }
1005
+ return this.getSessionSummary(sessionId)?.historyOnly === true;
1006
+ }
1007
+ canResumeSession(detail) {
1008
+ if (!detail)
1009
+ return false;
1010
+ return (detail.mode === 'sandbox' &&
1011
+ detail.bucket === 'paused' &&
1012
+ detail.historyOnly !== true &&
1013
+ detail.runtimeAvailable === false);
1014
+ }
1015
+ isArchivedStatus(status) {
1016
+ return status === 'archived' || status === 'shutdown' || status === 'stopped';
1017
+ }
1018
+ getStatusTone(status) {
1019
+ if (status === 'active' || status === 'running' || status === 'connected') {
1020
+ return 'success';
1021
+ }
1022
+ if (status === 'error' || status === 'failed' || status === 'stopped') {
1023
+ return 'error';
1024
+ }
1025
+ if (status === 'archived' || status === 'shutdown') {
1026
+ return 'muted';
1027
+ }
1028
+ return 'warning';
1029
+ }
1030
+ getConnectionState(input) {
1031
+ const isLocalSession = input.mode === 'tui';
1032
+ const isArchived = this.isArchivedStatus(input.status);
1033
+ const bucket = input.bucket ?? (input.historyOnly ? 'history' : 'running');
1034
+ const runtimeAvailable = input.runtimeAvailable !== false;
1035
+ const controlAvailable = input.controlAvailable ?? runtimeAvailable;
1036
+ const historyOnly = input.historyOnly === true || bucket === 'history';
1037
+ const canWake = !isLocalSession && !isArchived && bucket === 'paused' && historyOnly !== true;
1038
+ if (historyOnly)
1039
+ return { label: 'History only', tone: 'warning', controlAvailable };
1040
+ if (isLocalSession && bucket === 'provisioning')
1041
+ return { label: 'Starting', tone: 'warning', controlAvailable };
1042
+ if (isLocalSession && !isArchived)
1043
+ return { label: 'View only', tone: 'muted', controlAvailable };
1044
+ if (bucket === 'provisioning')
1045
+ return { label: 'Starting', tone: 'warning', controlAvailable };
1046
+ if (canWake) {
1047
+ return {
1048
+ label: this.resumeBusySessionId === input.sessionId ? 'Waking' : 'Paused',
1049
+ tone: this.resumeBusySessionId === input.sessionId ? 'accent' : 'warning',
1050
+ controlAvailable,
1051
+ };
1052
+ }
1053
+ if (!isArchived && runtimeAvailable && !controlAvailable) {
1054
+ return { label: 'Read only', tone: 'muted', controlAvailable };
1055
+ }
1056
+ if (!isArchived && runtimeAvailable)
1057
+ return { label: 'Live', tone: 'success', controlAvailable };
1058
+ if (isArchived)
1059
+ return { label: 'Archived', tone: 'muted', controlAvailable };
1060
+ return { label: 'Disconnected', tone: 'warning', controlAvailable };
1061
+ }
1062
+ formatTagSummary(tags, maxCount = 4) {
1063
+ if (!tags || tags.length === 0)
1064
+ return null;
1065
+ const visible = tags.slice(0, maxCount);
1066
+ const remainder = tags.length - visible.length;
1067
+ return remainder > 0 ? `${visible.join(', ')} +${remainder}` : visible.join(', ');
1068
+ }
1069
+ getSessionEventEntries(sessionId) {
1070
+ if (!this.isRuntimeAvailable(sessionId) || this.isHistoryOnly(sessionId)) {
1071
+ return this.sessionHistoryFeed.get(sessionId) ?? [];
1072
+ }
1073
+ return this.sessionFeed.get(sessionId) ?? [];
1074
+ }
1075
+ async refreshTodos(sessionId) {
1076
+ const requestId = ++this.detailTodoRequestId;
1077
+ try {
1078
+ const todosResponse = await this.fetchJson(`/api/hub/session/${encodeURIComponent(sessionId)}/todos?includeTerminal=true&includeSync=true&limit=30`, 15_000).catch((err) => {
1079
+ const isAbort = err?.name === 'AbortError' || err?.message?.includes('aborted');
1080
+ return {
1081
+ _fetchError: true,
1082
+ message: isAbort ? 'Todos loading...' : err?.message || 'Failed to load todos',
1083
+ };
1084
+ });
1085
+ if (this.disposed ||
1086
+ this.detail?.sessionId !== sessionId ||
1087
+ requestId !== this.detailTodoRequestId) {
1088
+ return;
1089
+ }
1090
+ if (todosResponse._fetchError) {
1091
+ if (this.cachedTodos && this.cachedTodos.sessionId === sessionId) {
1092
+ this.detail.todos = this.cachedTodos.todos;
1093
+ this.detail.todoSummary = this.cachedTodos.summary;
1094
+ }
1095
+ this.detail.todosUnavailable = todosResponse.message;
1096
+ this.requestRender();
1097
+ return;
1098
+ }
1099
+ this.detail.todos = Array.isArray(todosResponse.todos) ? todosResponse.todos : [];
1100
+ this.detail.todoSummary =
1101
+ todosResponse.summary && typeof todosResponse.summary === 'object'
1102
+ ? todosResponse.summary
1103
+ : undefined;
1104
+ this.detail.todosUnavailable = todosResponse.unavailable
1105
+ ? typeof todosResponse.message === 'string'
1106
+ ? todosResponse.message
1107
+ : 'Task service unavailable'
1108
+ : undefined;
1109
+ if (this.detail.todos.length > 0) {
1110
+ this.cachedTodos = {
1111
+ todos: this.detail.todos,
1112
+ summary: this.detail.todoSummary,
1113
+ sessionId,
1114
+ };
1115
+ }
1116
+ this.requestRender();
1117
+ }
1118
+ catch {
1119
+ // Best-effort todos loading.
1120
+ }
1121
+ }
1122
+ async refreshList(initial = false) {
1123
+ if (this.disposed || this.listInFlight)
1124
+ return;
1125
+ this.listInFlight = true;
1126
+ if (initial) {
1127
+ this.loadingList = true;
1128
+ this.listError = '';
1129
+ this.requestRender();
1130
+ }
1131
+ try {
1132
+ const data = await this.fetchJson('/api/hub/sessions');
1133
+ const sessions = (data.sessions?.websocket ?? []).slice().sort((a, b) => {
1134
+ return Date.parse(b.createdAt) - Date.parse(a.createdAt);
1135
+ });
1136
+ this.updateFeedFromList(sessions);
1137
+ this.sessions = sessions;
1138
+ if (this.selectedIndex >= this.sessions.length) {
1139
+ this.selectedIndex = Math.max(0, this.sessions.length - 1);
1140
+ }
1141
+ this.loadingList = false;
1142
+ this.listError = '';
1143
+ this.lastUpdatedAt = Date.now();
1144
+ void this.syncSseStreams(this.sessions);
1145
+ this.requestRender();
1146
+ }
1147
+ catch (err) {
1148
+ this.loadingList = false;
1149
+ this.listError = err instanceof Error ? err.message : String(err);
1150
+ this.requestRender();
1151
+ }
1152
+ finally {
1153
+ this.listInFlight = false;
1154
+ }
1155
+ }
1156
+ async refreshDetail(sessionId, initial = false) {
1157
+ if (this.disposed || this.detailInFlight)
1158
+ return;
1159
+ this.detailInFlight = true;
1160
+ if (initial) {
1161
+ this.loadingDetail = true;
1162
+ this.detailError = '';
1163
+ this.requestRender();
1164
+ }
1165
+ try {
1166
+ const detail = await this.fetchJson(`/api/hub/session/${encodeURIComponent(sessionId)}`);
1167
+ if (this.cachedTodos && this.cachedTodos.sessionId === sessionId) {
1168
+ detail.todos = this.cachedTodos.todos;
1169
+ detail.todoSummary = this.cachedTodos.summary;
1170
+ detail.todosUnavailable = 'Loading todos...';
1171
+ }
1172
+ else {
1173
+ detail.todosUnavailable = 'Loading todos...';
1174
+ }
1175
+ this.detail = detail;
1176
+ this.detailSessionId = sessionId;
1177
+ this.replaceStreamProjection(sessionId, detail.stream, 'snapshot');
1178
+ if (detail.runtimeAvailable !== false && detail.historyOnly !== true) {
1179
+ this.resetHistoricalSessionState(sessionId);
1180
+ }
1181
+ const taskCount = detail.tasks?.length ?? 0;
1182
+ if (taskCount === 0) {
1183
+ this.selectedTaskIndex = 0;
1184
+ }
1185
+ else if (this.selectedTaskIndex >= taskCount) {
1186
+ this.selectedTaskIndex = taskCount - 1;
1187
+ }
1188
+ this.loadingDetail = false;
1189
+ this.detailError = '';
1190
+ this.lastUpdatedAt = Date.now();
1191
+ this.requestRender();
1192
+ void this.refreshTodos(sessionId);
1193
+ if (detail.runtimeAvailable === false || detail.historyOnly === true) {
1194
+ void this.refreshReplay(sessionId);
1195
+ void this.refreshEventHistory(sessionId);
1196
+ }
1197
+ }
1198
+ catch (err) {
1199
+ this.loadingDetail = false;
1200
+ this.detailError = err instanceof Error ? err.message : String(err);
1201
+ this.requestRender();
1202
+ }
1203
+ finally {
1204
+ this.detailInFlight = false;
1205
+ }
1206
+ }
1207
+ async refreshReplay(sessionId) {
1208
+ if (this.isFailureCoolingDown(this.replayFailureUntil, sessionId))
1209
+ return;
1210
+ if (this.replayLoadedSessions.has(sessionId) || this.replayInFlight.has(sessionId))
1211
+ return;
1212
+ const controller = new AbortController();
1213
+ const generation = this.nextRequestGeneration(this.replayRequestGenerations, sessionId);
1214
+ this.replayInFlight.add(sessionId);
1215
+ this.replayRequestControllers.set(sessionId, controller);
1216
+ try {
1217
+ const data = await this.fetchJson(`/api/hub/session/${encodeURIComponent(sessionId)}/replay`, 15_000, { signal: controller.signal });
1218
+ if (!this.isCurrentSessionRequest(this.replayRequestGenerations, this.replayRequestControllers, sessionId, generation, controller)) {
1219
+ return;
1220
+ }
1221
+ this.applyReplayEntries(sessionId, Array.isArray(data.entries) ? data.entries : []);
1222
+ this.replayLoadedSessions.add(sessionId);
1223
+ this.requestRender();
1224
+ }
1225
+ catch (err) {
1226
+ if (controller.signal.aborted ||
1227
+ !this.isCurrentSessionRequest(this.replayRequestGenerations, this.replayRequestControllers, sessionId, generation, controller)) {
1228
+ return;
1229
+ }
1230
+ this.markFailureCooldown(this.replayFailureUntil, sessionId);
1231
+ this.pushFeed(`${this.getSessionLabel(sessionId)}: replay unavailable (${err instanceof Error ? err.message : String(err)})`, sessionId);
1232
+ this.requestRender();
1233
+ }
1234
+ finally {
1235
+ if (this.isCurrentSessionRequest(this.replayRequestGenerations, this.replayRequestControllers, sessionId, generation, controller)) {
1236
+ this.replayInFlight.delete(sessionId);
1237
+ this.replayRequestControllers.delete(sessionId);
1238
+ }
1239
+ }
1240
+ }
1241
+ formatHistoryEventLine(sessionId, event) {
1242
+ const payload = event.payload && typeof event.payload === 'object'
1243
+ ? event.payload
1244
+ : undefined;
1245
+ return (this.formatEventFeedLine(sessionId, event.event, payload) ??
1246
+ this.formatStreamLine(event.event, payload) ??
1247
+ `${event.event}${event.agent ? ` ${event.agent}` : ''}`);
1248
+ }
1249
+ async refreshEventHistory(sessionId) {
1250
+ if (this.isFailureCoolingDown(this.eventHistoryFailureUntil, sessionId)) {
1251
+ return;
1252
+ }
1253
+ if (this.eventHistoryLoadedSessions.has(sessionId) ||
1254
+ this.eventHistoryInFlight.has(sessionId)) {
1255
+ return;
1256
+ }
1257
+ const controller = new AbortController();
1258
+ const generation = this.nextRequestGeneration(this.eventHistoryRequestGenerations, sessionId);
1259
+ this.eventHistoryInFlight.add(sessionId);
1260
+ this.eventHistoryRequestControllers.set(sessionId, controller);
1261
+ try {
1262
+ const data = await this.fetchJson(`/api/hub/session/${encodeURIComponent(sessionId)}/events/history?limit=100`, 15_000, { signal: controller.signal });
1263
+ if (!this.isCurrentSessionRequest(this.eventHistoryRequestGenerations, this.eventHistoryRequestControllers, sessionId, generation, controller)) {
1264
+ return;
1265
+ }
1266
+ const entries = (data.events ?? [])
1267
+ .map((event) => {
1268
+ const text = this.formatHistoryEventLine(sessionId, event);
1269
+ if (!text)
1270
+ return null;
1271
+ return {
1272
+ at: Number.isNaN(Date.parse(event.occurredAt))
1273
+ ? Date.now()
1274
+ : Date.parse(event.occurredAt),
1275
+ sessionId,
1276
+ text,
1277
+ };
1278
+ })
1279
+ .filter((entry) => entry !== null)
1280
+ .sort((a, b) => b.at - a.at);
1281
+ this.sessionHistoryFeed.set(sessionId, entries);
1282
+ this.clearEventHistoryFailureCooldown(sessionId);
1283
+ this.eventHistoryLoadedSessions.add(sessionId);
1284
+ this.requestRender();
1285
+ }
1286
+ catch (err) {
1287
+ if (controller.signal.aborted ||
1288
+ !this.isCurrentSessionRequest(this.eventHistoryRequestGenerations, this.eventHistoryRequestControllers, sessionId, generation, controller)) {
1289
+ return;
1290
+ }
1291
+ this.markFailureCooldown(this.eventHistoryFailureUntil, sessionId);
1292
+ this.pushFeed(`${this.getSessionLabel(sessionId)}: event history unavailable (${err instanceof Error ? err.message : String(err)})`, sessionId);
1293
+ this.requestRender();
1294
+ }
1295
+ finally {
1296
+ if (this.isCurrentSessionRequest(this.eventHistoryRequestGenerations, this.eventHistoryRequestControllers, sessionId, generation, controller)) {
1297
+ this.eventHistoryInFlight.delete(sessionId);
1298
+ this.eventHistoryRequestControllers.delete(sessionId);
1299
+ }
1300
+ }
1301
+ }
1302
+ async resumeSession(sessionId) {
1303
+ if (this.resumeBusySessionId)
1304
+ return;
1305
+ this.resumeBusySessionId = sessionId;
1306
+ this.pushFeed(`${this.getSessionLabel(sessionId)}: resume requested`, sessionId);
1307
+ this.requestRender();
1308
+ try {
1309
+ const response = await this.fetchJson(`/api/hub/session/${encodeURIComponent(sessionId)}/resume`, 15_000, { method: 'POST' });
1310
+ if (response.resumed !== true) {
1311
+ throw new Error(response.error || 'Hub declined to resume the session');
1312
+ }
1313
+ this.resetHistoricalSessionState(sessionId);
1314
+ await this.refreshList();
1315
+ await this.refreshDetail(sessionId);
1316
+ }
1317
+ catch (err) {
1318
+ const message = err instanceof Error ? err.message : String(err);
1319
+ this.detailError = message;
1320
+ this.pushFeed(`${this.getSessionLabel(sessionId)}: resume failed (${message})`, sessionId);
1321
+ this.requestRender();
1322
+ }
1323
+ finally {
1324
+ this.resumeBusySessionId = null;
1325
+ this.requestRender();
1326
+ }
1327
+ }
1328
+ updateFeedFromList(sessions) {
1329
+ if (this.previousDigests.size === 0) {
1330
+ if (sessions.length > 0) {
1331
+ this.pushFeed(`Loaded ${sessions.length} active session${sessions.length === 1 ? '' : 's'}`);
1332
+ }
1333
+ for (const session of sessions) {
1334
+ this.previousDigests.set(session.sessionId, {
1335
+ status: session.status,
1336
+ taskCount: session.taskCount,
1337
+ observerCount: session.observerCount,
1338
+ subAgentCount: session.subAgentCount,
1339
+ });
1340
+ }
1341
+ return;
1342
+ }
1343
+ const nextDigests = new Map();
1344
+ for (const session of sessions) {
1345
+ const prev = this.previousDigests.get(session.sessionId);
1346
+ const label = session.label || shortId(session.sessionId);
1347
+ if (!prev) {
1348
+ this.pushFeed(`${label}: session discovered (${session.mode})`, session.sessionId);
1349
+ }
1350
+ else {
1351
+ if (prev.status !== session.status) {
1352
+ this.pushFeed(`${label}: ${prev.status} -> ${session.status}`, session.sessionId);
1353
+ }
1354
+ if (session.taskCount > prev.taskCount) {
1355
+ const delta = session.taskCount - prev.taskCount;
1356
+ this.pushFeed(`${label}: +${delta} task${delta === 1 ? '' : 's'}`, session.sessionId);
1357
+ }
1358
+ if (session.observerCount !== prev.observerCount) {
1359
+ this.pushFeed(`${label}: observers ${prev.observerCount} -> ${session.observerCount}`, session.sessionId);
1360
+ }
1361
+ if (session.subAgentCount !== prev.subAgentCount) {
1362
+ this.pushFeed(`${label}: agents ${prev.subAgentCount} -> ${session.subAgentCount}`, session.sessionId);
1363
+ }
1364
+ }
1365
+ nextDigests.set(session.sessionId, {
1366
+ status: session.status,
1367
+ taskCount: session.taskCount,
1368
+ observerCount: session.observerCount,
1369
+ subAgentCount: session.subAgentCount,
1370
+ });
1371
+ }
1372
+ for (const oldSessionId of this.previousDigests.keys()) {
1373
+ if (!nextDigests.has(oldSessionId)) {
1374
+ this.pushFeed(`${shortId(oldSessionId)}: session removed`, oldSessionId);
1375
+ }
1376
+ }
1377
+ this.previousDigests = nextDigests;
1378
+ }
1379
+ async syncSseStreams(sessions) {
1380
+ if (this.disposed)
1381
+ return;
1382
+ const desiredModes = new Map();
1383
+ for (const session of sessions.slice(0, STREAM_SESSION_LIMIT)) {
1384
+ if (session.runtimeAvailable === false || session.historyOnly === true)
1385
+ continue;
1386
+ desiredModes.set(session.sessionId, 'summary');
1387
+ }
1388
+ if (this.detailSessionId &&
1389
+ this.isSessionContext() &&
1390
+ this.isRuntimeAvailable(this.detailSessionId) &&
1391
+ !this.isHistoryOnly(this.detailSessionId)) {
1392
+ desiredModes.set(this.detailSessionId, 'full');
1393
+ }
1394
+ for (const [sessionId, stream] of this.sseControllers) {
1395
+ const desiredMode = desiredModes.get(sessionId);
1396
+ if (!desiredMode) {
1397
+ stream.controller.abort();
1398
+ this.sseControllers.delete(sessionId);
1399
+ continue;
1400
+ }
1401
+ if (stream.mode !== desiredMode) {
1402
+ stream.controller.abort();
1403
+ this.sseControllers.delete(sessionId);
1404
+ }
1405
+ }
1406
+ for (const [sessionId, mode] of desiredModes) {
1407
+ if (!this.sseControllers.has(sessionId)) {
1408
+ void this.startSseStream(sessionId, mode);
1409
+ }
1410
+ }
1411
+ }
1412
+ async startSseStream(sessionId, mode) {
1413
+ if (this.disposed)
1414
+ return;
1415
+ const existing = this.sseControllers.get(sessionId);
1416
+ if (existing && existing.mode === mode)
1417
+ return;
1418
+ if (existing) {
1419
+ existing.controller.abort();
1420
+ this.sseControllers.delete(sessionId);
1421
+ }
1422
+ const controller = new AbortController();
1423
+ this.sseControllers.set(sessionId, { controller, mode });
1424
+ const subscribe = mode === 'full' ? '*' : 'session_*,task_*,agent_*';
1425
+ try {
1426
+ // TODO: Remove/Change when we get Agentuity service level auth enabled, this is just temporary
1427
+ const apiKey = process.env.AGENTUITY_CODER_API_KEY;
1428
+ const sseHeaders = { accept: 'text/event-stream' };
1429
+ if (apiKey)
1430
+ sseHeaders['x-agentuity-auth-api-key'] = apiKey;
1431
+ const response = await fetch(`${this.baseUrl}/api/hub/session/${encodeURIComponent(sessionId)}/events?subscribe=${encodeURIComponent(subscribe)}`, {
1432
+ headers: sseHeaders,
1433
+ signal: controller.signal,
1434
+ });
1435
+ if (!response.ok || !response.body) {
1436
+ throw new Error(`Hub returned ${response.status} for stream`);
1437
+ }
1438
+ const reader = response.body.getReader();
1439
+ const decoder = new TextDecoder();
1440
+ let buffer = '';
1441
+ while (true) {
1442
+ const { done, value } = await reader.read();
1443
+ if (done)
1444
+ break;
1445
+ buffer += decoder.decode(value, { stream: true });
1446
+ buffer = this.consumeSseBuffer(sessionId, mode, buffer);
1447
+ }
1448
+ }
1449
+ catch (err) {
1450
+ if (controller.signal.aborted || this.disposed)
1451
+ return;
1452
+ const label = this.getSessionLabel(sessionId);
1453
+ const msg = err instanceof Error ? err.message : String(err);
1454
+ this.pushFeed(`${label}: stream error (${msg})`, sessionId);
1455
+ this.requestRender();
1456
+ }
1457
+ finally {
1458
+ const current = this.sseControllers.get(sessionId);
1459
+ if (current && current.controller === controller) {
1460
+ this.sseControllers.delete(sessionId);
1461
+ }
1462
+ }
1463
+ }
1464
+ consumeSseBuffer(sessionId, mode, rawBuffer) {
1465
+ const normalized = rawBuffer.replace(/\r\n/g, '\n');
1466
+ let cursor = 0;
1467
+ while (true) {
1468
+ const boundary = normalized.indexOf('\n\n', cursor);
1469
+ if (boundary === -1)
1470
+ break;
1471
+ const block = normalized.slice(cursor, boundary);
1472
+ cursor = boundary + 2;
1473
+ if (!block.trim())
1474
+ continue;
1475
+ let eventName = 'message';
1476
+ const dataLines = [];
1477
+ for (const line of block.split('\n')) {
1478
+ if (line.startsWith('event:')) {
1479
+ eventName = line.slice(6).trim() || eventName;
1480
+ }
1481
+ else if (line.startsWith('data:')) {
1482
+ dataLines.push(line.slice(5).trimStart());
1483
+ }
1484
+ }
1485
+ const dataText = dataLines.join('\n');
1486
+ this.handleSseEvent(sessionId, mode, eventName, dataText);
1487
+ }
1488
+ return normalized.slice(cursor);
1489
+ }
1490
+ handleSseEvent(sessionId, mode, sseEvent, dataText) {
1491
+ let payload = undefined;
1492
+ if (dataText) {
1493
+ try {
1494
+ payload = JSON.parse(dataText);
1495
+ }
1496
+ catch {
1497
+ payload = dataText;
1498
+ }
1499
+ }
1500
+ let eventName = sseEvent;
1501
+ let eventData = payload;
1502
+ if (payload && typeof payload === 'object') {
1503
+ const record = payload;
1504
+ if (typeof record.event === 'string') {
1505
+ eventName = record.event;
1506
+ eventData = record.data;
1507
+ }
1508
+ }
1509
+ if (eventName === 'hydration') {
1510
+ this.applyHydration(sessionId, eventData);
1511
+ return;
1512
+ }
1513
+ if (eventName === 'snapshot') {
1514
+ return;
1515
+ }
1516
+ const data = eventData && typeof eventData === 'object'
1517
+ ? eventData
1518
+ : undefined;
1519
+ const taskId = typeof data?.taskId === 'string' ? data.taskId : undefined;
1520
+ const text = this.formatEventFeedLine(sessionId, eventName, eventData);
1521
+ if (text) {
1522
+ this.pushFeed(text, sessionId);
1523
+ }
1524
+ if (mode === 'full') {
1525
+ this.captureStreamContent(sessionId, eventName, eventData, taskId);
1526
+ const streamLine = this.formatStreamLine(eventName, eventData);
1527
+ if (streamLine) {
1528
+ this.pushSessionFeed(sessionId, streamLine, taskId);
1529
+ }
1530
+ }
1531
+ this.requestRender();
1532
+ if (this.detailSessionId === sessionId &&
1533
+ (eventName === 'session_join' ||
1534
+ eventName === 'session_leave' ||
1535
+ eventName === 'task_start' ||
1536
+ eventName === 'task_complete' ||
1537
+ eventName === 'task_error' ||
1538
+ eventName === 'session_shutdown')) {
1539
+ void this.refreshDetail(sessionId);
1540
+ }
1541
+ if (eventName === 'session_shutdown') {
1542
+ const stream = this.sseControllers.get(sessionId);
1543
+ if (stream) {
1544
+ stream.controller.abort();
1545
+ this.sseControllers.delete(sessionId);
1546
+ }
1547
+ }
1548
+ }
1549
+ applyHydration(sessionId, eventData) {
1550
+ if (!eventData || typeof eventData !== 'object')
1551
+ return;
1552
+ const payload = eventData;
1553
+ const loadedFromProjection = this.replaceStreamProjection(sessionId, payload.stream, 'hydration');
1554
+ if (!loadedFromProjection) {
1555
+ const entries = Array.isArray(payload.entries)
1556
+ ? payload.entries.filter((entry) => !!entry && typeof entry === 'object')
1557
+ : [];
1558
+ this.replaceStreamProjection(sessionId, buildProjectionFromEntries(entries), 'hydration');
1559
+ }
1560
+ }
1561
+ captureStreamContent(sessionId, eventName, eventData, taskId) {
1562
+ const data = eventData && typeof eventData === 'object'
1563
+ ? eventData
1564
+ : undefined;
1565
+ if (eventName === 'message_update') {
1566
+ const assistantMessageEvent = data?.assistantMessageEvent;
1567
+ if (assistantMessageEvent && typeof assistantMessageEvent === 'object') {
1568
+ const ame = assistantMessageEvent;
1569
+ const type = typeof ame.type === 'string' ? ame.type : '';
1570
+ const delta = typeof ame.delta === 'string' ? ame.delta : '';
1571
+ if (type === 'text_delta' && delta) {
1572
+ this.appendBufferText(sessionId, 'output', delta, taskId);
1573
+ }
1574
+ else if (type === 'thinking_delta' && delta) {
1575
+ this.appendBufferText(sessionId, 'thinking', delta, taskId);
1576
+ }
1577
+ }
1578
+ return;
1579
+ }
1580
+ if (eventName === 'message_end') {
1581
+ const segments = extractMessageSegments(data);
1582
+ if (segments.thinking) {
1583
+ this.appendBufferText(sessionId, 'thinking', segments.thinking + '\n\n', taskId);
1584
+ }
1585
+ if (segments.output) {
1586
+ this.appendBufferText(sessionId, 'output', segments.output + '\n\n', taskId);
1587
+ }
1588
+ return;
1589
+ }
1590
+ if (eventName === 'thinking_end') {
1591
+ const text = typeof data?.text === 'string' ? data.text : '';
1592
+ if (text)
1593
+ this.appendBufferText(sessionId, 'thinking', text + '\n\n', taskId);
1594
+ return;
1595
+ }
1596
+ if (eventName === 'agent_progress') {
1597
+ const status = typeof data?.status === 'string' ? data.status : '';
1598
+ const delta = typeof data?.delta === 'string' ? data.delta : '';
1599
+ if (status === 'text_delta' && delta) {
1600
+ this.appendBufferText(sessionId, 'output', delta, taskId);
1601
+ }
1602
+ else if (status === 'thinking_delta' && delta) {
1603
+ this.appendBufferText(sessionId, 'thinking', delta, taskId);
1604
+ }
1605
+ return;
1606
+ }
1607
+ if (eventName === 'tool_call' || eventName === 'tool_result') {
1608
+ const line = this.formatStreamLine(eventName, eventData);
1609
+ if (line)
1610
+ this.appendBufferText(sessionId, 'output', `${line}\n\n`, taskId);
1611
+ return;
1612
+ }
1613
+ if (eventName === 'task_complete') {
1614
+ const result = typeof data?.result === 'string' ? data.result : '';
1615
+ if (result)
1616
+ this.appendBufferText(sessionId, 'output', result + '\n\n', taskId);
1617
+ return;
1618
+ }
1619
+ if (eventName === 'task_error') {
1620
+ const error = typeof data?.error === 'string' ? data.error : '';
1621
+ if (error)
1622
+ this.appendBufferText(sessionId, 'output', `[task error] ${error}\n`, taskId);
1623
+ }
1624
+ }
1625
+ formatEventFeedLine(sessionId, eventName, eventData) {
1626
+ const label = this.getSessionLabel(sessionId);
1627
+ const data = eventData && typeof eventData === 'object'
1628
+ ? eventData
1629
+ : undefined;
1630
+ if (eventName === 'task_start') {
1631
+ const taskId = typeof data?.taskId === 'string' ? shortId(data.taskId) : 'task';
1632
+ const agent = typeof data?.agent === 'string' ? data.agent : 'agent';
1633
+ return `${label}: ${taskId} started (${agent})`;
1634
+ }
1635
+ if (eventName === 'task_complete') {
1636
+ const taskId = typeof data?.taskId === 'string' ? shortId(data.taskId) : 'task';
1637
+ const duration = typeof data?.duration === 'number' ? ` ${data.duration}ms` : '';
1638
+ return `${label}: ${taskId} completed${duration}`;
1639
+ }
1640
+ if (eventName === 'task_error') {
1641
+ const taskId = typeof data?.taskId === 'string' ? shortId(data.taskId) : 'task';
1642
+ return `${label}: ${taskId} failed`;
1643
+ }
1644
+ if (eventName === 'session_join' || eventName === 'session_leave') {
1645
+ const participant = data?.participant;
1646
+ const role = typeof participant?.role === 'string' ? participant.role : 'participant';
1647
+ return `${label}: ${role} ${eventName === 'session_join' ? 'joined' : 'left'}`;
1648
+ }
1649
+ if (eventName === 'agent_start' || eventName === 'agent_end') {
1650
+ const agent = typeof data?.agentName === 'string'
1651
+ ? data.agentName
1652
+ : typeof data?.agent === 'string'
1653
+ ? data.agent
1654
+ : 'agent';
1655
+ return `${label}: ${agent} ${eventName === 'agent_start' ? 'started' : 'ended'}`;
1656
+ }
1657
+ if (eventName === 'session_complete' ||
1658
+ eventName === 'session_error' ||
1659
+ eventName === 'session_shutdown') {
1660
+ return `${label}: ${eventName}`;
1661
+ }
1662
+ return null;
1663
+ }
1664
+ getSessionLabel(sessionId) {
1665
+ const session = this.sessions.find((item) => item.sessionId === sessionId);
1666
+ return session?.label || shortId(sessionId);
1667
+ }
1668
+ getFeedEntries(scope) {
1669
+ if (scope === 'session' && this.detailSessionId) {
1670
+ return this.getSessionEventEntries(this.detailSessionId);
1671
+ }
1672
+ return this.feed;
1673
+ }
1674
+ pushFeed(text, sessionId) {
1675
+ this.feed.unshift({ at: Date.now(), sessionId, text });
1676
+ if (this.feed.length > MAX_FEED_ITEMS) {
1677
+ this.feed.length = MAX_FEED_ITEMS;
1678
+ }
1679
+ }
1680
+ pushSessionFeed(sessionId, text, taskId) {
1681
+ const entries = this.sessionFeed.get(sessionId) ?? [];
1682
+ entries.unshift({ at: Date.now(), sessionId, text });
1683
+ if (entries.length > MAX_FEED_ITEMS)
1684
+ entries.length = MAX_FEED_ITEMS;
1685
+ this.sessionFeed.set(sessionId, entries);
1686
+ if (taskId) {
1687
+ // Keep task-scoped stream buffers warm for immediate task-view drill-in.
1688
+ this.getTaskBuffer(sessionId, taskId);
1689
+ }
1690
+ }
1691
+ formatStreamLine(eventName, eventData) {
1692
+ const data = eventData && typeof eventData === 'object'
1693
+ ? eventData
1694
+ : undefined;
1695
+ const normalize = (value) => value.replace(/\s+/g, ' ').trim();
1696
+ if (eventName === 'message_update') {
1697
+ const assistantMessageEvent = data?.assistantMessageEvent;
1698
+ if (assistantMessageEvent && typeof assistantMessageEvent === 'object') {
1699
+ const ame = assistantMessageEvent;
1700
+ const type = typeof ame.type === 'string' ? ame.type : '';
1701
+ const delta = typeof ame.delta === 'string' ? ame.delta : '';
1702
+ // Keep event view high-signal: show text streaming, suppress thinking token spam.
1703
+ if (type === 'text_delta' && delta) {
1704
+ const cleaned = truncateToWidth(normalize(delta), 120);
1705
+ if (cleaned) {
1706
+ return `${type || 'delta'} ${cleaned}`;
1707
+ }
1708
+ }
1709
+ }
1710
+ return null;
1711
+ }
1712
+ if (eventName === 'message_end') {
1713
+ const segments = extractMessageSegments(data);
1714
+ const output = segments.output || segments.thinking;
1715
+ const cleaned = output ? truncateToWidth(normalize(output), 120) : '';
1716
+ return cleaned ? `message ${cleaned}` : 'message_end';
1717
+ }
1718
+ if (eventName === 'thinking_end' && typeof data?.text === 'string') {
1719
+ const cleaned = truncateToWidth(normalize(data.text), 120);
1720
+ return cleaned ? `thinking ${cleaned}` : 'thinking_end';
1721
+ }
1722
+ if (eventName === 'tool_call') {
1723
+ const name = typeof data?.name === 'string'
1724
+ ? data.name
1725
+ : typeof data?.toolName === 'string'
1726
+ ? data.toolName
1727
+ : 'tool';
1728
+ const input = data?.args ?? data?.input;
1729
+ const summarized = summarizeToolCall(name, input);
1730
+ if (summarized)
1731
+ return summarized;
1732
+ const argsPreview = summarizeArgs(input, 90);
1733
+ return argsPreview ? `tool_call ${name} ${argsPreview}` : `tool_call ${name}`;
1734
+ }
1735
+ if (eventName === 'tool_result') {
1736
+ const name = typeof data?.name === 'string'
1737
+ ? data.name
1738
+ : typeof data?.toolName === 'string'
1739
+ ? data.toolName
1740
+ : 'tool';
1741
+ const input = (data?.args ?? data?.input);
1742
+ if (name === 'task') {
1743
+ const agent = typeof input?.subagent_type === 'string' ? input.subagent_type : 'agent';
1744
+ const description = typeof input?.description === 'string'
1745
+ ? truncateToWidth(toSingleLine(input.description), 80)
1746
+ : '';
1747
+ const header = description ? `${agent} - ${description}` : `${agent} - delegated task`;
1748
+ const rawResult = extractToolResultText(data?.content);
1749
+ const stats = rawResult.match(/_(\w+): (\d+)ms \| (\d+) in (\d+) out tokens \| \$([0-9.]+)_/);
1750
+ if (stats) {
1751
+ const durationMs = Number(stats[2] ?? 0);
1752
+ const tokIn = stats[3] ?? '0';
1753
+ const tokOut = stats[4] ?? '0';
1754
+ const cost = stats[5] ?? '0';
1755
+ return `${header}\ndone ${formatDuration(durationMs)} ↑${tokIn} ↓${tokOut} $${cost}`;
1756
+ }
1757
+ const details = data?.details && typeof data.details === 'object'
1758
+ ? data.details
1759
+ : undefined;
1760
+ const duration = typeof details?.duration === 'number' ? ` ${formatDuration(details.duration)}` : '';
1761
+ const failed = data?.isError === true || details?.error === true;
1762
+ return `${header}\n${failed ? 'failed' : `done${duration}`}`;
1763
+ }
1764
+ return `tool_result ${name}`;
1765
+ }
1766
+ if (eventName === 'agent_progress') {
1767
+ const agent = typeof data?.agentName === 'string' ? data.agentName : 'agent';
1768
+ const status = typeof data?.status === 'string' ? data.status : 'progress';
1769
+ const toolName = typeof data?.currentTool === 'string' ? data.currentTool : '';
1770
+ const toolArgsRaw = typeof data?.currentToolArgs === 'string' ? data.currentToolArgs : '';
1771
+ const toolArgs = toolArgsRaw ? truncateToWidth(normalize(toolArgsRaw), 80) : '';
1772
+ // Deltas are already represented in rendered stream mode; skip them in event mode
1773
+ // to avoid noisy, low-signal token lines.
1774
+ if (status.endsWith('_delta')) {
1775
+ return null;
1776
+ }
1777
+ if (status === 'tool_start') {
1778
+ const parts = ['tool_call', agent];
1779
+ if (toolName)
1780
+ parts.push(toolName);
1781
+ if (toolArgs)
1782
+ parts.push(toolArgs);
1783
+ return parts.join(' ');
1784
+ }
1785
+ if (status === 'tool_end') {
1786
+ return toolName ? `tool_result ${agent} ${toolName}` : `tool_result ${agent}`;
1787
+ }
1788
+ if (status === 'completed' || status === 'failed') {
1789
+ return `agent ${agent} ${status}`;
1790
+ }
1791
+ if (status === 'running' && toolName) {
1792
+ return toolArgs
1793
+ ? `agent ${agent} running ${toolName} ${toolArgs}`
1794
+ : `agent ${agent} running ${toolName}`;
1795
+ }
1796
+ return null;
1797
+ }
1798
+ const parts = [];
1799
+ if (typeof data?.taskId === 'string')
1800
+ parts.push(shortId(data.taskId));
1801
+ if (typeof data?.agent === 'string')
1802
+ parts.push(data.agent);
1803
+ else if (typeof data?.agentName === 'string')
1804
+ parts.push(data.agentName);
1805
+ else if (typeof data?.name === 'string')
1806
+ parts.push(data.name);
1807
+ if (typeof data?.status === 'string')
1808
+ parts.push(data.status);
1809
+ if (typeof data?.message === 'string')
1810
+ parts.push(data.message);
1811
+ if (typeof data?.error === 'string')
1812
+ parts.push(`error: ${data.error}`);
1813
+ if (typeof data?.text === 'string')
1814
+ parts.push(data.text);
1815
+ if (typeof data?.delta === 'string')
1816
+ parts.push(data.delta);
1817
+ let detail = parts.find((part) => part.trim().length > 0) ?? '';
1818
+ if (detail) {
1819
+ detail = truncateToWidth(normalize(detail), 120);
1820
+ return `${eventName} ${detail}`;
1821
+ }
1822
+ if (eventData !== undefined) {
1823
+ try {
1824
+ const serialized = typeof eventData === 'string' ? eventData : JSON.stringify(eventData);
1825
+ if (serialized) {
1826
+ const compact = truncateToWidth(serialized.replace(/\s+/g, ' ').trim(), 140);
1827
+ return `${eventName} ${compact}`;
1828
+ }
1829
+ }
1830
+ catch {
1831
+ // Best-effort formatting only.
1832
+ }
1833
+ }
1834
+ return null;
1835
+ }
1836
+ getTaskFeedKey(sessionId, taskId) {
1837
+ return `${sessionId}:${taskId}`;
1838
+ }
1839
+ renderListScreen(width, maxLines) {
1840
+ const inner = Math.max(0, width - 2);
1841
+ const lines = [];
1842
+ const headerRows = 2;
1843
+ const footerRows = 2;
1844
+ const contentBudget = Math.max(5, maxLines - headerRows - footerRows);
1845
+ const body = [];
1846
+ lines.push(buildTopBorder(width, 'Coder Hub Sessions'));
1847
+ lines.push(this.contentLine('', inner));
1848
+ if (this.loadingList) {
1849
+ body.push(this.contentLine(this.theme.fg('dim', ' Loading sessions...'), inner));
1850
+ }
1851
+ else if (this.listError) {
1852
+ body.push(this.contentLine(this.theme.fg('error', ` ${this.listError}`), inner));
1853
+ }
1854
+ else {
1855
+ const updated = this.lastUpdatedAt
1856
+ ? `${formatClock(this.lastUpdatedAt)} updated`
1857
+ : 'not updated';
1858
+ const liveCount = this.sessions.filter((session) => {
1859
+ const connection = this.getConnectionState({
1860
+ sessionId: session.sessionId,
1861
+ mode: session.mode,
1862
+ status: session.status,
1863
+ bucket: session.bucket,
1864
+ runtimeAvailable: session.runtimeAvailable,
1865
+ controlAvailable: session.controlAvailable,
1866
+ historyOnly: session.historyOnly,
1867
+ });
1868
+ return connection.label === 'Live';
1869
+ }).length;
1870
+ const pausedCount = this.sessions.filter((session) => session.bucket === 'paused' && session.historyOnly !== true).length;
1871
+ const historyCount = this.sessions.filter((session) => session.historyOnly === true || session.bucket === 'history').length;
1872
+ body.push(this.contentLine(this.theme.fg('muted', ` Active: ${this.sessions.length} sessions Live:${liveCount} Paused:${pausedCount} History:${historyCount} ${updated}`), inner));
1873
+ }
1874
+ body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
1875
+ body.push(this.contentLine(this.theme.bold(' Teams / Observers'), inner));
1876
+ if (this.sessions.length === 0 && !this.loadingList && !this.listError) {
1877
+ body.push(this.contentLine(this.theme.fg('muted', ' No active Hub sessions'), inner));
1878
+ }
1879
+ else if (!this.loadingList && !this.listError) {
1880
+ const listBudget = Math.max(1, contentBudget - body.length);
1881
+ const [start, end] = getVisibleRange(this.sessions.length, this.selectedIndex, listBudget);
1882
+ if (start > 0) {
1883
+ body.push(this.contentLine(this.theme.fg('dim', ` ↑ ${start} more above`), inner));
1884
+ }
1885
+ for (let i = start; i < end; i++) {
1886
+ const session = this.sessions[i];
1887
+ const selected = i === this.selectedIndex;
1888
+ const marker = selected ? this.theme.fg('accent', '›') : ' ';
1889
+ const label = session.label || shortId(session.sessionId);
1890
+ const name = selected ? this.theme.bold(label) : label;
1891
+ const status = this.theme.fg(this.getStatusTone(session.status), session.status);
1892
+ const connection = this.getConnectionState({
1893
+ sessionId: session.sessionId,
1894
+ mode: session.mode,
1895
+ status: session.status,
1896
+ bucket: session.bucket,
1897
+ runtimeAvailable: session.runtimeAvailable,
1898
+ controlAvailable: session.controlAvailable,
1899
+ historyOnly: session.historyOnly,
1900
+ });
1901
+ const connectionLabel = this.theme.fg(connection.tone, connection.label);
1902
+ const self = this.currentSessionId === session.sessionId
1903
+ ? this.theme.fg('accent', ' (this)')
1904
+ : '';
1905
+ const tagSummary = this.formatTagSummary(session.tags, 2);
1906
+ const metricsParts = [
1907
+ `obs:${session.observerCount}`,
1908
+ `agents:${session.subAgentCount}`,
1909
+ `tasks:${session.taskCount}`,
1910
+ formatRelative(session.createdAt),
1911
+ session.defaultAgent ? `agent:${session.defaultAgent}` : undefined,
1912
+ tagSummary ? `tags:${tagSummary}` : undefined,
1913
+ ].filter((part) => !!part);
1914
+ const metrics = this.theme.fg('muted', metricsParts.join(' '));
1915
+ body.push(this.contentLine(` ${marker} ${name}${self} ${status} ${session.mode} ${connectionLabel} ${metrics}`, inner));
1916
+ }
1917
+ if (end < this.sessions.length) {
1918
+ body.push(this.contentLine(this.theme.fg('dim', ` ↓ ${this.sessions.length - end} more below`), inner));
1919
+ }
1920
+ }
1921
+ const windowedBody = body.slice(0, contentBudget);
1922
+ lines.push(...windowedBody);
1923
+ while (lines.length < maxLines - footerRows) {
1924
+ lines.push(this.contentLine('', inner));
1925
+ }
1926
+ lines.push(this.contentLine(this.theme.fg('dim', ' [↑↓] Select [Enter] Open [r] Refresh [Esc] Close'), inner));
1927
+ lines.push(buildBottomBorder(width));
1928
+ return lines.slice(0, maxLines);
1929
+ }
1930
+ renderDetailScreen(width, maxLines) {
1931
+ const inner = Math.max(0, width - 2);
1932
+ const lines = [];
1933
+ const title = this.detail?.label ||
1934
+ (this.detailSessionId ? shortId(this.detailSessionId) : 'Hub Session');
1935
+ const headerRows = 3;
1936
+ const footerRows = 2;
1937
+ const contentBudget = Math.max(5, maxLines - headerRows - footerRows);
1938
+ lines.push(buildTopBorder(width, `Session ${title}`));
1939
+ lines.push(this.contentLine('', inner));
1940
+ lines.push(this.contentLine(this.buildTopTabs('detail', true), inner));
1941
+ const body = [];
1942
+ if (this.loadingDetail) {
1943
+ body.push(this.contentLine(this.theme.fg('dim', ' Loading session detail...'), inner));
1944
+ }
1945
+ else if (this.detailError) {
1946
+ body.push(this.contentLine(this.theme.fg('error', ` ${this.detailError}`), inner));
1947
+ }
1948
+ else if (!this.detail) {
1949
+ body.push(this.contentLine(this.theme.fg('muted', ' No detail available'), inner));
1950
+ }
1951
+ else {
1952
+ const session = this.detail;
1953
+ const participants = session.participants ?? [];
1954
+ const tasks = session.tasks ?? [];
1955
+ const activityEntries = Object.entries(session.agentActivity ?? {});
1956
+ const connection = this.getConnectionState({
1957
+ sessionId: session.sessionId,
1958
+ mode: session.mode,
1959
+ status: session.status,
1960
+ bucket: session.bucket,
1961
+ runtimeAvailable: session.runtimeAvailable,
1962
+ controlAvailable: session.controlAvailable,
1963
+ historyOnly: session.historyOnly,
1964
+ });
1965
+ const inactiveTasks = session.diagnostics?.inactiveRunningTasks ?? [];
1966
+ const inactiveTaskById = new Map(inactiveTasks.map((item) => [item.taskId, item]));
1967
+ const pushWrappedLine = (text, tone = 'muted') => {
1968
+ for (const wrapped of wrapText(text, Math.max(12, inner - 4))) {
1969
+ body.push(this.contentLine(this.theme.fg(tone, ` ${wrapped}`), inner));
1970
+ }
1971
+ };
1972
+ body.push(this.contentLine(this.theme.bold(' Overview'), inner));
1973
+ body.push(this.contentLine(this.theme.fg('muted', ` ID: ${session.sessionId}`), inner));
1974
+ body.push(this.contentLine(` Status: ${this.theme.fg(this.getStatusTone(session.status), session.status)} Mode: ${session.mode} Bucket: ${session.bucket ?? '-'}`, inner));
1975
+ body.push(this.contentLine(` Connection: ${this.theme.fg(connection.tone, connection.label)} Runtime: ${session.runtimeAvailable === false ? 'offline' : 'ready'} Control: ${connection.controlAvailable ? 'enabled' : 'read-only'}`, inner));
1976
+ body.push(this.contentLine(this.theme.fg('muted', ` Created: ${formatRelative(session.createdAt)}`), inner));
1977
+ body.push(this.contentLine(this.theme.fg('muted', ` Participants: ${participants.length} Tasks: ${tasks.length} Active agents: ${activityEntries.length}`), inner));
1978
+ if (session.defaultAgent) {
1979
+ body.push(this.contentLine(this.theme.fg('muted', ` Default agent: ${session.defaultAgent}`), inner));
1980
+ }
1981
+ const tagSummary = this.formatTagSummary(session.tags);
1982
+ if (tagSummary) {
1983
+ pushWrappedLine(`Tags: ${tagSummary}`);
1984
+ }
1985
+ if (session.streamId || session.streamUrl) {
1986
+ const streamState = session.streamId
1987
+ ? shortId(session.streamId)
1988
+ : session.streamUrl
1989
+ ? 'attached'
1990
+ : 'none';
1991
+ body.push(this.contentLine(this.theme.fg('muted', ` Stream: ${streamState}`), inner));
1992
+ }
1993
+ if (session.context?.branch) {
1994
+ body.push(this.contentLine(this.theme.fg('muted', ` Branch: ${session.context.branch}`), inner));
1995
+ }
1996
+ if (session.context?.workingDirectory) {
1997
+ body.push(this.contentLine(this.theme.fg('muted', ` CWD: ${session.context.workingDirectory}`), inner));
1998
+ }
1999
+ if (session.task) {
2000
+ body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
2001
+ body.push(this.contentLine(this.theme.bold(' Root Task'), inner));
2002
+ for (const wrapped of wrapText(session.task, Math.max(12, inner - 4))) {
2003
+ body.push(this.contentLine(` ${wrapped}`, inner));
2004
+ }
2005
+ }
2006
+ if (session.error) {
2007
+ body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
2008
+ body.push(this.contentLine(this.theme.bold(' Error'), inner));
2009
+ pushWrappedLine(session.error, 'error');
2010
+ }
2011
+ if (this.canResumeSession(session)) {
2012
+ const resumeMessage = this.resumeBusySessionId === session.sessionId
2013
+ ? 'Waking sandbox session...'
2014
+ : 'Press [w] to wake this paused sandbox session.';
2015
+ pushWrappedLine(resumeMessage, this.resumeBusySessionId === session.sessionId ? 'muted' : 'warning');
2016
+ }
2017
+ body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
2018
+ body.push(this.contentLine(this.theme.bold(' Tasks'), inner));
2019
+ body.push(this.contentLine(this.theme.fg('dim', ' Use ↑ and ↓ to move, Enter to open task view'), inner));
2020
+ if (tasks.length === 0) {
2021
+ body.push(this.contentLine(this.theme.fg('dim', ' (no tasks yet)'), inner));
2022
+ }
2023
+ else {
2024
+ if (this.selectedTaskIndex >= tasks.length) {
2025
+ this.selectedTaskIndex = tasks.length - 1;
2026
+ }
2027
+ for (let i = 0; i < Math.min(tasks.length, 50); i++) {
2028
+ const task = tasks[i];
2029
+ const selected = i === this.selectedTaskIndex;
2030
+ const statusColor = task.status === 'completed'
2031
+ ? 'success'
2032
+ : task.status === 'failed'
2033
+ ? 'error'
2034
+ : task.status === 'running'
2035
+ ? 'accent'
2036
+ : 'warning';
2037
+ const status = this.theme.fg(statusColor, task.status);
2038
+ const inactive = inactiveTaskById.get(task.taskId);
2039
+ const prompt = task.prompt
2040
+ ? truncateToWidth(toSingleLine(task.prompt), Math.max(16, inner - 34))
2041
+ : '';
2042
+ const duration = typeof task.duration === 'number'
2043
+ ? ` ${formatElapsedCompact(task.duration)}`
2044
+ : '';
2045
+ const idle = inactive
2046
+ ? ` ${this.theme.fg('warning', `idle ${formatElapsedCompact(inactive.inactivityMs)}`)}`
2047
+ : '';
2048
+ const marker = selected ? this.theme.fg('accent', '›') : ' ';
2049
+ body.push(this.contentLine(`${marker} ${shortId(task.taskId).padEnd(12)} ${task.agent.padEnd(9)} ${status}${duration}${idle} ${prompt}`, inner));
2050
+ }
2051
+ }
2052
+ body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
2053
+ body.push(this.contentLine(this.theme.bold(' Session Todos'), inner));
2054
+ if (session.todosUnavailable) {
2055
+ body.push(this.contentLine(this.theme.fg('warning', ` ${session.todosUnavailable}`), inner));
2056
+ }
2057
+ else {
2058
+ const todos = session.todos ?? [];
2059
+ const summary = session.todoSummary ?? {
2060
+ open: 0,
2061
+ in_progress: 0,
2062
+ done: 0,
2063
+ closed: 0,
2064
+ cancelled: 0,
2065
+ };
2066
+ body.push(this.contentLine(this.theme.fg('dim', ` open:${summary.open ?? 0} in_progress:${summary.in_progress ?? 0} done:${(summary.done ?? 0) + (summary.closed ?? 0)} cancelled:${summary.cancelled ?? 0}`), inner));
2067
+ if (todos.length === 0) {
2068
+ body.push(this.contentLine(this.theme.fg('dim', ` (no todos linked to session ${shortId(session.sessionId)})`), inner));
2069
+ }
2070
+ else {
2071
+ for (const todo of todos.slice(0, 20)) {
2072
+ const statusColor = todo.status === 'done' || todo.status === 'closed'
2073
+ ? 'success'
2074
+ : todo.status === 'cancelled'
2075
+ ? 'error'
2076
+ : todo.status === 'in_progress'
2077
+ ? 'accent'
2078
+ : 'warning';
2079
+ const status = this.theme.fg(statusColor, todo.status);
2080
+ const details = [
2081
+ typeof todo.priority === 'string' ? `prio:${todo.priority}` : undefined,
2082
+ typeof todo.type === 'string' ? `type:${todo.type}` : undefined,
2083
+ typeof todo.assignee?.name === 'string' && todo.assignee.name.length > 0
2084
+ ? `owner:${todo.assignee.name}`
2085
+ : typeof todo.assignee?.id === 'string' && todo.assignee.id.length > 0
2086
+ ? `owner:${todo.assignee.id}`
2087
+ : undefined,
2088
+ typeof todo.attachmentCount === 'number' && todo.attachmentCount > 0
2089
+ ? `att:${todo.attachmentCount}`
2090
+ : undefined,
2091
+ ].filter((part) => !!part);
2092
+ const title = truncateToWidth(toSingleLine(todo.title || ''), Math.max(16, inner - 48));
2093
+ const meta = details.length > 0 ? this.theme.fg('dim', ` ${details.join(' ')}`) : '';
2094
+ body.push(this.contentLine(` ${shortId(todo.id).padEnd(12)} ${status} ${title}${meta}`, inner));
2095
+ }
2096
+ if (todos.length > 20) {
2097
+ body.push(this.contentLine(this.theme.fg('dim', ` ... ${todos.length - 20} more todos`), inner));
2098
+ }
2099
+ }
2100
+ }
2101
+ body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
2102
+ body.push(this.contentLine(this.theme.bold(' Participants'), inner));
2103
+ if (participants.length === 0) {
2104
+ body.push(this.contentLine(this.theme.fg('dim', ' (none)'), inner));
2105
+ }
2106
+ else {
2107
+ for (const participant of participants) {
2108
+ const when = participant.connectedAt ? formatRelative(participant.connectedAt) : '-';
2109
+ const idle = participant.idle ? this.theme.fg('warning', ' idle') : '';
2110
+ body.push(this.contentLine(` ${participant.id.padEnd(12)} ${participant.role.padEnd(9)} ${(participant.transport || 'ws').padEnd(3)} ${when}${idle}`, inner));
2111
+ }
2112
+ }
2113
+ if (inactiveTasks.length > 0) {
2114
+ body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
2115
+ body.push(this.contentLine(this.theme.bold(' Diagnostics'), inner));
2116
+ body.push(this.contentLine(this.theme.fg('warning', ` ${inactiveTasks.length} running task${inactiveTasks.length === 1 ? '' : 's'} without recent activity`), inner));
2117
+ for (const item of inactiveTasks.slice(0, 8)) {
2118
+ const lastSeen = item.lastActivityAt
2119
+ ? ` last ${formatRelative(item.lastActivityAt)}`
2120
+ : '';
2121
+ body.push(this.contentLine(this.theme.fg('warning', ` ${shortId(item.taskId).padEnd(12)} ${item.agent.padEnd(10)} idle ${formatElapsedCompact(item.inactivityMs)} started ${formatRelative(item.startedAt)}${lastSeen}`), inner));
2122
+ }
2123
+ }
2124
+ body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
2125
+ body.push(this.contentLine(this.theme.bold(' Agent Activity'), inner));
2126
+ if (activityEntries.length === 0) {
2127
+ body.push(this.contentLine(this.theme.fg('dim', ' (none)'), inner));
2128
+ }
2129
+ else {
2130
+ for (const [agent, info] of activityEntries.slice(0, 15)) {
2131
+ const status = info.status || 'idle';
2132
+ const statusTone = status === 'completed'
2133
+ ? 'success'
2134
+ : status === 'failed'
2135
+ ? 'error'
2136
+ : status === 'tool_start' || status === 'running'
2137
+ ? 'accent'
2138
+ : 'warning';
2139
+ const tool = info.currentTool ? ` tool:${info.currentTool}` : '';
2140
+ const calls = typeof info.toolCallCount === 'number' ? ` calls:${info.toolCallCount}` : '';
2141
+ const lastSeen = info.lastActivity !== undefined && info.lastActivity !== null
2142
+ ? ` last:${formatRelative(info.lastActivity)}`
2143
+ : '';
2144
+ const totalElapsed = typeof info.totalElapsed === 'number'
2145
+ ? ` total:${formatElapsedCompact(info.totalElapsed)}`
2146
+ : '';
2147
+ body.push(this.contentLine(` ${agent.padEnd(12)} ${this.theme.fg(statusTone, status)}${this.theme.fg('dim', `${tool}${calls}${lastSeen}${totalElapsed}`)}`, inner));
2148
+ if (info.currentToolArgs) {
2149
+ for (const wrapped of wrapText(toSingleLine(info.currentToolArgs), Math.max(12, inner - 6))) {
2150
+ body.push(this.contentLine(this.theme.fg('dim', ` ${wrapped}`), inner));
2151
+ }
2152
+ }
2153
+ }
2154
+ }
2155
+ }
2156
+ this.detailMaxScroll = Math.max(0, body.length - contentBudget);
2157
+ if (this.detailScrollOffset > this.detailMaxScroll) {
2158
+ this.detailScrollOffset = this.detailMaxScroll;
2159
+ }
2160
+ const windowedBody = body.slice(this.detailScrollOffset, this.detailScrollOffset + contentBudget);
2161
+ lines.push(...windowedBody);
2162
+ while (lines.length < maxLines - footerRows) {
2163
+ lines.push(this.contentLine('', inner));
2164
+ }
2165
+ const scrollInfo = this.detailMaxScroll > 0
2166
+ ? this.theme.fg('dim', ` scroll ${this.detailScrollOffset}/${this.detailMaxScroll}`)
2167
+ : this.theme.fg('dim', ' scroll 0/0');
2168
+ const wakeHint = this.canResumeSession(this.detail) ? ' [w] Wake' : '';
2169
+ lines.push(this.contentLine(`${scrollInfo} ${this.theme.fg('dim', `[↑↓] Task [j/k] Scroll [Enter] Open${wakeHint} [r] Refresh [Esc] Back`)}`, inner));
2170
+ lines.push(buildBottomBorder(width));
2171
+ return lines.slice(0, maxLines);
2172
+ }
2173
+ renderFeedScreen(width, maxLines) {
2174
+ const inner = Math.max(0, width - 2);
2175
+ const lines = [];
2176
+ const headerRows = 5;
2177
+ const footerRows = 2;
2178
+ const contentBudget = Math.max(5, maxLines - headerRows - footerRows);
2179
+ const scoped = this.feedScope === 'session' && !!this.detailSessionId;
2180
+ const sessionId = scoped ? this.detailSessionId : null;
2181
+ const historySession = sessionId
2182
+ ? !this.isRuntimeAvailable(sessionId) || this.isHistoryOnly(sessionId)
2183
+ : false;
2184
+ const title = scoped
2185
+ ? `${this.feedViewMode === 'events' ? 'Session Events' : 'Session Feed'} ${this.getSessionLabel(sessionId)}`
2186
+ : 'Global Feed';
2187
+ const entryBudget = Math.max(1, contentBudget - 2);
2188
+ const sessionBuffer = sessionId ? this.sessionBuffers.get(sessionId) : undefined;
2189
+ lines.push(buildTopBorder(width, title));
2190
+ lines.push(this.contentLine('', inner));
2191
+ lines.push(this.contentLine(this.buildTopTabs(scoped ? (this.feedViewMode === 'events' ? 'events' : 'feed') : 'feed', scoped), inner));
2192
+ lines.push(this.contentLine(this.theme.fg('muted', scoped
2193
+ ? this.feedViewMode === 'stream'
2194
+ ? historySession
2195
+ ? ' Rehydrated session output from stored replay — [v] task stream'
2196
+ : ' Live rendered session output (sub-agent style) — [v] task stream'
2197
+ : historySession
2198
+ ? ' Stored session events from history'
2199
+ : ' Live full session events'
2200
+ : ' Streaming event summaries across all sessions'), inner));
2201
+ lines.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
2202
+ let contentLines = [];
2203
+ if (scoped && this.feedViewMode === 'stream') {
2204
+ contentLines = this.renderStreamLines(sessionBuffer?.output ?? '', sessionBuffer?.thinking ?? '', this.showFeedThinking, Math.max(12, inner - 4));
2205
+ if (contentLines.length === 0) {
2206
+ if (sessionId && historySession && this.replayInFlight.has(sessionId)) {
2207
+ contentLines = [this.theme.fg('dim', '(loading replay...)')];
2208
+ }
2209
+ else if (sessionId && historySession && this.replayLoadedSessions.has(sessionId)) {
2210
+ contentLines = [this.theme.fg('dim', '(no replay output available)')];
2211
+ }
2212
+ else {
2213
+ contentLines = [this.theme.fg('dim', '(no streamed output yet)')];
2214
+ }
2215
+ }
2216
+ }
2217
+ else {
2218
+ const entries = this.getFeedEntries(scoped ? 'session' : 'global');
2219
+ contentLines = [...entries]
2220
+ .reverse()
2221
+ .map((entry) => `${this.theme.fg('dim', formatClock(entry.at))} ${entry.text}`);
2222
+ if (contentLines.length === 0) {
2223
+ if (sessionId && historySession && this.eventHistoryInFlight.has(sessionId)) {
2224
+ contentLines = [this.theme.fg('dim', '(loading event history...)')];
2225
+ }
2226
+ else if (sessionId &&
2227
+ historySession &&
2228
+ this.eventHistoryLoadedSessions.has(sessionId)) {
2229
+ contentLines = [this.theme.fg('dim', '(no stored events available)')];
2230
+ }
2231
+ else {
2232
+ contentLines = [
2233
+ this.theme.fg('dim', scoped ? '(no session feed items yet)' : '(no feed items yet)'),
2234
+ ];
2235
+ }
2236
+ }
2237
+ }
2238
+ this.feedMaxScroll = Math.max(0, contentLines.length - entryBudget);
2239
+ if (this.feedFollowing) {
2240
+ this.feedScrollOffset = this.feedMaxScroll;
2241
+ }
2242
+ if (this.feedScrollOffset > this.feedMaxScroll) {
2243
+ this.feedScrollOffset = this.feedMaxScroll;
2244
+ }
2245
+ const windowed = contentLines.slice(this.feedScrollOffset, this.feedScrollOffset + entryBudget);
2246
+ for (const line of windowed) {
2247
+ lines.push(this.contentLine(` ${line}`, inner));
2248
+ }
2249
+ while (lines.length < maxLines - footerRows) {
2250
+ lines.push(this.contentLine('', inner));
2251
+ }
2252
+ const scrollInfo = this.feedMaxScroll > 0
2253
+ ? this.theme.fg('dim', ` scroll ${this.feedScrollOffset}/${this.feedMaxScroll}`)
2254
+ : this.theme.fg('dim', ' scroll 0/0');
2255
+ const thinkingHint = scoped && this.feedViewMode === 'stream' && sessionBuffer?.thinking
2256
+ ? ' [t] Thinking'
2257
+ : '';
2258
+ const taskHint = scoped && this.feedViewMode === 'stream' ? ' [v] Task view' : '';
2259
+ const followHint = ` [f] ${this.feedFollowing ? 'Unfollow' : 'Follow'}`;
2260
+ lines.push(this.contentLine(`${scrollInfo} ${this.theme.fg('dim', `[↑↓] Scroll${thinkingHint}${taskHint}${followHint} [r] Refresh [Esc] Back`)}`, inner));
2261
+ lines.push(buildBottomBorder(width));
2262
+ return lines.slice(0, maxLines);
2263
+ }
2264
+ renderTaskScreen(width, maxLines) {
2265
+ const inner = Math.max(0, width - 2);
2266
+ const lines = [];
2267
+ const headerRows = 2;
2268
+ const footerRows = 2;
2269
+ const contentBudget = Math.max(5, maxLines - headerRows - footerRows);
2270
+ const tasks = this.getDetailTasks();
2271
+ const selected = tasks[this.selectedTaskIndex];
2272
+ const title = selected ? `Task ${shortId(selected.taskId)} ${selected.agent}` : 'Task Detail';
2273
+ lines.push(buildTopBorder(width, title));
2274
+ lines.push(this.contentLine('', inner));
2275
+ const body = [];
2276
+ if (!selected) {
2277
+ body.push(this.contentLine(this.theme.fg('dim', ' No task selected'), inner));
2278
+ }
2279
+ else {
2280
+ const historySession = this.detailSessionId
2281
+ ? !this.isRuntimeAvailable(this.detailSessionId) ||
2282
+ this.isHistoryOnly(this.detailSessionId)
2283
+ : false;
2284
+ body.push(this.contentLine(this.theme.bold(' Task Overview'), inner));
2285
+ body.push(this.contentLine(this.theme.fg('muted', ` Task ID: ${selected.taskId}`), inner));
2286
+ body.push(this.contentLine(this.theme.fg('muted', ` Agent: ${selected.agent}`), inner));
2287
+ body.push(this.contentLine(this.theme.fg('muted', ` Status: ${selected.status}`), inner));
2288
+ if (typeof selected.duration === 'number') {
2289
+ body.push(this.contentLine(this.theme.fg('muted', ` Duration: ${formatElapsedCompact(selected.duration)}`), inner));
2290
+ }
2291
+ if (selected.startedAt) {
2292
+ body.push(this.contentLine(this.theme.fg('muted', ` Started: ${selected.startedAt}`), inner));
2293
+ }
2294
+ if (selected.completedAt) {
2295
+ body.push(this.contentLine(this.theme.fg('muted', ` Completed: ${selected.completedAt}`), inner));
2296
+ }
2297
+ body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
2298
+ body.push(this.contentLine(this.theme.bold(' Prompt'), inner));
2299
+ const wrappedPrompt = wrapText(selected.prompt || '(no prompt recorded)', Math.max(10, inner - 4));
2300
+ for (const wrapped of wrappedPrompt) {
2301
+ body.push(this.contentLine(` ${wrapped}`, inner));
2302
+ }
2303
+ body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
2304
+ body.push(this.contentLine(this.theme.bold(' Task Output'), inner));
2305
+ const taskBuffer = this.detailSessionId
2306
+ ? this.taskBuffers.get(this.getTaskFeedKey(this.detailSessionId, selected.taskId))
2307
+ : undefined;
2308
+ const rendered = this.renderStreamLines(taskBuffer?.output ?? '', taskBuffer?.thinking ?? '', this.showTaskThinking, Math.max(12, inner - 4));
2309
+ if (rendered.length === 0) {
2310
+ if (historySession &&
2311
+ this.detailSessionId &&
2312
+ this.replayInFlight.has(this.detailSessionId)) {
2313
+ body.push(this.contentLine(this.theme.fg('dim', ' (loading replay...)'), inner));
2314
+ }
2315
+ else {
2316
+ body.push(this.contentLine(this.theme.fg('dim', ' (no task output yet)'), inner));
2317
+ }
2318
+ }
2319
+ else {
2320
+ for (const line of rendered) {
2321
+ body.push(this.contentLine(` ${line}`, inner));
2322
+ }
2323
+ }
2324
+ }
2325
+ this.taskMaxScroll = Math.max(0, body.length - contentBudget);
2326
+ if (this.taskFollowing) {
2327
+ this.taskScrollOffset = this.taskMaxScroll;
2328
+ }
2329
+ if (this.taskScrollOffset > this.taskMaxScroll) {
2330
+ this.taskScrollOffset = this.taskMaxScroll;
2331
+ }
2332
+ const windowedBody = body.slice(this.taskScrollOffset, this.taskScrollOffset + contentBudget);
2333
+ lines.push(...windowedBody);
2334
+ while (lines.length < maxLines - footerRows) {
2335
+ lines.push(this.contentLine('', inner));
2336
+ }
2337
+ const scrollInfo = this.taskMaxScroll > 0
2338
+ ? this.theme.fg('dim', ` scroll ${this.taskScrollOffset}/${this.taskMaxScroll}`)
2339
+ : this.theme.fg('dim', ' scroll 0/0');
2340
+ const selectedTaskThinking = this.detailSessionId && selected
2341
+ ? this.taskBuffers.get(this.getTaskFeedKey(this.detailSessionId, selected.taskId))
2342
+ ?.thinking
2343
+ : '';
2344
+ const thinkingHint = selectedTaskThinking ? ' [t] Thinking' : '';
2345
+ const followHint = ` [f] ${this.taskFollowing ? 'Unfollow' : 'Follow'}`;
2346
+ lines.push(this.contentLine(`${scrollInfo} ${this.theme.fg('dim', `[↑↓] Scroll [[ and ]] Task${thinkingHint}${followHint} [Esc] Back`)}`, inner));
2347
+ lines.push(buildBottomBorder(width));
2348
+ return lines.slice(0, maxLines);
2349
+ }
2350
+ contentLine(text, innerWidth) {
2351
+ return `│${padRight(text, innerWidth)}│`;
2352
+ }
2353
+ }
2354
+ //# sourceMappingURL=hub-overlay.js.map