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
|
@@ -2,20 +2,14 @@
|
|
|
2
2
|
* Chain Clarification TUI Component
|
|
3
3
|
*
|
|
4
4
|
* Shows templates and resolved behaviors for each step in a chain.
|
|
5
|
-
* Supports editing templates, output paths, reads lists, and progress toggle.
|
|
5
|
+
* Supports runtime editing of templates, output paths, reads lists, and progress toggle.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
10
10
|
import { matchesKey, visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
11
|
-
import
|
|
12
|
-
import * as path from "node:path";
|
|
13
|
-
import { getUserChainDir, type AgentConfig, type ChainConfig, type ChainStepConfig } from "../../agents/agents.ts";
|
|
11
|
+
import type { AgentConfig } from "../../agents/agents.ts";
|
|
14
12
|
import type { ResolvedStepBehavior } from "../../shared/settings.ts";
|
|
15
|
-
import type { TextEditorState } from "../../tui/text-editor.ts";
|
|
16
|
-
import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "../../tui/text-editor.ts";
|
|
17
|
-
import { updateFrontmatterField } from "../../agents/agent-serializer.ts";
|
|
18
|
-
import { serializeChain } from "../../agents/chain-serializer.ts";
|
|
19
13
|
import { resolveModelCandidate, splitThinkingSuffix } from "../shared/model-fallback.ts";
|
|
20
14
|
import { findModelInfo, getSupportedThinkingLevels, type ModelInfo, type ThinkingLevel } from "../../shared/model-info.ts";
|
|
21
15
|
|
|
@@ -38,6 +32,166 @@ export interface ChainClarifyResult {
|
|
|
38
32
|
|
|
39
33
|
type EditMode = "template" | "output" | "reads" | "model" | "thinking" | "skills";
|
|
40
34
|
|
|
35
|
+
|
|
36
|
+
interface TextEditorState {
|
|
37
|
+
buffer: string;
|
|
38
|
+
cursor: number;
|
|
39
|
+
viewportOffset: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createEditorState(initial = ""): TextEditorState {
|
|
43
|
+
return { buffer: initial, cursor: 0, viewportOffset: 0 };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function wrapText(text: string, width: number): { lines: string[]; starts: number[] } {
|
|
47
|
+
if (width <= 0) return { lines: [text], starts: [0] };
|
|
48
|
+
if (text.length === 0) return { lines: [""], starts: [0] };
|
|
49
|
+
|
|
50
|
+
const lines: string[] = [];
|
|
51
|
+
const starts: number[] = [];
|
|
52
|
+
let offset = 0;
|
|
53
|
+
const segments = text.split("\n");
|
|
54
|
+
for (const [index, segment] of segments.entries()) {
|
|
55
|
+
if (segment.length === 0) {
|
|
56
|
+
starts.push(offset);
|
|
57
|
+
lines.push("");
|
|
58
|
+
} else {
|
|
59
|
+
let lineStart = 0;
|
|
60
|
+
let pos = 0;
|
|
61
|
+
let lineWidth = 0;
|
|
62
|
+
while (pos < segment.length) {
|
|
63
|
+
const char = String.fromCodePoint(segment.codePointAt(pos)!);
|
|
64
|
+
const charWidth = visibleWidth(char);
|
|
65
|
+
if (lineWidth > 0 && lineWidth + charWidth > width) {
|
|
66
|
+
starts.push(offset + lineStart);
|
|
67
|
+
lines.push(segment.slice(lineStart, pos));
|
|
68
|
+
lineStart = pos;
|
|
69
|
+
lineWidth = 0;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
pos += char.length;
|
|
73
|
+
lineWidth += charWidth;
|
|
74
|
+
}
|
|
75
|
+
starts.push(offset + lineStart);
|
|
76
|
+
lines.push(segment.slice(lineStart));
|
|
77
|
+
}
|
|
78
|
+
offset += segment.length + (index < segments.length - 1 ? 1 : 0);
|
|
79
|
+
}
|
|
80
|
+
if (!text.endsWith("\n") && text.length > 0 && visibleWidth(lines[lines.length - 1] ?? "") === width) {
|
|
81
|
+
starts.push(text.length);
|
|
82
|
+
lines.push("");
|
|
83
|
+
}
|
|
84
|
+
return { lines, starts };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getCursorDisplayPos(cursor: number, starts: number[]): { line: number; col: number } {
|
|
88
|
+
for (let i = starts.length - 1; i >= 0; i--) {
|
|
89
|
+
if (cursor >= starts[i]!) return { line: i, col: cursor - starts[i]! };
|
|
90
|
+
}
|
|
91
|
+
return { line: 0, col: 0 };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ensureCursorVisible(cursorLine: number, viewportHeight: number, currentOffset: number): number {
|
|
95
|
+
if (cursorLine < currentOffset) return Math.max(0, cursorLine);
|
|
96
|
+
if (cursorLine >= currentOffset + viewportHeight) return Math.max(0, cursorLine - viewportHeight + 1);
|
|
97
|
+
return Math.max(0, currentOffset);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isWordChar(ch: string): boolean {
|
|
101
|
+
const code = ch.charCodeAt(0);
|
|
102
|
+
return (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || code === 95;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function wordBackward(buffer: string, cursor: number): number {
|
|
106
|
+
let pos = cursor;
|
|
107
|
+
while (pos > 0 && !isWordChar(buffer[pos - 1]!)) pos--;
|
|
108
|
+
while (pos > 0 && isWordChar(buffer[pos - 1]!)) pos--;
|
|
109
|
+
return pos;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function wordForward(buffer: string, cursor: number): number {
|
|
113
|
+
let pos = cursor;
|
|
114
|
+
while (pos < buffer.length && isWordChar(buffer[pos]!)) pos++;
|
|
115
|
+
while (pos < buffer.length && !isWordChar(buffer[pos]!)) pos++;
|
|
116
|
+
return pos;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeInsertText(data: string): string | null {
|
|
120
|
+
let text = data.split("\x1b[200~").join("").split("\x1b[201~").join("");
|
|
121
|
+
text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
122
|
+
const newline = text.indexOf("\n");
|
|
123
|
+
if (newline !== -1) text = text.slice(0, newline);
|
|
124
|
+
text = text.replace(/\t/g, " ");
|
|
125
|
+
if (text.length === 0) return null;
|
|
126
|
+
for (let i = 0; i < text.length; i++) {
|
|
127
|
+
if (text.charCodeAt(i) < 32) return null;
|
|
128
|
+
}
|
|
129
|
+
return text;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function handleEditorInput(state: TextEditorState, data: string, textWidth: number): TextEditorState | null {
|
|
133
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c") || matchesKey(data, "return")) return null;
|
|
134
|
+
|
|
135
|
+
const { lines: wrapped, starts } = wrapText(state.buffer, textWidth);
|
|
136
|
+
const cursorPos = getCursorDisplayPos(state.cursor, starts);
|
|
137
|
+
|
|
138
|
+
if (matchesKey(data, "alt+left") || matchesKey(data, "ctrl+left")) return { ...state, cursor: wordBackward(state.buffer, state.cursor) };
|
|
139
|
+
if (matchesKey(data, "alt+right") || matchesKey(data, "ctrl+right")) return { ...state, cursor: wordForward(state.buffer, state.cursor) };
|
|
140
|
+
if (matchesKey(data, "left")) return state.cursor > 0 ? { ...state, cursor: state.cursor - 1 } : state;
|
|
141
|
+
if (matchesKey(data, "right")) return state.cursor < state.buffer.length ? { ...state, cursor: state.cursor + 1 } : state;
|
|
142
|
+
if (matchesKey(data, "up") && cursorPos.line > 0) {
|
|
143
|
+
const targetLine = cursorPos.line - 1;
|
|
144
|
+
return { ...state, cursor: starts[targetLine]! + Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0) };
|
|
145
|
+
}
|
|
146
|
+
if (matchesKey(data, "down") && cursorPos.line < wrapped.length - 1) {
|
|
147
|
+
const targetLine = cursorPos.line + 1;
|
|
148
|
+
return { ...state, cursor: starts[targetLine]! + Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0) };
|
|
149
|
+
}
|
|
150
|
+
if (matchesKey(data, "home")) return { ...state, cursor: starts[cursorPos.line]! };
|
|
151
|
+
if (matchesKey(data, "end")) return { ...state, cursor: starts[cursorPos.line]! + (wrapped[cursorPos.line]?.length ?? 0) };
|
|
152
|
+
if (matchesKey(data, "ctrl+home")) return { ...state, cursor: 0 };
|
|
153
|
+
if (matchesKey(data, "ctrl+end")) return { ...state, cursor: state.buffer.length };
|
|
154
|
+
if (matchesKey(data, "alt+backspace")) {
|
|
155
|
+
const target = wordBackward(state.buffer, state.cursor);
|
|
156
|
+
return target === state.cursor ? state : { ...state, buffer: state.buffer.slice(0, target) + state.buffer.slice(state.cursor), cursor: target };
|
|
157
|
+
}
|
|
158
|
+
if (matchesKey(data, "backspace")) {
|
|
159
|
+
return state.cursor > 0
|
|
160
|
+
? { ...state, buffer: state.buffer.slice(0, state.cursor - 1) + state.buffer.slice(state.cursor), cursor: state.cursor - 1 }
|
|
161
|
+
: state;
|
|
162
|
+
}
|
|
163
|
+
if (matchesKey(data, "delete")) {
|
|
164
|
+
return state.cursor < state.buffer.length
|
|
165
|
+
? { ...state, buffer: state.buffer.slice(0, state.cursor) + state.buffer.slice(state.cursor + 1) }
|
|
166
|
+
: state;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const insert = normalizeInsertText(data);
|
|
170
|
+
return insert
|
|
171
|
+
? { ...state, buffer: state.buffer.slice(0, state.cursor) + insert + state.buffer.slice(state.cursor), cursor: state.cursor + insert.length }
|
|
172
|
+
: null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderWithCursor(text: string, cursorPos: number): string {
|
|
176
|
+
const before = text.slice(0, cursorPos);
|
|
177
|
+
const cursorChar = text[cursorPos] ?? " ";
|
|
178
|
+
const after = text.slice(cursorPos + 1);
|
|
179
|
+
return `${before}\x1b[7m${cursorChar}\x1b[27m${after}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function renderEditor(state: TextEditorState, width: number, viewportHeight: number): string[] {
|
|
183
|
+
const { lines: wrapped, starts } = wrapText(state.buffer, width);
|
|
184
|
+
const cursorPos = getCursorDisplayPos(state.cursor, starts);
|
|
185
|
+
const lines: string[] = [];
|
|
186
|
+
for (let i = 0; i < viewportHeight; i++) {
|
|
187
|
+
const lineIdx = state.viewportOffset + i;
|
|
188
|
+
let content = lineIdx < wrapped.length ? wrapped[lineIdx] ?? "" : "";
|
|
189
|
+
if (lineIdx === cursorPos.line) content = renderWithCursor(content, cursorPos.col);
|
|
190
|
+
lines.push(content);
|
|
191
|
+
}
|
|
192
|
+
return lines;
|
|
193
|
+
}
|
|
194
|
+
|
|
41
195
|
/**
|
|
42
196
|
* TUI component for chain clarification.
|
|
43
197
|
* Factory signature matches ctx.ui.custom: (tui, theme, kb, done) => Component
|
|
@@ -61,10 +215,8 @@ export class ChainClarifyComponent implements Component {
|
|
|
61
215
|
private skillSelectedNames: Set<string> = new Set();
|
|
62
216
|
private skillCursorIndex: number = 0;
|
|
63
217
|
private filteredSkills: Array<{ name: string; source: string; description?: string }> = [];
|
|
64
|
-
private
|
|
65
|
-
private
|
|
66
|
-
private saveChainNameState: TextEditorState = createEditorState();
|
|
67
|
-
private savingChain = false;
|
|
218
|
+
private noticeMessage: { text: string; type: "info" | "error" } | null = null;
|
|
219
|
+
private noticeMessageTimer: ReturnType<typeof setTimeout> | null = null;
|
|
68
220
|
/** Run in background (async) mode */
|
|
69
221
|
private runInBackground = false;
|
|
70
222
|
private tui: TUI;
|
|
@@ -260,171 +412,18 @@ export class ChainClarifyComponent implements Component {
|
|
|
260
412
|
this.behaviorOverrides.set(stepIndex, { ...existing, [field]: value });
|
|
261
413
|
}
|
|
262
414
|
|
|
263
|
-
private
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const template = this.templates[i] ?? "";
|
|
270
|
-
const step: ChainStepConfig = { agent: agent.name, task: template };
|
|
271
|
-
if (override?.output !== undefined) step.output = behavior.output;
|
|
272
|
-
if (behavior.outputMode !== "inline") step.outputMode = behavior.outputMode;
|
|
273
|
-
if (override?.reads !== undefined) step.reads = behavior.reads;
|
|
274
|
-
if (override?.model !== undefined) step.model = behavior.model;
|
|
275
|
-
if (override?.skills !== undefined) step.skills = behavior.skills;
|
|
276
|
-
if (override?.progress !== undefined) step.progress = behavior.progress;
|
|
277
|
-
steps.push(step);
|
|
278
|
-
}
|
|
279
|
-
return {
|
|
280
|
-
name,
|
|
281
|
-
description: `Chain: ${steps.map((s) => s.agent).join(" → ")}`,
|
|
282
|
-
source: "user",
|
|
283
|
-
filePath: "",
|
|
284
|
-
steps,
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
private enterSaveChainName(): void {
|
|
289
|
-
this.savingChain = true;
|
|
290
|
-
this.saveChainNameState = createEditorState();
|
|
291
|
-
this.tui.requestRender();
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
private handleSaveChainNameInput(data: string): void {
|
|
295
|
-
if (matchesKey(data, "tab")) return;
|
|
296
|
-
const innerW = this.width - 2;
|
|
297
|
-
const boxInnerWidth = Math.max(10, innerW - 4);
|
|
298
|
-
const nextState = handleEditorInput(this.saveChainNameState, data, boxInnerWidth);
|
|
299
|
-
if (nextState) {
|
|
300
|
-
this.saveChainNameState = nextState;
|
|
301
|
-
this.tui.requestRender();
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
305
|
-
this.savingChain = false;
|
|
306
|
-
this.saveChainNameState = createEditorState();
|
|
307
|
-
this.tui.requestRender();
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
if (matchesKey(data, "return")) {
|
|
311
|
-
const name = this.saveChainNameState.buffer.trim();
|
|
312
|
-
if (!name) {
|
|
313
|
-
this.showSaveMessage("Name is required", "error");
|
|
314
|
-
this.savingChain = false;
|
|
315
|
-
this.saveChainNameState = createEditorState();
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
try {
|
|
319
|
-
const dir = getUserChainDir();
|
|
320
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
321
|
-
const filePath = path.join(dir, `${name}.chain.md`);
|
|
322
|
-
const config = this.buildChainConfig(name);
|
|
323
|
-
config.filePath = filePath;
|
|
324
|
-
fs.writeFileSync(filePath, serializeChain(config), "utf-8");
|
|
325
|
-
this.showSaveMessage(`Saved ${name}.chain.md`, "info");
|
|
326
|
-
} catch (err) {
|
|
327
|
-
this.showSaveMessage(err instanceof Error ? err.message : String(err), "error");
|
|
328
|
-
}
|
|
329
|
-
this.savingChain = false;
|
|
330
|
-
this.saveChainNameState = createEditorState();
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
private showSaveMessage(text: string, type: "info" | "error"): void {
|
|
335
|
-
this.saveMessage = { text, type };
|
|
336
|
-
if (this.saveMessageTimer) clearTimeout(this.saveMessageTimer);
|
|
337
|
-
this.saveMessageTimer = setTimeout(() => {
|
|
338
|
-
this.saveMessage = null;
|
|
339
|
-
this.saveMessageTimer = null;
|
|
415
|
+
private showNotice(text: string, type: "info" | "error"): void {
|
|
416
|
+
this.noticeMessage = { text, type };
|
|
417
|
+
if (this.noticeMessageTimer) clearTimeout(this.noticeMessageTimer);
|
|
418
|
+
this.noticeMessageTimer = setTimeout(() => {
|
|
419
|
+
this.noticeMessage = null;
|
|
420
|
+
this.noticeMessageTimer = null;
|
|
340
421
|
this.tui.requestRender();
|
|
341
422
|
}, 2000);
|
|
342
423
|
this.tui.requestRender();
|
|
343
424
|
}
|
|
344
425
|
|
|
345
|
-
private arraysEqual(a: string[] | false, b: string[] | false): boolean {
|
|
346
|
-
if (a === b) return true;
|
|
347
|
-
if (a === false || b === false) return false;
|
|
348
|
-
if (a.length !== b.length) return false;
|
|
349
|
-
for (let i = 0; i < a.length; i++) {
|
|
350
|
-
if (a[i] !== b[i]) return false;
|
|
351
|
-
}
|
|
352
|
-
return true;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
private saveOverridesToAgent(): void {
|
|
356
|
-
const stepIndex = this.selectedStep;
|
|
357
|
-
const agent = this.agentConfigs[stepIndex];
|
|
358
|
-
if (!agent?.filePath) {
|
|
359
|
-
this.showSaveMessage("Agent file not found", "error");
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const override = this.behaviorOverrides.get(stepIndex);
|
|
364
|
-
if (!override) {
|
|
365
|
-
this.showSaveMessage("No changes to save", "info");
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const base = this.resolvedBehaviors[stepIndex]!;
|
|
370
|
-
const updates: Array<{ field: string; value: string | undefined }> = [];
|
|
371
|
-
|
|
372
|
-
if (override.output !== undefined && override.output !== base.output) {
|
|
373
|
-
updates.push({
|
|
374
|
-
field: "output",
|
|
375
|
-
value: override.output === false ? undefined : override.output,
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (override.reads !== undefined && !this.arraysEqual(override.reads, base.reads)) {
|
|
380
|
-
updates.push({
|
|
381
|
-
field: "defaultReads",
|
|
382
|
-
value: override.reads === false ? undefined : override.reads.join(", "),
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (override.progress !== undefined && override.progress !== base.progress) {
|
|
387
|
-
updates.push({
|
|
388
|
-
field: "defaultProgress",
|
|
389
|
-
value: override.progress ? "true" : undefined,
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (override.skills !== undefined && !this.arraysEqual(override.skills, base.skills)) {
|
|
394
|
-
updates.push({
|
|
395
|
-
field: "skills",
|
|
396
|
-
value: override.skills === false || override.skills.length === 0 ? undefined : override.skills.join(", "),
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (override.model !== undefined) {
|
|
401
|
-
const baseModel = agent.model ? this.resolveModelFullId(agent.model) : undefined;
|
|
402
|
-
if (override.model !== baseModel) {
|
|
403
|
-
updates.push({ field: "model", value: override.model });
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (updates.length === 0) {
|
|
408
|
-
this.showSaveMessage("No changes to save", "info");
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
try {
|
|
413
|
-
for (const update of updates) {
|
|
414
|
-
updateFrontmatterField(agent.filePath, update.field, update.value);
|
|
415
|
-
}
|
|
416
|
-
this.showSaveMessage("Saved agent settings", "info");
|
|
417
|
-
} catch (err) {
|
|
418
|
-
this.showSaveMessage(err instanceof Error ? err.message : String(err), "error");
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
426
|
handleInput(data: string): void {
|
|
423
|
-
if (this.savingChain) {
|
|
424
|
-
this.handleSaveChainNameInput(data);
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
427
|
if (this.editingStep !== null) {
|
|
429
428
|
if (this.editMode === "model") {
|
|
430
429
|
this.handleModelSelectorInput(data);
|
|
@@ -521,15 +520,6 @@ export class ChainClarifyComponent implements Component {
|
|
|
521
520
|
return;
|
|
522
521
|
}
|
|
523
522
|
|
|
524
|
-
if (data === "S") {
|
|
525
|
-
this.saveOverridesToAgent();
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (data === "W" && this.mode === "chain") {
|
|
530
|
-
this.enterSaveChainName();
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
523
|
}
|
|
534
524
|
|
|
535
525
|
private enterEditMode(mode: EditMode): void {
|
|
@@ -646,7 +636,7 @@ export class ChainClarifyComponent implements Component {
|
|
|
646
636
|
/** Enter thinking level selector mode */
|
|
647
637
|
private enterThinkingSelector(): void {
|
|
648
638
|
if (!this.getEffectiveBehavior(this.selectedStep).model) {
|
|
649
|
-
this.
|
|
639
|
+
this.showNotice("Select a model first", "error");
|
|
650
640
|
return;
|
|
651
641
|
}
|
|
652
642
|
this.editingStep = this.selectedStep;
|
|
@@ -888,32 +878,7 @@ export class ChainClarifyComponent implements Component {
|
|
|
888
878
|
}
|
|
889
879
|
}
|
|
890
880
|
|
|
891
|
-
private renderSaveChainName(): string[] {
|
|
892
|
-
const lines: string[] = [];
|
|
893
|
-
const innerW = this.width - 2;
|
|
894
|
-
const boxInnerWidth = Math.max(10, innerW - 4);
|
|
895
|
-
lines.push(this.renderHeader(" Save Chain "));
|
|
896
|
-
lines.push(this.row(""));
|
|
897
|
-
lines.push(this.row(` ${this.theme.fg("dim", "Name:")}`));
|
|
898
|
-
const top = `┌${"─".repeat(boxInnerWidth)}┐`;
|
|
899
|
-
const bottom = `└${"─".repeat(boxInnerWidth)}┘`;
|
|
900
|
-
lines.push(this.row(` ${top}`));
|
|
901
|
-
const editorState = { ...this.saveChainNameState };
|
|
902
|
-
const wrapped = wrapText(editorState.buffer, boxInnerWidth);
|
|
903
|
-
const cursorPos = getCursorDisplayPos(editorState.cursor, wrapped.starts);
|
|
904
|
-
editorState.viewportOffset = ensureCursorVisible(cursorPos.line, 1, editorState.viewportOffset);
|
|
905
|
-
const editorLine = renderEditor(editorState, boxInnerWidth, 1)[0] ?? "";
|
|
906
|
-
lines.push(this.row(` │${this.pad(editorLine, boxInnerWidth)}│`));
|
|
907
|
-
lines.push(this.row(` ${bottom}`));
|
|
908
|
-
lines.push(this.row(""));
|
|
909
|
-
lines.push(this.renderFooter(" [Enter] Save • [Esc] Cancel "));
|
|
910
|
-
return lines;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
881
|
render(_width: number): string[] {
|
|
914
|
-
if (this.savingChain) {
|
|
915
|
-
return this.renderSaveChainName();
|
|
916
|
-
}
|
|
917
882
|
if (this.editingStep !== null) {
|
|
918
883
|
if (this.editMode === "model") {
|
|
919
884
|
return this.renderModelSelector();
|
|
@@ -1141,18 +1106,18 @@ export class ChainClarifyComponent implements Component {
|
|
|
1141
1106
|
const bgLabel = this.runInBackground ? '[b]g:ON' : '[b]g';
|
|
1142
1107
|
switch (this.mode) {
|
|
1143
1108
|
case 'single':
|
|
1144
|
-
return ` [Enter] Run • [Esc] Cancel • e m t w s ${bgLabel}
|
|
1109
|
+
return ` [Enter] Run • [Esc] Cancel • e m t w s ${bgLabel} `;
|
|
1145
1110
|
case 'parallel':
|
|
1146
|
-
return ` [Enter] Run • [Esc] Cancel • e m t s ${bgLabel}
|
|
1111
|
+
return ` [Enter] Run • [Esc] Cancel • e m t s ${bgLabel} • ↑↓ Nav `;
|
|
1147
1112
|
case 'chain':
|
|
1148
|
-
return ` [Enter] Run • [Esc] Cancel • e m t w r p s ${bgLabel}
|
|
1113
|
+
return ` [Enter] Run • [Esc] Cancel • e m t w r p s ${bgLabel} • ↑↓ Nav `;
|
|
1149
1114
|
}
|
|
1150
1115
|
}
|
|
1151
1116
|
|
|
1152
|
-
private
|
|
1153
|
-
if (!this.
|
|
1154
|
-
const color = this.
|
|
1155
|
-
lines.push(this.row(` ${this.theme.fg(color, this.
|
|
1117
|
+
private appendNotice(lines: string[]): void {
|
|
1118
|
+
if (!this.noticeMessage) return;
|
|
1119
|
+
const color = this.noticeMessage.type === "error" ? "error" : "success";
|
|
1120
|
+
lines.push(this.row(` ${this.theme.fg(color, this.noticeMessage.text)}`));
|
|
1156
1121
|
}
|
|
1157
1122
|
|
|
1158
1123
|
private renderSingleMode(): string[] {
|
|
@@ -1199,7 +1164,7 @@ export class ChainClarifyComponent implements Component {
|
|
|
1199
1164
|
|
|
1200
1165
|
lines.push(this.row(""));
|
|
1201
1166
|
|
|
1202
|
-
this.
|
|
1167
|
+
this.appendNotice(lines);
|
|
1203
1168
|
lines.push(this.renderFooter(this.getFooterText()));
|
|
1204
1169
|
|
|
1205
1170
|
return lines;
|
|
@@ -1251,7 +1216,7 @@ export class ChainClarifyComponent implements Component {
|
|
|
1251
1216
|
lines.push(this.row(""));
|
|
1252
1217
|
}
|
|
1253
1218
|
|
|
1254
|
-
this.
|
|
1219
|
+
this.appendNotice(lines);
|
|
1255
1220
|
lines.push(this.renderFooter(this.getFooterText()));
|
|
1256
1221
|
|
|
1257
1222
|
return lines;
|
|
@@ -1354,7 +1319,7 @@ export class ChainClarifyComponent implements Component {
|
|
|
1354
1319
|
lines.push(this.row(""));
|
|
1355
1320
|
}
|
|
1356
1321
|
|
|
1357
|
-
this.
|
|
1322
|
+
this.appendNotice(lines);
|
|
1358
1323
|
lines.push(this.renderFooter(this.getFooterText()));
|
|
1359
1324
|
|
|
1360
1325
|
return lines;
|
|
@@ -1362,7 +1327,7 @@ export class ChainClarifyComponent implements Component {
|
|
|
1362
1327
|
|
|
1363
1328
|
invalidate(): void {}
|
|
1364
1329
|
dispose(): void {
|
|
1365
|
-
if (this.
|
|
1366
|
-
this.
|
|
1330
|
+
if (this.noticeMessageTimer) clearTimeout(this.noticeMessageTimer);
|
|
1331
|
+
this.noticeMessageTimer = null;
|
|
1367
1332
|
}
|
|
1368
1333
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ActivityState, AsyncJobStep } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
type StepStatusLike = Pick<AsyncJobStep, "status">;
|
|
4
|
+
|
|
5
|
+
function formatActivityAge(ms: number): string {
|
|
6
|
+
if (ms < 1000) return "now";
|
|
7
|
+
if (ms < 60000) return `${Math.floor(ms / 1000)}s`;
|
|
8
|
+
return `${Math.floor(ms / 60000)}m`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatActivityLabel(lastActivityAt: number | undefined, activityState?: ActivityState, now = Date.now()): string | undefined {
|
|
12
|
+
if (lastActivityAt === undefined) {
|
|
13
|
+
if (activityState === "needs_attention") return "needs attention";
|
|
14
|
+
if (activityState === "active_long_running") return "active but long-running";
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const age = formatActivityAge(Math.max(0, now - lastActivityAt));
|
|
18
|
+
if (activityState === "needs_attention") return `no activity for ${age}`;
|
|
19
|
+
if (activityState === "active_long_running") return `active but long-running · last activity ${age} ago`;
|
|
20
|
+
return age === "now" ? "active now" : `active ${age} ago`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isCompletedStepStatus(status: AsyncJobStep["status"]): boolean {
|
|
24
|
+
return status === "complete" || status === "completed";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function aggregateStepStatus(steps: StepStatusLike[]): AsyncJobStep["status"] {
|
|
28
|
+
if (steps.some((step) => step.status === "running")) return "running";
|
|
29
|
+
if (steps.some((step) => step.status === "failed")) return "failed";
|
|
30
|
+
if (steps.some((step) => step.status === "paused")) return "paused";
|
|
31
|
+
if (steps.length > 0 && steps.every((step) => isCompletedStepStatus(step.status))) return "complete";
|
|
32
|
+
return "pending";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function formatAgentRunningLabel(count: number): string {
|
|
36
|
+
return count === 1 ? "1 agent running" : `${count} agents running`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatParallelOutcome(steps: StepStatusLike[], total: number, options: { showRunning?: boolean } = {}): string {
|
|
40
|
+
const running = steps.filter((step) => step.status === "running").length;
|
|
41
|
+
const done = steps.filter((step) => isCompletedStepStatus(step.status)).length;
|
|
42
|
+
const failed = steps.filter((step) => step.status === "failed").length;
|
|
43
|
+
const paused = steps.filter((step) => step.status === "paused").length;
|
|
44
|
+
const parts = [`${done}/${total} done`];
|
|
45
|
+
if (options.showRunning !== false && running > 0) parts.unshift(formatAgentRunningLabel(running));
|
|
46
|
+
if (failed > 0) parts.push(`${failed} failed`);
|
|
47
|
+
if (paused > 0) parts.push(`${paused} paused`);
|
|
48
|
+
return parts.join(" · ");
|
|
49
|
+
}
|
package/src/shared/types.ts
CHANGED
|
@@ -494,10 +494,6 @@ interface TopLevelParallelConfig {
|
|
|
494
494
|
concurrency?: number;
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
-
interface AgentManagerConfig {
|
|
498
|
-
newShortcut?: string;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
497
|
export interface ExtensionConfig {
|
|
502
498
|
asyncByDefault?: boolean;
|
|
503
499
|
forceTopLevelAsync?: boolean;
|
|
@@ -505,7 +501,6 @@ export interface ExtensionConfig {
|
|
|
505
501
|
maxSubagentDepth?: number;
|
|
506
502
|
control?: ControlConfig;
|
|
507
503
|
parallel?: TopLevelParallelConfig;
|
|
508
|
-
agentManager?: AgentManagerConfig;
|
|
509
504
|
worktreeSetupHook?: string;
|
|
510
505
|
worktreeSetupHookTimeoutMs?: number;
|
|
511
506
|
intercomBridge?: IntercomBridgeConfig;
|
|
@@ -4,14 +4,9 @@ import * as path from "node:path";
|
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
5
5
|
import { Key, matchesKey } from "@mariozechner/pi-tui";
|
|
6
6
|
import { discoverAgents, discoverAgentsAll, type ChainConfig } from "../agents/agents.ts";
|
|
7
|
-
import { AgentManagerComponent, type ManagerResult } from "../manager-ui/agent-manager.ts";
|
|
8
|
-
import { SubagentsStatusComponent } from "../tui/subagents-status.ts";
|
|
9
|
-
import { discoverAvailableSkills } from "../agents/skills.ts";
|
|
10
7
|
import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
11
|
-
import { resolveCurrentSessionId } from "../shared/session-identity.ts";
|
|
12
8
|
import { isParallelStep, type ChainStep } from "../shared/settings.ts";
|
|
13
9
|
import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
|
|
14
|
-
import { toModelInfo } from "../shared/model-info.ts";
|
|
15
10
|
import {
|
|
16
11
|
applySlashUpdate,
|
|
17
12
|
buildSlashInitialResult,
|
|
@@ -25,7 +20,6 @@ import {
|
|
|
25
20
|
SLASH_SUBAGENT_RESPONSE_EVENT,
|
|
26
21
|
SLASH_SUBAGENT_STARTED_EVENT,
|
|
27
22
|
SLASH_SUBAGENT_UPDATE_EVENT,
|
|
28
|
-
type ExtensionConfig,
|
|
29
23
|
type SingleResult,
|
|
30
24
|
type SubagentState,
|
|
31
25
|
} from "../shared/types.ts";
|
|
@@ -328,49 +322,6 @@ async function runSlashSubagent(
|
|
|
328
322
|
}
|
|
329
323
|
}
|
|
330
324
|
|
|
331
|
-
async function openAgentManager(
|
|
332
|
-
pi: ExtensionAPI,
|
|
333
|
-
ctx: ExtensionContext,
|
|
334
|
-
config: ExtensionConfig = {},
|
|
335
|
-
): Promise<void> {
|
|
336
|
-
const agentData = { ...discoverAgentsAll(ctx.cwd), cwd: ctx.cwd };
|
|
337
|
-
const models = ctx.modelRegistry.getAvailable().map(toModelInfo);
|
|
338
|
-
const skills = discoverAvailableSkills(ctx.cwd);
|
|
339
|
-
|
|
340
|
-
const result = await ctx.ui.custom<ManagerResult>(
|
|
341
|
-
(tui, theme, _kb, done) => new AgentManagerComponent(tui, theme, agentData, models, skills, done, { newShortcut: config.agentManager?.newShortcut, preferredModelProvider: ctx.model?.provider }),
|
|
342
|
-
{ overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
|
|
343
|
-
);
|
|
344
|
-
if (!result) return;
|
|
345
|
-
|
|
346
|
-
const launchOptions: SubagentParamsLike = {
|
|
347
|
-
clarify: !result.skipClarify && !result.background,
|
|
348
|
-
agentScope: "both",
|
|
349
|
-
...(result.fork ? { context: "fork" as const } : {}),
|
|
350
|
-
...(result.background ? { async: true } : {}),
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
if (result.action === "chain") {
|
|
354
|
-
const chain = result.agents.map((name, i) => ({
|
|
355
|
-
agent: name,
|
|
356
|
-
...(i === 0 ? { task: result.task } : {}),
|
|
357
|
-
}));
|
|
358
|
-
await runSlashSubagent(pi, ctx, { chain, task: result.task, ...launchOptions });
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (result.action === "launch") {
|
|
363
|
-
await runSlashSubagent(pi, ctx, { agent: result.agent, task: result.task, ...launchOptions });
|
|
364
|
-
} else if (result.action === "launch-chain") {
|
|
365
|
-
await runSlashSubagent(pi, ctx, { chain: mapSavedChainSteps(result.chain, result.worktree), task: result.task, ...launchOptions });
|
|
366
|
-
} else if (result.action === "parallel") {
|
|
367
|
-
await runSlashSubagent(pi, ctx, {
|
|
368
|
-
tasks: result.tasks,
|
|
369
|
-
...launchOptions,
|
|
370
|
-
...(result.worktree ? { worktree: true } : {}),
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
325
|
|
|
375
326
|
interface ParsedStep { name: string; config: InlineConfig; task?: string }
|
|
376
327
|
|
|
@@ -452,15 +403,7 @@ const parseAgentArgs = (
|
|
|
452
403
|
export function registerSlashCommands(
|
|
453
404
|
pi: ExtensionAPI,
|
|
454
405
|
state: SubagentState,
|
|
455
|
-
config: ExtensionConfig = {},
|
|
456
406
|
): void {
|
|
457
|
-
pi.registerCommand("agents", {
|
|
458
|
-
description: "Open the Agents Manager",
|
|
459
|
-
handler: async (_args, ctx) => {
|
|
460
|
-
await openAgentManager(pi, ctx, config);
|
|
461
|
-
},
|
|
462
|
-
});
|
|
463
|
-
|
|
464
407
|
pi.registerCommand("run", {
|
|
465
408
|
description: "Run a subagent directly: /run agent[output=file] [task] [--bg] [--fork]",
|
|
466
409
|
getArgumentCompletions: makeAgentCompletions(state, false),
|
|
@@ -567,18 +510,6 @@ export function registerSlashCommands(
|
|
|
567
510
|
},
|
|
568
511
|
});
|
|
569
512
|
|
|
570
|
-
pi.registerCommand("subagents-status", {
|
|
571
|
-
description: "Show active and recent async subagent runs",
|
|
572
|
-
handler: async (_args, ctx) => {
|
|
573
|
-
const sessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
574
|
-
state.baseCwd = ctx.cwd;
|
|
575
|
-
state.currentSessionId = sessionId;
|
|
576
|
-
await ctx.ui.custom<void>(
|
|
577
|
-
(tui, theme, _kb, done) => new SubagentsStatusComponent(tui, theme, () => done(undefined), { sessionId }),
|
|
578
|
-
{ overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
|
|
579
|
-
);
|
|
580
|
-
},
|
|
581
|
-
});
|
|
582
513
|
|
|
583
514
|
pi.registerCommand("subagents-doctor", {
|
|
584
515
|
description: "Show subagent diagnostics",
|
|
@@ -587,9 +518,4 @@ export function registerSlashCommands(
|
|
|
587
518
|
},
|
|
588
519
|
});
|
|
589
520
|
|
|
590
|
-
pi.registerShortcut("ctrl+shift+a", {
|
|
591
|
-
handler: async (ctx) => {
|
|
592
|
-
await openAgentManager(pi, ctx, config);
|
|
593
|
-
},
|
|
594
|
-
});
|
|
595
521
|
}
|