pi-subagents 0.23.1 → 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 +13 -0
- package/README.md +13 -76
- 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 +11 -12
- package/src/agents/agent-serializer.ts +0 -42
- package/src/agents/agents.ts +1 -1
- package/src/extension/index.ts +2 -2
- package/src/runs/background/async-status.ts +16 -50
- package/src/runs/background/run-status.ts +8 -9
- package/src/runs/foreground/chain-clarify.ts +183 -218
- package/src/shared/status-format.ts +49 -0
- package/src/shared/types.ts +0 -5
- package/src/slash/slash-commands.ts +0 -74
- package/src/tui/render.ts +32 -58
- 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,456 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
3
|
-
import { buildRuntimeName, defaultSystemPromptMode, frontmatterNameForConfig, parsePackageName, type AgentConfig, type AgentDefaultContext, type BuiltinAgentOverrideBase } from "../agents/agents.ts";
|
|
4
|
-
import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "../tui/text-editor.ts";
|
|
5
|
-
import type { TextEditorState } from "../tui/text-editor.ts";
|
|
6
|
-
import { pad, row, renderHeader, renderFooter, formatScrollInfo } from "../tui/render-helpers.ts";
|
|
7
|
-
import { findModelInfo, getSupportedThinkingLevels, type ModelInfo, type ThinkingLevel } from "../shared/model-info.ts";
|
|
8
|
-
|
|
9
|
-
export type { ModelInfo };
|
|
10
|
-
export interface SkillInfo { name: string; source: string; description?: string; }
|
|
11
|
-
export type EditScreen = "edit" | "edit-field" | "edit-prompt";
|
|
12
|
-
export type EditField = typeof FIELD_ORDER[number];
|
|
13
|
-
|
|
14
|
-
export interface EditState {
|
|
15
|
-
draft: AgentConfig; isNew: boolean; fieldIndex: number; fieldMode: "text" | "model" | "thinking" | "skills" | null;
|
|
16
|
-
fieldEditor: TextEditorState; promptEditor: TextEditorState; modelSearchQuery: string; modelCursor: number; models: ModelInfo[]; filteredModels: ModelInfo[];
|
|
17
|
-
thinkingCursor: number; skillSearchQuery: string; skillCursor: number; filteredSkills: SkillInfo[]; skillSelected: Set<string>; error?: string;
|
|
18
|
-
fields: EditField[];
|
|
19
|
-
title?: string;
|
|
20
|
-
overrideBase?: BuiltinAgentOverrideBase;
|
|
21
|
-
preferredProvider?: string;
|
|
22
|
-
}
|
|
23
|
-
interface EditInputResult { action?: "save" | "discard" | "delete"; nextScreen?: EditScreen; }
|
|
24
|
-
interface CreateEditStateOptions {
|
|
25
|
-
fields?: EditField[];
|
|
26
|
-
title?: string;
|
|
27
|
-
overrideBase?: BuiltinAgentOverrideBase;
|
|
28
|
-
preferredProvider?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const FIELD_ORDER = ["name", "package", "description", "model", "fallbackModels", "thinking", "systemPromptMode", "inheritProjectContext", "inheritSkills", "defaultContext", "tools", "extensions", "skills", "output", "reads", "progress", "interactive", "prompt"] as const;
|
|
32
|
-
const PROMPT_VIEWPORT_HEIGHT = 16;
|
|
33
|
-
const MODEL_SELECTOR_HEIGHT = 10;
|
|
34
|
-
const SKILL_SELECTOR_HEIGHT = 10;
|
|
35
|
-
|
|
36
|
-
function formatTools(draft: Pick<AgentConfig, "tools" | "mcpDirectTools">): string { const tools = [...(draft.tools ?? []), ...(draft.mcpDirectTools ?? []).map((tool) => `mcp:${tool}`)]; return tools.length > 0 ? tools.join(", ") : ""; }
|
|
37
|
-
function toolList(draft: Pick<AgentConfig, "tools" | "mcpDirectTools">): string[] | undefined { const tools = [...(draft.tools ?? []), ...(draft.mcpDirectTools ?? []).map((tool) => `mcp:${tool}`)]; return tools.length > 0 ? tools : undefined; }
|
|
38
|
-
function parseTools(value: string): { tools?: string[]; mcp?: string[] } { const items = value.split(",").map((item) => item.trim()).filter((item) => item.length > 0); const tools: string[] = []; const mcp: string[] = []; for (const item of items) { if (item.startsWith("mcp:")) { const name = item.slice(4).trim(); if (name) mcp.push(name); } else { tools.push(item); } } return { tools: tools.length > 0 ? tools : undefined, mcp: mcp.length > 0 ? mcp : undefined }; }
|
|
39
|
-
function parseCommaList(value: string): string[] | undefined { const items = value.split(",").map((item) => item.trim()).filter((item) => item.length > 0); return items.length > 0 ? items : undefined; }
|
|
40
|
-
function arraysEqual(a: string[] | undefined, b: string[] | undefined): boolean { if (!a && !b) return true; if (!a || !b || a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; return true; }
|
|
41
|
-
|
|
42
|
-
function nextDefaultContext(value: AgentDefaultContext | undefined): AgentDefaultContext | undefined {
|
|
43
|
-
if (value === undefined) return "fork";
|
|
44
|
-
if (value === "fork") return "fresh";
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function fieldValueMatchesBase(field: EditField, state: EditState): boolean {
|
|
49
|
-
const base = state.overrideBase;
|
|
50
|
-
if (!base) return false;
|
|
51
|
-
switch (field) {
|
|
52
|
-
case "model": return state.draft.model === base.model;
|
|
53
|
-
case "fallbackModels": return arraysEqual(state.draft.fallbackModels, base.fallbackModels);
|
|
54
|
-
case "thinking": return state.draft.thinking === base.thinking;
|
|
55
|
-
case "systemPromptMode": return state.draft.systemPromptMode === base.systemPromptMode;
|
|
56
|
-
case "inheritProjectContext": return state.draft.inheritProjectContext === base.inheritProjectContext;
|
|
57
|
-
case "inheritSkills": return state.draft.inheritSkills === base.inheritSkills;
|
|
58
|
-
case "defaultContext": return state.draft.defaultContext === base.defaultContext;
|
|
59
|
-
case "disabled": return state.draft.disabled === base.disabled;
|
|
60
|
-
case "tools": return arraysEqual(toolList(state.draft), toolList(base));
|
|
61
|
-
case "skills": return arraysEqual(state.draft.skills, base.skills);
|
|
62
|
-
case "prompt": return state.draft.systemPrompt === base.systemPrompt;
|
|
63
|
-
default: return false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function resetFieldToBase(field: EditField, state: EditState): void {
|
|
68
|
-
const base = state.overrideBase;
|
|
69
|
-
if (!base) return;
|
|
70
|
-
switch (field) {
|
|
71
|
-
case "model": state.draft.model = base.model; break;
|
|
72
|
-
case "fallbackModels": state.draft.fallbackModels = base.fallbackModels ? [...base.fallbackModels] : undefined; break;
|
|
73
|
-
case "thinking": state.draft.thinking = base.thinking; break;
|
|
74
|
-
case "systemPromptMode": state.draft.systemPromptMode = base.systemPromptMode; break;
|
|
75
|
-
case "inheritProjectContext": state.draft.inheritProjectContext = base.inheritProjectContext; break;
|
|
76
|
-
case "inheritSkills": state.draft.inheritSkills = base.inheritSkills; break;
|
|
77
|
-
case "defaultContext": state.draft.defaultContext = base.defaultContext; break;
|
|
78
|
-
case "disabled": state.draft.disabled = base.disabled; break;
|
|
79
|
-
case "tools": state.draft.tools = base.tools ? [...base.tools] : undefined; state.draft.mcpDirectTools = base.mcpDirectTools ? [...base.mcpDirectTools] : undefined; break;
|
|
80
|
-
case "skills": state.draft.skills = base.skills ? [...base.skills] : undefined; break;
|
|
81
|
-
case "prompt": state.draft.systemPrompt = base.systemPrompt; state.promptEditor = createEditorState(base.systemPrompt); break;
|
|
82
|
-
default: break;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function createEditState(draft: AgentConfig, isNew: boolean, models: ModelInfo[], skills: SkillInfo[], options: CreateEditStateOptions = {}): EditState {
|
|
87
|
-
return {
|
|
88
|
-
draft: { ...draft, tools: draft.tools ? [...draft.tools] : undefined, mcpDirectTools: draft.mcpDirectTools ? [...draft.mcpDirectTools] : undefined, skills: draft.skills ? [...draft.skills] : undefined, fallbackModels: draft.fallbackModels ? [...draft.fallbackModels] : undefined, extensions: draft.extensions ? [...draft.extensions] : draft.extensions, defaultReads: draft.defaultReads ? [...draft.defaultReads] : undefined, extraFields: draft.extraFields ? { ...draft.extraFields } : undefined },
|
|
89
|
-
isNew, fieldIndex: 0, fieldMode: null, fieldEditor: createEditorState(), promptEditor: createEditorState(draft.systemPrompt ?? ""),
|
|
90
|
-
modelSearchQuery: "", modelCursor: 0, models: [...models], filteredModels: [...models], thinkingCursor: 0, skillSearchQuery: "", skillCursor: 0, filteredSkills: [...skills], skillSelected: new Set(draft.skills ?? []),
|
|
91
|
-
fields: options.fields ?? [...FIELD_ORDER], title: options.title, overrideBase: options.overrideBase, preferredProvider: options.preferredProvider,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function renderFieldValue(field: EditField, state: EditState): string {
|
|
96
|
-
const draft = state.draft;
|
|
97
|
-
switch (field) {
|
|
98
|
-
case "name": return frontmatterNameForConfig(draft);
|
|
99
|
-
case "package": return draft.packageName ?? "";
|
|
100
|
-
case "description": return draft.description;
|
|
101
|
-
case "model": return draft.model ?? "default";
|
|
102
|
-
case "fallbackModels": return draft.fallbackModels && draft.fallbackModels.length > 0 ? draft.fallbackModels.join(", ") : "";
|
|
103
|
-
case "thinking": return draft.thinking ?? "off";
|
|
104
|
-
case "systemPromptMode": return draft.systemPromptMode ?? defaultSystemPromptMode(frontmatterNameForConfig(draft));
|
|
105
|
-
case "inheritProjectContext": return draft.inheritProjectContext ? "on" : "off";
|
|
106
|
-
case "inheritSkills": return draft.inheritSkills ? "on" : "off";
|
|
107
|
-
case "defaultContext": return draft.defaultContext ?? "auto";
|
|
108
|
-
case "disabled": return draft.disabled ? "on" : "off";
|
|
109
|
-
case "tools": return formatTools(draft);
|
|
110
|
-
case "extensions": return draft.extensions !== undefined ? (draft.extensions.length > 0 ? draft.extensions.join(", ") : "") : "(all)";
|
|
111
|
-
case "skills": return draft.skills && draft.skills.length > 0 ? draft.skills.join(", ") : "";
|
|
112
|
-
case "output": return draft.output ?? "";
|
|
113
|
-
case "reads": return draft.defaultReads && draft.defaultReads.length > 0 ? draft.defaultReads.join(", ") : "";
|
|
114
|
-
case "progress": return draft.defaultProgress ? "on" : "off";
|
|
115
|
-
case "interactive": return draft.interactive ? "on" : "off";
|
|
116
|
-
default: return "";
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function applyFieldValue(field: EditField, state: EditState, value: string): void {
|
|
121
|
-
const draft = state.draft;
|
|
122
|
-
switch (field) {
|
|
123
|
-
case "name": {
|
|
124
|
-
const localName = value.trim();
|
|
125
|
-
draft.localName = localName || undefined;
|
|
126
|
-
draft.name = localName ? buildRuntimeName(localName, draft.packageName) : "";
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
129
|
-
case "package": {
|
|
130
|
-
const parsed = parsePackageName(value, "package");
|
|
131
|
-
if (parsed.error) {
|
|
132
|
-
state.error = parsed.error;
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
const packageName = parsed.packageName;
|
|
136
|
-
draft.packageName = packageName;
|
|
137
|
-
const localName = frontmatterNameForConfig(draft).trim();
|
|
138
|
-
draft.name = localName ? buildRuntimeName(localName, packageName) : "";
|
|
139
|
-
state.error = undefined;
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
case "description": draft.description = value.trim(); break;
|
|
143
|
-
case "model": draft.model = value.trim() || undefined; break;
|
|
144
|
-
case "fallbackModels": draft.fallbackModels = parseCommaList(value); break;
|
|
145
|
-
case "systemPromptMode": {
|
|
146
|
-
const trimmed = value.trim();
|
|
147
|
-
if (trimmed === "") {
|
|
148
|
-
draft.systemPromptMode = defaultSystemPromptMode(frontmatterNameForConfig(draft));
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
if (trimmed === "append" || trimmed === "replace") {
|
|
152
|
-
draft.systemPromptMode = trimmed;
|
|
153
|
-
}
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
case "tools": { const parsed = parseTools(value); draft.tools = parsed.tools; draft.mcpDirectTools = parsed.mcp; break; }
|
|
157
|
-
case "extensions": { const trimmed = value.trim(); draft.extensions = trimmed === "(all)" ? undefined : parseCommaList(trimmed) ?? []; break; }
|
|
158
|
-
case "skills": draft.skills = parseCommaList(value); break;
|
|
159
|
-
case "output": { const trimmed = value.trim(); draft.output = trimmed.length > 0 ? trimmed : undefined; break; }
|
|
160
|
-
case "reads": draft.defaultReads = parseCommaList(value); break;
|
|
161
|
-
case "defaultContext": {
|
|
162
|
-
const trimmed = value.trim();
|
|
163
|
-
if (trimmed === "" || trimmed === "auto") {
|
|
164
|
-
draft.defaultContext = undefined;
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
if (trimmed === "fresh" || trimmed === "fork") draft.defaultContext = trimmed;
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
170
|
-
case "inheritProjectContext":
|
|
171
|
-
case "inheritSkills":
|
|
172
|
-
case "disabled":
|
|
173
|
-
case "progress":
|
|
174
|
-
case "interactive":
|
|
175
|
-
case "prompt":
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function openModelPicker(state: EditState, models: ModelInfo[]): void {
|
|
181
|
-
state.fieldIndex = state.fields.indexOf("model"); state.fieldMode = "model"; state.modelSearchQuery = ""; state.filteredModels = [...models];
|
|
182
|
-
const idx = state.filteredModels.findIndex((m) => m.fullId === state.draft.model || m.id === state.draft.model); state.modelCursor = idx >= 0 ? idx : 0;
|
|
183
|
-
}
|
|
184
|
-
function getDraftThinkingLevels(state: EditState): ThinkingLevel[] {
|
|
185
|
-
return getSupportedThinkingLevels(findModelInfo(state.draft.model, state.models, state.preferredProvider));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function openThinkingPicker(state: EditState): void {
|
|
189
|
-
state.fieldIndex = state.fields.indexOf("thinking"); state.fieldMode = "thinking";
|
|
190
|
-
const levels = getDraftThinkingLevels(state);
|
|
191
|
-
const currentLevel = state.draft.thinking ?? "off";
|
|
192
|
-
const idx = levels.findIndex((level) => level === currentLevel);
|
|
193
|
-
state.thinkingCursor = idx >= 0 ? idx : Math.max(0, levels.indexOf("off"));
|
|
194
|
-
}
|
|
195
|
-
function openSkillPicker(state: EditState, skills: SkillInfo[]): void {
|
|
196
|
-
state.fieldIndex = state.fields.indexOf("skills"); state.fieldMode = "skills"; state.skillSearchQuery = ""; state.filteredSkills = [...skills]; state.skillSelected = new Set(state.draft.skills ?? []); state.skillCursor = 0;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function renderModelPicker(state: EditState, width: number, theme: Theme): string[] {
|
|
200
|
-
const lines: string[] = [];
|
|
201
|
-
lines.push(renderHeader(" Select Model ", width, theme));
|
|
202
|
-
lines.push(row("", width, theme));
|
|
203
|
-
const cursor = "\x1b[7m \x1b[27m";
|
|
204
|
-
lines.push(row(` ${theme.fg("dim", "Search: ")}${state.modelSearchQuery}${cursor}`, width, theme));
|
|
205
|
-
lines.push(row("", width, theme));
|
|
206
|
-
const currentModel = state.draft.model ?? "default";
|
|
207
|
-
lines.push(row(` ${theme.fg("dim", "Current: ")}${theme.fg("warning", currentModel)}`, width, theme));
|
|
208
|
-
lines.push(row("", width, theme));
|
|
209
|
-
const list = state.filteredModels;
|
|
210
|
-
if (list.length === 0) {
|
|
211
|
-
lines.push(row(` ${theme.fg("dim", "No matching models")}`, width, theme));
|
|
212
|
-
} else {
|
|
213
|
-
const maxVisible = MODEL_SELECTOR_HEIGHT; let startIdx = 0;
|
|
214
|
-
if (list.length > maxVisible) { startIdx = Math.max(0, state.modelCursor - Math.floor(maxVisible / 2)); startIdx = Math.min(startIdx, list.length - maxVisible); }
|
|
215
|
-
const endIdx = Math.min(startIdx + maxVisible, list.length);
|
|
216
|
-
if (startIdx > 0) lines.push(row(` ${theme.fg("dim", ` ↑ ${startIdx} more`)}`, width, theme));
|
|
217
|
-
for (let i = startIdx; i < endIdx; i++) { const model = list[i]!; const isSelected = i === state.modelCursor; const prefix = isSelected ? theme.fg("accent", "→ ") : " "; const modelText = isSelected ? theme.fg("accent", model.id) : model.id; const provider = theme.fg("dim", ` [${model.provider}]`); lines.push(row(` ${prefix}${modelText}${provider}`, width, theme)); }
|
|
218
|
-
const remaining = list.length - endIdx; if (remaining > 0) lines.push(row(` ${theme.fg("dim", ` ↓ ${remaining} more`)}`, width, theme));
|
|
219
|
-
}
|
|
220
|
-
while (lines.length < 19) lines.push(row("", width, theme));
|
|
221
|
-
lines.push(renderFooter(" [enter] select [esc] cancel type to search ", width, theme));
|
|
222
|
-
return lines;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function renderThinkingPicker(state: EditState, width: number, theme: Theme): string[] {
|
|
226
|
-
const lines: string[] = [];
|
|
227
|
-
lines.push(renderHeader(" Select Thinking Level ", width, theme));
|
|
228
|
-
lines.push(row("", width, theme));
|
|
229
|
-
const currentModel = state.draft.model ?? "default";
|
|
230
|
-
const current = truncateToWidth(currentModel, width - 13);
|
|
231
|
-
lines.push(row(` ${theme.fg("dim", "Model: ")}${theme.fg("warning", current)}`, width, theme));
|
|
232
|
-
lines.push(row("", width, theme));
|
|
233
|
-
const descriptions: Record<ThinkingLevel, string> = {
|
|
234
|
-
off: "No extended thinking",
|
|
235
|
-
minimal: "Brief reasoning",
|
|
236
|
-
low: "Light reasoning",
|
|
237
|
-
medium: "Moderate reasoning",
|
|
238
|
-
high: "Deep reasoning",
|
|
239
|
-
xhigh: "Maximum reasoning (ultrathink)",
|
|
240
|
-
};
|
|
241
|
-
const levels = getDraftThinkingLevels(state);
|
|
242
|
-
if (levels.length === 0) {
|
|
243
|
-
lines.push(row(` ${theme.fg("dim", "No supported thinking levels")}`, width, theme));
|
|
244
|
-
} else {
|
|
245
|
-
for (let i = 0; i < levels.length; i++) {
|
|
246
|
-
const level = levels[i]!;
|
|
247
|
-
const isSelected = i === state.thinkingCursor;
|
|
248
|
-
const prefix = isSelected ? theme.fg("accent", "→ ") : " ";
|
|
249
|
-
const levelText = isSelected ? theme.fg("accent", level) : level;
|
|
250
|
-
const desc = theme.fg("dim", ` - ${descriptions[level]}`);
|
|
251
|
-
lines.push(row(` ${prefix}${levelText}${desc}`, width, theme));
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
while (lines.length < 19) lines.push(row("", width, theme));
|
|
255
|
-
const footer = levels.length === 0 ? " [esc] cancel " : " [enter] select [esc] cancel [↑↓] navigate ";
|
|
256
|
-
lines.push(renderFooter(footer, width, theme));
|
|
257
|
-
return lines;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function renderSkillPicker(state: EditState, width: number, theme: Theme): string[] {
|
|
261
|
-
const lines: string[] = [];
|
|
262
|
-
lines.push(renderHeader(" Select Skills ", width, theme));
|
|
263
|
-
lines.push(row("", width, theme));
|
|
264
|
-
const cursor = "\x1b[7m \x1b[27m";
|
|
265
|
-
lines.push(row(` ${theme.fg("dim", "Search: ")}${state.skillSearchQuery}${cursor}`, width, theme));
|
|
266
|
-
lines.push(row("", width, theme));
|
|
267
|
-
const selected = [...state.skillSelected].join(", ") || theme.fg("dim", "(none)");
|
|
268
|
-
lines.push(row(` ${theme.fg("dim", "Selected: ")}${truncateToWidth(selected, width - 14)}`, width, theme));
|
|
269
|
-
lines.push(row("", width, theme));
|
|
270
|
-
const list = state.filteredSkills;
|
|
271
|
-
if (list.length === 0) {
|
|
272
|
-
lines.push(row(` ${theme.fg("dim", "No matching skills")}`, width, theme));
|
|
273
|
-
} else {
|
|
274
|
-
let startIdx = 0;
|
|
275
|
-
if (list.length > SKILL_SELECTOR_HEIGHT) { startIdx = Math.max(0, state.skillCursor - Math.floor(SKILL_SELECTOR_HEIGHT / 2)); startIdx = Math.min(startIdx, list.length - SKILL_SELECTOR_HEIGHT); }
|
|
276
|
-
const endIdx = Math.min(startIdx + SKILL_SELECTOR_HEIGHT, list.length);
|
|
277
|
-
if (startIdx > 0) lines.push(row(` ${theme.fg("dim", ` ↑ ${startIdx} more`)}`, width, theme));
|
|
278
|
-
for (let i = startIdx; i < endIdx; i++) { const skill = list[i]!; const isCursor = i === state.skillCursor; const isSelected = state.skillSelected.has(skill.name); const prefix = isCursor ? theme.fg("accent", "→ ") : " "; const checkbox = isSelected ? theme.fg("success", "[x]") : "[ ]"; const nameText = isCursor ? theme.fg("accent", skill.name) : skill.name; const sourceBadge = theme.fg("dim", ` [${skill.source}]`); const desc = skill.description ? theme.fg("dim", ` - ${truncateToWidth(skill.description, 25)}`) : ""; lines.push(row(` ${prefix}${checkbox} ${nameText}${sourceBadge}${desc}`, width, theme)); }
|
|
279
|
-
const remaining = list.length - endIdx; if (remaining > 0) lines.push(row(` ${theme.fg("dim", ` ↓ ${remaining} more`)}`, width, theme));
|
|
280
|
-
}
|
|
281
|
-
while (lines.length < 19) lines.push(row("", width, theme));
|
|
282
|
-
lines.push(renderFooter(" [enter] confirm [space] toggle [esc] cancel ", width, theme));
|
|
283
|
-
return lines;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function renderPromptEditor(state: EditState, width: number, theme: Theme): string[] {
|
|
287
|
-
const lines: string[] = [];
|
|
288
|
-
lines.push(renderHeader(" Editing System Prompt ", width, theme));
|
|
289
|
-
lines.push(row("", width, theme));
|
|
290
|
-
const textWidth = Math.max(10, width - 4);
|
|
291
|
-
const wrapped = wrapText(state.promptEditor.buffer, textWidth);
|
|
292
|
-
const cursorPos = getCursorDisplayPos(state.promptEditor.cursor, wrapped.starts);
|
|
293
|
-
state.promptEditor.viewportOffset = ensureCursorVisible(cursorPos.line, PROMPT_VIEWPORT_HEIGHT, state.promptEditor.viewportOffset);
|
|
294
|
-
const editorLines = renderEditor(state.promptEditor, textWidth, PROMPT_VIEWPORT_HEIGHT);
|
|
295
|
-
for (const line of editorLines) lines.push(row(` ${line}`, width, theme));
|
|
296
|
-
const scrollInfo = formatScrollInfo(state.promptEditor.viewportOffset, Math.max(0, wrapped.lines.length - state.promptEditor.viewportOffset - PROMPT_VIEWPORT_HEIGHT));
|
|
297
|
-
lines.push(row(scrollInfo ? ` ${theme.fg("dim", scrollInfo)}` : "", width, theme));
|
|
298
|
-
lines.push(renderFooter(" [esc] done ", width, theme));
|
|
299
|
-
return lines;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
export function handleEditInput(screen: EditScreen, state: EditState, data: string, width: number, models: ModelInfo[], skills: SkillInfo[]): EditInputResult | undefined {
|
|
303
|
-
if (screen === "edit") {
|
|
304
|
-
if (matchesKey(data, "ctrl+s")) return { action: "save" };
|
|
305
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) return { action: "discard" };
|
|
306
|
-
if (data === "D" && state.overrideBase) return { action: "delete" };
|
|
307
|
-
if (matchesKey(data, "up")) { state.fieldIndex = Math.max(0, state.fieldIndex - 1); return; }
|
|
308
|
-
if (matchesKey(data, "down")) { state.fieldIndex = Math.min(state.fields.length - 1, state.fieldIndex + 1); return; }
|
|
309
|
-
const field = state.fields[state.fieldIndex]!;
|
|
310
|
-
if (data === "r" && state.overrideBase) { resetFieldToBase(field, state); return; }
|
|
311
|
-
if (data === "m") { openModelPicker(state, models); return { nextScreen: "edit-field" }; }
|
|
312
|
-
if (data === "t") { openThinkingPicker(state); return { nextScreen: "edit-field" }; }
|
|
313
|
-
if (data === "s") { openSkillPicker(state, skills); return { nextScreen: "edit-field" }; }
|
|
314
|
-
if (data === " " && (field === "inheritProjectContext" || field === "inheritSkills" || field === "defaultContext" || field === "disabled" || field === "progress" || field === "interactive")) {
|
|
315
|
-
if (field === "inheritProjectContext") state.draft.inheritProjectContext = !state.draft.inheritProjectContext;
|
|
316
|
-
if (field === "inheritSkills") state.draft.inheritSkills = !state.draft.inheritSkills;
|
|
317
|
-
if (field === "defaultContext") state.draft.defaultContext = nextDefaultContext(state.draft.defaultContext);
|
|
318
|
-
if (field === "disabled") state.draft.disabled = !state.draft.disabled;
|
|
319
|
-
if (field === "progress") state.draft.defaultProgress = !state.draft.defaultProgress;
|
|
320
|
-
if (field === "interactive") state.draft.interactive = !state.draft.interactive;
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
if (matchesKey(data, "return")) {
|
|
324
|
-
if (field === "model") { openModelPicker(state, models); return { nextScreen: "edit-field" }; }
|
|
325
|
-
if (field === "thinking") { openThinkingPicker(state); return { nextScreen: "edit-field" }; }
|
|
326
|
-
if (field === "skills") { openSkillPicker(state, skills); return { nextScreen: "edit-field" }; }
|
|
327
|
-
if (field === "prompt") { state.promptEditor = createEditorState(state.draft.systemPrompt ?? ""); return { nextScreen: "edit-prompt" }; }
|
|
328
|
-
if (field === "defaultContext") {
|
|
329
|
-
state.draft.defaultContext = nextDefaultContext(state.draft.defaultContext);
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
if (field === "inheritProjectContext" || field === "inheritSkills" || field === "disabled" || field === "progress" || field === "interactive") return;
|
|
333
|
-
state.fieldMode = "text"; state.fieldEditor = createEditorState(renderFieldValue(field, state)); return { nextScreen: "edit-field" };
|
|
334
|
-
}
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
if (screen === "edit-field") {
|
|
338
|
-
if (state.fieldMode === "model") {
|
|
339
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { state.fieldMode = null; return { nextScreen: "edit" }; }
|
|
340
|
-
if (matchesKey(data, "return")) {
|
|
341
|
-
const selected = state.filteredModels[state.modelCursor];
|
|
342
|
-
if (selected) {
|
|
343
|
-
state.draft.model = selected.fullId;
|
|
344
|
-
if (state.draft.thinking && !getDraftThinkingLevels(state).some((level) => level === state.draft.thinking)) state.draft.thinking = undefined;
|
|
345
|
-
}
|
|
346
|
-
state.fieldMode = null;
|
|
347
|
-
return { nextScreen: "edit" };
|
|
348
|
-
}
|
|
349
|
-
if (matchesKey(data, "up")) { if (state.filteredModels.length > 0) state.modelCursor = state.modelCursor === 0 ? state.filteredModels.length - 1 : state.modelCursor - 1; return; }
|
|
350
|
-
if (matchesKey(data, "down")) { if (state.filteredModels.length > 0) state.modelCursor = state.modelCursor === state.filteredModels.length - 1 ? 0 : state.modelCursor + 1; return; }
|
|
351
|
-
if (matchesKey(data, "backspace")) { if (state.modelSearchQuery.length > 0) state.modelSearchQuery = state.modelSearchQuery.slice(0, -1); }
|
|
352
|
-
else if (data.length === 1 && data.charCodeAt(0) >= 32) state.modelSearchQuery += data;
|
|
353
|
-
const query = state.modelSearchQuery.toLowerCase();
|
|
354
|
-
state.filteredModels = query ? models.filter((m) => m.fullId.toLowerCase().includes(query) || m.id.toLowerCase().includes(query) || m.provider.toLowerCase().includes(query)) : [...models];
|
|
355
|
-
state.modelCursor = Math.min(state.modelCursor, Math.max(0, state.filteredModels.length - 1));
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
if (state.fieldMode === "thinking") {
|
|
359
|
-
const levels = getDraftThinkingLevels(state);
|
|
360
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { state.fieldMode = null; return { nextScreen: "edit" }; }
|
|
361
|
-
if (levels.length === 0) return;
|
|
362
|
-
if (matchesKey(data, "return")) { const selected = levels[state.thinkingCursor] ?? "off"; state.draft.thinking = selected === "off" ? undefined : selected; state.fieldMode = null; return { nextScreen: "edit" }; }
|
|
363
|
-
if (matchesKey(data, "up")) { state.thinkingCursor = state.thinkingCursor === 0 ? levels.length - 1 : state.thinkingCursor - 1; return; }
|
|
364
|
-
if (matchesKey(data, "down")) { state.thinkingCursor = state.thinkingCursor === levels.length - 1 ? 0 : state.thinkingCursor + 1; return; }
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
if (state.fieldMode === "skills") {
|
|
368
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { state.fieldMode = null; return { nextScreen: "edit" }; }
|
|
369
|
-
if (matchesKey(data, "return")) { const selected = [...state.skillSelected]; state.draft.skills = selected.length > 0 ? selected : undefined; state.fieldMode = null; return { nextScreen: "edit" }; }
|
|
370
|
-
if (data === " ") { const skill = state.filteredSkills[state.skillCursor]; if (skill) { if (state.skillSelected.has(skill.name)) state.skillSelected.delete(skill.name); else state.skillSelected.add(skill.name); } return; }
|
|
371
|
-
if (matchesKey(data, "up")) { if (state.filteredSkills.length > 0) state.skillCursor = state.skillCursor === 0 ? state.filteredSkills.length - 1 : state.skillCursor - 1; return; }
|
|
372
|
-
if (matchesKey(data, "down")) { if (state.filteredSkills.length > 0) state.skillCursor = state.skillCursor === state.filteredSkills.length - 1 ? 0 : state.skillCursor + 1; return; }
|
|
373
|
-
if (matchesKey(data, "backspace")) { if (state.skillSearchQuery.length > 0) state.skillSearchQuery = state.skillSearchQuery.slice(0, -1); }
|
|
374
|
-
else if (data.length === 1 && data.charCodeAt(0) >= 32) state.skillSearchQuery += data;
|
|
375
|
-
const query = state.skillSearchQuery.toLowerCase();
|
|
376
|
-
state.filteredSkills = query ? skills.filter((s) => s.name.toLowerCase().includes(query) || (s.description?.toLowerCase().includes(query) ?? false)) : [...skills];
|
|
377
|
-
state.skillCursor = Math.min(state.skillCursor, Math.max(0, state.filteredSkills.length - 1));
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { state.fieldMode = null; return { nextScreen: "edit" }; }
|
|
381
|
-
if (matchesKey(data, "return")) { const field = state.fields[state.fieldIndex]!; applyFieldValue(field, state, state.fieldEditor.buffer); state.fieldMode = null; return { nextScreen: "edit" }; }
|
|
382
|
-
if (matchesKey(data, "tab")) return;
|
|
383
|
-
const innerW = width - 2; const labelWidth = 12; const textWidth = Math.max(10, innerW - labelWidth - 6);
|
|
384
|
-
const nextState = handleEditorInput(state.fieldEditor, data, textWidth); if (nextState) state.fieldEditor = nextState; return;
|
|
385
|
-
}
|
|
386
|
-
if (screen === "edit-prompt") {
|
|
387
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { state.draft.systemPrompt = state.promptEditor.buffer; return { nextScreen: "edit" }; }
|
|
388
|
-
const textWidth = Math.max(10, width - 4);
|
|
389
|
-
if (matchesKey(data, "pageup") || matchesKey(data, "shift+up")) { const wrapped = wrapText(state.promptEditor.buffer, textWidth); const cursorPos = getCursorDisplayPos(state.promptEditor.cursor, wrapped.starts); const targetLine = Math.max(0, cursorPos.line - PROMPT_VIEWPORT_HEIGHT); const targetCol = Math.min(cursorPos.col, wrapped.lines[targetLine]?.length ?? 0); state.promptEditor = { ...state.promptEditor, cursor: wrapped.starts[targetLine] + targetCol }; return; }
|
|
390
|
-
if (matchesKey(data, "pagedown") || matchesKey(data, "shift+down")) { const wrapped = wrapText(state.promptEditor.buffer, textWidth); const cursorPos = getCursorDisplayPos(state.promptEditor.cursor, wrapped.starts); const targetLine = Math.min(wrapped.lines.length - 1, cursorPos.line + PROMPT_VIEWPORT_HEIGHT); const targetCol = Math.min(cursorPos.col, wrapped.lines[targetLine]?.length ?? 0); state.promptEditor = { ...state.promptEditor, cursor: wrapped.starts[targetLine] + targetCol }; return; }
|
|
391
|
-
const nextState = handleEditorInput(state.promptEditor, data, textWidth, { multiLine: true }); if (nextState) state.promptEditor = nextState; return;
|
|
392
|
-
}
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
export function renderEdit(screen: EditScreen, state: EditState, width: number, theme: Theme): string[] {
|
|
397
|
-
if (screen === "edit-field" && state.fieldMode === "model") return renderModelPicker(state, width, theme);
|
|
398
|
-
if (screen === "edit-field" && state.fieldMode === "thinking") return renderThinkingPicker(state, width, theme);
|
|
399
|
-
if (screen === "edit-field" && state.fieldMode === "skills") return renderSkillPicker(state, width, theme);
|
|
400
|
-
if (screen === "edit-prompt") return renderPromptEditor(state, width, theme);
|
|
401
|
-
const lines: string[] = [];
|
|
402
|
-
const scopeBadge = state.draft.source === "user" ? "[user]" : "[proj]"; const label = state.isNew ? " [new]" : "";
|
|
403
|
-
lines.push(renderHeader(` ${state.title ?? `Editing: ${state.draft.name} ${scopeBadge}${label}`} `, width, theme));
|
|
404
|
-
lines.push(row("", width, theme));
|
|
405
|
-
const innerW = width - 2; const labelWidth = 12; const valueWidth = Math.max(10, innerW - labelWidth - 6);
|
|
406
|
-
for (let i = 0; i < state.fields.length; i++) {
|
|
407
|
-
const field = state.fields[i]!; if (field === "prompt") break;
|
|
408
|
-
const isFocused = i === state.fieldIndex; const prefix = isFocused ? theme.fg("accent", "▸ ") : " ";
|
|
409
|
-
const fieldLabel = field === "systemPromptMode"
|
|
410
|
-
? "Prompt Mode"
|
|
411
|
-
: field === "inheritProjectContext"
|
|
412
|
-
? "Project Ctx"
|
|
413
|
-
: field === "inheritSkills"
|
|
414
|
-
? "Skills Ctx"
|
|
415
|
-
: field === "defaultContext"
|
|
416
|
-
? "Default Ctx"
|
|
417
|
-
: field === "disabled"
|
|
418
|
-
? "Disabled"
|
|
419
|
-
: `${field[0]!.toUpperCase()}${field.slice(1)}`;
|
|
420
|
-
const rawLabel = pad(`${fieldLabel}:`, labelWidth);
|
|
421
|
-
const labelText = state.overrideBase && !fieldValueMatchesBase(field, state) ? theme.fg("accent", rawLabel) : rawLabel; let valueText = renderFieldValue(field, state);
|
|
422
|
-
if (field === "inheritProjectContext") { const toggle = state.draft.inheritProjectContext ? theme.fg("success", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.inheritProjectContext ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
|
|
423
|
-
if (field === "inheritSkills") { const toggle = state.draft.inheritSkills ? theme.fg("success", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.inheritSkills ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
|
|
424
|
-
if (field === "disabled") { const toggle = state.draft.disabled ? theme.fg("warning", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.disabled ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
|
|
425
|
-
if (field === "progress") { const toggle = state.draft.defaultProgress ? theme.fg("success", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.defaultProgress ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
|
|
426
|
-
if (field === "interactive") { const toggle = state.draft.interactive ? theme.fg("success", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.interactive ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
|
|
427
|
-
let displayValue = truncateToWidth(valueText, valueWidth);
|
|
428
|
-
if (screen === "edit-field" && state.fieldMode === "text" && isFocused) {
|
|
429
|
-
const { starts } = wrapText(state.fieldEditor.buffer, valueWidth);
|
|
430
|
-
const pos = getCursorDisplayPos(state.fieldEditor.cursor, starts);
|
|
431
|
-
state.fieldEditor.viewportOffset = ensureCursorVisible(pos.line, 1, state.fieldEditor.viewportOffset);
|
|
432
|
-
const editorLine = renderEditor(state.fieldEditor, valueWidth, 1)[0] ?? "";
|
|
433
|
-
displayValue = pad(editorLine, valueWidth);
|
|
434
|
-
}
|
|
435
|
-
lines.push(row(` ${prefix}${labelText} [${displayValue}]`, width, theme));
|
|
436
|
-
}
|
|
437
|
-
if (state.fields.includes("prompt")) {
|
|
438
|
-
lines.push(row("", width, theme));
|
|
439
|
-
const promptIndex = state.fields.indexOf("prompt");
|
|
440
|
-
const promptFocused = state.fieldIndex === promptIndex;
|
|
441
|
-
const promptPrefix = promptFocused ? theme.fg("accent", "▸ ") : " ";
|
|
442
|
-
const promptTitle = state.overrideBase && !fieldValueMatchesBase("prompt", state)
|
|
443
|
-
? theme.fg("accent", "── System Prompt ──")
|
|
444
|
-
: theme.fg("dim", "── System Prompt ──");
|
|
445
|
-
lines.push(row(` ${promptPrefix}${promptTitle}`, width, theme));
|
|
446
|
-
const previewWidth = innerW - 2; const wrapped = wrapText(state.draft.systemPrompt ?? "", previewWidth); const previewLines = wrapped.lines.slice(0, 4);
|
|
447
|
-
for (const line of previewLines) lines.push(row(` ${line}`, width, theme));
|
|
448
|
-
for (let i = previewLines.length; i < 4; i++) lines.push(row("", width, theme));
|
|
449
|
-
}
|
|
450
|
-
if (state.error) lines.push(row(` ${theme.fg("error", state.error)}`, width, theme)); else lines.push(row("", width, theme));
|
|
451
|
-
const footer = state.overrideBase
|
|
452
|
-
? " [ctrl+s] save [r] reset field [D] remove override [esc] back "
|
|
453
|
-
: " [ctrl+s] save [esc] back ";
|
|
454
|
-
lines.push(renderFooter(footer, width, theme));
|
|
455
|
-
return lines;
|
|
456
|
-
}
|