gsd-pi 2.36.0-dev.f887f4e → 2.37.0-dev.3186675
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/dist/resources/extensions/cmux/index.js +321 -0
- package/dist/resources/extensions/cmux/package.json +7 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +334 -104
- package/dist/resources/extensions/gsd/auto-loop.js +29 -4
- package/dist/resources/extensions/gsd/auto.js +35 -5
- package/dist/resources/extensions/gsd/commands-cmux.js +120 -0
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +51 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -0
- package/dist/resources/extensions/gsd/git-service.js +9 -1
- package/dist/resources/extensions/gsd/history.js +2 -1
- package/dist/resources/extensions/gsd/index.js +5 -0
- package/dist/resources/extensions/gsd/metrics.js +4 -2
- package/dist/resources/extensions/gsd/notifications.js +10 -1
- package/dist/resources/extensions/gsd/preferences-types.js +2 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +29 -0
- package/dist/resources/extensions/gsd/preferences.js +3 -0
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +4 -3
- package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -2
- package/dist/resources/extensions/gsd/session-lock.js +26 -6
- package/dist/resources/extensions/gsd/templates/preferences.md +6 -0
- package/dist/resources/extensions/search-the-web/native-search.js +45 -4
- package/dist/resources/extensions/shared/format-utils.js +5 -41
- package/dist/resources/extensions/shared/layout-utils.js +46 -0
- package/dist/resources/extensions/shared/mod.js +2 -1
- package/dist/resources/extensions/shared/terminal.js +5 -0
- package/dist/resources/extensions/subagent/index.js +180 -60
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -4
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +8 -4
- package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
- package/packages/pi-tui/dist/terminal-image.js +4 -0
- package/packages/pi-tui/dist/terminal-image.js.map +1 -1
- package/packages/pi-tui/src/terminal-image.ts +5 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/cmux/index.ts +384 -0
- package/src/resources/extensions/cmux/package.json +7 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +363 -116
- package/src/resources/extensions/gsd/auto-loop.ts +66 -6
- package/src/resources/extensions/gsd/auto.ts +45 -5
- package/src/resources/extensions/gsd/commands-cmux.ts +143 -0
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +54 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -0
- package/src/resources/extensions/gsd/git-service.ts +12 -1
- package/src/resources/extensions/gsd/history.ts +2 -1
- package/src/resources/extensions/gsd/index.ts +8 -0
- package/src/resources/extensions/gsd/metrics.ts +4 -2
- package/src/resources/extensions/gsd/notifications.ts +10 -1
- package/src/resources/extensions/gsd/preferences-types.ts +13 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +26 -0
- package/src/resources/extensions/gsd/preferences.ts +4 -0
- package/src/resources/extensions/gsd/prompts/research-milestone.md +4 -3
- package/src/resources/extensions/gsd/prompts/research-slice.md +3 -2
- package/src/resources/extensions/gsd/session-lock.ts +41 -6
- package/src/resources/extensions/gsd/templates/preferences.md +6 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +39 -1
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/cmux.test.ts +122 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +23 -0
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +45 -0
- package/src/resources/extensions/search-the-web/native-search.ts +50 -4
- package/src/resources/extensions/shared/format-utils.ts +5 -44
- package/src/resources/extensions/shared/layout-utils.ts +49 -0
- package/src/resources/extensions/shared/mod.ts +7 -4
- package/src/resources/extensions/shared/terminal.ts +5 -0
- package/src/resources/extensions/shared/tests/format-utils.test.ts +5 -3
- package/src/resources/extensions/subagent/index.ts +236 -79
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared formatting
|
|
2
|
+
* Shared pure formatting utilities — no @gsd/pi-tui dependency.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* ANSI-aware layout helpers (padRight, joinColumns, centerLine, fitColumns)
|
|
5
|
+
* live in layout-utils.ts to avoid pulling @gsd/pi-tui into modules that
|
|
6
|
+
* run outside jiti's alias resolution (e.g. HTML report generation via
|
|
7
|
+
* dynamic import in auto-loop).
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
|
|
9
|
-
|
|
10
10
|
// ─── Duration Formatting ──────────────────────────────────────────────────────
|
|
11
11
|
|
|
12
12
|
/** Format a millisecond duration as a compact human-readable string. */
|
|
@@ -31,45 +31,6 @@ export function formatTokenCount(count: number): string {
|
|
|
31
31
|
return `${(count / 1_000_000).toFixed(2)}M`;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
// ─── Layout Helpers ───────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
/** Pad a string with trailing spaces to fill `width` (ANSI-aware). */
|
|
37
|
-
export function padRight(content: string, width: number): string {
|
|
38
|
-
const vis = visibleWidth(content);
|
|
39
|
-
return content + " ".repeat(Math.max(0, width - vis));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Build a line with left-aligned and right-aligned content. */
|
|
43
|
-
export function joinColumns(left: string, right: string, width: number): string {
|
|
44
|
-
const leftW = visibleWidth(left);
|
|
45
|
-
const rightW = visibleWidth(right);
|
|
46
|
-
if (leftW + rightW + 2 > width) {
|
|
47
|
-
return truncateToWidth(`${left} ${right}`, width);
|
|
48
|
-
}
|
|
49
|
-
return left + " ".repeat(width - leftW - rightW) + right;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Center content within `width` (ANSI-aware). */
|
|
53
|
-
export function centerLine(content: string, width: number): string {
|
|
54
|
-
const vis = visibleWidth(content);
|
|
55
|
-
if (vis >= width) return truncateToWidth(content, width);
|
|
56
|
-
const leftPad = Math.floor((width - vis) / 2);
|
|
57
|
-
return " ".repeat(leftPad) + content;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Join as many parts as fit within `width`, separated by `separator`. */
|
|
61
|
-
export function fitColumns(parts: string[], width: number, separator = " "): string {
|
|
62
|
-
const filtered = parts.filter(Boolean);
|
|
63
|
-
if (filtered.length === 0) return "";
|
|
64
|
-
let result = filtered[0];
|
|
65
|
-
for (let i = 1; i < filtered.length; i++) {
|
|
66
|
-
const candidate = `${result}${separator}${filtered[i]}`;
|
|
67
|
-
if (visibleWidth(candidate) > width) break;
|
|
68
|
-
result = candidate;
|
|
69
|
-
}
|
|
70
|
-
return truncateToWidth(result, width);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
34
|
// ─── Text Truncation ─────────────────────────────────────────────────────────
|
|
74
35
|
|
|
75
36
|
/** Truncate a string to `maxLength` characters, replacing the last character with an ellipsis if needed. */
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI-aware TUI layout utilities that depend on @gsd/pi-tui.
|
|
3
|
+
*
|
|
4
|
+
* Separated from format-utils.ts so that modules needing only pure
|
|
5
|
+
* formatting (e.g. HTML report generation) can import format-utils
|
|
6
|
+
* without pulling in the @gsd/pi-tui dependency — which fails when
|
|
7
|
+
* loaded outside jiti's alias resolution context.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
|
|
11
|
+
|
|
12
|
+
// ─── Layout Helpers ───────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/** Pad a string with trailing spaces to fill `width` (ANSI-aware). */
|
|
15
|
+
export function padRight(content: string, width: number): string {
|
|
16
|
+
const vis = visibleWidth(content);
|
|
17
|
+
return content + " ".repeat(Math.max(0, width - vis));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Build a line with left-aligned and right-aligned content. */
|
|
21
|
+
export function joinColumns(left: string, right: string, width: number): string {
|
|
22
|
+
const leftW = visibleWidth(left);
|
|
23
|
+
const rightW = visibleWidth(right);
|
|
24
|
+
if (leftW + rightW + 2 > width) {
|
|
25
|
+
return truncateToWidth(`${left} ${right}`, width);
|
|
26
|
+
}
|
|
27
|
+
return left + " ".repeat(width - leftW - rightW) + right;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Center content within `width` (ANSI-aware). */
|
|
31
|
+
export function centerLine(content: string, width: number): string {
|
|
32
|
+
const vis = visibleWidth(content);
|
|
33
|
+
if (vis >= width) return truncateToWidth(content, width);
|
|
34
|
+
const leftPad = Math.floor((width - vis) / 2);
|
|
35
|
+
return " ".repeat(leftPad) + content;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Join as many parts as fit within `width`, separated by `separator`. */
|
|
39
|
+
export function fitColumns(parts: string[], width: number, separator = " "): string {
|
|
40
|
+
const filtered = parts.filter(Boolean);
|
|
41
|
+
if (filtered.length === 0) return "";
|
|
42
|
+
let result = filtered[0];
|
|
43
|
+
for (let i = 1; i < filtered.length; i++) {
|
|
44
|
+
const candidate = `${result}${separator}${filtered[i]}`;
|
|
45
|
+
if (visibleWidth(candidate) > width) break;
|
|
46
|
+
result = candidate;
|
|
47
|
+
}
|
|
48
|
+
return truncateToWidth(result, width);
|
|
49
|
+
}
|
|
@@ -13,15 +13,18 @@ export {
|
|
|
13
13
|
stripAnsi,
|
|
14
14
|
formatTokenCount,
|
|
15
15
|
formatDuration,
|
|
16
|
-
padRight,
|
|
17
|
-
joinColumns,
|
|
18
|
-
centerLine,
|
|
19
|
-
fitColumns,
|
|
20
16
|
sparkline,
|
|
21
17
|
normalizeStringArray,
|
|
22
18
|
fileLink,
|
|
23
19
|
} from "./format-utils.js";
|
|
24
20
|
|
|
21
|
+
export {
|
|
22
|
+
padRight,
|
|
23
|
+
joinColumns,
|
|
24
|
+
centerLine,
|
|
25
|
+
fitColumns,
|
|
26
|
+
} from "./layout-utils.js";
|
|
27
|
+
|
|
25
28
|
export { shortcutDesc } from "./terminal.js";
|
|
26
29
|
export { toPosixPath } from "./path-display.js";
|
|
27
30
|
export { showInterviewRound } from "./interview-ui.js";
|
|
@@ -7,9 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
const UNSUPPORTED_TERMS = ["apple_terminal", "warpterm"];
|
|
9
9
|
|
|
10
|
+
export function isCmuxTerminal(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
11
|
+
return Boolean(env.CMUX_WORKSPACE_ID && env.CMUX_SURFACE_ID);
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
export function supportsCtrlAltShortcuts(): boolean {
|
|
11
15
|
const term = (process.env.TERM_PROGRAM || "").toLowerCase();
|
|
12
16
|
const jetbrains = (process.env.TERMINAL_EMULATOR || "").toLowerCase().includes("jetbrains");
|
|
17
|
+
if (isCmuxTerminal()) return true;
|
|
13
18
|
return !UNSUPPORTED_TERMS.some((t) => term.includes(t)) && !jetbrains;
|
|
14
19
|
}
|
|
15
20
|
|
|
@@ -2,13 +2,15 @@ import { describe, it } from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import {
|
|
4
4
|
formatDuration,
|
|
5
|
+
sparkline,
|
|
6
|
+
stripAnsi,
|
|
7
|
+
} from "../format-utils.js";
|
|
8
|
+
import {
|
|
5
9
|
padRight,
|
|
6
10
|
joinColumns,
|
|
7
11
|
centerLine,
|
|
8
12
|
fitColumns,
|
|
9
|
-
|
|
10
|
-
stripAnsi,
|
|
11
|
-
} from "../format-utils.js";
|
|
13
|
+
} from "../layout-utils.js";
|
|
12
14
|
|
|
13
15
|
describe("formatDuration", () => {
|
|
14
16
|
it("formats seconds", () => {
|
|
@@ -34,6 +34,8 @@ import {
|
|
|
34
34
|
readIsolationMode,
|
|
35
35
|
} from "./isolation.js";
|
|
36
36
|
import { registerWorker, updateWorker } from "./worker-registry.js";
|
|
37
|
+
import { loadEffectiveGSDPreferences } from "../gsd/preferences.js";
|
|
38
|
+
import { CmuxClient, shellEscape } from "../cmux/index.js";
|
|
37
39
|
|
|
38
40
|
const MAX_PARALLEL_TASKS = 8;
|
|
39
41
|
const MAX_CONCURRENCY = 4;
|
|
@@ -257,6 +259,70 @@ function writePromptToTempFile(agentName: string, prompt: string): { dir: string
|
|
|
257
259
|
return { dir: tmpDir, filePath };
|
|
258
260
|
}
|
|
259
261
|
|
|
262
|
+
function buildSubagentProcessArgs(
|
|
263
|
+
agent: AgentConfig,
|
|
264
|
+
task: string,
|
|
265
|
+
tmpPromptPath: string | null,
|
|
266
|
+
): string[] {
|
|
267
|
+
const args: string[] = ["--mode", "json", "-p", "--no-session"];
|
|
268
|
+
if (agent.model) args.push("--model", agent.model);
|
|
269
|
+
if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
|
|
270
|
+
if (tmpPromptPath) args.push("--append-system-prompt", tmpPromptPath);
|
|
271
|
+
args.push(`Task: ${task}`);
|
|
272
|
+
return args;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function processSubagentEventLine(
|
|
276
|
+
line: string,
|
|
277
|
+
currentResult: SingleResult,
|
|
278
|
+
emitUpdate: () => void,
|
|
279
|
+
): void {
|
|
280
|
+
if (!line.trim()) return;
|
|
281
|
+
let event: any;
|
|
282
|
+
try {
|
|
283
|
+
event = JSON.parse(line);
|
|
284
|
+
} catch {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (event.type === "message_end" && event.message) {
|
|
289
|
+
const msg = event.message as Message;
|
|
290
|
+
currentResult.messages.push(msg);
|
|
291
|
+
|
|
292
|
+
if (msg.role === "assistant") {
|
|
293
|
+
currentResult.usage.turns++;
|
|
294
|
+
const usage = msg.usage;
|
|
295
|
+
if (usage) {
|
|
296
|
+
currentResult.usage.input += usage.input || 0;
|
|
297
|
+
currentResult.usage.output += usage.output || 0;
|
|
298
|
+
currentResult.usage.cacheRead += usage.cacheRead || 0;
|
|
299
|
+
currentResult.usage.cacheWrite += usage.cacheWrite || 0;
|
|
300
|
+
currentResult.usage.cost += usage.cost?.total || 0;
|
|
301
|
+
currentResult.usage.contextTokens = usage.totalTokens || 0;
|
|
302
|
+
}
|
|
303
|
+
if (!currentResult.model && msg.model) currentResult.model = msg.model;
|
|
304
|
+
if (msg.stopReason) currentResult.stopReason = msg.stopReason;
|
|
305
|
+
if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
|
|
306
|
+
}
|
|
307
|
+
emitUpdate();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (event.type === "tool_result_end" && event.message) {
|
|
311
|
+
currentResult.messages.push(event.message as Message);
|
|
312
|
+
emitUpdate();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function waitForFile(filePath: string, signal: AbortSignal | undefined, timeoutMs = 30 * 60 * 1000): Promise<boolean> {
|
|
317
|
+
const started = Date.now();
|
|
318
|
+
while (Date.now() - started < timeoutMs) {
|
|
319
|
+
if (signal?.aborted) return false;
|
|
320
|
+
if (fs.existsSync(filePath)) return true;
|
|
321
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
322
|
+
}
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
260
326
|
type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
|
|
261
327
|
|
|
262
328
|
async function runSingleAgent(
|
|
@@ -286,10 +352,6 @@ async function runSingleAgent(
|
|
|
286
352
|
};
|
|
287
353
|
}
|
|
288
354
|
|
|
289
|
-
const args: string[] = ["--mode", "json", "-p", "--no-session"];
|
|
290
|
-
if (agent.model) args.push("--model", agent.model);
|
|
291
|
-
if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
|
|
292
|
-
|
|
293
355
|
let tmpPromptDir: string | null = null;
|
|
294
356
|
let tmpPromptPath: string | null = null;
|
|
295
357
|
|
|
@@ -319,10 +381,8 @@ async function runSingleAgent(
|
|
|
319
381
|
const tmp = writePromptToTempFile(agent.name, agent.systemPrompt);
|
|
320
382
|
tmpPromptDir = tmp.dir;
|
|
321
383
|
tmpPromptPath = tmp.filePath;
|
|
322
|
-
args.push("--append-system-prompt", tmpPromptPath);
|
|
323
384
|
}
|
|
324
|
-
|
|
325
|
-
args.push(`Task: ${task}`);
|
|
385
|
+
const args = buildSubagentProcessArgs(agent, task, tmpPromptPath);
|
|
326
386
|
let wasAborted = false;
|
|
327
387
|
|
|
328
388
|
const exitCode = await new Promise<number>((resolve) => {
|
|
@@ -336,48 +396,11 @@ async function runSingleAgent(
|
|
|
336
396
|
liveSubagentProcesses.add(proc);
|
|
337
397
|
let buffer = "";
|
|
338
398
|
|
|
339
|
-
const processLine = (line: string) => {
|
|
340
|
-
if (!line.trim()) return;
|
|
341
|
-
let event: any;
|
|
342
|
-
try {
|
|
343
|
-
event = JSON.parse(line);
|
|
344
|
-
} catch {
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (event.type === "message_end" && event.message) {
|
|
349
|
-
const msg = event.message as Message;
|
|
350
|
-
currentResult.messages.push(msg);
|
|
351
|
-
|
|
352
|
-
if (msg.role === "assistant") {
|
|
353
|
-
currentResult.usage.turns++;
|
|
354
|
-
const usage = msg.usage;
|
|
355
|
-
if (usage) {
|
|
356
|
-
currentResult.usage.input += usage.input || 0;
|
|
357
|
-
currentResult.usage.output += usage.output || 0;
|
|
358
|
-
currentResult.usage.cacheRead += usage.cacheRead || 0;
|
|
359
|
-
currentResult.usage.cacheWrite += usage.cacheWrite || 0;
|
|
360
|
-
currentResult.usage.cost += usage.cost?.total || 0;
|
|
361
|
-
currentResult.usage.contextTokens = usage.totalTokens || 0;
|
|
362
|
-
}
|
|
363
|
-
if (!currentResult.model && msg.model) currentResult.model = msg.model;
|
|
364
|
-
if (msg.stopReason) currentResult.stopReason = msg.stopReason;
|
|
365
|
-
if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
|
|
366
|
-
}
|
|
367
|
-
emitUpdate();
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (event.type === "tool_result_end" && event.message) {
|
|
371
|
-
currentResult.messages.push(event.message as Message);
|
|
372
|
-
emitUpdate();
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
|
-
|
|
376
399
|
proc.stdout.on("data", (data) => {
|
|
377
400
|
buffer += data.toString();
|
|
378
401
|
const lines = buffer.split("\n");
|
|
379
402
|
buffer = lines.pop() || "";
|
|
380
|
-
for (const line of lines)
|
|
403
|
+
for (const line of lines) processSubagentEventLine(line, currentResult, emitUpdate);
|
|
381
404
|
});
|
|
382
405
|
|
|
383
406
|
proc.stderr.on("data", (data) => {
|
|
@@ -386,7 +409,7 @@ async function runSingleAgent(
|
|
|
386
409
|
|
|
387
410
|
proc.on("close", (code) => {
|
|
388
411
|
liveSubagentProcesses.delete(proc);
|
|
389
|
-
if (buffer.trim())
|
|
412
|
+
if (buffer.trim()) processSubagentEventLine(buffer, currentResult, emitUpdate);
|
|
390
413
|
resolve(code ?? 0);
|
|
391
414
|
});
|
|
392
415
|
|
|
@@ -427,6 +450,120 @@ async function runSingleAgent(
|
|
|
427
450
|
}
|
|
428
451
|
}
|
|
429
452
|
|
|
453
|
+
async function runSingleAgentInCmuxSplit(
|
|
454
|
+
cmuxClient: CmuxClient,
|
|
455
|
+
direction: "right" | "down",
|
|
456
|
+
defaultCwd: string,
|
|
457
|
+
agents: AgentConfig[],
|
|
458
|
+
agentName: string,
|
|
459
|
+
task: string,
|
|
460
|
+
cwd: string | undefined,
|
|
461
|
+
step: number | undefined,
|
|
462
|
+
signal: AbortSignal | undefined,
|
|
463
|
+
onUpdate: OnUpdateCallback | undefined,
|
|
464
|
+
makeDetails: (results: SingleResult[]) => SubagentDetails,
|
|
465
|
+
): Promise<SingleResult> {
|
|
466
|
+
const agent = agents.find((a) => a.name === agentName);
|
|
467
|
+
if (!agent) {
|
|
468
|
+
return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
let tmpPromptDir: string | null = null;
|
|
472
|
+
let tmpPromptPath: string | null = null;
|
|
473
|
+
let tmpOutputDir: string | null = null;
|
|
474
|
+
|
|
475
|
+
const currentResult: SingleResult = {
|
|
476
|
+
agent: agentName,
|
|
477
|
+
agentSource: agent.source,
|
|
478
|
+
task,
|
|
479
|
+
exitCode: 0,
|
|
480
|
+
messages: [],
|
|
481
|
+
stderr: "",
|
|
482
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
483
|
+
model: agent.model,
|
|
484
|
+
step,
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const emitUpdate = () => {
|
|
488
|
+
if (onUpdate) {
|
|
489
|
+
onUpdate({
|
|
490
|
+
content: [{ type: "text", text: getFinalOutput(currentResult.messages) || "(running...)" }],
|
|
491
|
+
details: makeDetails([currentResult]),
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
if (agent.systemPrompt.trim()) {
|
|
498
|
+
const tmp = writePromptToTempFile(agent.name, agent.systemPrompt);
|
|
499
|
+
tmpPromptDir = tmp.dir;
|
|
500
|
+
tmpPromptPath = tmp.filePath;
|
|
501
|
+
}
|
|
502
|
+
tmpOutputDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-cmux-"));
|
|
503
|
+
const stdoutPath = path.join(tmpOutputDir, "stdout.jsonl");
|
|
504
|
+
const stderrPath = path.join(tmpOutputDir, "stderr.log");
|
|
505
|
+
const exitPath = path.join(tmpOutputDir, "exit.code");
|
|
506
|
+
const cmuxSurfaceId = await cmuxClient.createSplit(direction);
|
|
507
|
+
if (!cmuxSurfaceId) {
|
|
508
|
+
return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const bundledPaths = (process.env.GSD_BUNDLED_EXTENSION_PATHS ?? "").split(path.delimiter).map((s) => s.trim()).filter(Boolean);
|
|
512
|
+
const extensionArgs = bundledPaths.flatMap((p) => ["--extension", p]);
|
|
513
|
+
const processArgs = [process.env.GSD_BIN_PATH!, ...extensionArgs, ...buildSubagentProcessArgs(agent, task, tmpPromptPath)];
|
|
514
|
+
const innerScript = [
|
|
515
|
+
`cd ${shellEscape(cwd ?? defaultCwd)}`,
|
|
516
|
+
"set -o pipefail",
|
|
517
|
+
`${shellEscape(process.execPath)} ${processArgs.map(shellEscape).join(" ")} 2> >(tee ${shellEscape(stderrPath)} >&2) | tee ${shellEscape(stdoutPath)}`,
|
|
518
|
+
"status=${PIPESTATUS[0]}",
|
|
519
|
+
`printf '%s' "$status" > ${shellEscape(exitPath)}`,
|
|
520
|
+
].join("; ");
|
|
521
|
+
|
|
522
|
+
const sent = await cmuxClient.sendSurface(cmuxSurfaceId, `bash -lc ${shellEscape(innerScript)}`);
|
|
523
|
+
if (!sent) {
|
|
524
|
+
return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const finished = await waitForFile(exitPath, signal);
|
|
528
|
+
if (!finished) {
|
|
529
|
+
currentResult.exitCode = 1;
|
|
530
|
+
currentResult.stderr = "cmux split execution timed out or was aborted";
|
|
531
|
+
return currentResult;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (fs.existsSync(stdoutPath)) {
|
|
535
|
+
const stdout = fs.readFileSync(stdoutPath, "utf-8");
|
|
536
|
+
for (const line of stdout.split("\n")) {
|
|
537
|
+
processSubagentEventLine(line, currentResult, emitUpdate);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (fs.existsSync(stderrPath)) {
|
|
541
|
+
currentResult.stderr = fs.readFileSync(stderrPath, "utf-8");
|
|
542
|
+
}
|
|
543
|
+
currentResult.exitCode = Number.parseInt(fs.readFileSync(exitPath, "utf-8").trim() || "1", 10) || 0;
|
|
544
|
+
return currentResult;
|
|
545
|
+
} finally {
|
|
546
|
+
if (tmpPromptPath)
|
|
547
|
+
try {
|
|
548
|
+
fs.unlinkSync(tmpPromptPath);
|
|
549
|
+
} catch {
|
|
550
|
+
/* ignore */
|
|
551
|
+
}
|
|
552
|
+
if (tmpPromptDir)
|
|
553
|
+
try {
|
|
554
|
+
fs.rmdirSync(tmpPromptDir);
|
|
555
|
+
} catch {
|
|
556
|
+
/* ignore */
|
|
557
|
+
}
|
|
558
|
+
if (tmpOutputDir)
|
|
559
|
+
try {
|
|
560
|
+
fs.rmSync(tmpOutputDir, { recursive: true, force: true });
|
|
561
|
+
} catch {
|
|
562
|
+
/* ignore */
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
430
567
|
const TaskItem = Type.Object({
|
|
431
568
|
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
432
569
|
task: Type.String({ description: "Task to delegate to the agent" }),
|
|
@@ -511,6 +648,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
511
648
|
const discovery = discoverAgents(ctx.cwd, agentScope);
|
|
512
649
|
const agents = discovery.agents;
|
|
513
650
|
const confirmProjectAgents = params.confirmProjectAgents ?? false;
|
|
651
|
+
const cmuxClient = CmuxClient.fromPreferences(loadEffectiveGSDPreferences()?.preferences);
|
|
652
|
+
const cmuxSplitsEnabled = cmuxClient.getConfig().splits;
|
|
514
653
|
|
|
515
654
|
// Resolve isolation mode
|
|
516
655
|
const isolationMode = readIsolationMode();
|
|
@@ -669,28 +808,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
669
808
|
const batchSize = params.tasks.length;
|
|
670
809
|
const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
|
|
671
810
|
const workerId = registerWorker(t.agent, t.task, index, batchSize, batchId);
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (isFailed && MAX_RETRIES > 0 && !signal?.aborted) {
|
|
693
|
-
result = await runSingleAgent(
|
|
811
|
+
const runTask = () => cmuxSplitsEnabled
|
|
812
|
+
? runSingleAgentInCmuxSplit(
|
|
813
|
+
cmuxClient,
|
|
814
|
+
index % 2 === 0 ? "right" : "down",
|
|
815
|
+
ctx.cwd,
|
|
816
|
+
agents,
|
|
817
|
+
t.agent,
|
|
818
|
+
t.task,
|
|
819
|
+
t.cwd,
|
|
820
|
+
undefined,
|
|
821
|
+
signal,
|
|
822
|
+
(partial) => {
|
|
823
|
+
if (partial.details?.results[0]) {
|
|
824
|
+
allResults[index] = partial.details.results[0];
|
|
825
|
+
emitParallelUpdate();
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
makeDetails("parallel"),
|
|
829
|
+
)
|
|
830
|
+
: runSingleAgent(
|
|
694
831
|
ctx.cwd,
|
|
695
832
|
agents,
|
|
696
833
|
t.agent,
|
|
@@ -706,6 +843,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
706
843
|
},
|
|
707
844
|
makeDetails("parallel"),
|
|
708
845
|
);
|
|
846
|
+
let result = await runTask();
|
|
847
|
+
|
|
848
|
+
// Auto-retry failed tasks (likely API rate limit or transient error)
|
|
849
|
+
const isFailed = result.exitCode !== 0 || (result.messages.length === 0 && !signal?.aborted);
|
|
850
|
+
if (isFailed && MAX_RETRIES > 0 && !signal?.aborted) {
|
|
851
|
+
result = await runTask();
|
|
709
852
|
}
|
|
710
853
|
|
|
711
854
|
updateWorker(workerId, result.exitCode === 0 ? "completed" : "failed");
|
|
@@ -744,17 +887,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
744
887
|
isolation = await createIsolation(effectiveCwd, taskId, isolationMode);
|
|
745
888
|
}
|
|
746
889
|
|
|
747
|
-
const result =
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
890
|
+
const result = cmuxSplitsEnabled
|
|
891
|
+
? await runSingleAgentInCmuxSplit(
|
|
892
|
+
cmuxClient,
|
|
893
|
+
"right",
|
|
894
|
+
ctx.cwd,
|
|
895
|
+
agents,
|
|
896
|
+
params.agent,
|
|
897
|
+
params.task,
|
|
898
|
+
isolation ? isolation.workDir : params.cwd,
|
|
899
|
+
undefined,
|
|
900
|
+
signal,
|
|
901
|
+
onUpdate,
|
|
902
|
+
makeDetails("single"),
|
|
903
|
+
)
|
|
904
|
+
: await runSingleAgent(
|
|
905
|
+
ctx.cwd,
|
|
906
|
+
agents,
|
|
907
|
+
params.agent,
|
|
908
|
+
params.task,
|
|
909
|
+
isolation ? isolation.workDir : params.cwd,
|
|
910
|
+
undefined,
|
|
911
|
+
signal,
|
|
912
|
+
onUpdate,
|
|
913
|
+
makeDetails("single"),
|
|
914
|
+
);
|
|
758
915
|
|
|
759
916
|
// Capture and merge delta if isolated
|
|
760
917
|
if (isolation) {
|