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,283 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import type { AgentSource } from "../agents/agents.ts";
|
|
3
|
-
import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
4
|
-
import { pad, row, renderHeader, renderFooter, fuzzyFilter, formatScrollInfo } from "../tui/render-helpers.ts";
|
|
5
|
-
|
|
6
|
-
export interface ListAgent {
|
|
7
|
-
id: string;
|
|
8
|
-
name: string;
|
|
9
|
-
description: string;
|
|
10
|
-
model?: string;
|
|
11
|
-
source: AgentSource;
|
|
12
|
-
overrideScope?: "user" | "project";
|
|
13
|
-
disabled?: boolean;
|
|
14
|
-
kind: "agent" | "chain";
|
|
15
|
-
stepCount?: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface ListState {
|
|
19
|
-
cursor: number;
|
|
20
|
-
scrollOffset: number;
|
|
21
|
-
filterQuery: string;
|
|
22
|
-
selected: string[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface ListShortcuts {
|
|
26
|
-
newShortcut: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const DEFAULT_AGENT_MANAGER_NEW_SHORTCUT = "shift+ctrl+n";
|
|
30
|
-
|
|
31
|
-
export type ListAction =
|
|
32
|
-
| { type: "open-detail"; id: string }
|
|
33
|
-
| { type: "clone"; id: string }
|
|
34
|
-
| { type: "new" }
|
|
35
|
-
| { type: "delete"; id: string }
|
|
36
|
-
| { type: "run-chain"; ids: string[] }
|
|
37
|
-
| { type: "run-parallel"; ids: string[] }
|
|
38
|
-
| { type: "close" };
|
|
39
|
-
|
|
40
|
-
const LIST_VIEWPORT_HEIGHT = 8;
|
|
41
|
-
|
|
42
|
-
function selectionCount(selected: string[], id: string): number {
|
|
43
|
-
let count = 0;
|
|
44
|
-
for (const s of selected) if (s === id) count++;
|
|
45
|
-
return count;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function clampCursor(state: ListState, filtered: ListAgent[]): void {
|
|
49
|
-
if (filtered.length === 0) {
|
|
50
|
-
state.cursor = 0;
|
|
51
|
-
state.scrollOffset = 0;
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
state.cursor = Math.max(0, Math.min(state.cursor, filtered.length - 1));
|
|
56
|
-
const maxOffset = Math.max(0, filtered.length - LIST_VIEWPORT_HEIGHT);
|
|
57
|
-
state.scrollOffset = Math.max(0, Math.min(state.scrollOffset, maxOffset));
|
|
58
|
-
|
|
59
|
-
if (state.cursor < state.scrollOffset) {
|
|
60
|
-
state.scrollOffset = state.cursor;
|
|
61
|
-
} else if (state.cursor >= state.scrollOffset + LIST_VIEWPORT_HEIGHT) {
|
|
62
|
-
state.scrollOffset = state.cursor - LIST_VIEWPORT_HEIGHT + 1;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function handleListInput(state: ListState, agents: ListAgent[], data: string, shortcuts: ListShortcuts = { newShortcut: DEFAULT_AGENT_MANAGER_NEW_SHORTCUT }): ListAction | undefined {
|
|
67
|
-
const filtered = fuzzyFilter(agents, state.filterQuery);
|
|
68
|
-
|
|
69
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
70
|
-
if (state.filterQuery.length > 0) {
|
|
71
|
-
state.filterQuery = "";
|
|
72
|
-
state.cursor = 0;
|
|
73
|
-
state.scrollOffset = 0;
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
if (state.selected.length > 0) {
|
|
77
|
-
state.selected.length = 0;
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
return { type: "close" };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (matchesKey(data, "return")) {
|
|
84
|
-
if (filtered.length > 0) {
|
|
85
|
-
const agent = filtered[state.cursor];
|
|
86
|
-
if (agent) return { type: "open-detail", id: agent.id };
|
|
87
|
-
}
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (matchesKey(data, "up") || matchesKey(data, "down")) {
|
|
92
|
-
if (matchesKey(data, "up")) state.cursor -= 1;
|
|
93
|
-
if (matchesKey(data, "down")) state.cursor += 1;
|
|
94
|
-
clampCursor(state, filtered);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (matchesKey(data, "backspace")) {
|
|
99
|
-
if (state.filterQuery.length > 0) {
|
|
100
|
-
state.filterQuery = state.filterQuery.slice(0, -1);
|
|
101
|
-
state.cursor = 0;
|
|
102
|
-
state.scrollOffset = 0;
|
|
103
|
-
}
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (matchesKey(data, shortcuts.newShortcut)) {
|
|
108
|
-
return { type: "new" };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (matchesKey(data, "ctrl+k")) {
|
|
112
|
-
const agent = filtered[state.cursor];
|
|
113
|
-
if (agent) return { type: "clone", id: agent.id };
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (matchesKey(data, "ctrl+d") || matchesKey(data, "delete")) {
|
|
118
|
-
const agent = filtered[state.cursor];
|
|
119
|
-
if (agent) return { type: "delete", id: agent.id };
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (matchesKey(data, "tab")) {
|
|
124
|
-
const agent = filtered[state.cursor];
|
|
125
|
-
if (!agent) return;
|
|
126
|
-
if (agent.kind !== "agent") return;
|
|
127
|
-
state.selected.push(agent.id);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (matchesKey(data, "shift+tab")) {
|
|
132
|
-
const agent = filtered[state.cursor];
|
|
133
|
-
if (!agent) return;
|
|
134
|
-
const lastIdx = state.selected.lastIndexOf(agent.id);
|
|
135
|
-
if (lastIdx >= 0) state.selected.splice(lastIdx, 1);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (matchesKey(data, "ctrl+r")) {
|
|
140
|
-
if (state.selected.length > 0) return { type: "run-chain", ids: [...state.selected] };
|
|
141
|
-
const agent = filtered[state.cursor];
|
|
142
|
-
if (agent && agent.kind === "agent") return { type: "run-chain", ids: [agent.id] };
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (matchesKey(data, "ctrl+p")) {
|
|
147
|
-
if (state.selected.length > 0) return { type: "run-parallel", ids: [...state.selected] };
|
|
148
|
-
const agent = filtered[state.cursor];
|
|
149
|
-
if (agent && agent.kind === "agent") return { type: "run-parallel", ids: [agent.id] };
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
154
|
-
state.filterQuery += data;
|
|
155
|
-
state.cursor = 0;
|
|
156
|
-
state.scrollOffset = 0;
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function renderList(
|
|
164
|
-
state: ListState,
|
|
165
|
-
agents: ListAgent[],
|
|
166
|
-
width: number,
|
|
167
|
-
theme: Theme,
|
|
168
|
-
statusMessage?: { text: string; type: "error" | "info" },
|
|
169
|
-
shortcuts: ListShortcuts = { newShortcut: DEFAULT_AGENT_MANAGER_NEW_SHORTCUT },
|
|
170
|
-
): string[] {
|
|
171
|
-
const lines: string[] = [];
|
|
172
|
-
const filtered = fuzzyFilter(agents, state.filterQuery);
|
|
173
|
-
clampCursor(state, filtered);
|
|
174
|
-
|
|
175
|
-
const agentCount = agents.filter((a) => a.kind === "agent").length;
|
|
176
|
-
const chainCount = agents.filter((a) => a.kind === "chain").length;
|
|
177
|
-
const headerText = chainCount
|
|
178
|
-
? ` Subagents [${agentCount} agents ${chainCount} chains] `
|
|
179
|
-
: ` Subagents [${agentCount}] `;
|
|
180
|
-
lines.push(renderHeader(headerText, width, theme));
|
|
181
|
-
lines.push(row("", width, theme));
|
|
182
|
-
|
|
183
|
-
const cursor = theme.fg("accent", "│");
|
|
184
|
-
const searchIcon = theme.fg("dim", "◎");
|
|
185
|
-
const placeholder = theme.fg("dim", "\x1b[3mtype to filter...\x1b[23m");
|
|
186
|
-
const queryDisplay = state.filterQuery ? `${state.filterQuery}${cursor}` : `${cursor}${placeholder}`;
|
|
187
|
-
lines.push(row(` ${searchIcon} ${queryDisplay}`, width, theme));
|
|
188
|
-
lines.push(row("", width, theme));
|
|
189
|
-
|
|
190
|
-
const userNames = new Set(agents.filter((a) => a.source === "user" && a.kind === "agent").map((a) => a.name));
|
|
191
|
-
const startIdx = state.scrollOffset;
|
|
192
|
-
const endIdx = Math.min(filtered.length, startIdx + LIST_VIEWPORT_HEIGHT);
|
|
193
|
-
const visible = filtered.slice(startIdx, endIdx);
|
|
194
|
-
|
|
195
|
-
if (filtered.length === 0) {
|
|
196
|
-
lines.push(row(` ${theme.fg("dim", "No matching agents")}`, width, theme));
|
|
197
|
-
for (let i = 1; i < LIST_VIEWPORT_HEIGHT; i++) lines.push(row("", width, theme));
|
|
198
|
-
} else {
|
|
199
|
-
const innerW = width - 2;
|
|
200
|
-
const nameWidth = 16;
|
|
201
|
-
const modelWidth = 12;
|
|
202
|
-
const scopeWidth = 21;
|
|
203
|
-
|
|
204
|
-
for (let i = 0; i < visible.length; i++) {
|
|
205
|
-
const agent = visible[i]!;
|
|
206
|
-
const index = startIdx + i;
|
|
207
|
-
const isCursor = index === state.cursor;
|
|
208
|
-
const count = selectionCount(state.selected, agent.id);
|
|
209
|
-
const isShadowed = agent.kind === "agent" && agent.source === "project" && userNames.has(agent.name);
|
|
210
|
-
|
|
211
|
-
const cursorChar = isCursor ? theme.fg("accent", ">") : " ";
|
|
212
|
-
const selectBadge = count > 0 ? theme.fg("accent", String(count).padStart(2)) : " ";
|
|
213
|
-
const shadowMarker = isShadowed ? theme.fg("warning", "!") : " ";
|
|
214
|
-
const prefix = `${cursorChar}${selectBadge}${shadowMarker} `;
|
|
215
|
-
|
|
216
|
-
const modelRaw = agent.kind === "chain" ? `${agent.stepCount ?? 0} steps` : (agent.model ?? "default");
|
|
217
|
-
const modelDisplay = modelRaw.includes("/") ? modelRaw.split("/").pop() ?? modelRaw : modelRaw;
|
|
218
|
-
const nameText = isCursor ? theme.fg("accent", agent.name) : agent.name;
|
|
219
|
-
const modelText = theme.fg("dim", modelDisplay);
|
|
220
|
-
const scopeLabel = agent.kind === "chain"
|
|
221
|
-
? "[chain]"
|
|
222
|
-
: agent.source === "builtin"
|
|
223
|
-
? (agent.disabled
|
|
224
|
-
? (agent.overrideScope ? `[builtin off+${agent.overrideScope}]` : "[builtin off]")
|
|
225
|
-
: (agent.overrideScope ? `[builtin+${agent.overrideScope}]` : "[builtin]"))
|
|
226
|
-
: agent.source === "project"
|
|
227
|
-
? "[proj]"
|
|
228
|
-
: "[user]";
|
|
229
|
-
const scopeBadge = theme.fg("dim", scopeLabel);
|
|
230
|
-
const descText = theme.fg("dim", agent.description);
|
|
231
|
-
|
|
232
|
-
const descWidth = Math.max(0, innerW - 1 - visibleWidth(prefix) - nameWidth - modelWidth - scopeWidth - 3);
|
|
233
|
-
const line =
|
|
234
|
-
prefix +
|
|
235
|
-
pad(truncateToWidth(nameText, nameWidth), nameWidth) +
|
|
236
|
-
" " +
|
|
237
|
-
pad(truncateToWidth(modelText, modelWidth), modelWidth) +
|
|
238
|
-
" " +
|
|
239
|
-
pad(scopeBadge, scopeWidth) +
|
|
240
|
-
" " +
|
|
241
|
-
truncateToWidth(descText, descWidth);
|
|
242
|
-
|
|
243
|
-
lines.push(row(` ${line}`, width, theme));
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
for (let i = visible.length; i < LIST_VIEWPORT_HEIGHT; i++) {
|
|
247
|
-
lines.push(row("", width, theme));
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const scrollInfo = formatScrollInfo(state.scrollOffset, Math.max(0, filtered.length - (state.scrollOffset + LIST_VIEWPORT_HEIGHT)));
|
|
252
|
-
const selectedNames = state.selected
|
|
253
|
-
.map((id) => agents.find((a) => a.id === id))
|
|
254
|
-
.filter((a): a is ListAgent => Boolean(a))
|
|
255
|
-
.map((a) => a.name);
|
|
256
|
-
const preview = selectedNames.length > 0 ? truncateToWidth(selectedNames.join(" → "), width - 4) : "";
|
|
257
|
-
|
|
258
|
-
lines.push(row("", width, theme));
|
|
259
|
-
|
|
260
|
-
if (statusMessage) {
|
|
261
|
-
const color = statusMessage.type === "error" ? "error" : "success";
|
|
262
|
-
lines.push(row(` ${theme.fg(color, truncateToWidth(statusMessage.text, width - 4))}`, width, theme));
|
|
263
|
-
} else if (preview) {
|
|
264
|
-
lines.push(row(` ${theme.fg("dim", preview)}`, width, theme));
|
|
265
|
-
} else {
|
|
266
|
-
const cursorAgent = filtered[state.cursor];
|
|
267
|
-
const desc = cursorAgent ? truncateToWidth(cursorAgent.description, width - 4) : "";
|
|
268
|
-
const content = desc || scrollInfo;
|
|
269
|
-
lines.push(row(content ? ` ${theme.fg("dim", content)}` : "", width, theme));
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
lines.push(row("", width, theme));
|
|
273
|
-
|
|
274
|
-
const selCount = state.selected.length;
|
|
275
|
-
const footerText = selCount > 1
|
|
276
|
-
? ` [ctrl+r] chain [ctrl+p] parallel [tab] add [shift+tab] remove [esc] clear (${selCount}) `
|
|
277
|
-
: selCount === 1
|
|
278
|
-
? " [ctrl+r] run [ctrl+p] parallel [tab] add more [shift+tab] remove [esc] clear "
|
|
279
|
-
: ` [enter] view [ctrl+r] run [tab] select [${shortcuts.newShortcut}] new [esc] close `;
|
|
280
|
-
lines.push(renderFooter(footerText, width, theme));
|
|
281
|
-
|
|
282
|
-
return lines;
|
|
283
|
-
}
|
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
3
|
-
import type { TextEditorState } from "../tui/text-editor.ts";
|
|
4
|
-
import { createEditorState, handleEditorInput, renderEditor, wrapText, getCursorDisplayPos, ensureCursorVisible } from "../tui/text-editor.ts";
|
|
5
|
-
import { pad, row, renderHeader, renderFooter, fuzzyFilter } from "../tui/render-helpers.ts";
|
|
6
|
-
|
|
7
|
-
interface ParallelSlot {
|
|
8
|
-
agentName: string;
|
|
9
|
-
customTask: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface ParallelState {
|
|
13
|
-
slots: ParallelSlot[];
|
|
14
|
-
cursor: number;
|
|
15
|
-
scrollOffset: number;
|
|
16
|
-
mode: "browse" | "add" | "edit-task";
|
|
17
|
-
addQuery: string;
|
|
18
|
-
addCursor: number;
|
|
19
|
-
editIndex: number;
|
|
20
|
-
editEditor: TextEditorState | null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type ParallelAction =
|
|
24
|
-
| { type: "proceed" }
|
|
25
|
-
| { type: "back" };
|
|
26
|
-
|
|
27
|
-
export interface AgentOption {
|
|
28
|
-
name: string;
|
|
29
|
-
description: string;
|
|
30
|
-
model?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const CONTENT_HEIGHT = 14;
|
|
34
|
-
const SLOT_VIEWPORT_BROWSE = 12;
|
|
35
|
-
const SLOT_VIEWPORT_COMPACT = 5;
|
|
36
|
-
const ADD_RESULTS_MAX = 5;
|
|
37
|
-
|
|
38
|
-
export function createParallelState(agentNames: string[]): ParallelState {
|
|
39
|
-
return {
|
|
40
|
-
slots: agentNames.map((name) => ({ agentName: name, customTask: "" })),
|
|
41
|
-
cursor: 0,
|
|
42
|
-
scrollOffset: 0,
|
|
43
|
-
mode: "browse",
|
|
44
|
-
addQuery: "",
|
|
45
|
-
addCursor: 0,
|
|
46
|
-
editIndex: -1,
|
|
47
|
-
editEditor: null,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function clampSlotScroll(state: ParallelState, viewport: number): void {
|
|
52
|
-
if (state.slots.length === 0) { state.cursor = 0; state.scrollOffset = 0; return; }
|
|
53
|
-
state.cursor = Math.max(0, Math.min(state.cursor, state.slots.length - 1));
|
|
54
|
-
const maxOffset = Math.max(0, state.slots.length - viewport);
|
|
55
|
-
state.scrollOffset = Math.max(0, Math.min(state.scrollOffset, maxOffset));
|
|
56
|
-
if (state.cursor < state.scrollOffset) state.scrollOffset = state.cursor;
|
|
57
|
-
else if (state.cursor >= state.scrollOffset + viewport) state.scrollOffset = state.cursor - viewport + 1;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function handleParallelInput(
|
|
61
|
-
state: ParallelState,
|
|
62
|
-
agents: AgentOption[],
|
|
63
|
-
data: string,
|
|
64
|
-
width: number,
|
|
65
|
-
): ParallelAction | undefined {
|
|
66
|
-
switch (state.mode) {
|
|
67
|
-
case "browse": return handleBrowse(state, data);
|
|
68
|
-
case "add": return handleAdd(state, agents, data);
|
|
69
|
-
case "edit-task": return handleEditTask(state, data, width);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function handleBrowse(state: ParallelState, data: string): ParallelAction | undefined {
|
|
74
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) return { type: "back" };
|
|
75
|
-
if (matchesKey(data, "up")) { state.cursor = Math.max(0, state.cursor - 1); clampSlotScroll(state, SLOT_VIEWPORT_BROWSE); return; }
|
|
76
|
-
if (matchesKey(data, "down")) { state.cursor = Math.min(state.slots.length - 1, state.cursor + 1); clampSlotScroll(state, SLOT_VIEWPORT_BROWSE); return; }
|
|
77
|
-
|
|
78
|
-
if (matchesKey(data, "ctrl+a")) {
|
|
79
|
-
state.mode = "add";
|
|
80
|
-
state.addQuery = "";
|
|
81
|
-
state.addCursor = 0;
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (matchesKey(data, "delete") || matchesKey(data, "ctrl+d")) {
|
|
86
|
-
if (state.slots.length > 1) {
|
|
87
|
-
state.slots.splice(state.cursor, 1);
|
|
88
|
-
state.cursor = Math.min(state.cursor, state.slots.length - 1);
|
|
89
|
-
clampSlotScroll(state, SLOT_VIEWPORT_BROWSE);
|
|
90
|
-
}
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (matchesKey(data, "return")) {
|
|
95
|
-
if (state.slots.length === 0) return;
|
|
96
|
-
state.mode = "edit-task";
|
|
97
|
-
state.editIndex = state.cursor;
|
|
98
|
-
state.editEditor = createEditorState(state.slots[state.cursor]!.customTask);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (matchesKey(data, "ctrl+r")) {
|
|
103
|
-
if (state.slots.length >= 2) return { type: "proceed" };
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function handleAdd(state: ParallelState, agents: AgentOption[], data: string): ParallelAction | undefined {
|
|
111
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { state.mode = "browse"; return; }
|
|
112
|
-
|
|
113
|
-
const filtered = fuzzyFilter(agents, state.addQuery);
|
|
114
|
-
|
|
115
|
-
if (matchesKey(data, "up")) { state.addCursor = Math.max(0, state.addCursor - 1); return; }
|
|
116
|
-
if (matchesKey(data, "down")) { state.addCursor = Math.min(Math.max(0, filtered.length - 1), state.addCursor + 1); return; }
|
|
117
|
-
|
|
118
|
-
if (matchesKey(data, "return")) {
|
|
119
|
-
const selected = filtered[state.addCursor];
|
|
120
|
-
if (selected) {
|
|
121
|
-
state.slots.push({ agentName: selected.name, customTask: "" });
|
|
122
|
-
state.cursor = state.slots.length - 1;
|
|
123
|
-
clampSlotScroll(state, SLOT_VIEWPORT_BROWSE);
|
|
124
|
-
}
|
|
125
|
-
state.mode = "browse";
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (matchesKey(data, "backspace")) {
|
|
130
|
-
state.addQuery = state.addQuery.slice(0, -1);
|
|
131
|
-
state.addCursor = 0;
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
136
|
-
state.addQuery += data;
|
|
137
|
-
state.addCursor = 0;
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function handleEditTask(state: ParallelState, data: string, width: number): ParallelAction | undefined {
|
|
145
|
-
if (!state.editEditor) { state.mode = "browse"; return; }
|
|
146
|
-
|
|
147
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
148
|
-
state.editEditor = null;
|
|
149
|
-
state.mode = "browse";
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (matchesKey(data, "return")) {
|
|
154
|
-
const slot = state.slots[state.editIndex];
|
|
155
|
-
if (slot) slot.customTask = state.editEditor.buffer.trim();
|
|
156
|
-
state.editEditor = null;
|
|
157
|
-
state.mode = "browse";
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (matchesKey(data, "tab")) return;
|
|
162
|
-
|
|
163
|
-
const innerW = width - 2;
|
|
164
|
-
const boxInnerWidth = Math.max(10, innerW - 4);
|
|
165
|
-
const nextState = handleEditorInput(state.editEditor, data, boxInnerWidth);
|
|
166
|
-
if (nextState) state.editEditor = nextState;
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function renderSlotLine(
|
|
171
|
-
slot: ParallelSlot,
|
|
172
|
-
slotNumber: number,
|
|
173
|
-
isCursor: boolean,
|
|
174
|
-
width: number,
|
|
175
|
-
theme: Theme,
|
|
176
|
-
): string {
|
|
177
|
-
const cursor = isCursor ? theme.fg("accent", "▸") : " ";
|
|
178
|
-
const num = theme.fg("accent", slotNumber.toString().padStart(2));
|
|
179
|
-
const name = isCursor ? theme.fg("accent", slot.agentName) : slot.agentName;
|
|
180
|
-
const nameWidth = 16;
|
|
181
|
-
const prefix = `${cursor} ${num} `;
|
|
182
|
-
const prefixVis = visibleWidth(prefix);
|
|
183
|
-
const nameStr = pad(truncateToWidth(name, nameWidth), nameWidth);
|
|
184
|
-
|
|
185
|
-
if (slot.customTask) {
|
|
186
|
-
const taskWidth = Math.max(0, width - 2 - prefixVis - nameWidth - 3);
|
|
187
|
-
const taskPreview = theme.fg("dim", `"${truncateToWidth(slot.customTask, Math.max(0, taskWidth - 2))}"`);
|
|
188
|
-
return ` ${prefix}${nameStr} ${taskPreview}`;
|
|
189
|
-
}
|
|
190
|
-
return ` ${prefix}${nameStr}`;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export function renderParallel(
|
|
194
|
-
state: ParallelState,
|
|
195
|
-
agents: AgentOption[],
|
|
196
|
-
width: number,
|
|
197
|
-
theme: Theme,
|
|
198
|
-
): string[] {
|
|
199
|
-
const lines: string[] = [];
|
|
200
|
-
lines.push(renderHeader(" Parallel Builder ", width, theme));
|
|
201
|
-
lines.push(row("", width, theme));
|
|
202
|
-
|
|
203
|
-
const contentLines: string[] = [];
|
|
204
|
-
|
|
205
|
-
if (state.mode === "browse") {
|
|
206
|
-
clampSlotScroll(state, SLOT_VIEWPORT_BROWSE);
|
|
207
|
-
const start = state.scrollOffset;
|
|
208
|
-
const end = Math.min(state.slots.length, start + SLOT_VIEWPORT_BROWSE);
|
|
209
|
-
for (let i = start; i < end; i++) {
|
|
210
|
-
contentLines.push(renderSlotLine(state.slots[i]!, i + 1, i === state.cursor, width, theme));
|
|
211
|
-
}
|
|
212
|
-
} else if (state.mode === "add") {
|
|
213
|
-
const slotLines = Math.min(state.slots.length, SLOT_VIEWPORT_COMPACT);
|
|
214
|
-
const slotStart = Math.max(0, state.slots.length - slotLines);
|
|
215
|
-
for (let i = slotStart; i < state.slots.length; i++) {
|
|
216
|
-
contentLines.push(renderSlotLine(state.slots[i]!, i + 1, false, width, theme));
|
|
217
|
-
}
|
|
218
|
-
contentLines.push("");
|
|
219
|
-
|
|
220
|
-
const searchIcon = theme.fg("dim", "◎");
|
|
221
|
-
const cursor = theme.fg("accent", "│");
|
|
222
|
-
const placeholder = theme.fg("dim", "\x1b[3msearch agents...\x1b[23m");
|
|
223
|
-
const queryDisplay = state.addQuery ? `${state.addQuery}${cursor}` : `${cursor}${placeholder}`;
|
|
224
|
-
contentLines.push(` ${searchIcon} ${queryDisplay}`);
|
|
225
|
-
|
|
226
|
-
const filtered = fuzzyFilter(agents, state.addQuery);
|
|
227
|
-
const addStart = Math.max(0, Math.min(state.addCursor - ADD_RESULTS_MAX + 1, filtered.length - ADD_RESULTS_MAX));
|
|
228
|
-
const addEnd = Math.min(filtered.length, addStart + ADD_RESULTS_MAX);
|
|
229
|
-
for (let i = addStart; i < addEnd; i++) {
|
|
230
|
-
const a = filtered[i]!;
|
|
231
|
-
const isCursor = i === state.addCursor;
|
|
232
|
-
const cur = isCursor ? theme.fg("accent", "▸") : " ";
|
|
233
|
-
const nameStr = isCursor ? theme.fg("accent", a.name) : a.name;
|
|
234
|
-
const descWidth = Math.max(0, width - 2 - 1 - 1 - 16 - 2);
|
|
235
|
-
contentLines.push(` ${cur} ${pad(truncateToWidth(nameStr, 16), 16)} ${theme.fg("dim", truncateToWidth(a.description, descWidth))}`);
|
|
236
|
-
}
|
|
237
|
-
} else if (state.mode === "edit-task") {
|
|
238
|
-
const slotLines = Math.min(state.slots.length, SLOT_VIEWPORT_COMPACT);
|
|
239
|
-
const slotStart = Math.max(0, Math.min(state.editIndex - Math.floor(slotLines / 2), state.slots.length - slotLines));
|
|
240
|
-
for (let i = slotStart; i < slotStart + slotLines; i++) {
|
|
241
|
-
contentLines.push(renderSlotLine(state.slots[i]!, i + 1, i === state.editIndex, width, theme));
|
|
242
|
-
}
|
|
243
|
-
contentLines.push("");
|
|
244
|
-
|
|
245
|
-
const slot = state.slots[state.editIndex];
|
|
246
|
-
const label = slot ? `Task for ${slot.agentName} (slot ${state.editIndex + 1}):` : "Task:";
|
|
247
|
-
contentLines.push(` ${theme.fg("dim", label)}`);
|
|
248
|
-
|
|
249
|
-
const innerW = width - 2;
|
|
250
|
-
const boxInnerWidth = Math.max(10, innerW - 4);
|
|
251
|
-
contentLines.push(` \u250C${"\u2500".repeat(boxInnerWidth)}\u2510`);
|
|
252
|
-
|
|
253
|
-
if (state.editEditor) {
|
|
254
|
-
const editorState = { ...state.editEditor };
|
|
255
|
-
const wrapped = wrapText(editorState.buffer, boxInnerWidth);
|
|
256
|
-
const cursorPos = getCursorDisplayPos(editorState.cursor, wrapped.starts);
|
|
257
|
-
editorState.viewportOffset = ensureCursorVisible(cursorPos.line, 1, editorState.viewportOffset);
|
|
258
|
-
const editorLine = renderEditor(editorState, boxInnerWidth, 1)[0] ?? "";
|
|
259
|
-
contentLines.push(` \u2502${pad(editorLine, boxInnerWidth)}\u2502`);
|
|
260
|
-
} else {
|
|
261
|
-
contentLines.push(` \u2502${pad("", boxInnerWidth)}\u2502`);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
contentLines.push(` \u2514${"\u2500".repeat(boxInnerWidth)}\u2518`);
|
|
265
|
-
contentLines.push(` ${theme.fg("dim", "Empty = use shared task")}`);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
for (const line of contentLines) lines.push(row(line, width, theme));
|
|
269
|
-
for (let i = contentLines.length; i < CONTENT_HEIGHT; i++) lines.push(row("", width, theme));
|
|
270
|
-
|
|
271
|
-
let statusText = "";
|
|
272
|
-
if (state.mode === "browse") {
|
|
273
|
-
if (state.slots.length < 2) {
|
|
274
|
-
statusText = theme.fg("dim", `${state.slots.length} agent — add at least 2 for parallel`);
|
|
275
|
-
} else {
|
|
276
|
-
statusText = theme.fg("dim", formatSlotSummary(state.slots));
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
lines.push(row(statusText ? ` ${statusText}` : "", width, theme));
|
|
280
|
-
|
|
281
|
-
let footerText: string;
|
|
282
|
-
if (state.mode === "add") {
|
|
283
|
-
footerText = " [enter] add [esc] cancel ";
|
|
284
|
-
} else if (state.mode === "edit-task") {
|
|
285
|
-
footerText = " [enter] save [esc] cancel ";
|
|
286
|
-
} else {
|
|
287
|
-
footerText = " [ctrl+a] add [del] remove [enter] edit task [ctrl+r] continue [esc] back ";
|
|
288
|
-
}
|
|
289
|
-
lines.push(renderFooter(footerText, width, theme));
|
|
290
|
-
|
|
291
|
-
return lines;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function formatSlotSummary(slots: ParallelSlot[]): string {
|
|
295
|
-
const counts = new Map<string, number>();
|
|
296
|
-
for (const s of slots) counts.set(s.agentName, (counts.get(s.agentName) ?? 0) + 1);
|
|
297
|
-
return [...counts.entries()].map(([name, count]) => count > 1 ? `${count}\u00D7 ${name}` : name).join(" + ");
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
export function formatParallelTitle(slots: ParallelSlot[]): string {
|
|
301
|
-
return `Parallel: ${formatSlotSummary(slots)}`;
|
|
302
|
-
}
|