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/CHANGELOG.md +69 -0
- package/README.md +15 -2
- package/chain-clarify.ts +480 -19
- package/chain-execution.ts +109 -24
- package/execution.ts +72 -38
- package/index.ts +34 -4
- package/package.json +1 -1
- package/render.ts +31 -4
- package/schemas.ts +11 -9
- package/settings.ts +24 -20
- package/types.ts +3 -1
- package/utils.ts +51 -13
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.
|
|
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
|
-
// '
|
|
307
|
-
if (data === "
|
|
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
|
-
|
|
321
|
-
this.
|
|
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
|
-
|
|
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
|
-
//
|
|
571
|
-
const
|
|
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
|
|
575
|
-
lines.push(this.row(` ${
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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 [
|
|
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;
|