pi-subagents 0.3.0
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/CHANGELOG.md +94 -0
- package/README.md +300 -0
- package/agents.ts +172 -0
- package/artifacts.ts +70 -0
- package/chain-clarify.ts +612 -0
- package/index.ts +2186 -0
- package/install.mjs +93 -0
- package/notify.ts +87 -0
- package/package.json +38 -0
- package/settings.ts +492 -0
- package/subagent-runner.ts +608 -0
- package/types.ts +114 -0
package/chain-clarify.ts
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chain Clarification TUI Component
|
|
3
|
+
*
|
|
4
|
+
* Shows templates and resolved behaviors for each step in a chain.
|
|
5
|
+
* Supports editing templates, output paths, reads lists, and progress toggle.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
10
|
+
import { matchesKey, visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
11
|
+
import type { AgentConfig } from "./agents.js";
|
|
12
|
+
import type { ResolvedStepBehavior } from "./settings.js";
|
|
13
|
+
|
|
14
|
+
/** Modified behavior overrides from TUI editing */
|
|
15
|
+
export interface BehaviorOverride {
|
|
16
|
+
output?: string | false;
|
|
17
|
+
reads?: string[] | false;
|
|
18
|
+
progress?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ChainClarifyResult {
|
|
22
|
+
confirmed: boolean;
|
|
23
|
+
templates: string[];
|
|
24
|
+
/** User-modified behavior overrides per step (undefined = no changes) */
|
|
25
|
+
behaviorOverrides: (BehaviorOverride | undefined)[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type EditMode = "template" | "output" | "reads";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* TUI component for chain clarification.
|
|
32
|
+
* Factory signature matches ctx.ui.custom: (tui, theme, kb, done) => Component
|
|
33
|
+
*/
|
|
34
|
+
export class ChainClarifyComponent implements Component {
|
|
35
|
+
readonly width = 84;
|
|
36
|
+
|
|
37
|
+
private selectedStep = 0;
|
|
38
|
+
private editingStep: number | null = null;
|
|
39
|
+
private editMode: EditMode = "template";
|
|
40
|
+
private editBuffer: string = "";
|
|
41
|
+
private editCursor: number = 0;
|
|
42
|
+
private editViewportOffset: number = 0;
|
|
43
|
+
|
|
44
|
+
/** Lines visible in full edit mode */
|
|
45
|
+
private readonly EDIT_VIEWPORT_HEIGHT = 12;
|
|
46
|
+
|
|
47
|
+
/** Track user modifications to behaviors (sparse - only stores changes) */
|
|
48
|
+
private behaviorOverrides: Map<number, BehaviorOverride> = new Map();
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
private tui: TUI,
|
|
52
|
+
private theme: Theme,
|
|
53
|
+
private agentConfigs: AgentConfig[],
|
|
54
|
+
private templates: string[],
|
|
55
|
+
private originalTask: string,
|
|
56
|
+
private chainDir: string,
|
|
57
|
+
private resolvedBehaviors: ResolvedStepBehavior[],
|
|
58
|
+
private done: (result: ChainClarifyResult) => void,
|
|
59
|
+
) {}
|
|
60
|
+
|
|
61
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
62
|
+
// Helper methods for rendering
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/** Pad string to specified visible width */
|
|
66
|
+
private pad(s: string, len: number): string {
|
|
67
|
+
const vis = visibleWidth(s);
|
|
68
|
+
return s + " ".repeat(Math.max(0, len - vis));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Create a row with border characters */
|
|
72
|
+
private row(content: string): string {
|
|
73
|
+
const innerW = this.width - 2;
|
|
74
|
+
return this.theme.fg("border", "│") + this.pad(content, innerW) + this.theme.fg("border", "│");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Render centered header line with border */
|
|
78
|
+
private renderHeader(text: string): string {
|
|
79
|
+
const innerW = this.width - 2;
|
|
80
|
+
const padLen = Math.max(0, innerW - visibleWidth(text));
|
|
81
|
+
const padLeft = Math.floor(padLen / 2);
|
|
82
|
+
const padRight = padLen - padLeft;
|
|
83
|
+
return (
|
|
84
|
+
this.theme.fg("border", "╭" + "─".repeat(padLeft)) +
|
|
85
|
+
this.theme.fg("accent", text) +
|
|
86
|
+
this.theme.fg("border", "─".repeat(padRight) + "╮")
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Render centered footer line with border */
|
|
91
|
+
private renderFooter(text: string): string {
|
|
92
|
+
const innerW = this.width - 2;
|
|
93
|
+
const padLen = Math.max(0, innerW - visibleWidth(text));
|
|
94
|
+
const padLeft = Math.floor(padLen / 2);
|
|
95
|
+
const padRight = padLen - padLeft;
|
|
96
|
+
return (
|
|
97
|
+
this.theme.fg("border", "╰" + "─".repeat(padLeft)) +
|
|
98
|
+
this.theme.fg("dim", text) +
|
|
99
|
+
this.theme.fg("border", "─".repeat(padRight) + "╯")
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Exit edit mode and reset state */
|
|
104
|
+
private exitEditMode(): void {
|
|
105
|
+
this.editingStep = null;
|
|
106
|
+
this.editViewportOffset = 0;
|
|
107
|
+
this.tui.requestRender();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
111
|
+
// Full edit mode methods
|
|
112
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/** Word-wrap text to specified width, tracking buffer positions */
|
|
115
|
+
private wrapText(text: string, width: number): { lines: string[]; starts: number[] } {
|
|
116
|
+
const lines: string[] = [];
|
|
117
|
+
const starts: number[] = [];
|
|
118
|
+
|
|
119
|
+
// Guard against invalid width
|
|
120
|
+
if (width <= 0) {
|
|
121
|
+
return { lines: [text], starts: [0] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Handle empty text
|
|
125
|
+
if (text.length === 0) {
|
|
126
|
+
return { lines: [""], starts: [0] };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let pos = 0;
|
|
130
|
+
while (pos < text.length) {
|
|
131
|
+
starts.push(pos);
|
|
132
|
+
|
|
133
|
+
// Take up to `width` characters
|
|
134
|
+
const remaining = text.length - pos;
|
|
135
|
+
const lineLen = Math.min(width, remaining);
|
|
136
|
+
lines.push(text.slice(pos, pos + lineLen));
|
|
137
|
+
pos += lineLen;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Handle cursor at very end when text fills last line exactly
|
|
141
|
+
// Cursor at position text.length needs a place to render
|
|
142
|
+
if (text.length > 0 && text.length % width === 0) {
|
|
143
|
+
starts.push(text.length);
|
|
144
|
+
lines.push(""); // Empty line for cursor to sit on
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { lines, starts };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Convert buffer position to display line/column */
|
|
151
|
+
private getCursorDisplayPos(cursor: number, starts: number[]): { line: number; col: number } {
|
|
152
|
+
for (let i = starts.length - 1; i >= 0; i--) {
|
|
153
|
+
if (cursor >= starts[i]) {
|
|
154
|
+
return { line: i, col: cursor - starts[i] };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { line: 0, col: 0 };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Calculate new viewport offset to keep cursor visible */
|
|
161
|
+
private ensureCursorVisible(cursorLine: number, viewportHeight: number, currentOffset: number): number {
|
|
162
|
+
let offset = currentOffset;
|
|
163
|
+
|
|
164
|
+
// Cursor above viewport - scroll up
|
|
165
|
+
if (cursorLine < offset) {
|
|
166
|
+
offset = cursorLine;
|
|
167
|
+
}
|
|
168
|
+
// Cursor below viewport - scroll down
|
|
169
|
+
else if (cursorLine >= offset + viewportHeight) {
|
|
170
|
+
offset = cursorLine - viewportHeight + 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return Math.max(0, offset);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Render the full-edit takeover view */
|
|
177
|
+
private renderFullEditMode(): string[] {
|
|
178
|
+
const innerW = this.width - 2;
|
|
179
|
+
const textWidth = innerW - 2; // 1 char padding on each side
|
|
180
|
+
const lines: string[] = [];
|
|
181
|
+
|
|
182
|
+
// Word wrap the edit buffer
|
|
183
|
+
const { lines: wrapped, starts } = this.wrapText(this.editBuffer, textWidth);
|
|
184
|
+
|
|
185
|
+
// Find cursor display position
|
|
186
|
+
const cursorPos = this.getCursorDisplayPos(this.editCursor, starts);
|
|
187
|
+
|
|
188
|
+
// Auto-scroll to keep cursor visible
|
|
189
|
+
this.editViewportOffset = this.ensureCursorVisible(
|
|
190
|
+
cursorPos.line,
|
|
191
|
+
this.EDIT_VIEWPORT_HEIGHT,
|
|
192
|
+
this.editViewportOffset,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Header (truncate agent name to prevent overflow)
|
|
196
|
+
const fieldName = this.editMode === "template" ? "task" : this.editMode;
|
|
197
|
+
const rawAgentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
|
|
198
|
+
const maxAgentLen = innerW - 30; // Reserve space for " Editing X (Step N: ) "
|
|
199
|
+
const agentName = rawAgentName.length > maxAgentLen
|
|
200
|
+
? rawAgentName.slice(0, maxAgentLen - 1) + "…"
|
|
201
|
+
: rawAgentName;
|
|
202
|
+
const headerText = ` Editing ${fieldName} (Step ${this.editingStep! + 1}: ${agentName}) `;
|
|
203
|
+
lines.push(this.renderHeader(headerText));
|
|
204
|
+
lines.push(this.row(""));
|
|
205
|
+
|
|
206
|
+
// Render visible lines from viewport
|
|
207
|
+
for (let i = 0; i < this.EDIT_VIEWPORT_HEIGHT; i++) {
|
|
208
|
+
const lineIdx = this.editViewportOffset + i;
|
|
209
|
+
if (lineIdx < wrapped.length) {
|
|
210
|
+
let content = wrapped[lineIdx];
|
|
211
|
+
|
|
212
|
+
// Insert cursor if on this line
|
|
213
|
+
if (lineIdx === cursorPos.line) {
|
|
214
|
+
content = this.renderWithCursor(content, cursorPos.col);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
lines.push(this.row(` ${content}`));
|
|
218
|
+
} else {
|
|
219
|
+
lines.push(this.row(""));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Scroll indicators
|
|
224
|
+
const linesBelow = wrapped.length - this.editViewportOffset - this.EDIT_VIEWPORT_HEIGHT;
|
|
225
|
+
const hasMore = linesBelow > 0;
|
|
226
|
+
const hasLess = this.editViewportOffset > 0;
|
|
227
|
+
let scrollInfo = "";
|
|
228
|
+
if (hasLess) scrollInfo += "↑";
|
|
229
|
+
if (hasMore) scrollInfo += `↓ ${linesBelow}+`;
|
|
230
|
+
|
|
231
|
+
lines.push(this.row(""));
|
|
232
|
+
|
|
233
|
+
// Footer with scroll indicators if applicable
|
|
234
|
+
const footerText = scrollInfo
|
|
235
|
+
? ` [Esc] Done • [Ctrl+C] Discard • ${scrollInfo} `
|
|
236
|
+
: " [Esc] Done • [Ctrl+C] Discard ";
|
|
237
|
+
lines.push(this.renderFooter(footerText));
|
|
238
|
+
|
|
239
|
+
return lines;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
243
|
+
// Behavior helpers
|
|
244
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
/** Get effective behavior for a step (with user overrides applied) */
|
|
247
|
+
private getEffectiveBehavior(stepIndex: number): ResolvedStepBehavior {
|
|
248
|
+
const base = this.resolvedBehaviors[stepIndex]!;
|
|
249
|
+
const override = this.behaviorOverrides.get(stepIndex);
|
|
250
|
+
if (!override) return base;
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
output: override.output !== undefined ? override.output : base.output,
|
|
254
|
+
reads: override.reads !== undefined ? override.reads : base.reads,
|
|
255
|
+
progress: override.progress !== undefined ? override.progress : base.progress,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Update a behavior override for a step */
|
|
260
|
+
private updateBehavior(stepIndex: number, field: keyof BehaviorOverride, value: string | boolean | string[] | false): void {
|
|
261
|
+
const existing = this.behaviorOverrides.get(stepIndex) ?? {};
|
|
262
|
+
this.behaviorOverrides.set(stepIndex, { ...existing, [field]: value });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
handleInput(data: string): void {
|
|
266
|
+
if (this.editingStep !== null) {
|
|
267
|
+
this.handleEditInput(data);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Navigation mode
|
|
272
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
273
|
+
this.done({ confirmed: false, templates: [], behaviorOverrides: [] });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (matchesKey(data, "return")) {
|
|
278
|
+
// Build behavior overrides array
|
|
279
|
+
const overrides: (BehaviorOverride | undefined)[] = [];
|
|
280
|
+
for (let i = 0; i < this.agentConfigs.length; i++) {
|
|
281
|
+
overrides.push(this.behaviorOverrides.get(i));
|
|
282
|
+
}
|
|
283
|
+
this.done({ confirmed: true, templates: this.templates, behaviorOverrides: overrides });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (matchesKey(data, "up")) {
|
|
288
|
+
this.selectedStep = Math.max(0, this.selectedStep - 1);
|
|
289
|
+
this.tui.requestRender();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (matchesKey(data, "down")) {
|
|
294
|
+
const maxStep = Math.max(0, this.agentConfigs.length - 1);
|
|
295
|
+
this.selectedStep = Math.min(maxStep, this.selectedStep + 1);
|
|
296
|
+
this.tui.requestRender();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 'e' to edit template
|
|
301
|
+
if (data === "e") {
|
|
302
|
+
this.enterEditMode("template");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 'o' to edit output
|
|
307
|
+
if (data === "o") {
|
|
308
|
+
this.enterEditMode("output");
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 'r' to edit reads
|
|
313
|
+
if (data === "r") {
|
|
314
|
+
this.enterEditMode("reads");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 'p' to toggle progress
|
|
319
|
+
if (data === "p") {
|
|
320
|
+
const current = this.getEffectiveBehavior(this.selectedStep);
|
|
321
|
+
this.updateBehavior(this.selectedStep, "progress", !current.progress);
|
|
322
|
+
this.tui.requestRender();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private enterEditMode(mode: EditMode): void {
|
|
328
|
+
this.editingStep = this.selectedStep;
|
|
329
|
+
this.editMode = mode;
|
|
330
|
+
this.editViewportOffset = 0; // Reset scroll position
|
|
331
|
+
|
|
332
|
+
if (mode === "template") {
|
|
333
|
+
const template = this.templates[this.selectedStep] ?? "";
|
|
334
|
+
// For template, use first line only (single-line editor)
|
|
335
|
+
this.editBuffer = template.split("\n")[0] ?? "";
|
|
336
|
+
} else if (mode === "output") {
|
|
337
|
+
const behavior = this.getEffectiveBehavior(this.selectedStep);
|
|
338
|
+
this.editBuffer = behavior.output === false ? "" : (behavior.output || "");
|
|
339
|
+
} else if (mode === "reads") {
|
|
340
|
+
const behavior = this.getEffectiveBehavior(this.selectedStep);
|
|
341
|
+
this.editBuffer = behavior.reads === false ? "" : (behavior.reads?.join(", ") || "");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
this.editCursor = 0; // Start at beginning so cursor is visible
|
|
345
|
+
this.tui.requestRender();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private handleEditInput(data: string): void {
|
|
349
|
+
const textWidth = this.width - 4; // Must match render: innerW - 2 = (width - 2) - 2
|
|
350
|
+
const { lines: wrapped, starts } = this.wrapText(this.editBuffer, textWidth);
|
|
351
|
+
const cursorPos = this.getCursorDisplayPos(this.editCursor, starts);
|
|
352
|
+
|
|
353
|
+
// Escape - save and exit
|
|
354
|
+
if (matchesKey(data, "escape")) {
|
|
355
|
+
this.saveEdit();
|
|
356
|
+
this.exitEditMode();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Ctrl+C - discard and exit
|
|
361
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
362
|
+
this.exitEditMode();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Enter - ignored (single-line editing, no newlines)
|
|
367
|
+
if (matchesKey(data, "return")) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Left arrow - move cursor left
|
|
372
|
+
if (matchesKey(data, "left")) {
|
|
373
|
+
if (this.editCursor > 0) this.editCursor--;
|
|
374
|
+
this.tui.requestRender();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Right arrow - move cursor right
|
|
379
|
+
if (matchesKey(data, "right")) {
|
|
380
|
+
if (this.editCursor < this.editBuffer.length) this.editCursor++;
|
|
381
|
+
this.tui.requestRender();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Up arrow - move up one display line
|
|
386
|
+
if (matchesKey(data, "up")) {
|
|
387
|
+
if (cursorPos.line > 0) {
|
|
388
|
+
const targetLine = cursorPos.line - 1;
|
|
389
|
+
const targetCol = Math.min(cursorPos.col, wrapped[targetLine].length);
|
|
390
|
+
this.editCursor = starts[targetLine] + targetCol;
|
|
391
|
+
}
|
|
392
|
+
this.tui.requestRender();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Down arrow - move down one display line
|
|
397
|
+
if (matchesKey(data, "down")) {
|
|
398
|
+
if (cursorPos.line < wrapped.length - 1) {
|
|
399
|
+
const targetLine = cursorPos.line + 1;
|
|
400
|
+
const targetCol = Math.min(cursorPos.col, wrapped[targetLine].length);
|
|
401
|
+
this.editCursor = starts[targetLine] + targetCol;
|
|
402
|
+
}
|
|
403
|
+
this.tui.requestRender();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Page up (Shift+Up or PageUp)
|
|
408
|
+
if (matchesKey(data, "shift+up") || matchesKey(data, "pageup")) {
|
|
409
|
+
const targetLine = Math.max(0, cursorPos.line - this.EDIT_VIEWPORT_HEIGHT);
|
|
410
|
+
const targetCol = Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0);
|
|
411
|
+
this.editCursor = starts[targetLine] + targetCol;
|
|
412
|
+
this.tui.requestRender();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Page down (Shift+Down or PageDown)
|
|
417
|
+
if (matchesKey(data, "shift+down") || matchesKey(data, "pagedown")) {
|
|
418
|
+
const targetLine = Math.min(wrapped.length - 1, cursorPos.line + this.EDIT_VIEWPORT_HEIGHT);
|
|
419
|
+
const targetCol = Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0);
|
|
420
|
+
this.editCursor = starts[targetLine] + targetCol;
|
|
421
|
+
this.tui.requestRender();
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Home - start of current display line
|
|
426
|
+
if (matchesKey(data, "home")) {
|
|
427
|
+
this.editCursor = starts[cursorPos.line];
|
|
428
|
+
this.tui.requestRender();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// End - end of current display line
|
|
433
|
+
if (matchesKey(data, "end")) {
|
|
434
|
+
this.editCursor = starts[cursorPos.line] + wrapped[cursorPos.line].length;
|
|
435
|
+
this.tui.requestRender();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Ctrl+Home - start of text
|
|
440
|
+
if (matchesKey(data, "ctrl+home")) {
|
|
441
|
+
this.editCursor = 0;
|
|
442
|
+
this.tui.requestRender();
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Ctrl+End - end of text
|
|
447
|
+
if (matchesKey(data, "ctrl+end")) {
|
|
448
|
+
this.editCursor = this.editBuffer.length;
|
|
449
|
+
this.tui.requestRender();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Backspace - delete character before cursor
|
|
454
|
+
if (matchesKey(data, "backspace")) {
|
|
455
|
+
if (this.editCursor > 0) {
|
|
456
|
+
this.editBuffer =
|
|
457
|
+
this.editBuffer.slice(0, this.editCursor - 1) +
|
|
458
|
+
this.editBuffer.slice(this.editCursor);
|
|
459
|
+
this.editCursor--;
|
|
460
|
+
}
|
|
461
|
+
this.tui.requestRender();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Delete - delete character at cursor
|
|
466
|
+
if (matchesKey(data, "delete")) {
|
|
467
|
+
if (this.editCursor < this.editBuffer.length) {
|
|
468
|
+
this.editBuffer =
|
|
469
|
+
this.editBuffer.slice(0, this.editCursor) +
|
|
470
|
+
this.editBuffer.slice(this.editCursor + 1);
|
|
471
|
+
}
|
|
472
|
+
this.tui.requestRender();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Printable character - insert at cursor
|
|
477
|
+
if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
478
|
+
this.editBuffer =
|
|
479
|
+
this.editBuffer.slice(0, this.editCursor) +
|
|
480
|
+
data +
|
|
481
|
+
this.editBuffer.slice(this.editCursor);
|
|
482
|
+
this.editCursor++;
|
|
483
|
+
this.tui.requestRender();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private saveEdit(): void {
|
|
489
|
+
const stepIndex = this.editingStep!;
|
|
490
|
+
|
|
491
|
+
if (this.editMode === "template") {
|
|
492
|
+
// For template, preserve other lines if they existed
|
|
493
|
+
const original = this.templates[stepIndex] ?? "";
|
|
494
|
+
const originalLines = original.split("\n");
|
|
495
|
+
originalLines[0] = this.editBuffer;
|
|
496
|
+
this.templates[stepIndex] = originalLines.join("\n");
|
|
497
|
+
} else if (this.editMode === "output") {
|
|
498
|
+
// Empty string or whitespace means disable output
|
|
499
|
+
const trimmed = this.editBuffer.trim();
|
|
500
|
+
this.updateBehavior(stepIndex, "output", trimmed === "" ? false : trimmed);
|
|
501
|
+
} else if (this.editMode === "reads") {
|
|
502
|
+
// Parse comma-separated list, empty means disable reads
|
|
503
|
+
const trimmed = this.editBuffer.trim();
|
|
504
|
+
if (trimmed === "") {
|
|
505
|
+
this.updateBehavior(stepIndex, "reads", false);
|
|
506
|
+
} else {
|
|
507
|
+
const files = trimmed.split(",").map(f => f.trim()).filter(f => f !== "");
|
|
508
|
+
this.updateBehavior(stepIndex, "reads", files.length > 0 ? files : false);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
render(_width: number): string[] {
|
|
514
|
+
if (this.editingStep !== null) {
|
|
515
|
+
return this.renderFullEditMode();
|
|
516
|
+
}
|
|
517
|
+
return this.renderNavigationMode();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** Render navigation mode (step selection, preview) */
|
|
521
|
+
private renderNavigationMode(): string[] {
|
|
522
|
+
const innerW = this.width - 2;
|
|
523
|
+
const th = this.theme;
|
|
524
|
+
const lines: string[] = [];
|
|
525
|
+
|
|
526
|
+
// Header with chain name (truncate if too long)
|
|
527
|
+
const chainLabel = this.agentConfigs.map((c) => c.name).join(" → ");
|
|
528
|
+
const maxHeaderLen = innerW - 4;
|
|
529
|
+
const headerText = ` Chain: ${truncateToWidth(chainLabel, maxHeaderLen - 9)} `;
|
|
530
|
+
lines.push(this.renderHeader(headerText));
|
|
531
|
+
|
|
532
|
+
lines.push(this.row(""));
|
|
533
|
+
|
|
534
|
+
// Original task (truncated) and chain dir
|
|
535
|
+
const taskPreview = truncateToWidth(this.originalTask, innerW - 16);
|
|
536
|
+
lines.push(this.row(` Original Task: ${taskPreview}`));
|
|
537
|
+
const chainDirPreview = truncateToWidth(this.chainDir, innerW - 12);
|
|
538
|
+
lines.push(this.row(` Chain Dir: ${th.fg("dim", chainDirPreview)}`));
|
|
539
|
+
lines.push(this.row(""));
|
|
540
|
+
|
|
541
|
+
// Each step
|
|
542
|
+
for (let i = 0; i < this.agentConfigs.length; i++) {
|
|
543
|
+
const config = this.agentConfigs[i]!;
|
|
544
|
+
const isSelected = i === this.selectedStep;
|
|
545
|
+
const behavior = this.getEffectiveBehavior(i);
|
|
546
|
+
|
|
547
|
+
// Step header (truncate agent name to prevent overflow)
|
|
548
|
+
const color = isSelected ? "accent" : "dim";
|
|
549
|
+
const prefix = isSelected ? "▶ " : " ";
|
|
550
|
+
const stepPrefix = `Step ${i + 1}: `;
|
|
551
|
+
const maxNameLen = innerW - 4 - prefix.length - stepPrefix.length; // 4 for " " prefix and padding
|
|
552
|
+
const agentName = config.name.length > maxNameLen
|
|
553
|
+
? config.name.slice(0, maxNameLen - 1) + "…"
|
|
554
|
+
: config.name;
|
|
555
|
+
const stepLabel = `${stepPrefix}${agentName}`;
|
|
556
|
+
lines.push(
|
|
557
|
+
this.row(` ${th.fg(color, prefix + stepLabel)}`),
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
// Template line (with syntax highlighting for variables)
|
|
561
|
+
const template = (this.templates[i] ?? "").split("\n")[0] ?? "";
|
|
562
|
+
const highlighted = template
|
|
563
|
+
.replace(/\{task\}/g, th.fg("success", "{task}"))
|
|
564
|
+
.replace(/\{previous\}/g, th.fg("warning", "{previous}"))
|
|
565
|
+
.replace(/\{chain_dir\}/g, th.fg("accent", "{chain_dir}"));
|
|
566
|
+
|
|
567
|
+
const templateLabel = th.fg("dim", "task: ");
|
|
568
|
+
lines.push(this.row(` ${templateLabel}${truncateToWidth(highlighted, innerW - 12)}`));
|
|
569
|
+
|
|
570
|
+
// Output line
|
|
571
|
+
const outputValue = behavior.output === false
|
|
572
|
+
? th.fg("dim", "(disabled)")
|
|
573
|
+
: (behavior.output || th.fg("dim", "(none)"));
|
|
574
|
+
const outputLabel = th.fg("dim", "output: ");
|
|
575
|
+
lines.push(this.row(` ${outputLabel}${truncateToWidth(outputValue, innerW - 14)}`));
|
|
576
|
+
|
|
577
|
+
// Reads line
|
|
578
|
+
const readsValue = behavior.reads === false
|
|
579
|
+
? th.fg("dim", "(disabled)")
|
|
580
|
+
: (behavior.reads && behavior.reads.length > 0
|
|
581
|
+
? behavior.reads.join(", ")
|
|
582
|
+
: th.fg("dim", "(none)"));
|
|
583
|
+
const readsLabel = th.fg("dim", "reads: ");
|
|
584
|
+
lines.push(this.row(` ${readsLabel}${truncateToWidth(readsValue, innerW - 13)}`));
|
|
585
|
+
|
|
586
|
+
// Progress line
|
|
587
|
+
const progressValue = behavior.progress ? th.fg("success", "✓ enabled") : th.fg("dim", "✗ disabled");
|
|
588
|
+
const progressLabel = th.fg("dim", "progress: ");
|
|
589
|
+
lines.push(this.row(` ${progressLabel}${progressValue}`));
|
|
590
|
+
|
|
591
|
+
lines.push(this.row(""));
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Footer with keybindings
|
|
595
|
+
const footerText = " [Enter] Run • [Esc] Cancel • [e]dit [o]utput [r]eads [p]rogress ";
|
|
596
|
+
lines.push(this.renderFooter(footerText));
|
|
597
|
+
|
|
598
|
+
return lines;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** Render text with cursor at position (reverse video for visibility) */
|
|
602
|
+
private renderWithCursor(text: string, cursorPos: number): string {
|
|
603
|
+
const before = text.slice(0, cursorPos);
|
|
604
|
+
const cursorChar = text[cursorPos] ?? " ";
|
|
605
|
+
const after = text.slice(cursorPos + 1);
|
|
606
|
+
// Use reverse video (\x1b[7m) for cursor, then disable reverse (\x1b[27m)
|
|
607
|
+
return `${before}\x1b[7m${cursorChar}\x1b[27m${after}`;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
invalidate(): void {}
|
|
611
|
+
dispose(): void {}
|
|
612
|
+
}
|