@bastani/atomic 0.8.1 → 0.8.2
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 +12 -0
- package/dist/builtin/intercom/config.ts +3 -4
- package/dist/builtin/intercom/index.ts +6 -6
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/agent-dir.ts +11 -2
- package/dist/builtin/mcp/cli.js +12 -6
- package/dist/builtin/mcp/config.ts +31 -22
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/agents/agents.ts +63 -23
- package/dist/builtin/subagents/src/agents/skills.ts +21 -21
- package/dist/builtin/subagents/src/extension/index.ts +9 -8
- package/dist/builtin/subagents/src/runs/shared/run-history.ts +13 -10
- package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +3 -3
- package/dist/builtin/subagents/src/shared/artifacts.ts +18 -17
- package/dist/builtin/subagents/src/shared/types.ts +4 -4
- package/dist/builtin/web-access/config-paths.ts +11 -0
- package/dist/builtin/web-access/exa.ts +3 -2
- package/dist/builtin/web-access/gemini-api.ts +2 -1
- package/dist/builtin/web-access/gemini-search.ts +2 -1
- package/dist/builtin/web-access/gemini-web-config.ts +2 -1
- package/dist/builtin/web-access/github-extract.ts +2 -1
- package/dist/builtin/web-access/index.ts +11 -8
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/web-access/perplexity.ts +2 -1
- package/dist/builtin/web-access/video-extract.ts +2 -1
- package/dist/builtin/web-access/youtube-extract.ts +2 -1
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +4 -0
- package/dist/builtin/workflows/builtin/open-claude-design.ts +39 -22
- package/dist/builtin/workflows/builtin/ralph.ts +7 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/workflow/SKILL.md +28 -20
- package/dist/builtin/workflows/skills/workflow/references/design-checklist.md +8 -4
- package/dist/builtin/workflows/skills/workflow/references/running-workflows.md +52 -23
- package/dist/builtin/workflows/skills/workflow/references/sdk-authoring.md +41 -12
- package/dist/builtin/workflows/src/extension/config-loader.ts +13 -14
- package/dist/builtin/workflows/src/extension/discovery.ts +4 -6
- package/dist/builtin/workflows/src/extension/index.ts +675 -524
- package/dist/builtin/workflows/src/extension/runtime.ts +40 -16
- package/dist/builtin/workflows/src/extension/wiring.ts +3 -0
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +43 -33
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +34 -10
- package/dist/builtin/workflows/src/shared/types.ts +1 -5
- package/dist/builtin/workflows/src/tui/graph-view.ts +245 -75
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +23 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +259 -149
- package/dist/builtin/workflows/src/tui/status-helpers.ts +3 -3
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +99 -10
- package/dist/builtin/workflows/src/tui/switcher.ts +4 -5
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +29 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +11 -8
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts +21 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +59 -4
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +2 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +3 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +31 -8
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +9 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +11 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-registry.d.ts +3 -2
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +25 -8
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/package-manager.d.ts +3 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +97 -58
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/resource-loader.d.ts +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +37 -36
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +5 -4
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +2 -2
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +7 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +29 -8
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/telemetry.d.ts.map +1 -1
- package/dist/core/telemetry.js +2 -2
- package/dist/core/telemetry.js.map +1 -1
- package/dist/core/timings.d.ts.map +1 -1
- package/dist/core/timings.js +2 -2
- package/dist/core/timings.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +8 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/todos.d.ts.map +1 -1
- package/dist/core/tools/todos.js +3 -3
- package/dist/core/tools/todos.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +6 -6
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/atomic-banner.d.ts +4 -0
- package/dist/modes/interactive/components/atomic-banner.d.ts.map +1 -0
- package/dist/modes/interactive/components/atomic-banner.js +34 -0
- package/dist/modes/interactive/components/atomic-banner.js.map +1 -0
- package/dist/modes/interactive/components/chat-message-renderer.d.ts +99 -0
- package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -0
- package/dist/modes/interactive/components/chat-message-renderer.js +450 -0
- package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -0
- package/dist/modes/interactive/components/chat-transcript.d.ts +69 -0
- package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -0
- package/dist/modes/interactive/components/chat-transcript.js +183 -0
- package/dist/modes/interactive/components/chat-transcript.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts +16 -4
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +110 -137
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +2 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +2 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +9 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +192 -137
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/catppuccin-mocha.json +5 -5
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +11 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +2 -2
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +2 -2
- package/dist/utils/version-check.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { renderCall } from "./render-call.js";
|
|
2
2
|
import { renderResult } from "./render-result.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
RenderResultOpts,
|
|
5
|
+
WorkflowInputEntry,
|
|
6
|
+
WorkflowToolResult,
|
|
7
|
+
} from "./render-result.js";
|
|
4
8
|
import { renderInputsSchema } from "../shared/render-inputs-schema.js";
|
|
5
9
|
import { WorkflowParametersSchema } from "./workflow-schema.js";
|
|
6
10
|
import { renderRunBanner, renderRunSummary } from "./renderers.js";
|
|
@@ -66,7 +70,9 @@ import type {
|
|
|
66
70
|
WorkflowMaxOutput,
|
|
67
71
|
WorkflowModelCatalogPort,
|
|
68
72
|
WorkflowModelInfo,
|
|
69
|
-
|
|
73
|
+
StageOptions,
|
|
74
|
+
} from "../shared/types.js";
|
|
75
|
+
import { buildRuntimeAdapters } from "./wiring.js";
|
|
70
76
|
import type { PiUISurface } from "./wiring.js";
|
|
71
77
|
import { createStatusWriter } from "./status-writer.js";
|
|
72
78
|
import type { StatusWriter } from "./status-writer.js";
|
|
@@ -137,9 +143,7 @@ export type PiArgumentCompletionResult = PiArgumentCompletion[] | null;
|
|
|
137
143
|
export interface PiCommandOptions {
|
|
138
144
|
description: string;
|
|
139
145
|
handler: (args: string, ctx: PiCommandContext) => Promise<void> | void;
|
|
140
|
-
getArgumentCompletions?: (
|
|
141
|
-
partial: string,
|
|
142
|
-
) => PiArgumentCompletionResult;
|
|
146
|
+
getArgumentCompletions?: (partial: string) => PiArgumentCompletionResult;
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
/**
|
|
@@ -225,7 +229,9 @@ export interface PiExecuteContext extends PiModelContext {
|
|
|
225
229
|
sessionId?: string;
|
|
226
230
|
ui?: PiUISurface;
|
|
227
231
|
hasUI?: boolean;
|
|
228
|
-
sessionManager?: SessionManager & {
|
|
232
|
+
sessionManager?: SessionManager & {
|
|
233
|
+
getSessionFile?: () => string | undefined;
|
|
234
|
+
};
|
|
229
235
|
[key: string]: unknown;
|
|
230
236
|
}
|
|
231
237
|
|
|
@@ -362,11 +368,18 @@ export interface ExtensionAPI {
|
|
|
362
368
|
// Workflow tool argument shape
|
|
363
369
|
// ---------------------------------------------------------------------------
|
|
364
370
|
|
|
365
|
-
export interface WorkflowToolArgs {
|
|
371
|
+
export interface WorkflowToolArgs extends StageOptions {
|
|
366
372
|
/** Canonical named workflow identifier. */
|
|
367
373
|
workflow?: string;
|
|
368
374
|
inputs?: Record<string, unknown>;
|
|
369
|
-
action?:
|
|
375
|
+
action?:
|
|
376
|
+
| "run"
|
|
377
|
+
| "list"
|
|
378
|
+
| "get"
|
|
379
|
+
| "status"
|
|
380
|
+
| "interrupt"
|
|
381
|
+
| "resume"
|
|
382
|
+
| "inputs";
|
|
370
383
|
/** Canonical run identifier for status/interrupt/resume. */
|
|
371
384
|
runId?: string;
|
|
372
385
|
/** Direct single-task mode, or root task string when chain is present. */
|
|
@@ -385,18 +398,17 @@ export interface WorkflowToolArgs {
|
|
|
385
398
|
enabled?: boolean;
|
|
386
399
|
delivery?: "off" | "notify" | "result" | "control-and-result";
|
|
387
400
|
parentSession?: string;
|
|
388
|
-
notifyOn?: Array<
|
|
401
|
+
notifyOn?: Array<
|
|
402
|
+
"active_long_running" | "needs_attention" | "completed" | "failed"
|
|
403
|
+
>;
|
|
389
404
|
};
|
|
390
|
-
cwd?: string;
|
|
391
405
|
output?: string | false;
|
|
392
406
|
outputMode?: "inline" | "file-only";
|
|
393
407
|
chainDir?: string;
|
|
394
408
|
maxOutput?: WorkflowMaxOutput;
|
|
395
409
|
artifacts?: boolean;
|
|
396
|
-
sessionDir?: string;
|
|
397
410
|
progress?: boolean;
|
|
398
411
|
worktree?: boolean;
|
|
399
|
-
fallbackModels?: string[];
|
|
400
412
|
}
|
|
401
413
|
|
|
402
414
|
// ---------------------------------------------------------------------------
|
|
@@ -422,31 +434,42 @@ function directModeCount(args: WorkflowToolArgs): number {
|
|
|
422
434
|
}
|
|
423
435
|
|
|
424
436
|
function hasNamedExecutionMode(args: WorkflowToolArgs): boolean {
|
|
425
|
-
return (
|
|
426
|
-
typeof args.workflow === "string" && args.workflow.trim().length > 0
|
|
427
|
-
);
|
|
437
|
+
return typeof args.workflow === "string" && args.workflow.trim().length > 0;
|
|
428
438
|
}
|
|
429
439
|
|
|
430
440
|
function directRequestsFork(args: WorkflowToolArgs): boolean {
|
|
431
441
|
if (args.context === "fork") return true;
|
|
432
|
-
if (
|
|
442
|
+
if (
|
|
443
|
+
args.task !== undefined &&
|
|
444
|
+
typeof args.task === "object" &&
|
|
445
|
+
args.task.context === "fork"
|
|
446
|
+
)
|
|
447
|
+
return true;
|
|
433
448
|
if (args.tasks?.some((task) => task.context === "fork")) return true;
|
|
434
|
-
return
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
449
|
+
return (
|
|
450
|
+
args.chain?.some((step) =>
|
|
451
|
+
"parallel" in step
|
|
452
|
+
? step.parallel.some((task) => task.context === "fork")
|
|
453
|
+
: step.context === "fork",
|
|
454
|
+
) ?? false
|
|
455
|
+
);
|
|
439
456
|
}
|
|
440
457
|
|
|
441
|
-
function withForkParentSession(
|
|
442
|
-
|
|
458
|
+
function withForkParentSession(
|
|
459
|
+
args: WorkflowToolArgs,
|
|
460
|
+
ctx: PiExecuteContext,
|
|
461
|
+
): WorkflowToolArgs {
|
|
462
|
+
if (!directRequestsFork(args) || args.forkFromSessionFile !== undefined)
|
|
463
|
+
return args;
|
|
443
464
|
const sessionFile = ctx.sessionManager?.getSessionFile?.();
|
|
444
465
|
return typeof sessionFile === "string" && sessionFile.length > 0
|
|
445
466
|
? { ...args, forkFromSessionFile: sessionFile }
|
|
446
467
|
: args;
|
|
447
468
|
}
|
|
448
469
|
|
|
449
|
-
function workflowRunResultFromDetails(
|
|
470
|
+
function workflowRunResultFromDetails(
|
|
471
|
+
details: WorkflowDetails,
|
|
472
|
+
): WorkflowToolResult {
|
|
450
473
|
return {
|
|
451
474
|
action: "run",
|
|
452
475
|
name: `direct-${details.mode}`,
|
|
@@ -459,7 +482,10 @@ function workflowRunResultFromDetails(details: WorkflowDetails): WorkflowToolRes
|
|
|
459
482
|
};
|
|
460
483
|
}
|
|
461
484
|
|
|
462
|
-
function workflowGetResult(
|
|
485
|
+
function workflowGetResult(
|
|
486
|
+
runtime: ExtensionRuntime,
|
|
487
|
+
args: WorkflowToolArgs,
|
|
488
|
+
): WorkflowToolResult {
|
|
463
489
|
const workflow = args.workflow ?? "";
|
|
464
490
|
const def = runtime.registry.get(workflow);
|
|
465
491
|
if (!def) {
|
|
@@ -521,11 +547,16 @@ export function makeExecuteWorkflowTool(
|
|
|
521
547
|
case "inputs":
|
|
522
548
|
case "run":
|
|
523
549
|
if (action === "run" && hasDirectExecutionMode(args)) {
|
|
524
|
-
const normalModeCount =
|
|
550
|
+
const normalModeCount =
|
|
551
|
+
directModeCount(args) + (hasNamedExecutionMode(args) ? 1 : 0);
|
|
525
552
|
if (normalModeCount !== 1) {
|
|
526
|
-
throw new Error(
|
|
553
|
+
throw new Error(
|
|
554
|
+
"Workflow extension: specify exactly one normal execution mode: workflow, task, tasks, or chain",
|
|
555
|
+
);
|
|
527
556
|
}
|
|
528
|
-
const details = await activeRuntime.runDirect(
|
|
557
|
+
const details = await activeRuntime.runDirect(
|
|
558
|
+
withForkParentSession(args, ctx),
|
|
559
|
+
);
|
|
529
560
|
return workflowRunResultFromDetails(details);
|
|
530
561
|
}
|
|
531
562
|
// Delegate to registry-backed dispatcher.
|
|
@@ -638,7 +669,7 @@ export function makeExecuteWorkflowTool(
|
|
|
638
669
|
* `registerWorkflowCommand` alongside the host registration so the
|
|
639
670
|
* `on("input", …)` interceptor below can dispatch our commands directly
|
|
640
671
|
* — bypassing pi's optimistic `startPendingSubmission` flow which
|
|
641
|
-
* fires the `Working… (
|
|
672
|
+
* fires the `Working… (Esc to interrupt)` loader before the host knows
|
|
642
673
|
* the input is a synchronous picker/connect UI, not a streaming turn.
|
|
643
674
|
*
|
|
644
675
|
* See `installInputInterceptor()` for the dispatch path and rationale.
|
|
@@ -679,7 +710,7 @@ function registerWorkflowCommand(
|
|
|
679
710
|
* pi's editor `onSubmit` handler unconditionally calls
|
|
680
711
|
* `startPendingSubmission` for any text that isn't a built-in slash /
|
|
681
712
|
* skill / bash / python command — this echoes the message into chat
|
|
682
|
-
* scrollback AND starts the `Working… (
|
|
713
|
+
* scrollback AND starts the `Working… (Esc to interrupt)` loader in
|
|
683
714
|
* `statusContainer` before `session.prompt` even runs. The loader is
|
|
684
715
|
* an optimistic affordance for the agent-streaming case; for our
|
|
685
716
|
* synchronous picker/connect UIs (`/workflow connect`, `/workflow run`,
|
|
@@ -721,7 +752,8 @@ function installInputInterceptor(
|
|
|
721
752
|
// command from args; quote handling lives inside the command
|
|
722
753
|
// handler itself (`tokenizeWorkflowArgs`).
|
|
723
754
|
const firstSpace = trimmed.indexOf(" ");
|
|
724
|
-
const name =
|
|
755
|
+
const name =
|
|
756
|
+
firstSpace === -1 ? trimmed.slice(1) : trimmed.slice(1, firstSpace);
|
|
725
757
|
const handler = commands.get(name);
|
|
726
758
|
if (!handler) return undefined; // not ours — let host run its normal flow.
|
|
727
759
|
|
|
@@ -740,7 +772,6 @@ function installInputInterceptor(
|
|
|
740
772
|
});
|
|
741
773
|
}
|
|
742
774
|
|
|
743
|
-
|
|
744
775
|
/**
|
|
745
776
|
* Resolve a user-supplied run identifier (full UUID or unique prefix) to
|
|
746
777
|
* a concrete runId. The widget surfaces an 8-char prefix to keep the
|
|
@@ -777,7 +808,10 @@ function overlaySurfaceFromContext(ctx?: {
|
|
|
777
808
|
* Strip the clack-style `--yes` / `-y` confirmation skip flag from a token
|
|
778
809
|
* list. Used by `/workflow interrupt` to skip the confirmation overlay.
|
|
779
810
|
*/
|
|
780
|
-
export function stripYesFlag(tokens: string[]): {
|
|
811
|
+
export function stripYesFlag(tokens: string[]): {
|
|
812
|
+
tokens: string[];
|
|
813
|
+
yes: boolean;
|
|
814
|
+
} {
|
|
781
815
|
const yes = tokens.some((t) => t === "--yes" || t === "-y");
|
|
782
816
|
return { tokens: tokens.filter((t) => t !== "--yes" && t !== "-y"), yes };
|
|
783
817
|
}
|
|
@@ -876,7 +910,6 @@ export function parseWorkflowArgs(tokens: string[]): Record<string, unknown> {
|
|
|
876
910
|
return result;
|
|
877
911
|
}
|
|
878
912
|
|
|
879
|
-
|
|
880
913
|
// ---------------------------------------------------------------------------
|
|
881
914
|
// Persistence port builder
|
|
882
915
|
// ---------------------------------------------------------------------------
|
|
@@ -1004,7 +1037,8 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1004
1037
|
const intercomPort = {
|
|
1005
1038
|
emit:
|
|
1006
1039
|
typeof pi.events?.emit === "function"
|
|
1007
|
-
? (event: string, payload: Record<string, unknown>) =>
|
|
1040
|
+
? (event: string, payload: Record<string, unknown>) =>
|
|
1041
|
+
pi.events!.emit!(event, payload)
|
|
1008
1042
|
: undefined,
|
|
1009
1043
|
parentSession: () => intercomParentSession ?? undefined,
|
|
1010
1044
|
};
|
|
@@ -1040,11 +1074,16 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1040
1074
|
return `${String(model.provider)}/${model.id}`;
|
|
1041
1075
|
}
|
|
1042
1076
|
|
|
1043
|
-
function workflowModelCatalogFromContext(
|
|
1044
|
-
|
|
1077
|
+
function workflowModelCatalogFromContext(
|
|
1078
|
+
ctx?: PiModelContext,
|
|
1079
|
+
): WorkflowModelCatalogPort | undefined {
|
|
1080
|
+
if (ctx?.modelRegistry === undefined && ctx?.model === undefined)
|
|
1081
|
+
return undefined;
|
|
1045
1082
|
return {
|
|
1046
1083
|
listModels: async (): Promise<readonly WorkflowModelInfo[]> => {
|
|
1047
|
-
const available =
|
|
1084
|
+
const available =
|
|
1085
|
+
ctx.modelRegistry?.getAvailable() ??
|
|
1086
|
+
(ctx.model === undefined ? [] : [ctx.model]);
|
|
1048
1087
|
return available.map((model) => ({
|
|
1049
1088
|
provider: String(model.provider),
|
|
1050
1089
|
id: model.id,
|
|
@@ -1054,14 +1093,18 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1054
1093
|
},
|
|
1055
1094
|
...(ctx.model !== undefined
|
|
1056
1095
|
? {
|
|
1057
|
-
currentModel: ctx.model as NonNullable<
|
|
1096
|
+
currentModel: ctx.model as NonNullable<
|
|
1097
|
+
CreateAgentSessionOptions["model"]
|
|
1098
|
+
>,
|
|
1058
1099
|
preferredProvider: String(ctx.model.provider),
|
|
1059
1100
|
}
|
|
1060
1101
|
: {}),
|
|
1061
1102
|
};
|
|
1062
1103
|
}
|
|
1063
1104
|
|
|
1064
|
-
function runtimeWithModels(
|
|
1105
|
+
function runtimeWithModels(
|
|
1106
|
+
models: WorkflowModelCatalogPort | undefined,
|
|
1107
|
+
): ExtensionRuntime {
|
|
1065
1108
|
if (models === undefined) return runtimeProxy;
|
|
1066
1109
|
return createExtensionRuntime({
|
|
1067
1110
|
registry: runtimeRef.current.registry,
|
|
@@ -1077,7 +1120,9 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1077
1120
|
|
|
1078
1121
|
// The runtime normally does not depend on per-command UI, but model fallback
|
|
1079
1122
|
// resolution uses the live command/tool context when pi exposes modelRegistry.
|
|
1080
|
-
function runtimeForContext(
|
|
1123
|
+
function runtimeForContext(
|
|
1124
|
+
ctx?: { ui?: PiUISurface } & PiModelContext,
|
|
1125
|
+
): ExtensionRuntime {
|
|
1081
1126
|
return runtimeWithModels(workflowModelCatalogFromContext(ctx));
|
|
1082
1127
|
}
|
|
1083
1128
|
|
|
@@ -1093,57 +1138,58 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1093
1138
|
// On resolve: swap runtime ref so /workflow completions and dispatch see
|
|
1094
1139
|
// project-local, user-global, and settings-provided workflows.
|
|
1095
1140
|
// Load startup config before discovery so workflow paths and tunables are applied.
|
|
1096
|
-
const discoveryPromise = pi.disableAsyncDiscovery
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
discoveryRef.current = result;
|
|
1116
|
-
|
|
1117
|
-
// Resolve effective config (fills in all defaults) and build WorkflowRuntimeConfig.
|
|
1118
|
-
const effectiveConfig = withWorkflowDefaults(configResult.config ?? {});
|
|
1119
|
-
runtimeConfigRef.current = {
|
|
1120
|
-
maxDepth: effectiveConfig.maxDepth,
|
|
1121
|
-
defaultConcurrency: effectiveConfig.defaultConcurrency,
|
|
1122
|
-
persistRuns: effectiveConfig.persistRuns,
|
|
1123
|
-
statusFile: effectiveConfig.statusFile,
|
|
1124
|
-
resumeInFlight: effectiveConfig.resumeInFlight,
|
|
1125
|
-
};
|
|
1141
|
+
const discoveryPromise = pi.disableAsyncDiscovery
|
|
1142
|
+
? Promise.resolve()
|
|
1143
|
+
: loadWorkflowConfig().then(async (configResult) => {
|
|
1144
|
+
configLoadRef.current = configResult;
|
|
1145
|
+
|
|
1146
|
+
// Build scope-aware DiscoveryConfig: global entries → globalWorkflows (resolved
|
|
1147
|
+
// under <homeDir>/.atomic/agent), project entries → projectWorkflows (resolved under
|
|
1148
|
+
// projectRoot). Project keys override global keys. Paths pre-resolved to absolute.
|
|
1149
|
+
const { homedir } = await import("node:os");
|
|
1150
|
+
const hasGlobal = configResult.globalConfig != null;
|
|
1151
|
+
const hasProject = configResult.projectConfig != null;
|
|
1152
|
+
const discoveryConfig =
|
|
1153
|
+
hasGlobal || hasProject
|
|
1154
|
+
? toScopedDiscoveryConfig(
|
|
1155
|
+
configResult.globalConfig ?? null,
|
|
1156
|
+
configResult.projectConfig ?? null,
|
|
1157
|
+
{ projectRoot: process.cwd(), homeDir: homedir() },
|
|
1158
|
+
)
|
|
1159
|
+
: undefined;
|
|
1126
1160
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1161
|
+
const result = await discoverWorkflows({ config: discoveryConfig });
|
|
1162
|
+
discoveryRef.current = result;
|
|
1163
|
+
|
|
1164
|
+
// Resolve effective config (fills in all defaults) and build WorkflowRuntimeConfig.
|
|
1165
|
+
const effectiveConfig = withWorkflowDefaults(configResult.config ?? {});
|
|
1166
|
+
runtimeConfigRef.current = {
|
|
1167
|
+
maxDepth: effectiveConfig.maxDepth,
|
|
1168
|
+
defaultConcurrency: effectiveConfig.defaultConcurrency,
|
|
1169
|
+
persistRuns: effectiveConfig.persistRuns,
|
|
1170
|
+
statusFile: effectiveConfig.statusFile,
|
|
1171
|
+
resumeInFlight: effectiveConfig.resumeInFlight,
|
|
1172
|
+
};
|
|
1131
1173
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
runtimeRef.current = createExtensionRuntime({
|
|
1137
|
-
registry: result.registry,
|
|
1138
|
-
adapters,
|
|
1139
|
-
cancellation: cancellationRegistry,
|
|
1140
|
-
persistence: persistenceRef.current,
|
|
1141
|
-
mcp: mcpPort,
|
|
1142
|
-
intercom: intercomPort,
|
|
1143
|
-
config: runtimeConfigRef.current,
|
|
1144
|
-
});
|
|
1174
|
+
// Replace status writer with one that reflects the resolved config.
|
|
1175
|
+
// Unsubscribe the prior (no-op) writer before creating the new one.
|
|
1176
|
+
statusWriterRef.unsubscribe();
|
|
1177
|
+
statusWriterRef = createStatusWriter(store, runtimeConfigRef.current);
|
|
1145
1178
|
|
|
1146
|
-
|
|
1179
|
+
persistenceRef.current = makePersistencePort(
|
|
1180
|
+
pi,
|
|
1181
|
+
effectiveConfig.persistRuns,
|
|
1182
|
+
);
|
|
1183
|
+
runtimeRef.current = createExtensionRuntime({
|
|
1184
|
+
registry: result.registry,
|
|
1185
|
+
adapters,
|
|
1186
|
+
cancellation: cancellationRegistry,
|
|
1187
|
+
persistence: persistenceRef.current,
|
|
1188
|
+
mcp: mcpPort,
|
|
1189
|
+
intercom: intercomPort,
|
|
1190
|
+
config: runtimeConfigRef.current,
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1147
1193
|
|
|
1148
1194
|
// -------------------------------------------------------------------------
|
|
1149
1195
|
// 1. Register the `workflow` tool
|
|
@@ -1173,7 +1219,6 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1173
1219
|
renderResult: (result, opts, _theme, _context) =>
|
|
1174
1220
|
textRenderComponent(renderResult(result.details, opts)),
|
|
1175
1221
|
});
|
|
1176
|
-
|
|
1177
1222
|
}
|
|
1178
1223
|
|
|
1179
1224
|
// -------------------------------------------------------------------------
|
|
@@ -1221,7 +1266,9 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1221
1266
|
}
|
|
1222
1267
|
const confirmed = await openKillConfirm(ui, run, theme);
|
|
1223
1268
|
if (!confirmed) {
|
|
1224
|
-
print(
|
|
1269
|
+
print(
|
|
1270
|
+
`Cancelled. Run ${result.runId.slice(0, 8)} is still active.`,
|
|
1271
|
+
);
|
|
1225
1272
|
return true;
|
|
1226
1273
|
}
|
|
1227
1274
|
const killed = killRun(result.runId, {
|
|
@@ -1239,7 +1286,9 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1239
1286
|
}
|
|
1240
1287
|
const resolved = resolveRunIdPrefix(target);
|
|
1241
1288
|
if (resolved.kind === "not_found") {
|
|
1242
|
-
print(
|
|
1289
|
+
print(
|
|
1290
|
+
`Run not found: ${target}\n\n${renderSessionList(store.runs(), { theme, includeAll: true })}`,
|
|
1291
|
+
);
|
|
1243
1292
|
return true;
|
|
1244
1293
|
}
|
|
1245
1294
|
if (resolved.kind === "ambiguous") {
|
|
@@ -1251,7 +1300,9 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1251
1300
|
return true;
|
|
1252
1301
|
}
|
|
1253
1302
|
overlay.open(resolved.runId, overlaySurfaceFromContext(ctx));
|
|
1254
|
-
print(
|
|
1303
|
+
print(
|
|
1304
|
+
`Attached to ${resolved.runId.slice(0, 8)}. Press "h" or ctrl+d to hide, "q" to interrupt, esc to close.`,
|
|
1305
|
+
);
|
|
1255
1306
|
return true;
|
|
1256
1307
|
}
|
|
1257
1308
|
|
|
@@ -1287,7 +1338,11 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1287
1338
|
persistence: persistenceRef.current,
|
|
1288
1339
|
});
|
|
1289
1340
|
const killed = results.filter((r) => r.ok).length;
|
|
1290
|
-
print(
|
|
1341
|
+
print(
|
|
1342
|
+
killed > 0
|
|
1343
|
+
? `Interrupted ${killed} run(s).`
|
|
1344
|
+
: "No in-flight runs to interrupt.",
|
|
1345
|
+
);
|
|
1291
1346
|
return true;
|
|
1292
1347
|
}
|
|
1293
1348
|
const resolved = resolveRunIdPrefix(target!);
|
|
@@ -1307,7 +1362,9 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1307
1362
|
if (!yes && run && run.endedAt === undefined && ctx.ui) {
|
|
1308
1363
|
const confirmed = await openKillConfirm(ctx.ui, run, theme);
|
|
1309
1364
|
if (!confirmed) {
|
|
1310
|
-
print(
|
|
1365
|
+
print(
|
|
1366
|
+
`Cancelled. Run ${resolved.runId.slice(0, 8)} is still active.`,
|
|
1367
|
+
);
|
|
1311
1368
|
return true;
|
|
1312
1369
|
}
|
|
1313
1370
|
}
|
|
@@ -1316,7 +1373,9 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1316
1373
|
persistence: persistenceRef.current,
|
|
1317
1374
|
});
|
|
1318
1375
|
if (result.ok) {
|
|
1319
|
-
print(
|
|
1376
|
+
print(
|
|
1377
|
+
`Run ${result.runId.slice(0, 8)} interrupted (was ${result.previousStatus}).`,
|
|
1378
|
+
);
|
|
1320
1379
|
} else {
|
|
1321
1380
|
print(
|
|
1322
1381
|
result.reason === "not_found"
|
|
@@ -1334,7 +1393,9 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1334
1393
|
if (!target) {
|
|
1335
1394
|
const ui = ctx.ui;
|
|
1336
1395
|
if (!ui || typeof ui.custom !== "function") {
|
|
1337
|
-
print(
|
|
1396
|
+
print(
|
|
1397
|
+
`${renderSessionList(store.runs(), { theme, includeAll: false })}\n\nPicker requires a UI surface. Pass a runId: /workflow attach <id> [stageId]`,
|
|
1398
|
+
);
|
|
1338
1399
|
return true;
|
|
1339
1400
|
}
|
|
1340
1401
|
const picked = await openSessionPicker(ui, store, theme, "connect");
|
|
@@ -1343,7 +1404,11 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1343
1404
|
// The picker may have surfaced interrupt from the `x` shortcut.
|
|
1344
1405
|
// Forward through the existing interrupt flow for clarity.
|
|
1345
1406
|
if (picked.kind === "kill") {
|
|
1346
|
-
return handleRunControlCommand(
|
|
1407
|
+
return handleRunControlCommand(
|
|
1408
|
+
"interrupt",
|
|
1409
|
+
[picked.runId, "-y"],
|
|
1410
|
+
ctx,
|
|
1411
|
+
);
|
|
1347
1412
|
}
|
|
1348
1413
|
return true;
|
|
1349
1414
|
}
|
|
@@ -1355,7 +1420,9 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1355
1420
|
return true;
|
|
1356
1421
|
}
|
|
1357
1422
|
if (resolved.kind === "ambiguous") {
|
|
1358
|
-
print(
|
|
1423
|
+
print(
|
|
1424
|
+
`Ambiguous run prefix "${target}" matches: ${resolved.matches.map((id) => id.slice(0, 12)).join(", ")}`,
|
|
1425
|
+
);
|
|
1359
1426
|
return true;
|
|
1360
1427
|
}
|
|
1361
1428
|
runId = resolved.runId;
|
|
@@ -1364,7 +1431,8 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1364
1431
|
let stageId: string | undefined;
|
|
1365
1432
|
if (stageTarget && run) {
|
|
1366
1433
|
const exact = run.stages.find((s) => s.id === stageTarget);
|
|
1367
|
-
const prefix =
|
|
1434
|
+
const prefix =
|
|
1435
|
+
exact ?? run.stages.find((s) => s.id.startsWith(stageTarget));
|
|
1368
1436
|
const byName = prefix ?? run.stages.find((s) => s.name === stageTarget);
|
|
1369
1437
|
if (!byName) {
|
|
1370
1438
|
print(`Stage not found in run ${runId.slice(0, 8)}: ${stageTarget}`);
|
|
@@ -1408,7 +1476,9 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1408
1476
|
return true;
|
|
1409
1477
|
}
|
|
1410
1478
|
if (resolved.kind === "ambiguous") {
|
|
1411
|
-
print(
|
|
1479
|
+
print(
|
|
1480
|
+
`Ambiguous run prefix "${target}" matches: ${resolved.matches.map((id) => id.slice(0, 12)).join(", ")}`,
|
|
1481
|
+
);
|
|
1412
1482
|
return true;
|
|
1413
1483
|
}
|
|
1414
1484
|
runId = resolved.runId;
|
|
@@ -1416,7 +1486,12 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1416
1486
|
let stageId: string | undefined;
|
|
1417
1487
|
if (stageTarget) {
|
|
1418
1488
|
const run = store.runs().find((r) => r.id === runId);
|
|
1419
|
-
const stage = run?.stages.find(
|
|
1489
|
+
const stage = run?.stages.find(
|
|
1490
|
+
(s) =>
|
|
1491
|
+
s.id === stageTarget ||
|
|
1492
|
+
s.id.startsWith(stageTarget) ||
|
|
1493
|
+
s.name === stageTarget,
|
|
1494
|
+
);
|
|
1420
1495
|
if (!stage) {
|
|
1421
1496
|
print(`Stage not found in run ${runId.slice(0, 8)}: ${stageTarget}`);
|
|
1422
1497
|
return true;
|
|
@@ -1429,16 +1504,16 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1429
1504
|
result.reason === "not_found"
|
|
1430
1505
|
? `Run not found: ${runId.slice(0, 8)}`
|
|
1431
1506
|
: result.reason === "already_ended"
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1507
|
+
? `Run ${runId.slice(0, 8)} already ended.`
|
|
1508
|
+
: result.reason === "no_active_stages"
|
|
1509
|
+
? `No pausable stages on run ${runId.slice(0, 8)}.`
|
|
1510
|
+
: `Stage not found: ${stageTarget ?? "(unknown)"}`;
|
|
1436
1511
|
print(why);
|
|
1437
1512
|
return true;
|
|
1438
1513
|
}
|
|
1439
1514
|
// Open the orchestrator overlay (graph for run-level pause, stage
|
|
1440
1515
|
// chat when a stage was named). This mirrors connect/attach/resume:
|
|
1441
|
-
// the full-screen overlay hides Pi's "Working… (
|
|
1516
|
+
// the full-screen overlay hides Pi's "Working… (Esc to interrupt)"
|
|
1442
1517
|
// spinner, which otherwise stays visible because the host session
|
|
1443
1518
|
// is still streaming whatever was happening before the pause hit.
|
|
1444
1519
|
if (typeof ctx.ui?.custom === "function") {
|
|
@@ -1473,7 +1548,9 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1473
1548
|
return true;
|
|
1474
1549
|
}
|
|
1475
1550
|
if (resolved.kind === "ambiguous") {
|
|
1476
|
-
print(
|
|
1551
|
+
print(
|
|
1552
|
+
`Ambiguous run prefix "${target}" matches: ${resolved.matches.map((id) => id.slice(0, 12)).join(", ")}`,
|
|
1553
|
+
);
|
|
1477
1554
|
return true;
|
|
1478
1555
|
}
|
|
1479
1556
|
runId = resolved.runId;
|
|
@@ -1481,7 +1558,12 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1481
1558
|
let stageId: string | undefined;
|
|
1482
1559
|
const run = store.runs().find((r) => r.id === runId);
|
|
1483
1560
|
if (stageTarget) {
|
|
1484
|
-
const stage = run?.stages.find(
|
|
1561
|
+
const stage = run?.stages.find(
|
|
1562
|
+
(s) =>
|
|
1563
|
+
s.id === stageTarget ||
|
|
1564
|
+
s.id.startsWith(stageTarget) ||
|
|
1565
|
+
s.name === stageTarget,
|
|
1566
|
+
);
|
|
1485
1567
|
if (!stage) {
|
|
1486
1568
|
print(`Stage not found in run ${runId.slice(0, 8)}: ${stageTarget}`);
|
|
1487
1569
|
return true;
|
|
@@ -1489,7 +1571,8 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1489
1571
|
stageId = stage.id;
|
|
1490
1572
|
}
|
|
1491
1573
|
const isPaused =
|
|
1492
|
-
run?.status === "paused" ||
|
|
1574
|
+
run?.status === "paused" ||
|
|
1575
|
+
(run?.stages.some((s) => s.status === "paused") ?? false);
|
|
1493
1576
|
const result = resumeRun(runId, { stageId, message });
|
|
1494
1577
|
if (!result.ok) {
|
|
1495
1578
|
print(`Run not found: ${runId.slice(0, 8)}`);
|
|
@@ -1520,467 +1603,535 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1520
1603
|
return false;
|
|
1521
1604
|
}
|
|
1522
1605
|
|
|
1523
|
-
registerWorkflowCommand(
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1606
|
+
registerWorkflowCommand(
|
|
1607
|
+
pi,
|
|
1608
|
+
"workflow",
|
|
1609
|
+
{
|
|
1610
|
+
description:
|
|
1611
|
+
"Run or inspect pi workflows. Usage: /workflow <name> [key=value…] | /workflow [list|status|connect|attach|interrupt|pause|resume|inputs] [args]",
|
|
1612
|
+
handler: async (args: string, ctx: PiCommandContext) => {
|
|
1613
|
+
const print = (msg: string): void => ctx.ui.notify(msg, "info");
|
|
1614
|
+
// Quote-aware split so `prompt="map the codebase"` stays a single
|
|
1615
|
+
// token. Plain `.split(/\s+/)` would mangle quoted multi-word values
|
|
1616
|
+
// into `prompt="map`, `the`, `codebase"` — the dispatch confirm then
|
|
1617
|
+
// renders `prompt=""map"` (see ui/qa-current-render-2.png).
|
|
1618
|
+
const parts = tokenizeWorkflowArgs(args);
|
|
1619
|
+
const subcommand = parts[0] ?? "";
|
|
1620
|
+
|
|
1621
|
+
// -----------------------------------------------------------------------
|
|
1622
|
+
// connect — open the orchestrator pane (picker if no id).
|
|
1623
|
+
// attach — open the in-place attach pane on a stage (or pick run).
|
|
1624
|
+
// pause — pause a run or specific stage.
|
|
1625
|
+
// -----------------------------------------------------------------------
|
|
1626
|
+
if (subcommand === "connect") {
|
|
1627
|
+
await handleRunControlCommand("connect", parts.slice(1), ctx);
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
if (subcommand === "attach") {
|
|
1631
|
+
await handleRunControlCommand("attach", parts.slice(1), ctx);
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
if (subcommand === "pause") {
|
|
1635
|
+
await handleRunControlCommand("pause", parts.slice(1), ctx);
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1552
1638
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1639
|
+
// -----------------------------------------------------------------------
|
|
1640
|
+
// list (default when no subcommand) — render the workflow catalogue
|
|
1641
|
+
// via the same renderer used by the LLM tool path.
|
|
1642
|
+
// -----------------------------------------------------------------------
|
|
1643
|
+
if (!subcommand || subcommand === "list") {
|
|
1644
|
+
const items = runtimeProxy.registry.all().map((def) => ({
|
|
1645
|
+
name: def.normalizedName,
|
|
1646
|
+
description: def.description,
|
|
1647
|
+
inputs: Object.entries(def.inputs).map(([iname, schema]) => ({
|
|
1648
|
+
name: iname,
|
|
1649
|
+
required: schema.required === true,
|
|
1650
|
+
})),
|
|
1651
|
+
}));
|
|
1652
|
+
emitChatSurface(pi, { kind: "list", entries: items });
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1569
1655
|
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1656
|
+
// -----------------------------------------------------------------------
|
|
1657
|
+
// status — band-header rich list, or per-run detail when an id is
|
|
1658
|
+
// supplied. `/workflow status` lists everything in-flight (`--all`
|
|
1659
|
+
// includes ended runs older than an hour); `/workflow status <id>`
|
|
1660
|
+
// drills into a single run via the inspectRun detail block.
|
|
1661
|
+
// -----------------------------------------------------------------------
|
|
1662
|
+
if (subcommand === "status") {
|
|
1663
|
+
const target = parts[1];
|
|
1664
|
+
if (target && !target.startsWith("--")) {
|
|
1665
|
+
const resolved = resolveRunIdPrefix(target);
|
|
1666
|
+
if (resolved.kind === "not_found") {
|
|
1667
|
+
print(`Run not found: ${target}`);
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
if (resolved.kind === "ambiguous") {
|
|
1671
|
+
print(
|
|
1672
|
+
`Ambiguous run prefix "${target}" matches: ${resolved.matches
|
|
1673
|
+
.map((id) => id.slice(0, 12))
|
|
1674
|
+
.join(", ")}`,
|
|
1675
|
+
);
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
const inspected = inspectRun(resolved.runId);
|
|
1679
|
+
if (!inspected.ok) {
|
|
1680
|
+
print(`Run not found: ${target}`);
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
emitChatSurface(pi, { kind: "detail", detail: inspected.detail });
|
|
1595
1684
|
return;
|
|
1596
1685
|
}
|
|
1597
|
-
|
|
1686
|
+
// Mirror renderSessionList's filter: keep `--all` semantics, then
|
|
1687
|
+
// hand the already-filtered snapshot to the chat-surface renderer.
|
|
1688
|
+
const includeAll = parts.includes("--all");
|
|
1689
|
+
const rows = selectRunsForPicker(
|
|
1690
|
+
store.runs(),
|
|
1691
|
+
"",
|
|
1692
|
+
includeAll,
|
|
1693
|
+
Date.now(),
|
|
1694
|
+
);
|
|
1695
|
+
emitChatSurface(pi, { kind: "status", runs: rows.map((r) => r.run) });
|
|
1598
1696
|
return;
|
|
1599
1697
|
}
|
|
1600
|
-
// Mirror renderSessionList's filter: keep `--all` semantics, then
|
|
1601
|
-
// hand the already-filtered snapshot to the chat-surface renderer.
|
|
1602
|
-
const includeAll = parts.includes("--all");
|
|
1603
|
-
const rows = selectRunsForPicker(store.runs(), "", includeAll, Date.now());
|
|
1604
|
-
emitChatSurface(pi, { kind: "status", runs: rows.map((r) => r.run) });
|
|
1605
|
-
return;
|
|
1606
|
-
}
|
|
1607
1698
|
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1699
|
+
// -----------------------------------------------------------------------
|
|
1700
|
+
// interrupt — top-level chat fast path (no confirmation overlay).
|
|
1701
|
+
// -----------------------------------------------------------------------
|
|
1702
|
+
if (subcommand === "interrupt") {
|
|
1703
|
+
// The top-level chat command is the fast interrupt path surfaced by the
|
|
1704
|
+
// widget hint (`/workflow interrupt <id>`). The user's explicit slash
|
|
1705
|
+
// command should abort immediately, even when a confirm surface is
|
|
1706
|
+
// unavailable or would steal focus from the running workflow.
|
|
1707
|
+
const interruptArgs = parts.slice(1);
|
|
1708
|
+
const hasYes = interruptArgs.some((t) => t === "--yes" || t === "-y");
|
|
1709
|
+
await handleRunControlCommand(
|
|
1710
|
+
"interrupt",
|
|
1711
|
+
hasYes ? interruptArgs : [...interruptArgs, "-y"],
|
|
1712
|
+
ctx,
|
|
1713
|
+
);
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1625
1716
|
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1717
|
+
// -----------------------------------------------------------------------
|
|
1718
|
+
// resume — non-paused runs reopen the orchestrator pane (legacy
|
|
1719
|
+
// behaviour); paused runs resume live work through the registry.
|
|
1720
|
+
// -----------------------------------------------------------------------
|
|
1721
|
+
if (subcommand === "resume") {
|
|
1722
|
+
await handleRunControlCommand("resume", parts.slice(1), ctx);
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1634
1725
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1726
|
+
// -----------------------------------------------------------------------
|
|
1727
|
+
// inputs — pretty-printed via theme; falls back to plain in non-TTY tests.
|
|
1728
|
+
// -----------------------------------------------------------------------
|
|
1729
|
+
if (subcommand === "inputs") {
|
|
1730
|
+
const workflowName = parts[1] ?? "";
|
|
1731
|
+
if (!workflowName) {
|
|
1732
|
+
print("Usage: /workflow inputs <name>");
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
const result = await runtimeForContext(ctx).dispatch({
|
|
1736
|
+
workflow: workflowName,
|
|
1737
|
+
inputs: {},
|
|
1738
|
+
action: "inputs",
|
|
1739
|
+
});
|
|
1740
|
+
if (result.action === "inputs" && "inputs" in result) {
|
|
1741
|
+
const r = result as Extract<
|
|
1742
|
+
WorkflowToolResult,
|
|
1743
|
+
{ action: "inputs" }
|
|
1744
|
+
>;
|
|
1745
|
+
if (r.error) {
|
|
1746
|
+
const available = runtimeProxy.registry.names();
|
|
1747
|
+
print(
|
|
1748
|
+
`${r.error}\nAvailable: ${available.length > 0 ? available.join(", ") : "(none)"}`,
|
|
1749
|
+
);
|
|
1750
|
+
} else {
|
|
1751
|
+
print(
|
|
1752
|
+
renderInputsSchema(workflowName, r.inputs, {
|
|
1753
|
+
theme: deriveGraphTheme({}),
|
|
1754
|
+
}),
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1642
1758
|
return;
|
|
1643
1759
|
}
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1760
|
+
|
|
1761
|
+
// -----------------------------------------------------------------------
|
|
1762
|
+
// Workflow name dispatch — workflows always run as background tasks.
|
|
1763
|
+
// The chat editor remains usable; HIL prompts surface through the graph
|
|
1764
|
+
// viewer overlay (F2 / `/workflow connect`).
|
|
1765
|
+
// -----------------------------------------------------------------------
|
|
1766
|
+
const workflowName = subcommand;
|
|
1767
|
+
const inputTokens = parts.slice(1);
|
|
1768
|
+
|
|
1769
|
+
if (inputTokens.includes("--help")) {
|
|
1770
|
+
const helpResult = await runtimeForContext(ctx).dispatch({
|
|
1771
|
+
workflow: workflowName,
|
|
1772
|
+
inputs: {},
|
|
1773
|
+
action: "inputs",
|
|
1774
|
+
});
|
|
1775
|
+
if (helpResult.action === "inputs" && "inputs" in helpResult) {
|
|
1776
|
+
const r = helpResult as Extract<
|
|
1777
|
+
WorkflowToolResult,
|
|
1778
|
+
{ action: "inputs" }
|
|
1779
|
+
>;
|
|
1780
|
+
if (r.error) {
|
|
1781
|
+
const available = runtimeProxy.registry.names();
|
|
1782
|
+
print(
|
|
1783
|
+
`${r.error}\nAvailable: ${available.length > 0 ? available.join(", ") : "(none)"}`,
|
|
1784
|
+
);
|
|
1785
|
+
} else {
|
|
1786
|
+
print(
|
|
1787
|
+
renderInputsSchema(workflowName, r.inputs, {
|
|
1788
|
+
theme: deriveGraphTheme({}),
|
|
1789
|
+
}),
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1658
1792
|
}
|
|
1793
|
+
return;
|
|
1659
1794
|
}
|
|
1660
|
-
return;
|
|
1661
|
-
}
|
|
1662
1795
|
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1796
|
+
const inputs = parseWorkflowArgs(inputTokens);
|
|
1797
|
+
// -----------------------------------------------------------------------
|
|
1798
|
+
// Interactive argument picker.
|
|
1799
|
+
//
|
|
1800
|
+
// Triggers when:
|
|
1801
|
+
// - the workflow has at least one declared input (zero-input
|
|
1802
|
+
// workflows go straight to dispatch — there's nothing to ask),
|
|
1803
|
+
// - the user did not pass `--no-picker`,
|
|
1804
|
+
// - an interactive TUI surface is available,
|
|
1805
|
+
// - AND either no key=value was supplied or one of the required
|
|
1806
|
+
// inputs is still missing after parsing.
|
|
1807
|
+
//
|
|
1808
|
+
// The picker is seeded with whatever the user *did* type, so a
|
|
1809
|
+
// partial invocation like `/workflow gen-spec research_doc=notes.md`
|
|
1810
|
+
// pre-fills that field and focuses the next unfilled required one.
|
|
1811
|
+
// -----------------------------------------------------------------------
|
|
1812
|
+
const wantsPickerSkip = inputTokens.includes("--no-picker");
|
|
1813
|
+
let mergedInputs = inputs;
|
|
1814
|
+
// Track whether the inputs picker actually showed a UI to the user.
|
|
1815
|
+
// We use this below to mount the orchestrator overlay on dispatch
|
|
1816
|
+
// success — same UX as `/workflow connect|attach|pause|resume`,
|
|
1817
|
+
// which all cover Pi's `⠴ Working… (Esc to interrupt)` spinner
|
|
1818
|
+
// with the full-screen overlay instead of leaving it visible in
|
|
1819
|
+
// the chat while the workflow runs in the background.
|
|
1820
|
+
let pickerWasShown = false;
|
|
1821
|
+
// Prefer the sticky inline form when the host can install a custom
|
|
1822
|
+
// editor. If the host rejects that editor contract at runtime, fall
|
|
1823
|
+
// back to the supported overlay picker rather than surfacing the host
|
|
1824
|
+
// exception as a workflow command error.
|
|
1825
|
+
const canOpenPicker =
|
|
1826
|
+
!wantsPickerSkip &&
|
|
1827
|
+
(typeof ctx.ui?.setEditorComponent === "function" ||
|
|
1828
|
+
typeof ctx.ui?.custom === "function");
|
|
1829
|
+
if (canOpenPicker) {
|
|
1830
|
+
const schemaResult = await runtimeForContext(ctx).dispatch({
|
|
1831
|
+
workflow: workflowName,
|
|
1832
|
+
inputs: {},
|
|
1833
|
+
action: "inputs",
|
|
1834
|
+
});
|
|
1835
|
+
const schema =
|
|
1836
|
+
schemaResult.action === "inputs" && "inputs" in schemaResult
|
|
1837
|
+
? (schemaResult as Extract<
|
|
1838
|
+
WorkflowToolResult,
|
|
1839
|
+
{ action: "inputs" }
|
|
1840
|
+
>)
|
|
1841
|
+
: undefined;
|
|
1842
|
+
const fields = schema?.inputs ?? [];
|
|
1843
|
+
const hasFields = fields.length > 0;
|
|
1844
|
+
const missingRequired = fields.some(
|
|
1845
|
+
(f: WorkflowInputEntry) =>
|
|
1846
|
+
f.required === true &&
|
|
1847
|
+
(inputs[f.name] === undefined ||
|
|
1848
|
+
(typeof inputs[f.name] === "string" &&
|
|
1849
|
+
(inputs[f.name] as string).trim() === "")),
|
|
1850
|
+
);
|
|
1851
|
+
const noTokensAtAll = inputTokens.length === 0;
|
|
1852
|
+
if (hasFields && (noTokensAtAll || missingRequired)) {
|
|
1853
|
+
pickerWasShown = true;
|
|
1854
|
+
const pickerTheme = deriveGraphTheme({});
|
|
1855
|
+
let pickerResult =
|
|
1856
|
+
typeof ctx.ui?.setEditorComponent === "function"
|
|
1857
|
+
? await openInlineInputsForm(pi, ctx, {
|
|
1858
|
+
workflowName,
|
|
1859
|
+
fields,
|
|
1860
|
+
prefilled: inputs,
|
|
1861
|
+
theme: pickerTheme,
|
|
1862
|
+
})
|
|
1863
|
+
: { kind: "unsupported" as const };
|
|
1864
|
+
if (
|
|
1865
|
+
pickerResult.kind === "unsupported" &&
|
|
1866
|
+
typeof ctx.ui?.custom === "function"
|
|
1867
|
+
) {
|
|
1868
|
+
pickerResult = await openInputsPicker(ctx.ui, {
|
|
1869
|
+
workflowName,
|
|
1870
|
+
fields,
|
|
1871
|
+
prefilled: inputs,
|
|
1872
|
+
theme: pickerTheme,
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
if (pickerResult.kind === "cancel") {
|
|
1876
|
+
print(`Cancelled. /workflow ${workflowName} not started.`);
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
if (pickerResult.kind === "run") {
|
|
1880
|
+
mergedInputs = pickerResult.values;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1670
1884
|
|
|
1671
|
-
|
|
1672
|
-
const helpResult = await runtimeForContext(ctx).dispatch({
|
|
1885
|
+
const result = await runtimeForContext(ctx).dispatch({
|
|
1673
1886
|
workflow: workflowName,
|
|
1674
|
-
inputs:
|
|
1675
|
-
action: "
|
|
1887
|
+
inputs: mergedInputs,
|
|
1888
|
+
action: "run",
|
|
1676
1889
|
});
|
|
1677
|
-
if (
|
|
1678
|
-
const r =
|
|
1890
|
+
if (result.action === "run" && "runId" in result) {
|
|
1891
|
+
const r = result as Extract<
|
|
1679
1892
|
WorkflowToolResult,
|
|
1680
|
-
{ action: "
|
|
1893
|
+
{ action: "run"; runId: string }
|
|
1681
1894
|
>;
|
|
1682
|
-
if (r.
|
|
1895
|
+
if (r.status === "failed" && r.runId === "") {
|
|
1683
1896
|
const available = runtimeProxy.registry.names();
|
|
1684
1897
|
print(
|
|
1685
|
-
|
|
1898
|
+
`Workflow not found: ${workflowName}\nAvailable: ${available.length > 0 ? available.join(", ") : "(none)"}`,
|
|
1899
|
+
);
|
|
1900
|
+
} else if (r.status === "failed") {
|
|
1901
|
+
print(
|
|
1902
|
+
`Workflow "${workflowName}" failed: ${r.error ?? "unknown error"}`,
|
|
1686
1903
|
);
|
|
1687
1904
|
} else {
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
// -----------------------------------------------------------------------
|
|
1696
|
-
// Interactive argument picker.
|
|
1697
|
-
//
|
|
1698
|
-
// Triggers when:
|
|
1699
|
-
// - the workflow has at least one declared input (zero-input
|
|
1700
|
-
// workflows go straight to dispatch — there's nothing to ask),
|
|
1701
|
-
// - the user did not pass `--no-picker`,
|
|
1702
|
-
// - an interactive TUI surface is available,
|
|
1703
|
-
// - AND either no key=value was supplied or one of the required
|
|
1704
|
-
// inputs is still missing after parsing.
|
|
1705
|
-
//
|
|
1706
|
-
// The picker is seeded with whatever the user *did* type, so a
|
|
1707
|
-
// partial invocation like `/workflow gen-spec research_doc=notes.md`
|
|
1708
|
-
// pre-fills that field and focuses the next unfilled required one.
|
|
1709
|
-
// -----------------------------------------------------------------------
|
|
1710
|
-
const wantsPickerSkip = inputTokens.includes("--no-picker");
|
|
1711
|
-
let mergedInputs = inputs;
|
|
1712
|
-
// Track whether the inputs picker actually showed a UI to the user.
|
|
1713
|
-
// We use this below to mount the orchestrator overlay on dispatch
|
|
1714
|
-
// success — same UX as `/workflow connect|attach|pause|resume`,
|
|
1715
|
-
// which all cover Pi's `⠴ Working… (esc to interrupt)` spinner
|
|
1716
|
-
// with the full-screen overlay instead of leaving it visible in
|
|
1717
|
-
// the chat while the workflow runs in the background.
|
|
1718
|
-
let pickerWasShown = false;
|
|
1719
|
-
// Prefer the sticky inline form when the host can install a custom
|
|
1720
|
-
// editor. If the host rejects that editor contract at runtime, fall
|
|
1721
|
-
// back to the supported overlay picker rather than surfacing the host
|
|
1722
|
-
// exception as a workflow command error.
|
|
1723
|
-
const canOpenPicker =
|
|
1724
|
-
!wantsPickerSkip &&
|
|
1725
|
-
(typeof ctx.ui?.setEditorComponent === "function" ||
|
|
1726
|
-
typeof ctx.ui?.custom === "function");
|
|
1727
|
-
if (canOpenPicker) {
|
|
1728
|
-
const schemaResult = await runtimeForContext(ctx).dispatch({
|
|
1729
|
-
workflow: workflowName,
|
|
1730
|
-
inputs: {},
|
|
1731
|
-
action: "inputs",
|
|
1732
|
-
});
|
|
1733
|
-
const schema =
|
|
1734
|
-
schemaResult.action === "inputs" && "inputs" in schemaResult
|
|
1735
|
-
? (schemaResult as Extract<WorkflowToolResult, { action: "inputs" }>)
|
|
1736
|
-
: undefined;
|
|
1737
|
-
const fields = schema?.inputs ?? [];
|
|
1738
|
-
const hasFields = fields.length > 0;
|
|
1739
|
-
const missingRequired = fields.some(
|
|
1740
|
-
(f: WorkflowInputEntry) =>
|
|
1741
|
-
f.required === true &&
|
|
1742
|
-
(inputs[f.name] === undefined ||
|
|
1743
|
-
(typeof inputs[f.name] === "string" &&
|
|
1744
|
-
(inputs[f.name] as string).trim() === "")),
|
|
1745
|
-
);
|
|
1746
|
-
const noTokensAtAll = inputTokens.length === 0;
|
|
1747
|
-
if (hasFields && (noTokensAtAll || missingRequired)) {
|
|
1748
|
-
pickerWasShown = true;
|
|
1749
|
-
const pickerTheme = deriveGraphTheme({});
|
|
1750
|
-
let pickerResult =
|
|
1751
|
-
typeof ctx.ui?.setEditorComponent === "function"
|
|
1752
|
-
? await openInlineInputsForm(pi, ctx, {
|
|
1753
|
-
workflowName,
|
|
1754
|
-
fields,
|
|
1755
|
-
prefilled: inputs,
|
|
1756
|
-
theme: pickerTheme,
|
|
1757
|
-
})
|
|
1758
|
-
: { kind: "unsupported" as const };
|
|
1759
|
-
if (
|
|
1760
|
-
pickerResult.kind === "unsupported" &&
|
|
1761
|
-
typeof ctx.ui?.custom === "function"
|
|
1762
|
-
) {
|
|
1763
|
-
pickerResult = await openInputsPicker(ctx.ui, {
|
|
1905
|
+
// Always-background — the run is alive, the chat is free.
|
|
1906
|
+
// Route via emitChatSurface so the band+card chrome receives the
|
|
1907
|
+
// real chat content width via pi-tui's Component contract
|
|
1908
|
+
// (registered renderer returns `{ render(width): string[] }`),
|
|
1909
|
+
// not a `process.stdout.columns - 2` heuristic.
|
|
1910
|
+
emitChatSurface(pi, {
|
|
1911
|
+
kind: "dispatch",
|
|
1764
1912
|
workflowName,
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
theme: pickerTheme,
|
|
1913
|
+
runId: r.runId,
|
|
1914
|
+
inputs: mergedInputs,
|
|
1768
1915
|
});
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1916
|
+
// When the user reached this path via the inputs picker (i.e.
|
|
1917
|
+
// they didn't pre-supply all required args), open the
|
|
1918
|
+
// orchestrator overlay. The full-screen overlay covers the
|
|
1919
|
+
// chat statusContainer so Pi's working spinner is not left
|
|
1920
|
+
// visible behind the dispatch card. Direct invocations with
|
|
1921
|
+
// complete args remain opt-in via F2 / `/workflow connect`.
|
|
1922
|
+
if (pickerWasShown && typeof ctx.ui?.custom === "function") {
|
|
1923
|
+
overlay.open(r.runId, overlaySurfaceFromContext(ctx));
|
|
1924
|
+
}
|
|
1776
1925
|
}
|
|
1777
1926
|
}
|
|
1778
|
-
|
|
1927
|
+
return;
|
|
1928
|
+
},
|
|
1929
|
+
getArgumentCompletions: (partial: string): PiArgumentCompletionResult => {
|
|
1930
|
+
const completeToken = (
|
|
1931
|
+
argumentText: string,
|
|
1932
|
+
candidates: PiArgumentCompletion[],
|
|
1933
|
+
): PiArgumentCompletionResult => {
|
|
1934
|
+
const tokenStart = /\s$/.test(argumentText)
|
|
1935
|
+
? argumentText.length
|
|
1936
|
+
: Math.max(
|
|
1937
|
+
argumentText.lastIndexOf(" "),
|
|
1938
|
+
argumentText.lastIndexOf("\t"),
|
|
1939
|
+
) + 1;
|
|
1940
|
+
const head = argumentText.slice(0, tokenStart);
|
|
1941
|
+
const token = argumentText.slice(tokenStart);
|
|
1942
|
+
const filtered = candidates
|
|
1943
|
+
.filter((candidate) => candidate.value.startsWith(token))
|
|
1944
|
+
.map((candidate) => ({
|
|
1945
|
+
...candidate,
|
|
1946
|
+
value: `${head}${candidate.value}`,
|
|
1947
|
+
}));
|
|
1948
|
+
return filtered.length > 0 ? filtered : null;
|
|
1949
|
+
};
|
|
1779
1950
|
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
if (result.action === "run" && "runId" in result) {
|
|
1786
|
-
const r = result as Extract<
|
|
1787
|
-
WorkflowToolResult,
|
|
1788
|
-
{ action: "run"; runId: string }
|
|
1789
|
-
>;
|
|
1790
|
-
if (r.status === "failed" && r.runId === "") {
|
|
1791
|
-
const available = runtimeProxy.registry.names();
|
|
1792
|
-
print(
|
|
1793
|
-
`Workflow not found: ${workflowName}\nAvailable: ${available.length > 0 ? available.join(", ") : "(none)"}`,
|
|
1794
|
-
);
|
|
1795
|
-
} else if (r.status === "failed") {
|
|
1796
|
-
print(
|
|
1797
|
-
`Workflow "${workflowName}" failed: ${r.error ?? "unknown error"}`,
|
|
1798
|
-
);
|
|
1799
|
-
} else {
|
|
1800
|
-
// Always-background — the run is alive, the chat is free.
|
|
1801
|
-
// Route via emitChatSurface so the band+card chrome receives the
|
|
1802
|
-
// real chat content width via pi-tui's Component contract
|
|
1803
|
-
// (registered renderer returns `{ render(width): string[] }`),
|
|
1804
|
-
// not a `process.stdout.columns - 2` heuristic.
|
|
1805
|
-
emitChatSurface(pi, {
|
|
1806
|
-
kind: "dispatch",
|
|
1807
|
-
workflowName,
|
|
1808
|
-
runId: r.runId,
|
|
1809
|
-
inputs: mergedInputs,
|
|
1810
|
-
});
|
|
1811
|
-
// When the user reached this path via the inputs picker (i.e.
|
|
1812
|
-
// they didn't pre-supply all required args), open the
|
|
1813
|
-
// orchestrator overlay. The full-screen overlay covers the
|
|
1814
|
-
// chat statusContainer so Pi's working spinner is not left
|
|
1815
|
-
// visible behind the dispatch card. Direct invocations with
|
|
1816
|
-
// complete args remain opt-in via F2 / `/workflow connect`.
|
|
1817
|
-
if (pickerWasShown && typeof ctx.ui?.custom === "function") {
|
|
1818
|
-
overlay.open(r.runId, overlaySurfaceFromContext(ctx));
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
return;
|
|
1823
|
-
},
|
|
1824
|
-
getArgumentCompletions: (
|
|
1825
|
-
partial: string,
|
|
1826
|
-
): PiArgumentCompletionResult => {
|
|
1827
|
-
const completeToken = (
|
|
1828
|
-
argumentText: string,
|
|
1829
|
-
candidates: PiArgumentCompletion[],
|
|
1830
|
-
): PiArgumentCompletionResult => {
|
|
1831
|
-
const tokenStart = /\s$/.test(argumentText)
|
|
1832
|
-
? argumentText.length
|
|
1833
|
-
: Math.max(argumentText.lastIndexOf(" "), argumentText.lastIndexOf("\t")) + 1;
|
|
1834
|
-
const head = argumentText.slice(0, tokenStart);
|
|
1835
|
-
const token = argumentText.slice(tokenStart);
|
|
1836
|
-
const filtered = candidates
|
|
1837
|
-
.filter((candidate) => candidate.value.startsWith(token))
|
|
1838
|
-
.map((candidate) => ({
|
|
1839
|
-
...candidate,
|
|
1840
|
-
value: `${head}${candidate.value}`,
|
|
1951
|
+
const workflowNameItems = (): PiArgumentCompletion[] =>
|
|
1952
|
+
runtimeProxy.registry.names().map((name) => ({
|
|
1953
|
+
value: `${name} `,
|
|
1954
|
+
label: name,
|
|
1955
|
+
description: `Run workflow: ${name}`,
|
|
1841
1956
|
}));
|
|
1842
|
-
return filtered.length > 0 ? filtered : null;
|
|
1843
|
-
};
|
|
1844
1957
|
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
const runIdItems = (): PiArgumentCompletion[] =>
|
|
1853
|
-
store.runs().map((run) => ({
|
|
1854
|
-
value: `${run.id} `,
|
|
1855
|
-
label: run.id.slice(0, 8),
|
|
1856
|
-
description: `${run.name} — ${run.status}`,
|
|
1857
|
-
}));
|
|
1858
|
-
|
|
1859
|
-
const adminCompletions: PiArgumentCompletion[] = [
|
|
1860
|
-
{
|
|
1861
|
-
value: "connect ",
|
|
1862
|
-
label: "connect",
|
|
1863
|
-
description: "Attach to a run (picker if no id)",
|
|
1864
|
-
},
|
|
1865
|
-
{
|
|
1866
|
-
value: "attach ",
|
|
1867
|
-
label: "attach",
|
|
1868
|
-
description: "Open the in-place attach pane on a node",
|
|
1869
|
-
},
|
|
1870
|
-
{
|
|
1871
|
-
value: "list ",
|
|
1872
|
-
label: "list",
|
|
1873
|
-
description: "List registered workflows",
|
|
1874
|
-
},
|
|
1875
|
-
{
|
|
1876
|
-
value: "status ",
|
|
1877
|
-
label: "status",
|
|
1878
|
-
description: "List in-flight runs",
|
|
1879
|
-
},
|
|
1880
|
-
{ value: "interrupt ", label: "interrupt", description: "Interrupt a run" },
|
|
1881
|
-
{ value: "pause ", label: "pause", description: "Pause a run or stage" },
|
|
1882
|
-
{
|
|
1883
|
-
value: "resume ",
|
|
1884
|
-
label: "resume",
|
|
1885
|
-
description: "Re-open overlay for a run",
|
|
1886
|
-
},
|
|
1887
|
-
{
|
|
1888
|
-
value: "inputs ",
|
|
1889
|
-
label: "inputs",
|
|
1890
|
-
description: "Show a workflow's input schema",
|
|
1891
|
-
},
|
|
1892
|
-
];
|
|
1893
|
-
|
|
1894
|
-
const parts = partial.trim().split(/\s+/).filter(Boolean);
|
|
1895
|
-
const subcommand = parts[0] ?? "";
|
|
1896
|
-
if (!partial.includes(" ")) {
|
|
1897
|
-
return completeToken(partial, [...adminCompletions, ...workflowNameItems()]);
|
|
1898
|
-
}
|
|
1958
|
+
const runIdItems = (): PiArgumentCompletion[] =>
|
|
1959
|
+
store.runs().map((run) => ({
|
|
1960
|
+
value: `${run.id} `,
|
|
1961
|
+
label: run.id.slice(0, 8),
|
|
1962
|
+
description: `${run.name} — ${run.status}`,
|
|
1963
|
+
}));
|
|
1899
1964
|
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1965
|
+
const adminCompletions: PiArgumentCompletion[] = [
|
|
1966
|
+
{
|
|
1967
|
+
value: "connect ",
|
|
1968
|
+
label: "connect",
|
|
1969
|
+
description: "Attach to a run (picker if no id)",
|
|
1970
|
+
},
|
|
1971
|
+
{
|
|
1972
|
+
value: "attach ",
|
|
1973
|
+
label: "attach",
|
|
1974
|
+
description: "Open the in-place attach pane on a node",
|
|
1975
|
+
},
|
|
1976
|
+
{
|
|
1977
|
+
value: "list ",
|
|
1978
|
+
label: "list",
|
|
1979
|
+
description: "List registered workflows",
|
|
1980
|
+
},
|
|
1981
|
+
{
|
|
1982
|
+
value: "status ",
|
|
1983
|
+
label: "status",
|
|
1984
|
+
description: "List in-flight runs",
|
|
1985
|
+
},
|
|
1986
|
+
{
|
|
1987
|
+
value: "interrupt ",
|
|
1988
|
+
label: "interrupt",
|
|
1989
|
+
description: "Interrupt a run",
|
|
1990
|
+
},
|
|
1991
|
+
{
|
|
1992
|
+
value: "pause ",
|
|
1993
|
+
label: "pause",
|
|
1994
|
+
description: "Pause a run or stage",
|
|
1995
|
+
},
|
|
1996
|
+
{
|
|
1997
|
+
value: "resume ",
|
|
1998
|
+
label: "resume",
|
|
1999
|
+
description: "Re-open overlay for a run",
|
|
2000
|
+
},
|
|
2001
|
+
{
|
|
2002
|
+
value: "inputs ",
|
|
2003
|
+
label: "inputs",
|
|
2004
|
+
description: "Show a workflow's input schema",
|
|
2005
|
+
},
|
|
2006
|
+
];
|
|
1903
2007
|
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2008
|
+
const parts = partial.trim().split(/\s+/).filter(Boolean);
|
|
2009
|
+
const subcommand = parts[0] ?? "";
|
|
2010
|
+
if (!partial.includes(" ")) {
|
|
2011
|
+
return completeToken(partial, [
|
|
2012
|
+
...adminCompletions,
|
|
2013
|
+
...workflowNameItems(),
|
|
2014
|
+
]);
|
|
2015
|
+
}
|
|
1910
2016
|
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
2017
|
+
if (subcommand === "inputs") {
|
|
2018
|
+
return completeToken(partial, workflowNameItems());
|
|
2019
|
+
}
|
|
1914
2020
|
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
2021
|
+
if (subcommand === "status") {
|
|
2022
|
+
return completeToken(partial, [
|
|
2023
|
+
{
|
|
2024
|
+
value: "--all ",
|
|
2025
|
+
label: "--all",
|
|
2026
|
+
description: "Include recently ended runs",
|
|
2027
|
+
},
|
|
2028
|
+
...runIdItems(),
|
|
2029
|
+
]);
|
|
2030
|
+
}
|
|
1918
2031
|
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2032
|
+
if (subcommand === "connect") {
|
|
2033
|
+
return completeToken(partial, runIdItems());
|
|
2034
|
+
}
|
|
1922
2035
|
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
{ value: "--yes ", label: "--yes", description: "Skip confirmation" },
|
|
1927
|
-
{ value: "-y ", label: "-y", description: "Skip confirmation" },
|
|
1928
|
-
...runIdItems(),
|
|
1929
|
-
]);
|
|
1930
|
-
}
|
|
2036
|
+
if (subcommand === "resume") {
|
|
2037
|
+
return completeToken(partial, runIdItems());
|
|
2038
|
+
}
|
|
1931
2039
|
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
// menu as the no-space branch above. Skipping this guard would call
|
|
1936
|
-
// `registry.get("")`, which throws TypeError from normalizeWorkflowName.
|
|
1937
|
-
if (!subcommand) {
|
|
1938
|
-
return completeToken(partial, [...adminCompletions, ...workflowNameItems()]);
|
|
1939
|
-
}
|
|
2040
|
+
if (subcommand === "attach" || subcommand === "pause") {
|
|
2041
|
+
return completeToken(partial, runIdItems());
|
|
2042
|
+
}
|
|
1940
2043
|
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
value: `${inputName}=${choice} `,
|
|
1957
|
-
label: choice,
|
|
1958
|
-
description: inputName,
|
|
1959
|
-
})),
|
|
1960
|
-
);
|
|
2044
|
+
if (subcommand === "interrupt") {
|
|
2045
|
+
return completeToken(partial, [
|
|
2046
|
+
{
|
|
2047
|
+
value: "--all ",
|
|
2048
|
+
label: "--all",
|
|
2049
|
+
description: "Interrupt all in-flight runs",
|
|
2050
|
+
},
|
|
2051
|
+
{
|
|
2052
|
+
value: "--yes ",
|
|
2053
|
+
label: "--yes",
|
|
2054
|
+
description: "Skip confirmation",
|
|
2055
|
+
},
|
|
2056
|
+
{ value: "-y ", label: "-y", description: "Skip confirmation" },
|
|
2057
|
+
...runIdItems(),
|
|
2058
|
+
]);
|
|
1961
2059
|
}
|
|
1962
|
-
|
|
2060
|
+
|
|
2061
|
+
// `partial` ends with whitespace and no subcommand was typed yet
|
|
2062
|
+
// (e.g. `/workflow `). pi's autocomplete is asking what to suggest
|
|
2063
|
+
// after the trailing space; offer the same admin + workflow-name
|
|
2064
|
+
// menu as the no-space branch above. Skipping this guard would call
|
|
2065
|
+
// `registry.get("")`, which throws TypeError from normalizeWorkflowName.
|
|
2066
|
+
if (!subcommand) {
|
|
1963
2067
|
return completeToken(partial, [
|
|
1964
|
-
|
|
1965
|
-
|
|
2068
|
+
...adminCompletions,
|
|
2069
|
+
...workflowNameItems(),
|
|
1966
2070
|
]);
|
|
1967
2071
|
}
|
|
1968
|
-
return null;
|
|
1969
|
-
}
|
|
1970
2072
|
|
|
1971
|
-
|
|
1972
|
-
|
|
2073
|
+
const workflow = runtimeProxy.registry.get(subcommand);
|
|
2074
|
+
if (!workflow) return null;
|
|
2075
|
+
|
|
2076
|
+
const tokenStart = /\s$/.test(partial)
|
|
2077
|
+
? partial.length
|
|
2078
|
+
: Math.max(partial.lastIndexOf(" "), partial.lastIndexOf("\t")) + 1;
|
|
2079
|
+
const token = partial.slice(tokenStart);
|
|
2080
|
+
const equalsIndex = token.indexOf("=");
|
|
2081
|
+
if (equalsIndex > 0) {
|
|
2082
|
+
const inputName = token.slice(0, equalsIndex);
|
|
2083
|
+
const schema = workflow.inputs[inputName];
|
|
2084
|
+
if (schema?.type === "select") {
|
|
2085
|
+
return completeToken(
|
|
2086
|
+
partial,
|
|
2087
|
+
schema.choices.map((choice) => ({
|
|
2088
|
+
value: `${inputName}=${choice} `,
|
|
2089
|
+
label: choice,
|
|
2090
|
+
description: inputName,
|
|
2091
|
+
})),
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
if (schema?.type === "boolean") {
|
|
2095
|
+
return completeToken(partial, [
|
|
2096
|
+
{
|
|
2097
|
+
value: `${inputName}=true `,
|
|
2098
|
+
label: "true",
|
|
2099
|
+
description: inputName,
|
|
2100
|
+
},
|
|
2101
|
+
{
|
|
2102
|
+
value: `${inputName}=false `,
|
|
2103
|
+
label: "false",
|
|
2104
|
+
description: inputName,
|
|
2105
|
+
},
|
|
2106
|
+
]);
|
|
2107
|
+
}
|
|
2108
|
+
return null;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
const inputCompletions: PiArgumentCompletion[] = Object.entries(
|
|
2112
|
+
workflow.inputs,
|
|
2113
|
+
).map(([name, schema]) => ({
|
|
1973
2114
|
value: `${name}=`,
|
|
1974
2115
|
label: name,
|
|
1975
2116
|
description: schema.description,
|
|
1976
2117
|
}));
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2118
|
+
return completeToken(partial, [
|
|
2119
|
+
{
|
|
2120
|
+
value: "--no-picker ",
|
|
2121
|
+
label: "--no-picker",
|
|
2122
|
+
description: "Skip interactive input picker",
|
|
2123
|
+
},
|
|
2124
|
+
{
|
|
2125
|
+
value: "--help ",
|
|
2126
|
+
label: "--help",
|
|
2127
|
+
description: "Show this workflow's input schema",
|
|
2128
|
+
},
|
|
2129
|
+
...inputCompletions,
|
|
2130
|
+
]);
|
|
2131
|
+
},
|
|
1982
2132
|
},
|
|
1983
|
-
|
|
2133
|
+
workflowCommands,
|
|
2134
|
+
);
|
|
1984
2135
|
|
|
1985
2136
|
// -------------------------------------------------------------------------
|
|
1986
2137
|
// 3. Register message renderers for lifecycle events (§5.6)
|
|
@@ -2128,7 +2279,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2128
2279
|
);
|
|
2129
2280
|
|
|
2130
2281
|
// -------------------------------------------------------------------------
|
|
2131
|
-
// 7. Suppress pi's optimistic "Working… (
|
|
2282
|
+
// 7. Suppress pi's optimistic "Working… (Esc to interrupt)" loader
|
|
2132
2283
|
// for our slash commands. Workflow commands are synchronous picker /
|
|
2133
2284
|
// connect / inspect UIs, not streaming turns — the loader is noise
|
|
2134
2285
|
// that pads chrome above the picker. The `on("input")` hook fires
|