@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,621 @@
1
+ import type { Theme } from '@mariozechner/pi-coding-agent';
2
+ import { matchesKey } from '@mariozechner/pi-tui';
3
+ import type { AgentDefinition } from './protocol.ts';
4
+ import { truncateToWidth } from './renderers.ts';
5
+
6
+ export interface ChainResult {
7
+ mode: 'sequential' | 'parallel';
8
+ steps: Array<{
9
+ agent: string;
10
+ task: string;
11
+ }>;
12
+ }
13
+
14
+ interface Component {
15
+ render(width: number): string[];
16
+ handleInput?(data: string): void;
17
+ invalidate(): void;
18
+ }
19
+
20
+ interface Focusable {
21
+ focused: boolean;
22
+ }
23
+
24
+ interface ChainStep {
25
+ agent: string;
26
+ task: string;
27
+ }
28
+
29
+ type DoneFn = (result: ChainResult | undefined) => void;
30
+ type Mode = 'sequential' | 'parallel';
31
+ type ScreenMode = 'compose' | 'picker' | 'edit';
32
+
33
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
34
+
35
+ function visibleWidth(text: string): number {
36
+ return text.replace(ANSI_RE, '').length;
37
+ }
38
+
39
+ function padRight(text: string, width: number): string {
40
+ if (width <= 0) return '';
41
+ const truncated = truncateToWidth(text, width);
42
+ const remaining = width - visibleWidth(truncated);
43
+ return remaining > 0 ? truncated + ' '.repeat(remaining) : truncated;
44
+ }
45
+
46
+ function hLine(width: number): string {
47
+ return width > 0 ? '─'.repeat(width) : '';
48
+ }
49
+
50
+ function buildTopBorder(width: number, title: string): string {
51
+ if (width <= 0) return '';
52
+ if (width === 1) return '╭';
53
+ if (width === 2) return '╭╮';
54
+
55
+ const inner = width - 2;
56
+ const titleText = ` ${title} `;
57
+ if (titleText.length >= inner) {
58
+ return `╭${hLine(inner)}╮`;
59
+ }
60
+
61
+ const left = Math.floor((inner - titleText.length) / 2);
62
+ const right = inner - titleText.length - left;
63
+ return `╭${hLine(left)}${titleText}${hLine(right)}╮`;
64
+ }
65
+
66
+ function buildBottomBorder(width: number): string {
67
+ if (width <= 0) return '';
68
+ if (width === 1) return '╰';
69
+ if (width === 2) return '╰╯';
70
+ return `╰${hLine(width - 2)}╯`;
71
+ }
72
+
73
+ function parsePrintableChar(data: string): string | null {
74
+ if (!data || data.length !== 1) return null;
75
+ const code = data.charCodeAt(0);
76
+ if (code < 32 || code === 127) return null;
77
+ return data;
78
+ }
79
+
80
+ export class ChainEditorOverlay implements Component, Focusable {
81
+ public focused = true;
82
+
83
+ private readonly theme: Theme;
84
+ private readonly done: DoneFn;
85
+ private readonly agentByName: Map<string, AgentDefinition>;
86
+ private readonly availableAgents: AgentDefinition[];
87
+
88
+ private mode: Mode = 'sequential';
89
+ private screen: ScreenMode = 'compose';
90
+ private steps: ChainStep[];
91
+ private selectedStepIndex = 0;
92
+ private statusMessage = '';
93
+ private readonly maxVisibleItems = 6;
94
+
95
+ private pickerIndex = 0;
96
+ private pickerFilter = '';
97
+
98
+ private editBuffer = '';
99
+ private editCursor = 0;
100
+ private previousTask = '';
101
+
102
+ private disposed = false;
103
+
104
+ constructor(
105
+ theme: Theme,
106
+ agents: AgentDefinition[],
107
+ done: DoneFn,
108
+ initialAgents: string[] = []
109
+ ) {
110
+ this.theme = theme;
111
+ this.done = done;
112
+ this.availableAgents = [...agents];
113
+ this.agentByName = new Map(agents.map((agent) => [agent.name, agent]));
114
+ this.steps = this.buildInitialSteps(initialAgents);
115
+ }
116
+
117
+ handleInput(data: string): void {
118
+ if (this.disposed) return;
119
+
120
+ if (this.screen === 'picker') {
121
+ this.handlePickerInput(data);
122
+ return;
123
+ }
124
+
125
+ if (this.screen === 'edit') {
126
+ this.handleEditInput(data);
127
+ return;
128
+ }
129
+
130
+ this.handleComposeInput(data);
131
+ }
132
+
133
+ render(width: number): string[] {
134
+ const safeWidth = Math.max(4, width);
135
+ const termHeight = process.stdout.rows || 40;
136
+ // Match overlay maxHeight of 95%, leave margin for overlay chrome
137
+ const maxLines = Math.max(10, Math.floor(termHeight * 0.95) - 2);
138
+
139
+ const lines =
140
+ this.screen === 'picker'
141
+ ? this.renderPickerScreen(safeWidth, maxLines)
142
+ : this.renderComposeScreen(safeWidth, maxLines);
143
+ return lines.map((line) => truncateToWidth(line, safeWidth));
144
+ }
145
+
146
+ invalidate(): void {
147
+ // Stateless rendering; no cache invalidation required.
148
+ }
149
+
150
+ dispose(): void {
151
+ this.disposed = true;
152
+ }
153
+
154
+ private buildInitialSteps(initialAgents: string[]): ChainStep[] {
155
+ const names = initialAgents
156
+ .map((name) => name.trim())
157
+ .filter((name) => name.length > 0)
158
+ .filter((name) => this.agentByName.has(name));
159
+
160
+ return names.map((agent, index) => ({
161
+ agent,
162
+ task: index === 0 ? '' : '(from previous step)',
163
+ }));
164
+ }
165
+
166
+ private handleComposeInput(data: string): void {
167
+ if (matchesKey(data, 'escape')) {
168
+ this.close(undefined);
169
+ return;
170
+ }
171
+
172
+ if (matchesKey(data, 'up')) {
173
+ if (this.steps.length > 0) {
174
+ this.selectedStepIndex =
175
+ (this.selectedStepIndex - 1 + this.steps.length) % this.steps.length;
176
+ this.statusMessage = '';
177
+ }
178
+ this.invalidate();
179
+ return;
180
+ }
181
+
182
+ if (matchesKey(data, 'down')) {
183
+ if (this.steps.length > 0) {
184
+ this.selectedStepIndex = (this.selectedStepIndex + 1) % this.steps.length;
185
+ this.statusMessage = '';
186
+ }
187
+ this.invalidate();
188
+ return;
189
+ }
190
+
191
+ if (matchesKey(data, 'a') || data.toLowerCase() === 'a') {
192
+ this.screen = 'picker';
193
+ this.pickerFilter = '';
194
+ this.pickerIndex = 0;
195
+ this.statusMessage = '';
196
+ this.invalidate();
197
+ return;
198
+ }
199
+
200
+ if (matchesKey(data, 'd') || matchesKey(data, 'delete') || data.toLowerCase() === 'd') {
201
+ if (this.steps.length > 0) {
202
+ this.steps.splice(this.selectedStepIndex, 1);
203
+ if (this.selectedStepIndex >= this.steps.length) {
204
+ this.selectedStepIndex = Math.max(0, this.steps.length - 1);
205
+ }
206
+ this.statusMessage = '';
207
+ this.invalidate();
208
+ }
209
+ return;
210
+ }
211
+
212
+ if (matchesKey(data, 'e') || data.toLowerCase() === 'e') {
213
+ const selected = this.steps[this.selectedStepIndex];
214
+ if (!selected) return;
215
+ this.previousTask = selected.task;
216
+ this.editBuffer = selected.task;
217
+ this.editCursor = this.editBuffer.length;
218
+ this.screen = 'edit';
219
+ this.statusMessage = '';
220
+ this.invalidate();
221
+ return;
222
+ }
223
+
224
+ if (matchesKey(data, 'p') || data.toLowerCase() === 'p') {
225
+ this.mode = this.mode === 'sequential' ? 'parallel' : 'sequential';
226
+ this.statusMessage = '';
227
+ this.invalidate();
228
+ return;
229
+ }
230
+
231
+ if (matchesKey(data, 'enter')) {
232
+ if (this.steps.length < 2) {
233
+ this.statusMessage = 'Need at least 2 steps to run.';
234
+ this.invalidate();
235
+ return;
236
+ }
237
+
238
+ this.close({
239
+ mode: this.mode,
240
+ steps: this.steps.map((step) => ({
241
+ agent: step.agent,
242
+ task: step.task,
243
+ })),
244
+ });
245
+ }
246
+ }
247
+
248
+ private handlePickerInput(data: string): void {
249
+ if (matchesKey(data, 'escape')) {
250
+ this.screen = 'compose';
251
+ this.invalidate();
252
+ return;
253
+ }
254
+
255
+ const filtered = this.getFilteredAgents();
256
+
257
+ if (matchesKey(data, 'up')) {
258
+ if (filtered.length > 0) {
259
+ this.pickerIndex = (this.pickerIndex - 1 + filtered.length) % filtered.length;
260
+ }
261
+ this.invalidate();
262
+ return;
263
+ }
264
+
265
+ if (matchesKey(data, 'down')) {
266
+ if (filtered.length > 0) {
267
+ this.pickerIndex = (this.pickerIndex + 1) % filtered.length;
268
+ }
269
+ this.invalidate();
270
+ return;
271
+ }
272
+
273
+ if (matchesKey(data, 'backspace')) {
274
+ if (this.pickerFilter.length > 0) {
275
+ this.pickerFilter = this.pickerFilter.slice(0, -1);
276
+ this.pickerIndex = 0;
277
+ this.invalidate();
278
+ }
279
+ return;
280
+ }
281
+
282
+ if (matchesKey(data, 'enter')) {
283
+ const selected = filtered[this.pickerIndex];
284
+ if (!selected) return;
285
+
286
+ this.steps.push({
287
+ agent: selected.name,
288
+ task: this.steps.length === 0 ? '' : '(from previous step)',
289
+ });
290
+ this.selectedStepIndex = this.steps.length - 1;
291
+ this.screen = 'compose';
292
+ this.statusMessage = '';
293
+ this.invalidate();
294
+ return;
295
+ }
296
+
297
+ const char = parsePrintableChar(data);
298
+ if (char) {
299
+ this.pickerFilter += char;
300
+ this.pickerIndex = 0;
301
+ this.invalidate();
302
+ }
303
+ }
304
+
305
+ private handleEditInput(data: string): void {
306
+ const selected = this.steps[this.selectedStepIndex];
307
+ if (!selected) {
308
+ this.screen = 'compose';
309
+ return;
310
+ }
311
+
312
+ if (matchesKey(data, 'escape')) {
313
+ selected.task = this.previousTask;
314
+ this.screen = 'compose';
315
+ this.invalidate();
316
+ return;
317
+ }
318
+
319
+ if (matchesKey(data, 'enter')) {
320
+ selected.task = this.editBuffer;
321
+ this.screen = 'compose';
322
+ this.invalidate();
323
+ return;
324
+ }
325
+
326
+ if (matchesKey(data, 'left')) {
327
+ this.editCursor = Math.max(0, this.editCursor - 1);
328
+ this.invalidate();
329
+ return;
330
+ }
331
+
332
+ if (matchesKey(data, 'right')) {
333
+ this.editCursor = Math.min(this.editBuffer.length, this.editCursor + 1);
334
+ this.invalidate();
335
+ return;
336
+ }
337
+
338
+ if (matchesKey(data, 'home')) {
339
+ this.editCursor = 0;
340
+ this.invalidate();
341
+ return;
342
+ }
343
+
344
+ if (matchesKey(data, 'end')) {
345
+ this.editCursor = this.editBuffer.length;
346
+ this.invalidate();
347
+ return;
348
+ }
349
+
350
+ if (matchesKey(data, 'backspace')) {
351
+ if (this.editCursor > 0) {
352
+ this.editBuffer =
353
+ this.editBuffer.slice(0, this.editCursor - 1) +
354
+ this.editBuffer.slice(this.editCursor);
355
+ this.editCursor -= 1;
356
+ selected.task = this.editBuffer;
357
+ this.invalidate();
358
+ }
359
+ return;
360
+ }
361
+
362
+ if (matchesKey(data, 'delete')) {
363
+ if (this.editCursor < this.editBuffer.length) {
364
+ this.editBuffer =
365
+ this.editBuffer.slice(0, this.editCursor) +
366
+ this.editBuffer.slice(this.editCursor + 1);
367
+ selected.task = this.editBuffer;
368
+ this.invalidate();
369
+ }
370
+ return;
371
+ }
372
+
373
+ const char = parsePrintableChar(data);
374
+ if (char) {
375
+ this.editBuffer =
376
+ this.editBuffer.slice(0, this.editCursor) +
377
+ char +
378
+ this.editBuffer.slice(this.editCursor);
379
+ this.editCursor += char.length;
380
+ selected.task = this.editBuffer;
381
+ this.invalidate();
382
+ }
383
+ }
384
+
385
+ private getFilteredAgents(): AgentDefinition[] {
386
+ const query = this.pickerFilter.trim().toLowerCase();
387
+ if (!query) return this.availableAgents;
388
+ return this.availableAgents.filter((agent) => {
389
+ const haystack = `${agent.name} ${agent.description}`.toLowerCase();
390
+ return haystack.includes(query);
391
+ });
392
+ }
393
+
394
+ private renderComposeScreen(width: number, maxLines: number): string[] {
395
+ const inner = Math.max(0, width - 2);
396
+
397
+ // Fixed header (always rendered)
398
+ const chainSummary =
399
+ this.steps.length > 0 ? this.steps.map((step) => step.agent).join(' → ') : '(empty)';
400
+ const header: string[] = [
401
+ buildTopBorder(width, 'Chain Editor'),
402
+ this.contentLine('', inner),
403
+ this.contentLine(this.theme.fg('text', ` Chain: ${chainSummary}`), inner),
404
+ this.contentLine(this.theme.fg('muted', ` Mode: ${this.mode}`), inner),
405
+ this.contentLine('', inner),
406
+ ];
407
+
408
+ // Fixed footer (always rendered)
409
+ const hintRun = this.steps.length >= 2 ? '[Enter] Run' : '[Enter] Run (needs 2+ steps)';
410
+ const footer: string[] = [
411
+ this.contentLine(
412
+ this.theme.fg('dim', ` [↑↓] Navigate [e] Edit task [d] Remove`),
413
+ inner
414
+ ),
415
+ this.contentLine(
416
+ this.theme.fg('dim', ` [a] Add step [p] Toggle mode ${hintRun} [Esc] Cancel`),
417
+ inner
418
+ ),
419
+ buildBottomBorder(width),
420
+ ];
421
+
422
+ // Available lines for scrollable content area
423
+ const contentBudget = Math.max(4, maxLines - header.length - footer.length);
424
+
425
+ if (this.steps.length === 0) {
426
+ const content = [
427
+ this.contentLine(
428
+ this.theme.fg('muted', ' No steps yet. Press [a] to add an agent step.'),
429
+ inner
430
+ ),
431
+ this.contentLine('', inner),
432
+ ];
433
+ return [...header, ...content, ...footer];
434
+ }
435
+
436
+ // Each step takes 3 lines (name+model, task, empty) or 4 lines (with edit hint)
437
+ const LINES_PER_STEP = 3;
438
+ // Reserve 2 lines for possible scroll indicators + status message
439
+ const scrollReserve = this.statusMessage ? 3 : 2;
440
+ const maxSteps = Math.max(1, Math.floor((contentBudget - scrollReserve) / LINES_PER_STEP));
441
+
442
+ const windowSize = Math.min(maxSteps, this.steps.length);
443
+ const [startIdx, endIdx] = this.getStepVisibleRange(windowSize);
444
+
445
+ const content: string[] = [];
446
+
447
+ if (startIdx > 0) {
448
+ content.push(this.contentLine(this.theme.fg('dim', ` ↑ ${startIdx} more above`), inner));
449
+ }
450
+
451
+ for (let i = startIdx; i < endIdx; i++) {
452
+ const step = this.steps[i]!;
453
+ const selected = i === this.selectedStepIndex;
454
+ const marker = selected ? this.theme.fg('accent', '►') : ' ';
455
+ const agent = this.agentByName.get(step.agent);
456
+ const model = agent?.model ? this.theme.fg('dim', ` [${agent.model}]`) : '';
457
+
458
+ content.push(
459
+ this.contentLine(
460
+ `${marker} ${this.theme.bold(`Step ${i + 1}: ${step.agent}`)}${model}`,
461
+ inner
462
+ )
463
+ );
464
+
465
+ if (this.screen === 'edit' && selected) {
466
+ const displayTask =
467
+ this.editBuffer.slice(0, this.editCursor) +
468
+ this.theme.fg('accent', '│') +
469
+ this.editBuffer.slice(this.editCursor);
470
+ content.push(this.contentLine(this.theme.fg('text', ` task: ${displayTask}`), inner));
471
+ content.push(
472
+ this.contentLine(
473
+ this.theme.fg('dim', ' editing: [Enter] Save [Esc] Cancel [←→] Move cursor'),
474
+ inner
475
+ )
476
+ );
477
+ } else {
478
+ const task = step.task || this.theme.fg('muted', '(empty)');
479
+ content.push(this.contentLine(this.theme.fg('text', ` task: ${task}`), inner));
480
+ }
481
+
482
+ content.push(this.contentLine('', inner));
483
+ }
484
+
485
+ if (endIdx < this.steps.length) {
486
+ content.push(
487
+ this.contentLine(
488
+ this.theme.fg('dim', ` ↓ ${this.steps.length - endIdx} more below`),
489
+ inner
490
+ )
491
+ );
492
+ }
493
+
494
+ if (this.statusMessage) {
495
+ content.push(this.contentLine(this.theme.fg('warning', ` ${this.statusMessage}`), inner));
496
+ }
497
+
498
+ return [...header, ...content, ...footer];
499
+ }
500
+
501
+ private renderPickerScreen(width: number, maxLines: number): string[] {
502
+ const inner = Math.max(0, width - 2);
503
+ const filtered = this.getFilteredAgents();
504
+
505
+ if (this.pickerIndex >= filtered.length) {
506
+ this.pickerIndex = Math.max(0, filtered.length - 1);
507
+ }
508
+
509
+ // Fixed header (always rendered)
510
+ const header: string[] = [
511
+ buildTopBorder(width, 'Add Agent Step'),
512
+ this.contentLine('', inner),
513
+ this.contentLine(
514
+ this.theme.fg('text', ` Filter: ${this.pickerFilter || '(type to filter)'}`),
515
+ inner
516
+ ),
517
+ this.contentLine('', inner),
518
+ ];
519
+
520
+ // Fixed footer (always rendered)
521
+ const footer: string[] = [
522
+ this.contentLine(
523
+ this.theme.fg('dim', ' [↑↓] Navigate [Enter] Select [Esc] Back [Backspace] Filter'),
524
+ inner
525
+ ),
526
+ buildBottomBorder(width),
527
+ ];
528
+
529
+ // Available lines for scrollable content area
530
+ const contentBudget = Math.max(4, maxLines - header.length - footer.length);
531
+
532
+ if (filtered.length === 0) {
533
+ const content = [
534
+ this.contentLine(this.theme.fg('muted', ' No agents match filter.'), inner),
535
+ this.contentLine('', inner),
536
+ ];
537
+ return [...header, ...content, ...footer];
538
+ }
539
+
540
+ // Each agent takes 3 lines: name, description, empty line
541
+ const LINES_PER_AGENT = 3;
542
+ // Reserve 2 lines for possible scroll indicators
543
+ const scrollReserve = 2;
544
+ const maxAgents = Math.max(1, Math.floor((contentBudget - scrollReserve) / LINES_PER_AGENT));
545
+
546
+ const windowSize = Math.min(maxAgents, filtered.length);
547
+ const [startIdx, endIdx] = this.getPickerVisibleRange(filtered.length, windowSize);
548
+
549
+ const content: string[] = [];
550
+
551
+ if (startIdx > 0) {
552
+ content.push(this.contentLine(this.theme.fg('dim', ` ↑ ${startIdx} more above`), inner));
553
+ }
554
+
555
+ for (let i = startIdx; i < endIdx; i++) {
556
+ const agent = filtered[i]!;
557
+ const selected = i === this.pickerIndex;
558
+ const marker = selected ? this.theme.fg('accent', '► ') : ' ';
559
+ const model = agent.model ? this.theme.fg('dim', ` [${agent.model}]`) : '';
560
+ content.push(this.contentLine(`${marker}${this.theme.bold(agent.name)}${model}`, inner));
561
+ content.push(
562
+ this.contentLine(this.theme.fg('muted', ` ${agent.description || ''}`), inner)
563
+ );
564
+ content.push(this.contentLine('', inner));
565
+ }
566
+
567
+ if (endIdx < filtered.length) {
568
+ content.push(
569
+ this.contentLine(
570
+ this.theme.fg('dim', ` ↓ ${filtered.length - endIdx} more below`),
571
+ inner
572
+ )
573
+ );
574
+ }
575
+
576
+ return [...header, ...content, ...footer];
577
+ }
578
+
579
+ private contentLine(content: string, innerWidth: number): string {
580
+ return `│${padRight(content, innerWidth)}│`;
581
+ }
582
+
583
+ private getStepVisibleRange(windowSize?: number): [number, number] {
584
+ const count = this.steps.length;
585
+ const ws = windowSize ?? this.maxVisibleItems;
586
+ if (count <= ws) return [0, count];
587
+
588
+ const half = Math.floor(ws / 2);
589
+ let start = Math.max(0, this.selectedStepIndex - half);
590
+ let end = start + ws;
591
+
592
+ if (end > count) {
593
+ end = count;
594
+ start = Math.max(0, end - ws);
595
+ }
596
+
597
+ return [start, end];
598
+ }
599
+
600
+ private getPickerVisibleRange(count: number, windowSize?: number): [number, number] {
601
+ const ws = windowSize ?? this.maxVisibleItems;
602
+ if (count <= ws) return [0, count];
603
+
604
+ const half = Math.floor(ws / 2);
605
+ let start = Math.max(0, this.pickerIndex - half);
606
+ let end = start + ws;
607
+
608
+ if (end > count) {
609
+ end = count;
610
+ start = Math.max(0, end - ws);
611
+ }
612
+
613
+ return [start, end];
614
+ }
615
+
616
+ private close(result: ChainResult | undefined): void {
617
+ if (this.disposed) return;
618
+ this.disposed = true;
619
+ this.done(result);
620
+ }
621
+ }