pi-subagents 0.3.1 → 0.3.3

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,19 @@ 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
+ /** Model info for display */
15
+ export interface ModelInfo {
16
+ provider: string;
17
+ id: string;
18
+ fullId: string; // "provider/id"
19
+ }
20
+
14
21
  /** Modified behavior overrides from TUI editing */
15
22
  export interface BehaviorOverride {
16
23
  output?: string | false;
17
24
  reads?: string[] | false;
18
25
  progress?: boolean;
26
+ model?: string; // Override agent's default model (format: "provider/id")
19
27
  }
20
28
 
21
29
  export interface ChainClarifyResult {
@@ -25,7 +33,11 @@ export interface ChainClarifyResult {
25
33
  behaviorOverrides: (BehaviorOverride | undefined)[];
26
34
  }
27
35
 
28
- type EditMode = "template" | "output" | "reads";
36
+ type EditMode = "template" | "output" | "reads" | "model" | "thinking";
37
+
38
+ /** Valid thinking levels */
39
+ const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
40
+ type ThinkingLevel = typeof THINKING_LEVELS[number];
29
41
 
30
42
  /**
31
43
  * TUI component for chain clarification.
@@ -47,6 +59,17 @@ export class ChainClarifyComponent implements Component {
47
59
  /** Track user modifications to behaviors (sparse - only stores changes) */
48
60
  private behaviorOverrides: Map<number, BehaviorOverride> = new Map();
49
61
 
62
+ /** Model selector state */
63
+ private modelSearchQuery: string = "";
64
+ private modelSelectedIndex: number = 0;
65
+ private filteredModels: ModelInfo[] = [];
66
+
67
+ /** Max models visible in selector */
68
+ private readonly MODEL_SELECTOR_HEIGHT = 10;
69
+
70
+ /** Thinking level selector state */
71
+ private thinkingSelectedIndex: number = 0;
72
+
50
73
  constructor(
51
74
  private tui: TUI,
52
75
  private theme: Theme,
@@ -55,8 +78,12 @@ export class ChainClarifyComponent implements Component {
55
78
  private originalTask: string,
56
79
  private chainDir: string,
57
80
  private resolvedBehaviors: ResolvedStepBehavior[],
81
+ private availableModels: ModelInfo[],
58
82
  private done: (result: ChainClarifyResult) => void,
59
- ) {}
83
+ ) {
84
+ // Initialize filtered models
85
+ this.filteredModels = [...availableModels];
86
+ }
60
87
 
61
88
  // ─────────────────────────────────────────────────────────────────────────────
62
89
  // Helper methods for rendering
@@ -244,7 +271,7 @@ export class ChainClarifyComponent implements Component {
244
271
  // ─────────────────────────────────────────────────────────────────────────────
245
272
 
246
273
  /** Get effective behavior for a step (with user overrides applied) */
247
- private getEffectiveBehavior(stepIndex: number): ResolvedStepBehavior {
274
+ private getEffectiveBehavior(stepIndex: number): ResolvedStepBehavior & { model?: string } {
248
275
  const base = this.resolvedBehaviors[stepIndex]!;
249
276
  const override = this.behaviorOverrides.get(stepIndex);
250
277
  if (!override) return base;
@@ -253,9 +280,44 @@ export class ChainClarifyComponent implements Component {
253
280
  output: override.output !== undefined ? override.output : base.output,
254
281
  reads: override.reads !== undefined ? override.reads : base.reads,
255
282
  progress: override.progress !== undefined ? override.progress : base.progress,
283
+ model: override.model,
256
284
  };
257
285
  }
258
286
 
287
+ /** Get the effective model for a step (override or agent default) */
288
+ private getEffectiveModel(stepIndex: number): string {
289
+ const override = this.behaviorOverrides.get(stepIndex);
290
+ if (override?.model) return override.model; // Override is already in provider/model format
291
+
292
+ // Use agent's configured model or "default"
293
+ const agentModel = this.agentConfigs[stepIndex]?.model;
294
+ if (!agentModel) return "default";
295
+
296
+ // Resolve model name to full provider/model format
297
+ return this.resolveModelFullId(agentModel);
298
+ }
299
+
300
+ /** Resolve a model name to its full provider/model format */
301
+ private resolveModelFullId(modelName: string): string {
302
+ // If already in provider/model format, return as-is
303
+ if (modelName.includes("/")) return modelName;
304
+
305
+ // Handle thinking level suffixes (e.g., "claude-sonnet-4-5:high")
306
+ // Strip the suffix for lookup, then add it back
307
+ const colonIdx = modelName.lastIndexOf(":");
308
+ const baseModel = colonIdx !== -1 ? modelName.substring(0, colonIdx) : modelName;
309
+ const thinkingSuffix = colonIdx !== -1 ? modelName.substring(colonIdx) : "";
310
+
311
+ // Look up base model in available models to find provider
312
+ const match = this.availableModels.find(m => m.id === baseModel);
313
+ if (match) {
314
+ return thinkingSuffix ? `${match.fullId}${thinkingSuffix}` : match.fullId;
315
+ }
316
+
317
+ // Fallback to just the model name if not found
318
+ return modelName;
319
+ }
320
+
259
321
  /** Update a behavior override for a step */
260
322
  private updateBehavior(stepIndex: number, field: keyof BehaviorOverride, value: string | boolean | string[] | false): void {
261
323
  const existing = this.behaviorOverrides.get(stepIndex) ?? {};
@@ -264,7 +326,13 @@ export class ChainClarifyComponent implements Component {
264
326
 
265
327
  handleInput(data: string): void {
266
328
  if (this.editingStep !== null) {
267
- this.handleEditInput(data);
329
+ if (this.editMode === "model") {
330
+ this.handleModelSelectorInput(data);
331
+ } else if (this.editMode === "thinking") {
332
+ this.handleThinkingSelectorInput(data);
333
+ } else {
334
+ this.handleEditInput(data);
335
+ }
268
336
  return;
269
337
  }
270
338
 
@@ -303,8 +371,8 @@ export class ChainClarifyComponent implements Component {
303
371
  return;
304
372
  }
305
373
 
306
- // 'o' to edit output
307
- if (data === "o") {
374
+ // 'w' to edit writes (output file)
375
+ if (data === "w") {
308
376
  this.enterEditMode("output");
309
377
  return;
310
378
  }
@@ -315,13 +383,30 @@ export class ChainClarifyComponent implements Component {
315
383
  return;
316
384
  }
317
385
 
318
- // 'p' to toggle progress
386
+ // 'p' to toggle progress for ALL steps (chains share a single progress.md)
319
387
  if (data === "p") {
320
- const current = this.getEffectiveBehavior(this.selectedStep);
321
- this.updateBehavior(this.selectedStep, "progress", !current.progress);
388
+ // Check if any step has progress enabled
389
+ const anyEnabled = this.agentConfigs.some((_, i) => this.getEffectiveBehavior(i).progress);
390
+ // Toggle all steps to the opposite state
391
+ const newState = !anyEnabled;
392
+ for (let i = 0; i < this.agentConfigs.length; i++) {
393
+ this.updateBehavior(i, "progress", newState);
394
+ }
322
395
  this.tui.requestRender();
323
396
  return;
324
397
  }
398
+
399
+ // 'm' to select model
400
+ if (data === "m") {
401
+ this.enterModelSelector();
402
+ return;
403
+ }
404
+
405
+ // 't' to select thinking level
406
+ if (data === "t") {
407
+ this.enterThinkingSelector();
408
+ return;
409
+ }
325
410
  }
326
411
 
327
412
  private enterEditMode(mode: EditMode): void {
@@ -345,6 +430,173 @@ export class ChainClarifyComponent implements Component {
345
430
  this.tui.requestRender();
346
431
  }
347
432
 
433
+ /** Enter model selector mode */
434
+ private enterModelSelector(): void {
435
+ this.editingStep = this.selectedStep;
436
+ this.editMode = "model";
437
+ this.modelSearchQuery = "";
438
+ this.modelSelectedIndex = 0;
439
+ this.filteredModels = [...this.availableModels];
440
+
441
+ // Pre-select current model if it exists in the list
442
+ const currentModel = this.getEffectiveModel(this.selectedStep);
443
+ const currentIndex = this.filteredModels.findIndex(m => m.fullId === currentModel || m.id === currentModel);
444
+ if (currentIndex >= 0) {
445
+ this.modelSelectedIndex = currentIndex;
446
+ }
447
+
448
+ this.tui.requestRender();
449
+ }
450
+
451
+ /** Filter models based on search query (fuzzy match) */
452
+ private filterModels(): void {
453
+ const query = this.modelSearchQuery.toLowerCase();
454
+ if (!query) {
455
+ this.filteredModels = [...this.availableModels];
456
+ } else {
457
+ this.filteredModels = this.availableModels.filter(m =>
458
+ m.fullId.toLowerCase().includes(query) ||
459
+ m.id.toLowerCase().includes(query) ||
460
+ m.provider.toLowerCase().includes(query)
461
+ );
462
+ }
463
+ // Clamp selected index
464
+ this.modelSelectedIndex = Math.min(this.modelSelectedIndex, Math.max(0, this.filteredModels.length - 1));
465
+ }
466
+
467
+ /** Handle input in model selector mode */
468
+ private handleModelSelectorInput(data: string): void {
469
+ // Escape or Ctrl+C - cancel and exit
470
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
471
+ this.exitEditMode();
472
+ return;
473
+ }
474
+
475
+ // Enter - select current model
476
+ if (matchesKey(data, "return")) {
477
+ const selected = this.filteredModels[this.modelSelectedIndex];
478
+ if (selected) {
479
+ this.updateBehavior(this.editingStep!, "model", selected.fullId);
480
+ }
481
+ this.exitEditMode();
482
+ return;
483
+ }
484
+
485
+ // Up arrow - move selection up
486
+ if (matchesKey(data, "up")) {
487
+ if (this.filteredModels.length > 0) {
488
+ this.modelSelectedIndex = this.modelSelectedIndex === 0
489
+ ? this.filteredModels.length - 1
490
+ : this.modelSelectedIndex - 1;
491
+ }
492
+ this.tui.requestRender();
493
+ return;
494
+ }
495
+
496
+ // Down arrow - move selection down
497
+ if (matchesKey(data, "down")) {
498
+ if (this.filteredModels.length > 0) {
499
+ this.modelSelectedIndex = this.modelSelectedIndex === this.filteredModels.length - 1
500
+ ? 0
501
+ : this.modelSelectedIndex + 1;
502
+ }
503
+ this.tui.requestRender();
504
+ return;
505
+ }
506
+
507
+ // Backspace - delete last character from search
508
+ if (matchesKey(data, "backspace")) {
509
+ if (this.modelSearchQuery.length > 0) {
510
+ this.modelSearchQuery = this.modelSearchQuery.slice(0, -1);
511
+ this.filterModels();
512
+ }
513
+ this.tui.requestRender();
514
+ return;
515
+ }
516
+
517
+ // Printable character - add to search query
518
+ if (data.length === 1 && data.charCodeAt(0) >= 32) {
519
+ this.modelSearchQuery += data;
520
+ this.filterModels();
521
+ this.tui.requestRender();
522
+ return;
523
+ }
524
+ }
525
+
526
+ /** Enter thinking level selector mode */
527
+ private enterThinkingSelector(): void {
528
+ this.editingStep = this.selectedStep;
529
+ this.editMode = "thinking";
530
+
531
+ // Pre-select current thinking level if set
532
+ const currentModel = this.getEffectiveModel(this.selectedStep);
533
+ const colonIdx = currentModel.lastIndexOf(":");
534
+ if (colonIdx !== -1) {
535
+ const suffix = currentModel.substring(colonIdx + 1);
536
+ const levelIdx = THINKING_LEVELS.indexOf(suffix as ThinkingLevel);
537
+ this.thinkingSelectedIndex = levelIdx >= 0 ? levelIdx : 0;
538
+ } else {
539
+ this.thinkingSelectedIndex = 0; // Default to "off"
540
+ }
541
+
542
+ this.tui.requestRender();
543
+ }
544
+
545
+ /** Handle input in thinking level selector mode */
546
+ private handleThinkingSelectorInput(data: string): void {
547
+ // Escape or Ctrl+C - cancel and exit
548
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
549
+ this.exitEditMode();
550
+ return;
551
+ }
552
+
553
+ // Enter - select current thinking level
554
+ if (matchesKey(data, "return")) {
555
+ const selectedLevel = THINKING_LEVELS[this.thinkingSelectedIndex];
556
+ this.applyThinkingLevel(selectedLevel);
557
+ this.exitEditMode();
558
+ return;
559
+ }
560
+
561
+ // Up arrow - move selection up
562
+ if (matchesKey(data, "up")) {
563
+ this.thinkingSelectedIndex = this.thinkingSelectedIndex === 0
564
+ ? THINKING_LEVELS.length - 1
565
+ : this.thinkingSelectedIndex - 1;
566
+ this.tui.requestRender();
567
+ return;
568
+ }
569
+
570
+ // Down arrow - move selection down
571
+ if (matchesKey(data, "down")) {
572
+ this.thinkingSelectedIndex = this.thinkingSelectedIndex === THINKING_LEVELS.length - 1
573
+ ? 0
574
+ : this.thinkingSelectedIndex + 1;
575
+ this.tui.requestRender();
576
+ return;
577
+ }
578
+ }
579
+
580
+ /** Apply thinking level to the current step's model */
581
+ private applyThinkingLevel(level: ThinkingLevel): void {
582
+ const stepIndex = this.editingStep!;
583
+ const currentModel = this.getEffectiveModel(stepIndex);
584
+
585
+ // Strip any existing thinking level suffix
586
+ const colonIdx = currentModel.lastIndexOf(":");
587
+ let baseModel = currentModel;
588
+ if (colonIdx !== -1) {
589
+ const suffix = currentModel.substring(colonIdx + 1);
590
+ if (THINKING_LEVELS.includes(suffix as ThinkingLevel)) {
591
+ baseModel = currentModel.substring(0, colonIdx);
592
+ }
593
+ }
594
+
595
+ // Apply new thinking level (don't add suffix for "off")
596
+ const newModel = level === "off" ? baseModel : `${baseModel}:${level}`;
597
+ this.updateBehavior(stepIndex, "model", newModel);
598
+ }
599
+
348
600
  private handleEditInput(data: string): void {
349
601
  const textWidth = this.width - 4; // Must match render: innerW - 2 = (width - 2) - 2
350
602
  const { lines: wrapped, starts } = this.wrapText(this.editBuffer, textWidth);
@@ -495,9 +747,19 @@ export class ChainClarifyComponent implements Component {
495
747
  originalLines[0] = this.editBuffer;
496
748
  this.templates[stepIndex] = originalLines.join("\n");
497
749
  } else if (this.editMode === "output") {
750
+ // Capture OLD output before updating (for downstream propagation)
751
+ const oldBehavior = this.getEffectiveBehavior(stepIndex);
752
+ const oldOutput = typeof oldBehavior.output === "string" ? oldBehavior.output : null;
753
+
498
754
  // Empty string or whitespace means disable output
499
755
  const trimmed = this.editBuffer.trim();
500
- this.updateBehavior(stepIndex, "output", trimmed === "" ? false : trimmed);
756
+ const newOutput = trimmed === "" ? false : trimmed;
757
+ this.updateBehavior(stepIndex, "output", newOutput);
758
+
759
+ // Propagate output filename change to downstream steps' reads
760
+ if (oldOutput && typeof newOutput === "string" && oldOutput !== newOutput) {
761
+ this.propagateOutputChange(stepIndex, oldOutput, newOutput);
762
+ }
501
763
  } else if (this.editMode === "reads") {
502
764
  // Parse comma-separated list, empty means disable reads
503
765
  const trimmed = this.editBuffer.trim();
@@ -510,13 +772,181 @@ export class ChainClarifyComponent implements Component {
510
772
  }
511
773
  }
512
774
 
775
+ /**
776
+ * When a step's output filename changes, update downstream steps that read from it.
777
+ * This maintains the chain dependency automatically.
778
+ */
779
+ private propagateOutputChange(changedStepIndex: number, oldOutput: string, newOutput: string): void {
780
+ // Check all downstream steps (steps that come after the changed step)
781
+ for (let i = changedStepIndex + 1; i < this.agentConfigs.length; i++) {
782
+ const behavior = this.getEffectiveBehavior(i);
783
+
784
+ // Skip if reads is disabled or empty
785
+ if (behavior.reads === false || !behavior.reads || behavior.reads.length === 0) {
786
+ continue;
787
+ }
788
+
789
+ // Check if this step reads the old output file
790
+ const readsArray = behavior.reads;
791
+ const oldIndex = readsArray.indexOf(oldOutput);
792
+
793
+ if (oldIndex !== -1) {
794
+ // Replace old filename with new filename in reads
795
+ const newReads = [...readsArray];
796
+ newReads[oldIndex] = newOutput;
797
+ this.updateBehavior(i, "reads", newReads);
798
+ }
799
+ }
800
+ }
801
+
513
802
  render(_width: number): string[] {
514
803
  if (this.editingStep !== null) {
804
+ if (this.editMode === "model") {
805
+ return this.renderModelSelector();
806
+ }
807
+ if (this.editMode === "thinking") {
808
+ return this.renderThinkingSelector();
809
+ }
515
810
  return this.renderFullEditMode();
516
811
  }
517
812
  return this.renderNavigationMode();
518
813
  }
519
814
 
815
+ /** Render the model selector view */
816
+ private renderModelSelector(): string[] {
817
+ const innerW = this.width - 2;
818
+ const th = this.theme;
819
+ const lines: string[] = [];
820
+
821
+ // Header
822
+ const agentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
823
+ const headerText = ` Select Model (Step ${this.editingStep! + 1}: ${agentName}) `;
824
+ lines.push(this.renderHeader(headerText));
825
+ lines.push(this.row(""));
826
+
827
+ // Search input
828
+ const searchPrefix = th.fg("dim", "Search: ");
829
+ const cursor = "\x1b[7m \x1b[27m"; // Reverse video space for cursor
830
+ const searchDisplay = this.modelSearchQuery + cursor;
831
+ lines.push(this.row(` ${searchPrefix}${searchDisplay}`));
832
+ lines.push(this.row(""));
833
+
834
+ // Current model info
835
+ const currentModel = this.getEffectiveModel(this.editingStep!);
836
+ const currentLabel = th.fg("dim", "Current: ");
837
+ lines.push(this.row(` ${currentLabel}${th.fg("warning", currentModel)}`));
838
+ lines.push(this.row(""));
839
+
840
+ // Model list with scroll
841
+ if (this.filteredModels.length === 0) {
842
+ lines.push(this.row(` ${th.fg("dim", "No matching models")}`));
843
+ } else {
844
+ // Calculate visible range (scroll to keep selection visible)
845
+ const maxVisible = this.MODEL_SELECTOR_HEIGHT;
846
+ let startIdx = 0;
847
+
848
+ // Keep selection centered if possible
849
+ if (this.filteredModels.length > maxVisible) {
850
+ startIdx = Math.max(0, this.modelSelectedIndex - Math.floor(maxVisible / 2));
851
+ startIdx = Math.min(startIdx, this.filteredModels.length - maxVisible);
852
+ }
853
+
854
+ const endIdx = Math.min(startIdx + maxVisible, this.filteredModels.length);
855
+
856
+ // Show scroll indicator if needed
857
+ if (startIdx > 0) {
858
+ lines.push(this.row(` ${th.fg("dim", ` ↑ ${startIdx} more`)}`));
859
+ }
860
+
861
+ for (let i = startIdx; i < endIdx; i++) {
862
+ const model = this.filteredModels[i]!;
863
+ const isSelected = i === this.modelSelectedIndex;
864
+ const isCurrent = model.fullId === currentModel || model.id === currentModel;
865
+
866
+ const prefix = isSelected ? th.fg("accent", "→ ") : " ";
867
+ const modelText = isSelected ? th.fg("accent", model.id) : model.id;
868
+ const providerBadge = th.fg("dim", ` [${model.provider}]`);
869
+ const currentBadge = isCurrent ? th.fg("success", " ✓") : "";
870
+
871
+ lines.push(this.row(` ${prefix}${modelText}${providerBadge}${currentBadge}`));
872
+ }
873
+
874
+ // Show scroll indicator if needed
875
+ const remaining = this.filteredModels.length - endIdx;
876
+ if (remaining > 0) {
877
+ lines.push(this.row(` ${th.fg("dim", ` ↓ ${remaining} more`)}`));
878
+ }
879
+ }
880
+
881
+ // Pad to consistent height
882
+ const contentLines = lines.length;
883
+ const targetHeight = 18; // Consistent height
884
+ for (let i = contentLines; i < targetHeight; i++) {
885
+ lines.push(this.row(""));
886
+ }
887
+
888
+ // Footer
889
+ const footerText = " [Enter] Select • [Esc] Cancel • Type to search ";
890
+ lines.push(this.renderFooter(footerText));
891
+
892
+ return lines;
893
+ }
894
+
895
+ /** Render the thinking level selector view */
896
+ private renderThinkingSelector(): string[] {
897
+ const innerW = this.width - 2;
898
+ const th = this.theme;
899
+ const lines: string[] = [];
900
+
901
+ // Header
902
+ const agentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
903
+ const headerText = ` Thinking Level (Step ${this.editingStep! + 1}: ${agentName}) `;
904
+ lines.push(this.renderHeader(headerText));
905
+ lines.push(this.row(""));
906
+
907
+ // Current model info
908
+ const currentModel = this.getEffectiveModel(this.editingStep!);
909
+ const currentLabel = th.fg("dim", "Model: ");
910
+ lines.push(this.row(` ${currentLabel}${th.fg("accent", currentModel)}`));
911
+ lines.push(this.row(""));
912
+
913
+ // Description
914
+ lines.push(this.row(` ${th.fg("dim", "Select thinking level (extended thinking budget):")}`));
915
+ lines.push(this.row(""));
916
+
917
+ // Thinking level options
918
+ const levelDescriptions: Record<ThinkingLevel, string> = {
919
+ "off": "No extended thinking",
920
+ "minimal": "Brief reasoning",
921
+ "low": "Light reasoning",
922
+ "medium": "Moderate reasoning",
923
+ "high": "Deep reasoning",
924
+ "xhigh": "Maximum reasoning (ultrathink)",
925
+ };
926
+
927
+ for (let i = 0; i < THINKING_LEVELS.length; i++) {
928
+ const level = THINKING_LEVELS[i];
929
+ const isSelected = i === this.thinkingSelectedIndex;
930
+ const prefix = isSelected ? th.fg("accent", "→ ") : " ";
931
+ const levelText = isSelected ? th.fg("accent", level) : level;
932
+ const desc = th.fg("dim", ` - ${levelDescriptions[level]}`);
933
+ lines.push(this.row(` ${prefix}${levelText}${desc}`));
934
+ }
935
+
936
+ // Pad to consistent height
937
+ const contentLines = lines.length;
938
+ const targetHeight = 16;
939
+ for (let i = contentLines; i < targetHeight; i++) {
940
+ lines.push(this.row(""));
941
+ }
942
+
943
+ // Footer
944
+ const footerText = " [Enter] Select • [Esc] Cancel • ↑↓ Navigate ";
945
+ lines.push(this.renderFooter(footerText));
946
+
947
+ return lines;
948
+ }
949
+
520
950
  /** Render navigation mode (step selection, preview) */
521
951
  private renderNavigationMode(): string[] {
522
952
  const innerW = this.width - 2;
@@ -536,6 +966,11 @@ export class ChainClarifyComponent implements Component {
536
966
  lines.push(this.row(` Original Task: ${taskPreview}`));
537
967
  const chainDirPreview = truncateToWidth(this.chainDir, innerW - 12);
538
968
  lines.push(this.row(` Chain Dir: ${th.fg("dim", chainDirPreview)}`));
969
+
970
+ // Chain-wide progress setting
971
+ const progressEnabled = this.agentConfigs.some((_, i) => this.getEffectiveBehavior(i).progress);
972
+ const progressValue = progressEnabled ? th.fg("success", "✓ enabled") : th.fg("dim", "✗ disabled");
973
+ lines.push(this.row(` Progress: ${progressValue} ${th.fg("dim", "(press [p] to toggle)")}`));
539
974
  lines.push(this.row(""));
540
975
 
541
976
  // Each step
@@ -567,12 +1002,22 @@ export class ChainClarifyComponent implements Component {
567
1002
  const templateLabel = th.fg("dim", "task: ");
568
1003
  lines.push(this.row(` ${templateLabel}${truncateToWidth(highlighted, innerW - 12)}`));
569
1004
 
570
- // Output line
571
- const outputValue = behavior.output === false
1005
+ // Model line (show override indicator if modified)
1006
+ const effectiveModel = this.getEffectiveModel(i);
1007
+ const override = this.behaviorOverrides.get(i);
1008
+ const isOverridden = override?.model !== undefined;
1009
+ const modelValue = isOverridden
1010
+ ? th.fg("warning", effectiveModel) + th.fg("dim", " ✎")
1011
+ : effectiveModel;
1012
+ const modelLabel = th.fg("dim", "model: ");
1013
+ lines.push(this.row(` ${modelLabel}${truncateToWidth(modelValue, innerW - 13)}`));
1014
+
1015
+ // Writes line (output file) - renamed from "output" for clarity
1016
+ const writesValue = behavior.output === false
572
1017
  ? th.fg("dim", "(disabled)")
573
1018
  : (behavior.output || th.fg("dim", "(none)"));
574
- const outputLabel = th.fg("dim", "output: ");
575
- lines.push(this.row(` ${outputLabel}${truncateToWidth(outputValue, innerW - 14)}`));
1019
+ const writesLabel = th.fg("dim", "writes: ");
1020
+ lines.push(this.row(` ${writesLabel}${truncateToWidth(writesValue, innerW - 14)}`));
576
1021
 
577
1022
  // Reads line
578
1023
  const readsValue = behavior.reads === false
@@ -583,16 +1028,32 @@ export class ChainClarifyComponent implements Component {
583
1028
  const readsLabel = th.fg("dim", "reads: ");
584
1029
  lines.push(this.row(` ${readsLabel}${truncateToWidth(readsValue, innerW - 13)}`));
585
1030
 
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}`));
1031
+ // Progress line - show when chain-wide progress is enabled
1032
+ // First step creates & updates, subsequent steps read & update
1033
+ if (progressEnabled) {
1034
+ const isFirstStep = i === 0;
1035
+ const progressAction = isFirstStep
1036
+ ? th.fg("success", "●") + th.fg("dim", " creates & updates progress.md")
1037
+ : th.fg("accent", "↔") + th.fg("dim", " reads & updates progress.md");
1038
+ const progressLabel = th.fg("dim", "progress: ");
1039
+ lines.push(this.row(` ${progressLabel}${progressAction}`));
1040
+ }
1041
+
1042
+ // Show {previous} indicator for all steps except the last
1043
+ // This shows that this step's text response becomes {previous} for the next step
1044
+ if (i < this.agentConfigs.length - 1) {
1045
+ const nextStepUsePrevious = (this.templates[i + 1] ?? "").includes("{previous}");
1046
+ if (nextStepUsePrevious) {
1047
+ const indicator = th.fg("dim", " ↳ response → ") + th.fg("warning", "{previous}");
1048
+ lines.push(this.row(indicator));
1049
+ }
1050
+ }
590
1051
 
591
1052
  lines.push(this.row(""));
592
1053
  }
593
1054
 
594
1055
  // Footer with keybindings
595
- const footerText = " [Enter] Run • [Esc] Cancel • [e]dit [o]utput [r]eads [p]rogress ";
1056
+ const footerText = " [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hinking [w]rites [r]eads [p]rogress ";
596
1057
  lines.push(this.renderFooter(footerText));
597
1058
 
598
1059
  return lines;