@leing2021/super-pi 0.22.0 → 0.23.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.
Files changed (101) hide show
  1. package/README.md +38 -209
  2. package/extensions/ce-core/index.ts +190 -7
  3. package/extensions/ce-core/tools/parallel-subagent.ts +3 -3
  4. package/extensions/ce-core/tools/subagent.ts +2 -2
  5. package/package.json +4 -18
  6. package/rules/common/naming.md +154 -0
  7. package/skills/02-plan/SKILL.md +3 -2
  8. package/skills/03-work/SKILL.md +9 -6
  9. package/skills/04-review/SKILL.md +3 -2
  10. package/skills/06-next/SKILL.md +38 -10
  11. package/skills/06-next/references/recommendation-logic.md +2 -2
  12. package/skills/07-worktree/SKILL.md +9 -8
  13. package/skills/{09-help → 08-help}/SKILL.md +3 -3
  14. package/skills/{09-help → 08-help}/references/workflow-sequence.md +2 -1
  15. package/skills/references/language-detection.md +44 -0
  16. package/extensions/subagent/agent-management.ts +0 -595
  17. package/extensions/subagent/agent-manager-chain-detail.ts +0 -162
  18. package/extensions/subagent/agent-manager-detail.ts +0 -231
  19. package/extensions/subagent/agent-manager-edit.ts +0 -390
  20. package/extensions/subagent/agent-manager-list.ts +0 -278
  21. package/extensions/subagent/agent-manager-parallel.ts +0 -304
  22. package/extensions/subagent/agent-manager.ts +0 -705
  23. package/extensions/subagent/agent-scope.ts +0 -8
  24. package/extensions/subagent/agent-selection.ts +0 -25
  25. package/extensions/subagent/agent-serializer.ts +0 -123
  26. package/extensions/subagent/agent-templates.ts +0 -62
  27. package/extensions/subagent/agents/context-builder.md +0 -37
  28. package/extensions/subagent/agents/delegate.md +0 -9
  29. package/extensions/subagent/agents/oracle.md +0 -73
  30. package/extensions/subagent/agents/planner.md +0 -52
  31. package/extensions/subagent/agents/researcher.md +0 -50
  32. package/extensions/subagent/agents/reviewer.md +0 -38
  33. package/extensions/subagent/agents/scout.md +0 -48
  34. package/extensions/subagent/agents/worker.md +0 -52
  35. package/extensions/subagent/agents.ts +0 -761
  36. package/extensions/subagent/artifacts.ts +0 -100
  37. package/extensions/subagent/async-execution.ts +0 -520
  38. package/extensions/subagent/async-job-tracker.ts +0 -216
  39. package/extensions/subagent/async-status.ts +0 -241
  40. package/extensions/subagent/chain-clarify.ts +0 -1364
  41. package/extensions/subagent/chain-execution.ts +0 -853
  42. package/extensions/subagent/chain-serializer.ts +0 -126
  43. package/extensions/subagent/completion-dedupe.ts +0 -65
  44. package/extensions/subagent/doctor.ts +0 -200
  45. package/extensions/subagent/execution.ts +0 -738
  46. package/extensions/subagent/file-coalescer.ts +0 -42
  47. package/extensions/subagent/fork-context.ts +0 -63
  48. package/extensions/subagent/formatters.ts +0 -122
  49. package/extensions/subagent/frontmatter.ts +0 -31
  50. package/extensions/subagent/index.ts +0 -585
  51. package/extensions/subagent/intercom-bridge.ts +0 -240
  52. package/extensions/subagent/jsonl-writer.ts +0 -83
  53. package/extensions/subagent/model-fallback.ts +0 -108
  54. package/extensions/subagent/notify.ts +0 -110
  55. package/extensions/subagent/parallel-utils.ts +0 -108
  56. package/extensions/subagent/pi-args.ts +0 -138
  57. package/extensions/subagent/pi-spawn.ts +0 -100
  58. package/extensions/subagent/post-exit-stdio-guard.ts +0 -87
  59. package/extensions/subagent/prompt-template-bridge.ts +0 -399
  60. package/extensions/subagent/prompts/gather-context-and-clarify.md +0 -13
  61. package/extensions/subagent/prompts/parallel-cleanup.md +0 -42
  62. package/extensions/subagent/prompts/parallel-research.md +0 -50
  63. package/extensions/subagent/prompts/parallel-review.md +0 -40
  64. package/extensions/subagent/render-helpers.ts +0 -82
  65. package/extensions/subagent/render.ts +0 -836
  66. package/extensions/subagent/result-intercom.ts +0 -237
  67. package/extensions/subagent/result-watcher.ts +0 -171
  68. package/extensions/subagent/run-history.ts +0 -57
  69. package/extensions/subagent/run-status.ts +0 -136
  70. package/extensions/subagent/schemas.ts +0 -164
  71. package/extensions/subagent/session-tokens.ts +0 -50
  72. package/extensions/subagent/settings.ts +0 -367
  73. package/extensions/subagent/single-output.ts +0 -97
  74. package/extensions/subagent/skills.ts +0 -626
  75. package/extensions/subagent/slash-bridge.ts +0 -176
  76. package/extensions/subagent/slash-commands.ts +0 -303
  77. package/extensions/subagent/slash-live-state.ts +0 -294
  78. package/extensions/subagent/subagent-control.ts +0 -150
  79. package/extensions/subagent/subagent-executor.ts +0 -1899
  80. package/extensions/subagent/subagent-prompt-runtime.ts +0 -75
  81. package/extensions/subagent/subagent-runner.ts +0 -1470
  82. package/extensions/subagent/subagents-status.ts +0 -472
  83. package/extensions/subagent/text-editor.ts +0 -272
  84. package/extensions/subagent/top-level-async.ts +0 -15
  85. package/extensions/subagent/types.ts +0 -623
  86. package/extensions/subagent/utils.ts +0 -456
  87. package/extensions/subagent/worktree.ts +0 -579
  88. package/extensions/super-pi-extension/agents/ce-oracle.md +0 -109
  89. package/extensions/super-pi-extension/agents/ce-planner.md +0 -116
  90. package/extensions/super-pi-extension/agents/ce-reviewer.md +0 -99
  91. package/extensions/super-pi-extension/agents/ce-scout.md +0 -80
  92. package/extensions/super-pi-extension/agents/ce-worker.md +0 -78
  93. package/extensions/super-pi-extension/chains/ce-parallel-review.chain.md +0 -34
  94. package/extensions/super-pi-extension/chains/ce-review-only.chain.md +0 -12
  95. package/extensions/super-pi-extension/chains/ce-standard.chain.md +0 -28
  96. package/extensions/super-pi-extension/index.ts +0 -129
  97. package/extensions/super-pi-extension/model-sync.ts +0 -204
  98. package/skills/08-status/SKILL.md +0 -41
  99. package/skills/08-status/references/artifact-locations.md +0 -10
  100. package/skills/10-rules/SKILL.md +0 -76
  101. package/skills/pi-subagents/SKILL.md +0 -566
@@ -1,390 +0,0 @@
1
- // Based on pi-subagents by Nico Bailon (https://github.com/nicobailon/pi-subagents)
2
- // MIT License
3
- import type { Theme } from "@mariozechner/pi-coding-agent";
4
- import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
5
- import { defaultSystemPromptMode, type AgentConfig, type BuiltinAgentOverrideBase } from "./agents.ts";
6
- import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.ts";
7
- import type { TextEditorState } from "./text-editor.ts";
8
- import { pad, row, renderHeader, renderFooter, formatScrollInfo } from "./render-helpers.ts";
9
-
10
- export interface ModelInfo { provider: string; id: string; fullId: string; }
11
- export interface SkillInfo { name: string; source: string; description?: string; }
12
- export type EditScreen = "edit" | "edit-field" | "edit-prompt";
13
- export type EditField = typeof FIELD_ORDER[number];
14
-
15
- export interface EditState {
16
- draft: AgentConfig; isNew: boolean; fieldIndex: number; fieldMode: "text" | "model" | "thinking" | "skills" | null;
17
- fieldEditor: TextEditorState; promptEditor: TextEditorState; modelSearchQuery: string; modelCursor: number; filteredModels: ModelInfo[];
18
- thinkingCursor: number; skillSearchQuery: string; skillCursor: number; filteredSkills: SkillInfo[]; skillSelected: Set<string>; error?: string;
19
- fields: EditField[];
20
- title?: string;
21
- overrideBase?: BuiltinAgentOverrideBase;
22
- }
23
- export interface EditInputResult { action?: "save" | "discard" | "delete"; nextScreen?: EditScreen; }
24
- export interface CreateEditStateOptions {
25
- fields?: EditField[];
26
- title?: string;
27
- overrideBase?: BuiltinAgentOverrideBase;
28
- }
29
-
30
- const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
31
- const FIELD_ORDER = ["name", "description", "model", "fallbackModels", "thinking", "systemPromptMode", "inheritProjectContext", "inheritSkills", "tools", "extensions", "skills", "output", "reads", "progress", "interactive", "prompt"] as const;
32
- type ThinkingLevel = typeof THINKING_LEVELS[number];
33
- const PROMPT_VIEWPORT_HEIGHT = 16;
34
- const MODEL_SELECTOR_HEIGHT = 10;
35
- const SKILL_SELECTOR_HEIGHT = 10;
36
-
37
- function formatTools(draft: Pick<AgentConfig, "tools" | "mcpDirectTools">): string { const tools = [...(draft.tools ?? []), ...(draft.mcpDirectTools ?? []).map((tool) => `mcp:${tool}`)]; return tools.length > 0 ? tools.join(", ") : ""; }
38
- 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; }
39
- 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 }; }
40
- 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; }
41
- 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; }
42
-
43
- function fieldValueMatchesBase(field: EditField, state: EditState): boolean {
44
- const base = state.overrideBase;
45
- if (!base) return false;
46
- switch (field) {
47
- case "model": return state.draft.model === base.model;
48
- case "fallbackModels": return arraysEqual(state.draft.fallbackModels, base.fallbackModels);
49
- case "thinking": return state.draft.thinking === base.thinking;
50
- case "systemPromptMode": return state.draft.systemPromptMode === base.systemPromptMode;
51
- case "inheritProjectContext": return state.draft.inheritProjectContext === base.inheritProjectContext;
52
- case "inheritSkills": return state.draft.inheritSkills === base.inheritSkills;
53
- case "disabled": return state.draft.disabled === base.disabled;
54
- case "tools": return arraysEqual(toolList(state.draft), toolList(base));
55
- case "skills": return arraysEqual(state.draft.skills, base.skills);
56
- case "prompt": return state.draft.systemPrompt === base.systemPrompt;
57
- default: return false;
58
- }
59
- }
60
-
61
- function resetFieldToBase(field: EditField, state: EditState): void {
62
- const base = state.overrideBase;
63
- if (!base) return;
64
- switch (field) {
65
- case "model": state.draft.model = base.model; break;
66
- case "fallbackModels": state.draft.fallbackModels = base.fallbackModels ? [...base.fallbackModels] : undefined; break;
67
- case "thinking": state.draft.thinking = base.thinking; break;
68
- case "systemPromptMode": state.draft.systemPromptMode = base.systemPromptMode; break;
69
- case "inheritProjectContext": state.draft.inheritProjectContext = base.inheritProjectContext; break;
70
- case "inheritSkills": state.draft.inheritSkills = base.inheritSkills; break;
71
- case "disabled": state.draft.disabled = base.disabled; break;
72
- case "tools": state.draft.tools = base.tools ? [...base.tools] : undefined; state.draft.mcpDirectTools = base.mcpDirectTools ? [...base.mcpDirectTools] : undefined; break;
73
- case "skills": state.draft.skills = base.skills ? [...base.skills] : undefined; break;
74
- case "prompt": state.draft.systemPrompt = base.systemPrompt; state.promptEditor = createEditorState(base.systemPrompt); break;
75
- default: break;
76
- }
77
- }
78
-
79
- export function createEditState(draft: AgentConfig, isNew: boolean, models: ModelInfo[], skills: SkillInfo[], options: CreateEditStateOptions = {}): EditState {
80
- return {
81
- 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 },
82
- isNew, fieldIndex: 0, fieldMode: null, fieldEditor: createEditorState(), promptEditor: createEditorState(draft.systemPrompt ?? ""),
83
- modelSearchQuery: "", modelCursor: 0, filteredModels: [...models], thinkingCursor: 0, skillSearchQuery: "", skillCursor: 0, filteredSkills: [...skills], skillSelected: new Set(draft.skills ?? []),
84
- fields: options.fields ?? [...FIELD_ORDER], title: options.title, overrideBase: options.overrideBase,
85
- };
86
- }
87
-
88
- function renderFieldValue(field: EditField, state: EditState): string {
89
- const draft = state.draft;
90
- switch (field) {
91
- case "name": return draft.name;
92
- case "description": return draft.description;
93
- case "model": return draft.model ?? "default";
94
- case "fallbackModels": return draft.fallbackModels && draft.fallbackModels.length > 0 ? draft.fallbackModels.join(", ") : "";
95
- case "thinking": return draft.thinking ?? "off";
96
- case "systemPromptMode": return draft.systemPromptMode ?? defaultSystemPromptMode(draft.name);
97
- case "inheritProjectContext": return draft.inheritProjectContext ? "on" : "off";
98
- case "inheritSkills": return draft.inheritSkills ? "on" : "off";
99
- case "disabled": return draft.disabled ? "on" : "off";
100
- case "tools": return formatTools(draft);
101
- case "extensions": return draft.extensions !== undefined ? (draft.extensions.length > 0 ? draft.extensions.join(", ") : "") : "(all)";
102
- case "skills": return draft.skills && draft.skills.length > 0 ? draft.skills.join(", ") : "";
103
- case "output": return draft.output ?? "";
104
- case "reads": return draft.defaultReads && draft.defaultReads.length > 0 ? draft.defaultReads.join(", ") : "";
105
- case "progress": return draft.defaultProgress ? "on" : "off";
106
- case "interactive": return draft.interactive ? "on" : "off";
107
- default: return "";
108
- }
109
- }
110
-
111
- function applyFieldValue(field: EditField, state: EditState, value: string): void {
112
- const draft = state.draft;
113
- switch (field) {
114
- case "name": draft.name = value.trim(); break;
115
- case "description": draft.description = value.trim(); break;
116
- case "model": draft.model = value.trim() || undefined; break;
117
- case "fallbackModels": draft.fallbackModels = parseCommaList(value); break;
118
- case "systemPromptMode": {
119
- const trimmed = value.trim();
120
- if (trimmed === "") {
121
- draft.systemPromptMode = defaultSystemPromptMode(draft.name);
122
- break;
123
- }
124
- if (trimmed === "append" || trimmed === "replace") {
125
- draft.systemPromptMode = trimmed;
126
- }
127
- break;
128
- }
129
- case "tools": { const parsed = parseTools(value); draft.tools = parsed.tools; draft.mcpDirectTools = parsed.mcp; break; }
130
- case "extensions": { const trimmed = value.trim(); draft.extensions = trimmed === "(all)" ? undefined : parseCommaList(trimmed) ?? []; break; }
131
- case "skills": draft.skills = parseCommaList(value); break;
132
- case "output": { const trimmed = value.trim(); draft.output = trimmed.length > 0 ? trimmed : undefined; break; }
133
- case "reads": draft.defaultReads = parseCommaList(value); break;
134
- case "inheritProjectContext":
135
- case "inheritSkills":
136
- case "disabled":
137
- case "progress":
138
- case "interactive":
139
- case "prompt":
140
- break;
141
- }
142
- }
143
-
144
- function openModelPicker(state: EditState, models: ModelInfo[]): void {
145
- state.fieldIndex = state.fields.indexOf("model"); state.fieldMode = "model"; state.modelSearchQuery = ""; state.filteredModels = [...models];
146
- const idx = state.filteredModels.findIndex((m) => m.fullId === state.draft.model || m.id === state.draft.model); state.modelCursor = idx >= 0 ? idx : 0;
147
- }
148
- function openThinkingPicker(state: EditState): void {
149
- state.fieldIndex = state.fields.indexOf("thinking"); state.fieldMode = "thinking";
150
- const idx = THINKING_LEVELS.indexOf((state.draft.thinking ?? "off") as ThinkingLevel); state.thinkingCursor = idx >= 0 ? idx : 0;
151
- }
152
- function openSkillPicker(state: EditState, skills: SkillInfo[]): void {
153
- state.fieldIndex = state.fields.indexOf("skills"); state.fieldMode = "skills"; state.skillSearchQuery = ""; state.filteredSkills = [...skills]; state.skillSelected = new Set(state.draft.skills ?? []); state.skillCursor = 0;
154
- }
155
-
156
- function renderModelPicker(state: EditState, width: number, theme: Theme): string[] {
157
- const lines: string[] = [];
158
- lines.push(renderHeader(" Select Model ", width, theme));
159
- lines.push(row("", width, theme));
160
- const cursor = "\x1b[7m \x1b[27m";
161
- lines.push(row(` ${theme.fg("dim", "Search: ")}${state.modelSearchQuery}${cursor}`, width, theme));
162
- lines.push(row("", width, theme));
163
- const currentModel = state.draft.model ?? "default";
164
- lines.push(row(` ${theme.fg("dim", "Current: ")}${theme.fg("warning", currentModel)}`, width, theme));
165
- lines.push(row("", width, theme));
166
- const list = state.filteredModels;
167
- if (list.length === 0) {
168
- lines.push(row(` ${theme.fg("dim", "No matching models")}`, width, theme));
169
- } else {
170
- const maxVisible = MODEL_SELECTOR_HEIGHT; let startIdx = 0;
171
- if (list.length > maxVisible) { startIdx = Math.max(0, state.modelCursor - Math.floor(maxVisible / 2)); startIdx = Math.min(startIdx, list.length - maxVisible); }
172
- const endIdx = Math.min(startIdx + maxVisible, list.length);
173
- if (startIdx > 0) lines.push(row(` ${theme.fg("dim", ` ↑ ${startIdx} more`)}`, width, theme));
174
- 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)); }
175
- const remaining = list.length - endIdx; if (remaining > 0) lines.push(row(` ${theme.fg("dim", ` ↓ ${remaining} more`)}`, width, theme));
176
- }
177
- while (lines.length < 19) lines.push(row("", width, theme));
178
- lines.push(renderFooter(" [enter] select [esc] cancel type to search ", width, theme));
179
- return lines;
180
- }
181
-
182
- function renderThinkingPicker(state: EditState, width: number, theme: Theme): string[] {
183
- const lines: string[] = [];
184
- lines.push(renderHeader(" Select Thinking Level ", width, theme));
185
- lines.push(row("", width, theme));
186
- const currentModel = state.draft.model ?? "default";
187
- const current = truncateToWidth(currentModel, width - 13);
188
- lines.push(row(` ${theme.fg("dim", "Model: ")}${theme.fg("warning", current)}`, width, theme));
189
- lines.push(row("", width, theme));
190
- const descriptions: Record<ThinkingLevel, string> = {
191
- off: "No extended thinking",
192
- minimal: "Brief reasoning",
193
- low: "Light reasoning",
194
- medium: "Moderate reasoning",
195
- high: "Deep reasoning",
196
- xhigh: "Maximum reasoning (ultrathink)",
197
- };
198
- for (let i = 0; i < THINKING_LEVELS.length; i++) {
199
- const level = THINKING_LEVELS[i]!;
200
- const isSelected = i === state.thinkingCursor;
201
- const prefix = isSelected ? theme.fg("accent", "→ ") : " ";
202
- const levelText = isSelected ? theme.fg("accent", level) : level;
203
- const desc = theme.fg("dim", ` - ${descriptions[level]}`);
204
- lines.push(row(` ${prefix}${levelText}${desc}`, width, theme));
205
- }
206
- while (lines.length < 19) lines.push(row("", width, theme));
207
- lines.push(renderFooter(" [enter] select [esc] cancel [↑↓] navigate ", width, theme));
208
- return lines;
209
- }
210
-
211
- function renderSkillPicker(state: EditState, width: number, theme: Theme): string[] {
212
- const lines: string[] = [];
213
- lines.push(renderHeader(" Select Skills ", width, theme));
214
- lines.push(row("", width, theme));
215
- const cursor = "\x1b[7m \x1b[27m";
216
- lines.push(row(` ${theme.fg("dim", "Search: ")}${state.skillSearchQuery}${cursor}`, width, theme));
217
- lines.push(row("", width, theme));
218
- const selected = [...state.skillSelected].join(", ") || theme.fg("dim", "(none)");
219
- lines.push(row(` ${theme.fg("dim", "Selected: ")}${truncateToWidth(selected, width - 14)}`, width, theme));
220
- lines.push(row("", width, theme));
221
- const list = state.filteredSkills;
222
- if (list.length === 0) {
223
- lines.push(row(` ${theme.fg("dim", "No matching skills")}`, width, theme));
224
- } else {
225
- let startIdx = 0;
226
- 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); }
227
- const endIdx = Math.min(startIdx + SKILL_SELECTOR_HEIGHT, list.length);
228
- if (startIdx > 0) lines.push(row(` ${theme.fg("dim", ` ↑ ${startIdx} more`)}`, width, theme));
229
- 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)); }
230
- const remaining = list.length - endIdx; if (remaining > 0) lines.push(row(` ${theme.fg("dim", ` ↓ ${remaining} more`)}`, width, theme));
231
- }
232
- while (lines.length < 19) lines.push(row("", width, theme));
233
- lines.push(renderFooter(" [enter] confirm [space] toggle [esc] cancel ", width, theme));
234
- return lines;
235
- }
236
-
237
- function renderPromptEditor(state: EditState, width: number, theme: Theme): string[] {
238
- const lines: string[] = [];
239
- lines.push(renderHeader(" Editing System Prompt ", width, theme));
240
- lines.push(row("", width, theme));
241
- const textWidth = Math.max(10, width - 4);
242
- const wrapped = wrapText(state.promptEditor.buffer, textWidth);
243
- const cursorPos = getCursorDisplayPos(state.promptEditor.cursor, wrapped.starts);
244
- state.promptEditor.viewportOffset = ensureCursorVisible(cursorPos.line, PROMPT_VIEWPORT_HEIGHT, state.promptEditor.viewportOffset);
245
- const editorLines = renderEditor(state.promptEditor, textWidth, PROMPT_VIEWPORT_HEIGHT);
246
- for (const line of editorLines) lines.push(row(` ${line}`, width, theme));
247
- const scrollInfo = formatScrollInfo(state.promptEditor.viewportOffset, Math.max(0, wrapped.lines.length - state.promptEditor.viewportOffset - PROMPT_VIEWPORT_HEIGHT));
248
- lines.push(row(scrollInfo ? ` ${theme.fg("dim", scrollInfo)}` : "", width, theme));
249
- lines.push(renderFooter(" [esc] done ", width, theme));
250
- return lines;
251
- }
252
-
253
- export function handleEditInput(screen: EditScreen, state: EditState, data: string, width: number, models: ModelInfo[], skills: SkillInfo[]): EditInputResult | undefined {
254
- if (screen === "edit") {
255
- if (matchesKey(data, "ctrl+s")) return { action: "save" };
256
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) return { action: "discard" };
257
- if (data === "D" && state.overrideBase) return { action: "delete" };
258
- if (matchesKey(data, "up")) { state.fieldIndex = Math.max(0, state.fieldIndex - 1); return; }
259
- if (matchesKey(data, "down")) { state.fieldIndex = Math.min(state.fields.length - 1, state.fieldIndex + 1); return; }
260
- const field = state.fields[state.fieldIndex]!;
261
- if (data === "r" && state.overrideBase) { resetFieldToBase(field, state); return; }
262
- if (data === "m") { openModelPicker(state, models); return { nextScreen: "edit-field" }; }
263
- if (data === "t") { openThinkingPicker(state); return { nextScreen: "edit-field" }; }
264
- if (data === "s") { openSkillPicker(state, skills); return { nextScreen: "edit-field" }; }
265
- if (data === " " && (field === "inheritProjectContext" || field === "inheritSkills" || field === "disabled" || field === "progress" || field === "interactive")) {
266
- if (field === "inheritProjectContext") state.draft.inheritProjectContext = !state.draft.inheritProjectContext;
267
- if (field === "inheritSkills") state.draft.inheritSkills = !state.draft.inheritSkills;
268
- if (field === "disabled") state.draft.disabled = !state.draft.disabled;
269
- if (field === "progress") state.draft.defaultProgress = !state.draft.defaultProgress;
270
- if (field === "interactive") state.draft.interactive = !state.draft.interactive;
271
- return;
272
- }
273
- if (matchesKey(data, "return")) {
274
- if (field === "model") { openModelPicker(state, models); return { nextScreen: "edit-field" }; }
275
- if (field === "thinking") { openThinkingPicker(state); return { nextScreen: "edit-field" }; }
276
- if (field === "skills") { openSkillPicker(state, skills); return { nextScreen: "edit-field" }; }
277
- if (field === "prompt") { state.promptEditor = createEditorState(state.draft.systemPrompt ?? ""); return { nextScreen: "edit-prompt" }; }
278
- if (field === "inheritProjectContext" || field === "inheritSkills" || field === "disabled" || field === "progress" || field === "interactive") return;
279
- state.fieldMode = "text"; state.fieldEditor = createEditorState(renderFieldValue(field, state)); return { nextScreen: "edit-field" };
280
- }
281
- return;
282
- }
283
- if (screen === "edit-field") {
284
- if (state.fieldMode === "model") {
285
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { state.fieldMode = null; return { nextScreen: "edit" }; }
286
- if (matchesKey(data, "return")) { const selected = state.filteredModels[state.modelCursor]; if (selected) state.draft.model = selected.fullId; state.fieldMode = null; return { nextScreen: "edit" }; }
287
- if (matchesKey(data, "up")) { if (state.filteredModels.length > 0) state.modelCursor = state.modelCursor === 0 ? state.filteredModels.length - 1 : state.modelCursor - 1; return; }
288
- if (matchesKey(data, "down")) { if (state.filteredModels.length > 0) state.modelCursor = state.modelCursor === state.filteredModels.length - 1 ? 0 : state.modelCursor + 1; return; }
289
- if (matchesKey(data, "backspace")) { if (state.modelSearchQuery.length > 0) state.modelSearchQuery = state.modelSearchQuery.slice(0, -1); }
290
- else if (data.length === 1 && data.charCodeAt(0) >= 32) state.modelSearchQuery += data;
291
- const query = state.modelSearchQuery.toLowerCase();
292
- state.filteredModels = query ? models.filter((m) => m.fullId.toLowerCase().includes(query) || m.id.toLowerCase().includes(query) || m.provider.toLowerCase().includes(query)) : [...models];
293
- state.modelCursor = Math.min(state.modelCursor, Math.max(0, state.filteredModels.length - 1));
294
- return;
295
- }
296
- if (state.fieldMode === "thinking") {
297
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { state.fieldMode = null; return { nextScreen: "edit" }; }
298
- if (matchesKey(data, "return")) { const selected = THINKING_LEVELS[state.thinkingCursor]; state.draft.thinking = selected === "off" ? undefined : selected; state.fieldMode = null; return { nextScreen: "edit" }; }
299
- if (matchesKey(data, "up")) { state.thinkingCursor = state.thinkingCursor === 0 ? THINKING_LEVELS.length - 1 : state.thinkingCursor - 1; return; }
300
- if (matchesKey(data, "down")) { state.thinkingCursor = state.thinkingCursor === THINKING_LEVELS.length - 1 ? 0 : state.thinkingCursor + 1; return; }
301
- return;
302
- }
303
- if (state.fieldMode === "skills") {
304
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { state.fieldMode = null; return { nextScreen: "edit" }; }
305
- if (matchesKey(data, "return")) { const selected = [...state.skillSelected]; state.draft.skills = selected.length > 0 ? selected : undefined; state.fieldMode = null; return { nextScreen: "edit" }; }
306
- 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; }
307
- if (matchesKey(data, "up")) { if (state.filteredSkills.length > 0) state.skillCursor = state.skillCursor === 0 ? state.filteredSkills.length - 1 : state.skillCursor - 1; return; }
308
- if (matchesKey(data, "down")) { if (state.filteredSkills.length > 0) state.skillCursor = state.skillCursor === state.filteredSkills.length - 1 ? 0 : state.skillCursor + 1; return; }
309
- if (matchesKey(data, "backspace")) { if (state.skillSearchQuery.length > 0) state.skillSearchQuery = state.skillSearchQuery.slice(0, -1); }
310
- else if (data.length === 1 && data.charCodeAt(0) >= 32) state.skillSearchQuery += data;
311
- const query = state.skillSearchQuery.toLowerCase();
312
- state.filteredSkills = query ? skills.filter((s) => s.name.toLowerCase().includes(query) || (s.description?.toLowerCase().includes(query) ?? false)) : [...skills];
313
- state.skillCursor = Math.min(state.skillCursor, Math.max(0, state.filteredSkills.length - 1));
314
- return;
315
- }
316
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { state.fieldMode = null; return { nextScreen: "edit" }; }
317
- if (matchesKey(data, "return")) { const field = state.fields[state.fieldIndex]!; applyFieldValue(field, state, state.fieldEditor.buffer); state.fieldMode = null; return { nextScreen: "edit" }; }
318
- if (matchesKey(data, "tab")) return;
319
- const innerW = width - 2; const labelWidth = 12; const textWidth = Math.max(10, innerW - labelWidth - 6);
320
- const nextState = handleEditorInput(state.fieldEditor, data, textWidth); if (nextState) state.fieldEditor = nextState; return;
321
- }
322
- if (screen === "edit-prompt") {
323
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { state.draft.systemPrompt = state.promptEditor.buffer; return { nextScreen: "edit" }; }
324
- const textWidth = Math.max(10, width - 4);
325
- 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; }
326
- 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; }
327
- const nextState = handleEditorInput(state.promptEditor, data, textWidth, { multiLine: true }); if (nextState) state.promptEditor = nextState; return;
328
- }
329
- return;
330
- }
331
-
332
- export function renderEdit(screen: EditScreen, state: EditState, width: number, theme: Theme): string[] {
333
- if (screen === "edit-field" && state.fieldMode === "model") return renderModelPicker(state, width, theme);
334
- if (screen === "edit-field" && state.fieldMode === "thinking") return renderThinkingPicker(state, width, theme);
335
- if (screen === "edit-field" && state.fieldMode === "skills") return renderSkillPicker(state, width, theme);
336
- if (screen === "edit-prompt") return renderPromptEditor(state, width, theme);
337
- const lines: string[] = [];
338
- const scopeBadge = state.draft.source === "user" ? "[user]" : "[proj]"; const label = state.isNew ? " [new]" : "";
339
- lines.push(renderHeader(` ${state.title ?? `Editing: ${state.draft.name} ${scopeBadge}${label}`} `, width, theme));
340
- lines.push(row("", width, theme));
341
- const innerW = width - 2; const labelWidth = 12; const valueWidth = Math.max(10, innerW - labelWidth - 6);
342
- for (let i = 0; i < state.fields.length; i++) {
343
- const field = state.fields[i]!; if (field === "prompt") break;
344
- const isFocused = i === state.fieldIndex; const prefix = isFocused ? theme.fg("accent", "▸ ") : " ";
345
- const fieldLabel = field === "systemPromptMode"
346
- ? "Prompt Mode"
347
- : field === "inheritProjectContext"
348
- ? "Project Ctx"
349
- : field === "inheritSkills"
350
- ? "Skills Ctx"
351
- : field === "disabled"
352
- ? "Disabled"
353
- : `${field[0]!.toUpperCase()}${field.slice(1)}`;
354
- const rawLabel = pad(`${fieldLabel}:`, labelWidth);
355
- const labelText = state.overrideBase && !fieldValueMatchesBase(field, state) ? theme.fg("accent", rawLabel) : rawLabel; let valueText = renderFieldValue(field, state);
356
- 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; }
357
- 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; }
358
- 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; }
359
- 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; }
360
- 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; }
361
- let displayValue = truncateToWidth(valueText, valueWidth);
362
- if (screen === "edit-field" && state.fieldMode === "text" && isFocused) {
363
- const { starts } = wrapText(state.fieldEditor.buffer, valueWidth);
364
- const pos = getCursorDisplayPos(state.fieldEditor.cursor, starts);
365
- state.fieldEditor.viewportOffset = ensureCursorVisible(pos.line, 1, state.fieldEditor.viewportOffset);
366
- const editorLine = renderEditor(state.fieldEditor, valueWidth, 1)[0] ?? "";
367
- displayValue = pad(editorLine, valueWidth);
368
- }
369
- lines.push(row(` ${prefix}${labelText} [${displayValue}]`, width, theme));
370
- }
371
- if (state.fields.includes("prompt")) {
372
- lines.push(row("", width, theme));
373
- const promptIndex = state.fields.indexOf("prompt");
374
- const promptFocused = state.fieldIndex === promptIndex;
375
- const promptPrefix = promptFocused ? theme.fg("accent", "▸ ") : " ";
376
- const promptTitle = state.overrideBase && !fieldValueMatchesBase("prompt", state)
377
- ? theme.fg("accent", "── System Prompt ──")
378
- : theme.fg("dim", "── System Prompt ──");
379
- lines.push(row(` ${promptPrefix}${promptTitle}`, width, theme));
380
- const previewWidth = innerW - 2; const wrapped = wrapText(state.draft.systemPrompt ?? "", previewWidth); const previewLines = wrapped.lines.slice(0, 4);
381
- for (const line of previewLines) lines.push(row(` ${line}`, width, theme));
382
- for (let i = previewLines.length; i < 4; i++) lines.push(row("", width, theme));
383
- }
384
- if (state.error) lines.push(row(` ${theme.fg("error", state.error)}`, width, theme)); else lines.push(row("", width, theme));
385
- const footer = state.overrideBase
386
- ? " [ctrl+s] save [r] reset field [D] remove override [esc] back "
387
- : " [ctrl+s] save [esc] back ";
388
- lines.push(renderFooter(footer, width, theme));
389
- return lines;
390
- }