pi-subagents 0.3.2 → 0.4.1
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 +77 -0
- package/README.md +34 -7
- package/chain-clarify.ts +631 -31
- package/chain-execution.ts +93 -16
- package/execution.ts +5 -2
- package/index.ts +143 -21
- package/package.json +2 -1
- package/render.ts +4 -2
- package/schemas.ts +13 -11
- package/settings.ts +24 -20
- package/types.ts +2 -0
package/chain-clarify.ts
CHANGED
|
@@ -11,11 +11,22 @@ import { matchesKey, visibleWidth, truncateToWidth } from "@mariozechner/pi-tui"
|
|
|
11
11
|
import type { AgentConfig } from "./agents.js";
|
|
12
12
|
import type { ResolvedStepBehavior } from "./settings.js";
|
|
13
13
|
|
|
14
|
+
/** Clarify TUI mode */
|
|
15
|
+
export type ClarifyMode = 'single' | 'parallel' | 'chain';
|
|
16
|
+
|
|
17
|
+
/** Model info for display */
|
|
18
|
+
export interface ModelInfo {
|
|
19
|
+
provider: string;
|
|
20
|
+
id: string;
|
|
21
|
+
fullId: string; // "provider/id"
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
/** Modified behavior overrides from TUI editing */
|
|
15
25
|
export interface BehaviorOverride {
|
|
16
26
|
output?: string | false;
|
|
17
27
|
reads?: string[] | false;
|
|
18
28
|
progress?: boolean;
|
|
29
|
+
model?: string; // Override agent's default model (format: "provider/id")
|
|
19
30
|
}
|
|
20
31
|
|
|
21
32
|
export interface ChainClarifyResult {
|
|
@@ -25,7 +36,11 @@ export interface ChainClarifyResult {
|
|
|
25
36
|
behaviorOverrides: (BehaviorOverride | undefined)[];
|
|
26
37
|
}
|
|
27
38
|
|
|
28
|
-
type EditMode = "template" | "output" | "reads";
|
|
39
|
+
type EditMode = "template" | "output" | "reads" | "model" | "thinking";
|
|
40
|
+
|
|
41
|
+
/** Valid thinking levels */
|
|
42
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
43
|
+
type ThinkingLevel = typeof THINKING_LEVELS[number];
|
|
29
44
|
|
|
30
45
|
/**
|
|
31
46
|
* TUI component for chain clarification.
|
|
@@ -47,16 +62,32 @@ export class ChainClarifyComponent implements Component {
|
|
|
47
62
|
/** Track user modifications to behaviors (sparse - only stores changes) */
|
|
48
63
|
private behaviorOverrides: Map<number, BehaviorOverride> = new Map();
|
|
49
64
|
|
|
65
|
+
/** Model selector state */
|
|
66
|
+
private modelSearchQuery: string = "";
|
|
67
|
+
private modelSelectedIndex: number = 0;
|
|
68
|
+
private filteredModels: ModelInfo[] = [];
|
|
69
|
+
|
|
70
|
+
/** Max models visible in selector */
|
|
71
|
+
private readonly MODEL_SELECTOR_HEIGHT = 10;
|
|
72
|
+
|
|
73
|
+
/** Thinking level selector state */
|
|
74
|
+
private thinkingSelectedIndex: number = 0;
|
|
75
|
+
|
|
50
76
|
constructor(
|
|
51
77
|
private tui: TUI,
|
|
52
78
|
private theme: Theme,
|
|
53
79
|
private agentConfigs: AgentConfig[],
|
|
54
80
|
private templates: string[],
|
|
55
81
|
private originalTask: string,
|
|
56
|
-
private chainDir: string,
|
|
82
|
+
private chainDir: string | undefined, // undefined for single/parallel modes
|
|
57
83
|
private resolvedBehaviors: ResolvedStepBehavior[],
|
|
84
|
+
private availableModels: ModelInfo[],
|
|
58
85
|
private done: (result: ChainClarifyResult) => void,
|
|
59
|
-
|
|
86
|
+
private mode: ClarifyMode = 'chain', // Mode: 'single', 'parallel', or 'chain'
|
|
87
|
+
) {
|
|
88
|
+
// Initialize filtered models
|
|
89
|
+
this.filteredModels = [...availableModels];
|
|
90
|
+
}
|
|
60
91
|
|
|
61
92
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
62
93
|
// Helper methods for rendering
|
|
@@ -195,11 +226,17 @@ export class ChainClarifyComponent implements Component {
|
|
|
195
226
|
// Header (truncate agent name to prevent overflow)
|
|
196
227
|
const fieldName = this.editMode === "template" ? "task" : this.editMode;
|
|
197
228
|
const rawAgentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
|
|
198
|
-
const maxAgentLen = innerW - 30; // Reserve space for " Editing X (Step N: ) "
|
|
229
|
+
const maxAgentLen = innerW - 30; // Reserve space for " Editing X (Step/Task N: ) "
|
|
199
230
|
const agentName = rawAgentName.length > maxAgentLen
|
|
200
231
|
? rawAgentName.slice(0, maxAgentLen - 1) + "…"
|
|
201
232
|
: rawAgentName;
|
|
202
|
-
|
|
233
|
+
// Use mode-appropriate terminology
|
|
234
|
+
const stepLabel = this.mode === 'single'
|
|
235
|
+
? agentName
|
|
236
|
+
: this.mode === 'parallel'
|
|
237
|
+
? `Task ${this.editingStep! + 1}: ${agentName}`
|
|
238
|
+
: `Step ${this.editingStep! + 1}: ${agentName}`;
|
|
239
|
+
const headerText = ` Editing ${fieldName} (${stepLabel}) `;
|
|
203
240
|
lines.push(this.renderHeader(headerText));
|
|
204
241
|
lines.push(this.row(""));
|
|
205
242
|
|
|
@@ -244,7 +281,7 @@ export class ChainClarifyComponent implements Component {
|
|
|
244
281
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
245
282
|
|
|
246
283
|
/** Get effective behavior for a step (with user overrides applied) */
|
|
247
|
-
private getEffectiveBehavior(stepIndex: number): ResolvedStepBehavior {
|
|
284
|
+
private getEffectiveBehavior(stepIndex: number): ResolvedStepBehavior & { model?: string } {
|
|
248
285
|
const base = this.resolvedBehaviors[stepIndex]!;
|
|
249
286
|
const override = this.behaviorOverrides.get(stepIndex);
|
|
250
287
|
if (!override) return base;
|
|
@@ -253,9 +290,44 @@ export class ChainClarifyComponent implements Component {
|
|
|
253
290
|
output: override.output !== undefined ? override.output : base.output,
|
|
254
291
|
reads: override.reads !== undefined ? override.reads : base.reads,
|
|
255
292
|
progress: override.progress !== undefined ? override.progress : base.progress,
|
|
293
|
+
model: override.model,
|
|
256
294
|
};
|
|
257
295
|
}
|
|
258
296
|
|
|
297
|
+
/** Get the effective model for a step (override or agent default) */
|
|
298
|
+
private getEffectiveModel(stepIndex: number): string {
|
|
299
|
+
const override = this.behaviorOverrides.get(stepIndex);
|
|
300
|
+
if (override?.model) return override.model; // Override is already in provider/model format
|
|
301
|
+
|
|
302
|
+
// Use agent's configured model or "default"
|
|
303
|
+
const agentModel = this.agentConfigs[stepIndex]?.model;
|
|
304
|
+
if (!agentModel) return "default";
|
|
305
|
+
|
|
306
|
+
// Resolve model name to full provider/model format
|
|
307
|
+
return this.resolveModelFullId(agentModel);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Resolve a model name to its full provider/model format */
|
|
311
|
+
private resolveModelFullId(modelName: string): string {
|
|
312
|
+
// If already in provider/model format, return as-is
|
|
313
|
+
if (modelName.includes("/")) return modelName;
|
|
314
|
+
|
|
315
|
+
// Handle thinking level suffixes (e.g., "claude-sonnet-4-5:high")
|
|
316
|
+
// Strip the suffix for lookup, then add it back
|
|
317
|
+
const colonIdx = modelName.lastIndexOf(":");
|
|
318
|
+
const baseModel = colonIdx !== -1 ? modelName.substring(0, colonIdx) : modelName;
|
|
319
|
+
const thinkingSuffix = colonIdx !== -1 ? modelName.substring(colonIdx) : "";
|
|
320
|
+
|
|
321
|
+
// Look up base model in available models to find provider
|
|
322
|
+
const match = this.availableModels.find(m => m.id === baseModel);
|
|
323
|
+
if (match) {
|
|
324
|
+
return thinkingSuffix ? `${match.fullId}${thinkingSuffix}` : match.fullId;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Fallback to just the model name if not found
|
|
328
|
+
return modelName;
|
|
329
|
+
}
|
|
330
|
+
|
|
259
331
|
/** Update a behavior override for a step */
|
|
260
332
|
private updateBehavior(stepIndex: number, field: keyof BehaviorOverride, value: string | boolean | string[] | false): void {
|
|
261
333
|
const existing = this.behaviorOverrides.get(stepIndex) ?? {};
|
|
@@ -264,7 +336,13 @@ export class ChainClarifyComponent implements Component {
|
|
|
264
336
|
|
|
265
337
|
handleInput(data: string): void {
|
|
266
338
|
if (this.editingStep !== null) {
|
|
267
|
-
this.
|
|
339
|
+
if (this.editMode === "model") {
|
|
340
|
+
this.handleModelSelectorInput(data);
|
|
341
|
+
} else if (this.editMode === "thinking") {
|
|
342
|
+
this.handleThinkingSelectorInput(data);
|
|
343
|
+
} else {
|
|
344
|
+
this.handleEditInput(data);
|
|
345
|
+
}
|
|
268
346
|
return;
|
|
269
347
|
}
|
|
270
348
|
|
|
@@ -297,28 +375,45 @@ export class ChainClarifyComponent implements Component {
|
|
|
297
375
|
return;
|
|
298
376
|
}
|
|
299
377
|
|
|
300
|
-
// 'e' to edit template
|
|
378
|
+
// 'e' to edit template (all modes)
|
|
301
379
|
if (data === "e") {
|
|
302
380
|
this.enterEditMode("template");
|
|
303
381
|
return;
|
|
304
382
|
}
|
|
305
383
|
|
|
306
|
-
// '
|
|
307
|
-
if (data === "
|
|
384
|
+
// 'm' to select model (all modes)
|
|
385
|
+
if (data === "m") {
|
|
386
|
+
this.enterModelSelector();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 't' to select thinking level (all modes)
|
|
391
|
+
if (data === "t") {
|
|
392
|
+
this.enterThinkingSelector();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 'w' to edit writes (single and chain only - not parallel)
|
|
397
|
+
if (data === "w" && this.mode !== 'parallel') {
|
|
308
398
|
this.enterEditMode("output");
|
|
309
399
|
return;
|
|
310
400
|
}
|
|
311
401
|
|
|
312
|
-
// 'r' to edit reads
|
|
313
|
-
if (data === "r") {
|
|
402
|
+
// 'r' to edit reads (chain only)
|
|
403
|
+
if (data === "r" && this.mode === 'chain') {
|
|
314
404
|
this.enterEditMode("reads");
|
|
315
405
|
return;
|
|
316
406
|
}
|
|
317
407
|
|
|
318
|
-
// 'p' to toggle progress
|
|
319
|
-
if (data === "p") {
|
|
320
|
-
|
|
321
|
-
this.
|
|
408
|
+
// 'p' to toggle progress for ALL steps (chain only - chains share a single progress.md)
|
|
409
|
+
if (data === "p" && this.mode === 'chain') {
|
|
410
|
+
// Check if any step has progress enabled
|
|
411
|
+
const anyEnabled = this.agentConfigs.some((_, i) => this.getEffectiveBehavior(i).progress);
|
|
412
|
+
// Toggle all steps to the opposite state
|
|
413
|
+
const newState = !anyEnabled;
|
|
414
|
+
for (let i = 0; i < this.agentConfigs.length; i++) {
|
|
415
|
+
this.updateBehavior(i, "progress", newState);
|
|
416
|
+
}
|
|
322
417
|
this.tui.requestRender();
|
|
323
418
|
return;
|
|
324
419
|
}
|
|
@@ -345,6 +440,173 @@ export class ChainClarifyComponent implements Component {
|
|
|
345
440
|
this.tui.requestRender();
|
|
346
441
|
}
|
|
347
442
|
|
|
443
|
+
/** Enter model selector mode */
|
|
444
|
+
private enterModelSelector(): void {
|
|
445
|
+
this.editingStep = this.selectedStep;
|
|
446
|
+
this.editMode = "model";
|
|
447
|
+
this.modelSearchQuery = "";
|
|
448
|
+
this.modelSelectedIndex = 0;
|
|
449
|
+
this.filteredModels = [...this.availableModels];
|
|
450
|
+
|
|
451
|
+
// Pre-select current model if it exists in the list
|
|
452
|
+
const currentModel = this.getEffectiveModel(this.selectedStep);
|
|
453
|
+
const currentIndex = this.filteredModels.findIndex(m => m.fullId === currentModel || m.id === currentModel);
|
|
454
|
+
if (currentIndex >= 0) {
|
|
455
|
+
this.modelSelectedIndex = currentIndex;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
this.tui.requestRender();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Filter models based on search query (fuzzy match) */
|
|
462
|
+
private filterModels(): void {
|
|
463
|
+
const query = this.modelSearchQuery.toLowerCase();
|
|
464
|
+
if (!query) {
|
|
465
|
+
this.filteredModels = [...this.availableModels];
|
|
466
|
+
} else {
|
|
467
|
+
this.filteredModels = this.availableModels.filter(m =>
|
|
468
|
+
m.fullId.toLowerCase().includes(query) ||
|
|
469
|
+
m.id.toLowerCase().includes(query) ||
|
|
470
|
+
m.provider.toLowerCase().includes(query)
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
// Clamp selected index
|
|
474
|
+
this.modelSelectedIndex = Math.min(this.modelSelectedIndex, Math.max(0, this.filteredModels.length - 1));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Handle input in model selector mode */
|
|
478
|
+
private handleModelSelectorInput(data: string): void {
|
|
479
|
+
// Escape or Ctrl+C - cancel and exit
|
|
480
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
481
|
+
this.exitEditMode();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Enter - select current model
|
|
486
|
+
if (matchesKey(data, "return")) {
|
|
487
|
+
const selected = this.filteredModels[this.modelSelectedIndex];
|
|
488
|
+
if (selected) {
|
|
489
|
+
this.updateBehavior(this.editingStep!, "model", selected.fullId);
|
|
490
|
+
}
|
|
491
|
+
this.exitEditMode();
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Up arrow - move selection up
|
|
496
|
+
if (matchesKey(data, "up")) {
|
|
497
|
+
if (this.filteredModels.length > 0) {
|
|
498
|
+
this.modelSelectedIndex = this.modelSelectedIndex === 0
|
|
499
|
+
? this.filteredModels.length - 1
|
|
500
|
+
: this.modelSelectedIndex - 1;
|
|
501
|
+
}
|
|
502
|
+
this.tui.requestRender();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Down arrow - move selection down
|
|
507
|
+
if (matchesKey(data, "down")) {
|
|
508
|
+
if (this.filteredModels.length > 0) {
|
|
509
|
+
this.modelSelectedIndex = this.modelSelectedIndex === this.filteredModels.length - 1
|
|
510
|
+
? 0
|
|
511
|
+
: this.modelSelectedIndex + 1;
|
|
512
|
+
}
|
|
513
|
+
this.tui.requestRender();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Backspace - delete last character from search
|
|
518
|
+
if (matchesKey(data, "backspace")) {
|
|
519
|
+
if (this.modelSearchQuery.length > 0) {
|
|
520
|
+
this.modelSearchQuery = this.modelSearchQuery.slice(0, -1);
|
|
521
|
+
this.filterModels();
|
|
522
|
+
}
|
|
523
|
+
this.tui.requestRender();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Printable character - add to search query
|
|
528
|
+
if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
529
|
+
this.modelSearchQuery += data;
|
|
530
|
+
this.filterModels();
|
|
531
|
+
this.tui.requestRender();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Enter thinking level selector mode */
|
|
537
|
+
private enterThinkingSelector(): void {
|
|
538
|
+
this.editingStep = this.selectedStep;
|
|
539
|
+
this.editMode = "thinking";
|
|
540
|
+
|
|
541
|
+
// Pre-select current thinking level if set
|
|
542
|
+
const currentModel = this.getEffectiveModel(this.selectedStep);
|
|
543
|
+
const colonIdx = currentModel.lastIndexOf(":");
|
|
544
|
+
if (colonIdx !== -1) {
|
|
545
|
+
const suffix = currentModel.substring(colonIdx + 1);
|
|
546
|
+
const levelIdx = THINKING_LEVELS.indexOf(suffix as ThinkingLevel);
|
|
547
|
+
this.thinkingSelectedIndex = levelIdx >= 0 ? levelIdx : 0;
|
|
548
|
+
} else {
|
|
549
|
+
this.thinkingSelectedIndex = 0; // Default to "off"
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
this.tui.requestRender();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Handle input in thinking level selector mode */
|
|
556
|
+
private handleThinkingSelectorInput(data: string): void {
|
|
557
|
+
// Escape or Ctrl+C - cancel and exit
|
|
558
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
559
|
+
this.exitEditMode();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Enter - select current thinking level
|
|
564
|
+
if (matchesKey(data, "return")) {
|
|
565
|
+
const selectedLevel = THINKING_LEVELS[this.thinkingSelectedIndex];
|
|
566
|
+
this.applyThinkingLevel(selectedLevel);
|
|
567
|
+
this.exitEditMode();
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Up arrow - move selection up
|
|
572
|
+
if (matchesKey(data, "up")) {
|
|
573
|
+
this.thinkingSelectedIndex = this.thinkingSelectedIndex === 0
|
|
574
|
+
? THINKING_LEVELS.length - 1
|
|
575
|
+
: this.thinkingSelectedIndex - 1;
|
|
576
|
+
this.tui.requestRender();
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Down arrow - move selection down
|
|
581
|
+
if (matchesKey(data, "down")) {
|
|
582
|
+
this.thinkingSelectedIndex = this.thinkingSelectedIndex === THINKING_LEVELS.length - 1
|
|
583
|
+
? 0
|
|
584
|
+
: this.thinkingSelectedIndex + 1;
|
|
585
|
+
this.tui.requestRender();
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** Apply thinking level to the current step's model */
|
|
591
|
+
private applyThinkingLevel(level: ThinkingLevel): void {
|
|
592
|
+
const stepIndex = this.editingStep!;
|
|
593
|
+
const currentModel = this.getEffectiveModel(stepIndex);
|
|
594
|
+
|
|
595
|
+
// Strip any existing thinking level suffix
|
|
596
|
+
const colonIdx = currentModel.lastIndexOf(":");
|
|
597
|
+
let baseModel = currentModel;
|
|
598
|
+
if (colonIdx !== -1) {
|
|
599
|
+
const suffix = currentModel.substring(colonIdx + 1);
|
|
600
|
+
if (THINKING_LEVELS.includes(suffix as ThinkingLevel)) {
|
|
601
|
+
baseModel = currentModel.substring(0, colonIdx);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Apply new thinking level (don't add suffix for "off")
|
|
606
|
+
const newModel = level === "off" ? baseModel : `${baseModel}:${level}`;
|
|
607
|
+
this.updateBehavior(stepIndex, "model", newModel);
|
|
608
|
+
}
|
|
609
|
+
|
|
348
610
|
private handleEditInput(data: string): void {
|
|
349
611
|
const textWidth = this.width - 4; // Must match render: innerW - 2 = (width - 2) - 2
|
|
350
612
|
const { lines: wrapped, starts } = this.wrapText(this.editBuffer, textWidth);
|
|
@@ -495,9 +757,19 @@ export class ChainClarifyComponent implements Component {
|
|
|
495
757
|
originalLines[0] = this.editBuffer;
|
|
496
758
|
this.templates[stepIndex] = originalLines.join("\n");
|
|
497
759
|
} else if (this.editMode === "output") {
|
|
760
|
+
// Capture OLD output before updating (for downstream propagation)
|
|
761
|
+
const oldBehavior = this.getEffectiveBehavior(stepIndex);
|
|
762
|
+
const oldOutput = typeof oldBehavior.output === "string" ? oldBehavior.output : null;
|
|
763
|
+
|
|
498
764
|
// Empty string or whitespace means disable output
|
|
499
765
|
const trimmed = this.editBuffer.trim();
|
|
500
|
-
|
|
766
|
+
const newOutput = trimmed === "" ? false : trimmed;
|
|
767
|
+
this.updateBehavior(stepIndex, "output", newOutput);
|
|
768
|
+
|
|
769
|
+
// Propagate output filename change to downstream steps' reads
|
|
770
|
+
if (oldOutput && typeof newOutput === "string" && oldOutput !== newOutput) {
|
|
771
|
+
this.propagateOutputChange(stepIndex, oldOutput, newOutput);
|
|
772
|
+
}
|
|
501
773
|
} else if (this.editMode === "reads") {
|
|
502
774
|
// Parse comma-separated list, empty means disable reads
|
|
503
775
|
const trimmed = this.editBuffer.trim();
|
|
@@ -510,15 +782,312 @@ export class ChainClarifyComponent implements Component {
|
|
|
510
782
|
}
|
|
511
783
|
}
|
|
512
784
|
|
|
785
|
+
/**
|
|
786
|
+
* When a step's output filename changes, update downstream steps that read from it.
|
|
787
|
+
* This maintains the chain dependency automatically.
|
|
788
|
+
*/
|
|
789
|
+
private propagateOutputChange(changedStepIndex: number, oldOutput: string, newOutput: string): void {
|
|
790
|
+
// Check all downstream steps (steps that come after the changed step)
|
|
791
|
+
for (let i = changedStepIndex + 1; i < this.agentConfigs.length; i++) {
|
|
792
|
+
const behavior = this.getEffectiveBehavior(i);
|
|
793
|
+
|
|
794
|
+
// Skip if reads is disabled or empty
|
|
795
|
+
if (behavior.reads === false || !behavior.reads || behavior.reads.length === 0) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Check if this step reads the old output file
|
|
800
|
+
const readsArray = behavior.reads;
|
|
801
|
+
const oldIndex = readsArray.indexOf(oldOutput);
|
|
802
|
+
|
|
803
|
+
if (oldIndex !== -1) {
|
|
804
|
+
// Replace old filename with new filename in reads
|
|
805
|
+
const newReads = [...readsArray];
|
|
806
|
+
newReads[oldIndex] = newOutput;
|
|
807
|
+
this.updateBehavior(i, "reads", newReads);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
513
812
|
render(_width: number): string[] {
|
|
514
813
|
if (this.editingStep !== null) {
|
|
814
|
+
if (this.editMode === "model") {
|
|
815
|
+
return this.renderModelSelector();
|
|
816
|
+
}
|
|
817
|
+
if (this.editMode === "thinking") {
|
|
818
|
+
return this.renderThinkingSelector();
|
|
819
|
+
}
|
|
515
820
|
return this.renderFullEditMode();
|
|
516
821
|
}
|
|
517
|
-
|
|
822
|
+
// Mode-based navigation rendering
|
|
823
|
+
switch (this.mode) {
|
|
824
|
+
case 'single': return this.renderSingleMode();
|
|
825
|
+
case 'parallel': return this.renderParallelMode();
|
|
826
|
+
case 'chain': return this.renderChainMode();
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/** Render the model selector view */
|
|
831
|
+
private renderModelSelector(): string[] {
|
|
832
|
+
const innerW = this.width - 2;
|
|
833
|
+
const th = this.theme;
|
|
834
|
+
const lines: string[] = [];
|
|
835
|
+
|
|
836
|
+
// Header (mode-aware terminology)
|
|
837
|
+
const agentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
|
|
838
|
+
const stepLabel = this.mode === 'single'
|
|
839
|
+
? agentName
|
|
840
|
+
: this.mode === 'parallel'
|
|
841
|
+
? `Task ${this.editingStep! + 1}: ${agentName}`
|
|
842
|
+
: `Step ${this.editingStep! + 1}: ${agentName}`;
|
|
843
|
+
const headerText = ` Select Model (${stepLabel}) `;
|
|
844
|
+
lines.push(this.renderHeader(headerText));
|
|
845
|
+
lines.push(this.row(""));
|
|
846
|
+
|
|
847
|
+
// Search input
|
|
848
|
+
const searchPrefix = th.fg("dim", "Search: ");
|
|
849
|
+
const cursor = "\x1b[7m \x1b[27m"; // Reverse video space for cursor
|
|
850
|
+
const searchDisplay = this.modelSearchQuery + cursor;
|
|
851
|
+
lines.push(this.row(` ${searchPrefix}${searchDisplay}`));
|
|
852
|
+
lines.push(this.row(""));
|
|
853
|
+
|
|
854
|
+
// Current model info
|
|
855
|
+
const currentModel = this.getEffectiveModel(this.editingStep!);
|
|
856
|
+
const currentLabel = th.fg("dim", "Current: ");
|
|
857
|
+
lines.push(this.row(` ${currentLabel}${th.fg("warning", currentModel)}`));
|
|
858
|
+
lines.push(this.row(""));
|
|
859
|
+
|
|
860
|
+
// Model list with scroll
|
|
861
|
+
if (this.filteredModels.length === 0) {
|
|
862
|
+
lines.push(this.row(` ${th.fg("dim", "No matching models")}`));
|
|
863
|
+
} else {
|
|
864
|
+
// Calculate visible range (scroll to keep selection visible)
|
|
865
|
+
const maxVisible = this.MODEL_SELECTOR_HEIGHT;
|
|
866
|
+
let startIdx = 0;
|
|
867
|
+
|
|
868
|
+
// Keep selection centered if possible
|
|
869
|
+
if (this.filteredModels.length > maxVisible) {
|
|
870
|
+
startIdx = Math.max(0, this.modelSelectedIndex - Math.floor(maxVisible / 2));
|
|
871
|
+
startIdx = Math.min(startIdx, this.filteredModels.length - maxVisible);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const endIdx = Math.min(startIdx + maxVisible, this.filteredModels.length);
|
|
875
|
+
|
|
876
|
+
// Show scroll indicator if needed
|
|
877
|
+
if (startIdx > 0) {
|
|
878
|
+
lines.push(this.row(` ${th.fg("dim", ` ↑ ${startIdx} more`)}`));
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
for (let i = startIdx; i < endIdx; i++) {
|
|
882
|
+
const model = this.filteredModels[i]!;
|
|
883
|
+
const isSelected = i === this.modelSelectedIndex;
|
|
884
|
+
const isCurrent = model.fullId === currentModel || model.id === currentModel;
|
|
885
|
+
|
|
886
|
+
const prefix = isSelected ? th.fg("accent", "→ ") : " ";
|
|
887
|
+
const modelText = isSelected ? th.fg("accent", model.id) : model.id;
|
|
888
|
+
const providerBadge = th.fg("dim", ` [${model.provider}]`);
|
|
889
|
+
const currentBadge = isCurrent ? th.fg("success", " ✓") : "";
|
|
890
|
+
|
|
891
|
+
lines.push(this.row(` ${prefix}${modelText}${providerBadge}${currentBadge}`));
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Show scroll indicator if needed
|
|
895
|
+
const remaining = this.filteredModels.length - endIdx;
|
|
896
|
+
if (remaining > 0) {
|
|
897
|
+
lines.push(this.row(` ${th.fg("dim", ` ↓ ${remaining} more`)}`));
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Pad to consistent height
|
|
902
|
+
const contentLines = lines.length;
|
|
903
|
+
const targetHeight = 18; // Consistent height
|
|
904
|
+
for (let i = contentLines; i < targetHeight; i++) {
|
|
905
|
+
lines.push(this.row(""));
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Footer
|
|
909
|
+
const footerText = " [Enter] Select • [Esc] Cancel • Type to search ";
|
|
910
|
+
lines.push(this.renderFooter(footerText));
|
|
911
|
+
|
|
912
|
+
return lines;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/** Render the thinking level selector view */
|
|
916
|
+
private renderThinkingSelector(): string[] {
|
|
917
|
+
const innerW = this.width - 2;
|
|
918
|
+
const th = this.theme;
|
|
919
|
+
const lines: string[] = [];
|
|
920
|
+
|
|
921
|
+
// Header (mode-aware terminology)
|
|
922
|
+
const agentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
|
|
923
|
+
const stepLabel = this.mode === 'single'
|
|
924
|
+
? agentName
|
|
925
|
+
: this.mode === 'parallel'
|
|
926
|
+
? `Task ${this.editingStep! + 1}: ${agentName}`
|
|
927
|
+
: `Step ${this.editingStep! + 1}: ${agentName}`;
|
|
928
|
+
const headerText = ` Thinking Level (${stepLabel}) `;
|
|
929
|
+
lines.push(this.renderHeader(headerText));
|
|
930
|
+
lines.push(this.row(""));
|
|
931
|
+
|
|
932
|
+
// Current model info
|
|
933
|
+
const currentModel = this.getEffectiveModel(this.editingStep!);
|
|
934
|
+
const currentLabel = th.fg("dim", "Model: ");
|
|
935
|
+
lines.push(this.row(` ${currentLabel}${th.fg("accent", currentModel)}`));
|
|
936
|
+
lines.push(this.row(""));
|
|
937
|
+
|
|
938
|
+
// Description
|
|
939
|
+
lines.push(this.row(` ${th.fg("dim", "Select thinking level (extended thinking budget):")}`));
|
|
940
|
+
lines.push(this.row(""));
|
|
941
|
+
|
|
942
|
+
// Thinking level options
|
|
943
|
+
const levelDescriptions: Record<ThinkingLevel, string> = {
|
|
944
|
+
"off": "No extended thinking",
|
|
945
|
+
"minimal": "Brief reasoning",
|
|
946
|
+
"low": "Light reasoning",
|
|
947
|
+
"medium": "Moderate reasoning",
|
|
948
|
+
"high": "Deep reasoning",
|
|
949
|
+
"xhigh": "Maximum reasoning (ultrathink)",
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
for (let i = 0; i < THINKING_LEVELS.length; i++) {
|
|
953
|
+
const level = THINKING_LEVELS[i];
|
|
954
|
+
const isSelected = i === this.thinkingSelectedIndex;
|
|
955
|
+
const prefix = isSelected ? th.fg("accent", "→ ") : " ";
|
|
956
|
+
const levelText = isSelected ? th.fg("accent", level) : level;
|
|
957
|
+
const desc = th.fg("dim", ` - ${levelDescriptions[level]}`);
|
|
958
|
+
lines.push(this.row(` ${prefix}${levelText}${desc}`));
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Pad to consistent height
|
|
962
|
+
const contentLines = lines.length;
|
|
963
|
+
const targetHeight = 16;
|
|
964
|
+
for (let i = contentLines; i < targetHeight; i++) {
|
|
965
|
+
lines.push(this.row(""));
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Footer
|
|
969
|
+
const footerText = " [Enter] Select • [Esc] Cancel • ↑↓ Navigate ";
|
|
970
|
+
lines.push(this.renderFooter(footerText));
|
|
971
|
+
|
|
972
|
+
return lines;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** Get footer text based on mode */
|
|
976
|
+
private getFooterText(): string {
|
|
977
|
+
switch (this.mode) {
|
|
978
|
+
case 'single':
|
|
979
|
+
return ' [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hinking [w]rites ';
|
|
980
|
+
case 'parallel':
|
|
981
|
+
return ' [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hinking • ↑↓ Navigate ';
|
|
982
|
+
case 'chain':
|
|
983
|
+
return ' [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hinking [w]rites [r]eads [p]rogress ';
|
|
984
|
+
}
|
|
518
985
|
}
|
|
519
986
|
|
|
520
|
-
/** Render
|
|
521
|
-
private
|
|
987
|
+
/** Render single agent mode (simplified view) */
|
|
988
|
+
private renderSingleMode(): string[] {
|
|
989
|
+
const innerW = this.width - 2;
|
|
990
|
+
const th = this.theme;
|
|
991
|
+
const lines: string[] = [];
|
|
992
|
+
|
|
993
|
+
// Header with agent name
|
|
994
|
+
const agentName = this.agentConfigs[0]?.name ?? "unknown";
|
|
995
|
+
const maxHeaderLen = innerW - 4;
|
|
996
|
+
const headerText = ` Agent: ${truncateToWidth(agentName, maxHeaderLen - 9)} `;
|
|
997
|
+
lines.push(this.renderHeader(headerText));
|
|
998
|
+
lines.push(this.row(""));
|
|
999
|
+
|
|
1000
|
+
// Single step - always index 0, always selected
|
|
1001
|
+
const config = this.agentConfigs[0]!;
|
|
1002
|
+
const behavior = this.getEffectiveBehavior(0);
|
|
1003
|
+
|
|
1004
|
+
// Agent name with selection indicator
|
|
1005
|
+
const stepLabel = config.name;
|
|
1006
|
+
lines.push(this.row(` ${th.fg("accent", "▶ " + stepLabel)}`));
|
|
1007
|
+
|
|
1008
|
+
// Task line
|
|
1009
|
+
const template = (this.templates[0] ?? "").split("\n")[0] ?? "";
|
|
1010
|
+
const taskLabel = th.fg("dim", "task: ");
|
|
1011
|
+
lines.push(this.row(` ${taskLabel}${truncateToWidth(template, innerW - 12)}`));
|
|
1012
|
+
|
|
1013
|
+
// Model line
|
|
1014
|
+
const effectiveModel = this.getEffectiveModel(0);
|
|
1015
|
+
const override = this.behaviorOverrides.get(0);
|
|
1016
|
+
const isOverridden = override?.model !== undefined;
|
|
1017
|
+
const modelValue = isOverridden
|
|
1018
|
+
? th.fg("warning", effectiveModel) + th.fg("dim", " ✎")
|
|
1019
|
+
: effectiveModel;
|
|
1020
|
+
const modelLabel = th.fg("dim", "model: ");
|
|
1021
|
+
lines.push(this.row(` ${modelLabel}${truncateToWidth(modelValue, innerW - 13)}`));
|
|
1022
|
+
|
|
1023
|
+
// Writes line (output file)
|
|
1024
|
+
const writesValue = behavior.output === false
|
|
1025
|
+
? th.fg("dim", "(disabled)")
|
|
1026
|
+
: (behavior.output || th.fg("dim", "(none)"));
|
|
1027
|
+
const writesLabel = th.fg("dim", "writes: ");
|
|
1028
|
+
lines.push(this.row(` ${writesLabel}${truncateToWidth(writesValue, innerW - 14)}`));
|
|
1029
|
+
|
|
1030
|
+
lines.push(this.row(""));
|
|
1031
|
+
|
|
1032
|
+
// Footer
|
|
1033
|
+
lines.push(this.renderFooter(this.getFooterText()));
|
|
1034
|
+
|
|
1035
|
+
return lines;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/** Render parallel mode (multi-task view without chain features) */
|
|
1039
|
+
private renderParallelMode(): string[] {
|
|
1040
|
+
const innerW = this.width - 2;
|
|
1041
|
+
const th = this.theme;
|
|
1042
|
+
const lines: string[] = [];
|
|
1043
|
+
|
|
1044
|
+
// Header with task count
|
|
1045
|
+
const headerText = ` Parallel Tasks (${this.agentConfigs.length}) `;
|
|
1046
|
+
lines.push(this.renderHeader(headerText));
|
|
1047
|
+
lines.push(this.row(""));
|
|
1048
|
+
|
|
1049
|
+
// Each task
|
|
1050
|
+
for (let i = 0; i < this.agentConfigs.length; i++) {
|
|
1051
|
+
const config = this.agentConfigs[i]!;
|
|
1052
|
+
const isSelected = i === this.selectedStep;
|
|
1053
|
+
|
|
1054
|
+
// Task header (truncate agent name to prevent overflow)
|
|
1055
|
+
const color = isSelected ? "accent" : "dim";
|
|
1056
|
+
const prefix = isSelected ? "▶ " : " ";
|
|
1057
|
+
const taskPrefix = `Task ${i + 1}: `;
|
|
1058
|
+
const maxNameLen = innerW - 4 - prefix.length - taskPrefix.length;
|
|
1059
|
+
const agentName = config.name.length > maxNameLen
|
|
1060
|
+
? config.name.slice(0, maxNameLen - 1) + "…"
|
|
1061
|
+
: config.name;
|
|
1062
|
+
const taskLabel = `${taskPrefix}${agentName}`;
|
|
1063
|
+
lines.push(this.row(` ${th.fg(color, prefix + taskLabel)}`));
|
|
1064
|
+
|
|
1065
|
+
// Task line
|
|
1066
|
+
const template = (this.templates[i] ?? "").split("\n")[0] ?? "";
|
|
1067
|
+
const taskTextLabel = th.fg("dim", "task: ");
|
|
1068
|
+
lines.push(this.row(` ${taskTextLabel}${truncateToWidth(template, innerW - 12)}`));
|
|
1069
|
+
|
|
1070
|
+
// Model line
|
|
1071
|
+
const effectiveModel = this.getEffectiveModel(i);
|
|
1072
|
+
const override = this.behaviorOverrides.get(i);
|
|
1073
|
+
const isOverridden = override?.model !== undefined;
|
|
1074
|
+
const modelValue = isOverridden
|
|
1075
|
+
? th.fg("warning", effectiveModel) + th.fg("dim", " ✎")
|
|
1076
|
+
: effectiveModel;
|
|
1077
|
+
const modelLabel = th.fg("dim", "model: ");
|
|
1078
|
+
lines.push(this.row(` ${modelLabel}${truncateToWidth(modelValue, innerW - 13)}`));
|
|
1079
|
+
|
|
1080
|
+
lines.push(this.row(""));
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Footer
|
|
1084
|
+
lines.push(this.renderFooter(this.getFooterText()));
|
|
1085
|
+
|
|
1086
|
+
return lines;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/** Render chain mode (step selection, preview) */
|
|
1090
|
+
private renderChainMode(): string[] {
|
|
522
1091
|
const innerW = this.width - 2;
|
|
523
1092
|
const th = this.theme;
|
|
524
1093
|
const lines: string[] = [];
|
|
@@ -534,8 +1103,14 @@ export class ChainClarifyComponent implements Component {
|
|
|
534
1103
|
// Original task (truncated) and chain dir
|
|
535
1104
|
const taskPreview = truncateToWidth(this.originalTask, innerW - 16);
|
|
536
1105
|
lines.push(this.row(` Original Task: ${taskPreview}`));
|
|
537
|
-
|
|
1106
|
+
// chainDir is guaranteed to be defined in chain mode
|
|
1107
|
+
const chainDirPreview = truncateToWidth(this.chainDir ?? "", innerW - 12);
|
|
538
1108
|
lines.push(this.row(` Chain Dir: ${th.fg("dim", chainDirPreview)}`));
|
|
1109
|
+
|
|
1110
|
+
// Chain-wide progress setting
|
|
1111
|
+
const progressEnabled = this.agentConfigs.some((_, i) => this.getEffectiveBehavior(i).progress);
|
|
1112
|
+
const progressValue = progressEnabled ? th.fg("success", "✓ enabled") : th.fg("dim", "✗ disabled");
|
|
1113
|
+
lines.push(this.row(` Progress: ${progressValue} ${th.fg("dim", "(press [p] to toggle)")}`));
|
|
539
1114
|
lines.push(this.row(""));
|
|
540
1115
|
|
|
541
1116
|
// Each step
|
|
@@ -567,12 +1142,22 @@ export class ChainClarifyComponent implements Component {
|
|
|
567
1142
|
const templateLabel = th.fg("dim", "task: ");
|
|
568
1143
|
lines.push(this.row(` ${templateLabel}${truncateToWidth(highlighted, innerW - 12)}`));
|
|
569
1144
|
|
|
570
|
-
//
|
|
571
|
-
const
|
|
1145
|
+
// Model line (show override indicator if modified)
|
|
1146
|
+
const effectiveModel = this.getEffectiveModel(i);
|
|
1147
|
+
const override = this.behaviorOverrides.get(i);
|
|
1148
|
+
const isOverridden = override?.model !== undefined;
|
|
1149
|
+
const modelValue = isOverridden
|
|
1150
|
+
? th.fg("warning", effectiveModel) + th.fg("dim", " ✎")
|
|
1151
|
+
: effectiveModel;
|
|
1152
|
+
const modelLabel = th.fg("dim", "model: ");
|
|
1153
|
+
lines.push(this.row(` ${modelLabel}${truncateToWidth(modelValue, innerW - 13)}`));
|
|
1154
|
+
|
|
1155
|
+
// Writes line (output file) - renamed from "output" for clarity
|
|
1156
|
+
const writesValue = behavior.output === false
|
|
572
1157
|
? th.fg("dim", "(disabled)")
|
|
573
1158
|
: (behavior.output || th.fg("dim", "(none)"));
|
|
574
|
-
const
|
|
575
|
-
lines.push(this.row(` ${
|
|
1159
|
+
const writesLabel = th.fg("dim", "writes: ");
|
|
1160
|
+
lines.push(this.row(` ${writesLabel}${truncateToWidth(writesValue, innerW - 14)}`));
|
|
576
1161
|
|
|
577
1162
|
// Reads line
|
|
578
1163
|
const readsValue = behavior.reads === false
|
|
@@ -583,17 +1168,32 @@ export class ChainClarifyComponent implements Component {
|
|
|
583
1168
|
const readsLabel = th.fg("dim", "reads: ");
|
|
584
1169
|
lines.push(this.row(` ${readsLabel}${truncateToWidth(readsValue, innerW - 13)}`));
|
|
585
1170
|
|
|
586
|
-
// Progress line
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
1171
|
+
// Progress line - show when chain-wide progress is enabled
|
|
1172
|
+
// First step creates & updates, subsequent steps read & update
|
|
1173
|
+
if (progressEnabled) {
|
|
1174
|
+
const isFirstStep = i === 0;
|
|
1175
|
+
const progressAction = isFirstStep
|
|
1176
|
+
? th.fg("success", "●") + th.fg("dim", " creates & updates progress.md")
|
|
1177
|
+
: th.fg("accent", "↔") + th.fg("dim", " reads & updates progress.md");
|
|
1178
|
+
const progressLabel = th.fg("dim", "progress: ");
|
|
1179
|
+
lines.push(this.row(` ${progressLabel}${progressAction}`));
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Show {previous} indicator for all steps except the last
|
|
1183
|
+
// This shows that this step's text response becomes {previous} for the next step
|
|
1184
|
+
if (i < this.agentConfigs.length - 1) {
|
|
1185
|
+
const nextStepUsePrevious = (this.templates[i + 1] ?? "").includes("{previous}");
|
|
1186
|
+
if (nextStepUsePrevious) {
|
|
1187
|
+
const indicator = th.fg("dim", " ↳ response → ") + th.fg("warning", "{previous}");
|
|
1188
|
+
lines.push(this.row(indicator));
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
590
1191
|
|
|
591
1192
|
lines.push(this.row(""));
|
|
592
1193
|
}
|
|
593
1194
|
|
|
594
1195
|
// Footer with keybindings
|
|
595
|
-
|
|
596
|
-
lines.push(this.renderFooter(footerText));
|
|
1196
|
+
lines.push(this.renderFooter(this.getFooterText()));
|
|
597
1197
|
|
|
598
1198
|
return lines;
|
|
599
1199
|
}
|