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
|
@@ -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
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
buildChainInstructions,
|
|
19
19
|
writeInitialProgressFile,
|
|
20
20
|
createParallelDirs,
|
|
21
|
+
suppressProgressForReadOnlyTask,
|
|
21
22
|
aggregateParallelOutputs,
|
|
22
23
|
isParallelStep,
|
|
23
24
|
type StepOverrides,
|
|
@@ -178,8 +179,8 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
178
179
|
} as SingleResult;
|
|
179
180
|
}
|
|
180
181
|
|
|
181
|
-
const behavior = input.parallelBehaviors[taskIndex]!;
|
|
182
182
|
const taskTemplate = input.parallelTemplates[taskIndex] ?? "{previous}";
|
|
183
|
+
const behavior = suppressProgressForReadOnlyTask(input.parallelBehaviors[taskIndex]!, taskTemplate, input.originalTask);
|
|
183
184
|
const templateHasPrevious = taskTemplate.includes("{previous}");
|
|
184
185
|
const { prefix, suffix } = buildChainInstructions(
|
|
185
186
|
behavior,
|
|
@@ -537,7 +538,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
537
538
|
|
|
538
539
|
try {
|
|
539
540
|
const agentNames = step.parallel.map((task) => task.agent);
|
|
540
|
-
const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex, chainSkills)
|
|
541
|
+
const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex, chainSkills)
|
|
542
|
+
.map((behavior, taskIndex) => suppressProgressForReadOnlyTask(behavior, parallelTemplates[taskIndex] ?? step.parallel[taskIndex]?.task, originalTask));
|
|
541
543
|
for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
|
|
542
544
|
const behavior = parallelBehaviors[taskIndex]!;
|
|
543
545
|
const outputPath = typeof behavior.output === "string"
|
|
@@ -616,6 +618,23 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
616
618
|
}),
|
|
617
619
|
};
|
|
618
620
|
}
|
|
621
|
+
const detachedIndexInStep = parallelResults.findIndex((result) => result.detached);
|
|
622
|
+
const detached = detachedIndexInStep >= 0 ? parallelResults[detachedIndexInStep] : undefined;
|
|
623
|
+
if (detached) {
|
|
624
|
+
return {
|
|
625
|
+
content: [{ type: "text", text: `Chain detached for intercom coordination at step ${stepIndex + 1} (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
|
|
626
|
+
details: buildChainExecutionDetails({
|
|
627
|
+
results,
|
|
628
|
+
includeProgress,
|
|
629
|
+
allProgress,
|
|
630
|
+
allArtifactPaths,
|
|
631
|
+
artifactsDir,
|
|
632
|
+
chainAgents,
|
|
633
|
+
totalSteps,
|
|
634
|
+
currentStepIndex: stepIndex,
|
|
635
|
+
}),
|
|
636
|
+
};
|
|
637
|
+
}
|
|
619
638
|
|
|
620
639
|
const failures = parallelResults
|
|
621
640
|
.map((result, originalIndex) => ({ ...result, originalIndex }))
|
|
@@ -695,7 +714,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
695
714
|
? tuiOverride.skills
|
|
696
715
|
: normalizeSkillInput(seqStep.skill),
|
|
697
716
|
};
|
|
698
|
-
const behavior = resolveStepBehavior(agentConfig, stepOverride, chainSkills);
|
|
717
|
+
const behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agentConfig, stepOverride, chainSkills), stepTemplate, originalTask);
|
|
699
718
|
|
|
700
719
|
const isFirstProgress = behavior.progress && !progressCreated;
|
|
701
720
|
if (isFirstProgress) {
|
|
@@ -822,24 +841,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
822
841
|
if (r.progress) allProgress.push(r.progress);
|
|
823
842
|
if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
|
|
824
843
|
|
|
825
|
-
if (behavior.output && r.exitCode === 0) {
|
|
826
|
-
try {
|
|
827
|
-
const expectedPath = path.isAbsolute(behavior.output)
|
|
828
|
-
? behavior.output
|
|
829
|
-
: path.join(chainDir, behavior.output);
|
|
830
|
-
if (!fs.existsSync(expectedPath)) {
|
|
831
|
-
const dirFiles = fs.readdirSync(chainDir);
|
|
832
|
-
const mdFiles = dirFiles.filter((file) => file.endsWith(".md") && file !== "progress.md");
|
|
833
|
-
const warning = mdFiles.length > 0
|
|
834
|
-
? `Agent wrote to different file(s): ${mdFiles.join(", ")} instead of ${behavior.output}`
|
|
835
|
-
: `Agent did not create expected output file: ${behavior.output}`;
|
|
836
|
-
r.error = r.error ? `${r.error}\n${warning}` : warning;
|
|
837
|
-
}
|
|
838
|
-
} catch {
|
|
839
|
-
// Ignore validation errors - this is just a diagnostic
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
|
|
843
844
|
if (r.interrupted) {
|
|
844
845
|
return {
|
|
845
846
|
content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${r.agent}). Waiting for explicit next action.` }],
|
|
@@ -855,6 +856,21 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
855
856
|
}),
|
|
856
857
|
};
|
|
857
858
|
}
|
|
859
|
+
if (r.detached) {
|
|
860
|
+
return {
|
|
861
|
+
content: [{ type: "text", text: `Chain detached for intercom coordination at step ${stepIndex + 1} (${r.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
|
|
862
|
+
details: buildChainExecutionDetails({
|
|
863
|
+
results,
|
|
864
|
+
includeProgress,
|
|
865
|
+
allProgress,
|
|
866
|
+
allArtifactPaths,
|
|
867
|
+
artifactsDir,
|
|
868
|
+
chainAgents,
|
|
869
|
+
totalSteps,
|
|
870
|
+
currentStepIndex: stepIndex,
|
|
871
|
+
}),
|
|
872
|
+
};
|
|
873
|
+
}
|
|
858
874
|
|
|
859
875
|
if (r.exitCode !== 0) {
|
|
860
876
|
const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
|
|
@@ -877,6 +893,24 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
877
893
|
};
|
|
878
894
|
}
|
|
879
895
|
|
|
896
|
+
if (behavior.output) {
|
|
897
|
+
try {
|
|
898
|
+
const expectedPath = path.isAbsolute(behavior.output)
|
|
899
|
+
? behavior.output
|
|
900
|
+
: path.join(chainDir, behavior.output);
|
|
901
|
+
if (!fs.existsSync(expectedPath)) {
|
|
902
|
+
const dirFiles = fs.readdirSync(chainDir);
|
|
903
|
+
const mdFiles = dirFiles.filter((file) => file.endsWith(".md") && file !== "progress.md");
|
|
904
|
+
const warning = mdFiles.length > 0
|
|
905
|
+
? `Agent wrote to different file(s): ${mdFiles.join(", ")} instead of ${behavior.output}`
|
|
906
|
+
: `Agent did not create expected output file: ${behavior.output}`;
|
|
907
|
+
r.error = r.error ? `${r.error}\n${warning}` : warning;
|
|
908
|
+
}
|
|
909
|
+
} catch {
|
|
910
|
+
// Ignore validation errors; this diagnostic should not mask successful chain output.
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
880
914
|
prev = getSingleResultOutput(r);
|
|
881
915
|
}
|
|
882
916
|
}
|
|
@@ -421,7 +421,7 @@ async function runSingleAttempt(
|
|
|
421
421
|
const toolArgs = evt.args && typeof evt.args === "object" && !Array.isArray(evt.args)
|
|
422
422
|
? evt.args as Record<string, unknown>
|
|
423
423
|
: {};
|
|
424
|
-
if (options.allowIntercomDetach && evt.toolName === "intercom") {
|
|
424
|
+
if (options.allowIntercomDetach && (evt.toolName === "intercom" || evt.toolName === "contact_supervisor")) {
|
|
425
425
|
intercomStarted = true;
|
|
426
426
|
}
|
|
427
427
|
progress.toolCount++;
|
|
@@ -633,7 +633,10 @@ async function runSingleAttempt(
|
|
|
633
633
|
return result;
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
-
if (exitCode === 0
|
|
636
|
+
if (result.error && result.exitCode === 0) {
|
|
637
|
+
result.exitCode = 1;
|
|
638
|
+
}
|
|
639
|
+
if (result.exitCode === 0 && !result.error) {
|
|
637
640
|
const errInfo = detectSubagentError(result.messages);
|
|
638
641
|
if (errInfo.hasError) {
|
|
639
642
|
result.exitCode = errInfo.exitCode ?? 1;
|
|
@@ -746,7 +749,6 @@ export async function runSync(
|
|
|
746
749
|
|
|
747
750
|
const shareEnabled = options.share === true;
|
|
748
751
|
const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
|
|
749
|
-
const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
|
|
750
752
|
const skillNames = options.skills ?? agent.skills ?? [];
|
|
751
753
|
const skillCwd = options.cwd ?? runtimeCwd;
|
|
752
754
|
const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, skillCwd, runtimeCwd);
|
|
@@ -797,6 +799,7 @@ export async function runSync(
|
|
|
797
799
|
for (let i = 0; i < modelsToTry.length; i++) {
|
|
798
800
|
const candidate = modelsToTry[i];
|
|
799
801
|
if (candidate) attemptedModels.push(candidate);
|
|
802
|
+
const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
|
|
800
803
|
const result = await runSingleAttempt(runtimeCwd, agent, task, candidate, options, {
|
|
801
804
|
sessionEnabled,
|
|
802
805
|
systemPrompt,
|