@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,480 @@
1
+ import { type Theme, getMarkdownTheme } from '@mariozechner/pi-coding-agent';
2
+ import { matchesKey, Markdown as MdComponent } from '@mariozechner/pi-tui';
3
+ import { truncateToWidth } from './renderers.ts';
4
+
5
+ export interface StoredResult {
6
+ agentName: string;
7
+ text: string;
8
+ thinking: string; // Accumulated thinking tokens from streaming
9
+ timestamp: number;
10
+ tokenInfo?: string; // e.g. "scout: 1200ms | 500 in 800 out | $0.0123"
11
+ description?: string; // Short 3-5 word task description
12
+ prompt?: string; // Full detailed prompt sent to the agent
13
+ isStreaming: boolean; // true while agent is still running
14
+ }
15
+
16
+ interface Component {
17
+ render(width: number): string[];
18
+ handleInput?(data: string): void;
19
+ invalidate(): void;
20
+ }
21
+
22
+ interface Focusable {
23
+ focused: boolean;
24
+ }
25
+
26
+ type DoneFn = (result: undefined) => void;
27
+
28
+ interface TUIRef {
29
+ requestRender(): void;
30
+ }
31
+
32
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
33
+
34
+ function visibleWidth(text: string): number {
35
+ return text.replace(ANSI_RE, '').length;
36
+ }
37
+
38
+ function padRight(text: string, width: number): string {
39
+ if (width <= 0) return '';
40
+ const truncated = truncateToWidth(text, width);
41
+ const remaining = width - visibleWidth(truncated);
42
+ return remaining > 0 ? truncated + ' '.repeat(remaining) : truncated;
43
+ }
44
+
45
+ function hLine(width: number): string {
46
+ return width > 0 ? '\u2500'.repeat(width) : '';
47
+ }
48
+
49
+ function buildTopBorder(width: number, title: string): string {
50
+ if (width <= 0) return '';
51
+ if (width === 1) return '\u256D';
52
+ if (width === 2) return '\u256D\u256E';
53
+
54
+ const inner = width - 2;
55
+ const titleText = ` ${title} `;
56
+ if (titleText.length >= inner) {
57
+ return `\u256D${hLine(inner)}\u256E`;
58
+ }
59
+
60
+ const left = Math.floor((inner - titleText.length) / 2);
61
+ const right = inner - titleText.length - left;
62
+ return `\u256D${hLine(left)}${titleText}${hLine(right)}\u256E`;
63
+ }
64
+
65
+ function buildBottomBorder(width: number): string {
66
+ if (width <= 0) return '';
67
+ if (width === 1) return '\u2570';
68
+ if (width === 2) return '\u2570\u256F';
69
+ return `\u2570${hLine(width - 2)}\u256F`;
70
+ }
71
+
72
+ export class OutputViewerOverlay implements Component, Focusable {
73
+ public focused = true;
74
+
75
+ private readonly tui: TUIRef;
76
+ private readonly theme: Theme;
77
+ private readonly results: StoredResult[];
78
+ private readonly done: DoneFn;
79
+
80
+ private currentIndex: number;
81
+ private scrollOffset = 0;
82
+ private disposed = false;
83
+ private viewMode: 'output' | 'prompt' = 'output';
84
+ private showThinking = false;
85
+ private following = true;
86
+ private pollTimer: ReturnType<typeof setInterval> | null = null;
87
+ private mdRenderer: MdComponent | null = null;
88
+
89
+ constructor(
90
+ tui: TUIRef,
91
+ theme: Theme,
92
+ results: StoredResult[],
93
+ done: DoneFn,
94
+ startIndex?: number
95
+ ) {
96
+ this.tui = tui;
97
+ this.theme = theme;
98
+ this.results = results;
99
+ this.done = done;
100
+ this.currentIndex = startIndex ?? 0;
101
+
102
+ // Initialize markdown renderer
103
+ try {
104
+ const mdTheme = getMarkdownTheme?.();
105
+ if (mdTheme) {
106
+ this.mdRenderer = new MdComponent('', 0, 0, mdTheme);
107
+ }
108
+ } catch {
109
+ // Fallback: no markdown rendering
110
+ this.mdRenderer = null;
111
+ }
112
+
113
+ // Poll for streaming updates — when viewing a streaming result, trigger re-renders
114
+ this.pollTimer = setInterval(() => {
115
+ if (this.disposed) return;
116
+ const current = this.results[this.currentIndex];
117
+ if (current?.isStreaming) {
118
+ this.tui.requestRender();
119
+ }
120
+ }, 100);
121
+ }
122
+
123
+ handleInput(data: string): void {
124
+ if (this.disposed) return;
125
+
126
+ // Close overlay
127
+ if (matchesKey(data, 'escape')) {
128
+ this.close();
129
+ return;
130
+ }
131
+
132
+ const result = this.results[this.currentIndex];
133
+ if (!result) {
134
+ this.close();
135
+ return;
136
+ }
137
+
138
+ // Toggle thinking visibility
139
+ if (matchesKey(data, 't') || data.toLowerCase() === 't') {
140
+ if (result?.thinking) {
141
+ this.showThinking = !this.showThinking;
142
+ this.scrollOffset = 0;
143
+ this.invalidate();
144
+ }
145
+ return;
146
+ }
147
+
148
+ // Toggle follow mode (auto-scroll)
149
+ if (matchesKey(data, 'f') || data.toLowerCase() === 'f') {
150
+ this.following = !this.following;
151
+ if (this.following) {
152
+ const lines = this.getContentLines();
153
+ const budget = this.getContentBudget();
154
+ this.scrollOffset = Math.max(0, lines.length - budget);
155
+ }
156
+ this.invalidate();
157
+ return;
158
+ }
159
+
160
+ // Toggle between output and prompt views
161
+ if (matchesKey(data, 'p') || data.toLowerCase() === 'p') {
162
+ if (this.viewMode === 'output' && result?.prompt) {
163
+ this.viewMode = 'prompt';
164
+ } else {
165
+ this.viewMode = 'output';
166
+ }
167
+ this.scrollOffset = 0;
168
+ this.invalidate();
169
+ return;
170
+ }
171
+
172
+ const contentLines = this.getContentLines();
173
+ const contentBudget = this.getContentBudget();
174
+ const maxScroll = Math.max(0, contentLines.length - contentBudget);
175
+ const halfPage = Math.max(1, Math.floor(contentBudget / 2));
176
+
177
+ // Vim: g = top, G = bottom
178
+ if (data === 'g') {
179
+ this.following = false;
180
+ this.scrollOffset = 0;
181
+ this.invalidate();
182
+ return;
183
+ }
184
+
185
+ if (data === 'G') {
186
+ this.following = false;
187
+ this.scrollOffset = maxScroll;
188
+ this.invalidate();
189
+ return;
190
+ }
191
+
192
+ // Vim: { = jump back 25%, } = jump forward 25%
193
+ if (data === '{') {
194
+ this.following = false;
195
+ const jump = Math.max(1, Math.floor(contentLines.length * 0.25));
196
+ this.scrollOffset = Math.max(0, this.scrollOffset - jump);
197
+ this.invalidate();
198
+ return;
199
+ }
200
+
201
+ if (data === '}') {
202
+ this.following = false;
203
+ const jump = Math.max(1, Math.floor(contentLines.length * 0.25));
204
+ this.scrollOffset = Math.min(maxScroll, this.scrollOffset + jump);
205
+ this.invalidate();
206
+ return;
207
+ }
208
+
209
+ // Navigate between results
210
+ if (matchesKey(data, 'left')) {
211
+ if (this.results.length > 1) {
212
+ this.currentIndex = (this.currentIndex + 1) % this.results.length;
213
+ this.scrollOffset = 0;
214
+ this.viewMode = 'output';
215
+ this.showThinking = false;
216
+ this.invalidate();
217
+ }
218
+ return;
219
+ }
220
+
221
+ if (matchesKey(data, 'right')) {
222
+ if (this.results.length > 1) {
223
+ this.currentIndex = (this.currentIndex - 1 + this.results.length) % this.results.length;
224
+ this.scrollOffset = 0;
225
+ this.viewMode = 'output';
226
+ this.showThinking = false;
227
+ this.invalidate();
228
+ }
229
+ return;
230
+ }
231
+
232
+ // Scroll content — manual scroll disables following
233
+ if (matchesKey(data, 'up')) {
234
+ if (this.scrollOffset > 0) {
235
+ this.following = false;
236
+ this.scrollOffset -= 1;
237
+ this.invalidate();
238
+ }
239
+ return;
240
+ }
241
+
242
+ if (matchesKey(data, 'down')) {
243
+ if (this.scrollOffset < maxScroll) {
244
+ this.following = false;
245
+ this.scrollOffset += 1;
246
+ this.invalidate();
247
+ }
248
+ return;
249
+ }
250
+
251
+ // Page up (Shift+Up or PageUp)
252
+ if (matchesKey(data, 'shift+up') || matchesKey(data, 'pageUp')) {
253
+ this.following = false;
254
+ this.scrollOffset = Math.max(0, this.scrollOffset - halfPage);
255
+ this.invalidate();
256
+ return;
257
+ }
258
+
259
+ // Page down (Shift+Down or PageDown)
260
+ if (matchesKey(data, 'shift+down') || matchesKey(data, 'pageDown')) {
261
+ this.following = false;
262
+ this.scrollOffset = Math.min(maxScroll, this.scrollOffset + halfPage);
263
+ this.invalidate();
264
+ return;
265
+ }
266
+
267
+ // Home — jump to top
268
+ if (matchesKey(data, 'home')) {
269
+ this.following = false;
270
+ this.scrollOffset = 0;
271
+ this.invalidate();
272
+ return;
273
+ }
274
+
275
+ // End — jump to bottom
276
+ if (matchesKey(data, 'end')) {
277
+ this.following = false;
278
+ this.scrollOffset = maxScroll;
279
+ this.invalidate();
280
+ return;
281
+ }
282
+ }
283
+
284
+ render(width: number): string[] {
285
+ const safeWidth = Math.max(4, width);
286
+ const inner = Math.max(0, safeWidth - 2);
287
+ const termHeight = process.stdout.rows || 40;
288
+ // Match overlay maxHeight of 95%, leave margin for overlay chrome
289
+ const maxLines = Math.max(10, Math.floor(termHeight * 0.95) - 2);
290
+
291
+ const result = this.results[this.currentIndex];
292
+ if (!result) {
293
+ const lines = [
294
+ buildTopBorder(safeWidth, 'Output Viewer'),
295
+ this.contentLine(this.theme.fg('muted', ' No results available'), inner),
296
+ this.contentLine(this.theme.fg('dim', ' [Esc] Close'), inner),
297
+ buildBottomBorder(safeWidth),
298
+ ];
299
+ return lines.map((line) => truncateToWidth(line, safeWidth));
300
+ }
301
+
302
+ // Build header title with streaming indicator
303
+ const streamLabel = result.isStreaming ? ' LIVE' : '';
304
+ const nameLabel = result.description
305
+ ? `${result.agentName} - ${result.description}`
306
+ : result.agentName;
307
+ const posLabel =
308
+ this.results.length > 1
309
+ ? `${nameLabel}${streamLabel} (${this.currentIndex + 1} of ${this.results.length})`
310
+ : `${nameLabel}${streamLabel}`;
311
+ const titleLabel = this.viewMode === 'prompt' ? `${posLabel} [PROMPT]` : posLabel;
312
+
313
+ const header: string[] = [buildTopBorder(safeWidth, titleLabel)];
314
+
315
+ // Sub-header: token info or prompt-mode indicator
316
+ if (this.viewMode === 'prompt') {
317
+ header.push(this.contentLine(this.theme.fg('dim', ' Prompt sent to agent:'), inner));
318
+ } else if (result.tokenInfo) {
319
+ header.push(this.contentLine(this.theme.fg('dim', ` ${result.tokenInfo}`), inner));
320
+ } else {
321
+ header.push(this.contentLine('', inner));
322
+ }
323
+ header.push(this.contentLine('', inner)); // padding line after header
324
+
325
+ // Footer with position indicator and new hints
326
+ const thinkingHint = result.thinking
327
+ ? `[t] ${this.showThinking ? 'Hide' : 'Show'} thinking `
328
+ : '';
329
+ const followHint = result.isStreaming
330
+ ? `[f] ${this.following ? 'Unfollow' : 'Follow'} `
331
+ : '';
332
+ const promptHint = result.prompt ? '[p] Prompt ' : '';
333
+ const navHint = this.results.length > 1 ? '[<- ->] Switch ' : '';
334
+
335
+ // Content assembly via helper
336
+ const contentLines = this.getContentLines();
337
+ const totalLines = contentLines.length;
338
+
339
+ // Content area
340
+ const contentBudget = Math.max(1, maxLines - header.length - 3); // 3 = footer lines (padding + help + border)
341
+
342
+ // Auto-follow when streaming
343
+ if (this.following && result.isStreaming) {
344
+ this.scrollOffset = Math.max(0, totalLines - contentBudget);
345
+ }
346
+
347
+ const maxScroll = Math.max(0, totalLines - contentBudget);
348
+
349
+ // Clamp scroll offset
350
+ if (this.scrollOffset > maxScroll) {
351
+ this.scrollOffset = maxScroll;
352
+ }
353
+
354
+ // Position indicator
355
+ const currentLine = Math.min(this.scrollOffset + 1, totalLines);
356
+ const pct = totalLines > 0 ? Math.round((currentLine / totalLines) * 100) : 0;
357
+ const posInfo = totalLines > 0 ? `L${currentLine}/${totalLines} ${pct}% ` : '';
358
+
359
+ const vimHint = totalLines > contentBudget ? '[g/G] Top/Bot [{/}] Jump ' : '';
360
+ const footer: string[] = [
361
+ this.contentLine('', inner), // padding line before footer
362
+ this.contentLine(
363
+ this.theme.fg(
364
+ 'dim',
365
+ ` ${posInfo}${vimHint}[Up/Dn] Scroll ${thinkingHint}${followHint}${promptHint}${navHint}[Esc] Close`
366
+ ),
367
+ inner
368
+ ),
369
+ buildBottomBorder(safeWidth),
370
+ ];
371
+
372
+ const content: string[] = [];
373
+
374
+ // Scroll indicator: above
375
+ const aboveCount = this.scrollOffset;
376
+ if (aboveCount > 0) {
377
+ content.push(
378
+ this.contentLine(this.theme.fg('dim', ` ^ ${aboveCount} more above`), inner)
379
+ );
380
+ }
381
+
382
+ // Visible lines
383
+ const visibleBudget =
384
+ aboveCount > 0
385
+ ? contentBudget - 1 // reserve 1 line for "above" indicator
386
+ : contentBudget;
387
+ const belowCount = totalLines - this.scrollOffset - visibleBudget;
388
+ const actualVisible = belowCount > 0 ? visibleBudget - 1 : visibleBudget; // reserve 1 for "below"
389
+
390
+ const sliceEnd = Math.min(this.scrollOffset + actualVisible, totalLines);
391
+ for (let i = this.scrollOffset; i < sliceEnd; i++) {
392
+ const line = contentLines[i] ?? '';
393
+ content.push(
394
+ this.contentLine(' ' + truncateToWidth(line, Math.max(0, inner - 3)), inner)
395
+ );
396
+ }
397
+
398
+ // Scroll indicator: below
399
+ const remainingBelow = totalLines - sliceEnd;
400
+ if (remainingBelow > 0) {
401
+ content.push(
402
+ this.contentLine(this.theme.fg('dim', ` v ${remainingBelow} more below`), inner)
403
+ );
404
+ }
405
+
406
+ const lines = [...header, ...content, ...footer];
407
+ return lines.map((line) => truncateToWidth(line, safeWidth));
408
+ }
409
+
410
+ invalidate(): void {
411
+ // Stateless rendering; no cache invalidation required.
412
+ }
413
+
414
+ dispose(): void {
415
+ this.disposed = true;
416
+ if (this.pollTimer) {
417
+ clearInterval(this.pollTimer);
418
+ this.pollTimer = null;
419
+ }
420
+ }
421
+
422
+ private getContentLines(): string[] {
423
+ const result = this.results[this.currentIndex];
424
+ if (!result) return [];
425
+
426
+ const lines: string[] = [];
427
+
428
+ // Thinking section (dimmed) — keep as plain text, it's raw thinking
429
+ if (this.showThinking && result.thinking) {
430
+ const thinkingLines = result.thinking.split('\n');
431
+ for (const line of thinkingLines) {
432
+ lines.push(this.theme.fg('dim', line));
433
+ }
434
+ lines.push(this.theme.fg('muted', '--- end thinking ---'));
435
+ lines.push(''); // empty line separator
436
+ }
437
+
438
+ // Main content — render as markdown for syntax highlighting
439
+ const mainText = this.viewMode === 'prompt' && result.prompt ? result.prompt : result.text;
440
+ if (mainText) {
441
+ if (this.mdRenderer) {
442
+ try {
443
+ this.mdRenderer.setText(mainText);
444
+ // Render with inner width minus padding (3 spaces left + border chars)
445
+ const termWidth = Math.max(20, (process.stdout.columns || 80) - 10);
446
+ const rendered = this.mdRenderer.render(termWidth);
447
+ lines.push(...rendered);
448
+ } catch {
449
+ // Fallback: plain text if markdown rendering fails
450
+ lines.push(...mainText.split('\n'));
451
+ }
452
+ } else {
453
+ // Fallback: plain text
454
+ lines.push(...mainText.split('\n'));
455
+ }
456
+ }
457
+
458
+ return lines;
459
+ }
460
+
461
+ private getContentBudget(): number {
462
+ const termHeight = process.stdout.rows || 40;
463
+ const maxLines = Math.max(10, Math.floor(termHeight * 0.95) - 2);
464
+ return Math.max(1, maxLines - 4); // 4 = header + footer lines
465
+ }
466
+
467
+ private contentLine(content: string, innerWidth: number): string {
468
+ return `\u2502${padRight(content, innerWidth)}\u2502`;
469
+ }
470
+
471
+ private close(): void {
472
+ if (this.disposed) return;
473
+ this.disposed = true;
474
+ if (this.pollTimer) {
475
+ clearInterval(this.pollTimer);
476
+ this.pollTimer = null;
477
+ }
478
+ this.done(undefined);
479
+ }
480
+ }