@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.
Files changed (149) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/builtin/intercom/config.ts +3 -4
  3. package/dist/builtin/intercom/index.ts +6 -6
  4. package/dist/builtin/intercom/package.json +1 -1
  5. package/dist/builtin/mcp/agent-dir.ts +11 -2
  6. package/dist/builtin/mcp/cli.js +12 -6
  7. package/dist/builtin/mcp/config.ts +31 -22
  8. package/dist/builtin/mcp/package.json +1 -1
  9. package/dist/builtin/subagents/package.json +1 -1
  10. package/dist/builtin/subagents/src/agents/agents.ts +63 -23
  11. package/dist/builtin/subagents/src/agents/skills.ts +21 -21
  12. package/dist/builtin/subagents/src/extension/index.ts +9 -8
  13. package/dist/builtin/subagents/src/runs/shared/run-history.ts +13 -10
  14. package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +3 -3
  15. package/dist/builtin/subagents/src/shared/artifacts.ts +18 -17
  16. package/dist/builtin/subagents/src/shared/types.ts +4 -4
  17. package/dist/builtin/web-access/config-paths.ts +11 -0
  18. package/dist/builtin/web-access/exa.ts +3 -2
  19. package/dist/builtin/web-access/gemini-api.ts +2 -1
  20. package/dist/builtin/web-access/gemini-search.ts +2 -1
  21. package/dist/builtin/web-access/gemini-web-config.ts +2 -1
  22. package/dist/builtin/web-access/github-extract.ts +2 -1
  23. package/dist/builtin/web-access/index.ts +11 -8
  24. package/dist/builtin/web-access/package.json +1 -1
  25. package/dist/builtin/web-access/perplexity.ts +2 -1
  26. package/dist/builtin/web-access/video-extract.ts +2 -1
  27. package/dist/builtin/web-access/youtube-extract.ts +2 -1
  28. package/dist/builtin/workflows/builtin/deep-research-codebase.ts +4 -0
  29. package/dist/builtin/workflows/builtin/open-claude-design.ts +39 -22
  30. package/dist/builtin/workflows/builtin/ralph.ts +7 -0
  31. package/dist/builtin/workflows/package.json +1 -1
  32. package/dist/builtin/workflows/skills/workflow/SKILL.md +28 -20
  33. package/dist/builtin/workflows/skills/workflow/references/design-checklist.md +8 -4
  34. package/dist/builtin/workflows/skills/workflow/references/running-workflows.md +52 -23
  35. package/dist/builtin/workflows/skills/workflow/references/sdk-authoring.md +41 -12
  36. package/dist/builtin/workflows/src/extension/config-loader.ts +13 -14
  37. package/dist/builtin/workflows/src/extension/discovery.ts +4 -6
  38. package/dist/builtin/workflows/src/extension/index.ts +675 -524
  39. package/dist/builtin/workflows/src/extension/runtime.ts +40 -16
  40. package/dist/builtin/workflows/src/extension/wiring.ts +3 -0
  41. package/dist/builtin/workflows/src/extension/workflow-schema.ts +43 -33
  42. package/dist/builtin/workflows/src/runs/foreground/executor.ts +34 -10
  43. package/dist/builtin/workflows/src/shared/types.ts +1 -5
  44. package/dist/builtin/workflows/src/tui/graph-view.ts +245 -75
  45. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +23 -0
  46. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +259 -149
  47. package/dist/builtin/workflows/src/tui/status-helpers.ts +3 -3
  48. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +99 -10
  49. package/dist/builtin/workflows/src/tui/switcher.ts +4 -5
  50. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +29 -0
  51. package/dist/cli/args.d.ts.map +1 -1
  52. package/dist/cli/args.js +11 -8
  53. package/dist/cli/args.js.map +1 -1
  54. package/dist/config.d.ts +21 -0
  55. package/dist/config.d.ts.map +1 -1
  56. package/dist/config.js +59 -4
  57. package/dist/config.js.map +1 -1
  58. package/dist/core/agent-session.d.ts +1 -1
  59. package/dist/core/agent-session.d.ts.map +1 -1
  60. package/dist/core/agent-session.js +2 -2
  61. package/dist/core/agent-session.js.map +1 -1
  62. package/dist/core/auth-storage.d.ts +3 -1
  63. package/dist/core/auth-storage.d.ts.map +1 -1
  64. package/dist/core/auth-storage.js +31 -8
  65. package/dist/core/auth-storage.js.map +1 -1
  66. package/dist/core/extensions/runner.d.ts.map +1 -1
  67. package/dist/core/extensions/runner.js +9 -0
  68. package/dist/core/extensions/runner.js.map +1 -1
  69. package/dist/core/extensions/types.d.ts +11 -0
  70. package/dist/core/extensions/types.d.ts.map +1 -1
  71. package/dist/core/extensions/types.js.map +1 -1
  72. package/dist/core/model-registry.d.ts +3 -2
  73. package/dist/core/model-registry.d.ts.map +1 -1
  74. package/dist/core/model-registry.js +25 -8
  75. package/dist/core/model-registry.js.map +1 -1
  76. package/dist/core/package-manager.d.ts +3 -0
  77. package/dist/core/package-manager.d.ts.map +1 -1
  78. package/dist/core/package-manager.js +97 -58
  79. package/dist/core/package-manager.js.map +1 -1
  80. package/dist/core/resource-loader.d.ts +1 -0
  81. package/dist/core/resource-loader.d.ts.map +1 -1
  82. package/dist/core/resource-loader.js +37 -36
  83. package/dist/core/resource-loader.js.map +1 -1
  84. package/dist/core/sdk.d.ts +5 -4
  85. package/dist/core/sdk.d.ts.map +1 -1
  86. package/dist/core/sdk.js +2 -2
  87. package/dist/core/sdk.js.map +1 -1
  88. package/dist/core/settings-manager.d.ts +7 -1
  89. package/dist/core/settings-manager.d.ts.map +1 -1
  90. package/dist/core/settings-manager.js +29 -8
  91. package/dist/core/settings-manager.js.map +1 -1
  92. package/dist/core/system-prompt.d.ts +1 -1
  93. package/dist/core/system-prompt.d.ts.map +1 -1
  94. package/dist/core/system-prompt.js.map +1 -1
  95. package/dist/core/telemetry.d.ts.map +1 -1
  96. package/dist/core/telemetry.js +2 -2
  97. package/dist/core/telemetry.js.map +1 -1
  98. package/dist/core/timings.d.ts.map +1 -1
  99. package/dist/core/timings.js +2 -2
  100. package/dist/core/timings.js.map +1 -1
  101. package/dist/core/tools/index.d.ts +1 -0
  102. package/dist/core/tools/index.d.ts.map +1 -1
  103. package/dist/core/tools/index.js +8 -0
  104. package/dist/core/tools/index.js.map +1 -1
  105. package/dist/core/tools/todos.d.ts.map +1 -1
  106. package/dist/core/tools/todos.js +3 -3
  107. package/dist/core/tools/todos.js.map +1 -1
  108. package/dist/index.d.ts +2 -2
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +2 -2
  111. package/dist/index.js.map +1 -1
  112. package/dist/main.d.ts.map +1 -1
  113. package/dist/main.js +6 -6
  114. package/dist/main.js.map +1 -1
  115. package/dist/modes/interactive/components/atomic-banner.d.ts +4 -0
  116. package/dist/modes/interactive/components/atomic-banner.d.ts.map +1 -0
  117. package/dist/modes/interactive/components/atomic-banner.js +34 -0
  118. package/dist/modes/interactive/components/atomic-banner.js.map +1 -0
  119. package/dist/modes/interactive/components/chat-message-renderer.d.ts +99 -0
  120. package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -0
  121. package/dist/modes/interactive/components/chat-message-renderer.js +450 -0
  122. package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -0
  123. package/dist/modes/interactive/components/chat-transcript.d.ts +69 -0
  124. package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -0
  125. package/dist/modes/interactive/components/chat-transcript.js +183 -0
  126. package/dist/modes/interactive/components/chat-transcript.js.map +1 -0
  127. package/dist/modes/interactive/components/footer.d.ts +16 -4
  128. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  129. package/dist/modes/interactive/components/footer.js +110 -137
  130. package/dist/modes/interactive/components/footer.js.map +1 -1
  131. package/dist/modes/interactive/components/index.d.ts +2 -0
  132. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  133. package/dist/modes/interactive/components/index.js +2 -0
  134. package/dist/modes/interactive/components/index.js.map +1 -1
  135. package/dist/modes/interactive/interactive-mode.d.ts +9 -0
  136. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  137. package/dist/modes/interactive/interactive-mode.js +192 -137
  138. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  139. package/dist/modes/interactive/theme/catppuccin-mocha.json +5 -5
  140. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  141. package/dist/modes/rpc/rpc-mode.js +11 -0
  142. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  143. package/dist/utils/tools-manager.d.ts.map +1 -1
  144. package/dist/utils/tools-manager.js +2 -2
  145. package/dist/utils/tools-manager.js.map +1 -1
  146. package/dist/utils/version-check.d.ts.map +1 -1
  147. package/dist/utils/version-check.js +2 -2
  148. package/dist/utils/version-check.js.map +1 -1
  149. 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 { RenderResultOpts, WorkflowInputEntry, WorkflowToolResult } from "./render-result.js";
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
- } from "../shared/types.js";import { buildRuntimeAdapters } from "./wiring.js";
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 & { getSessionFile?: () => string | undefined };
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?: "run" | "list" | "get" | "status" | "interrupt" | "resume" | "inputs";
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<"active_long_running" | "needs_attention" | "completed" | "failed">;
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 (args.task !== undefined && typeof args.task === "object" && args.task.context === "fork") return true;
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 args.chain?.some((step) =>
435
- "parallel" in step
436
- ? step.parallel.some((task) => task.context === "fork")
437
- : step.context === "fork",
438
- ) ?? false;
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(args: WorkflowToolArgs, ctx: PiExecuteContext): WorkflowToolArgs {
442
- if (!directRequestsFork(args) || args.forkFromSessionFile !== undefined) return args;
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(details: WorkflowDetails): WorkflowToolResult {
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(runtime: ExtensionRuntime, args: WorkflowToolArgs): WorkflowToolResult {
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 = directModeCount(args) + (hasNamedExecutionMode(args) ? 1 : 0);
550
+ const normalModeCount =
551
+ directModeCount(args) + (hasNamedExecutionMode(args) ? 1 : 0);
525
552
  if (normalModeCount !== 1) {
526
- throw new Error("Workflow extension: specify exactly one normal execution mode: workflow, task, tasks, or chain");
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(withForkParentSession(args, ctx));
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… (esc to interrupt)` loader before the host knows
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… (esc to interrupt)` loader in
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 = firstSpace === -1 ? trimmed.slice(1) : trimmed.slice(1, firstSpace);
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[]): { tokens: string[]; yes: boolean } {
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>) => pi.events!.emit!(event, payload)
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(ctx?: PiModelContext): WorkflowModelCatalogPort | undefined {
1044
- if (ctx?.modelRegistry === undefined && ctx?.model === undefined) return undefined;
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 = ctx.modelRegistry?.getAvailable() ?? (ctx.model === undefined ? [] : [ctx.model]);
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<CreateAgentSessionOptions["model"]>,
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(models: WorkflowModelCatalogPort | undefined): ExtensionRuntime {
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(ctx?: ({ ui?: PiUISurface } & PiModelContext)): ExtensionRuntime {
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 ? Promise.resolve() : loadWorkflowConfig().then(async (configResult) => {
1097
- configLoadRef.current = configResult;
1098
-
1099
- // Build scope-aware DiscoveryConfig: global entries → globalWorkflows (resolved
1100
- // under <homeDir>/.atomic/agent), project entries → projectWorkflows (resolved under
1101
- // projectRoot). Project keys override global keys. Paths pre-resolved to absolute.
1102
- const { homedir } = await import("node:os");
1103
- const hasGlobal = configResult.globalConfig != null;
1104
- const hasProject = configResult.projectConfig != null;
1105
- const discoveryConfig =
1106
- hasGlobal || hasProject
1107
- ? toScopedDiscoveryConfig(
1108
- configResult.globalConfig ?? null,
1109
- configResult.projectConfig ?? null,
1110
- { projectRoot: process.cwd(), homeDir: homedir() },
1111
- )
1112
- : undefined;
1113
-
1114
- const result = await discoverWorkflows({ config: discoveryConfig });
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
- // Replace status writer with one that reflects the resolved config.
1128
- // Unsubscribe the prior (no-op) writer before creating the new one.
1129
- statusWriterRef.unsubscribe();
1130
- statusWriterRef = createStatusWriter(store, runtimeConfigRef.current);
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
- persistenceRef.current = makePersistencePort(
1133
- pi,
1134
- effectiveConfig.persistRuns,
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(`Cancelled. Run ${result.runId.slice(0, 8)} is still active.`);
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(`Run not found: ${target}\n\n${renderSessionList(store.runs(), { theme, includeAll: true })}`);
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(`Attached to ${resolved.runId.slice(0, 8)}. Press "h" or ctrl+d to hide, "q" to interrupt, esc to close.`);
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(killed > 0 ? `Interrupted ${killed} run(s).` : "No in-flight runs to interrupt.");
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(`Cancelled. Run ${resolved.runId.slice(0, 8)} is still active.`);
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(`Run ${result.runId.slice(0, 8)} interrupted (was ${result.previousStatus}).`);
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(`${renderSessionList(store.runs(), { theme, includeAll: false })}\n\nPicker requires a UI surface. Pass a runId: /workflow attach <id> [stageId]`);
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("interrupt", [picked.runId, "-y"], ctx);
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(`Ambiguous run prefix "${target}" matches: ${resolved.matches.map((id) => id.slice(0, 12)).join(", ")}`);
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 = exact ?? run.stages.find((s) => s.id.startsWith(stageTarget));
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(`Ambiguous run prefix "${target}" matches: ${resolved.matches.map((id) => id.slice(0, 12)).join(", ")}`);
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((s) => s.id === stageTarget || s.id.startsWith(stageTarget) || s.name === stageTarget);
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
- ? `Run ${runId.slice(0, 8)} already ended.`
1433
- : result.reason === "no_active_stages"
1434
- ? `No pausable stages on run ${runId.slice(0, 8)}.`
1435
- : `Stage not found: ${stageTarget ?? "(unknown)"}`;
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… (esc to interrupt)"
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(`Ambiguous run prefix "${target}" matches: ${resolved.matches.map((id) => id.slice(0, 12)).join(", ")}`);
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((s) => s.id === stageTarget || s.id.startsWith(stageTarget) || s.name === stageTarget);
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" || (run?.stages.some((s) => s.status === "paused") ?? false);
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(pi, "workflow", {
1524
- description:
1525
- "Run or inspect pi workflows. Usage: /workflow <name> [key=value…] | /workflow [list|status|connect|attach|interrupt|pause|resume|inputs] [args]",
1526
- handler: async (args: string, ctx: PiCommandContext) => {
1527
- const print = (msg: string): void => ctx.ui.notify(msg, "info");
1528
- // Quote-aware split so `prompt="map the codebase"` stays a single
1529
- // token. Plain `.split(/\s+/)` would mangle quoted multi-word values
1530
- // into `prompt="map`, `the`, `codebase"` the dispatch confirm then
1531
- // renders `prompt=""map"` (see ui/qa-current-render-2.png).
1532
- const parts = tokenizeWorkflowArgs(args);
1533
- const subcommand = parts[0] ?? "";
1534
-
1535
- // -----------------------------------------------------------------------
1536
- // connect open the orchestrator pane (picker if no id).
1537
- // attach — open the in-place attach pane on a stage (or pick run).
1538
- // pause — pause a run or specific stage.
1539
- // -----------------------------------------------------------------------
1540
- if (subcommand === "connect") {
1541
- await handleRunControlCommand("connect", parts.slice(1), ctx);
1542
- return;
1543
- }
1544
- if (subcommand === "attach") {
1545
- await handleRunControlCommand("attach", parts.slice(1), ctx);
1546
- return;
1547
- }
1548
- if (subcommand === "pause") {
1549
- await handleRunControlCommand("pause", parts.slice(1), ctx);
1550
- return;
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
- // list (default when no subcommand) — render the workflow catalogue
1555
- // via the same renderer used by the LLM tool path.
1556
- // -----------------------------------------------------------------------
1557
- if (!subcommand || subcommand === "list") {
1558
- const items = runtimeProxy.registry.all().map((def) => ({
1559
- name: def.normalizedName,
1560
- description: def.description,
1561
- inputs: Object.entries(def.inputs).map(([iname, schema]) => ({
1562
- name: iname,
1563
- required: schema.required === true,
1564
- })),
1565
- }));
1566
- emitChatSurface(pi, { kind: "list", entries: items });
1567
- return;
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
- // status — band-header rich list, or per-run detail when an id is
1572
- // supplied. `/workflow status` lists everything in-flight (`--all`
1573
- // includes ended runs older than an hour); `/workflow status <id>`
1574
- // drills into a single run via the inspectRun detail block.
1575
- // -----------------------------------------------------------------------
1576
- if (subcommand === "status") {
1577
- const target = parts[1];
1578
- if (target && !target.startsWith("--")) {
1579
- const resolved = resolveRunIdPrefix(target);
1580
- if (resolved.kind === "not_found") {
1581
- print(`Run not found: ${target}`);
1582
- return;
1583
- }
1584
- if (resolved.kind === "ambiguous") {
1585
- print(
1586
- `Ambiguous run prefix "${target}" matches: ${resolved.matches
1587
- .map((id) => id.slice(0, 12))
1588
- .join(", ")}`,
1589
- );
1590
- return;
1591
- }
1592
- const inspected = inspectRun(resolved.runId);
1593
- if (!inspected.ok) {
1594
- print(`Run not found: ${target}`);
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
- emitChatSurface(pi, { kind: "detail", detail: inspected.detail });
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
- // interrupt — top-level chat fast path (no confirmation overlay).
1610
- // -----------------------------------------------------------------------
1611
- if (subcommand === "interrupt") {
1612
- // The top-level chat command is the fast interrupt path surfaced by the
1613
- // widget hint (`/workflow interrupt <id>`). The user's explicit slash
1614
- // command should abort immediately, even when a confirm surface is
1615
- // unavailable or would steal focus from the running workflow.
1616
- const interruptArgs = parts.slice(1);
1617
- const hasYes = interruptArgs.some((t) => t === "--yes" || t === "-y");
1618
- await handleRunControlCommand(
1619
- "interrupt",
1620
- hasYes ? interruptArgs : [...interruptArgs, "-y"],
1621
- ctx,
1622
- );
1623
- return;
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
- // resume — non-paused runs reopen the orchestrator pane (legacy
1628
- // behaviour); paused runs resume live work through the registry.
1629
- // -----------------------------------------------------------------------
1630
- if (subcommand === "resume") {
1631
- await handleRunControlCommand("resume", parts.slice(1), ctx);
1632
- return;
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
- // inputs — pretty-printed via theme; falls back to plain in non-TTY tests.
1637
- // -----------------------------------------------------------------------
1638
- if (subcommand === "inputs") {
1639
- const workflowName = parts[1] ?? "";
1640
- if (!workflowName) {
1641
- print("Usage: /workflow inputs <name>");
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
- const result = await runtimeForContext(ctx).dispatch({
1645
- workflow: workflowName,
1646
- inputs: {},
1647
- action: "inputs",
1648
- });
1649
- if (result.action === "inputs" && "inputs" in result) {
1650
- const r = result as Extract<WorkflowToolResult, { action: "inputs" }>;
1651
- if (r.error) {
1652
- const available = runtimeProxy.registry.names();
1653
- print(
1654
- `${r.error}\nAvailable: ${available.length > 0 ? available.join(", ") : "(none)"}`,
1655
- );
1656
- } else {
1657
- print(renderInputsSchema(workflowName, r.inputs, { theme: deriveGraphTheme({}) }));
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
- // Workflow name dispatch — workflows always run as background tasks.
1665
- // The chat editor remains usable; HIL prompts surface through the graph
1666
- // viewer overlay (F2 / `/workflow connect`).
1667
- // -----------------------------------------------------------------------
1668
- const workflowName = subcommand;
1669
- const inputTokens = parts.slice(1);
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
- if (inputTokens.includes("--help")) {
1672
- const helpResult = await runtimeForContext(ctx).dispatch({
1885
+ const result = await runtimeForContext(ctx).dispatch({
1673
1886
  workflow: workflowName,
1674
- inputs: {},
1675
- action: "inputs",
1887
+ inputs: mergedInputs,
1888
+ action: "run",
1676
1889
  });
1677
- if (helpResult.action === "inputs" && "inputs" in helpResult) {
1678
- const r = helpResult as Extract<
1890
+ if (result.action === "run" && "runId" in result) {
1891
+ const r = result as Extract<
1679
1892
  WorkflowToolResult,
1680
- { action: "inputs" }
1893
+ { action: "run"; runId: string }
1681
1894
  >;
1682
- if (r.error) {
1895
+ if (r.status === "failed" && r.runId === "") {
1683
1896
  const available = runtimeProxy.registry.names();
1684
1897
  print(
1685
- `${r.error}\nAvailable: ${available.length > 0 ? available.join(", ") : "(none)"}`,
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
- print(renderInputsSchema(workflowName, r.inputs, { theme: deriveGraphTheme({}) }));
1689
- }
1690
- }
1691
- return;
1692
- }
1693
-
1694
- const inputs = parseWorkflowArgs(inputTokens);
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
- fields,
1766
- prefilled: inputs,
1767
- theme: pickerTheme,
1913
+ runId: r.runId,
1914
+ inputs: mergedInputs,
1768
1915
  });
1769
- }
1770
- if (pickerResult.kind === "cancel") {
1771
- print(`Cancelled. /workflow ${workflowName} not started.`);
1772
- return;
1773
- }
1774
- if (pickerResult.kind === "run") {
1775
- mergedInputs = pickerResult.values;
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
- const result = await runtimeForContext(ctx).dispatch({
1781
- workflow: workflowName,
1782
- inputs: mergedInputs,
1783
- action: "run",
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
- const workflowNameItems = (): PiArgumentCompletion[] =>
1846
- runtimeProxy.registry.names().map((name) => ({
1847
- value: `${name} `,
1848
- label: name,
1849
- description: `Run workflow: ${name}`,
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
- if (subcommand === "inputs") {
1901
- return completeToken(partial, workflowNameItems());
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
- if (subcommand === "status") {
1905
- return completeToken(partial, [
1906
- { value: "--all ", label: "--all", description: "Include recently ended runs" },
1907
- ...runIdItems(),
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
- if (subcommand === "connect") {
1912
- return completeToken(partial, runIdItems());
1913
- }
2017
+ if (subcommand === "inputs") {
2018
+ return completeToken(partial, workflowNameItems());
2019
+ }
1914
2020
 
1915
- if (subcommand === "resume") {
1916
- return completeToken(partial, runIdItems());
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
- if (subcommand === "attach" || subcommand === "pause") {
1920
- return completeToken(partial, runIdItems());
1921
- }
2032
+ if (subcommand === "connect") {
2033
+ return completeToken(partial, runIdItems());
2034
+ }
1922
2035
 
1923
- if (subcommand === "interrupt") {
1924
- return completeToken(partial, [
1925
- { value: "--all ", label: "--all", description: "Interrupt all in-flight runs" },
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
- // `partial` ends with whitespace and no subcommand was typed yet
1933
- // (e.g. `/workflow `). pi's autocomplete is asking what to suggest
1934
- // after the trailing space; offer the same admin + workflow-name
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
- const workflow = runtimeProxy.registry.get(subcommand);
1942
- if (!workflow) return null;
1943
-
1944
- const tokenStart = /\s$/.test(partial)
1945
- ? partial.length
1946
- : Math.max(partial.lastIndexOf(" "), partial.lastIndexOf("\t")) + 1;
1947
- const token = partial.slice(tokenStart);
1948
- const equalsIndex = token.indexOf("=");
1949
- if (equalsIndex > 0) {
1950
- const inputName = token.slice(0, equalsIndex);
1951
- const schema = workflow.inputs[inputName];
1952
- if (schema?.type === "select") {
1953
- return completeToken(
1954
- partial,
1955
- schema.choices.map((choice) => ({
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
- if (schema?.type === "boolean") {
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
- { value: `${inputName}=true `, label: "true", description: inputName },
1965
- { value: `${inputName}=false `, label: "false", description: inputName },
2068
+ ...adminCompletions,
2069
+ ...workflowNameItems(),
1966
2070
  ]);
1967
2071
  }
1968
- return null;
1969
- }
1970
2072
 
1971
- const inputCompletions: PiArgumentCompletion[] = Object.entries(workflow.inputs)
1972
- .map(([name, schema]) => ({
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
- return completeToken(partial, [
1978
- { value: "--no-picker ", label: "--no-picker", description: "Skip interactive input picker" },
1979
- { value: "--help ", label: "--help", description: "Show this workflow's input schema" },
1980
- ...inputCompletions,
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
- }, workflowCommands);
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… (esc to interrupt)" loader
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