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