@leing2021/super-pi 0.22.1 → 0.23.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.
Files changed (117) hide show
  1. package/README.md +66 -293
  2. package/extensions/ce-core/index.ts +276 -48
  3. package/extensions/ce-core/tools/parallel-subagent.ts +3 -3
  4. package/extensions/ce-core/tools/subagent.ts +2 -2
  5. package/package.json +4 -20
  6. package/rules/common/naming.md +154 -0
  7. package/skills/01-brainstorm/SKILL.md +42 -79
  8. package/skills/01-brainstorm/references/ce-brainstorm-mode.md +47 -0
  9. package/skills/02-plan/SKILL.md +43 -61
  10. package/skills/02-plan/references/solution-search.md +46 -0
  11. package/skills/03-work/SKILL.md +34 -52
  12. package/skills/03-work/references/completion-report.md +51 -0
  13. package/skills/04-review/SKILL.md +44 -51
  14. package/skills/04-review/references/solution-search.md +46 -0
  15. package/skills/06-next/SKILL.md +35 -20
  16. package/skills/06-next/references/recommendation-logic.md +47 -2
  17. package/skills/07-worktree/SKILL.md +21 -20
  18. package/skills/08-help/SKILL.md +46 -0
  19. package/skills/08-help/references/workflow-sequence.md +135 -0
  20. package/skills/references/language-detection.md +44 -0
  21. package/LICENSE +0 -21
  22. package/THIRD-PARTY-NOTICES.md +0 -35
  23. package/extensions/subagent/__tests__/async-job-tracker.test.ts +0 -21
  24. package/extensions/subagent/__tests__/execution-activity.test.ts +0 -15
  25. package/extensions/subagent/__tests__/parallel-render-stress.test.ts +0 -82
  26. package/extensions/subagent/__tests__/render-dedup.test.ts +0 -98
  27. package/extensions/subagent/__tests__/render-widget.test.ts +0 -28
  28. package/extensions/subagent/__tests__/throttle.test.ts +0 -90
  29. package/extensions/subagent/agent-management.ts +0 -596
  30. package/extensions/subagent/agent-manager-chain-detail.ts +0 -163
  31. package/extensions/subagent/agent-manager-detail.ts +0 -232
  32. package/extensions/subagent/agent-manager-edit.ts +0 -391
  33. package/extensions/subagent/agent-manager-list.ts +0 -279
  34. package/extensions/subagent/agent-manager-parallel.ts +0 -305
  35. package/extensions/subagent/agent-manager.ts +0 -706
  36. package/extensions/subagent/agent-scope.ts +0 -9
  37. package/extensions/subagent/agent-selection.ts +0 -26
  38. package/extensions/subagent/agent-serializer.ts +0 -124
  39. package/extensions/subagent/agent-templates.ts +0 -63
  40. package/extensions/subagent/agents/context-builder.md +0 -37
  41. package/extensions/subagent/agents/delegate.md +0 -9
  42. package/extensions/subagent/agents/oracle.md +0 -73
  43. package/extensions/subagent/agents/planner.md +0 -52
  44. package/extensions/subagent/agents/researcher.md +0 -50
  45. package/extensions/subagent/agents/reviewer.md +0 -38
  46. package/extensions/subagent/agents/scout.md +0 -48
  47. package/extensions/subagent/agents/worker.md +0 -52
  48. package/extensions/subagent/agents.ts +0 -762
  49. package/extensions/subagent/artifacts.ts +0 -101
  50. package/extensions/subagent/async-execution.ts +0 -521
  51. package/extensions/subagent/async-job-tracker.ts +0 -240
  52. package/extensions/subagent/async-status.ts +0 -242
  53. package/extensions/subagent/chain-clarify.ts +0 -1365
  54. package/extensions/subagent/chain-execution.ts +0 -854
  55. package/extensions/subagent/chain-serializer.ts +0 -127
  56. package/extensions/subagent/completion-dedupe.ts +0 -66
  57. package/extensions/subagent/doctor.ts +0 -201
  58. package/extensions/subagent/execution.ts +0 -748
  59. package/extensions/subagent/file-coalescer.ts +0 -43
  60. package/extensions/subagent/fork-context.ts +0 -64
  61. package/extensions/subagent/formatters.ts +0 -123
  62. package/extensions/subagent/frontmatter.ts +0 -32
  63. package/extensions/subagent/index.ts +0 -586
  64. package/extensions/subagent/intercom-bridge.ts +0 -241
  65. package/extensions/subagent/jsonl-writer.ts +0 -84
  66. package/extensions/subagent/model-fallback.ts +0 -109
  67. package/extensions/subagent/notify.ts +0 -111
  68. package/extensions/subagent/parallel-utils.ts +0 -109
  69. package/extensions/subagent/pi-args.ts +0 -139
  70. package/extensions/subagent/pi-spawn.ts +0 -101
  71. package/extensions/subagent/post-exit-stdio-guard.ts +0 -88
  72. package/extensions/subagent/prompt-template-bridge.ts +0 -400
  73. package/extensions/subagent/prompts/gather-context-and-clarify.md +0 -13
  74. package/extensions/subagent/prompts/parallel-cleanup.md +0 -42
  75. package/extensions/subagent/prompts/parallel-research.md +0 -50
  76. package/extensions/subagent/prompts/parallel-review.md +0 -40
  77. package/extensions/subagent/render-helpers.ts +0 -83
  78. package/extensions/subagent/render.ts +0 -908
  79. package/extensions/subagent/result-intercom.ts +0 -238
  80. package/extensions/subagent/result-watcher.ts +0 -172
  81. package/extensions/subagent/run-history.ts +0 -58
  82. package/extensions/subagent/run-status.ts +0 -137
  83. package/extensions/subagent/schemas.ts +0 -165
  84. package/extensions/subagent/session-tokens.ts +0 -51
  85. package/extensions/subagent/settings.ts +0 -368
  86. package/extensions/subagent/single-output.ts +0 -98
  87. package/extensions/subagent/skills.ts +0 -627
  88. package/extensions/subagent/slash-bridge.ts +0 -177
  89. package/extensions/subagent/slash-commands.ts +0 -304
  90. package/extensions/subagent/slash-live-state.ts +0 -295
  91. package/extensions/subagent/subagent-control.ts +0 -151
  92. package/extensions/subagent/subagent-executor.ts +0 -1915
  93. package/extensions/subagent/subagent-prompt-runtime.ts +0 -76
  94. package/extensions/subagent/subagent-runner.ts +0 -1471
  95. package/extensions/subagent/subagents-status.ts +0 -473
  96. package/extensions/subagent/text-editor.ts +0 -273
  97. package/extensions/subagent/throttle.ts +0 -77
  98. package/extensions/subagent/top-level-async.ts +0 -16
  99. package/extensions/subagent/types.ts +0 -624
  100. package/extensions/subagent/utils.ts +0 -457
  101. package/extensions/subagent/worktree.ts +0 -580
  102. package/extensions/super-pi-extension/agents/ce-oracle.md +0 -109
  103. package/extensions/super-pi-extension/agents/ce-planner.md +0 -116
  104. package/extensions/super-pi-extension/agents/ce-reviewer.md +0 -99
  105. package/extensions/super-pi-extension/agents/ce-scout.md +0 -80
  106. package/extensions/super-pi-extension/agents/ce-worker.md +0 -78
  107. package/extensions/super-pi-extension/chains/ce-parallel-review.chain.md +0 -34
  108. package/extensions/super-pi-extension/chains/ce-review-only.chain.md +0 -12
  109. package/extensions/super-pi-extension/chains/ce-standard.chain.md +0 -28
  110. package/extensions/super-pi-extension/index.ts +0 -129
  111. package/extensions/super-pi-extension/model-sync.ts +0 -204
  112. package/skills/08-status/SKILL.md +0 -41
  113. package/skills/08-status/references/artifact-locations.md +0 -10
  114. package/skills/09-help/SKILL.md +0 -37
  115. package/skills/09-help/references/workflow-sequence.md +0 -9
  116. package/skills/10-rules/SKILL.md +0 -76
  117. package/skills/pi-subagents/SKILL.md +0 -566
@@ -1,1365 +0,0 @@
1
- // SPDX-FileCopyrightText: 2025 Nico Bailon
2
- // SPDX-License-Identifier: MIT
3
- // Source: https://github.com/nicobailon/pi-subagents
4
- /**
5
- * Chain Clarification TUI Component
6
- *
7
- * Shows templates and resolved behaviors for each step in a chain.
8
- * Supports editing templates, output paths, reads lists, and progress toggle.
9
- */
10
-
11
- import type { Theme } from "@mariozechner/pi-coding-agent";
12
- import type { Component, TUI } from "@mariozechner/pi-tui";
13
- import { matchesKey, visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
14
- import * as fs from "node:fs";
15
- import * as os from "node:os";
16
- import * as path from "node:path";
17
- import type { AgentConfig, ChainConfig, ChainStepConfig } from "./agents.ts";
18
- import type { ResolvedStepBehavior } from "./settings.ts";
19
- import type { TextEditorState } from "./text-editor.ts";
20
- import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.ts";
21
- import { updateFrontmatterField } from "./agent-serializer.ts";
22
- import { serializeChain } from "./chain-serializer.ts";
23
- import { resolveModelCandidate, splitThinkingSuffix } from "./model-fallback.ts";
24
-
25
- export type ClarifyMode = 'single' | 'parallel' | 'chain';
26
-
27
- export interface ModelInfo {
28
- provider: string;
29
- id: string;
30
- fullId: string;
31
- }
32
-
33
- export interface BehaviorOverride {
34
- output?: string | false;
35
- reads?: string[] | false;
36
- progress?: boolean;
37
- model?: string;
38
- skills?: string[] | false;
39
- }
40
-
41
- export interface ChainClarifyResult {
42
- confirmed: boolean;
43
- templates: string[];
44
- behaviorOverrides: (BehaviorOverride | undefined)[];
45
- runInBackground?: boolean;
46
- }
47
-
48
- type EditMode = "template" | "output" | "reads" | "model" | "thinking" | "skills";
49
-
50
- const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
51
- type ThinkingLevel = typeof THINKING_LEVELS[number];
52
-
53
- /**
54
- * TUI component for chain clarification.
55
- * Factory signature matches ctx.ui.custom: (tui, theme, kb, done) => Component
56
- */
57
- export class ChainClarifyComponent implements Component {
58
- readonly width = 84;
59
-
60
- private selectedStep = 0;
61
- private editingStep: number | null = null;
62
- private editMode: EditMode = "template";
63
- private editState: TextEditorState = createEditorState();
64
-
65
- private readonly EDIT_VIEWPORT_HEIGHT = 12;
66
- private behaviorOverrides: Map<number, BehaviorOverride> = new Map();
67
- private modelSearchQuery: string = "";
68
- private modelSelectedIndex: number = 0;
69
- private filteredModels: ModelInfo[] = [];
70
- private readonly MODEL_SELECTOR_HEIGHT = 10;
71
- private thinkingSelectedIndex: number = 0;
72
- private skillSearchQuery: string = "";
73
- private skillSelectedNames: Set<string> = new Set();
74
- private skillCursorIndex: number = 0;
75
- private filteredSkills: Array<{ name: string; source: string; description?: string }> = [];
76
- private saveMessage: { text: string; type: "info" | "error" } | null = null;
77
- private saveMessageTimer: ReturnType<typeof setTimeout> | null = null;
78
- private saveChainNameState: TextEditorState = createEditorState();
79
- private savingChain = false;
80
- /** Run in background (async) mode */
81
- private runInBackground = false;
82
- private tui: TUI;
83
- private theme: Theme;
84
- private agentConfigs: AgentConfig[];
85
- private templates: string[];
86
- private originalTask: string;
87
- private chainDir: string | undefined;
88
- private resolvedBehaviors: ResolvedStepBehavior[];
89
- private availableModels: ModelInfo[];
90
- private preferredProvider: string | undefined;
91
- private availableSkills: Array<{ name: string; source: string; description?: string }>;
92
- private done: (result: ChainClarifyResult) => void;
93
- private mode: ClarifyMode;
94
-
95
- constructor(
96
- tui: TUI,
97
- theme: Theme,
98
- agentConfigs: AgentConfig[],
99
- templates: string[],
100
- originalTask: string,
101
- chainDir: string | undefined,
102
- resolvedBehaviors: ResolvedStepBehavior[],
103
- availableModels: ModelInfo[],
104
- preferredProvider: string | undefined,
105
- availableSkills: Array<{ name: string; source: string; description?: string }>,
106
- done: (result: ChainClarifyResult) => void,
107
- mode: ClarifyMode = 'chain',
108
- ) {
109
- this.tui = tui;
110
- this.theme = theme;
111
- this.agentConfigs = agentConfigs;
112
- this.templates = templates;
113
- this.originalTask = originalTask;
114
- this.chainDir = chainDir;
115
- this.resolvedBehaviors = resolvedBehaviors;
116
- this.availableModels = availableModels;
117
- this.preferredProvider = preferredProvider;
118
- this.availableSkills = availableSkills;
119
- this.done = done;
120
- this.mode = mode;
121
- this.filteredModels = [...availableModels];
122
- this.filteredSkills = [...availableSkills];
123
- }
124
-
125
- // ─────────────────────────────────────────────────────────────────────────────
126
- // Helper methods for rendering
127
- // ─────────────────────────────────────────────────────────────────────────────
128
-
129
- /** Pad string to specified visible width */
130
- private pad(s: string, len: number): string {
131
- const vis = visibleWidth(s);
132
- return s + " ".repeat(Math.max(0, len - vis));
133
- }
134
-
135
- /** Create a row with border characters */
136
- private row(content: string): string {
137
- const innerW = this.width - 2;
138
- return this.theme.fg("border", "│") + this.pad(content, innerW) + this.theme.fg("border", "│");
139
- }
140
-
141
- /** Render centered header line with border */
142
- private renderHeader(text: string): string {
143
- const innerW = this.width - 2;
144
- const padLen = Math.max(0, innerW - visibleWidth(text));
145
- const padLeft = Math.floor(padLen / 2);
146
- const padRight = padLen - padLeft;
147
- return (
148
- this.theme.fg("border", "╭" + "─".repeat(padLeft)) +
149
- this.theme.fg("accent", text) +
150
- this.theme.fg("border", "─".repeat(padRight) + "╮")
151
- );
152
- }
153
-
154
- /** Render centered footer line with border */
155
- private renderFooter(text: string): string {
156
- const innerW = this.width - 2;
157
- const padLen = Math.max(0, innerW - visibleWidth(text));
158
- const padLeft = Math.floor(padLen / 2);
159
- const padRight = padLen - padLeft;
160
- return (
161
- this.theme.fg("border", "╰" + "─".repeat(padLeft)) +
162
- this.theme.fg("dim", text) +
163
- this.theme.fg("border", "─".repeat(padRight) + "╯")
164
- );
165
- }
166
-
167
- /** Exit edit mode and reset state */
168
- private exitEditMode(): void {
169
- this.editingStep = null;
170
- this.editState = createEditorState();
171
- this.tui.requestRender();
172
- }
173
-
174
- // ─────────────────────────────────────────────────────────────────────────────
175
- // Full edit mode methods
176
- // ─────────────────────────────────────────────────────────────────────────────
177
-
178
- /** Render the full-edit takeover view */
179
- private renderFullEditMode(): string[] {
180
- const innerW = this.width - 2;
181
- const textWidth = innerW - 2; // 1 char padding on each side
182
- const lines: string[] = [];
183
-
184
- const { lines: wrapped, starts } = wrapText(this.editState.buffer, textWidth);
185
- const cursorPos = getCursorDisplayPos(this.editState.cursor, starts);
186
- this.editState = {
187
- ...this.editState,
188
- viewportOffset: ensureCursorVisible(
189
- cursorPos.line,
190
- this.EDIT_VIEWPORT_HEIGHT,
191
- this.editState.viewportOffset,
192
- ),
193
- };
194
-
195
- // Header (truncate agent name to prevent overflow)
196
- const fieldName = this.editMode === "template" ? "task" : this.editMode;
197
- const rawAgentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
198
- const maxAgentLen = innerW - 30; // Reserve space for " Editing X (Step/Task N: ) "
199
- const agentName = rawAgentName.length > maxAgentLen
200
- ? rawAgentName.slice(0, maxAgentLen - 1) + "…"
201
- : rawAgentName;
202
- // Use mode-appropriate terminology
203
- const stepLabel = this.mode === 'single'
204
- ? agentName
205
- : this.mode === 'parallel'
206
- ? `Task ${this.editingStep! + 1}: ${agentName}`
207
- : `Step ${this.editingStep! + 1}: ${agentName}`;
208
- const headerText = ` Editing ${fieldName} (${stepLabel}) `;
209
- lines.push(this.renderHeader(headerText));
210
- lines.push(this.row(""));
211
-
212
- const editorLines = renderEditor(this.editState, textWidth, this.EDIT_VIEWPORT_HEIGHT);
213
- for (const line of editorLines) {
214
- lines.push(this.row(` ${line}`));
215
- }
216
-
217
- const linesBelow = wrapped.length - this.editState.viewportOffset - this.EDIT_VIEWPORT_HEIGHT;
218
- const hasMore = linesBelow > 0;
219
- const hasLess = this.editState.viewportOffset > 0;
220
- let scrollInfo = "";
221
- if (hasLess) scrollInfo += "↑";
222
- if (hasMore) scrollInfo += `↓ ${linesBelow}+`;
223
-
224
- lines.push(this.row(""));
225
-
226
- const footerText = scrollInfo
227
- ? ` [Esc] Done • [Ctrl+C] Discard • ${scrollInfo} `
228
- : " [Esc] Done • [Ctrl+C] Discard ";
229
- lines.push(this.renderFooter(footerText));
230
-
231
- return lines;
232
- }
233
-
234
- // ─────────────────────────────────────────────────────────────────────────────
235
- // Behavior helpers
236
- // ─────────────────────────────────────────────────────────────────────────────
237
-
238
- /** Get effective behavior for a step (with user overrides applied) */
239
- private getEffectiveBehavior(stepIndex: number): ResolvedStepBehavior {
240
- const base = this.resolvedBehaviors[stepIndex]!;
241
- const override = this.behaviorOverrides.get(stepIndex);
242
- if (!override) return base;
243
-
244
- return {
245
- output: override.output !== undefined ? override.output : base.output,
246
- reads: override.reads !== undefined ? override.reads : base.reads,
247
- progress: override.progress !== undefined ? override.progress : base.progress,
248
- skills: override.skills !== undefined ? override.skills : base.skills,
249
- model: override.model !== undefined ? override.model : base.model,
250
- };
251
- }
252
-
253
- /** Get the effective model for a step (override or agent default) */
254
- private getEffectiveModel(stepIndex: number): string {
255
- const override = this.behaviorOverrides.get(stepIndex);
256
- if (override?.model) return this.resolveModelFullId(override.model);
257
-
258
- const baseModel = this.resolvedBehaviors[stepIndex]?.model;
259
- if (baseModel) return this.resolveModelFullId(baseModel);
260
- return "default";
261
- }
262
-
263
- /** Resolve a model name to its full provider/model format */
264
- private resolveModelFullId(modelName: string): string {
265
- return resolveModelCandidate(modelName, this.availableModels, this.preferredProvider) ?? modelName;
266
- }
267
-
268
- /** Update a behavior override for a step */
269
- private updateBehavior(stepIndex: number, field: keyof BehaviorOverride, value: string | boolean | string[] | false): void {
270
- const existing = this.behaviorOverrides.get(stepIndex) ?? {};
271
- this.behaviorOverrides.set(stepIndex, { ...existing, [field]: value });
272
- }
273
-
274
- private buildChainConfig(name: string): ChainConfig {
275
- const steps: ChainStepConfig[] = [];
276
- for (let i = 0; i < this.agentConfigs.length; i++) {
277
- const agent = this.agentConfigs[i]!;
278
- const behavior = this.getEffectiveBehavior(i);
279
- const override = this.behaviorOverrides.get(i);
280
- const template = this.templates[i] ?? "";
281
- const step: ChainStepConfig = { agent: agent.name, task: template };
282
- if (override?.output !== undefined) step.output = behavior.output;
283
- if (override?.reads !== undefined) step.reads = behavior.reads;
284
- if (override?.model !== undefined) step.model = behavior.model;
285
- if (override?.skills !== undefined) step.skills = behavior.skills;
286
- if (override?.progress !== undefined) step.progress = behavior.progress;
287
- steps.push(step);
288
- }
289
- return {
290
- name,
291
- description: `Chain: ${steps.map((s) => s.agent).join(" → ")}`,
292
- source: "user",
293
- filePath: "",
294
- steps,
295
- };
296
- }
297
-
298
- private enterSaveChainName(): void {
299
- this.savingChain = true;
300
- this.saveChainNameState = createEditorState();
301
- this.tui.requestRender();
302
- }
303
-
304
- private handleSaveChainNameInput(data: string): void {
305
- if (matchesKey(data, "tab")) return;
306
- const innerW = this.width - 2;
307
- const boxInnerWidth = Math.max(10, innerW - 4);
308
- const nextState = handleEditorInput(this.saveChainNameState, data, boxInnerWidth);
309
- if (nextState) {
310
- this.saveChainNameState = nextState;
311
- this.tui.requestRender();
312
- return;
313
- }
314
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
315
- this.savingChain = false;
316
- this.saveChainNameState = createEditorState();
317
- this.tui.requestRender();
318
- return;
319
- }
320
- if (matchesKey(data, "return")) {
321
- const name = this.saveChainNameState.buffer.trim();
322
- if (!name) {
323
- this.showSaveMessage("Name is required", "error");
324
- this.savingChain = false;
325
- this.saveChainNameState = createEditorState();
326
- return;
327
- }
328
- try {
329
- const dir = path.join(os.homedir(), ".pi", "agent", "agents");
330
- fs.mkdirSync(dir, { recursive: true });
331
- const filePath = path.join(dir, `${name}.chain.md`);
332
- const config = this.buildChainConfig(name);
333
- config.filePath = filePath;
334
- fs.writeFileSync(filePath, serializeChain(config), "utf-8");
335
- this.showSaveMessage(`Saved ${name}.chain.md`, "info");
336
- } catch (err) {
337
- this.showSaveMessage(err instanceof Error ? err.message : String(err), "error");
338
- }
339
- this.savingChain = false;
340
- this.saveChainNameState = createEditorState();
341
- }
342
- }
343
-
344
- private showSaveMessage(text: string, type: "info" | "error"): void {
345
- this.saveMessage = { text, type };
346
- if (this.saveMessageTimer) clearTimeout(this.saveMessageTimer);
347
- this.saveMessageTimer = setTimeout(() => {
348
- this.saveMessage = null;
349
- this.saveMessageTimer = null;
350
- this.tui.requestRender();
351
- }, 2000);
352
- this.tui.requestRender();
353
- }
354
-
355
- private arraysEqual(a: string[] | false, b: string[] | false): boolean {
356
- if (a === b) return true;
357
- if (a === false || b === false) return false;
358
- if (a.length !== b.length) return false;
359
- for (let i = 0; i < a.length; i++) {
360
- if (a[i] !== b[i]) return false;
361
- }
362
- return true;
363
- }
364
-
365
- private saveOverridesToAgent(): void {
366
- const stepIndex = this.selectedStep;
367
- const agent = this.agentConfigs[stepIndex];
368
- if (!agent?.filePath) {
369
- this.showSaveMessage("Agent file not found", "error");
370
- return;
371
- }
372
-
373
- const override = this.behaviorOverrides.get(stepIndex);
374
- if (!override) {
375
- this.showSaveMessage("No changes to save", "info");
376
- return;
377
- }
378
-
379
- const base = this.resolvedBehaviors[stepIndex]!;
380
- const updates: Array<{ field: string; value: string | undefined }> = [];
381
-
382
- if (override.output !== undefined && override.output !== base.output) {
383
- updates.push({
384
- field: "output",
385
- value: override.output === false ? undefined : override.output,
386
- });
387
- }
388
-
389
- if (override.reads !== undefined && !this.arraysEqual(override.reads, base.reads)) {
390
- updates.push({
391
- field: "defaultReads",
392
- value: override.reads === false ? undefined : override.reads.join(", "),
393
- });
394
- }
395
-
396
- if (override.progress !== undefined && override.progress !== base.progress) {
397
- updates.push({
398
- field: "defaultProgress",
399
- value: override.progress ? "true" : undefined,
400
- });
401
- }
402
-
403
- if (override.skills !== undefined && !this.arraysEqual(override.skills, base.skills)) {
404
- updates.push({
405
- field: "skills",
406
- value: override.skills === false || override.skills.length === 0 ? undefined : override.skills.join(", "),
407
- });
408
- }
409
-
410
- if (override.model !== undefined) {
411
- const baseModel = agent.model ? this.resolveModelFullId(agent.model) : undefined;
412
- if (override.model !== baseModel) {
413
- updates.push({ field: "model", value: override.model });
414
- }
415
- }
416
-
417
- if (updates.length === 0) {
418
- this.showSaveMessage("No changes to save", "info");
419
- return;
420
- }
421
-
422
- try {
423
- for (const update of updates) {
424
- updateFrontmatterField(agent.filePath, update.field, update.value);
425
- }
426
- this.showSaveMessage("Saved agent settings", "info");
427
- } catch (err) {
428
- this.showSaveMessage(err instanceof Error ? err.message : String(err), "error");
429
- }
430
- }
431
-
432
- handleInput(data: string): void {
433
- if (this.savingChain) {
434
- this.handleSaveChainNameInput(data);
435
- return;
436
- }
437
-
438
- if (this.editingStep !== null) {
439
- if (this.editMode === "model") {
440
- this.handleModelSelectorInput(data);
441
- } else if (this.editMode === "thinking") {
442
- this.handleThinkingSelectorInput(data);
443
- } else if (this.editMode === "skills") {
444
- this.handleSkillSelectorInput(data);
445
- } else {
446
- this.handleEditInput(data);
447
- }
448
- return;
449
- }
450
-
451
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
452
- this.done({ confirmed: false, templates: [], behaviorOverrides: [] });
453
- return;
454
- }
455
-
456
- if (matchesKey(data, "return")) {
457
- const overrides: (BehaviorOverride | undefined)[] = [];
458
- for (let i = 0; i < this.agentConfigs.length; i++) {
459
- overrides.push(this.behaviorOverrides.get(i));
460
- }
461
- this.done({ confirmed: true, templates: this.templates, behaviorOverrides: overrides, runInBackground: this.runInBackground });
462
- return;
463
- }
464
-
465
- if (matchesKey(data, "up")) {
466
- this.selectedStep = Math.max(0, this.selectedStep - 1);
467
- this.tui.requestRender();
468
- return;
469
- }
470
-
471
- if (matchesKey(data, "down")) {
472
- const maxStep = Math.max(0, this.agentConfigs.length - 1);
473
- this.selectedStep = Math.min(maxStep, this.selectedStep + 1);
474
- this.tui.requestRender();
475
- return;
476
- }
477
-
478
- if (data === "e") {
479
- this.enterEditMode("template");
480
- return;
481
- }
482
-
483
- if (data === "m") {
484
- this.enterModelSelector();
485
- return;
486
- }
487
-
488
- if (data === "t") {
489
- this.enterThinkingSelector();
490
- return;
491
- }
492
-
493
- if (data === "s") {
494
- this.editingStep = this.selectedStep;
495
- this.editMode = "skills";
496
- this.skillSearchQuery = "";
497
- this.skillCursorIndex = 0;
498
- this.filteredSkills = [...this.availableSkills];
499
- const current = this.getEffectiveBehavior(this.selectedStep).skills;
500
- this.skillSelectedNames.clear();
501
- if (current !== false && current.length > 0) {
502
- current.forEach((skillName) => this.skillSelectedNames.add(skillName));
503
- }
504
- this.tui.requestRender();
505
- return;
506
- }
507
-
508
- if (data === "w" && this.mode !== 'parallel') {
509
- this.enterEditMode("output");
510
- return;
511
- }
512
-
513
- if (data === "r" && this.mode === 'chain') {
514
- this.enterEditMode("reads");
515
- return;
516
- }
517
-
518
- if (data === "p" && this.mode === 'chain') {
519
- const anyEnabled = this.agentConfigs.some((_, i) => this.getEffectiveBehavior(i).progress);
520
- const newState = !anyEnabled;
521
- for (let i = 0; i < this.agentConfigs.length; i++) {
522
- this.updateBehavior(i, "progress", newState);
523
- }
524
- this.tui.requestRender();
525
- return;
526
- }
527
-
528
- if (data === "b") {
529
- this.runInBackground = !this.runInBackground;
530
- this.tui.requestRender();
531
- return;
532
- }
533
-
534
- if (data === "S") {
535
- this.saveOverridesToAgent();
536
- return;
537
- }
538
-
539
- if (data === "W" && this.mode === "chain") {
540
- this.enterSaveChainName();
541
- return;
542
- }
543
- }
544
-
545
- private enterEditMode(mode: EditMode): void {
546
- this.editingStep = this.selectedStep;
547
- this.editMode = mode;
548
- let buffer = "";
549
-
550
- if (mode === "template") {
551
- const template = this.templates[this.selectedStep] ?? "";
552
- buffer = template.split("\n")[0] ?? "";
553
- } else if (mode === "output") {
554
- const behavior = this.getEffectiveBehavior(this.selectedStep);
555
- buffer = behavior.output === false ? "" : (behavior.output || "");
556
- } else if (mode === "reads") {
557
- const behavior = this.getEffectiveBehavior(this.selectedStep);
558
- buffer = behavior.reads === false ? "" : (behavior.reads?.join(", ") || "");
559
- }
560
-
561
- this.editState = createEditorState(buffer);
562
- this.tui.requestRender();
563
- }
564
-
565
- /** Enter model selector mode */
566
- private enterModelSelector(): void {
567
- this.editingStep = this.selectedStep;
568
- this.editMode = "model";
569
- this.modelSearchQuery = "";
570
- this.modelSelectedIndex = 0;
571
- this.filteredModels = [...this.availableModels];
572
- const currentModel = splitThinkingSuffix(this.getEffectiveModel(this.selectedStep)).baseModel;
573
- const currentIndex = this.filteredModels.findIndex((m) => m.fullId === currentModel || m.id === currentModel);
574
- if (currentIndex >= 0) {
575
- this.modelSelectedIndex = currentIndex;
576
- }
577
-
578
- this.tui.requestRender();
579
- }
580
-
581
- /** Filter models based on search query */
582
- private filterModels(): void {
583
- const query = this.modelSearchQuery.toLowerCase();
584
- if (!query) {
585
- this.filteredModels = [...this.availableModels];
586
- } else {
587
- this.filteredModels = this.availableModels.filter((m) =>
588
- m.fullId.toLowerCase().includes(query) ||
589
- m.id.toLowerCase().includes(query) ||
590
- m.provider.toLowerCase().includes(query)
591
- );
592
- }
593
- this.modelSelectedIndex = Math.min(this.modelSelectedIndex, Math.max(0, this.filteredModels.length - 1));
594
- }
595
-
596
- private handleModelSelectorInput(data: string): void {
597
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
598
- this.exitEditMode();
599
- return;
600
- }
601
-
602
- if (matchesKey(data, "return")) {
603
- const selected = this.filteredModels[this.modelSelectedIndex];
604
- if (selected) {
605
- const { thinkingSuffix } = splitThinkingSuffix(this.getEffectiveModel(this.editingStep!));
606
- this.updateBehavior(this.editingStep!, "model", `${selected.fullId}${thinkingSuffix}`);
607
- }
608
- this.exitEditMode();
609
- return;
610
- }
611
-
612
- if (matchesKey(data, "up")) {
613
- if (this.filteredModels.length > 0) {
614
- this.modelSelectedIndex = this.modelSelectedIndex === 0
615
- ? this.filteredModels.length - 1
616
- : this.modelSelectedIndex - 1;
617
- }
618
- this.tui.requestRender();
619
- return;
620
- }
621
-
622
- if (matchesKey(data, "down")) {
623
- if (this.filteredModels.length > 0) {
624
- this.modelSelectedIndex = this.modelSelectedIndex === this.filteredModels.length - 1
625
- ? 0
626
- : this.modelSelectedIndex + 1;
627
- }
628
- this.tui.requestRender();
629
- return;
630
- }
631
-
632
- if (matchesKey(data, "backspace")) {
633
- if (this.modelSearchQuery.length > 0) {
634
- this.modelSearchQuery = this.modelSearchQuery.slice(0, -1);
635
- this.filterModels();
636
- }
637
- this.tui.requestRender();
638
- return;
639
- }
640
-
641
- if (data.length === 1 && data.charCodeAt(0) >= 32) {
642
- this.modelSearchQuery += data;
643
- this.filterModels();
644
- this.tui.requestRender();
645
- return;
646
- }
647
- }
648
-
649
- /** Enter thinking level selector mode */
650
- private enterThinkingSelector(): void {
651
- if (!this.getEffectiveBehavior(this.selectedStep).model) {
652
- this.showSaveMessage("Select a model first", "error");
653
- return;
654
- }
655
- this.editingStep = this.selectedStep;
656
- this.editMode = "thinking";
657
-
658
- const currentModel = this.getEffectiveModel(this.selectedStep);
659
- const colonIdx = currentModel.lastIndexOf(":");
660
- if (colonIdx !== -1) {
661
- const suffix = currentModel.substring(colonIdx + 1);
662
- const levelIdx = THINKING_LEVELS.indexOf(suffix as ThinkingLevel);
663
- this.thinkingSelectedIndex = levelIdx >= 0 ? levelIdx : 0;
664
- } else {
665
- this.thinkingSelectedIndex = 0;
666
- }
667
-
668
- this.tui.requestRender();
669
- }
670
-
671
- private handleThinkingSelectorInput(data: string): void {
672
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
673
- this.exitEditMode();
674
- return;
675
- }
676
-
677
- if (matchesKey(data, "return")) {
678
- const selectedLevel = THINKING_LEVELS[this.thinkingSelectedIndex];
679
- this.applyThinkingLevel(selectedLevel);
680
- this.exitEditMode();
681
- return;
682
- }
683
-
684
- if (matchesKey(data, "up")) {
685
- this.thinkingSelectedIndex = this.thinkingSelectedIndex === 0
686
- ? THINKING_LEVELS.length - 1
687
- : this.thinkingSelectedIndex - 1;
688
- this.tui.requestRender();
689
- return;
690
- }
691
-
692
- if (matchesKey(data, "down")) {
693
- this.thinkingSelectedIndex = this.thinkingSelectedIndex === THINKING_LEVELS.length - 1
694
- ? 0
695
- : this.thinkingSelectedIndex + 1;
696
- this.tui.requestRender();
697
- return;
698
- }
699
- }
700
-
701
- /** Apply thinking level to the current step's model */
702
- private applyThinkingLevel(level: ThinkingLevel): void {
703
- const stepIndex = this.editingStep!;
704
- const currentModel = this.getEffectiveBehavior(stepIndex).model;
705
- if (!currentModel) return;
706
-
707
- const { baseModel } = splitThinkingSuffix(currentModel);
708
- const newModel = level === "off" ? baseModel : `${baseModel}:${level}`;
709
- this.updateBehavior(stepIndex, "model", newModel);
710
- }
711
-
712
- private filterSkills(): void {
713
- const query = this.skillSearchQuery.toLowerCase();
714
- if (!query) {
715
- this.filteredSkills = [...this.availableSkills];
716
- } else {
717
- this.filteredSkills = this.availableSkills.filter((s) =>
718
- s.name.toLowerCase().includes(query) ||
719
- (s.description?.toLowerCase().includes(query) ?? false),
720
- );
721
- }
722
- this.skillCursorIndex = Math.min(this.skillCursorIndex, Math.max(0, this.filteredSkills.length - 1));
723
- }
724
-
725
- private handleSkillSelectorInput(data: string): void {
726
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
727
- this.exitEditMode();
728
- return;
729
- }
730
-
731
- if (matchesKey(data, "return")) {
732
- const selected = [...this.skillSelectedNames];
733
- this.updateBehavior(this.editingStep!, "skills", selected);
734
- this.exitEditMode();
735
- return;
736
- }
737
-
738
- if (data === " ") {
739
- if (this.filteredSkills.length > 0) {
740
- const skill = this.filteredSkills[this.skillCursorIndex];
741
- if (skill) {
742
- if (this.skillSelectedNames.has(skill.name)) {
743
- this.skillSelectedNames.delete(skill.name);
744
- } else {
745
- this.skillSelectedNames.add(skill.name);
746
- }
747
- }
748
- }
749
- this.tui.requestRender();
750
- return;
751
- }
752
-
753
- if (matchesKey(data, "up")) {
754
- if (this.filteredSkills.length > 0) {
755
- this.skillCursorIndex = this.skillCursorIndex === 0
756
- ? this.filteredSkills.length - 1
757
- : this.skillCursorIndex - 1;
758
- }
759
- this.tui.requestRender();
760
- return;
761
- }
762
-
763
- if (matchesKey(data, "down")) {
764
- if (this.filteredSkills.length > 0) {
765
- this.skillCursorIndex = this.skillCursorIndex === this.filteredSkills.length - 1
766
- ? 0
767
- : this.skillCursorIndex + 1;
768
- }
769
- this.tui.requestRender();
770
- return;
771
- }
772
-
773
- if (matchesKey(data, "backspace")) {
774
- if (this.skillSearchQuery.length > 0) {
775
- this.skillSearchQuery = this.skillSearchQuery.slice(0, -1);
776
- this.filterSkills();
777
- }
778
- this.tui.requestRender();
779
- return;
780
- }
781
-
782
- if (data.length === 1 && data.charCodeAt(0) >= 32) {
783
- this.skillSearchQuery += data;
784
- this.filterSkills();
785
- this.tui.requestRender();
786
- return;
787
- }
788
- }
789
-
790
- private handleEditInput(data: string): void {
791
- const textWidth = this.width - 4; // Must match render: innerW - 2 = (width - 2) - 2
792
- if (matchesKey(data, "shift+up") || matchesKey(data, "pageup")) {
793
- const { lines: wrapped, starts } = wrapText(this.editState.buffer, textWidth);
794
- const cursorPos = getCursorDisplayPos(this.editState.cursor, starts);
795
- const targetLine = Math.max(0, cursorPos.line - this.EDIT_VIEWPORT_HEIGHT);
796
- const targetCol = Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0);
797
- this.editState = { ...this.editState, cursor: starts[targetLine] + targetCol };
798
- this.tui.requestRender();
799
- return;
800
- }
801
-
802
- if (matchesKey(data, "shift+down") || matchesKey(data, "pagedown")) {
803
- const { lines: wrapped, starts } = wrapText(this.editState.buffer, textWidth);
804
- const cursorPos = getCursorDisplayPos(this.editState.cursor, starts);
805
- const targetLine = Math.min(wrapped.length - 1, cursorPos.line + this.EDIT_VIEWPORT_HEIGHT);
806
- const targetCol = Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0);
807
- this.editState = { ...this.editState, cursor: starts[targetLine] + targetCol };
808
- this.tui.requestRender();
809
- return;
810
- }
811
-
812
- if (matchesKey(data, "tab")) return;
813
-
814
- const nextState = handleEditorInput(this.editState, data, textWidth);
815
- if (nextState) {
816
- this.editState = nextState;
817
- this.tui.requestRender();
818
- return;
819
- }
820
-
821
- if (matchesKey(data, "escape")) {
822
- this.saveEdit();
823
- this.exitEditMode();
824
- return;
825
- }
826
-
827
- if (matchesKey(data, "ctrl+c")) {
828
- this.exitEditMode();
829
- return;
830
- }
831
- }
832
-
833
- private saveEdit(): void {
834
- const stepIndex = this.editingStep!;
835
-
836
- if (this.editMode === "template") {
837
- // For template, preserve other lines if they existed
838
- const original = this.templates[stepIndex] ?? "";
839
- const originalLines = original.split("\n");
840
- originalLines[0] = this.editState.buffer;
841
- this.templates[stepIndex] = originalLines.join("\n");
842
- } else if (this.editMode === "output") {
843
- // Capture OLD output before updating (for downstream propagation)
844
- const oldBehavior = this.getEffectiveBehavior(stepIndex);
845
- const oldOutput = typeof oldBehavior.output === "string" ? oldBehavior.output : null;
846
-
847
- // Empty string or whitespace means disable output
848
- const trimmed = this.editState.buffer.trim();
849
- const newOutput = trimmed === "" ? false : trimmed;
850
- this.updateBehavior(stepIndex, "output", newOutput);
851
-
852
- // Propagate output filename change to downstream steps' reads
853
- if (oldOutput && typeof newOutput === "string" && oldOutput !== newOutput) {
854
- this.propagateOutputChange(stepIndex, oldOutput, newOutput);
855
- }
856
- } else if (this.editMode === "reads") {
857
- // Parse comma-separated list, empty means disable reads
858
- const trimmed = this.editState.buffer.trim();
859
- if (trimmed === "") {
860
- this.updateBehavior(stepIndex, "reads", false);
861
- } else {
862
- const files = trimmed.split(",").map(f => f.trim()).filter(f => f !== "");
863
- this.updateBehavior(stepIndex, "reads", files.length > 0 ? files : false);
864
- }
865
- }
866
- }
867
-
868
- /**
869
- * When a step's output filename changes, update downstream steps that read from it.
870
- * This maintains the chain dependency automatically.
871
- */
872
- private propagateOutputChange(changedStepIndex: number, oldOutput: string, newOutput: string): void {
873
- // Check all downstream steps (steps that come after the changed step)
874
- for (let i = changedStepIndex + 1; i < this.agentConfigs.length; i++) {
875
- const behavior = this.getEffectiveBehavior(i);
876
-
877
- // Skip if reads is disabled or empty
878
- if (behavior.reads === false || !behavior.reads || behavior.reads.length === 0) {
879
- continue;
880
- }
881
-
882
- // Check if this step reads the old output file
883
- const readsArray = behavior.reads;
884
- const oldIndex = readsArray.indexOf(oldOutput);
885
-
886
- if (oldIndex !== -1) {
887
- // Replace old filename with new filename in reads
888
- const newReads = [...readsArray];
889
- newReads[oldIndex] = newOutput;
890
- this.updateBehavior(i, "reads", newReads);
891
- }
892
- }
893
- }
894
-
895
- private renderSaveChainName(): string[] {
896
- const lines: string[] = [];
897
- const innerW = this.width - 2;
898
- const boxInnerWidth = Math.max(10, innerW - 4);
899
- lines.push(this.renderHeader(" Save Chain "));
900
- lines.push(this.row(""));
901
- lines.push(this.row(` ${this.theme.fg("dim", "Name:")}`));
902
- const top = `┌${"─".repeat(boxInnerWidth)}┐`;
903
- const bottom = `└${"─".repeat(boxInnerWidth)}┘`;
904
- lines.push(this.row(` ${top}`));
905
- const editorState = { ...this.saveChainNameState };
906
- const wrapped = wrapText(editorState.buffer, boxInnerWidth);
907
- const cursorPos = getCursorDisplayPos(editorState.cursor, wrapped.starts);
908
- editorState.viewportOffset = ensureCursorVisible(cursorPos.line, 1, editorState.viewportOffset);
909
- const editorLine = renderEditor(editorState, boxInnerWidth, 1)[0] ?? "";
910
- lines.push(this.row(` │${this.pad(editorLine, boxInnerWidth)}│`));
911
- lines.push(this.row(` ${bottom}`));
912
- lines.push(this.row(""));
913
- lines.push(this.renderFooter(" [Enter] Save • [Esc] Cancel "));
914
- return lines;
915
- }
916
-
917
- render(_width: number): string[] {
918
- if (this.savingChain) {
919
- return this.renderSaveChainName();
920
- }
921
- if (this.editingStep !== null) {
922
- if (this.editMode === "model") {
923
- return this.renderModelSelector();
924
- }
925
- if (this.editMode === "thinking") {
926
- return this.renderThinkingSelector();
927
- }
928
- if (this.editMode === "skills") {
929
- return this.renderSkillSelector();
930
- }
931
- return this.renderFullEditMode();
932
- }
933
- // Mode-based navigation rendering
934
- switch (this.mode) {
935
- case 'single': return this.renderSingleMode();
936
- case 'parallel': return this.renderParallelMode();
937
- case 'chain': return this.renderChainMode();
938
- }
939
- }
940
-
941
- /** Render the model selector view */
942
- private renderModelSelector(): string[] {
943
- const th = this.theme;
944
- const lines: string[] = [];
945
-
946
- // Header (mode-aware terminology)
947
- const agentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
948
- const stepLabel = this.mode === 'single'
949
- ? agentName
950
- : this.mode === 'parallel'
951
- ? `Task ${this.editingStep! + 1}: ${agentName}`
952
- : `Step ${this.editingStep! + 1}: ${agentName}`;
953
- const headerText = ` Select Model (${stepLabel}) `;
954
- lines.push(this.renderHeader(headerText));
955
- lines.push(this.row(""));
956
-
957
- const searchPrefix = th.fg("dim", "Search: ");
958
- const cursor = "\x1b[7m \x1b[27m"; // Reverse video space for cursor
959
- const searchDisplay = this.modelSearchQuery + cursor;
960
- lines.push(this.row(` ${searchPrefix}${searchDisplay}`));
961
- lines.push(this.row(""));
962
-
963
- const currentModel = this.getEffectiveModel(this.editingStep!);
964
- const currentModelBase = splitThinkingSuffix(currentModel).baseModel;
965
- const currentLabel = th.fg("dim", "Current: ");
966
- lines.push(this.row(` ${currentLabel}${th.fg("warning", currentModel)}`));
967
- lines.push(this.row(""));
968
-
969
- if (this.filteredModels.length === 0) {
970
- lines.push(this.row(` ${th.fg("dim", "No matching models")}`));
971
- } else {
972
- const maxVisible = this.MODEL_SELECTOR_HEIGHT;
973
- let startIdx = 0;
974
-
975
- if (this.filteredModels.length > maxVisible) {
976
- startIdx = Math.max(0, this.modelSelectedIndex - Math.floor(maxVisible / 2));
977
- startIdx = Math.min(startIdx, this.filteredModels.length - maxVisible);
978
- }
979
-
980
- const endIdx = Math.min(startIdx + maxVisible, this.filteredModels.length);
981
-
982
- if (startIdx > 0) {
983
- lines.push(this.row(` ${th.fg("dim", ` ↑ ${startIdx} more`)}`));
984
- }
985
-
986
- for (let i = startIdx; i < endIdx; i++) {
987
- const model = this.filteredModels[i]!;
988
- const isSelected = i === this.modelSelectedIndex;
989
- const isCurrent = model.fullId === currentModelBase || model.id === currentModelBase;
990
- const prefix = isSelected ? th.fg("accent", "→ ") : " ";
991
- const modelText = isSelected ? th.fg("accent", model.id) : model.id;
992
- const providerBadge = th.fg("dim", ` [${model.provider}]`);
993
- const currentBadge = isCurrent ? th.fg("success", " current") : "";
994
-
995
- lines.push(this.row(` ${prefix}${modelText}${providerBadge}${currentBadge}`));
996
- }
997
-
998
- const remaining = this.filteredModels.length - endIdx;
999
- if (remaining > 0) {
1000
- lines.push(this.row(` ${th.fg("dim", ` ↓ ${remaining} more`)}`));
1001
- }
1002
- }
1003
-
1004
- const contentLines = lines.length;
1005
- const targetHeight = 18;
1006
- for (let i = contentLines; i < targetHeight; i++) {
1007
- lines.push(this.row(""));
1008
- }
1009
-
1010
- const footerText = " [Enter] Select • [Esc] Cancel • Type to search ";
1011
- lines.push(this.renderFooter(footerText));
1012
-
1013
- return lines;
1014
- }
1015
-
1016
- /** Render the thinking level selector view */
1017
- private renderThinkingSelector(): string[] {
1018
- const th = this.theme;
1019
- const lines: string[] = [];
1020
-
1021
- const agentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
1022
- const stepLabel = this.mode === 'single'
1023
- ? agentName
1024
- : this.mode === 'parallel'
1025
- ? `Task ${this.editingStep! + 1}: ${agentName}`
1026
- : `Step ${this.editingStep! + 1}: ${agentName}`;
1027
- const headerText = ` Thinking Level (${stepLabel}) `;
1028
- lines.push(this.renderHeader(headerText));
1029
- lines.push(this.row(""));
1030
-
1031
- const currentModel = this.getEffectiveModel(this.editingStep!);
1032
- const currentLabel = th.fg("dim", "Model: ");
1033
- lines.push(this.row(` ${currentLabel}${th.fg("accent", currentModel)}`));
1034
- lines.push(this.row(""));
1035
-
1036
- lines.push(this.row(` ${th.fg("dim", "Select thinking level (extended thinking budget):")}`));
1037
- lines.push(this.row(""));
1038
-
1039
- const levelDescriptions: Record<ThinkingLevel, string> = {
1040
- "off": "No extended thinking",
1041
- "minimal": "Brief reasoning",
1042
- "low": "Light reasoning",
1043
- "medium": "Moderate reasoning",
1044
- "high": "Deep reasoning",
1045
- "xhigh": "Maximum reasoning (ultrathink)",
1046
- };
1047
-
1048
- for (let i = 0; i < THINKING_LEVELS.length; i++) {
1049
- const level = THINKING_LEVELS[i];
1050
- const isSelected = i === this.thinkingSelectedIndex;
1051
- const prefix = isSelected ? th.fg("accent", "→ ") : " ";
1052
- const levelText = isSelected ? th.fg("accent", level) : level;
1053
- const desc = th.fg("dim", ` - ${levelDescriptions[level]}`);
1054
- lines.push(this.row(` ${prefix}${levelText}${desc}`));
1055
- }
1056
-
1057
- const contentLines = lines.length;
1058
- const targetHeight = 16;
1059
- for (let i = contentLines; i < targetHeight; i++) {
1060
- lines.push(this.row(""));
1061
- }
1062
-
1063
- const footerText = " [Enter] Select • [Esc] Cancel • ↑↓ Navigate ";
1064
- lines.push(this.renderFooter(footerText));
1065
-
1066
- return lines;
1067
- }
1068
-
1069
- private renderSkillSelector(): string[] {
1070
- const innerW = this.width - 2;
1071
- const th = this.theme;
1072
- const lines: string[] = [];
1073
-
1074
- const agentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
1075
- const stepLabel = this.mode === 'single'
1076
- ? agentName
1077
- : this.mode === 'parallel'
1078
- ? `Task ${this.editingStep! + 1}: ${agentName}`
1079
- : `Step ${this.editingStep! + 1}: ${agentName}`;
1080
- lines.push(this.renderHeader(` Select Skills (${stepLabel}) `));
1081
- lines.push(this.row(""));
1082
-
1083
- const cursor = "\x1b[7m \x1b[27m";
1084
- lines.push(this.row(` ${th.fg("dim", "Search: ")}${this.skillSearchQuery}${cursor}`));
1085
- lines.push(this.row(""));
1086
-
1087
- const selected = [...this.skillSelectedNames].join(", ") || th.fg("dim", "(none)");
1088
- lines.push(this.row(` ${th.fg("dim", "Selected: ")}${truncateToWidth(selected, innerW - 12)}`));
1089
- lines.push(this.row(""));
1090
-
1091
- const selectorHeight = 10;
1092
- if (this.filteredSkills.length === 0) {
1093
- lines.push(this.row(` ${th.fg("dim", "No matching skills")}`));
1094
- } else {
1095
- let startIdx = 0;
1096
- if (this.filteredSkills.length > selectorHeight) {
1097
- startIdx = Math.max(0, this.skillCursorIndex - Math.floor(selectorHeight / 2));
1098
- startIdx = Math.min(startIdx, this.filteredSkills.length - selectorHeight);
1099
- }
1100
- const endIdx = Math.min(startIdx + selectorHeight, this.filteredSkills.length);
1101
-
1102
- if (startIdx > 0) {
1103
- lines.push(this.row(` ${th.fg("dim", ` ↑ ${startIdx} more`)}`));
1104
- }
1105
-
1106
- for (let i = startIdx; i < endIdx; i++) {
1107
- const skill = this.filteredSkills[i]!;
1108
- const isCursor = i === this.skillCursorIndex;
1109
- const isSelected = this.skillSelectedNames.has(skill.name);
1110
-
1111
- const prefix = isCursor ? th.fg("accent", "→ ") : " ";
1112
- const checkbox = isSelected ? th.fg("success", "[x]") : "[ ]";
1113
- const nameText = isCursor ? th.fg("accent", skill.name) : skill.name;
1114
- const sourceBadge = th.fg("dim", ` [${skill.source}]`);
1115
- const desc = skill.description
1116
- ? th.fg("dim", ` - ${truncateToWidth(skill.description, 25)}`)
1117
- : "";
1118
-
1119
- lines.push(this.row(` ${prefix}${checkbox} ${nameText}${sourceBadge}${desc}`));
1120
- }
1121
-
1122
- const remaining = this.filteredSkills.length - endIdx;
1123
- if (remaining > 0) {
1124
- lines.push(this.row(` ${th.fg("dim", ` ↓ ${remaining} more`)}`));
1125
- }
1126
- }
1127
-
1128
- const targetHeight = 18;
1129
- for (let i = lines.length; i < targetHeight; i++) {
1130
- lines.push(this.row(""));
1131
- }
1132
-
1133
- lines.push(this.renderFooter(" [Enter] Confirm • [Space] Toggle • [Esc] Cancel "));
1134
- return lines;
1135
- }
1136
-
1137
- private getFooterText(): string {
1138
- const bgLabel = this.runInBackground ? '[b]g:ON' : '[b]g';
1139
- switch (this.mode) {
1140
- case 'single':
1141
- return ` [Enter] Run • [Esc] Cancel • e m t w s ${bgLabel} S `;
1142
- case 'parallel':
1143
- return ` [Enter] Run • [Esc] Cancel • e m t s ${bgLabel} S • ↑↓ Nav `;
1144
- case 'chain':
1145
- return ` [Enter] Run • [Esc] Cancel • e m t w r p s ${bgLabel} S W • ↑↓ Nav `;
1146
- }
1147
- }
1148
-
1149
- private appendSaveMessage(lines: string[]): void {
1150
- if (!this.saveMessage) return;
1151
- const color = this.saveMessage.type === "error" ? "error" : "success";
1152
- lines.push(this.row(` ${this.theme.fg(color, this.saveMessage.text)}`));
1153
- }
1154
-
1155
- private renderSingleMode(): string[] {
1156
- const innerW = this.width - 2;
1157
- const th = this.theme;
1158
- const lines: string[] = [];
1159
-
1160
- const agentName = this.agentConfigs[0]?.name ?? "unknown";
1161
- const maxHeaderLen = innerW - 4;
1162
- const headerText = ` Agent: ${truncateToWidth(agentName, maxHeaderLen - 9)} `;
1163
- lines.push(this.renderHeader(headerText));
1164
- lines.push(this.row(""));
1165
-
1166
- const config = this.agentConfigs[0]!;
1167
- const behavior = this.getEffectiveBehavior(0);
1168
-
1169
- const stepLabel = config.name;
1170
- lines.push(this.row(` ${th.fg("accent", "▶ " + stepLabel)}`));
1171
-
1172
- const template = (this.templates[0] ?? "").split("\n")[0] ?? "";
1173
- const taskLabel = th.fg("dim", "task: ");
1174
- lines.push(this.row(` ${taskLabel}${truncateToWidth(template, innerW - 12)}`));
1175
-
1176
- const effectiveModel = this.getEffectiveModel(0);
1177
- const override = this.behaviorOverrides.get(0);
1178
- const isOverridden = override?.model !== undefined;
1179
- const modelValue = isOverridden
1180
- ? th.fg("warning", effectiveModel) + th.fg("dim", " ✎")
1181
- : effectiveModel;
1182
- const modelLabel = th.fg("dim", "model: ");
1183
- lines.push(this.row(` ${modelLabel}${truncateToWidth(modelValue, innerW - 13)}`));
1184
-
1185
- const writesValue = behavior.output === false
1186
- ? th.fg("dim", "(disabled)")
1187
- : (behavior.output || th.fg("dim", "(none)"));
1188
- const writesLabel = th.fg("dim", "writes: ");
1189
- lines.push(this.row(` ${writesLabel}${truncateToWidth(writesValue, innerW - 14)}`));
1190
-
1191
- const skillsValue = behavior.skills === false
1192
- ? th.fg("dim", "(disabled)")
1193
- : (behavior.skills?.length ? behavior.skills.join(", ") : th.fg("dim", "(none)"));
1194
- const skillsLabel = th.fg("dim", "skills: ");
1195
- lines.push(this.row(` ${skillsLabel}${truncateToWidth(skillsValue, innerW - 14)}`));
1196
-
1197
- lines.push(this.row(""));
1198
-
1199
- this.appendSaveMessage(lines);
1200
- lines.push(this.renderFooter(this.getFooterText()));
1201
-
1202
- return lines;
1203
- }
1204
-
1205
- private renderParallelMode(): string[] {
1206
- const innerW = this.width - 2;
1207
- const th = this.theme;
1208
- const lines: string[] = [];
1209
-
1210
- const headerText = ` Parallel Tasks (${this.agentConfigs.length}) `;
1211
- lines.push(this.renderHeader(headerText));
1212
- lines.push(this.row(""));
1213
-
1214
- for (let i = 0; i < this.agentConfigs.length; i++) {
1215
- const config = this.agentConfigs[i]!;
1216
- const isSelected = i === this.selectedStep;
1217
-
1218
- const color = isSelected ? "accent" : "dim";
1219
- const prefix = isSelected ? "▶ " : " ";
1220
- const taskPrefix = `Task ${i + 1}: `;
1221
- const maxNameLen = innerW - 4 - prefix.length - taskPrefix.length;
1222
- const agentName = config.name.length > maxNameLen
1223
- ? config.name.slice(0, maxNameLen - 1) + "…"
1224
- : config.name;
1225
- const taskLabel = `${taskPrefix}${agentName}`;
1226
- lines.push(this.row(` ${th.fg(color, prefix + taskLabel)}`));
1227
-
1228
- const template = (this.templates[i] ?? "").split("\n")[0] ?? "";
1229
- const taskTextLabel = th.fg("dim", "task: ");
1230
- lines.push(this.row(` ${taskTextLabel}${truncateToWidth(template, innerW - 12)}`));
1231
-
1232
- const effectiveModel = this.getEffectiveModel(i);
1233
- const override = this.behaviorOverrides.get(i);
1234
- const isOverridden = override?.model !== undefined;
1235
- const modelValue = isOverridden
1236
- ? th.fg("warning", effectiveModel) + th.fg("dim", " ✎")
1237
- : effectiveModel;
1238
- const modelLabel = th.fg("dim", "model: ");
1239
- lines.push(this.row(` ${modelLabel}${truncateToWidth(modelValue, innerW - 13)}`));
1240
-
1241
- const behavior = this.getEffectiveBehavior(i);
1242
- const skillsValue = behavior.skills === false
1243
- ? th.fg("dim", "(disabled)")
1244
- : (behavior.skills?.length ? behavior.skills.join(", ") : th.fg("dim", "(none)"));
1245
- const skillsLabel = th.fg("dim", "skills: ");
1246
- lines.push(this.row(` ${skillsLabel}${truncateToWidth(skillsValue, innerW - 14)}`));
1247
-
1248
- lines.push(this.row(""));
1249
- }
1250
-
1251
- this.appendSaveMessage(lines);
1252
- lines.push(this.renderFooter(this.getFooterText()));
1253
-
1254
- return lines;
1255
- }
1256
-
1257
- private renderChainMode(): string[] {
1258
- const innerW = this.width - 2;
1259
- const th = this.theme;
1260
- const lines: string[] = [];
1261
-
1262
- const chainLabel = this.agentConfigs.map((c) => c.name).join(" → ");
1263
- const maxHeaderLen = innerW - 4;
1264
- const headerText = ` Chain: ${truncateToWidth(chainLabel, maxHeaderLen - 9)} `;
1265
- lines.push(this.renderHeader(headerText));
1266
-
1267
- lines.push(this.row(""));
1268
-
1269
- const taskPreview = truncateToWidth(this.originalTask, innerW - 16);
1270
- lines.push(this.row(` Original Task: ${taskPreview}`));
1271
- const chainDirPreview = truncateToWidth(this.chainDir ?? "", innerW - 12);
1272
- lines.push(this.row(` Chain Dir: ${th.fg("dim", chainDirPreview)}`));
1273
-
1274
- const progressEnabled = this.agentConfigs.some((_, i) => this.getEffectiveBehavior(i).progress);
1275
- const progressValue = progressEnabled ? th.fg("success", "enabled") : th.fg("dim", "disabled");
1276
- lines.push(this.row(` Progress: ${progressValue} ${th.fg("dim", "(press [p] to toggle)")}`));
1277
- lines.push(this.row(""));
1278
-
1279
- for (let i = 0; i < this.agentConfigs.length; i++) {
1280
- const config = this.agentConfigs[i]!;
1281
- const isSelected = i === this.selectedStep;
1282
- const behavior = this.getEffectiveBehavior(i);
1283
-
1284
- const color = isSelected ? "accent" : "dim";
1285
- const prefix = isSelected ? "▶ " : " ";
1286
- const stepPrefix = `Step ${i + 1}: `;
1287
- const maxNameLen = innerW - 4 - prefix.length - stepPrefix.length;
1288
- const agentName = config.name.length > maxNameLen
1289
- ? config.name.slice(0, maxNameLen - 1) + "…"
1290
- : config.name;
1291
- const stepLabel = `${stepPrefix}${agentName}`;
1292
- lines.push(
1293
- this.row(` ${th.fg(color, prefix + stepLabel)}`),
1294
- );
1295
-
1296
- const template = (this.templates[i] ?? "").split("\n")[0] ?? "";
1297
- const highlighted = template
1298
- .replace(/\{task\}/g, th.fg("success", "{task}"))
1299
- .replace(/\{previous\}/g, th.fg("warning", "{previous}"))
1300
- .replace(/\{chain_dir\}/g, th.fg("accent", "{chain_dir}"));
1301
-
1302
- const templateLabel = th.fg("dim", "task: ");
1303
- lines.push(this.row(` ${templateLabel}${truncateToWidth(highlighted, innerW - 12)}`));
1304
-
1305
- const effectiveModel = this.getEffectiveModel(i);
1306
- const override = this.behaviorOverrides.get(i);
1307
- const isOverridden = override?.model !== undefined;
1308
- const modelValue = isOverridden
1309
- ? th.fg("warning", effectiveModel) + th.fg("dim", " ✎")
1310
- : effectiveModel;
1311
- const modelLabel = th.fg("dim", "model: ");
1312
- lines.push(this.row(` ${modelLabel}${truncateToWidth(modelValue, innerW - 13)}`));
1313
-
1314
- const writesValue = behavior.output === false
1315
- ? th.fg("dim", "(disabled)")
1316
- : (behavior.output || th.fg("dim", "(none)"));
1317
- const writesLabel = th.fg("dim", "writes: ");
1318
- lines.push(this.row(` ${writesLabel}${truncateToWidth(writesValue, innerW - 14)}`));
1319
-
1320
- const readsValue = behavior.reads === false
1321
- ? th.fg("dim", "(disabled)")
1322
- : (behavior.reads && behavior.reads.length > 0
1323
- ? behavior.reads.join(", ")
1324
- : th.fg("dim", "(none)"));
1325
- const readsLabel = th.fg("dim", "reads: ");
1326
- lines.push(this.row(` ${readsLabel}${truncateToWidth(readsValue, innerW - 13)}`));
1327
-
1328
- const skillsValue = behavior.skills === false
1329
- ? th.fg("dim", "(disabled)")
1330
- : (behavior.skills?.length ? behavior.skills.join(", ") : th.fg("dim", "(none)"));
1331
- const skillsLabel = th.fg("dim", "skills: ");
1332
- lines.push(this.row(` ${skillsLabel}${truncateToWidth(skillsValue, innerW - 14)}`));
1333
-
1334
- if (progressEnabled) {
1335
- const isFirstStep = i === 0;
1336
- const progressAction = isFirstStep
1337
- ? th.fg("success", "writes progress.md")
1338
- : th.fg("accent", "reads progress.md");
1339
- const progressLabel = th.fg("dim", "progress: ");
1340
- lines.push(this.row(` ${progressLabel}${progressAction}`));
1341
- }
1342
-
1343
- if (i < this.agentConfigs.length - 1) {
1344
- const nextStepUsePrevious = (this.templates[i + 1] ?? "").includes("{previous}");
1345
- if (nextStepUsePrevious) {
1346
- const indicator = th.fg("dim", " ↳ response → ") + th.fg("warning", "{previous}");
1347
- lines.push(this.row(indicator));
1348
- }
1349
- }
1350
-
1351
- lines.push(this.row(""));
1352
- }
1353
-
1354
- this.appendSaveMessage(lines);
1355
- lines.push(this.renderFooter(this.getFooterText()));
1356
-
1357
- return lines;
1358
- }
1359
-
1360
- invalidate(): void {}
1361
- dispose(): void {
1362
- if (this.saveMessageTimer) clearTimeout(this.saveMessageTimer);
1363
- this.saveMessageTimer = null;
1364
- }
1365
- }