@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.
- package/README.md +57 -0
- package/dist/chain-preview.d.ts +55 -0
- package/dist/chain-preview.d.ts.map +1 -0
- package/dist/chain-preview.js +472 -0
- package/dist/chain-preview.js.map +1 -0
- package/dist/client.d.ts +43 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +402 -0
- package/dist/client.js.map +1 -0
- package/dist/commands.d.ts +22 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +99 -0
- package/dist/commands.js.map +1 -0
- package/dist/footer.d.ts +34 -0
- package/dist/footer.d.ts.map +1 -0
- package/dist/footer.js +249 -0
- package/dist/footer.js.map +1 -0
- package/dist/handlers.d.ts +24 -0
- package/dist/handlers.d.ts.map +1 -0
- package/dist/handlers.js +83 -0
- package/dist/handlers.js.map +1 -0
- package/dist/hub-overlay.d.ts +107 -0
- package/dist/hub-overlay.d.ts.map +1 -0
- package/dist/hub-overlay.js +1794 -0
- package/dist/hub-overlay.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1585 -0
- package/dist/index.js.map +1 -0
- package/dist/output-viewer.d.ts +49 -0
- package/dist/output-viewer.d.ts.map +1 -0
- package/dist/output-viewer.js +389 -0
- package/dist/output-viewer.js.map +1 -0
- package/dist/overlay.d.ts +40 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +225 -0
- package/dist/overlay.js.map +1 -0
- package/dist/protocol.d.ts +118 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +3 -0
- package/dist/protocol.js.map +1 -0
- package/dist/remote-session.d.ts +113 -0
- package/dist/remote-session.d.ts.map +1 -0
- package/dist/remote-session.js +645 -0
- package/dist/remote-session.js.map +1 -0
- package/dist/remote-tui.d.ts +40 -0
- package/dist/remote-tui.d.ts.map +1 -0
- package/dist/remote-tui.js +606 -0
- package/dist/remote-tui.js.map +1 -0
- package/dist/renderers.d.ts +34 -0
- package/dist/renderers.d.ts.map +1 -0
- package/dist/renderers.js +669 -0
- package/dist/renderers.js.map +1 -0
- package/dist/review.d.ts +15 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +154 -0
- package/dist/review.js.map +1 -0
- package/dist/titlebar.d.ts +3 -0
- package/dist/titlebar.d.ts.map +1 -0
- package/dist/titlebar.js +59 -0
- package/dist/titlebar.js.map +1 -0
- package/dist/todo/index.d.ts +3 -0
- package/dist/todo/index.d.ts.map +1 -0
- package/dist/todo/index.js +3 -0
- package/dist/todo/index.js.map +1 -0
- package/dist/todo/store.d.ts +6 -0
- package/dist/todo/store.d.ts.map +1 -0
- package/dist/todo/store.js +43 -0
- package/dist/todo/store.js.map +1 -0
- package/dist/todo/types.d.ts +13 -0
- package/dist/todo/types.d.ts.map +1 -0
- package/dist/todo/types.js +2 -0
- package/dist/todo/types.js.map +1 -0
- package/package.json +44 -0
- package/src/chain-preview.ts +621 -0
- package/src/client.ts +515 -0
- package/src/commands.ts +132 -0
- package/src/footer.ts +305 -0
- package/src/handlers.ts +113 -0
- package/src/hub-overlay.ts +2324 -0
- package/src/index.ts +1907 -0
- package/src/output-viewer.ts +480 -0
- package/src/overlay.ts +294 -0
- package/src/protocol.ts +157 -0
- package/src/remote-session.ts +800 -0
- package/src/remote-tui.ts +707 -0
- package/src/renderers.ts +740 -0
- package/src/review.ts +201 -0
- package/src/titlebar.ts +63 -0
- package/src/todo/index.ts +2 -0
- package/src/todo/store.ts +49 -0
- 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
|
+
}
|