@agentuity/coder 1.0.37

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 (92) 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 +43 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +402 -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.d.ts +107 -0
  23. package/dist/hub-overlay.d.ts.map +1 -0
  24. package/dist/hub-overlay.js +1794 -0
  25. package/dist/hub-overlay.js.map +1 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +1585 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/output-viewer.d.ts +49 -0
  31. package/dist/output-viewer.d.ts.map +1 -0
  32. package/dist/output-viewer.js +389 -0
  33. package/dist/output-viewer.js.map +1 -0
  34. package/dist/overlay.d.ts +40 -0
  35. package/dist/overlay.d.ts.map +1 -0
  36. package/dist/overlay.js +225 -0
  37. package/dist/overlay.js.map +1 -0
  38. package/dist/protocol.d.ts +118 -0
  39. package/dist/protocol.d.ts.map +1 -0
  40. package/dist/protocol.js +3 -0
  41. package/dist/protocol.js.map +1 -0
  42. package/dist/remote-session.d.ts +113 -0
  43. package/dist/remote-session.d.ts.map +1 -0
  44. package/dist/remote-session.js +645 -0
  45. package/dist/remote-session.js.map +1 -0
  46. package/dist/remote-tui.d.ts +40 -0
  47. package/dist/remote-tui.d.ts.map +1 -0
  48. package/dist/remote-tui.js +606 -0
  49. package/dist/remote-tui.js.map +1 -0
  50. package/dist/renderers.d.ts +34 -0
  51. package/dist/renderers.d.ts.map +1 -0
  52. package/dist/renderers.js +669 -0
  53. package/dist/renderers.js.map +1 -0
  54. package/dist/review.d.ts +15 -0
  55. package/dist/review.d.ts.map +1 -0
  56. package/dist/review.js +154 -0
  57. package/dist/review.js.map +1 -0
  58. package/dist/titlebar.d.ts +3 -0
  59. package/dist/titlebar.d.ts.map +1 -0
  60. package/dist/titlebar.js +59 -0
  61. package/dist/titlebar.js.map +1 -0
  62. package/dist/todo/index.d.ts +3 -0
  63. package/dist/todo/index.d.ts.map +1 -0
  64. package/dist/todo/index.js +3 -0
  65. package/dist/todo/index.js.map +1 -0
  66. package/dist/todo/store.d.ts +6 -0
  67. package/dist/todo/store.d.ts.map +1 -0
  68. package/dist/todo/store.js +43 -0
  69. package/dist/todo/store.js.map +1 -0
  70. package/dist/todo/types.d.ts +13 -0
  71. package/dist/todo/types.d.ts.map +1 -0
  72. package/dist/todo/types.js +2 -0
  73. package/dist/todo/types.js.map +1 -0
  74. package/package.json +44 -0
  75. package/src/chain-preview.ts +621 -0
  76. package/src/client.ts +515 -0
  77. package/src/commands.ts +132 -0
  78. package/src/footer.ts +305 -0
  79. package/src/handlers.ts +113 -0
  80. package/src/hub-overlay.ts +2324 -0
  81. package/src/index.ts +1907 -0
  82. package/src/output-viewer.ts +480 -0
  83. package/src/overlay.ts +294 -0
  84. package/src/protocol.ts +157 -0
  85. package/src/remote-session.ts +800 -0
  86. package/src/remote-tui.ts +707 -0
  87. package/src/renderers.ts +740 -0
  88. package/src/review.ts +201 -0
  89. package/src/titlebar.ts +63 -0
  90. package/src/todo/index.ts +2 -0
  91. package/src/todo/store.ts +49 -0
  92. package/src/todo/types.ts +14 -0
@@ -0,0 +1,1794 @@
1
+ import { getMarkdownTheme } from '@mariozechner/pi-coding-agent';
2
+ import { matchesKey, Markdown as MdComponent } from '@mariozechner/pi-tui';
3
+ import { truncateToWidth } from "./renderers.js";
4
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
5
+ const POLL_MS = 4_000;
6
+ const REQUEST_TIMEOUT_MS = 5_000;
7
+ const MAX_FEED_ITEMS = 80;
8
+ const STREAM_SESSION_LIMIT = 8;
9
+ function visibleWidth(text) {
10
+ return text.replace(ANSI_RE, '').replace(/\t/g, ' ').length;
11
+ }
12
+ function padRight(text, width) {
13
+ if (width <= 0)
14
+ return '';
15
+ const normalized = text.replace(/\t/g, ' ');
16
+ const truncated = truncateToWidth(normalized, width);
17
+ const remaining = width - visibleWidth(truncated);
18
+ return remaining > 0 ? truncated + ' '.repeat(remaining) : truncated;
19
+ }
20
+ function hLine(width) {
21
+ return width > 0 ? '─'.repeat(width) : '';
22
+ }
23
+ function buildTopBorder(width, title) {
24
+ if (width <= 0)
25
+ return '';
26
+ if (width === 1)
27
+ return '╭';
28
+ if (width === 2)
29
+ return '╭╮';
30
+ const inner = width - 2;
31
+ const titleText = ` ${title} `;
32
+ if (titleText.length >= inner)
33
+ return `╭${hLine(inner)}╮`;
34
+ const left = Math.floor((inner - titleText.length) / 2);
35
+ const right = inner - titleText.length - left;
36
+ return `╭${hLine(left)}${titleText}${hLine(right)}╮`;
37
+ }
38
+ function buildBottomBorder(width) {
39
+ if (width <= 0)
40
+ return '';
41
+ if (width === 1)
42
+ return '╰';
43
+ if (width === 2)
44
+ return '╰╯';
45
+ return `╰${hLine(width - 2)}╯`;
46
+ }
47
+ function formatClock(ms) {
48
+ const d = new Date(ms);
49
+ return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
50
+ }
51
+ function formatRelative(isoDate) {
52
+ const ts = Date.parse(isoDate);
53
+ if (Number.isNaN(ts))
54
+ return '-';
55
+ const seconds = Math.max(0, Math.floor((Date.now() - ts) / 1000));
56
+ if (seconds < 60)
57
+ return `${seconds}s ago`;
58
+ const minutes = Math.floor(seconds / 60);
59
+ if (minutes < 60)
60
+ return `${minutes}m ago`;
61
+ const hours = Math.floor(minutes / 60);
62
+ if (hours < 24)
63
+ return `${hours}h ago`;
64
+ const days = Math.floor(hours / 24);
65
+ return `${days}d ago`;
66
+ }
67
+ function shortId(id) {
68
+ if (id.length <= 12)
69
+ return id;
70
+ return id.slice(0, 12);
71
+ }
72
+ function getVisibleRange(total, selected, windowSize) {
73
+ if (total <= windowSize)
74
+ return [0, total];
75
+ const half = Math.floor(windowSize / 2);
76
+ let start = Math.max(0, selected - half);
77
+ let end = start + windowSize;
78
+ if (end > total) {
79
+ end = total;
80
+ start = end - windowSize;
81
+ }
82
+ return [start, end];
83
+ }
84
+ function wrapText(text, width) {
85
+ if (width <= 0)
86
+ return [''];
87
+ if (!text)
88
+ return [''];
89
+ const lines = [];
90
+ const paragraphs = text.split(/\r?\n/);
91
+ for (const paragraph of paragraphs) {
92
+ const words = paragraph.split(/\s+/).filter(Boolean);
93
+ if (words.length === 0) {
94
+ lines.push('');
95
+ continue;
96
+ }
97
+ let current = words[0];
98
+ for (let i = 1; i < words.length; i++) {
99
+ const word = words[i];
100
+ const candidate = `${current} ${word}`;
101
+ if (candidate.length <= width) {
102
+ current = candidate;
103
+ }
104
+ else {
105
+ lines.push(current);
106
+ current = word.length > width ? word.slice(0, width) : word;
107
+ }
108
+ }
109
+ lines.push(current);
110
+ }
111
+ return lines;
112
+ }
113
+ function toSingleLine(text) {
114
+ return text
115
+ .replace(/[\r\n\t]+/g, ' ')
116
+ .replace(/[\x00-\x1f\x7f]/g, '')
117
+ .replace(/\s+/g, ' ')
118
+ .trim();
119
+ }
120
+ function summarizeArgs(value, maxWidth = 100) {
121
+ if (!value || typeof value !== 'object')
122
+ return '';
123
+ const args = value;
124
+ if (typeof args.command === 'string')
125
+ return truncateToWidth(toSingleLine(args.command), maxWidth);
126
+ if (typeof args.path === 'string')
127
+ return truncateToWidth(toSingleLine(args.path), maxWidth);
128
+ if (typeof args.filePath === 'string')
129
+ return truncateToWidth(toSingleLine(args.filePath), maxWidth);
130
+ if (typeof args.pattern === 'string')
131
+ return truncateToWidth(toSingleLine(args.pattern), maxWidth);
132
+ try {
133
+ const raw = JSON.stringify(value);
134
+ return truncateToWidth(toSingleLine(raw), 100);
135
+ }
136
+ catch {
137
+ return '';
138
+ }
139
+ }
140
+ function summarizeToolCall(name, argsRaw) {
141
+ const args = argsRaw && typeof argsRaw === 'object' ? argsRaw : undefined;
142
+ if (name === 'task') {
143
+ const agent = typeof args?.subagent_type === 'string' ? args.subagent_type : '?';
144
+ const description = typeof args?.description === 'string'
145
+ ? truncateToWidth(toSingleLine(args.description), 80)
146
+ : '';
147
+ return description ? `${agent} - ${description}` : `${agent} - delegated task`;
148
+ }
149
+ if (name === 'parallel_tasks') {
150
+ const tasks = Array.isArray(args?.tasks) ? args.tasks : [];
151
+ const agents = tasks
152
+ .map((task) => task && typeof task === 'object'
153
+ ? task.subagent_type
154
+ : undefined)
155
+ .filter((agent) => typeof agent === 'string');
156
+ if (agents.length > 0)
157
+ return agents.join(' + ');
158
+ return 'parallel delegated tasks';
159
+ }
160
+ return null;
161
+ }
162
+ function extractToolResultText(contentRaw) {
163
+ if (typeof contentRaw === 'string')
164
+ return contentRaw;
165
+ if (!Array.isArray(contentRaw))
166
+ return '';
167
+ const parts = [];
168
+ for (const item of contentRaw) {
169
+ if (!item || typeof item !== 'object')
170
+ continue;
171
+ const block = item;
172
+ if (typeof block.text === 'string') {
173
+ parts.push(block.text);
174
+ }
175
+ }
176
+ return parts.join('\n');
177
+ }
178
+ function formatDuration(ms) {
179
+ if (ms >= 1000)
180
+ return `${(ms / 1000).toFixed(1)}s`;
181
+ return `${ms}ms`;
182
+ }
183
+ function extractMessageSegments(data) {
184
+ const fallback = typeof data?.text === 'string' ? data.text : '';
185
+ const messageRaw = data?.message;
186
+ if (!messageRaw || typeof messageRaw !== 'object') {
187
+ return { output: fallback, thinking: '' };
188
+ }
189
+ const message = messageRaw;
190
+ const role = typeof message.role === 'string' ? message.role : '';
191
+ if (role && role !== 'assistant') {
192
+ return { output: '', thinking: '' };
193
+ }
194
+ const content = message.content;
195
+ const outputParts = [];
196
+ const thinkingParts = [];
197
+ if (typeof content === 'string') {
198
+ outputParts.push(content);
199
+ }
200
+ else if (Array.isArray(content)) {
201
+ for (const blockRaw of content) {
202
+ if (!blockRaw || typeof blockRaw !== 'object')
203
+ continue;
204
+ const block = blockRaw;
205
+ const type = typeof block.type === 'string' ? block.type : '';
206
+ if (type === 'text' && typeof block.text === 'string') {
207
+ outputParts.push(block.text);
208
+ continue;
209
+ }
210
+ if (type === 'thinking' && typeof block.thinking === 'string') {
211
+ thinkingParts.push(block.thinking);
212
+ continue;
213
+ }
214
+ }
215
+ }
216
+ if (outputParts.length === 0 && fallback) {
217
+ outputParts.push(fallback);
218
+ }
219
+ return {
220
+ output: outputParts.join('\n\n').trim(),
221
+ thinking: thinkingParts.join('\n\n').trim(),
222
+ };
223
+ }
224
+ export class HubOverlay {
225
+ focused = true;
226
+ tui;
227
+ theme;
228
+ done;
229
+ baseUrl;
230
+ currentSessionId;
231
+ screen;
232
+ selectedIndex = 0;
233
+ detailSessionId;
234
+ detailScrollOffset = 0;
235
+ detailMaxScroll = 0;
236
+ feedScrollOffset = 0;
237
+ feedMaxScroll = 0;
238
+ feedScope = 'global';
239
+ feedViewMode = 'stream';
240
+ showFeedThinking = true;
241
+ feedFollowing = true;
242
+ taskScrollOffset = 0;
243
+ taskMaxScroll = 0;
244
+ selectedTaskIndex = 0;
245
+ showTaskThinking = true;
246
+ taskFollowing = true;
247
+ sessions = [];
248
+ detail = null;
249
+ cachedTodos = null;
250
+ feed = [];
251
+ sessionFeed = new Map();
252
+ sessionBuffers = new Map();
253
+ taskBuffers = new Map();
254
+ hydratedSessions = new Set();
255
+ previousDigests = new Map();
256
+ loadingList = true;
257
+ loadingDetail = false;
258
+ listError = '';
259
+ detailError = '';
260
+ lastUpdatedAt = 0;
261
+ listInFlight = false;
262
+ detailInFlight = false;
263
+ sseControllers = new Map();
264
+ disposed = false;
265
+ pollTimer = null;
266
+ mdRenderer = null;
267
+ constructor(tui, theme, options) {
268
+ this.tui = tui;
269
+ this.theme = theme;
270
+ this.done = options.done;
271
+ this.baseUrl = options.baseUrl;
272
+ this.currentSessionId = options.currentSessionId;
273
+ this.detailSessionId = options.initialSessionId ?? null;
274
+ this.screen = options.startInDetail && options.initialSessionId ? 'detail' : 'list';
275
+ try {
276
+ const mdTheme = getMarkdownTheme?.();
277
+ if (mdTheme) {
278
+ this.mdRenderer = new MdComponent('', 0, 0, mdTheme);
279
+ }
280
+ }
281
+ catch {
282
+ this.mdRenderer = null;
283
+ }
284
+ void this.refreshList(true);
285
+ if (this.detailSessionId) {
286
+ void this.refreshDetail(this.detailSessionId, true);
287
+ }
288
+ this.pollTimer = setInterval(() => {
289
+ if (this.disposed)
290
+ return;
291
+ void this.refreshList();
292
+ if (this.detailSessionId) {
293
+ void this.refreshDetail(this.detailSessionId);
294
+ }
295
+ }, POLL_MS);
296
+ }
297
+ handleInput(data) {
298
+ if (this.disposed)
299
+ return;
300
+ const inSessionContext = this.isSessionContext();
301
+ if (data === '1') {
302
+ this.screen = inSessionContext ? 'detail' : 'list';
303
+ void this.syncSseStreams(this.sessions);
304
+ this.requestRender();
305
+ return;
306
+ }
307
+ if (data === '2') {
308
+ // Session/task views use task drill-down, not direct feed jumps from task screen.
309
+ if (this.screen === 'task')
310
+ return;
311
+ if (inSessionContext && this.detailSessionId) {
312
+ this.feedScope = 'session';
313
+ this.feedViewMode = 'stream';
314
+ }
315
+ else {
316
+ this.feedScope = 'global';
317
+ this.feedViewMode = 'events';
318
+ }
319
+ this.feedScrollOffset = 0;
320
+ this.feedFollowing = true;
321
+ this.screen = 'feed';
322
+ void this.syncSseStreams(this.sessions);
323
+ this.requestRender();
324
+ return;
325
+ }
326
+ if (data === '3') {
327
+ const atSessionLevel = !!this.detailSessionId &&
328
+ (this.screen === 'detail' || (this.screen === 'feed' && this.feedScope === 'session'));
329
+ if (!atSessionLevel)
330
+ return;
331
+ this.feedScope = 'session';
332
+ this.feedViewMode = 'events';
333
+ this.feedScrollOffset = 0;
334
+ this.feedFollowing = true;
335
+ this.screen = 'feed';
336
+ void this.syncSseStreams(this.sessions);
337
+ this.requestRender();
338
+ return;
339
+ }
340
+ if (matchesKey(data, 'escape')) {
341
+ if (this.screen === 'task') {
342
+ this.screen = 'detail';
343
+ void this.syncSseStreams(this.sessions);
344
+ this.requestRender();
345
+ return;
346
+ }
347
+ if (this.screen === 'feed') {
348
+ this.screen = this.feedScope === 'session' ? 'detail' : 'list';
349
+ void this.syncSseStreams(this.sessions);
350
+ this.requestRender();
351
+ return;
352
+ }
353
+ if (this.screen === 'detail') {
354
+ this.screen = 'list';
355
+ void this.syncSseStreams(this.sessions);
356
+ this.requestRender();
357
+ return;
358
+ }
359
+ this.close();
360
+ return;
361
+ }
362
+ if (matchesKey(data, 'r') || data.toLowerCase() === 'r') {
363
+ void this.refreshList();
364
+ if ((this.screen === 'detail' || this.screen === 'task' || this.screen === 'feed') &&
365
+ this.detailSessionId) {
366
+ void this.refreshDetail(this.detailSessionId);
367
+ }
368
+ return;
369
+ }
370
+ if (this.screen === 'list') {
371
+ this.handleListInput(data);
372
+ return;
373
+ }
374
+ if (this.screen === 'feed') {
375
+ this.handleFeedInput(data);
376
+ return;
377
+ }
378
+ if (this.screen === 'task') {
379
+ this.handleTaskInput(data);
380
+ return;
381
+ }
382
+ this.handleDetailInput(data);
383
+ }
384
+ render(width) {
385
+ const safeWidth = Math.max(6, width);
386
+ const termHeight = process.stdout.rows || 40;
387
+ const maxLines = Math.max(12, Math.floor(termHeight * 0.95) - 2);
388
+ const lines = this.screen === 'detail'
389
+ ? this.renderDetailScreen(safeWidth, maxLines)
390
+ : this.screen === 'feed'
391
+ ? this.renderFeedScreen(safeWidth, maxLines)
392
+ : this.screen === 'task'
393
+ ? this.renderTaskScreen(safeWidth, maxLines)
394
+ : this.renderListScreen(safeWidth, maxLines);
395
+ return lines.map((line) => truncateToWidth(line, safeWidth));
396
+ }
397
+ invalidate() {
398
+ this.requestRender();
399
+ }
400
+ dispose() {
401
+ if (this.disposed)
402
+ return;
403
+ this.disposed = true;
404
+ if (this.pollTimer) {
405
+ clearInterval(this.pollTimer);
406
+ this.pollTimer = null;
407
+ }
408
+ for (const stream of this.sseControllers.values()) {
409
+ stream.controller.abort();
410
+ }
411
+ this.sseControllers.clear();
412
+ }
413
+ requestRender() {
414
+ try {
415
+ this.tui.requestRender();
416
+ }
417
+ catch {
418
+ // Best effort render invalidation.
419
+ }
420
+ }
421
+ close() {
422
+ this.dispose();
423
+ this.done(undefined);
424
+ }
425
+ handleListInput(data) {
426
+ const count = this.sessions.length;
427
+ if (matchesKey(data, 'up') || data.toLowerCase() === 'k') {
428
+ if (count > 0) {
429
+ this.selectedIndex = (this.selectedIndex - 1 + count) % count;
430
+ this.requestRender();
431
+ }
432
+ return;
433
+ }
434
+ if (matchesKey(data, 'down') || data.toLowerCase() === 'j') {
435
+ if (count > 0) {
436
+ this.selectedIndex = (this.selectedIndex + 1) % count;
437
+ this.requestRender();
438
+ }
439
+ return;
440
+ }
441
+ if (matchesKey(data, 'enter')) {
442
+ const selected = this.sessions[this.selectedIndex];
443
+ if (!selected)
444
+ return;
445
+ this.detailSessionId = selected.sessionId;
446
+ this.detailScrollOffset = 0;
447
+ this.selectedTaskIndex = 0;
448
+ this.showTaskThinking = true;
449
+ this.taskFollowing = true;
450
+ this.screen = 'detail';
451
+ void this.syncSseStreams(this.sessions);
452
+ void this.refreshDetail(selected.sessionId, true);
453
+ this.requestRender();
454
+ }
455
+ }
456
+ isSessionContext() {
457
+ return (this.screen === 'detail' ||
458
+ this.screen === 'task' ||
459
+ (this.screen === 'feed' && this.feedScope === 'session'));
460
+ }
461
+ handleDetailInput(data) {
462
+ const tasks = this.getDetailTasks();
463
+ if (matchesKey(data, 'up')) {
464
+ if (tasks.length > 0) {
465
+ this.selectedTaskIndex = (this.selectedTaskIndex - 1 + tasks.length) % tasks.length;
466
+ this.requestRender();
467
+ }
468
+ return;
469
+ }
470
+ if (matchesKey(data, 'down')) {
471
+ if (tasks.length > 0) {
472
+ this.selectedTaskIndex = (this.selectedTaskIndex + 1) % tasks.length;
473
+ this.requestRender();
474
+ }
475
+ return;
476
+ }
477
+ if (matchesKey(data, 'enter')) {
478
+ if (tasks.length > 0) {
479
+ this.selectedTaskIndex = Math.min(this.selectedTaskIndex, tasks.length - 1);
480
+ this.taskScrollOffset = 0;
481
+ this.showTaskThinking = true;
482
+ this.taskFollowing = true;
483
+ this.screen = 'task';
484
+ this.requestRender();
485
+ }
486
+ return;
487
+ }
488
+ if (data.toLowerCase() === 'k') {
489
+ if (this.detailScrollOffset > 0) {
490
+ this.detailScrollOffset -= 1;
491
+ this.requestRender();
492
+ }
493
+ return;
494
+ }
495
+ if (data.toLowerCase() === 'j') {
496
+ if (this.detailScrollOffset < this.detailMaxScroll) {
497
+ this.detailScrollOffset += 1;
498
+ this.requestRender();
499
+ }
500
+ return;
501
+ }
502
+ if (matchesKey(data, 'pageUp') || matchesKey(data, 'shift+up')) {
503
+ const jump = Math.max(1, Math.floor((process.stdout.rows || 40) / 3));
504
+ this.detailScrollOffset = Math.max(0, this.detailScrollOffset - jump);
505
+ this.requestRender();
506
+ return;
507
+ }
508
+ if (matchesKey(data, 'pageDown') || matchesKey(data, 'shift+down')) {
509
+ const jump = Math.max(1, Math.floor((process.stdout.rows || 40) / 3));
510
+ this.detailScrollOffset = Math.min(this.detailMaxScroll, this.detailScrollOffset + jump);
511
+ this.requestRender();
512
+ }
513
+ }
514
+ handleFeedInput(data) {
515
+ const maxScroll = this.feedMaxScroll;
516
+ const jump = Math.max(1, Math.floor((process.stdout.rows || 40) / 3));
517
+ if (matchesKey(data, 't') || data.toLowerCase() === 't') {
518
+ if (this.feedScope === 'session' && this.feedViewMode === 'stream') {
519
+ this.showFeedThinking = !this.showFeedThinking;
520
+ this.requestRender();
521
+ }
522
+ return;
523
+ }
524
+ if (this.feedScope === 'session' &&
525
+ this.feedViewMode === 'stream' &&
526
+ (matchesKey(data, 'enter') || matchesKey(data, 'v') || data.toLowerCase() === 'v')) {
527
+ const tasks = this.getDetailTasks();
528
+ if (tasks.length > 0) {
529
+ this.selectedTaskIndex = Math.min(this.selectedTaskIndex, tasks.length - 1);
530
+ this.taskScrollOffset = 0;
531
+ this.showTaskThinking = true;
532
+ this.taskFollowing = true;
533
+ this.screen = 'task';
534
+ this.requestRender();
535
+ }
536
+ return;
537
+ }
538
+ if (matchesKey(data, 'f') || data.toLowerCase() === 'f') {
539
+ this.feedFollowing = !this.feedFollowing;
540
+ if (this.feedFollowing) {
541
+ this.feedScrollOffset = maxScroll;
542
+ }
543
+ this.requestRender();
544
+ return;
545
+ }
546
+ if (matchesKey(data, 'up') || data.toLowerCase() === 'k') {
547
+ if (this.feedScrollOffset > 0) {
548
+ this.feedFollowing = false;
549
+ this.feedScrollOffset -= 1;
550
+ this.requestRender();
551
+ }
552
+ return;
553
+ }
554
+ if (matchesKey(data, 'down') || data.toLowerCase() === 'j') {
555
+ if (this.feedScrollOffset < this.feedMaxScroll) {
556
+ this.feedFollowing = false;
557
+ this.feedScrollOffset += 1;
558
+ this.requestRender();
559
+ }
560
+ return;
561
+ }
562
+ if (matchesKey(data, 'pageUp') || matchesKey(data, 'shift+up')) {
563
+ this.feedFollowing = false;
564
+ this.feedScrollOffset = Math.max(0, this.feedScrollOffset - jump);
565
+ this.requestRender();
566
+ return;
567
+ }
568
+ if (matchesKey(data, 'pageDown') || matchesKey(data, 'shift+down')) {
569
+ this.feedFollowing = false;
570
+ this.feedScrollOffset = Math.min(this.feedMaxScroll, this.feedScrollOffset + jump);
571
+ this.requestRender();
572
+ return;
573
+ }
574
+ if (data === 'g') {
575
+ this.feedFollowing = false;
576
+ this.feedScrollOffset = 0;
577
+ this.requestRender();
578
+ return;
579
+ }
580
+ if (data === 'G') {
581
+ this.feedFollowing = false;
582
+ this.feedScrollOffset = this.feedMaxScroll;
583
+ this.requestRender();
584
+ return;
585
+ }
586
+ if (data === '{') {
587
+ this.feedFollowing = false;
588
+ const segment = Math.max(1, Math.floor((this.feedMaxScroll || 1) * 0.25));
589
+ this.feedScrollOffset = Math.max(0, this.feedScrollOffset - segment);
590
+ this.requestRender();
591
+ return;
592
+ }
593
+ if (data === '}') {
594
+ this.feedFollowing = false;
595
+ const segment = Math.max(1, Math.floor((this.feedMaxScroll || 1) * 0.25));
596
+ this.feedScrollOffset = Math.min(this.feedMaxScroll, this.feedScrollOffset + segment);
597
+ this.requestRender();
598
+ return;
599
+ }
600
+ }
601
+ handleTaskInput(data) {
602
+ const tasks = this.getDetailTasks();
603
+ const maxScroll = this.taskMaxScroll;
604
+ const jump = Math.max(1, Math.floor((process.stdout.rows || 40) / 3));
605
+ if (matchesKey(data, 't') || data.toLowerCase() === 't') {
606
+ this.showTaskThinking = !this.showTaskThinking;
607
+ this.requestRender();
608
+ return;
609
+ }
610
+ if (matchesKey(data, 'f') || data.toLowerCase() === 'f') {
611
+ this.taskFollowing = !this.taskFollowing;
612
+ if (this.taskFollowing) {
613
+ this.taskScrollOffset = maxScroll;
614
+ }
615
+ this.requestRender();
616
+ return;
617
+ }
618
+ if (data === '[') {
619
+ if (tasks.length > 0) {
620
+ this.selectedTaskIndex = (this.selectedTaskIndex - 1 + tasks.length) % tasks.length;
621
+ this.taskScrollOffset = 0;
622
+ this.taskFollowing = true;
623
+ this.requestRender();
624
+ }
625
+ return;
626
+ }
627
+ if (data === ']') {
628
+ if (tasks.length > 0) {
629
+ this.selectedTaskIndex = (this.selectedTaskIndex + 1) % tasks.length;
630
+ this.taskScrollOffset = 0;
631
+ this.taskFollowing = true;
632
+ this.requestRender();
633
+ }
634
+ return;
635
+ }
636
+ if (matchesKey(data, 'up') || data.toLowerCase() === 'k') {
637
+ if (this.taskScrollOffset > 0) {
638
+ this.taskFollowing = false;
639
+ this.taskScrollOffset -= 1;
640
+ this.requestRender();
641
+ }
642
+ return;
643
+ }
644
+ if (matchesKey(data, 'down') || data.toLowerCase() === 'j') {
645
+ if (this.taskScrollOffset < this.taskMaxScroll) {
646
+ this.taskFollowing = false;
647
+ this.taskScrollOffset += 1;
648
+ this.requestRender();
649
+ }
650
+ return;
651
+ }
652
+ if (matchesKey(data, 'pageUp') || matchesKey(data, 'shift+up')) {
653
+ this.taskFollowing = false;
654
+ this.taskScrollOffset = Math.max(0, this.taskScrollOffset - jump);
655
+ this.requestRender();
656
+ return;
657
+ }
658
+ if (matchesKey(data, 'pageDown') || matchesKey(data, 'shift+down')) {
659
+ this.taskFollowing = false;
660
+ this.taskScrollOffset = Math.min(this.taskMaxScroll, this.taskScrollOffset + jump);
661
+ this.requestRender();
662
+ return;
663
+ }
664
+ if (data === 'g') {
665
+ this.taskFollowing = false;
666
+ this.taskScrollOffset = 0;
667
+ this.requestRender();
668
+ return;
669
+ }
670
+ if (data === 'G') {
671
+ this.taskFollowing = false;
672
+ this.taskScrollOffset = this.taskMaxScroll;
673
+ this.requestRender();
674
+ return;
675
+ }
676
+ if (data === '{') {
677
+ this.taskFollowing = false;
678
+ const segment = Math.max(1, Math.floor((this.taskMaxScroll || 1) * 0.25));
679
+ this.taskScrollOffset = Math.max(0, this.taskScrollOffset - segment);
680
+ this.requestRender();
681
+ return;
682
+ }
683
+ if (data === '}') {
684
+ this.taskFollowing = false;
685
+ const segment = Math.max(1, Math.floor((this.taskMaxScroll || 1) * 0.25));
686
+ this.taskScrollOffset = Math.min(this.taskMaxScroll, this.taskScrollOffset + segment);
687
+ this.requestRender();
688
+ return;
689
+ }
690
+ }
691
+ getDetailTasks() {
692
+ return this.detail?.tasks ?? [];
693
+ }
694
+ getSessionBuffer(sessionId) {
695
+ let buffer = this.sessionBuffers.get(sessionId);
696
+ if (!buffer) {
697
+ buffer = { output: '', thinking: '' };
698
+ this.sessionBuffers.set(sessionId, buffer);
699
+ }
700
+ return buffer;
701
+ }
702
+ getTaskBuffer(sessionId, taskId) {
703
+ const key = this.getTaskFeedKey(sessionId, taskId);
704
+ let buffer = this.taskBuffers.get(key);
705
+ if (!buffer) {
706
+ buffer = { output: '', thinking: '' };
707
+ this.taskBuffers.set(key, buffer);
708
+ }
709
+ return buffer;
710
+ }
711
+ appendBufferText(sessionId, kind, chunk, taskId) {
712
+ if (!chunk)
713
+ return;
714
+ // Session stream is lead/top-level only; task-scoped output lives in task buffers.
715
+ if (!taskId) {
716
+ const sessionBuffer = this.getSessionBuffer(sessionId);
717
+ if (kind === 'output')
718
+ sessionBuffer.output += chunk;
719
+ else
720
+ sessionBuffer.thinking += chunk;
721
+ }
722
+ if (taskId) {
723
+ const taskBuffer = this.getTaskBuffer(sessionId, taskId);
724
+ if (kind === 'output')
725
+ taskBuffer.output += chunk;
726
+ else
727
+ taskBuffer.thinking += chunk;
728
+ }
729
+ }
730
+ renderMarkdownLines(text, width) {
731
+ if (!text)
732
+ return [];
733
+ const safeWidth = Math.max(20, width);
734
+ if (this.mdRenderer) {
735
+ try {
736
+ this.mdRenderer.setText(text);
737
+ return this.mdRenderer.render(safeWidth);
738
+ }
739
+ catch {
740
+ return text.split(/\r?\n/);
741
+ }
742
+ }
743
+ return text.split(/\r?\n/);
744
+ }
745
+ renderStreamLines(output, thinking, showThinking, width) {
746
+ const lines = [];
747
+ if (showThinking && thinking.trim()) {
748
+ for (const line of thinking.split(/\r?\n/)) {
749
+ lines.push(this.theme.fg('dim', line));
750
+ }
751
+ lines.push(this.theme.fg('muted', '--- end thinking ---'));
752
+ lines.push('');
753
+ }
754
+ if (output.trim()) {
755
+ lines.push(...this.renderMarkdownLines(output, width));
756
+ }
757
+ return lines;
758
+ }
759
+ buildTopTabs(active, sessionTabs) {
760
+ const divider = this.theme.fg('dim', ' | ');
761
+ const tab = (key, label, isActive) => {
762
+ const text = `[${key}] ${label}`;
763
+ return isActive
764
+ ? this.theme.bold(this.theme.fg('accent', text))
765
+ : this.theme.fg('dim', text);
766
+ };
767
+ if (sessionTabs) {
768
+ return ` ${tab('1', 'Detail', active === 'detail')}${divider}${tab('2', 'Feed', active === 'feed')}${divider}${tab('3', 'Events', active === 'events')}`;
769
+ }
770
+ return ` ${tab('1', 'List', active === 'list')}${divider}${tab('2', 'Feed', active === 'feed')}`;
771
+ }
772
+ async fetchJson(path, timeoutMs = REQUEST_TIMEOUT_MS) {
773
+ const controller = new AbortController();
774
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
775
+ try {
776
+ // TODO: Remove/Change when we get Agentuity service level auth enabled, this is just temporary
777
+ const apiKey = process.env.AGENTUITY_CODER_API_KEY;
778
+ const headers = { accept: 'application/json' };
779
+ if (apiKey)
780
+ headers['x-agentuity-auth-api-key'] = apiKey;
781
+ const response = await fetch(`${this.baseUrl}${path}`, {
782
+ headers,
783
+ signal: controller.signal,
784
+ });
785
+ if (!response.ok) {
786
+ throw new Error(`Hub returned ${response.status}`);
787
+ }
788
+ return (await response.json());
789
+ }
790
+ finally {
791
+ clearTimeout(timeout);
792
+ }
793
+ }
794
+ async refreshList(initial = false) {
795
+ if (this.disposed || this.listInFlight)
796
+ return;
797
+ this.listInFlight = true;
798
+ if (initial) {
799
+ this.loadingList = true;
800
+ this.listError = '';
801
+ this.requestRender();
802
+ }
803
+ try {
804
+ const data = await this.fetchJson('/api/hub/sessions');
805
+ const sessions = (data.sessions?.websocket ?? []).slice().sort((a, b) => {
806
+ return Date.parse(b.createdAt) - Date.parse(a.createdAt);
807
+ });
808
+ this.updateFeedFromList(sessions);
809
+ this.sessions = sessions;
810
+ if (this.selectedIndex >= this.sessions.length) {
811
+ this.selectedIndex = Math.max(0, this.sessions.length - 1);
812
+ }
813
+ this.loadingList = false;
814
+ this.listError = '';
815
+ this.lastUpdatedAt = Date.now();
816
+ void this.syncSseStreams(this.sessions);
817
+ this.requestRender();
818
+ }
819
+ catch (err) {
820
+ this.loadingList = false;
821
+ this.listError = err instanceof Error ? err.message : String(err);
822
+ this.requestRender();
823
+ }
824
+ finally {
825
+ this.listInFlight = false;
826
+ }
827
+ }
828
+ async refreshDetail(sessionId, initial = false) {
829
+ if (this.disposed || this.detailInFlight)
830
+ return;
831
+ this.detailInFlight = true;
832
+ if (initial) {
833
+ this.loadingDetail = true;
834
+ this.detailError = '';
835
+ this.requestRender();
836
+ }
837
+ try {
838
+ const [detail, todosResponse] = await Promise.all([
839
+ this.fetchJson(`/api/hub/session/${encodeURIComponent(sessionId)}`),
840
+ this.fetchJson(`/api/hub/session/${encodeURIComponent(sessionId)}/todos?includeTerminal=true&includeSync=true&limit=30`, 15_000).catch((err) => {
841
+ const isAbort = err?.name === 'AbortError' || err?.message?.includes('aborted');
842
+ return {
843
+ _fetchError: true,
844
+ message: isAbort ? 'Todos loading\u2026' : err?.message || 'Failed to load todos',
845
+ };
846
+ }),
847
+ ]);
848
+ if (todosResponse?._fetchError) {
849
+ // Use cached todos if available, show loading indicator
850
+ if (this.cachedTodos && this.cachedTodos.sessionId === sessionId) {
851
+ detail.todos = this.cachedTodos.todos;
852
+ detail.todoSummary = this.cachedTodos.summary;
853
+ detail.todosUnavailable = todosResponse.message;
854
+ }
855
+ else {
856
+ detail.todosUnavailable = todosResponse.message;
857
+ }
858
+ }
859
+ else if (todosResponse) {
860
+ detail.todos = Array.isArray(todosResponse.todos) ? todosResponse.todos : [];
861
+ detail.todoSummary =
862
+ todosResponse.summary && typeof todosResponse.summary === 'object'
863
+ ? todosResponse.summary
864
+ : undefined;
865
+ detail.todosUnavailable = todosResponse.unavailable
866
+ ? typeof todosResponse.message === 'string'
867
+ ? todosResponse.message
868
+ : 'Task service unavailable'
869
+ : undefined;
870
+ // Cache successful todo data
871
+ if (detail.todos && detail.todos.length > 0) {
872
+ this.cachedTodos = { todos: detail.todos, summary: detail.todoSummary, sessionId };
873
+ }
874
+ }
875
+ this.detail = detail;
876
+ this.detailSessionId = sessionId;
877
+ this.applyStreamProjection(sessionId, detail.stream);
878
+ const taskCount = detail.tasks?.length ?? 0;
879
+ if (taskCount === 0) {
880
+ this.selectedTaskIndex = 0;
881
+ }
882
+ else if (this.selectedTaskIndex >= taskCount) {
883
+ this.selectedTaskIndex = taskCount - 1;
884
+ }
885
+ this.loadingDetail = false;
886
+ this.detailError = '';
887
+ this.lastUpdatedAt = Date.now();
888
+ this.requestRender();
889
+ }
890
+ catch (err) {
891
+ this.loadingDetail = false;
892
+ this.detailError = err instanceof Error ? err.message : String(err);
893
+ this.requestRender();
894
+ }
895
+ finally {
896
+ this.detailInFlight = false;
897
+ }
898
+ }
899
+ updateFeedFromList(sessions) {
900
+ if (this.previousDigests.size === 0) {
901
+ if (sessions.length > 0) {
902
+ this.pushFeed(`Loaded ${sessions.length} active session${sessions.length === 1 ? '' : 's'}`);
903
+ }
904
+ for (const session of sessions) {
905
+ this.previousDigests.set(session.sessionId, {
906
+ status: session.status,
907
+ taskCount: session.taskCount,
908
+ observerCount: session.observerCount,
909
+ subAgentCount: session.subAgentCount,
910
+ });
911
+ }
912
+ return;
913
+ }
914
+ const nextDigests = new Map();
915
+ for (const session of sessions) {
916
+ const prev = this.previousDigests.get(session.sessionId);
917
+ const label = session.label || shortId(session.sessionId);
918
+ if (!prev) {
919
+ this.pushFeed(`${label}: session discovered (${session.mode})`, session.sessionId);
920
+ }
921
+ else {
922
+ if (prev.status !== session.status) {
923
+ this.pushFeed(`${label}: ${prev.status} -> ${session.status}`, session.sessionId);
924
+ }
925
+ if (session.taskCount > prev.taskCount) {
926
+ const delta = session.taskCount - prev.taskCount;
927
+ this.pushFeed(`${label}: +${delta} task${delta === 1 ? '' : 's'}`, session.sessionId);
928
+ }
929
+ if (session.observerCount !== prev.observerCount) {
930
+ this.pushFeed(`${label}: observers ${prev.observerCount} -> ${session.observerCount}`, session.sessionId);
931
+ }
932
+ if (session.subAgentCount !== prev.subAgentCount) {
933
+ this.pushFeed(`${label}: agents ${prev.subAgentCount} -> ${session.subAgentCount}`, session.sessionId);
934
+ }
935
+ }
936
+ nextDigests.set(session.sessionId, {
937
+ status: session.status,
938
+ taskCount: session.taskCount,
939
+ observerCount: session.observerCount,
940
+ subAgentCount: session.subAgentCount,
941
+ });
942
+ }
943
+ for (const oldSessionId of this.previousDigests.keys()) {
944
+ if (!nextDigests.has(oldSessionId)) {
945
+ this.pushFeed(`${shortId(oldSessionId)}: session removed`, oldSessionId);
946
+ }
947
+ }
948
+ this.previousDigests = nextDigests;
949
+ }
950
+ async syncSseStreams(sessions) {
951
+ if (this.disposed)
952
+ return;
953
+ const desiredModes = new Map();
954
+ for (const session of sessions.slice(0, STREAM_SESSION_LIMIT)) {
955
+ desiredModes.set(session.sessionId, 'summary');
956
+ }
957
+ if (this.detailSessionId && this.isSessionContext()) {
958
+ desiredModes.set(this.detailSessionId, 'full');
959
+ }
960
+ for (const [sessionId, stream] of this.sseControllers) {
961
+ const desiredMode = desiredModes.get(sessionId);
962
+ if (!desiredMode) {
963
+ stream.controller.abort();
964
+ this.sseControllers.delete(sessionId);
965
+ continue;
966
+ }
967
+ if (stream.mode !== desiredMode) {
968
+ stream.controller.abort();
969
+ this.sseControllers.delete(sessionId);
970
+ }
971
+ }
972
+ for (const [sessionId, mode] of desiredModes) {
973
+ if (!this.sseControllers.has(sessionId)) {
974
+ void this.startSseStream(sessionId, mode);
975
+ }
976
+ }
977
+ }
978
+ async startSseStream(sessionId, mode) {
979
+ if (this.disposed)
980
+ return;
981
+ const existing = this.sseControllers.get(sessionId);
982
+ if (existing && existing.mode === mode)
983
+ return;
984
+ if (existing) {
985
+ existing.controller.abort();
986
+ this.sseControllers.delete(sessionId);
987
+ }
988
+ const controller = new AbortController();
989
+ this.sseControllers.set(sessionId, { controller, mode });
990
+ const subscribe = mode === 'full' ? '*' : 'session_*,task_*,agent_*';
991
+ try {
992
+ // TODO: Remove/Change when we get Agentuity service level auth enabled, this is just temporary
993
+ const apiKey = process.env.AGENTUITY_CODER_API_KEY;
994
+ const sseHeaders = { accept: 'text/event-stream' };
995
+ if (apiKey)
996
+ sseHeaders['x-agentuity-auth-api-key'] = apiKey;
997
+ const response = await fetch(`${this.baseUrl}/api/hub/session/${encodeURIComponent(sessionId)}/events?subscribe=${encodeURIComponent(subscribe)}`, {
998
+ headers: sseHeaders,
999
+ signal: controller.signal,
1000
+ });
1001
+ if (!response.ok || !response.body) {
1002
+ throw new Error(`Hub returned ${response.status} for stream`);
1003
+ }
1004
+ const reader = response.body.getReader();
1005
+ const decoder = new TextDecoder();
1006
+ let buffer = '';
1007
+ while (true) {
1008
+ const { done, value } = await reader.read();
1009
+ if (done)
1010
+ break;
1011
+ buffer += decoder.decode(value, { stream: true });
1012
+ buffer = this.consumeSseBuffer(sessionId, mode, buffer);
1013
+ }
1014
+ }
1015
+ catch (err) {
1016
+ if (controller.signal.aborted || this.disposed)
1017
+ return;
1018
+ const label = this.getSessionLabel(sessionId);
1019
+ const msg = err instanceof Error ? err.message : String(err);
1020
+ this.pushFeed(`${label}: stream error (${msg})`, sessionId);
1021
+ this.requestRender();
1022
+ }
1023
+ finally {
1024
+ const current = this.sseControllers.get(sessionId);
1025
+ if (current && current.controller === controller) {
1026
+ this.sseControllers.delete(sessionId);
1027
+ }
1028
+ }
1029
+ }
1030
+ consumeSseBuffer(sessionId, mode, rawBuffer) {
1031
+ const normalized = rawBuffer.replace(/\r\n/g, '\n');
1032
+ let cursor = 0;
1033
+ while (true) {
1034
+ const boundary = normalized.indexOf('\n\n', cursor);
1035
+ if (boundary === -1)
1036
+ break;
1037
+ const block = normalized.slice(cursor, boundary);
1038
+ cursor = boundary + 2;
1039
+ if (!block.trim())
1040
+ continue;
1041
+ let eventName = 'message';
1042
+ const dataLines = [];
1043
+ for (const line of block.split('\n')) {
1044
+ if (line.startsWith('event:')) {
1045
+ eventName = line.slice(6).trim() || eventName;
1046
+ }
1047
+ else if (line.startsWith('data:')) {
1048
+ dataLines.push(line.slice(5).trimStart());
1049
+ }
1050
+ }
1051
+ const dataText = dataLines.join('\n');
1052
+ this.handleSseEvent(sessionId, mode, eventName, dataText);
1053
+ }
1054
+ return normalized.slice(cursor);
1055
+ }
1056
+ handleSseEvent(sessionId, mode, sseEvent, dataText) {
1057
+ let payload = undefined;
1058
+ if (dataText) {
1059
+ try {
1060
+ payload = JSON.parse(dataText);
1061
+ }
1062
+ catch {
1063
+ payload = dataText;
1064
+ }
1065
+ }
1066
+ let eventName = sseEvent;
1067
+ let eventData = payload;
1068
+ if (payload && typeof payload === 'object') {
1069
+ const record = payload;
1070
+ if (typeof record.event === 'string') {
1071
+ eventName = record.event;
1072
+ eventData = record.data;
1073
+ }
1074
+ }
1075
+ if (eventName === 'hydration') {
1076
+ this.applyHydration(sessionId, eventData);
1077
+ return;
1078
+ }
1079
+ if (eventName === 'snapshot') {
1080
+ return;
1081
+ }
1082
+ const data = eventData && typeof eventData === 'object'
1083
+ ? eventData
1084
+ : undefined;
1085
+ const taskId = typeof data?.taskId === 'string' ? data.taskId : undefined;
1086
+ const text = this.formatEventFeedLine(sessionId, eventName, eventData);
1087
+ if (text) {
1088
+ this.pushFeed(text, sessionId);
1089
+ }
1090
+ if (mode === 'full') {
1091
+ this.captureStreamContent(sessionId, eventName, eventData, taskId);
1092
+ const streamLine = this.formatStreamLine(eventName, eventData);
1093
+ if (streamLine) {
1094
+ this.pushSessionFeed(sessionId, streamLine, taskId);
1095
+ }
1096
+ }
1097
+ this.requestRender();
1098
+ if (this.detailSessionId === sessionId &&
1099
+ (eventName === 'session_join' ||
1100
+ eventName === 'session_leave' ||
1101
+ eventName === 'task_start' ||
1102
+ eventName === 'task_complete' ||
1103
+ eventName === 'task_error' ||
1104
+ eventName === 'session_shutdown')) {
1105
+ void this.refreshDetail(sessionId);
1106
+ }
1107
+ if (eventName === 'session_shutdown') {
1108
+ const stream = this.sseControllers.get(sessionId);
1109
+ if (stream) {
1110
+ stream.controller.abort();
1111
+ this.sseControllers.delete(sessionId);
1112
+ }
1113
+ }
1114
+ }
1115
+ applyHydration(sessionId, eventData) {
1116
+ if (this.hydratedSessions.has(sessionId))
1117
+ return;
1118
+ if (!eventData || typeof eventData !== 'object')
1119
+ return;
1120
+ const payload = eventData;
1121
+ const loadedFromProjection = this.applyStreamProjection(sessionId, payload.stream);
1122
+ if (!loadedFromProjection) {
1123
+ const entries = Array.isArray(payload.entries) ? payload.entries : [];
1124
+ for (const rawEntry of entries) {
1125
+ if (!rawEntry || typeof rawEntry !== 'object')
1126
+ continue;
1127
+ const entry = rawEntry;
1128
+ const type = typeof entry.type === 'string' ? entry.type : '';
1129
+ const content = typeof entry.content === 'string' ? entry.content : '';
1130
+ const taskId = typeof entry.taskId === 'string' ? entry.taskId : undefined;
1131
+ if (!content)
1132
+ continue;
1133
+ if (type === 'message' || type === 'task_result') {
1134
+ this.appendBufferText(sessionId, 'output', content + '\n\n', taskId);
1135
+ }
1136
+ else if (type === 'thinking') {
1137
+ this.appendBufferText(sessionId, 'thinking', content + '\n\n', taskId);
1138
+ }
1139
+ }
1140
+ }
1141
+ this.hydratedSessions.add(sessionId);
1142
+ }
1143
+ applyStreamProjection(sessionId, streamRaw) {
1144
+ if (!streamRaw || typeof streamRaw !== 'object')
1145
+ return false;
1146
+ const stream = streamRaw;
1147
+ const sessionBuffer = this.getSessionBuffer(sessionId);
1148
+ sessionBuffer.output = typeof stream.output === 'string' ? stream.output : '';
1149
+ sessionBuffer.thinking = typeof stream.thinking === 'string' ? stream.thinking : '';
1150
+ const taskStreams = stream.tasks && typeof stream.tasks === 'object'
1151
+ ? stream.tasks
1152
+ : {};
1153
+ for (const [taskId, rawBlock] of Object.entries(taskStreams)) {
1154
+ if (!rawBlock || typeof rawBlock !== 'object')
1155
+ continue;
1156
+ const block = rawBlock;
1157
+ const taskBuffer = this.getTaskBuffer(sessionId, taskId);
1158
+ taskBuffer.output = typeof block.output === 'string' ? block.output : '';
1159
+ taskBuffer.thinking = typeof block.thinking === 'string' ? block.thinking : '';
1160
+ }
1161
+ return true;
1162
+ }
1163
+ captureStreamContent(sessionId, eventName, eventData, taskId) {
1164
+ const data = eventData && typeof eventData === 'object'
1165
+ ? eventData
1166
+ : undefined;
1167
+ if (eventName === 'message_update') {
1168
+ const assistantMessageEvent = data?.assistantMessageEvent;
1169
+ if (assistantMessageEvent && typeof assistantMessageEvent === 'object') {
1170
+ const ame = assistantMessageEvent;
1171
+ const type = typeof ame.type === 'string' ? ame.type : '';
1172
+ const delta = typeof ame.delta === 'string' ? ame.delta : '';
1173
+ if (type === 'text_delta' && delta) {
1174
+ this.appendBufferText(sessionId, 'output', delta, taskId);
1175
+ }
1176
+ else if (type === 'thinking_delta' && delta) {
1177
+ this.appendBufferText(sessionId, 'thinking', delta, taskId);
1178
+ }
1179
+ }
1180
+ return;
1181
+ }
1182
+ if (eventName === 'message_end') {
1183
+ const segments = extractMessageSegments(data);
1184
+ if (segments.thinking) {
1185
+ this.appendBufferText(sessionId, 'thinking', segments.thinking + '\n\n', taskId);
1186
+ }
1187
+ if (segments.output) {
1188
+ this.appendBufferText(sessionId, 'output', segments.output + '\n\n', taskId);
1189
+ }
1190
+ return;
1191
+ }
1192
+ if (eventName === 'thinking_end') {
1193
+ const text = typeof data?.text === 'string' ? data.text : '';
1194
+ if (text)
1195
+ this.appendBufferText(sessionId, 'thinking', text + '\n\n', taskId);
1196
+ return;
1197
+ }
1198
+ if (eventName === 'agent_progress') {
1199
+ const status = typeof data?.status === 'string' ? data.status : '';
1200
+ const delta = typeof data?.delta === 'string' ? data.delta : '';
1201
+ if (status === 'text_delta' && delta) {
1202
+ this.appendBufferText(sessionId, 'output', delta, taskId);
1203
+ }
1204
+ else if (status === 'thinking_delta' && delta) {
1205
+ this.appendBufferText(sessionId, 'thinking', delta, taskId);
1206
+ }
1207
+ return;
1208
+ }
1209
+ if (eventName === 'tool_call' || eventName === 'tool_result') {
1210
+ const line = this.formatStreamLine(eventName, eventData);
1211
+ if (line)
1212
+ this.appendBufferText(sessionId, 'output', `${line}\n\n`, taskId);
1213
+ return;
1214
+ }
1215
+ if (eventName === 'task_complete') {
1216
+ const result = typeof data?.result === 'string' ? data.result : '';
1217
+ if (result)
1218
+ this.appendBufferText(sessionId, 'output', result + '\n\n', taskId);
1219
+ return;
1220
+ }
1221
+ if (eventName === 'task_error') {
1222
+ const error = typeof data?.error === 'string' ? data.error : '';
1223
+ if (error)
1224
+ this.appendBufferText(sessionId, 'output', `[task error] ${error}\n`, taskId);
1225
+ }
1226
+ }
1227
+ formatEventFeedLine(sessionId, eventName, eventData) {
1228
+ const label = this.getSessionLabel(sessionId);
1229
+ const data = eventData && typeof eventData === 'object'
1230
+ ? eventData
1231
+ : undefined;
1232
+ if (eventName === 'task_start') {
1233
+ const taskId = typeof data?.taskId === 'string' ? shortId(data.taskId) : 'task';
1234
+ const agent = typeof data?.agent === 'string' ? data.agent : 'agent';
1235
+ return `${label}: ${taskId} started (${agent})`;
1236
+ }
1237
+ if (eventName === 'task_complete') {
1238
+ const taskId = typeof data?.taskId === 'string' ? shortId(data.taskId) : 'task';
1239
+ const duration = typeof data?.duration === 'number' ? ` ${data.duration}ms` : '';
1240
+ return `${label}: ${taskId} completed${duration}`;
1241
+ }
1242
+ if (eventName === 'task_error') {
1243
+ const taskId = typeof data?.taskId === 'string' ? shortId(data.taskId) : 'task';
1244
+ return `${label}: ${taskId} failed`;
1245
+ }
1246
+ if (eventName === 'session_join' || eventName === 'session_leave') {
1247
+ const participant = data?.participant;
1248
+ const role = typeof participant?.role === 'string' ? participant.role : 'participant';
1249
+ return `${label}: ${role} ${eventName === 'session_join' ? 'joined' : 'left'}`;
1250
+ }
1251
+ if (eventName === 'agent_start' || eventName === 'agent_end') {
1252
+ const agent = typeof data?.agentName === 'string'
1253
+ ? data.agentName
1254
+ : typeof data?.agent === 'string'
1255
+ ? data.agent
1256
+ : 'agent';
1257
+ return `${label}: ${agent} ${eventName === 'agent_start' ? 'started' : 'ended'}`;
1258
+ }
1259
+ if (eventName === 'session_complete' ||
1260
+ eventName === 'session_error' ||
1261
+ eventName === 'session_shutdown') {
1262
+ return `${label}: ${eventName}`;
1263
+ }
1264
+ return null;
1265
+ }
1266
+ getSessionLabel(sessionId) {
1267
+ const session = this.sessions.find((item) => item.sessionId === sessionId);
1268
+ return session?.label || shortId(sessionId);
1269
+ }
1270
+ getFeedEntries(scope) {
1271
+ if (scope === 'session' && this.detailSessionId) {
1272
+ return this.sessionFeed.get(this.detailSessionId) ?? [];
1273
+ }
1274
+ return this.feed;
1275
+ }
1276
+ pushFeed(text, sessionId) {
1277
+ this.feed.unshift({ at: Date.now(), sessionId, text });
1278
+ if (this.feed.length > MAX_FEED_ITEMS) {
1279
+ this.feed.length = MAX_FEED_ITEMS;
1280
+ }
1281
+ }
1282
+ pushSessionFeed(sessionId, text, taskId) {
1283
+ const entries = this.sessionFeed.get(sessionId) ?? [];
1284
+ entries.unshift({ at: Date.now(), sessionId, text });
1285
+ if (entries.length > MAX_FEED_ITEMS)
1286
+ entries.length = MAX_FEED_ITEMS;
1287
+ this.sessionFeed.set(sessionId, entries);
1288
+ if (taskId) {
1289
+ // Keep task-scoped stream buffers warm for immediate task-view drill-in.
1290
+ this.getTaskBuffer(sessionId, taskId);
1291
+ }
1292
+ }
1293
+ formatStreamLine(eventName, eventData) {
1294
+ const data = eventData && typeof eventData === 'object'
1295
+ ? eventData
1296
+ : undefined;
1297
+ const normalize = (value) => value.replace(/\s+/g, ' ').trim();
1298
+ if (eventName === 'message_update') {
1299
+ const assistantMessageEvent = data?.assistantMessageEvent;
1300
+ if (assistantMessageEvent && typeof assistantMessageEvent === 'object') {
1301
+ const ame = assistantMessageEvent;
1302
+ const type = typeof ame.type === 'string' ? ame.type : '';
1303
+ const delta = typeof ame.delta === 'string' ? ame.delta : '';
1304
+ // Keep event view high-signal: show text streaming, suppress thinking token spam.
1305
+ if (type === 'text_delta' && delta) {
1306
+ const cleaned = truncateToWidth(normalize(delta), 120);
1307
+ if (cleaned) {
1308
+ return `${type || 'delta'} ${cleaned}`;
1309
+ }
1310
+ }
1311
+ }
1312
+ return null;
1313
+ }
1314
+ if (eventName === 'message_end') {
1315
+ const segments = extractMessageSegments(data);
1316
+ const output = segments.output || segments.thinking;
1317
+ const cleaned = output ? truncateToWidth(normalize(output), 120) : '';
1318
+ return cleaned ? `message ${cleaned}` : 'message_end';
1319
+ }
1320
+ if (eventName === 'thinking_end' && typeof data?.text === 'string') {
1321
+ const cleaned = truncateToWidth(normalize(data.text), 120);
1322
+ return cleaned ? `thinking ${cleaned}` : 'thinking_end';
1323
+ }
1324
+ if (eventName === 'tool_call') {
1325
+ const name = typeof data?.name === 'string'
1326
+ ? data.name
1327
+ : typeof data?.toolName === 'string'
1328
+ ? data.toolName
1329
+ : 'tool';
1330
+ const input = data?.args ?? data?.input;
1331
+ const summarized = summarizeToolCall(name, input);
1332
+ if (summarized)
1333
+ return summarized;
1334
+ const argsPreview = summarizeArgs(input, 90);
1335
+ return argsPreview ? `tool_call ${name} ${argsPreview}` : `tool_call ${name}`;
1336
+ }
1337
+ if (eventName === 'tool_result') {
1338
+ const name = typeof data?.name === 'string'
1339
+ ? data.name
1340
+ : typeof data?.toolName === 'string'
1341
+ ? data.toolName
1342
+ : 'tool';
1343
+ const input = (data?.args ?? data?.input);
1344
+ if (name === 'task') {
1345
+ const agent = typeof input?.subagent_type === 'string' ? input.subagent_type : 'agent';
1346
+ const description = typeof input?.description === 'string'
1347
+ ? truncateToWidth(toSingleLine(input.description), 80)
1348
+ : '';
1349
+ const header = description ? `${agent} - ${description}` : `${agent} - delegated task`;
1350
+ const rawResult = extractToolResultText(data?.content);
1351
+ const stats = rawResult.match(/_(\w+): (\d+)ms \| (\d+) in (\d+) out tokens \| \$([0-9.]+)_/);
1352
+ if (stats) {
1353
+ const durationMs = Number(stats[2] ?? 0);
1354
+ const tokIn = stats[3] ?? '0';
1355
+ const tokOut = stats[4] ?? '0';
1356
+ const cost = stats[5] ?? '0';
1357
+ return `${header}\ndone ${formatDuration(durationMs)} ↑${tokIn} ↓${tokOut} $${cost}`;
1358
+ }
1359
+ const details = data?.details && typeof data.details === 'object'
1360
+ ? data.details
1361
+ : undefined;
1362
+ const duration = typeof details?.duration === 'number' ? ` ${formatDuration(details.duration)}` : '';
1363
+ const failed = data?.isError === true || details?.error === true;
1364
+ return `${header}\n${failed ? 'failed' : `done${duration}`}`;
1365
+ }
1366
+ return `tool_result ${name}`;
1367
+ }
1368
+ if (eventName === 'agent_progress') {
1369
+ const agent = typeof data?.agentName === 'string' ? data.agentName : 'agent';
1370
+ const status = typeof data?.status === 'string' ? data.status : 'progress';
1371
+ const toolName = typeof data?.currentTool === 'string' ? data.currentTool : '';
1372
+ const toolArgsRaw = typeof data?.currentToolArgs === 'string' ? data.currentToolArgs : '';
1373
+ const toolArgs = toolArgsRaw ? truncateToWidth(normalize(toolArgsRaw), 80) : '';
1374
+ // Deltas are already represented in rendered stream mode; skip them in event mode
1375
+ // to avoid noisy, low-signal token lines.
1376
+ if (status.endsWith('_delta')) {
1377
+ return null;
1378
+ }
1379
+ if (status === 'tool_start') {
1380
+ const parts = ['tool_call', agent];
1381
+ if (toolName)
1382
+ parts.push(toolName);
1383
+ if (toolArgs)
1384
+ parts.push(toolArgs);
1385
+ return parts.join(' ');
1386
+ }
1387
+ if (status === 'tool_end') {
1388
+ return toolName ? `tool_result ${agent} ${toolName}` : `tool_result ${agent}`;
1389
+ }
1390
+ if (status === 'completed' || status === 'failed') {
1391
+ return `agent ${agent} ${status}`;
1392
+ }
1393
+ if (status === 'running' && toolName) {
1394
+ return toolArgs
1395
+ ? `agent ${agent} running ${toolName} ${toolArgs}`
1396
+ : `agent ${agent} running ${toolName}`;
1397
+ }
1398
+ return null;
1399
+ }
1400
+ const parts = [];
1401
+ if (typeof data?.taskId === 'string')
1402
+ parts.push(shortId(data.taskId));
1403
+ if (typeof data?.agent === 'string')
1404
+ parts.push(data.agent);
1405
+ else if (typeof data?.agentName === 'string')
1406
+ parts.push(data.agentName);
1407
+ else if (typeof data?.name === 'string')
1408
+ parts.push(data.name);
1409
+ if (typeof data?.status === 'string')
1410
+ parts.push(data.status);
1411
+ if (typeof data?.message === 'string')
1412
+ parts.push(data.message);
1413
+ if (typeof data?.error === 'string')
1414
+ parts.push(`error: ${data.error}`);
1415
+ if (typeof data?.text === 'string')
1416
+ parts.push(data.text);
1417
+ if (typeof data?.delta === 'string')
1418
+ parts.push(data.delta);
1419
+ let detail = parts.find((part) => part.trim().length > 0) ?? '';
1420
+ if (detail) {
1421
+ detail = truncateToWidth(normalize(detail), 120);
1422
+ return `${eventName} ${detail}`;
1423
+ }
1424
+ if (eventData !== undefined) {
1425
+ try {
1426
+ const serialized = typeof eventData === 'string' ? eventData : JSON.stringify(eventData);
1427
+ if (serialized) {
1428
+ const compact = truncateToWidth(serialized.replace(/\s+/g, ' ').trim(), 140);
1429
+ return `${eventName} ${compact}`;
1430
+ }
1431
+ }
1432
+ catch {
1433
+ // Best-effort formatting only.
1434
+ }
1435
+ }
1436
+ return null;
1437
+ }
1438
+ getTaskFeedKey(sessionId, taskId) {
1439
+ return `${sessionId}:${taskId}`;
1440
+ }
1441
+ renderListScreen(width, maxLines) {
1442
+ const inner = Math.max(0, width - 2);
1443
+ const lines = [];
1444
+ const headerRows = 2;
1445
+ const footerRows = 2;
1446
+ const contentBudget = Math.max(5, maxLines - headerRows - footerRows);
1447
+ const body = [];
1448
+ lines.push(buildTopBorder(width, 'Coder Hub Sessions'));
1449
+ lines.push(this.contentLine('', inner));
1450
+ if (this.loadingList) {
1451
+ body.push(this.contentLine(this.theme.fg('dim', ' Loading sessions...'), inner));
1452
+ }
1453
+ else if (this.listError) {
1454
+ body.push(this.contentLine(this.theme.fg('error', ` ${this.listError}`), inner));
1455
+ }
1456
+ else {
1457
+ const updated = this.lastUpdatedAt
1458
+ ? `${formatClock(this.lastUpdatedAt)} updated`
1459
+ : 'not updated';
1460
+ body.push(this.contentLine(this.theme.fg('muted', ` Active: ${this.sessions.length} sessions ${updated}`), inner));
1461
+ }
1462
+ body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
1463
+ body.push(this.contentLine(this.theme.bold(' Teams / Observers'), inner));
1464
+ if (this.sessions.length === 0 && !this.loadingList && !this.listError) {
1465
+ body.push(this.contentLine(this.theme.fg('muted', ' No active Hub sessions'), inner));
1466
+ }
1467
+ else if (!this.loadingList && !this.listError) {
1468
+ const listBudget = Math.max(1, contentBudget - body.length);
1469
+ const [start, end] = getVisibleRange(this.sessions.length, this.selectedIndex, listBudget);
1470
+ if (start > 0) {
1471
+ body.push(this.contentLine(this.theme.fg('dim', ` ↑ ${start} more above`), inner));
1472
+ }
1473
+ for (let i = start; i < end; i++) {
1474
+ const session = this.sessions[i];
1475
+ const selected = i === this.selectedIndex;
1476
+ const marker = selected ? this.theme.fg('accent', '›') : ' ';
1477
+ const label = session.label || shortId(session.sessionId);
1478
+ const name = selected ? this.theme.bold(label) : label;
1479
+ const statusColor = session.status === 'running'
1480
+ ? 'success'
1481
+ : session.status === 'error' || session.status === 'failed'
1482
+ ? 'error'
1483
+ : 'warning';
1484
+ const status = this.theme.fg(statusColor, session.status);
1485
+ const self = this.currentSessionId === session.sessionId
1486
+ ? this.theme.fg('accent', ' (this)')
1487
+ : '';
1488
+ const metrics = this.theme.fg('muted', `obs:${session.observerCount} agents:${session.subAgentCount} tasks:${session.taskCount} ${formatRelative(session.createdAt)}`);
1489
+ body.push(this.contentLine(` ${marker} ${name}${self} ${status} ${session.mode} ${metrics}`, inner));
1490
+ }
1491
+ if (end < this.sessions.length) {
1492
+ body.push(this.contentLine(this.theme.fg('dim', ` ↓ ${this.sessions.length - end} more below`), inner));
1493
+ }
1494
+ }
1495
+ const windowedBody = body.slice(0, contentBudget);
1496
+ lines.push(...windowedBody);
1497
+ while (lines.length < maxLines - footerRows) {
1498
+ lines.push(this.contentLine('', inner));
1499
+ }
1500
+ lines.push(this.contentLine(this.theme.fg('dim', ' [↑↓] Select [Enter] Open [r] Refresh [Esc] Close'), inner));
1501
+ lines.push(buildBottomBorder(width));
1502
+ return lines.slice(0, maxLines);
1503
+ }
1504
+ renderDetailScreen(width, maxLines) {
1505
+ const inner = Math.max(0, width - 2);
1506
+ const lines = [];
1507
+ const title = this.detail?.label || this.detailSessionId || 'Hub Session';
1508
+ const headerRows = 3;
1509
+ const footerRows = 2;
1510
+ const contentBudget = Math.max(5, maxLines - headerRows - footerRows);
1511
+ lines.push(buildTopBorder(width, `Session ${shortId(title)}`));
1512
+ lines.push(this.contentLine('', inner));
1513
+ lines.push(this.contentLine(this.buildTopTabs('detail', true), inner));
1514
+ const body = [];
1515
+ if (this.loadingDetail) {
1516
+ body.push(this.contentLine(this.theme.fg('dim', ' Loading session detail...'), inner));
1517
+ }
1518
+ else if (this.detailError) {
1519
+ body.push(this.contentLine(this.theme.fg('error', ` ${this.detailError}`), inner));
1520
+ }
1521
+ else if (!this.detail) {
1522
+ body.push(this.contentLine(this.theme.fg('muted', ' No detail available'), inner));
1523
+ }
1524
+ else {
1525
+ const session = this.detail;
1526
+ const participants = session.participants ?? [];
1527
+ const tasks = session.tasks ?? [];
1528
+ const activityEntries = Object.entries(session.agentActivity ?? {});
1529
+ body.push(this.contentLine(this.theme.bold(' Overview'), inner));
1530
+ body.push(this.contentLine(this.theme.fg('muted', ` ID: ${session.sessionId}`), inner));
1531
+ body.push(this.contentLine(this.theme.fg('muted', ` Status: ${session.status} Mode: ${session.mode}`), inner));
1532
+ body.push(this.contentLine(this.theme.fg('muted', ` Created: ${formatRelative(session.createdAt)}`), inner));
1533
+ body.push(this.contentLine(this.theme.fg('muted', ` Participants: ${participants.length} Tasks: ${tasks.length} Active agents: ${activityEntries.length}`), inner));
1534
+ if (session.context?.branch) {
1535
+ body.push(this.contentLine(this.theme.fg('muted', ` Branch: ${session.context.branch}`), inner));
1536
+ }
1537
+ if (session.context?.workingDirectory) {
1538
+ body.push(this.contentLine(this.theme.fg('muted', ` CWD: ${session.context.workingDirectory}`), inner));
1539
+ }
1540
+ body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
1541
+ body.push(this.contentLine(this.theme.bold(' Tasks'), inner));
1542
+ body.push(this.contentLine(this.theme.fg('dim', ' Use ↑ and ↓ to move, Enter to open task view'), inner));
1543
+ if (tasks.length === 0) {
1544
+ body.push(this.contentLine(this.theme.fg('dim', ' (no tasks yet)'), inner));
1545
+ }
1546
+ else {
1547
+ if (this.selectedTaskIndex >= tasks.length) {
1548
+ this.selectedTaskIndex = tasks.length - 1;
1549
+ }
1550
+ for (let i = 0; i < Math.min(tasks.length, 50); i++) {
1551
+ const task = tasks[i];
1552
+ const selected = i === this.selectedTaskIndex;
1553
+ const statusColor = task.status === 'completed'
1554
+ ? 'success'
1555
+ : task.status === 'failed'
1556
+ ? 'error'
1557
+ : 'warning';
1558
+ const status = this.theme.fg(statusColor, task.status);
1559
+ const prompt = task.prompt
1560
+ ? truncateToWidth(toSingleLine(task.prompt), Math.max(16, inner - 34))
1561
+ : '';
1562
+ const duration = typeof task.duration === 'number' ? ` ${task.duration}ms` : '';
1563
+ const marker = selected ? this.theme.fg('accent', '›') : ' ';
1564
+ body.push(this.contentLine(`${marker} ${shortId(task.taskId).padEnd(12)} ${task.agent.padEnd(9)} ${status}${duration} ${prompt}`, inner));
1565
+ }
1566
+ }
1567
+ body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
1568
+ body.push(this.contentLine(this.theme.bold(' Session Todos'), inner));
1569
+ if (session.todosUnavailable) {
1570
+ body.push(this.contentLine(this.theme.fg('warning', ` ${session.todosUnavailable}`), inner));
1571
+ }
1572
+ else {
1573
+ const todos = session.todos ?? [];
1574
+ const summary = session.todoSummary ?? {};
1575
+ body.push(this.contentLine(this.theme.fg('dim', ` open:${summary.open ?? 0} in_progress:${summary.in_progress ?? 0} done:${summary.done ?? 0} closed:${summary.closed ?? 0} cancelled:${summary.cancelled ?? 0}`), inner));
1576
+ if (todos.length === 0) {
1577
+ body.push(this.contentLine(this.theme.fg('dim', ` (no todos linked to session ${shortId(session.sessionId)})`), inner));
1578
+ }
1579
+ else {
1580
+ for (const todo of todos.slice(0, 20)) {
1581
+ const statusColor = todo.status === 'done'
1582
+ ? 'success'
1583
+ : todo.status === 'cancelled' || todo.status === 'closed'
1584
+ ? 'error'
1585
+ : todo.status === 'in_progress'
1586
+ ? 'accent'
1587
+ : 'warning';
1588
+ const status = this.theme.fg(statusColor, todo.status);
1589
+ const details = [
1590
+ typeof todo.priority === 'string' ? `prio:${todo.priority}` : undefined,
1591
+ typeof todo.type === 'string' ? `type:${todo.type}` : undefined,
1592
+ typeof todo.assignee === 'string' && todo.assignee.length > 0
1593
+ ? `owner:${todo.assignee}`
1594
+ : undefined,
1595
+ typeof todo.attachmentCount === 'number' && todo.attachmentCount > 0
1596
+ ? `att:${todo.attachmentCount}`
1597
+ : undefined,
1598
+ ].filter((part) => !!part);
1599
+ const title = truncateToWidth(toSingleLine(todo.title || ''), Math.max(16, inner - 48));
1600
+ const meta = details.length > 0 ? this.theme.fg('dim', ` ${details.join(' ')}`) : '';
1601
+ body.push(this.contentLine(` ${shortId(todo.id).padEnd(12)} ${status} ${title}${meta}`, inner));
1602
+ }
1603
+ if (todos.length > 20) {
1604
+ body.push(this.contentLine(this.theme.fg('dim', ` ... ${todos.length - 20} more todos`), inner));
1605
+ }
1606
+ }
1607
+ }
1608
+ body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
1609
+ body.push(this.contentLine(this.theme.bold(' Participants'), inner));
1610
+ if (participants.length === 0) {
1611
+ body.push(this.contentLine(this.theme.fg('dim', ' (none)'), inner));
1612
+ }
1613
+ else {
1614
+ for (const participant of participants) {
1615
+ const when = participant.connectedAt ? formatRelative(participant.connectedAt) : '-';
1616
+ const idle = participant.idle ? this.theme.fg('warning', ' idle') : '';
1617
+ body.push(this.contentLine(` ${participant.id.padEnd(12)} ${participant.role.padEnd(9)} ${(participant.transport || 'ws').padEnd(3)} ${when}${idle}`, inner));
1618
+ }
1619
+ }
1620
+ body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
1621
+ body.push(this.contentLine(this.theme.bold(' Agent Activity'), inner));
1622
+ if (activityEntries.length === 0) {
1623
+ body.push(this.contentLine(this.theme.fg('dim', ' (none)'), inner));
1624
+ }
1625
+ else {
1626
+ for (const [agent, info] of activityEntries.slice(0, 15)) {
1627
+ const tool = info.currentTool ? ` ${info.currentTool}` : '';
1628
+ const calls = typeof info.toolCallCount === 'number'
1629
+ ? this.theme.fg('dim', ` (${info.toolCallCount} calls)`)
1630
+ : '';
1631
+ const status = info.status || 'idle';
1632
+ body.push(this.contentLine(` ${agent.padEnd(12)} ${status}${tool}${calls}`, inner));
1633
+ }
1634
+ }
1635
+ }
1636
+ this.detailMaxScroll = Math.max(0, body.length - contentBudget);
1637
+ if (this.detailScrollOffset > this.detailMaxScroll) {
1638
+ this.detailScrollOffset = this.detailMaxScroll;
1639
+ }
1640
+ const windowedBody = body.slice(this.detailScrollOffset, this.detailScrollOffset + contentBudget);
1641
+ lines.push(...windowedBody);
1642
+ while (lines.length < maxLines - footerRows) {
1643
+ lines.push(this.contentLine('', inner));
1644
+ }
1645
+ const scrollInfo = this.detailMaxScroll > 0
1646
+ ? this.theme.fg('dim', ` scroll ${this.detailScrollOffset}/${this.detailMaxScroll}`)
1647
+ : this.theme.fg('dim', ' scroll 0/0');
1648
+ lines.push(this.contentLine(`${scrollInfo} ${this.theme.fg('dim', '[↑↓] Task [j/k] Scroll [Enter] Open [r] Refresh [Esc] Back')}`, inner));
1649
+ lines.push(buildBottomBorder(width));
1650
+ return lines.slice(0, maxLines);
1651
+ }
1652
+ renderFeedScreen(width, maxLines) {
1653
+ const inner = Math.max(0, width - 2);
1654
+ const lines = [];
1655
+ const headerRows = 5;
1656
+ const footerRows = 2;
1657
+ const contentBudget = Math.max(5, maxLines - headerRows - footerRows);
1658
+ const scoped = this.feedScope === 'session' && !!this.detailSessionId;
1659
+ const title = scoped
1660
+ ? `${this.feedViewMode === 'events' ? 'Session Events' : 'Session Feed'} ${shortId(this.getSessionLabel(this.detailSessionId))}`
1661
+ : 'Global Feed';
1662
+ const entryBudget = Math.max(1, contentBudget - 2);
1663
+ const sessionBuffer = scoped && this.detailSessionId ? this.sessionBuffers.get(this.detailSessionId) : undefined;
1664
+ lines.push(buildTopBorder(width, title));
1665
+ lines.push(this.contentLine('', inner));
1666
+ lines.push(this.contentLine(this.buildTopTabs(scoped ? (this.feedViewMode === 'events' ? 'events' : 'feed') : 'feed', scoped), inner));
1667
+ lines.push(this.contentLine(this.theme.fg('muted', scoped
1668
+ ? this.feedViewMode === 'stream'
1669
+ ? ' Streaming rendered session output (sub-agent style) — [v] task stream'
1670
+ : ' Streaming full session events'
1671
+ : ' Streaming event summaries across all sessions'), inner));
1672
+ lines.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
1673
+ let contentLines = [];
1674
+ if (scoped && this.feedViewMode === 'stream') {
1675
+ contentLines = this.renderStreamLines(sessionBuffer?.output ?? '', sessionBuffer?.thinking ?? '', this.showFeedThinking, Math.max(12, inner - 4));
1676
+ if (contentLines.length === 0) {
1677
+ contentLines = [this.theme.fg('dim', '(no streamed output yet)')];
1678
+ }
1679
+ }
1680
+ else {
1681
+ const entries = this.getFeedEntries(scoped ? 'session' : 'global');
1682
+ contentLines = [...entries]
1683
+ .reverse()
1684
+ .map((entry) => `${this.theme.fg('dim', formatClock(entry.at))} ${entry.text}`);
1685
+ if (contentLines.length === 0) {
1686
+ contentLines = [this.theme.fg('dim', '(no feed items yet)')];
1687
+ }
1688
+ }
1689
+ this.feedMaxScroll = Math.max(0, contentLines.length - entryBudget);
1690
+ if (this.feedFollowing) {
1691
+ this.feedScrollOffset = this.feedMaxScroll;
1692
+ }
1693
+ if (this.feedScrollOffset > this.feedMaxScroll) {
1694
+ this.feedScrollOffset = this.feedMaxScroll;
1695
+ }
1696
+ const windowed = contentLines.slice(this.feedScrollOffset, this.feedScrollOffset + entryBudget);
1697
+ for (const line of windowed) {
1698
+ lines.push(this.contentLine(` ${line}`, inner));
1699
+ }
1700
+ while (lines.length < maxLines - footerRows) {
1701
+ lines.push(this.contentLine('', inner));
1702
+ }
1703
+ const scrollInfo = this.feedMaxScroll > 0
1704
+ ? this.theme.fg('dim', ` scroll ${this.feedScrollOffset}/${this.feedMaxScroll}`)
1705
+ : this.theme.fg('dim', ' scroll 0/0');
1706
+ const thinkingHint = scoped && this.feedViewMode === 'stream' && sessionBuffer?.thinking
1707
+ ? ' [t] Thinking'
1708
+ : '';
1709
+ const taskHint = scoped && this.feedViewMode === 'stream' ? ' [v] Task view' : '';
1710
+ const followHint = ` [f] ${this.feedFollowing ? 'Unfollow' : 'Follow'}`;
1711
+ lines.push(this.contentLine(`${scrollInfo} ${this.theme.fg('dim', `[↑↓] Scroll${thinkingHint}${taskHint}${followHint} [r] Refresh [Esc] Back`)}`, inner));
1712
+ lines.push(buildBottomBorder(width));
1713
+ return lines.slice(0, maxLines);
1714
+ }
1715
+ renderTaskScreen(width, maxLines) {
1716
+ const inner = Math.max(0, width - 2);
1717
+ const lines = [];
1718
+ const headerRows = 2;
1719
+ const footerRows = 2;
1720
+ const contentBudget = Math.max(5, maxLines - headerRows - footerRows);
1721
+ const tasks = this.getDetailTasks();
1722
+ const selected = tasks[this.selectedTaskIndex];
1723
+ const title = selected ? `Task ${shortId(selected.taskId)} ${selected.agent}` : 'Task Detail';
1724
+ lines.push(buildTopBorder(width, title));
1725
+ lines.push(this.contentLine('', inner));
1726
+ const body = [];
1727
+ if (!selected) {
1728
+ body.push(this.contentLine(this.theme.fg('dim', ' No task selected'), inner));
1729
+ }
1730
+ else {
1731
+ body.push(this.contentLine(this.theme.bold(' Task Overview'), inner));
1732
+ body.push(this.contentLine(this.theme.fg('muted', ` Task ID: ${selected.taskId}`), inner));
1733
+ body.push(this.contentLine(this.theme.fg('muted', ` Agent: ${selected.agent}`), inner));
1734
+ body.push(this.contentLine(this.theme.fg('muted', ` Status: ${selected.status}`), inner));
1735
+ if (typeof selected.duration === 'number') {
1736
+ body.push(this.contentLine(this.theme.fg('muted', ` Duration: ${selected.duration}ms`), inner));
1737
+ }
1738
+ if (selected.startedAt) {
1739
+ body.push(this.contentLine(this.theme.fg('muted', ` Started: ${selected.startedAt}`), inner));
1740
+ }
1741
+ if (selected.completedAt) {
1742
+ body.push(this.contentLine(this.theme.fg('muted', ` Completed: ${selected.completedAt}`), inner));
1743
+ }
1744
+ body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
1745
+ body.push(this.contentLine(this.theme.bold(' Prompt'), inner));
1746
+ const wrappedPrompt = wrapText(selected.prompt || '(no prompt recorded)', Math.max(10, inner - 4));
1747
+ for (const wrapped of wrappedPrompt) {
1748
+ body.push(this.contentLine(` ${wrapped}`, inner));
1749
+ }
1750
+ body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
1751
+ body.push(this.contentLine(this.theme.bold(' Task Output'), inner));
1752
+ const taskBuffer = this.detailSessionId
1753
+ ? this.taskBuffers.get(this.getTaskFeedKey(this.detailSessionId, selected.taskId))
1754
+ : undefined;
1755
+ const rendered = this.renderStreamLines(taskBuffer?.output ?? '', taskBuffer?.thinking ?? '', this.showTaskThinking, Math.max(12, inner - 4));
1756
+ if (rendered.length === 0) {
1757
+ body.push(this.contentLine(this.theme.fg('dim', ' (no task output yet)'), inner));
1758
+ }
1759
+ else {
1760
+ for (const line of rendered) {
1761
+ body.push(this.contentLine(` ${line}`, inner));
1762
+ }
1763
+ }
1764
+ }
1765
+ this.taskMaxScroll = Math.max(0, body.length - contentBudget);
1766
+ if (this.taskFollowing) {
1767
+ this.taskScrollOffset = this.taskMaxScroll;
1768
+ }
1769
+ if (this.taskScrollOffset > this.taskMaxScroll) {
1770
+ this.taskScrollOffset = this.taskMaxScroll;
1771
+ }
1772
+ const windowedBody = body.slice(this.taskScrollOffset, this.taskScrollOffset + contentBudget);
1773
+ lines.push(...windowedBody);
1774
+ while (lines.length < maxLines - footerRows) {
1775
+ lines.push(this.contentLine('', inner));
1776
+ }
1777
+ const scrollInfo = this.taskMaxScroll > 0
1778
+ ? this.theme.fg('dim', ` scroll ${this.taskScrollOffset}/${this.taskMaxScroll}`)
1779
+ : this.theme.fg('dim', ' scroll 0/0');
1780
+ const selectedTaskThinking = this.detailSessionId && selected
1781
+ ? this.taskBuffers.get(this.getTaskFeedKey(this.detailSessionId, selected.taskId))
1782
+ ?.thinking
1783
+ : '';
1784
+ const thinkingHint = selectedTaskThinking ? ' [t] Thinking' : '';
1785
+ const followHint = ` [f] ${this.taskFollowing ? 'Unfollow' : 'Follow'}`;
1786
+ lines.push(this.contentLine(`${scrollInfo} ${this.theme.fg('dim', `[↑↓] Scroll [[ and ]] Task${thinkingHint}${followHint} [Esc] Back`)}`, inner));
1787
+ lines.push(buildBottomBorder(width));
1788
+ return lines.slice(0, maxLines);
1789
+ }
1790
+ contentLine(text, innerWidth) {
1791
+ return `│${padRight(text, innerWidth)}│`;
1792
+ }
1793
+ }
1794
+ //# sourceMappingURL=hub-overlay.js.map