@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,706 +0,0 @@
1
- // SPDX-FileCopyrightText: 2025 Nico Bailon
2
- // SPDX-License-Identifier: MIT
3
- // Source: https://github.com/nicobailon/pi-subagents
4
- import * as fs from "node:fs";
5
- import * as path from "node:path";
6
- import type { Theme } from "@mariozechner/pi-coding-agent";
7
- import type { Component, TUI } from "@mariozechner/pi-tui";
8
- import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
9
- import {
10
- buildBuiltinOverrideConfig,
11
- defaultInheritProjectContext,
12
- defaultInheritSkills,
13
- defaultSystemPromptMode,
14
- discoverAgentsAll,
15
- removeBuiltinAgentOverride,
16
- saveBuiltinAgentOverride,
17
- type AgentConfig,
18
- type BuiltinAgentOverrideBase,
19
- type ChainConfig,
20
- } from "./agents.ts";
21
- import { serializeAgent } from "./agent-serializer.ts";
22
- import { TEMPLATE_ITEMS, type AgentTemplate, type TemplateItem } from "./agent-templates.ts";
23
- import { parseChain, serializeChain } from "./chain-serializer.ts";
24
- import { renderList, handleListInput, type ListAgent, type ListState, type ListAction } from "./agent-manager-list.ts";
25
- import { createParallelState, handleParallelInput, renderParallel, formatParallelTitle, type ParallelState, type AgentOption } from "./agent-manager-parallel.ts";
26
- import { renderDetail, handleDetailInput, renderTaskInput, type DetailState, type DetailAction, type LaunchToggleState } from "./agent-manager-detail.ts";
27
- import { renderChainDetail, handleChainDetailInput, type ChainDetailAction, type ChainDetailState } from "./agent-manager-chain-detail.ts";
28
- import { createEditState, handleEditInput, renderEdit, type EditField, type EditScreen, type EditState, type ModelInfo, type SkillInfo } from "./agent-manager-edit.ts";
29
- import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.ts";
30
- import type { TextEditorState } from "./text-editor.ts";
31
- import { loadRunsForAgent } from "./run-history.ts";
32
- import { pad, row, renderHeader, renderFooter } from "./render-helpers.ts";
33
- import { isParallelStep, type ChainStep } from "./settings.ts";
34
-
35
- export type ManagerResult =
36
- | { action: "launch"; agent: string; task: string; skipClarify?: boolean; fork?: boolean; background?: boolean }
37
- | { action: "chain"; agents: string[]; task: string; skipClarify?: boolean; fork?: boolean; background?: boolean }
38
- | { action: "parallel"; tasks: Array<{ agent: string; task: string }>; skipClarify?: boolean; fork?: boolean; background?: boolean; worktree?: boolean }
39
- | { action: "launch-chain"; chain: ChainConfig; task: string; skipClarify?: boolean; fork?: boolean; background?: boolean; worktree?: boolean }
40
- | undefined;
41
-
42
- export interface AgentData { builtin: AgentConfig[]; user: AgentConfig[]; project: AgentConfig[]; chains: ChainConfig[]; userDir: string; projectDir: string | null; userSettingsPath: string; projectSettingsPath: string | null; cwd: string; }
43
- type ManagerScreen = "list" | "detail" | "chain-detail" | "edit" | "edit-field" | "edit-prompt" | "task-input" | "confirm-delete" | "name-input" | "chain-edit" | "template-select" | "parallel-builder" | "override-scope";
44
- interface AgentEntry { id: string; kind: "agent"; config: AgentConfig; isNew: boolean; }
45
- interface ChainEntry { id: string; kind: "chain"; config: ChainConfig; }
46
- interface NameInputState { mode: "new-agent" | "clone-agent" | "clone-chain" | "new-chain"; editor: TextEditorState; scope: "user" | "project"; allowProject: boolean; sourceId?: string; template?: AgentTemplate; error?: string; }
47
- interface StatusMessage { text: string; type: "error" | "info"; }
48
- interface OverrideScopeState { selectedScope: "user" | "project"; allowProject: boolean; }
49
-
50
- const BUILTIN_OVERRIDE_FIELDS: EditField[] = ["model", "fallbackModels", "thinking", "systemPromptMode", "inheritProjectContext", "inheritSkills", "disabled", "tools", "skills", "prompt"];
51
-
52
- function cloneConfig(config: AgentConfig): AgentConfig {
53
- return {
54
- ...config,
55
- tools: config.tools ? [...config.tools] : undefined,
56
- mcpDirectTools: config.mcpDirectTools ? [...config.mcpDirectTools] : undefined,
57
- skills: config.skills ? [...config.skills] : undefined,
58
- fallbackModels: config.fallbackModels ? [...config.fallbackModels] : undefined,
59
- defaultReads: config.defaultReads ? [...config.defaultReads] : undefined,
60
- extraFields: config.extraFields ? { ...config.extraFields } : undefined,
61
- override: config.override
62
- ? {
63
- ...config.override,
64
- base: {
65
- ...config.override.base,
66
- disabled: config.override.base.disabled,
67
- fallbackModels: config.override.base.fallbackModels ? [...config.override.base.fallbackModels] : undefined,
68
- skills: config.override.base.skills ? [...config.override.base.skills] : undefined,
69
- tools: config.override.base.tools ? [...config.override.base.tools] : undefined,
70
- mcpDirectTools: config.override.base.mcpDirectTools ? [...config.override.base.mcpDirectTools] : undefined,
71
- },
72
- }
73
- : undefined,
74
- };
75
- }
76
- function cloneChainConfig(config: ChainConfig): ChainConfig {
77
- const steps = (config.steps as unknown as ChainStep[]).map((step) => {
78
- if (isParallelStep(step)) {
79
- return {
80
- ...step,
81
- parallel: step.parallel.map((task) => ({
82
- ...task,
83
- reads: Array.isArray(task.reads) ? [...task.reads] : task.reads,
84
- skill: Array.isArray(task.skill) ? [...task.skill] : task.skill,
85
- })),
86
- };
87
- }
88
- return {
89
- ...step,
90
- reads: Array.isArray(step.reads) ? [...step.reads] : step.reads,
91
- ...(Array.isArray((step as typeof step & { skills?: string[] | false }).skills) ? { skills: [...(step as typeof step & { skills: string[] }).skills] } : { skills: (step as typeof step & { skills?: false }).skills }),
92
- ...(Array.isArray(step.skill) ? { skill: [...step.skill] } : { skill: step.skill }),
93
- };
94
- });
95
- return { ...config, steps: steps as unknown as ChainConfig["steps"], extraFields: config.extraFields ? { ...config.extraFields } : undefined };
96
- }
97
- function slugTemplateName(name: string): string { return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); }
98
- function nextSelectableIndex(items: TemplateItem[], current: number, direction: 1 | -1): number { let next = current + direction; while (next >= 0 && next < items.length && items[next]!.type === "separator") next += direction; if (next < 0 || next >= items.length) return current; return next; }
99
- const CHAIN_EDIT_VIEWPORT = 10;
100
-
101
- export class AgentManagerComponent implements Component {
102
- private overlayWidth = 84;
103
- private screen: ManagerScreen = "list";
104
- private agents: AgentEntry[] = [];
105
- private chains: ChainEntry[] = [];
106
- private listState: ListState = { cursor: 0, scrollOffset: 0, filterQuery: "", selected: [] };
107
- private detailState: DetailState = { resolved: true, scrollOffset: 0 };
108
- private chainDetailState: ChainDetailState = { scrollOffset: 0 };
109
- private editState: EditState | null = null;
110
- private currentAgentId: string | null = null;
111
- private currentChainId: string | null = null;
112
- private confirmDeleteId: string | null = null;
113
- private nameInputState: NameInputState | null = null;
114
- private chainEditState: { editor: TextEditorState; error?: string } | null = null;
115
- private taskEditor: TextEditorState = createEditorState();
116
- private skipClarify = false;
117
- private launchFork = false;
118
- private launchBackground = false;
119
- private launchWorktree = false;
120
- private chainAgentIds: string[] = [];
121
- private chainLaunchId: string | null = null;
122
- private parallelMode = false;
123
- private parallelState: ParallelState | null = null;
124
- private taskBackScreen: ManagerScreen = "list";
125
- private templateCursor = 0;
126
- private statusMessage?: StatusMessage;
127
- private overrideScopeState: OverrideScopeState | null = null;
128
- private builtinOverrideScope: "user" | "project" | null = null;
129
- private nextId = 1;
130
- private tui: TUI;
131
- private theme: Theme;
132
- private agentData: AgentData;
133
- private models: ModelInfo[];
134
- private skills: SkillInfo[];
135
- private done: (result: ManagerResult) => void;
136
-
137
- constructor(tui: TUI, theme: Theme, agentData: AgentData, models: ModelInfo[], skills: SkillInfo[], done: (result: ManagerResult) => void) {
138
- this.tui = tui;
139
- this.theme = theme;
140
- this.agentData = agentData;
141
- this.models = models;
142
- this.skills = skills;
143
- this.done = done;
144
- this.loadEntries();
145
- }
146
-
147
- private loadEntries(): void {
148
- const overridden = new Set([...this.agentData.user, ...this.agentData.project].map((c) => c.name));
149
- const agents: AgentEntry[] = []; for (const config of this.agentData.builtin) { if (!overridden.has(config.name)) agents.push({ id: `a${this.nextId++}`, kind: "agent", config: cloneConfig(config), isNew: false }); } for (const config of this.agentData.user) agents.push({ id: `a${this.nextId++}`, kind: "agent", config: cloneConfig(config), isNew: false }); for (const config of this.agentData.project) agents.push({ id: `a${this.nextId++}`, kind: "agent", config: cloneConfig(config), isNew: false }); this.agents = agents;
150
- const chains: ChainEntry[] = []; for (const config of this.agentData.chains) chains.push({ id: `c${this.nextId++}`, kind: "chain", config: cloneChainConfig(config) }); this.chains = chains;
151
- }
152
-
153
- private getAgentEntry(id: string | null): AgentEntry | undefined { if (!id) return undefined; return this.agents.find((entry) => entry.id === id); }
154
- private getChainEntry(id: string | null): ChainEntry | undefined { if (!id) return undefined; return this.chains.find((entry) => entry.id === id); }
155
- private listAgents(): ListAgent[] { const a = this.agents.map((entry) => ({ id: entry.id, name: entry.config.name, description: entry.config.description, model: entry.config.model, source: entry.config.source, overrideScope: entry.config.override?.scope, disabled: entry.config.disabled, kind: "agent" as const })); const c = this.chains.map((entry) => ({ id: entry.id, name: entry.config.name, description: entry.config.description, source: entry.config.source, kind: "chain" as const, stepCount: entry.config.steps.length })); return [...a, ...c]; }
156
- private clearStatus(): void { this.statusMessage = undefined; }
157
- private disabledAgentEntries(ids: string[]): AgentEntry[] { return ids.map((id) => this.getAgentEntry(id)).filter((entry): entry is AgentEntry => Boolean(entry?.config.disabled)); }
158
-
159
- private resolveBuiltinOverrideBase(entry: AgentEntry): BuiltinAgentOverrideBase {
160
- if (entry.config.override) return entry.config.override.base;
161
- return {
162
- model: entry.config.model,
163
- fallbackModels: entry.config.fallbackModels ? [...entry.config.fallbackModels] : undefined,
164
- thinking: entry.config.thinking,
165
- systemPromptMode: entry.config.systemPromptMode,
166
- inheritProjectContext: entry.config.inheritProjectContext,
167
- inheritSkills: entry.config.inheritSkills,
168
- disabled: entry.config.disabled,
169
- systemPrompt: entry.config.systemPrompt,
170
- skills: entry.config.skills ? [...entry.config.skills] : undefined,
171
- tools: entry.config.tools ? [...entry.config.tools] : undefined,
172
- mcpDirectTools: entry.config.mcpDirectTools ? [...entry.config.mcpDirectTools] : undefined,
173
- };
174
- }
175
-
176
- private refreshAgentData(agentName?: string, chainName?: string): void {
177
- this.agentData = { ...discoverAgentsAll(this.agentData.cwd), cwd: this.agentData.cwd };
178
- this.nextId = 1;
179
- this.loadEntries();
180
- if (agentName) {
181
- const entry = this.agents.find((candidate) => candidate.config.name === agentName);
182
- this.currentAgentId = entry?.id ?? null;
183
- }
184
- if (chainName) {
185
- const entry = this.chains.find((candidate) => candidate.config.name === chainName);
186
- this.currentChainId = entry?.id ?? null;
187
- }
188
- }
189
-
190
- private removeAgentEntry(entry: AgentEntry): void { this.agents = this.agents.filter((e) => e.id !== entry.id); this.listState.selected = this.listState.selected.filter((id) => id !== entry.id); }
191
- private removeChainEntry(entry: ChainEntry): void { this.chains = this.chains.filter((e) => e.id !== entry.id); }
192
-
193
- private enterDetail(entry: AgentEntry): void { this.currentAgentId = entry.id; this.detailState = { resolved: true, scrollOffset: 0, recentRuns: loadRunsForAgent(entry.config.name).slice(0, 5) }; this.screen = "detail"; }
194
- private enterChainDetail(entry: ChainEntry): void { this.currentChainId = entry.id; this.chainDetailState = { scrollOffset: 0 }; this.screen = "chain-detail"; }
195
- private enterEdit(entry: AgentEntry): void { this.currentAgentId = entry.id; this.builtinOverrideScope = null; this.editState = createEditState(entry.config, entry.isNew, this.models, this.skills); this.screen = "edit"; }
196
- private enterBuiltinOverrideScope(entry: AgentEntry): void {
197
- this.currentAgentId = entry.id;
198
- this.overrideScopeState = { selectedScope: this.agentData.projectSettingsPath ? "project" : "user", allowProject: Boolean(this.agentData.projectSettingsPath) };
199
- this.screen = "override-scope";
200
- }
201
- private enterBuiltinOverrideEdit(entry: AgentEntry, scope: "user" | "project"): void {
202
- this.currentAgentId = entry.id;
203
- this.builtinOverrideScope = scope;
204
- this.editState = createEditState(entry.config, false, this.models, this.skills, {
205
- fields: BUILTIN_OVERRIDE_FIELDS,
206
- title: `Builtin Override: ${entry.config.name} [${scope}]`,
207
- overrideBase: this.resolveBuiltinOverrideBase(entry),
208
- });
209
- this.screen = "edit";
210
- }
211
- private enterParallelBuilder(ids: string[]): void {
212
- const names = ids.map((id) => this.getAgentEntry(id)?.config.name).filter((n): n is string => Boolean(n));
213
- if (names.length === 0) return;
214
- this.parallelState = createParallelState(names);
215
- this.screen = "parallel-builder";
216
- }
217
- private resetLaunchToggles(): void { this.launchFork = false; this.launchBackground = false; this.launchWorktree = false; }
218
- private enterParallelTaskInput(): void {
219
- this.chainAgentIds = [];
220
- this.chainLaunchId = null;
221
- this.parallelMode = true;
222
- this.taskBackScreen = "parallel-builder";
223
- this.taskEditor = createEditorState();
224
- this.skipClarify = true;
225
- this.resetLaunchToggles();
226
- this.screen = "task-input";
227
- }
228
- private enterTaskInput(ids: string[], backScreen: ManagerScreen = "list"): void {
229
- this.chainAgentIds = ids; this.chainLaunchId = null; this.parallelMode = false; this.taskBackScreen = backScreen; this.taskEditor = createEditorState(); this.skipClarify = true; this.resetLaunchToggles(); this.screen = "task-input";
230
- }
231
- private enterSavedChainLaunch(entry: ChainEntry): void { this.chainLaunchId = entry.id; this.chainAgentIds = []; this.parallelMode = false; this.taskBackScreen = "chain-detail"; this.taskEditor = createEditorState(); this.skipClarify = true; this.resetLaunchToggles(); this.screen = "task-input"; }
232
- private enterTemplateSelect(): void { this.templateCursor = TEMPLATE_ITEMS.findIndex((item) => item.type !== "separator"); if (this.templateCursor < 0) this.templateCursor = 0; this.screen = "template-select"; }
233
-
234
- private enterChainEdit(entry: ChainEntry): void {
235
- try { const content = fs.readFileSync(entry.config.filePath, "utf-8"); this.currentChainId = entry.id; this.chainEditState = { editor: createEditorState(content) }; this.screen = "chain-edit"; }
236
- catch (err) { this.statusMessage = { text: err instanceof Error ? err.message : "Failed to load chain file.", type: "error" }; this.screen = "list"; }
237
- }
238
-
239
- private enterNameInput(mode: NameInputState["mode"], sourceId?: string, template?: AgentTemplate): void {
240
- const allowProject = Boolean(this.agentData.projectDir); let initial = ""; let scope: "user" | "project" = "user";
241
- if (mode === "clone-agent" && sourceId) { const entry = this.getAgentEntry(sourceId); if (entry) { initial = `${entry.config.name}-copy`; scope = entry.config.source === "project" ? "project" : "user"; } }
242
- if (mode === "clone-chain" && sourceId) { const entry = this.getChainEntry(sourceId); if (entry) { initial = `${entry.config.name}-copy`; scope = entry.config.source === "project" ? "project" : "user"; } }
243
- if (mode === "new-agent" && template && template.name !== "Blank") initial = slugTemplateName(template.name);
244
- this.nameInputState = { mode, editor: createEditorState(initial), scope, allowProject, sourceId, template }; this.screen = "name-input";
245
- }
246
-
247
- private saveEdit(): boolean {
248
- const edit = this.editState; if (!edit) return false; const entry = this.getAgentEntry(this.currentAgentId); if (!entry) return false;
249
- if (entry.config.source === "builtin") {
250
- const scope = entry.config.override?.scope ?? this.builtinOverrideScope;
251
- if (!scope) { edit.error = "Choose where to store the override first."; return false; }
252
- try {
253
- const override = buildBuiltinOverrideConfig(this.resolveBuiltinOverrideBase(entry), edit.draft);
254
- if (override) {
255
- saveBuiltinAgentOverride(this.agentData.cwd, entry.config.name, scope, override);
256
- } else {
257
- removeBuiltinAgentOverride(this.agentData.cwd, entry.config.name, scope);
258
- }
259
- this.refreshAgentData(entry.config.name);
260
- this.builtinOverrideScope = null;
261
- this.editState = null;
262
- const refreshed = this.getAgentEntry(this.currentAgentId);
263
- if (refreshed) this.enterDetail(refreshed);
264
- return true;
265
- } catch (err) {
266
- edit.error = err instanceof Error ? err.message : "Failed to save builtin override.";
267
- return false;
268
- }
269
- }
270
- if (!edit.draft.name || !edit.draft.description) { edit.error = "Name and description are required."; return false; }
271
- let filePath = entry.config.filePath;
272
- if (entry.isNew) {
273
- const dir = edit.draft.source === "project" ? this.agentData.projectDir : this.agentData.userDir;
274
- if (!dir) { edit.error = "Project agents directory not found."; return false; }
275
- filePath = path.join(dir, `${edit.draft.name}.md`);
276
- if (fs.existsSync(filePath)) { edit.error = "An agent with that name already exists."; return false; }
277
- fs.mkdirSync(dir, { recursive: true });
278
- } else if (edit.draft.name !== entry.config.name) {
279
- const nextPath = path.join(path.dirname(filePath), `${edit.draft.name}.md`);
280
- if (nextPath !== filePath && fs.existsSync(nextPath)) {
281
- edit.error = "An agent with that name already exists.";
282
- return false;
283
- }
284
- if (nextPath !== filePath) {
285
- fs.renameSync(filePath, nextPath);
286
- filePath = nextPath;
287
- }
288
- }
289
- try { const toSave: AgentConfig = { ...edit.draft, filePath }; fs.writeFileSync(filePath, serializeAgent(toSave), "utf-8"); entry.config = cloneConfig(toSave); entry.isNew = false; edit.error = undefined; return true; }
290
- catch (err) { edit.error = err instanceof Error ? err.message : "Failed to save agent."; return false; }
291
- }
292
-
293
- private removeBuiltinOverride(): boolean {
294
- const edit = this.editState; if (!edit) return false; const entry = this.getAgentEntry(this.currentAgentId); if (!entry || entry.config.source !== "builtin") return false;
295
- const scope = entry.config.override?.scope ?? this.builtinOverrideScope;
296
- if (!scope) { edit.error = "No builtin override to remove."; return false; }
297
- try {
298
- removeBuiltinAgentOverride(this.agentData.cwd, entry.config.name, scope);
299
- this.refreshAgentData(entry.config.name);
300
- this.builtinOverrideScope = null;
301
- this.editState = null;
302
- const refreshed = this.getAgentEntry(this.currentAgentId);
303
- if (refreshed) this.enterDetail(refreshed);
304
- return true;
305
- } catch (err) {
306
- edit.error = err instanceof Error ? err.message : "Failed to remove builtin override.";
307
- return false;
308
- }
309
- }
310
-
311
- private saveChainEdit(): boolean {
312
- const state = this.chainEditState; const entry = this.getChainEntry(this.currentChainId); if (!state || !entry) return false;
313
- try { const parsed = parseChain(state.editor.buffer, entry.config.source, entry.config.filePath); fs.writeFileSync(entry.config.filePath, serializeChain(parsed), "utf-8"); entry.config = parsed; state.error = undefined; return true; }
314
- catch (err) { state.error = err instanceof Error ? err.message : "Failed to save chain."; return false; }
315
- }
316
-
317
- private canToggleLaunchWorktree(): boolean {
318
- if (this.parallelMode && this.parallelState) return true;
319
- if (!this.chainLaunchId) return false;
320
- const chainEntry = this.getChainEntry(this.chainLaunchId);
321
- return chainEntry ? (chainEntry.config.steps as unknown as ChainStep[]).some(isParallelStep) : false;
322
- }
323
- private launchFlags(): { fork?: boolean; background?: boolean; worktree?: boolean } {
324
- return {
325
- ...(this.launchFork ? { fork: true } : {}),
326
- ...(this.launchBackground ? { background: true } : {}),
327
- ...(this.launchWorktree && this.canToggleLaunchWorktree() ? { worktree: true } : {}),
328
- };
329
- }
330
- private launchToggleState(): LaunchToggleState {
331
- return {
332
- fork: this.launchFork,
333
- background: this.launchBackground,
334
- ...(this.canToggleLaunchWorktree() ? { worktree: this.launchWorktree } : {}),
335
- };
336
- }
337
-
338
- private handleTemplateSelectInput(data: string): void {
339
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { this.screen = "list"; this.tui.requestRender(); return; }
340
- if (matchesKey(data, "up")) { this.templateCursor = nextSelectableIndex(TEMPLATE_ITEMS, this.templateCursor, -1); this.tui.requestRender(); return; }
341
- if (matchesKey(data, "down")) { this.templateCursor = nextSelectableIndex(TEMPLATE_ITEMS, this.templateCursor, 1); this.tui.requestRender(); return; }
342
- if (matchesKey(data, "return")) {
343
- const item = TEMPLATE_ITEMS[this.templateCursor];
344
- if (!item || item.type === "separator") return;
345
- if (item.type === "agent") this.enterNameInput("new-agent", undefined, { name: item.name, config: item.config });
346
- else if (item.type === "chain") this.enterNameInput("new-chain");
347
- this.tui.requestRender();
348
- }
349
- }
350
-
351
- private handleOverrideScopeInput(data: string): void {
352
- const state = this.overrideScopeState;
353
- const entry = this.getAgentEntry(this.currentAgentId);
354
- if (!state || !entry) {
355
- this.screen = "detail";
356
- this.tui.requestRender();
357
- return;
358
- }
359
-
360
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
361
- this.overrideScopeState = null;
362
- this.enterDetail(entry);
363
- this.tui.requestRender();
364
- return;
365
- }
366
-
367
- if (matchesKey(data, "tab") || matchesKey(data, "up") || matchesKey(data, "down")) {
368
- if (state.allowProject) state.selectedScope = state.selectedScope === "user" ? "project" : "user";
369
- this.tui.requestRender();
370
- return;
371
- }
372
-
373
- if (data === "u") {
374
- state.selectedScope = "user";
375
- this.tui.requestRender();
376
- return;
377
- }
378
-
379
- if (data === "p" && state.allowProject) {
380
- state.selectedScope = "project";
381
- this.tui.requestRender();
382
- return;
383
- }
384
-
385
- if (!matchesKey(data, "return")) return;
386
- this.overrideScopeState = null;
387
- this.enterBuiltinOverrideEdit(entry, state.selectedScope);
388
- this.tui.requestRender();
389
- }
390
-
391
- private handleNameInput(data: string): void {
392
- const state = this.nameInputState; if (!state) return; state.error = undefined;
393
- const canToggleScope = state.allowProject;
394
- if (matchesKey(data, "tab")) { if (canToggleScope) { state.scope = state.scope === "user" ? "project" : "user"; this.tui.requestRender(); } return; }
395
- const innerW = this.overlayWidth - 2; const boxInnerWidth = Math.max(10, innerW - 4);
396
- const nextState = handleEditorInput(state.editor, data, boxInnerWidth); if (nextState) { state.editor = nextState; this.tui.requestRender(); return; }
397
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { this.nameInputState = null; this.screen = "list"; this.tui.requestRender(); return; }
398
- if (!matchesKey(data, "return")) return;
399
- const name = state.editor.buffer.trim(); if (!name) { state.error = "Name is required."; this.tui.requestRender(); return; }
400
-
401
- if (state.mode === "clone-chain" && state.sourceId) {
402
- const sourceEntry = this.getChainEntry(state.sourceId); if (!sourceEntry) { this.screen = "list"; this.tui.requestRender(); return; }
403
- const dir = state.scope === "project" ? this.agentData.projectDir : this.agentData.userDir;
404
- if (!dir) { state.error = "Project agents directory not found."; this.tui.requestRender(); return; }
405
- const filePath = path.join(dir, `${name}.chain.md`); if (fs.existsSync(filePath)) { state.error = "A chain with that name already exists."; this.tui.requestRender(); return; }
406
- try { const cloned = cloneChainConfig({ ...sourceEntry.config, name, source: state.scope, filePath }); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(filePath, serializeChain(cloned), "utf-8"); const added: ChainEntry = { id: `c${this.nextId++}`, kind: "chain", config: cloned }; this.chains.push(added); this.nameInputState = null; this.enterChainDetail(added); this.tui.requestRender(); return; }
407
- catch (err) { state.error = err instanceof Error ? err.message : "Failed to clone chain."; this.tui.requestRender(); return; }
408
- }
409
- if (state.mode === "new-chain") {
410
- const dir = state.scope === "project" ? this.agentData.projectDir : this.agentData.userDir;
411
- if (!dir) { state.error = "Directory not found."; this.tui.requestRender(); return; }
412
- const filePath = path.join(dir, `${name}.chain.md`); if (fs.existsSync(filePath)) { state.error = "A chain with that name already exists."; this.tui.requestRender(); return; }
413
- const config: ChainConfig = { name, description: "Describe this chain", source: state.scope, filePath, steps: [{ agent: "agent-name", task: "{task}" }] };
414
- try { fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(filePath, serializeChain(config), "utf-8"); const entry: ChainEntry = { id: `c${this.nextId++}`, kind: "chain", config }; this.chains.push(entry); this.nameInputState = null; this.enterChainEdit(entry); }
415
- catch (err) { state.error = err instanceof Error ? err.message : "Failed to create chain."; }
416
- this.tui.requestRender(); return;
417
- }
418
-
419
- let baseConfig: AgentConfig;
420
- if (state.mode === "clone-agent" && state.sourceId) {
421
- const sourceEntry = this.getAgentEntry(state.sourceId); if (!sourceEntry) { this.screen = "list"; this.tui.requestRender(); return; }
422
- baseConfig = cloneConfig(sourceEntry.config);
423
- } else {
424
- const templateConfig = state.template?.config ?? {};
425
- baseConfig = {
426
- name,
427
- description: "Describe this agent",
428
- systemPrompt: "",
429
- systemPromptMode: defaultSystemPromptMode(name),
430
- inheritProjectContext: defaultInheritProjectContext(name),
431
- inheritSkills: defaultInheritSkills(),
432
- source: state.scope,
433
- filePath: "",
434
- ...templateConfig,
435
- };
436
- }
437
- const dir = state.scope === "project" ? this.agentData.projectDir : this.agentData.userDir;
438
- if (!dir) { state.error = "Project agents directory not found."; this.tui.requestRender(); return; }
439
- const filePath = path.join(dir, `${name}.md`); const config: AgentConfig = { ...baseConfig, name, source: state.scope, filePath };
440
- const entry: AgentEntry = { id: `a${this.nextId++}`, kind: "agent", config, isNew: true };
441
- this.agents.push(entry); this.nameInputState = null; this.enterEdit(entry); this.tui.requestRender();
442
- }
443
-
444
- private renderNameInput(w: number): string[] {
445
- const state = this.nameInputState; if (!state) return [];
446
- const lines: string[] = []; const title = state.mode === "new-agent" ? "New Agent" : state.mode === "clone-agent" ? "Clone Agent" : state.mode === "new-chain" ? "New Chain" : "Clone Chain";
447
- lines.push(renderHeader(` ${title} `, w, this.theme)); lines.push(row("", w, this.theme)); lines.push(row(` ${this.theme.fg("dim", "Name:")}`, w, this.theme));
448
- const innerW = w - 2; const boxInnerWidth = Math.max(10, innerW - 4); const top = `┌${"─".repeat(boxInnerWidth)}┐`; const bottom = `└${"─".repeat(boxInnerWidth)}┘`;
449
- lines.push(row(` ${top}`, w, this.theme));
450
- const editorState = { ...state.editor }; const wrapped = wrapText(editorState.buffer, boxInnerWidth); const cursorPos = getCursorDisplayPos(editorState.cursor, wrapped.starts); editorState.viewportOffset = ensureCursorVisible(cursorPos.line, 1, editorState.viewportOffset); const editorLine = renderEditor(editorState, boxInnerWidth, 1)[0] ?? "";
451
- lines.push(row(` │${pad(editorLine, boxInnerWidth)}│`, w, this.theme)); lines.push(row(` ${bottom}`, w, this.theme));
452
- if (state.mode === "new-agent" && state.template) lines.push(row(` ${this.theme.fg("dim", "Template:")} ${state.template.name}`, w, this.theme));
453
- else if (state.mode === "new-chain") lines.push(row(` ${this.theme.fg("dim", "Creates a .chain.md configuration file")}`, w, this.theme));
454
- else lines.push(row("", w, this.theme));
455
- if (state.allowProject) { const scopeLabel = state.scope === "user" ? "[user]" : "[proj]"; lines.push(row(` ${this.theme.fg("dim", "Scope:")} ${scopeLabel} ${this.theme.fg("dim", "[tab] toggle")}`, w, this.theme)); }
456
- else lines.push(row("", w, this.theme));
457
- if (state.error) lines.push(row(` ${this.theme.fg("error", state.error)}`, w, this.theme)); else lines.push(row("", w, this.theme));
458
- lines.push(renderFooter(" [enter] continue [esc] cancel ", w, this.theme)); return lines;
459
- }
460
-
461
- private renderOverrideScope(w: number): string[] {
462
- const state = this.overrideScopeState;
463
- const entry = this.getAgentEntry(this.currentAgentId);
464
- if (!state || !entry) return [];
465
- const lines: string[] = [];
466
- lines.push(renderHeader(` Create Override: ${entry.config.name} `, w, this.theme));
467
- lines.push(row("", w, this.theme));
468
- lines.push(row(` ${this.theme.fg("dim", "Where should this builtin override live?")}`, w, this.theme));
469
- lines.push(row("", w, this.theme));
470
- const userLine = state.selectedScope === "user" ? this.theme.fg("accent", "▸ user") : " user";
471
- lines.push(row(` ${userLine}${this.theme.fg("dim", ` ${this.agentData.userSettingsPath}`)}`, w, this.theme));
472
- if (state.allowProject) {
473
- const projectPath = this.agentData.projectSettingsPath ?? ".pi/settings.json";
474
- const projectLine = state.selectedScope === "project" ? this.theme.fg("accent", "▸ project") : " project";
475
- lines.push(row(` ${projectLine}${this.theme.fg("dim", ` ${projectPath}`)}`, w, this.theme));
476
- }
477
- while (lines.length < 8) lines.push(row("", w, this.theme));
478
- lines.push(renderFooter(" [enter] continue [↑↓/tab] choose [esc] cancel ", w, this.theme));
479
- return lines;
480
- }
481
-
482
- private renderTemplateSelect(w: number): string[] {
483
- const lines: string[] = []; lines.push(renderHeader(" Select Template ", w, this.theme)); lines.push(row("", w, this.theme));
484
- const innerW = w - 2; const viewport = 12; const start = Math.max(0, Math.min(this.templateCursor - Math.floor(viewport / 2), Math.max(0, TEMPLATE_ITEMS.length - viewport))); const visible = TEMPLATE_ITEMS.slice(start, start + viewport);
485
- for (let i = 0; i < visible.length; i++) {
486
- const idx = start + i; const item = visible[i]!;
487
- if (item.type === "separator") {
488
- const label = `── ${item.label} `;
489
- lines.push(row(` ${this.theme.fg("dim", label + "─".repeat(Math.max(0, innerW - 1 - visibleWidth(label))))}`, w, this.theme));
490
- } else {
491
- const isCursor = idx === this.templateCursor; const cursor = isCursor ? this.theme.fg("accent", "▸") : " ";
492
- const name = isCursor ? this.theme.fg("accent", item.name) : item.name; const desc = item.type === "agent" ? (item.config.description ?? "") : item.description;
493
- lines.push(row(` ${cursor} ${pad(name, 16)} ${this.theme.fg("dim", truncateToWidth(desc, Math.max(0, innerW - 24)))}`, w, this.theme));
494
- }
495
- }
496
- for (let i = visible.length; i < viewport; i++) lines.push(row("", w, this.theme));
497
- const selected = TEMPLATE_ITEMS[this.templateCursor]; const info = selected ? selected.type === "separator" ? selected.label : selected.name : "";
498
- lines.push(row(` ${this.theme.fg("dim", info)}`, w, this.theme));
499
- lines.push(renderFooter(" [enter] select [esc] cancel [↑↓] navigate ", w, this.theme)); return lines;
500
- }
501
-
502
- private renderConfirmDelete(w: number): string[] {
503
- const agent = this.getAgentEntry(this.confirmDeleteId); const chain = this.getChainEntry(this.confirmDeleteId); const name = agent?.config.name ?? chain?.config.name ?? ""; const filePath = agent?.config.filePath ?? chain?.config.filePath ?? "";
504
- const lines: string[] = []; lines.push(renderHeader(` Delete "${name}"? `, w, this.theme)); lines.push(row("", w, this.theme)); const label = "File: "; const maxPath = Math.max(0, w - 2 - label.length - 1); const trimmed = truncateToWidth(filePath, maxPath);
505
- lines.push(row(` ${label}${trimmed}`, w, this.theme)); lines.push(row("", w, this.theme)); lines.push(row(` ${this.theme.fg("warning", "This cannot be undone.")}`, w, this.theme)); lines.push(row("", w, this.theme)); lines.push(renderFooter(" [y] confirm [n / esc] cancel ", w, this.theme)); return lines;
506
- }
507
-
508
- private renderChainEdit(w: number): string[] {
509
- const state = this.chainEditState; const entry = this.getChainEntry(this.currentChainId); if (!state || !entry) return [];
510
- const lines: string[] = []; lines.push(renderHeader(` Edit Chain: ${entry.config.name} `, w, this.theme)); lines.push(row("", w, this.theme));
511
- const innerW = w - 2; const boxInnerWidth = Math.max(10, innerW - 4); const top = `┌${"─".repeat(boxInnerWidth)}┐`; const bottom = `└${"─".repeat(boxInnerWidth)}┘`;
512
- lines.push(row(` ${top}`, w, this.theme));
513
- const editorState = { ...state.editor }; const wrapped = wrapText(editorState.buffer, boxInnerWidth); const cursorPos = getCursorDisplayPos(editorState.cursor, wrapped.starts); editorState.viewportOffset = ensureCursorVisible(cursorPos.line, CHAIN_EDIT_VIEWPORT, editorState.viewportOffset); const editorLines = renderEditor(editorState, boxInnerWidth, CHAIN_EDIT_VIEWPORT);
514
- for (const line of editorLines) lines.push(row(` │${pad(line, boxInnerWidth)}│`, w, this.theme));
515
- lines.push(row(` ${bottom}`, w, this.theme)); if (state.error) lines.push(row(` ${this.theme.fg("error", state.error)}`, w, this.theme)); else lines.push(row("", w, this.theme)); lines.push(renderFooter(" [ctrl+s] save [esc] back ", w, this.theme)); return lines;
516
- }
517
-
518
- handleInput(data: string): void {
519
- if (this.screen === "list" && this.statusMessage) this.clearStatus();
520
- if (this.screen.startsWith("edit") && this.editState?.error) this.editState.error = undefined;
521
- switch (this.screen) {
522
- case "list": { const action = handleListInput(this.listState, this.listAgents(), data); if (action) this.handleListAction(action); this.tui.requestRender(); return; }
523
- case "template-select": this.handleTemplateSelectInput(data); return;
524
- case "override-scope": this.handleOverrideScopeInput(data); return;
525
- case "detail": {
526
- const entry = this.getAgentEntry(this.currentAgentId); if (!entry) { this.screen = "list"; this.tui.requestRender(); return; }
527
- const action = handleDetailInput(this.detailState, data); if (action) this.handleDetailAction(action, entry); this.tui.requestRender(); return;
528
- }
529
- case "chain-detail": {
530
- const entry = this.getChainEntry(this.currentChainId); if (!entry) { this.screen = "list"; this.tui.requestRender(); return; }
531
- const action = handleChainDetailInput(this.chainDetailState, data); if (action) this.handleChainDetailAction(action, entry); this.tui.requestRender(); return;
532
- }
533
- case "parallel-builder": {
534
- if (!this.parallelState) { this.screen = "list"; this.tui.requestRender(); return; }
535
- const agentOptions: AgentOption[] = this.agents.map((e) => ({ name: e.config.name, description: e.config.description, model: e.config.model }));
536
- const pAction = handleParallelInput(this.parallelState, agentOptions, data, this.overlayWidth);
537
- if (pAction?.type === "proceed") {
538
- this.enterParallelTaskInput();
539
- } else if (pAction?.type === "back") {
540
- this.parallelState = null;
541
- this.parallelMode = false;
542
- this.screen = "list";
543
- }
544
- this.tui.requestRender();
545
- return;
546
- }
547
- case "task-input": {
548
- if (matchesKey(data, "tab")) { this.skipClarify = !this.skipClarify; this.tui.requestRender(); return; }
549
- if (matchesKey(data, "ctrl+f")) { this.launchFork = !this.launchFork; this.tui.requestRender(); return; }
550
- if (matchesKey(data, "ctrl+b")) { this.launchBackground = !this.launchBackground; this.tui.requestRender(); return; }
551
- if (matchesKey(data, "ctrl+w") && this.canToggleLaunchWorktree()) { this.launchWorktree = !this.launchWorktree; this.tui.requestRender(); return; }
552
- const innerW = this.overlayWidth - 2; const boxInnerWidth = Math.max(10, innerW - 4); const nextState = handleEditorInput(this.taskEditor, data, boxInnerWidth);
553
- if (nextState) { this.taskEditor = nextState; this.tui.requestRender(); return; }
554
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { this.screen = this.taskBackScreen; this.tui.requestRender(); return; }
555
- if (matchesKey(data, "return")) {
556
- if (this.chainLaunchId) {
557
- const chainEntry = this.getChainEntry(this.chainLaunchId); if (!chainEntry) { this.screen = "list"; this.tui.requestRender(); return; }
558
- this.done({ action: "launch-chain", chain: cloneChainConfig(chainEntry.config), task: this.taskEditor.buffer, skipClarify: this.skipClarify, ...this.launchFlags() }); return;
559
- } else if (this.parallelMode && this.parallelState) {
560
- const sharedTask = this.taskEditor.buffer;
561
- const tasks = this.parallelState.slots.map((slot) => ({ agent: slot.agentName, task: slot.customTask || sharedTask }));
562
- this.done({ action: "parallel", tasks, skipClarify: this.skipClarify, ...this.launchFlags() }); return;
563
- }
564
- if (this.chainAgentIds.length > 1) {
565
- const agents = this.chainAgentIds
566
- .map((id) => this.getAgentEntry(id)?.config.name)
567
- .filter((name): name is string => Boolean(name));
568
- if (agents.length !== this.chainAgentIds.length) { this.screen = "list"; this.tui.requestRender(); return; }
569
- this.done({ action: "chain", agents, task: this.taskEditor.buffer, skipClarify: this.skipClarify, ...this.launchFlags() }); return;
570
- }
571
- const name = this.getAgentEntry(this.chainAgentIds[0] ?? null)?.config.name;
572
- if (!name) { this.screen = "list"; this.tui.requestRender(); return; }
573
- this.done({ action: "launch", agent: name, task: this.taskEditor.buffer, skipClarify: this.skipClarify, ...this.launchFlags() }); return;
574
- }
575
- return;
576
- }
577
- case "confirm-delete": {
578
- const agent = this.getAgentEntry(this.confirmDeleteId); const chain = this.getChainEntry(this.confirmDeleteId); if (!agent && !chain) { this.screen = "list"; this.tui.requestRender(); return; }
579
- if (data === "y" || data === "Y") {
580
- try { if (agent) { fs.unlinkSync(agent.config.filePath); this.removeAgentEntry(agent); } else if (chain) { fs.unlinkSync(chain.config.filePath); this.removeChainEntry(chain); } this.confirmDeleteId = null; this.screen = "list"; this.tui.requestRender(); return; }
581
- catch (err) { this.statusMessage = { text: err instanceof Error ? err.message : "Failed to delete item.", type: "error" }; this.confirmDeleteId = null; this.screen = "list"; this.tui.requestRender(); return; }
582
- }
583
- if (data === "n" || data === "N" || matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { this.confirmDeleteId = null; this.screen = "list"; this.tui.requestRender(); return; }
584
- return;
585
- }
586
- case "name-input": this.handleNameInput(data); return;
587
- case "chain-edit": {
588
- if (!this.chainEditState) { this.screen = "chain-detail"; this.tui.requestRender(); return; }
589
- if (matchesKey(data, "ctrl+s")) { this.saveChainEdit(); this.tui.requestRender(); return; }
590
- const innerW = this.overlayWidth - 2; const boxInnerWidth = Math.max(10, innerW - 4);
591
- if (matchesKey(data, "shift+up") || matchesKey(data, "pageup") || matchesKey(data, "shift+down") || matchesKey(data, "pagedown")) {
592
- const { lines: wrapped, starts } = wrapText(this.chainEditState.editor.buffer, boxInnerWidth); const cursorPos = getCursorDisplayPos(this.chainEditState.editor.cursor, starts);
593
- const dir = matchesKey(data, "shift+up") || matchesKey(data, "pageup") ? -1 : 1; const targetLine = Math.max(0, Math.min(wrapped.length - 1, cursorPos.line + dir * CHAIN_EDIT_VIEWPORT));
594
- const targetCol = Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0); this.chainEditState.editor = { ...this.chainEditState.editor, cursor: starts[targetLine] + targetCol }; this.tui.requestRender(); return;
595
- }
596
- const nextState = handleEditorInput(this.chainEditState.editor, data, boxInnerWidth, { multiLine: true });
597
- if (nextState) { this.chainEditState.editor = nextState; this.tui.requestRender(); return; }
598
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { this.chainEditState = null; this.screen = "chain-detail"; this.tui.requestRender(); return; }
599
- return;
600
- }
601
- case "edit": case "edit-field": case "edit-prompt": {
602
- if (!this.editState) { this.screen = "list"; this.tui.requestRender(); return; }
603
- const result = handleEditInput(this.screen as EditScreen, this.editState, data, this.overlayWidth, this.models, this.skills);
604
- if (result?.action === "discard") { this.handleEditDiscard(); return; }
605
- if (result?.action === "delete") { this.removeBuiltinOverride(); this.tui.requestRender(); return; }
606
- if (result?.action === "save") { const ok = this.saveEdit(); if (ok) { const entry = this.getAgentEntry(this.currentAgentId); if (entry) this.enterDetail(entry); } this.tui.requestRender(); return; }
607
- if (result?.nextScreen) this.screen = result.nextScreen; this.tui.requestRender(); return;
608
- }
609
- }
610
- }
611
-
612
- private handleEditDiscard(): void {
613
- const entry = this.getAgentEntry(this.currentAgentId); if (!entry) { this.screen = "list"; this.editState = null; this.builtinOverrideScope = null; this.tui.requestRender(); return; }
614
- if (entry.isNew) { this.removeAgentEntry(entry); this.editState = null; this.builtinOverrideScope = null; this.screen = "list"; this.tui.requestRender(); return; }
615
- this.editState = null; this.builtinOverrideScope = null; this.enterDetail(entry); this.tui.requestRender();
616
- }
617
-
618
- private isBuiltin(id: string): boolean { const a = this.getAgentEntry(id); return a?.config.source === "builtin"; }
619
-
620
- private handleListAction(action: ListAction): void {
621
- switch (action.type) {
622
- case "open-detail": { const agent = this.getAgentEntry(action.id); if (agent) { this.enterDetail(agent); return; } const chain = this.getChainEntry(action.id); if (chain) this.enterChainDetail(chain); return; }
623
- case "clone": if (this.getAgentEntry(action.id)) this.enterNameInput("clone-agent", action.id); else if (this.getChainEntry(action.id)) this.enterNameInput("clone-chain", action.id); return;
624
- case "new": this.enterTemplateSelect(); return;
625
- case "delete": { if (this.isBuiltin(action.id)) { this.statusMessage = { text: "Builtin agents cannot be deleted. Clone to user scope to override.", type: "error" }; return; } this.confirmDeleteId = action.id; this.screen = "confirm-delete"; return; }
626
- case "run-chain": {
627
- const disabled = this.disabledAgentEntries(action.ids);
628
- if (disabled.length > 0) {
629
- this.statusMessage = { text: `Disabled builtin agents cannot run: ${disabled.map((entry) => entry.config.name).join(", ")}. Edit the override to re-enable them.`, type: "error" };
630
- return;
631
- }
632
- this.enterTaskInput(action.ids);
633
- return;
634
- }
635
- case "run-parallel": {
636
- const disabled = this.disabledAgentEntries(action.ids);
637
- if (disabled.length > 0) {
638
- this.statusMessage = { text: `Disabled builtin agents cannot run: ${disabled.map((entry) => entry.config.name).join(", ")}. Edit the override to re-enable them.`, type: "error" };
639
- return;
640
- }
641
- this.enterParallelBuilder(action.ids);
642
- return;
643
- }
644
- case "close": this.done(undefined); return;
645
- }
646
- }
647
-
648
- private handleDetailAction(action: DetailAction, entry: AgentEntry): void {
649
- if (action.type === "back") { this.screen = "list"; return; }
650
- if (action.type === "edit") {
651
- if (entry.config.source === "builtin") {
652
- if (entry.config.override) this.enterBuiltinOverrideEdit(entry, entry.config.override.scope);
653
- else this.enterBuiltinOverrideScope(entry);
654
- return;
655
- }
656
- this.enterEdit(entry);
657
- return;
658
- }
659
- if (action.type === "launch") {
660
- if (entry.config.disabled) return;
661
- this.enterTaskInput([entry.id], "detail");
662
- return;
663
- }
664
- }
665
-
666
- private handleChainDetailAction(action: ChainDetailAction, entry: ChainEntry): void {
667
- if (action.type === "back") { this.screen = "list"; return; }
668
- if (action.type === "launch") { this.enterSavedChainLaunch(entry); return; }
669
- if (action.type === "edit") this.enterChainEdit(entry);
670
- }
671
-
672
- render(width: number): string[] {
673
- this.overlayWidth = Math.min(width, 84); const w = this.overlayWidth;
674
- switch (this.screen) {
675
- case "list": return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage);
676
- case "template-select": return this.renderTemplateSelect(w);
677
- case "override-scope": return this.renderOverrideScope(w);
678
- case "detail": { const entry = this.getAgentEntry(this.currentAgentId); if (!entry) return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage); return renderDetail(this.detailState, entry.config, this.agentData.cwd, w, this.theme); }
679
- case "chain-detail": { const entry = this.getChainEntry(this.currentChainId); if (!entry) return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage); return renderChainDetail(this.chainDetailState, entry.config, w, this.theme); }
680
- case "edit": case "edit-field": case "edit-prompt": return this.editState ? renderEdit(this.screen as EditScreen, this.editState, w, this.theme) : [];
681
- case "parallel-builder": {
682
- if (!this.parallelState) return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage);
683
- const agentOptions: AgentOption[] = this.agents.map((e) => ({ name: e.config.name, description: e.config.description, model: e.config.model }));
684
- return renderParallel(this.parallelState, agentOptions, w, this.theme);
685
- }
686
- case "task-input": {
687
- if (this.chainLaunchId) { const entry = this.getChainEntry(this.chainLaunchId); const title = entry ? `Chain: ${entry.config.name}` : "Chain"; return renderTaskInput(title, this.taskEditor, this.skipClarify, w, this.theme, this.launchToggleState()); }
688
- if (this.parallelMode && this.parallelState) return renderTaskInput(formatParallelTitle(this.parallelState.slots), this.taskEditor, this.skipClarify, w, this.theme, this.launchToggleState());
689
- if (this.chainAgentIds.length > 1) {
690
- const names = this.chainAgentIds
691
- .map((id) => this.getAgentEntry(id)?.config.name)
692
- .filter((name): name is string => Boolean(name));
693
- return renderTaskInput(`Chain: ${names.join(" → ")}`, this.taskEditor, this.skipClarify, w, this.theme, this.launchToggleState());
694
- }
695
- const name = this.getAgentEntry(this.chainAgentIds[0] ?? null)?.config.name ?? "Agent";
696
- return renderTaskInput(`Run: ${name}`, this.taskEditor, this.skipClarify, w, this.theme, this.launchToggleState());
697
- }
698
- case "confirm-delete": return this.renderConfirmDelete(w);
699
- case "name-input": return this.renderNameInput(w);
700
- case "chain-edit": return this.renderChainEdit(w);
701
- }
702
- }
703
-
704
- invalidate(): void {}
705
- dispose(): void {}
706
- }