@oh-my-pi/pi-coding-agent 15.11.7 → 15.12.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 +63 -1
- package/dist/cli.js +8106 -7708
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/collab/crypto.d.ts +7 -0
- package/dist/types/collab/guest.d.ts +23 -0
- package/dist/types/collab/host.d.ts +29 -0
- package/dist/types/collab/protocol.d.ts +113 -0
- package/dist/types/collab/relay-client.d.ts +22 -0
- package/dist/types/commands/join.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +60 -5
- package/dist/types/export/custom-share.d.ts +1 -2
- package/dist/types/export/html/index.d.ts +39 -1
- package/dist/types/export/share.d.ts +43 -0
- package/dist/types/extensibility/slash-commands.d.ts +1 -11
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/agent-hub.d.ts +32 -1
- package/dist/types/modes/components/collab-prompt-message.d.ts +10 -0
- package/dist/types/modes/components/hook-selector.d.ts +4 -6
- package/dist/types/modes/components/segment-track.d.ts +11 -6
- package/dist/types/modes/components/status-line/component.d.ts +10 -2
- package/dist/types/modes/components/status-line/types.d.ts +11 -0
- package/dist/types/modes/controllers/event-controller.d.ts +7 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/session-focus-controller.d.ts +31 -0
- package/dist/types/modes/interactive-mode.d.ts +16 -0
- package/dist/types/modes/session-observer-registry.d.ts +7 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +20 -0
- package/dist/types/session/agent-session.d.ts +13 -0
- package/dist/types/session/codex-auto-reset.d.ts +8 -4
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/snapcompact-inline.d.ts +6 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +9 -0
- package/dist/types/task/executor.d.ts +7 -0
- package/dist/types/task/types.d.ts +9 -0
- package/package.json +14 -13
- package/scripts/bench-guard.ts +71 -0
- package/scripts/build-binary.ts +4 -0
- package/scripts/bundle-dist.ts +4 -0
- package/scripts/generate-share-viewer.ts +34 -0
- package/src/cli/args.ts +2 -0
- package/src/cli-commands.ts +1 -0
- package/src/collab/crypto.ts +63 -0
- package/src/collab/guest.ts +450 -0
- package/src/collab/host.ts +556 -0
- package/src/collab/protocol.ts +232 -0
- package/src/collab/relay-client.ts +216 -0
- package/src/commands/join.ts +39 -0
- package/src/config/model-registry.ts +22 -14
- package/src/config/settings-schema.ts +67 -5
- package/src/config/settings.ts +12 -0
- package/src/export/custom-share.ts +1 -1
- package/src/export/html/index.ts +122 -17
- package/src/export/html/share-loader.js +102 -0
- package/src/export/html/template.css +745 -459
- package/src/export/html/template.html +6 -3
- package/src/export/html/template.js +240 -915
- package/src/export/html/tool-views.generated.js +38 -0
- package/src/export/share.ts +268 -0
- package/src/extensibility/slash-commands.ts +1 -97
- package/src/internal-urls/docs-index.generated.ts +74 -73
- package/src/main.ts +33 -11
- package/src/modes/components/agent-hub.ts +659 -431
- package/src/modes/components/assistant-message.ts +126 -6
- package/src/modes/components/collab-prompt-message.ts +30 -0
- package/src/modes/components/hook-selector.ts +4 -5
- package/src/modes/components/segment-track.ts +44 -7
- package/src/modes/components/status-line/component.ts +59 -6
- package/src/modes/components/status-line/presets.ts +1 -1
- package/src/modes/components/status-line/segments.ts +18 -1
- package/src/modes/components/status-line/types.ts +12 -0
- package/src/modes/components/tips.txt +4 -1
- package/src/modes/controllers/command-controller.ts +55 -96
- package/src/modes/controllers/event-controller.ts +45 -16
- package/src/modes/controllers/input-controller.ts +175 -9
- package/src/modes/controllers/selector-controller.ts +13 -15
- package/src/modes/controllers/session-focus-controller.ts +112 -0
- package/src/modes/controllers/streaming-reveal.ts +7 -0
- package/src/modes/interactive-mode.ts +56 -6
- package/src/modes/session-observer-registry.ts +11 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/modes/types.ts +20 -0
- package/src/modes/utils/ui-helpers.ts +23 -13
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk.ts +239 -36
- package/src/session/agent-session.ts +82 -7
- package/src/session/codex-auto-reset.ts +23 -11
- package/src/session/session-manager.ts +44 -0
- package/src/session/snapcompact-inline.ts +9 -3
- package/src/slash-commands/builtin-registry.ts +261 -24
- package/src/task/executor.ts +14 -0
- package/src/task/index.ts +5 -1
- package/src/task/render.ts +76 -5
- package/src/task/types.ts +9 -0
- package/src/tiny/worker.ts +17 -95
- package/src/tools/job.ts +6 -9
- package/src/tools/read.ts +38 -5
- package/src/tools/write.ts +13 -42
- package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
- package/dist/types/export/html/template.generated.d.ts +0 -1
- package/dist/types/export/html/template.macro.d.ts +0 -5
- package/dist/types/tiny/compiled-runtime.d.ts +0 -35
- package/scripts/generate-template.ts +0 -33
- package/src/bun-imports.d.ts +0 -28
- package/src/export/html/template.generated.ts +0 -2
- package/src/export/html/template.macro.ts +0 -25
- package/src/tiny/compiled-runtime.ts +0 -179
|
@@ -3,8 +3,10 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
5
5
|
import { setNextRequestDebugPath } from "@oh-my-pi/pi-ai/utils/request-debug";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import type { AutocompleteItem } from "@oh-my-pi/pi-tui";
|
|
7
|
+
import { APP_NAME, setProjectDir } from "@oh-my-pi/pi-utils";
|
|
8
|
+
import { COLLAB_GUEST_ALLOWED_COMMANDS, CollabGuestLink } from "../collab/guest";
|
|
9
|
+
import { CollabHost } from "../collab/host";
|
|
8
10
|
import type { SettingPath, SettingValue } from "../config/settings";
|
|
9
11
|
import { settings } from "../config/settings";
|
|
10
12
|
import {
|
|
@@ -12,6 +14,7 @@ import {
|
|
|
12
14
|
resolveActiveProjectRegistryPath,
|
|
13
15
|
resolveOrDefaultProjectRegistryPath,
|
|
14
16
|
} from "../discovery/helpers.js";
|
|
17
|
+
import { shareSession } from "../export/share";
|
|
15
18
|
import { PluginManager } from "../extensibility/plugins";
|
|
16
19
|
import {
|
|
17
20
|
getInstalledPluginsRegistryPath,
|
|
@@ -21,9 +24,11 @@ import {
|
|
|
21
24
|
MarketplaceManager,
|
|
22
25
|
} from "../extensibility/plugins/marketplace";
|
|
23
26
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
27
|
+
import { theme } from "../modes/theme/theme";
|
|
24
28
|
import type { InteractiveModeContext } from "../modes/types";
|
|
25
29
|
import type { AgentSession, FreshSessionResult } from "../session/agent-session";
|
|
26
30
|
import { formatShakeSummary, type ShakeMode } from "../session/shake-types";
|
|
31
|
+
import { urlHyperlinkAlways } from "../tui";
|
|
27
32
|
import { getChangelogPath, parseChangelog } from "../utils/changelog";
|
|
28
33
|
import { buildContextReportText } from "./helpers/context-report";
|
|
29
34
|
import { formatDuration } from "./helpers/format";
|
|
@@ -42,6 +47,7 @@ import type {
|
|
|
42
47
|
SlashCommandResult,
|
|
43
48
|
SlashCommandRuntime,
|
|
44
49
|
SlashCommandSpec,
|
|
50
|
+
SubcommandDef,
|
|
45
51
|
TuiSlashCommandRuntime,
|
|
46
52
|
} from "./types";
|
|
47
53
|
|
|
@@ -56,6 +62,30 @@ function refreshStatusLine(ctx: InteractiveModeContext): void {
|
|
|
56
62
|
ctx.ui.requestRender();
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
/** Scheme-less display form of a browser deep link: accent + underline, OSC-8 linked to the full URL. */
|
|
66
|
+
function collabWebLinkClickable(webLink: string): string {
|
|
67
|
+
const display = theme.fg("accent", `\x1b[4m${webLink.replace(/^https?:\/\//, "")}\x1b[24m`);
|
|
68
|
+
return urlHyperlinkAlways(webLink, display);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Join hint printed by /collab: compact terminal link + clickable browser deep link. */
|
|
72
|
+
function collabLinkHint(host: CollabHost, heading: string, view = false): string {
|
|
73
|
+
const bullet = theme.fg("accent", theme.format.bullet);
|
|
74
|
+
const link = view ? host.viewLink : host.link;
|
|
75
|
+
const webLink = view ? host.webViewLink : host.webLink;
|
|
76
|
+
return [
|
|
77
|
+
theme.fg("success", heading),
|
|
78
|
+
` ${bullet} ${theme.fg("muted", view ? "Watch from another terminal:" : "Join from another terminal:")} ${APP_NAME} join "${link}"`,
|
|
79
|
+
` ${bullet} ${theme.fg("muted", "or any web browser:")} ${collabWebLinkClickable(webLink)}`,
|
|
80
|
+
theme.fg(
|
|
81
|
+
"dim",
|
|
82
|
+
view
|
|
83
|
+
? "Anyone with this link can watch the session but cannot prompt the agent."
|
|
84
|
+
: "Anyone with the link can read the session and prompt the agent. Read-only link: /collab view",
|
|
85
|
+
),
|
|
86
|
+
].join("\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
59
89
|
function formatFreshSessionResult(result: FreshSessionResult): string {
|
|
60
90
|
const stateLabel = result.closedProviderSessions === 1 ? "provider state" : "provider states";
|
|
61
91
|
return `Fresh provider session started (${result.closedProviderSessions} ${stateLabel} pruned).`;
|
|
@@ -410,31 +440,21 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
410
440
|
},
|
|
411
441
|
{
|
|
412
442
|
name: "share",
|
|
413
|
-
description: "Share session
|
|
443
|
+
description: "Share session via an encrypted link (secret gist or share server)",
|
|
414
444
|
handle: async (_command, runtime) => {
|
|
415
|
-
const tmpFile = path.join(os.tmpdir(), `${Snowflake.next()}.html`);
|
|
416
445
|
try {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
422
|
-
const
|
|
423
|
-
if (result.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
runtime,
|
|
427
|
-
);
|
|
428
|
-
}
|
|
429
|
-
const gistUrl = result.stdout.toString("utf-8").trim();
|
|
430
|
-
const gistId = gistUrl.split("/").pop();
|
|
431
|
-
if (!gistId) return usage("Failed to parse gist ID from gh output", runtime);
|
|
432
|
-
await runtime.output(`Share URL: https://gistpreview.github.io/?${gistId}\nGist: ${gistUrl}`);
|
|
446
|
+
const result = await shareSession(runtime.sessionManager, {
|
|
447
|
+
serverUrl: runtime.settings.get("share.serverUrl"),
|
|
448
|
+
state: runtime.session.state,
|
|
449
|
+
obfuscator: runtime.settings.get("share.redactSecrets") ? runtime.session.obfuscator : undefined,
|
|
450
|
+
});
|
|
451
|
+
const lines = [`Share URL: ${result.url}`];
|
|
452
|
+
if (result.gistUrl) lines.push(`Gist: ${result.gistUrl}`);
|
|
453
|
+
if (result.truncated) lines.push("Note: large content was trimmed to fit the share size limit.");
|
|
454
|
+
await runtime.output(lines.join("\n"));
|
|
433
455
|
return commandConsumed();
|
|
434
|
-
} catch {
|
|
435
|
-
return usage(
|
|
436
|
-
} finally {
|
|
437
|
-
await fs.rm(tmpFile, { force: true }).catch(() => {});
|
|
456
|
+
} catch (err) {
|
|
457
|
+
return usage(`Failed to share session: ${errorMessage(err)}`, runtime);
|
|
438
458
|
}
|
|
439
459
|
},
|
|
440
460
|
handleTui: async (_command, runtime) => {
|
|
@@ -442,6 +462,126 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
442
462
|
runtime.ctx.editor.setText("");
|
|
443
463
|
},
|
|
444
464
|
},
|
|
465
|
+
{
|
|
466
|
+
name: "collab",
|
|
467
|
+
description: "Share this session live via a relay",
|
|
468
|
+
inlineHint: "[start|view|stop|status] [relayUrl]",
|
|
469
|
+
subcommands: [
|
|
470
|
+
{ name: "view", description: "Share a read-only link (guests can watch, not prompt)" },
|
|
471
|
+
{ name: "status", description: "Show link + participants" },
|
|
472
|
+
{ name: "stop", description: "Stop sharing" },
|
|
473
|
+
],
|
|
474
|
+
allowArgs: true,
|
|
475
|
+
handleTui: async (command, runtime) => {
|
|
476
|
+
const ctx = runtime.ctx;
|
|
477
|
+
ctx.editor.setText("");
|
|
478
|
+
const args = command.args.trim();
|
|
479
|
+
const [first = ""] = args.split(/\s+/, 1);
|
|
480
|
+
if (first === "stop") {
|
|
481
|
+
if (!ctx.collabHost) {
|
|
482
|
+
ctx.showStatus("Not hosting a collab session");
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
await ctx.collabHost.stop("host stopped");
|
|
486
|
+
ctx.showStatus("Collab stopped");
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (first === "status") {
|
|
490
|
+
if (ctx.collabHost) {
|
|
491
|
+
const names = ctx.collabHost.participants.map(p =>
|
|
492
|
+
p.role === "host" ? `${p.name} (host)` : p.readOnly ? `${p.name} (view-only)` : p.name,
|
|
493
|
+
);
|
|
494
|
+
ctx.showStatus(`Collab: ${names.join(", ")} — ${collabWebLinkClickable(ctx.collabHost.webLink)}`);
|
|
495
|
+
} else if (ctx.collabGuest) {
|
|
496
|
+
ctx.showStatus(
|
|
497
|
+
ctx.collabGuest.readOnly
|
|
498
|
+
? "In a collab session as a read-only guest (/leave to exit)"
|
|
499
|
+
: "In a collab session as a guest (/leave to exit)",
|
|
500
|
+
);
|
|
501
|
+
} else {
|
|
502
|
+
ctx.showStatus("Not in a collab session");
|
|
503
|
+
}
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (ctx.collabGuest) {
|
|
507
|
+
ctx.showError("Already in a collab session as a guest (/leave first)");
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const view = first === "view";
|
|
511
|
+
if (ctx.collabHost) {
|
|
512
|
+
ctx.showStatus(
|
|
513
|
+
collabLinkHint(ctx.collabHost, view ? "Read-only collab link" : "Collab session active", view),
|
|
514
|
+
{ dim: false },
|
|
515
|
+
);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const explicitUrl = first === "start" || view ? args.slice(first.length).trim() : args;
|
|
519
|
+
const relayInput = explicitUrl || ctx.settings.get("collab.relayUrl") || "";
|
|
520
|
+
if (!relayInput) {
|
|
521
|
+
ctx.showError(
|
|
522
|
+
"No relay configured. Set collab.relayUrl in /settings or pass one: /collab relay.example.com",
|
|
523
|
+
);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
// Scheme-less relay args default to wss (ws:// must be spelled out for localhost).
|
|
527
|
+
const relayUrl = relayInput.includes("://") ? relayInput : `wss://${relayInput}`;
|
|
528
|
+
const host = new CollabHost(ctx);
|
|
529
|
+
try {
|
|
530
|
+
await host.start(relayUrl);
|
|
531
|
+
} catch (err) {
|
|
532
|
+
ctx.showError(`Failed to start collab session: ${errorMessage(err)}`);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
ctx.collabHost = host;
|
|
536
|
+
ctx.showStatus(collabLinkHint(host, "Collab session started!", view), { dim: false });
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
name: "join",
|
|
541
|
+
description: "Join a shared collab session",
|
|
542
|
+
inlineHint: "<link>",
|
|
543
|
+
allowArgs: true,
|
|
544
|
+
handleTui: async (command, runtime) => {
|
|
545
|
+
const ctx = runtime.ctx;
|
|
546
|
+
ctx.editor.setText("");
|
|
547
|
+
const link = command.args.trim();
|
|
548
|
+
if (!link) {
|
|
549
|
+
ctx.showError("Usage: /join <link>");
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (ctx.collabHost) {
|
|
553
|
+
ctx.showError("Stop hosting first (/collab stop)");
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (ctx.collabGuest) {
|
|
557
|
+
ctx.showError("Already in a collab session (/leave first)");
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
await new CollabGuestLink(ctx).join(link);
|
|
562
|
+
} catch (err) {
|
|
563
|
+
ctx.showError(`Failed to join collab session: ${errorMessage(err)}`);
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
name: "leave",
|
|
569
|
+
description: "Leave the collab session",
|
|
570
|
+
handleTui: async (_command, runtime) => {
|
|
571
|
+
const ctx = runtime.ctx;
|
|
572
|
+
ctx.editor.setText("");
|
|
573
|
+
if (ctx.collabGuest) {
|
|
574
|
+
await ctx.collabGuest.leave("left");
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (ctx.collabHost) {
|
|
578
|
+
await ctx.collabHost.stop("host stopped");
|
|
579
|
+
ctx.showStatus("Collab stopped");
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
ctx.showStatus("Not in a collab session");
|
|
583
|
+
},
|
|
584
|
+
},
|
|
445
585
|
{
|
|
446
586
|
name: "browser",
|
|
447
587
|
description: "Toggle browser headless vs visible mode",
|
|
@@ -1853,6 +1993,70 @@ for (const command of BUILTIN_SLASH_COMMAND_REGISTRY) {
|
|
|
1853
1993
|
|
|
1854
1994
|
export const BUILTIN_SLASH_COMMAND_RESERVED_NAMES: ReadonlySet<string> = new Set(BUILTIN_SLASH_COMMAND_LOOKUP.keys());
|
|
1855
1995
|
|
|
1996
|
+
/**
|
|
1997
|
+
* Build getArgumentCompletions from declarative subcommand definitions.
|
|
1998
|
+
* Returns subcommand names filtered by prefix in the dropdown.
|
|
1999
|
+
*/
|
|
2000
|
+
function buildArgumentCompletions(subcommands: SubcommandDef[]): (prefix: string) => AutocompleteItem[] | null {
|
|
2001
|
+
return (argumentPrefix: string) => {
|
|
2002
|
+
if (argumentPrefix.includes(" ")) return null; // past the subcommand
|
|
2003
|
+
const lower = argumentPrefix.toLowerCase();
|
|
2004
|
+
const matches = subcommands
|
|
2005
|
+
.filter(s => s.name.startsWith(lower))
|
|
2006
|
+
.map(s => ({
|
|
2007
|
+
value: `${s.name} `,
|
|
2008
|
+
label: s.name,
|
|
2009
|
+
description: s.description,
|
|
2010
|
+
hint: s.usage,
|
|
2011
|
+
}));
|
|
2012
|
+
return matches.length > 0 ? matches : null;
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
/**
|
|
2017
|
+
* Build getInlineHint from declarative subcommand definitions.
|
|
2018
|
+
* Shows remaining completion + usage as dim ghost text after cursor.
|
|
2019
|
+
*/
|
|
2020
|
+
function buildSubcommandInlineHint(subcommands: SubcommandDef[]): (argumentText: string) => string | null {
|
|
2021
|
+
return (argumentText: string) => {
|
|
2022
|
+
const trimmed = argumentText.trimStart();
|
|
2023
|
+
const spaceIndex = trimmed.indexOf(" ");
|
|
2024
|
+
|
|
2025
|
+
if (spaceIndex === -1) {
|
|
2026
|
+
// Still typing subcommand name — show remaining chars + usage
|
|
2027
|
+
const prefix = trimmed.toLowerCase();
|
|
2028
|
+
if (prefix.length === 0) return null;
|
|
2029
|
+
const match = subcommands.find(s => s.name.startsWith(prefix));
|
|
2030
|
+
if (!match) return null;
|
|
2031
|
+
const remaining = match.name.slice(prefix.length);
|
|
2032
|
+
return remaining + (match.usage ? ` ${match.usage}` : "");
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// Subcommand typed — show remaining usage params
|
|
2036
|
+
const subName = trimmed.slice(0, spaceIndex).toLowerCase();
|
|
2037
|
+
const afterSub = trimmed.slice(spaceIndex + 1);
|
|
2038
|
+
const sub = subcommands.find(s => s.name === subName);
|
|
2039
|
+
if (!sub?.usage) return null;
|
|
2040
|
+
|
|
2041
|
+
if (afterSub.length > 0) {
|
|
2042
|
+
const usageParts = sub.usage.split(" ");
|
|
2043
|
+
const inputParts = afterSub.trim().split(/\s+/);
|
|
2044
|
+
const remaining = usageParts.slice(inputParts.length);
|
|
2045
|
+
return remaining.length > 0 ? remaining.join(" ") : null;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
return sub.usage;
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
/**
|
|
2053
|
+
* Build getInlineHint for commands with a simple static hint string.
|
|
2054
|
+
* Shows the hint only when no arguments have been typed yet.
|
|
2055
|
+
*/
|
|
2056
|
+
function buildStaticInlineHint(hint: string): (argumentText: string) => string | null {
|
|
2057
|
+
return (argumentText: string) => (argumentText.trim().length === 0 ? hint : null);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
1856
2060
|
/** Builtin command metadata used for slash-command autocomplete and help text. */
|
|
1857
2061
|
export const BUILTIN_SLASH_COMMAND_DEFS: ReadonlyArray<BuiltinSlashCommand> = BUILTIN_SLASH_COMMAND_REGISTRY.map(
|
|
1858
2062
|
command => ({
|
|
@@ -1864,6 +2068,32 @@ export const BUILTIN_SLASH_COMMAND_DEFS: ReadonlyArray<BuiltinSlashCommand> = BU
|
|
|
1864
2068
|
}),
|
|
1865
2069
|
);
|
|
1866
2070
|
|
|
2071
|
+
/**
|
|
2072
|
+
* Materialized builtin slash commands with completion functions derived from
|
|
2073
|
+
* declarative subcommand/hint definitions.
|
|
2074
|
+
*/
|
|
2075
|
+
export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<
|
|
2076
|
+
BuiltinSlashCommand & {
|
|
2077
|
+
getArgumentCompletions?: (prefix: string) => AutocompleteItem[] | null;
|
|
2078
|
+
getInlineHint?: (argumentText: string) => string | null;
|
|
2079
|
+
}
|
|
2080
|
+
> = BUILTIN_SLASH_COMMAND_DEFS.map(cmd => {
|
|
2081
|
+
if (cmd.subcommands) {
|
|
2082
|
+
return {
|
|
2083
|
+
...cmd,
|
|
2084
|
+
getArgumentCompletions: buildArgumentCompletions(cmd.subcommands),
|
|
2085
|
+
getInlineHint: buildSubcommandInlineHint(cmd.subcommands),
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
if (cmd.inlineHint) {
|
|
2089
|
+
return {
|
|
2090
|
+
...cmd,
|
|
2091
|
+
getInlineHint: buildStaticInlineHint(cmd.inlineHint),
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
return cmd;
|
|
2095
|
+
});
|
|
2096
|
+
|
|
1867
2097
|
/**
|
|
1868
2098
|
* Unified registry exposed for cross-mode tooling. Each spec carries at least
|
|
1869
2099
|
* one of `handle` / `handleTui`. The TUI dispatcher prefers `handleTui`; the
|
|
@@ -1890,6 +2120,13 @@ export async function executeBuiltinSlashCommand(
|
|
|
1890
2120
|
if (parsed.args.length > 0 && !command.allowArgs) {
|
|
1891
2121
|
return false;
|
|
1892
2122
|
}
|
|
2123
|
+
// Collab guests run a read-mostly replica: session-mutating builtins are
|
|
2124
|
+
// host-only; the allowlist covers purely local/read-only commands.
|
|
2125
|
+
if (runtime.ctx.collabGuest && !COLLAB_GUEST_ALLOWED_COMMANDS[command.name]) {
|
|
2126
|
+
runtime.ctx.showStatus(`/${command.name} is host-only during a collab session`);
|
|
2127
|
+
runtime.ctx.editor.setText("");
|
|
2128
|
+
return true;
|
|
2129
|
+
}
|
|
1893
2130
|
if (command.handleTui) {
|
|
1894
2131
|
const result = await command.handleTui(parsed, runtime);
|
|
1895
2132
|
if (result && typeof result === "object" && "prompt" in result) return result.prompt;
|
package/src/task/executor.ts
CHANGED
|
@@ -195,6 +195,13 @@ export interface ExecutorOptions {
|
|
|
195
195
|
index: number;
|
|
196
196
|
id: string;
|
|
197
197
|
parentToolCallId?: string;
|
|
198
|
+
/**
|
|
199
|
+
* Spawn runs as a detached background job (parent turn not blocked on it).
|
|
200
|
+
* Rides the subagent lifecycle/progress payloads so HUD-style surfaces can
|
|
201
|
+
* skip spawns the transcript already renders inline. See
|
|
202
|
+
* {@link SubagentLifecyclePayload.detached}.
|
|
203
|
+
*/
|
|
204
|
+
detached?: boolean;
|
|
198
205
|
modelOverride?: string | string[];
|
|
199
206
|
/**
|
|
200
207
|
* Active model selector of the parent session, used as an auth-aware fallback
|
|
@@ -656,6 +663,7 @@ interface RunMonitorArgs {
|
|
|
656
663
|
onProgress?: (progress: AgentProgress) => void;
|
|
657
664
|
eventBus?: EventBus;
|
|
658
665
|
parentToolCallId?: string;
|
|
666
|
+
detached?: boolean;
|
|
659
667
|
sessionFile?: string;
|
|
660
668
|
/** Soft assistant-request budget; 0 disables the guard. */
|
|
661
669
|
softRequestBudget: number;
|
|
@@ -836,6 +844,7 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
836
844
|
agentSource: agent.source,
|
|
837
845
|
task,
|
|
838
846
|
parentToolCallId: args.parentToolCallId,
|
|
847
|
+
detached: args.detached,
|
|
839
848
|
assignment,
|
|
840
849
|
progress: { ...progress },
|
|
841
850
|
sessionFile: args.sessionFile,
|
|
@@ -1413,6 +1422,7 @@ interface FinalizeRunArgs {
|
|
|
1413
1422
|
artifactsDir?: string;
|
|
1414
1423
|
eventBus?: EventBus;
|
|
1415
1424
|
parentToolCallId?: string;
|
|
1425
|
+
detached?: boolean;
|
|
1416
1426
|
sessionFile?: string;
|
|
1417
1427
|
startTime: number;
|
|
1418
1428
|
}
|
|
@@ -1511,6 +1521,7 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
|
|
|
1511
1521
|
id,
|
|
1512
1522
|
agent: agent.name,
|
|
1513
1523
|
parentToolCallId: args.parentToolCallId,
|
|
1524
|
+
detached: args.detached,
|
|
1514
1525
|
agentSource: agent.source,
|
|
1515
1526
|
description: args.description,
|
|
1516
1527
|
status: progress.status as "completed" | "failed" | "aborted",
|
|
@@ -1677,6 +1688,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1677
1688
|
onProgress,
|
|
1678
1689
|
eventBus: options.eventBus,
|
|
1679
1690
|
parentToolCallId: options.parentToolCallId,
|
|
1691
|
+
detached: options.detached,
|
|
1680
1692
|
sessionFile: subtaskSessionFile,
|
|
1681
1693
|
softRequestBudget,
|
|
1682
1694
|
maxRuntimeMs,
|
|
@@ -1921,6 +1933,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1921
1933
|
id,
|
|
1922
1934
|
agent: agent.name,
|
|
1923
1935
|
parentToolCallId: options.parentToolCallId,
|
|
1936
|
+
detached: options.detached,
|
|
1924
1937
|
agentSource: agent.source,
|
|
1925
1938
|
description: options.description,
|
|
1926
1939
|
status: "started",
|
|
@@ -2127,6 +2140,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2127
2140
|
artifactsDir: options.artifactsDir,
|
|
2128
2141
|
eventBus: options.eventBus,
|
|
2129
2142
|
parentToolCallId: options.parentToolCallId,
|
|
2143
|
+
detached: options.detached,
|
|
2130
2144
|
sessionFile: subtaskSessionFile,
|
|
2131
2145
|
startTime,
|
|
2132
2146
|
});
|
package/src/task/index.ts
CHANGED
|
@@ -705,6 +705,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
705
705
|
undefined,
|
|
706
706
|
agentId,
|
|
707
707
|
progress.index,
|
|
708
|
+
true,
|
|
708
709
|
);
|
|
709
710
|
const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
|
|
710
711
|
const singleResult = result.details?.results[0];
|
|
@@ -897,8 +898,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
897
898
|
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
|
898
899
|
preAllocatedId?: string,
|
|
899
900
|
spawnIndex = 0,
|
|
901
|
+
detached = false,
|
|
900
902
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
901
|
-
return this.#runSpawn(toolCallId, params, signal, onUpdate, preAllocatedId, spawnIndex);
|
|
903
|
+
return this.#runSpawn(toolCallId, params, signal, onUpdate, preAllocatedId, spawnIndex, detached);
|
|
902
904
|
}
|
|
903
905
|
|
|
904
906
|
/** Spawn a fresh subagent and run it to completion. */
|
|
@@ -909,6 +911,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
909
911
|
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
|
910
912
|
preAllocatedId?: string,
|
|
911
913
|
spawnIndex = 0,
|
|
914
|
+
detached = false,
|
|
912
915
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
913
916
|
const startTime = Date.now();
|
|
914
917
|
const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
|
|
@@ -1145,6 +1148,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1145
1148
|
description: params.description,
|
|
1146
1149
|
index: spawnIndex,
|
|
1147
1150
|
parentToolCallId: toolCallId,
|
|
1151
|
+
detached,
|
|
1148
1152
|
id: agentId,
|
|
1149
1153
|
taskDepth,
|
|
1150
1154
|
modelOverride,
|
package/src/task/render.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
|
|
|
15
15
|
import {
|
|
16
16
|
formatBadge,
|
|
17
17
|
formatDuration,
|
|
18
|
+
formatExpandHint,
|
|
18
19
|
formatMoreItems,
|
|
19
20
|
formatStatusIcon,
|
|
20
21
|
replaceTabs,
|
|
@@ -542,6 +543,12 @@ function renderTaskCallLines(args: Partial<TaskParams> | undefined, theme: Theme
|
|
|
542
543
|
return lines;
|
|
543
544
|
}
|
|
544
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Agent rows shown per collapsed task list; the rest fold into a single
|
|
548
|
+
* `… N more agents` summary line (expand uncaps).
|
|
549
|
+
*/
|
|
550
|
+
const COLLAPSED_AGENT_LIMIT = 4;
|
|
551
|
+
|
|
545
552
|
/**
|
|
546
553
|
* Render the per-item list (`id` + ui `description`) for a batch call's
|
|
547
554
|
* streaming preview. The args stream in token by token, so the array grows
|
|
@@ -552,7 +559,7 @@ function renderTaskItemLines(tasks: TaskItem[] | undefined, theme: Theme): strin
|
|
|
552
559
|
if (!Array.isArray(tasks) || tasks.length === 0) return [];
|
|
553
560
|
|
|
554
561
|
const bullet = theme.fg("dim", "•");
|
|
555
|
-
const cap = Math.min(tasks.length,
|
|
562
|
+
const cap = Math.min(tasks.length, COLLAPSED_AGENT_LIMIT);
|
|
556
563
|
const lines: string[] = [];
|
|
557
564
|
for (let i = 0; i < cap; i++) {
|
|
558
565
|
const task = tasks[i] as Partial<TaskItem> | undefined;
|
|
@@ -1170,6 +1177,53 @@ function orderResultsForDisplay(results: readonly SingleResult[]): SingleResult[
|
|
|
1170
1177
|
return [...results].sort((a, b) => a.durationMs - b.durationMs || a.index - b.index);
|
|
1171
1178
|
}
|
|
1172
1179
|
|
|
1180
|
+
/**
|
|
1181
|
+
* Summary line for progress rows folded away by the collapsed cap: per-status
|
|
1182
|
+
* counts plus the expand hint, e.g. `… 21 more agents (18 pending · 3 done)`.
|
|
1183
|
+
*/
|
|
1184
|
+
function formatHiddenProgressLine(hidden: readonly AgentProgress[], theme: Theme): string {
|
|
1185
|
+
const counts: Record<AgentProgress["status"], number> = {
|
|
1186
|
+
pending: 0,
|
|
1187
|
+
running: 0,
|
|
1188
|
+
completed: 0,
|
|
1189
|
+
failed: 0,
|
|
1190
|
+
aborted: 0,
|
|
1191
|
+
};
|
|
1192
|
+
for (const p of hidden) counts[p.status]++;
|
|
1193
|
+
const parts: string[] = [];
|
|
1194
|
+
if (counts.completed > 0) parts.push(theme.fg("dim", `${counts.completed} done`));
|
|
1195
|
+
if (counts.running > 0) parts.push(theme.fg("dim", `${counts.running} running`));
|
|
1196
|
+
if (counts.pending > 0) parts.push(theme.fg("dim", `${counts.pending} pending`));
|
|
1197
|
+
if (counts.failed > 0) parts.push(theme.fg("error", `${counts.failed} failed`));
|
|
1198
|
+
if (counts.aborted > 0) parts.push(theme.fg("error", `${counts.aborted} aborted`));
|
|
1199
|
+
const breakdown =
|
|
1200
|
+
parts.length > 0
|
|
1201
|
+
? `${theme.fg("dim", " (")}${parts.join(theme.fg("dim", theme.sep.dot))}${theme.fg("dim", ")")}`
|
|
1202
|
+
: "";
|
|
1203
|
+
const hint = formatExpandHint(theme, false, true);
|
|
1204
|
+
return `${theme.fg("dim", formatMoreItems(hidden.length, "agent"))}${breakdown}${hint ? ` ${hint}` : ""}`;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* Pick the agent rows that stay visible when a finalized batch is collapsed:
|
|
1209
|
+
* problem rows (aborted/failed/merge-failed) claim slots first so they are
|
|
1210
|
+
* never folded away, then fastest finishers fill the remainder. The pick is
|
|
1211
|
+
* filtered out of the display order, so visible rows keep the expanded layout.
|
|
1212
|
+
*/
|
|
1213
|
+
function selectCollapsedResults(ordered: readonly SingleResult[]): readonly SingleResult[] {
|
|
1214
|
+
if (ordered.length <= COLLAPSED_AGENT_LIMIT) return ordered;
|
|
1215
|
+
const picked = new Set<SingleResult>();
|
|
1216
|
+
for (const result of ordered) {
|
|
1217
|
+
if (picked.size >= COLLAPSED_AGENT_LIMIT) break;
|
|
1218
|
+
if (result.aborted || result.exitCode !== 0 || result.error) picked.add(result);
|
|
1219
|
+
}
|
|
1220
|
+
for (const result of ordered) {
|
|
1221
|
+
if (picked.size >= COLLAPSED_AGENT_LIMIT) break;
|
|
1222
|
+
picked.add(result);
|
|
1223
|
+
}
|
|
1224
|
+
return ordered.filter(result => picked.has(result));
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1173
1227
|
/**
|
|
1174
1228
|
* Render the tool result.
|
|
1175
1229
|
*/
|
|
@@ -1248,13 +1302,30 @@ export function renderResult(
|
|
|
1248
1302
|
const shouldRenderProgress =
|
|
1249
1303
|
Boolean(details.progress && details.progress.length > 0) && (isPartial || details.results.length === 0);
|
|
1250
1304
|
if (shouldRenderProgress && details.progress) {
|
|
1251
|
-
orderProgressForDisplay(details.progress)
|
|
1305
|
+
const ordered = orderProgressForDisplay(details.progress);
|
|
1306
|
+
// Collapsed view keeps the live edge: finished rows sort to the top of
|
|
1307
|
+
// the display order, so folding from the top keeps running/pending
|
|
1308
|
+
// agents (and their current-tool lines) visible while one summary line
|
|
1309
|
+
// stands in for everything above it.
|
|
1310
|
+
const visible = expanded ? ordered : ordered.slice(Math.max(0, ordered.length - COLLAPSED_AGENT_LIMIT));
|
|
1311
|
+
if (visible.length < ordered.length) {
|
|
1312
|
+
lines.push(formatHiddenProgressLine(ordered.slice(0, ordered.length - visible.length), theme));
|
|
1313
|
+
}
|
|
1314
|
+
for (const progress of visible) {
|
|
1252
1315
|
lines.push(...renderAgentProgress(progress, "", " ", expanded, theme, spinnerFrame, frozen));
|
|
1253
|
-
}
|
|
1316
|
+
}
|
|
1254
1317
|
} else if (details.results && details.results.length > 0) {
|
|
1255
|
-
orderResultsForDisplay(details.results)
|
|
1318
|
+
const ordered = orderResultsForDisplay(details.results);
|
|
1319
|
+
const visible = expanded ? ordered : selectCollapsedResults(ordered);
|
|
1320
|
+
for (const res of visible) {
|
|
1256
1321
|
lines.push(...renderAgentResult(res, "", " ", expanded, theme));
|
|
1257
|
-
}
|
|
1322
|
+
}
|
|
1323
|
+
if (visible.length < ordered.length) {
|
|
1324
|
+
const hint = formatExpandHint(theme, false, true);
|
|
1325
|
+
lines.push(
|
|
1326
|
+
`${theme.fg("dim", formatMoreItems(ordered.length - visible.length, "agent"))}${hint ? ` ${hint}` : ""}`,
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1258
1329
|
|
|
1259
1330
|
const abortedCount = details.results.filter(r => r.aborted).length;
|
|
1260
1331
|
const mergeFailedCount = details.results.filter(r => !r.aborted && r.exitCode === 0 && r.error).length;
|
package/src/task/types.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface SubagentProgressPayload {
|
|
|
45
45
|
assignment?: string;
|
|
46
46
|
progress: AgentProgress;
|
|
47
47
|
sessionFile?: string;
|
|
48
|
+
/** See {@link SubagentLifecyclePayload.detached}. */
|
|
49
|
+
detached?: boolean;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
/** Payload emitted on TASK_SUBAGENT_EVENT_CHANNEL */
|
|
@@ -63,6 +65,13 @@ export interface SubagentLifecyclePayload {
|
|
|
63
65
|
sessionFile?: string;
|
|
64
66
|
parentToolCallId?: string;
|
|
65
67
|
index: number;
|
|
68
|
+
/**
|
|
69
|
+
* Spawn runs as a detached background job: the parent turn keeps working
|
|
70
|
+
* while this agent runs. Sync task spawns (parent blocked on the call) and
|
|
71
|
+
* eval `agent()` bridge spawns (rendered inside their eval cell) leave this
|
|
72
|
+
* unset — surfaces like the subagent HUD only list detached spawns.
|
|
73
|
+
*/
|
|
74
|
+
detached?: boolean;
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
/**
|