@bastani/atomic 0.5.34 → 0.6.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +329 -50
  2. package/dist/commands/cli/session.d.ts +67 -0
  3. package/dist/commands/cli/session.d.ts.map +1 -0
  4. package/dist/commands/cli/workflow-status.d.ts +63 -0
  5. package/dist/commands/cli/workflow-status.d.ts.map +1 -0
  6. package/dist/sdk/commander.d.ts +74 -0
  7. package/dist/sdk/commander.d.ts.map +1 -0
  8. package/dist/sdk/components/workflow-picker-panel.d.ts +14 -17
  9. package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -1
  10. package/dist/sdk/define-workflow.d.ts +18 -9
  11. package/dist/sdk/define-workflow.d.ts.map +1 -1
  12. package/dist/sdk/index.d.ts +4 -3
  13. package/dist/sdk/index.d.ts.map +1 -1
  14. package/dist/sdk/management-commands.d.ts +42 -0
  15. package/dist/sdk/management-commands.d.ts.map +1 -0
  16. package/dist/sdk/registry.d.ts +27 -0
  17. package/dist/sdk/registry.d.ts.map +1 -0
  18. package/dist/sdk/runtime/attached-footer.d.ts +1 -1
  19. package/dist/sdk/runtime/executor-env.d.ts +20 -0
  20. package/dist/sdk/runtime/executor-env.d.ts.map +1 -0
  21. package/dist/sdk/runtime/executor.d.ts +61 -10
  22. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  23. package/dist/sdk/types.d.ts +147 -4
  24. package/dist/sdk/types.d.ts.map +1 -1
  25. package/dist/sdk/worker-shared.d.ts +42 -0
  26. package/dist/sdk/worker-shared.d.ts.map +1 -0
  27. package/dist/sdk/workflow-cli.d.ts +103 -0
  28. package/dist/sdk/workflow-cli.d.ts.map +1 -0
  29. package/dist/sdk/workflows/builtin-registry.d.ts +113 -0
  30. package/dist/sdk/workflows/builtin-registry.d.ts.map +1 -0
  31. package/dist/sdk/workflows/index.d.ts +5 -5
  32. package/dist/sdk/workflows/index.d.ts.map +1 -1
  33. package/package.json +12 -8
  34. package/src/cli.ts +85 -144
  35. package/src/commands/cli/chat/index.ts +10 -0
  36. package/src/commands/cli/workflow-command.test.ts +279 -938
  37. package/src/commands/cli/workflow-inputs.test.ts +41 -11
  38. package/src/commands/cli/workflow-inputs.ts +47 -12
  39. package/src/commands/cli/workflow-list.test.ts +234 -0
  40. package/src/commands/cli/workflow-list.ts +0 -0
  41. package/src/commands/cli/workflow.ts +11 -798
  42. package/src/scripts/constants.ts +2 -1
  43. package/src/sdk/commander.ts +161 -0
  44. package/src/sdk/components/workflow-picker-panel.tsx +78 -258
  45. package/src/sdk/define-workflow.test.ts +104 -11
  46. package/src/sdk/define-workflow.ts +47 -11
  47. package/src/sdk/errors.test.ts +16 -0
  48. package/src/sdk/index.ts +8 -8
  49. package/src/sdk/management-commands.ts +151 -0
  50. package/src/sdk/registry.ts +132 -0
  51. package/src/sdk/runtime/attached-footer.ts +1 -1
  52. package/src/sdk/runtime/executor-env.ts +45 -0
  53. package/src/sdk/runtime/executor.test.ts +37 -0
  54. package/src/sdk/runtime/executor.ts +147 -68
  55. package/src/sdk/types.ts +169 -4
  56. package/src/sdk/worker-shared.test.ts +163 -0
  57. package/src/sdk/worker-shared.ts +155 -0
  58. package/src/sdk/workflow-cli.ts +409 -0
  59. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +1 -1
  60. package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +1 -1
  61. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +1 -1
  62. package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +1 -1
  63. package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +1 -1
  64. package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +1 -1
  65. package/src/sdk/workflows/builtin/ralph/claude/index.ts +1 -1
  66. package/src/sdk/workflows/builtin/ralph/copilot/index.ts +1 -1
  67. package/src/sdk/workflows/builtin/ralph/opencode/index.ts +1 -1
  68. package/src/sdk/workflows/builtin-registry.ts +23 -0
  69. package/src/sdk/workflows/index.ts +10 -20
  70. package/src/services/system/auth.test.ts +63 -1
  71. package/.agents/skills/workflow-creator/SKILL.md +0 -334
  72. package/.agents/skills/workflow-creator/references/agent-sessions.md +0 -888
  73. package/.agents/skills/workflow-creator/references/computation-and-validation.md +0 -201
  74. package/.agents/skills/workflow-creator/references/control-flow.md +0 -470
  75. package/.agents/skills/workflow-creator/references/discovery-and-verification.md +0 -232
  76. package/.agents/skills/workflow-creator/references/failure-modes.md +0 -903
  77. package/.agents/skills/workflow-creator/references/getting-started.md +0 -275
  78. package/.agents/skills/workflow-creator/references/running-workflows.md +0 -235
  79. package/.agents/skills/workflow-creator/references/session-config.md +0 -384
  80. package/.agents/skills/workflow-creator/references/state-and-data-flow.md +0 -357
  81. package/.agents/skills/workflow-creator/references/user-input.md +0 -234
  82. package/.agents/skills/workflow-creator/references/workflow-inputs.md +0 -272
  83. package/dist/sdk/runtime/discovery.d.ts +0 -132
  84. package/dist/sdk/runtime/discovery.d.ts.map +0 -1
  85. package/dist/sdk/runtime/executor-entry.d.ts +0 -11
  86. package/dist/sdk/runtime/executor-entry.d.ts.map +0 -1
  87. package/dist/sdk/runtime/loader.d.ts +0 -70
  88. package/dist/sdk/runtime/loader.d.ts.map +0 -1
  89. package/dist/version.d.ts +0 -2
  90. package/dist/version.d.ts.map +0 -1
  91. package/src/commands/cli/workflow.test.ts +0 -317
  92. package/src/sdk/runtime/discovery.ts +0 -368
  93. package/src/sdk/runtime/executor-entry.ts +0 -18
  94. package/src/sdk/runtime/loader.ts +0 -267
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * Lifecycle:
18
18
  *
19
- * const panel = await WorkflowPickerPanel.create({ agent, workflows });
19
+ * const panel = await WorkflowPickerPanel.create({ agent, registry });
20
20
  * const result = await panel.waitForSelection();
21
21
  * panel.destroy();
22
22
  * if (result) await executeWorkflow({ ... });
@@ -41,17 +41,13 @@ import {
41
41
  import { useState, useEffect, useMemo, useRef, useCallback, useContext, createContext, memo } from "react";
42
42
  import { useLatest } from "./hooks.ts";
43
43
  import { resolveTheme, type TerminalTheme } from "../runtime/theme.ts";
44
- import type { AgentType, WorkflowInput } from "../types.ts";
45
- import type {
46
- WorkflowWithMetadata,
47
- WorkflowMetadataStatus,
48
- } from "../runtime/discovery.ts";
44
+ import type { AgentType, WorkflowInput, WorkflowDefinition, Registry } from "../types.ts";
49
45
  import { ErrorBoundary } from "./error-boundary.tsx";
50
46
 
51
47
  // ─── Theme ──────────────────────────────────────
52
48
  // The picker uses a slightly extended palette vs. the base terminal theme:
53
49
  // an `info` (sky) hue for built-in workflows and a `mauve` hue for global
54
- // ones — the same distinctions `atomic workflow -l` already draws. The
50
+ // ones — the same distinctions `atomic workflow list` already draws. The
55
51
  // rest is sourced from {@link resolveTheme} so light/dark mode tracks the
56
52
  // orchestrator panel.
57
53
  export interface PickerTheme {
@@ -110,42 +106,23 @@ function usePickerTheme(): PickerTheme {
110
106
 
111
107
  // ─── Types ──────────────────────────────────────
112
108
 
113
- type Source = "local" | "global" | "builtin";
114
109
  type Phase = "pick" | "prompt";
115
110
 
116
111
  /** The payload the picker resolves with on successful submission. */
117
112
  export interface WorkflowPickerResult {
118
113
  /** The workflow the user committed to running. */
119
- workflow: WorkflowWithMetadata;
114
+ workflow: WorkflowDefinition;
120
115
  /** Populated form values, one per declared input (or { prompt } for free-form). */
121
116
  inputs: Record<string, string>;
122
117
  }
123
118
 
124
119
  // ─── Helpers ────────────────────────────────────
125
120
 
126
- const SOURCE_DISPLAY: Record<Source, string> = {
127
- local: "local",
128
- global: "global",
129
- builtin: "builtin",
130
- };
131
-
132
- const SOURCE_DIR: Record<Source, string> = {
133
- local: ".atomic/workflows",
134
- global: "~/.atomic/workflows",
135
- builtin: "built-in",
136
- };
137
-
138
- const SOURCE_COLOR: Record<Source, keyof PickerTheme> = {
139
- local: "success",
140
- global: "mauve",
141
- builtin: "info",
142
- };
143
-
144
- /** Higher number wins when two workflows share a name. */
145
- const SOURCE_PRECEDENCE: Record<Source, number> = {
146
- global: 0,
147
- local: 1,
148
- builtin: 2,
121
+ /** Per-agent display color in the picker list / section headers. */
122
+ const AGENT_COLOR: Record<AgentType, keyof PickerTheme> = {
123
+ claude: "warning",
124
+ copilot: "success",
125
+ opencode: "mauve",
149
126
  };
150
127
 
151
128
  /**
@@ -180,39 +157,22 @@ export function fuzzyMatch(query: string, target: string): number | null {
180
157
  // ─── List Building ──────────────────────────────
181
158
 
182
159
  interface ListEntry {
183
- workflow: WorkflowWithMetadata;
184
- section: Source;
160
+ workflow: WorkflowDefinition;
161
+ /** Agent the workflow belongs to — used for section grouping. */
162
+ section: AgentType;
185
163
  }
186
164
 
187
165
  type ListRow =
188
- | { kind: "section"; source: Source }
166
+ | { kind: "section"; agent: AgentType }
189
167
  | { kind: "entry"; entry: ListEntry };
190
168
 
191
- /**
192
- * Deduplicate workflows by name using builtin > local > global precedence.
193
- * When two workflows share a name, only the higher-precedence entry is kept.
194
- */
195
- export function deduplicateByName(
196
- workflows: WorkflowWithMetadata[],
197
- ): WorkflowWithMetadata[] {
198
- const byName = new Map<string, WorkflowWithMetadata>();
199
- for (const wf of workflows) {
200
- const existing = byName.get(wf.name);
201
- if (!existing || SOURCE_PRECEDENCE[wf.source] > SOURCE_PRECEDENCE[existing.source]) {
202
- byName.set(wf.name, wf);
203
- }
204
- }
205
- return Array.from(byName.values());
206
- }
207
-
208
169
  export function buildEntries(
209
170
  query: string,
210
- workflows: WorkflowWithMetadata[],
171
+ workflows: WorkflowDefinition[],
211
172
  ): ListEntry[] {
212
- const deduped = deduplicateByName(workflows);
213
- type Scored = { wf: WorkflowWithMetadata; score: number };
173
+ type Scored = { wf: WorkflowDefinition; score: number };
214
174
  const scored: Scored[] = [];
215
- for (const wf of deduped) {
175
+ for (const wf of workflows) {
216
176
  const nameScore = fuzzyMatch(query, wf.name);
217
177
  const descScore = fuzzyMatch(query, wf.description);
218
178
  const best =
@@ -228,11 +188,11 @@ export function buildEntries(
228
188
 
229
189
  if (query === "") {
230
190
  const rest: ListEntry[] = [];
231
- for (const source of ["local", "global", "builtin"] as Source[]) {
191
+ for (const agent of ["claude", "copilot", "opencode"] as AgentType[]) {
232
192
  const group = scored
233
- .filter((s) => s.wf.source === source)
193
+ .filter((s) => s.wf.agent === agent)
234
194
  .sort((a, b) => a.wf.name.localeCompare(b.wf.name));
235
- for (const s of group) rest.push({ workflow: s.wf, section: source });
195
+ for (const s of group) rest.push({ workflow: s.wf, section: agent });
236
196
  }
237
197
  return rest;
238
198
  }
@@ -240,7 +200,7 @@ export function buildEntries(
240
200
  scored.sort((a, b) => a.score - b.score);
241
201
  return scored.map<ListEntry>((s) => ({
242
202
  workflow: s.wf,
243
- section: s.wf.source,
203
+ section: s.wf.agent,
244
204
  }));
245
205
  }
246
206
 
@@ -250,7 +210,7 @@ export function buildRows(entries: ListEntry[], query: string): ListRow[] {
250
210
  let lastSection: string | null = null;
251
211
  for (const e of entries) {
252
212
  if (e.section !== lastSection) {
253
- rows.push({ kind: "section", source: e.section });
213
+ rows.push({ kind: "section", agent: e.section });
254
214
  lastSection = e.section;
255
215
  }
256
216
  rows.push({ kind: "entry", entry: e });
@@ -261,38 +221,6 @@ export function buildRows(entries: ListEntry[], query: string): ListRow[] {
261
221
  return rows;
262
222
  }
263
223
 
264
- // ─── Status helpers ─────────────────────────────
265
- // Non-ok entries stay visible in the picker so the user sees that a
266
- // workflow from an older SDK release (or one with a load error) still
267
- // exists on disk — the previous behaviour silently dropped them, which
268
- // made user/global workflows appear to vanish after an atomic upgrade.
269
-
270
- /** Unicode glyph that prefixes a non-ok entry and heads its preview. */
271
- const STATUS_ICON: Record<WorkflowMetadataStatus["kind"], string> = {
272
- ok: " ",
273
- incompatible: "⚠",
274
- error: "✗",
275
- };
276
-
277
- /** Compact single-word label used in list rows and the bottom hint. */
278
- const STATUS_LABEL: Record<WorkflowMetadataStatus["kind"], string> = {
279
- ok: "",
280
- incompatible: "update",
281
- error: "broken",
282
- };
283
-
284
- /** Map each status kind to its semantic palette slot. */
285
- const STATUS_COLOR: Record<WorkflowMetadataStatus["kind"], keyof PickerTheme> = {
286
- ok: "success",
287
- incompatible: "warning",
288
- error: "error",
289
- };
290
-
291
- /** Non-ok rows are inert — Enter / run commands must not transition the picker. */
292
- function isRunnable(wf: WorkflowWithMetadata): boolean {
293
- return wf.status.kind === "ok";
294
- }
295
-
296
224
  // ─── Validation ─────────────────────────────────
297
225
 
298
226
  export function isFieldValid(field: WorkflowInput, value: string): boolean {
@@ -408,20 +336,17 @@ const WorkflowList = memo(function WorkflowList({
408
336
  <box flexDirection="column">
409
337
  {rows.map((row, i) => {
410
338
  if (row.kind === "section") {
411
- const src = row.source;
339
+ const ag = row.agent;
412
340
  return (
413
341
  <box
414
- key={`section-${src}`}
342
+ key={`section-${ag}`}
415
343
  height={2}
416
344
  paddingTop={1}
417
345
  paddingLeft={2}
418
346
  >
419
347
  <text>
420
- <span fg={theme[SOURCE_COLOR[src]]}>
421
- {SOURCE_DISPLAY[src]}
422
- </span>
423
- <span fg={theme.textDim}>
424
- {" (" + SOURCE_DIR[src] + ")"}
348
+ <span fg={theme[AGENT_COLOR[ag]]}>
349
+ {ag}
425
350
  </span>
426
351
  </text>
427
352
  </box>
@@ -430,28 +355,11 @@ const WorkflowList = memo(function WorkflowList({
430
355
  const entryIdx = entryIndexByRow.get(i) ?? -1;
431
356
  const isFocused = entryIdx === focusedEntryIdx;
432
357
  const wf = row.entry.workflow;
433
- const statusKind = wf.status.kind;
434
- const runnable = statusKind === "ok";
435
- // Status indicator sits between the focus marker and the name so
436
- // every row shares a 4-character gutter — ok rows render a blank
437
- // gutter, so the list stays visually flush while non-ok rows
438
- // always occupy a fixed slot (no layout jitter when filtering).
439
- const statusIcon = STATUS_ICON[statusKind];
440
- const statusCol = theme[STATUS_COLOR[statusKind]];
441
- // Non-ok rows fade the name so the "diagnostic, not selectable"
442
- // read is immediate even when the user hasn't reached the row
443
- // yet — the eye catches the warning glyph + dim text together.
444
- const nameCol = runnable
445
- ? isFocused
446
- ? theme.text
447
- : theme.textMuted
448
- : isFocused
449
- ? theme.textMuted
450
- : theme.textDim;
358
+ const nameCol = isFocused ? theme.text : theme.textMuted;
451
359
 
452
360
  return (
453
361
  <box
454
- key={`wf-${wf.name}`}
362
+ key={`wf-${wf.agent}-${wf.name}`}
455
363
  height={1}
456
364
  flexDirection="row"
457
365
  backgroundColor={isFocused ? theme.border : "transparent"}
@@ -462,9 +370,6 @@ const WorkflowList = memo(function WorkflowList({
462
370
  <span fg={isFocused ? theme.primary : theme.textDim}>
463
371
  {isFocused ? "▸ " : " "}
464
372
  </span>
465
- <span fg={statusCol}>
466
- {statusIcon + " "}
467
- </span>
468
373
  <span fg={nameCol}>
469
374
  {wf.name}
470
375
  </span>
@@ -525,67 +430,13 @@ const ArgumentRow = memo(function ArgumentRow({
525
430
  );
526
431
  });
527
432
 
528
- /**
529
- * Diagnostic block for non-ok workflows. Replaces the description +
530
- * arguments sections in the preview pane so the user sees *why* a row
531
- * is inert and what to do about it, instead of an empty-looking panel.
532
- */
533
- const StatusDiagnostic = memo(function StatusDiagnostic({
534
- status,
535
- }: {
536
- status: Exclude<WorkflowMetadataStatus, { kind: "ok" }>;
537
- }) {
538
- const theme = usePickerTheme();
539
- const color = theme[STATUS_COLOR[status.kind]];
540
- const icon = STATUS_ICON[status.kind];
541
-
542
- // Headline + sub-copy split: the headline is terse (three words) so
543
- // it reads at a glance even on a narrow right pane; the sub-copy
544
- // explains *why* and the third paragraph tells the user what to do.
545
- const headline =
546
- status.kind === "incompatible"
547
- ? "update required"
548
- : "failed to load";
549
- const detail =
550
- status.kind === "incompatible"
551
- ? `Needs Atomic v${status.requiredVersion}. Installed: v${status.currentVersion}.`
552
- : status.message;
553
- const remediation =
554
- status.kind === "incompatible"
555
- ? "Update Atomic, or re-save the workflow against the current SDK."
556
- : "Open the workflow file and fix the error above.";
557
-
558
- return (
559
- <box flexDirection="column">
560
- <text>
561
- <span fg={color}>
562
- <strong>{icon + " " + headline}</strong>
563
- </span>
564
- </text>
565
-
566
- <box height={1} />
567
-
568
- <text>
569
- <span fg={theme.textMuted}>{detail}</span>
570
- </text>
571
-
572
- <box height={1} />
573
-
574
- <text>
575
- <span fg={theme.textDim}>{remediation}</span>
576
- </text>
577
- </box>
578
- );
579
- });
580
-
581
433
  const Preview = memo(function Preview({
582
434
  wf,
583
435
  }: {
584
- wf: WorkflowWithMetadata;
436
+ wf: WorkflowDefinition;
585
437
  }) {
586
438
  const theme = usePickerTheme();
587
439
  const args = wf.inputs;
588
- const status = wf.status;
589
440
 
590
441
  return (
591
442
  <box
@@ -603,37 +454,28 @@ const Preview = memo(function Preview({
603
454
  <box height={1} />
604
455
 
605
456
  <text>
606
- <span fg={theme[SOURCE_COLOR[wf.source]]}>
607
- {SOURCE_DISPLAY[wf.source]}
608
- </span>
609
- <span fg={theme.textDim}>
610
- {" (" + SOURCE_DIR[wf.source] + ")"}
457
+ <span fg={theme[AGENT_COLOR[wf.agent]]}>
458
+ {wf.agent}
611
459
  </span>
612
460
  </text>
613
461
 
614
462
  <box height={2} />
615
463
 
616
- {status.kind === "ok" ? (
617
- <>
618
- <text>
619
- <span fg={theme.textMuted}>
620
- {wf.description || "(no description)"}
621
- </span>
622
- </text>
464
+ <text>
465
+ <span fg={theme.textMuted}>
466
+ {wf.description || "(no description)"}
467
+ </span>
468
+ </text>
623
469
 
624
- {args.length > 0 && (
625
- <>
626
- <box height={2} />
627
- <SectionLabel label="ARGUMENTS" />
628
- <box height={1} />
629
- {args.map((f) => (
630
- <ArgumentRow key={f.name} field={f} />
631
- ))}
632
- </>
633
- )}
470
+ {args.length > 0 && (
471
+ <>
472
+ <box height={2} />
473
+ <SectionLabel label="ARGUMENTS" />
474
+ <box height={1} />
475
+ {args.map((f) => (
476
+ <ArgumentRow key={f.name} field={f} />
477
+ ))}
634
478
  </>
635
- ) : (
636
- <StatusDiagnostic status={status} />
637
479
  )}
638
480
  </box>
639
481
  );
@@ -664,16 +506,25 @@ function EmptyPreview({
664
506
  </text>
665
507
  <box height={2} />
666
508
  <text>
667
- <span fg={theme.textDim}>create a new one at</span>
509
+ <span fg={theme.textDim}>
510
+ define one with{" "}
511
+ </span>
512
+ <span fg={theme.primary}>defineWorkflow(...).for&lt;"agent"&gt;(...)</span>
513
+ <span fg={theme.textDim}>,</span>
668
514
  </text>
669
515
  <box height={1} />
670
- <box paddingLeft={2}>
671
- <text>
672
- <span fg={theme.primary}>
673
- .atomic/workflows/&lt;name&gt;/&lt;agent&gt;/index.ts
674
- </span>
675
- </text>
676
- </box>
516
+ <text>
517
+ <span fg={theme.textDim}>
518
+ then register and start it via{" "}
519
+ </span>
520
+ <span fg={theme.primary}>createRegistry().register(wf)</span>
521
+ </text>
522
+ <box height={1} />
523
+ <text>
524
+ <span fg={theme.textDim}>{"and "}</span>
525
+ <span fg={theme.primary}>createWorkflowCli(registry).run()</span>
526
+ <span fg={theme.textDim}>{" in your entrypoint"}</span>
527
+ </text>
677
528
  </box>
678
529
  );
679
530
  }
@@ -912,7 +763,7 @@ function InputPhase({
912
763
  focusedFieldIdx,
913
764
  onFieldInput,
914
765
  }: {
915
- workflow: WorkflowWithMetadata;
766
+ workflow: WorkflowDefinition;
916
767
  agent: AgentType;
917
768
  fields: readonly WorkflowInput[];
918
769
  values: Record<string, string>;
@@ -998,13 +849,6 @@ function InputPhase({
998
849
  </span>
999
850
  <span fg={theme.textDim}>{" · "}</span>
1000
851
  <span fg={theme.mauve}>{agent}</span>
1001
- <span fg={theme.textDim}>{" · "}</span>
1002
- <span fg={theme[SOURCE_COLOR[workflow.source]]}>
1003
- {SOURCE_DISPLAY[workflow.source]}
1004
- </span>
1005
- <span fg={theme.textDim}>
1006
- {" (" + SOURCE_DIR[workflow.source] + ")"}
1007
- </span>
1008
852
  </text>
1009
853
  <box height={1} />
1010
854
  <text>
@@ -1076,7 +920,7 @@ function ConfirmModal({
1076
920
  workflow,
1077
921
  agent,
1078
922
  }: {
1079
- workflow: WorkflowWithMetadata;
923
+ workflow: WorkflowDefinition;
1080
924
  agent: AgentType;
1081
925
  }) {
1082
926
  const theme = usePickerTheme();
@@ -1148,14 +992,6 @@ const PICK_HINTS: Hint[] = [
1148
992
  { key: "↵", label: "select" },
1149
993
  { key: "esc", label: "quit" },
1150
994
  ];
1151
- // Shown instead of PICK_HINTS when the focused row is non-ok — the dim
1152
- // `↵ unavailable` reads immediately as "this row is navigable but not
1153
- // runnable", which matches the muted row colour and preview diagnostic.
1154
- const PICK_HINTS_UNAVAILABLE: Hint[] = [
1155
- { key: "↑↓", label: "navigate" },
1156
- { key: "↵", label: "unavailable", dim: true },
1157
- { key: "esc", label: "quit" },
1158
- ];
1159
995
  const CONFIRM_HINTS: Hint[] = [
1160
996
  { key: "y", label: "submit" },
1161
997
  { key: "n", label: "cancel" },
@@ -1238,7 +1074,7 @@ const Statusline = memo(function Statusline({
1238
1074
  phase: Phase;
1239
1075
  confirmOpen: boolean;
1240
1076
  hints: { key: string; label: string; dim?: boolean }[];
1241
- focusedWf: WorkflowWithMetadata | undefined;
1077
+ focusedWf: WorkflowDefinition | undefined;
1242
1078
  }) {
1243
1079
  const theme = usePickerTheme();
1244
1080
  const modeLabel = confirmOpen
@@ -1268,26 +1104,9 @@ const Statusline = memo(function Statusline({
1268
1104
  {focusedWf ? (
1269
1105
  <box paddingLeft={1} paddingRight={1} alignItems="center">
1270
1106
  <text>
1271
- {focusedWf.status.kind !== "ok" ? (
1272
- <span fg={theme[STATUS_COLOR[focusedWf.status.kind]]}>
1273
- {STATUS_ICON[focusedWf.status.kind] + " "}
1274
- </span>
1275
- ) : null}
1276
- <span
1277
- fg={
1278
- focusedWf.status.kind === "ok" ? theme.text : theme.textMuted
1279
- }
1280
- >
1107
+ <span fg={theme.text}>
1281
1108
  {focusedWf.name}
1282
1109
  </span>
1283
- {focusedWf.status.kind !== "ok" ? (
1284
- <>
1285
- <span fg={theme.textDim}>{" · "}</span>
1286
- <span fg={theme[STATUS_COLOR[focusedWf.status.kind]]}>
1287
- {STATUS_LABEL[focusedWf.status.kind]}
1288
- </span>
1289
- </>
1290
- ) : null}
1291
1110
  </text>
1292
1111
  </box>
1293
1112
  ) : null}
@@ -1321,7 +1140,7 @@ interface PickerKeyboardState {
1321
1140
  entries: ListEntry[];
1322
1141
  clampedEntryIdx: number;
1323
1142
  savedEntryIdx: number;
1324
- focusedWf: WorkflowWithMetadata | undefined;
1143
+ focusedWf: WorkflowDefinition | undefined;
1325
1144
  fieldValues: Record<string, string>;
1326
1145
  isFormValid: boolean;
1327
1146
  invalidFieldIndices: number[];
@@ -1400,10 +1219,7 @@ function usePickerKeyboard(state: PickerKeyboardState): void {
1400
1219
  if (key.name === "return") {
1401
1220
  key.stopPropagation();
1402
1221
  const wf = focusedWfRef.current;
1403
- // Silently swallow Enter on incompatible / broken entries — the
1404
- // preview pane already explains the failure; advancing into the
1405
- // prompt phase would be misleading since the workflow can't run.
1406
- if (wf && isRunnable(wf)) {
1222
+ if (wf) {
1407
1223
  const initial: Record<string, string> = {};
1408
1224
  for (const f of wf.inputs) {
1409
1225
  initial[f.name] =
@@ -1494,7 +1310,7 @@ function usePickerKeyboard(state: PickerKeyboardState): void {
1494
1310
  interface PickerAppProps {
1495
1311
  theme: PickerTheme;
1496
1312
  agent: AgentType;
1497
- workflows: WorkflowWithMetadata[];
1313
+ workflows: WorkflowDefinition[];
1498
1314
  onSubmit: (result: WorkflowPickerResult) => void;
1499
1315
  onCancel: () => void;
1500
1316
  }
@@ -1570,13 +1386,10 @@ export function WorkflowPicker({
1570
1386
  setConfirmOpen,
1571
1387
  });
1572
1388
 
1573
- const focusedIsRunnable = focusedWf ? isRunnable(focusedWf) : true;
1574
1389
  const hints = confirmOpen
1575
1390
  ? CONFIRM_HINTS
1576
1391
  : phase === "pick"
1577
- ? focusedIsRunnable
1578
- ? PICK_HINTS
1579
- : PICK_HINTS_UNAVAILABLE
1392
+ ? PICK_HINTS
1580
1393
  : isFormValid
1581
1394
  ? PROMPT_HINTS_VALID
1582
1395
  : PROMPT_HINTS_INVALID;
@@ -1652,8 +1465,11 @@ export function WorkflowPicker({
1652
1465
 
1653
1466
  export interface WorkflowPickerPanelOptions {
1654
1467
  agent: AgentType;
1655
- /** Pre-loaded workflows to show. Must already be filtered to `agent`. */
1656
- workflows: WorkflowWithMetadata[];
1468
+ /**
1469
+ * Registry of compiled workflow definitions. The panel calls
1470
+ * `registry.list()` and filters to the selected `agent`.
1471
+ */
1472
+ registry: Registry<Record<string, WorkflowDefinition>>;
1657
1473
  }
1658
1474
 
1659
1475
  /**
@@ -1680,6 +1496,10 @@ export class WorkflowPickerPanel {
1680
1496
 
1681
1497
  const isDark = renderer.themeMode !== "light";
1682
1498
  const theme = buildPickerTheme(resolveTheme(renderer.themeMode), isDark);
1499
+ // Filter registry to only the workflows for the selected agent.
1500
+ const workflows = options.registry
1501
+ .list()
1502
+ .filter((wf) => wf.agent === options.agent);
1683
1503
  this.root = createRoot(renderer);
1684
1504
  this.root.render(
1685
1505
  <ErrorBoundary
@@ -1702,7 +1522,7 @@ export class WorkflowPickerPanel {
1702
1522
  <WorkflowPicker
1703
1523
  theme={theme}
1704
1524
  agent={options.agent}
1705
- workflows={options.workflows}
1525
+ workflows={workflows}
1706
1526
  onSubmit={(result) => this.handleSubmit(result)}
1707
1527
  onCancel={() => this.handleCancel()}
1708
1528
  />