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/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
- const headerText = ` Editing ${fieldName} (Step ${this.editingStep! + 1}: ${agentName}) `;
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.handleEditInput(data);
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
- // 'o' to edit output
307
- if (data === "o") {
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
- const current = this.getEffectiveBehavior(this.selectedStep);
321
- this.updateBehavior(this.selectedStep, "progress", !current.progress);
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
- this.updateBehavior(stepIndex, "output", trimmed === "" ? false : trimmed);
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
- return this.renderNavigationMode();
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 navigation mode (step selection, preview) */
521
- private renderNavigationMode(): string[] {
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
- const chainDirPreview = truncateToWidth(this.chainDir, innerW - 12);
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
- // Output line
571
- const outputValue = behavior.output === false
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 outputLabel = th.fg("dim", "output: ");
575
- lines.push(this.row(` ${outputLabel}${truncateToWidth(outputValue, innerW - 14)}`));
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
- 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}`));
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
- const footerText = " [Enter] Run • [Esc] Cancel • [e]dit [o]utput [r]eads [p]rogress ";
596
- lines.push(this.renderFooter(footerText));
1196
+ lines.push(this.renderFooter(this.getFooterText()));
597
1197
 
598
1198
  return lines;
599
1199
  }