@smithers-orchestrator/cli 0.20.0 → 0.20.3

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 (37) hide show
  1. package/dist/agent-detection.d.ts +20 -3
  2. package/package.json +17 -19
  3. package/src/AgentAvailability.ts +3 -0
  4. package/src/agent-detection.js +226 -14
  5. package/src/ask.js +4 -6
  6. package/src/index.js +89 -45
  7. package/src/workflow-pack.js +48 -10
  8. package/src/tui/app.jsx +0 -139
  9. package/src/tui/app.tsx +0 -5
  10. package/src/tui/components/AskModal.jsx +0 -109
  11. package/src/tui/components/AskModal.tsx +0 -3
  12. package/src/tui/components/AttentionPane.jsx +0 -112
  13. package/src/tui/components/AttentionPane.tsx +0 -6
  14. package/src/tui/components/ChatPane.jsx +0 -57
  15. package/src/tui/components/ChatPane.tsx +0 -7
  16. package/src/tui/components/CronList.jsx +0 -87
  17. package/src/tui/components/CronList.tsx +0 -5
  18. package/src/tui/components/DetailsPane.jsx +0 -96
  19. package/src/tui/components/DetailsPane.tsx +0 -7
  20. package/src/tui/components/FramesPane.jsx +0 -147
  21. package/src/tui/components/FramesPane.tsx +0 -8
  22. package/src/tui/components/LogsPane.jsx +0 -46
  23. package/src/tui/components/LogsPane.tsx +0 -6
  24. package/src/tui/components/MetricsPane.jsx +0 -108
  25. package/src/tui/components/MetricsPane.tsx +0 -5
  26. package/src/tui/components/NodeDetailView.jsx +0 -284
  27. package/src/tui/components/NodeDetailView.tsx +0 -7
  28. package/src/tui/components/NodeInspector.jsx +0 -51
  29. package/src/tui/components/NodeInspector.tsx +0 -7
  30. package/src/tui/components/RunDetailView.jsx +0 -190
  31. package/src/tui/components/RunDetailView.tsx +0 -7
  32. package/src/tui/components/RunsList.jsx +0 -184
  33. package/src/tui/components/RunsList.tsx +0 -7
  34. package/src/tui/components/SqliteBrowser.jsx +0 -131
  35. package/src/tui/components/SqliteBrowser.tsx +0 -5
  36. package/src/tui/components/WorkflowLauncher.jsx +0 -63
  37. package/src/tui/components/WorkflowLauncher.tsx +0 -3
package/src/index.js CHANGED
@@ -259,6 +259,36 @@ function parseJsonInput(raw, label, fail) {
259
259
  });
260
260
  }
261
261
  }
262
+ /**
263
+ * @param {string | undefined} raw
264
+ * @param {FailFn} fail
265
+ * @returns {Record<string, string | number | boolean> | undefined}
266
+ */
267
+ function parseAnnotations(raw, fail) {
268
+ const parsed = parseJsonInput(raw, "annotations", fail);
269
+ if (parsed === undefined)
270
+ return undefined;
271
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
272
+ return fail({
273
+ code: "INVALID_ANNOTATIONS",
274
+ message: "Run annotations must be a flat JSON object of string/number/boolean values",
275
+ exitCode: 4,
276
+ });
277
+ }
278
+ /** @type {Record<string, string | number | boolean>} */
279
+ const annotations = {};
280
+ for (const [key, value] of Object.entries(parsed)) {
281
+ if (!["string", "number", "boolean"].includes(typeof value)) {
282
+ return fail({
283
+ code: "INVALID_ANNOTATIONS",
284
+ message: `Run annotation ${key} must be a string, number, or boolean`,
285
+ exitCode: 4,
286
+ });
287
+ }
288
+ annotations[key] = /** @type {string | number | boolean} */ (value);
289
+ }
290
+ return annotations;
291
+ }
262
292
  /**
263
293
  * @param {string | undefined} status
264
294
  */
@@ -454,6 +484,41 @@ function isRunStatusTerminal(status) {
454
484
  status !== "waiting-timer" &&
455
485
  status !== "waiting-event");
456
486
  }
487
+ /**
488
+ * Fetch a docs file from smithers.sh and write it to stdout.
489
+ * Honors --json (global) by emitting `{ url, content }`.
490
+ *
491
+ * @param {{ error: Function; ok: Function; format?: string; options?: { json?: boolean } }} c
492
+ * @param {string} url
493
+ * @param {string} errorCode
494
+ */
495
+ async function printSmithersDocs(c, url, errorCode) {
496
+ let body;
497
+ try {
498
+ const res = await fetch(url);
499
+ if (!res.ok) {
500
+ return c.error({
501
+ code: errorCode,
502
+ message: `Failed to fetch ${url}: HTTP ${res.status}`,
503
+ exitCode: 1,
504
+ });
505
+ }
506
+ body = await res.text();
507
+ }
508
+ catch (err) {
509
+ return c.error({
510
+ code: errorCode,
511
+ message: `Failed to fetch ${url}: ${err?.message ?? String(err)}`,
512
+ exitCode: 1,
513
+ });
514
+ }
515
+ const wantsJson = Boolean(c.options?.json) || c.format === "json";
516
+ if (wantsJson) {
517
+ return c.ok({ url, content: body });
518
+ }
519
+ process.stdout.write(body.endsWith("\n") ? body : `${body}\n`);
520
+ return c.ok(undefined);
521
+ }
457
522
  /**
458
523
  * @param {string | undefined} format
459
524
  * @param {unknown} payload
@@ -1225,6 +1290,7 @@ const upOptions = z.object({
1225
1290
  toolTimeoutMs: z.number().int().min(1).optional().describe("Max wall-clock time per tool call in ms"),
1226
1291
  hot: z.boolean().default(false).describe("Enable hot module replacement for .tsx workflows"),
1227
1292
  input: z.string().optional().describe("Input data as JSON string"),
1293
+ annotations: z.string().optional().describe("Run annotations as a flat JSON object of string/number/boolean values"),
1228
1294
  resume: z.union([z.boolean(), z.string()]).default(false).describe("Resume a previous run. Pass true with --run-id, or pass the run ID directly (e.g. --resume <run-id>)"),
1229
1295
  force: z.boolean().default(false).describe("Resume even if still marked running"),
1230
1296
  resumeClaimOwner: z.string().optional().describe("Internal durable resume claim owner"),
@@ -1450,6 +1516,7 @@ async function executeUpCommand(c, workflowPath, options, fail) {
1450
1516
  try {
1451
1517
  const resolvedWorkflowPath = resolve(process.cwd(), workflowPath);
1452
1518
  const input = parseJsonInput(options.input, "input", fail) ?? {};
1519
+ const annotations = parseAnnotations(options.annotations, fail);
1453
1520
  const { resume, resumeRunId } = normalizeResumeOption(options.resume);
1454
1521
  const runId = options.runId ?? resumeRunId;
1455
1522
  // Detached mode: spawn ourselves as a background process
@@ -1460,6 +1527,8 @@ async function executeUpCommand(c, workflowPath, options, fail) {
1460
1527
  childArgs.push("--run-id", runId);
1461
1528
  if (options.input)
1462
1529
  childArgs.push("--input", options.input);
1530
+ if (options.annotations)
1531
+ childArgs.push("--annotations", options.annotations);
1463
1532
  if (options.maxConcurrency)
1464
1533
  childArgs.push("--max-concurrency", String(options.maxConcurrency));
1465
1534
  if (options.root)
@@ -1647,6 +1716,7 @@ async function executeUpCommand(c, workflowPath, options, fail) {
1647
1716
  maxOutputBytes: options.maxOutputBytes,
1648
1717
  toolTimeoutMs: options.toolTimeoutMs,
1649
1718
  hot: options.hot,
1719
+ annotations,
1650
1720
  onProgress,
1651
1721
  signal: abort.signal,
1652
1722
  }));
@@ -1699,6 +1769,7 @@ async function executeUpCommand(c, workflowPath, options, fail) {
1699
1769
  maxOutputBytes: options.maxOutputBytes,
1700
1770
  toolTimeoutMs: options.toolTimeoutMs,
1701
1771
  hot: options.hot,
1772
+ annotations,
1702
1773
  onProgress,
1703
1774
  signal: abort.signal,
1704
1775
  }));
@@ -1819,7 +1890,7 @@ const workflowCli = Cli.create({
1819
1890
  path: resolve(workflowRoot, "bunfig.toml"),
1820
1891
  exists: existsSync(resolve(workflowRoot, "bunfig.toml")),
1821
1892
  },
1822
- agents: detectAvailableAgents(),
1893
+ agents: detectAvailableAgents(process.env, { cwd: process.cwd() }),
1823
1894
  });
1824
1895
  },
1825
1896
  });
@@ -2515,8 +2586,8 @@ const cli = Cli.create({
2515
2586
  { command: "workflow run implement", description: "Run the implementation workflow" },
2516
2587
  ]
2517
2588
  : [
2518
- { command: "tui", description: "Open the interactive dashboard" },
2519
2589
  { command: "workflow list", description: "View all available workflows" },
2590
+ { command: "workflow run implement", description: "Run the implementation workflow" },
2520
2591
  ],
2521
2592
  },
2522
2593
  });
@@ -2605,49 +2676,6 @@ const cli = Cli.create({
2605
2676
  cleanup();
2606
2677
  }
2607
2678
  },
2608
- })
2609
- // =========================================================================
2610
- // smithers tui
2611
- // =========================================================================
2612
- .command("tui", {
2613
- description: "Open the interactive Smithers observability dashboard",
2614
- async run(c) {
2615
- const fail = (opts) => {
2616
- commandExitOverride = opts.exitCode ?? 1;
2617
- return c.error(opts);
2618
- };
2619
- let cleanup;
2620
- let renderer;
2621
- try {
2622
- const db = await findAndOpenDb(undefined, {
2623
- timeoutMs: 5000,
2624
- intervalMs: 100,
2625
- });
2626
- const adapter = db.adapter;
2627
- cleanup = db.cleanup;
2628
- const { createCliRenderer } = await import("@opentui/core");
2629
- const { createRoot } = await import("@opentui/react");
2630
- const { TuiApp } = await import("./tui/app.jsx");
2631
- const React = await import("react");
2632
- renderer = await createCliRenderer({ exitOnCtrlC: false });
2633
- const root = createRoot(renderer);
2634
- await new Promise((resolve) => {
2635
- root.render(React.createElement(TuiApp, {
2636
- adapter,
2637
- onExit: () => resolve(true),
2638
- }));
2639
- });
2640
- return c.ok(undefined);
2641
- }
2642
- catch (err) {
2643
- return fail({ code: "TUI_FAILED", message: err?.message ?? String(err), exitCode: 1 });
2644
- }
2645
- finally {
2646
- if (renderer)
2647
- renderer.destroy();
2648
- cleanup?.();
2649
- }
2650
- }
2651
2679
  })
2652
2680
  // =========================================================================
2653
2681
  // smithers ps
@@ -4930,6 +4958,22 @@ const cli = Cli.create({
4930
4958
  return c.error({ code: "GUI_LAUNCH_FAILED", message: err?.message ?? String(err), exitCode: 1 });
4931
4959
  }
4932
4960
  },
4961
+ })
4962
+ // =========================================================================
4963
+ // smithers docs / smithers docs-full
4964
+ // Print the published llms.txt / llms-full.txt from smithers.sh.
4965
+ // =========================================================================
4966
+ .command("docs", {
4967
+ description: "Print llms.txt (concise docs index for LLMs) from smithers.sh.",
4968
+ async run(c) {
4969
+ return printSmithersDocs(c, "https://smithers.sh/llms.txt", "DOCS_FETCH_FAILED");
4970
+ },
4971
+ })
4972
+ .command("docs-full", {
4973
+ description: "Print llms-full.txt (full docs bundle for LLMs) from smithers.sh.",
4974
+ async run(c) {
4975
+ return printSmithersDocs(c, "https://smithers.sh/llms-full.txt", "DOCS_FULL_FETCH_FAILED");
4976
+ },
4933
4977
  })
4934
4978
  .command(workflowCli)
4935
4979
  .command(cronCli)
@@ -133,7 +133,6 @@ function renderPackageJson(versions) {
133
133
  dependencies: {
134
134
  react: versions.reactVersion,
135
135
  "react-dom": versions.reactDomVersion,
136
- skills: "github:mattpocock/skills",
137
136
  "smithers-orchestrator": smithersSpec,
138
137
  zod: versions.zodVersion,
139
138
  },
@@ -413,6 +412,17 @@ function renderPrompts() {
413
412
  "",
414
413
  ].join("\n"),
415
414
  },
415
+ {
416
+ path: ".smithers/prompts/grill-me.mdx",
417
+ contents: [
418
+ "Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.",
419
+ "",
420
+ "Ask the questions one at a time.",
421
+ "",
422
+ "If a question can be answered by exploring the codebase, explore the codebase instead.",
423
+ "",
424
+ ].join("\n"),
425
+ },
416
426
  {
417
427
  path: ".smithers/prompts/write-a-prd.mdx",
418
428
  contents: [
@@ -1185,7 +1195,7 @@ function renderComponents() {
1185
1195
  "/** @jsxImportSource smithers-orchestrator */",
1186
1196
  'import { Loop, Sequence, Task, type AgentLike, type OutputTarget } from "smithers-orchestrator";',
1187
1197
  'import { z } from "zod/v4";',
1188
- 'import GrillMeSkill from "skills/grill-me/SKILL.md";',
1198
+ 'import GrillMeSkill from "../prompts/grill-me.mdx";',
1189
1199
  'import AskUserInstructions from "../prompts/ask-user-instructions.mdx";',
1190
1200
  "",
1191
1201
  "export const grillOutputSchema = z.looseObject({",
@@ -3261,13 +3271,42 @@ function renderWorkflows() {
3261
3271
  /**
3262
3272
  * @param {DependencyVersions} versions
3263
3273
  * @param {NodeJS.ProcessEnv} env
3274
+ * @param {string} projectRoot
3264
3275
  * @returns {TemplateFile[]}
3265
3276
  */
3266
- function renderTemplateFiles(versions, env) {
3277
+ function renderTemplateFiles(versions, env, projectRoot) {
3267
3278
  return [
3268
3279
  {
3269
3280
  path: ".smithers/.gitignore",
3270
- contents: ["node_modules/", "executions/", "runs/", "sandboxes/", "state/", "tmp/", "*.db", "*.sqlite", "dist/", ".DS_Store", ""].join("\n"),
3281
+ contents: [
3282
+ "# Ephemeral data (never commit)",
3283
+ "node_modules/",
3284
+ "executions/",
3285
+ "runs/",
3286
+ "sandboxes/",
3287
+ "state/",
3288
+ "tmp/",
3289
+ "*.db",
3290
+ "*.sqlite",
3291
+ "*.db-shm",
3292
+ "*.db-wal",
3293
+ "dist/",
3294
+ ".DS_Store",
3295
+ "",
3296
+ "# Log files",
3297
+ "*.log",
3298
+ "logs/",
3299
+ ""
3300
+ ].join("\n"),
3301
+ },
3302
+ {
3303
+ path: ".smithers/workflows/.gitignore",
3304
+ contents: [
3305
+ "# Ignore log files in workflows",
3306
+ "*.log",
3307
+ "run-*.log",
3308
+ ""
3309
+ ].join("\n"),
3271
3310
  },
3272
3311
  {
3273
3312
  path: ".smithers/package.json",
@@ -3304,7 +3343,7 @@ function renderTemplateFiles(versions, env) {
3304
3343
  ...renderAgentScaffoldFiles(),
3305
3344
  {
3306
3345
  path: ".smithers/agents.ts",
3307
- contents: generateAgentsTs(env),
3346
+ contents: generateAgentsTs(env, { cwd: projectRoot }),
3308
3347
  },
3309
3348
  {
3310
3349
  path: ".smithers/smithers.config.ts",
@@ -3361,7 +3400,7 @@ export function initWorkflowPack(options = {}) {
3361
3400
  else {
3362
3401
  ensureDir(executionsDir);
3363
3402
  }
3364
- templateFiles = renderTemplateFiles(versions, env);
3403
+ templateFiles = renderTemplateFiles(versions, env, projectRoot);
3365
3404
  }
3366
3405
  for (const file of templateFiles) {
3367
3406
  const absolutePath = resolve(projectRoot, file.path);
@@ -3388,10 +3427,9 @@ export function initWorkflowPack(options = {}) {
3388
3427
  };
3389
3428
  }
3390
3429
  /**
3391
- * Install `.smithers/` workspace deps so git-specifier packages like
3392
- * `github:mattpocock/skills` which Bun's runtime auto-install doesn't fetch
3393
- * are available the first time a workflow runs. Failures here don't fail init:
3394
- * the scaffold is on disk, the user can always re-run `bun install` by hand.
3430
+ * Install `.smithers/` workspace deps so the first workflow run isn't blocked
3431
+ * on a cold install. Failures here don't fail init: the scaffold is on disk,
3432
+ * the user can always re-run `bun install` by hand.
3395
3433
  *
3396
3434
  * @param {string} rootDir
3397
3435
  * @param {boolean} skip
package/src/tui/app.jsx DELETED
@@ -1,139 +0,0 @@
1
- // @ts-nocheck
2
- import React, { useEffect, useState } from "react";
3
- import { useKeyboard } from "@opentui/react";
4
- import { RunsList } from "./components/RunsList.jsx";
5
- import { WorkflowLauncher } from "./components/WorkflowLauncher.jsx";
6
- import { RunDetailView } from "./components/RunDetailView.jsx";
7
- import { NodeDetailView } from "./components/NodeDetailView.jsx";
8
- import { AskModal } from "./components/AskModal.jsx";
9
- import { SqliteBrowser } from "./components/SqliteBrowser.jsx";
10
- import { CronList } from "./components/CronList.jsx";
11
- import { MetricsPane } from "./components/MetricsPane.jsx";
12
- /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
13
-
14
- const TABS = [
15
- { id: "runs", label: "Runs", access: "r" },
16
- { id: "ask", label: "Agent Console", access: "a" },
17
- { id: "crons", label: "Triggers", access: "t" },
18
- { id: "metrics", label: "Telemetry", access: "m" },
19
- { id: "sqlite", label: "Data Grid", access: "s" },
20
- ];
21
- /**
22
- * @param {{ adapter: SmithersDb; onExit: () => void; }} value
23
- */
24
- export function TuiApp({ adapter, onExit, }) {
25
- const [view, setView] = useState("runs");
26
- const [selectedRunId, setSelectedRunId] = useState(null);
27
- const [selectedNodeId, setSelectedNodeId] = useState(null);
28
- const activeTabId = (view === "detail" || view === "node" || view === "launcher") ? "runs" : view;
29
- useKeyboard(async (key) => {
30
- // Tab switching
31
- if (key.name === "left" || key.name === "right") {
32
- // only accept left/right at the root view so we don't clobber text inputs
33
- if (view === "runs" || view === "crons" || view === "metrics") {
34
- const currentIndex = TABS.findIndex((t) => t.id === activeTabId);
35
- if (key.name === "right") {
36
- const next = TABS[(currentIndex + 1) % TABS.length];
37
- setView(next.id);
38
- }
39
- else {
40
- const prev = TABS[(currentIndex - 1 + TABS.length) % TABS.length];
41
- setView(prev.id);
42
- }
43
- return;
44
- }
45
- }
46
- if (key.name === "s" && view !== "sqlite" && view !== "ask") {
47
- setView("sqlite");
48
- return;
49
- }
50
- if (key.name === "t" && view !== "crons" && view !== "ask") {
51
- setView("crons");
52
- return;
53
- }
54
- if (key.name === "m" && view !== "metrics" && view !== "ask") {
55
- setView("metrics");
56
- return;
57
- }
58
- if (view === "runs") {
59
- if (key.name === "escape" || (key.name === "c" && key.ctrl)) {
60
- onExit();
61
- }
62
- if (key.name === "n") {
63
- setView("launcher");
64
- }
65
- if (key.name === "a") {
66
- setView("ask");
67
- }
68
- }
69
- else if (view === "detail") {
70
- if (key.name === "escape") {
71
- setView("runs");
72
- }
73
- else if (key.name === "c" && key.ctrl) {
74
- onExit();
75
- }
76
- }
77
- else if (view === "node") {
78
- if (key.name === "escape") {
79
- setView("detail");
80
- }
81
- else if (key.name === "c" && key.ctrl) {
82
- onExit();
83
- }
84
- }
85
- else if (view === "launcher") {
86
- if (key.name === "escape") {
87
- setView("runs");
88
- }
89
- else if (key.name === "c" && key.ctrl) {
90
- onExit();
91
- }
92
- }
93
- else if (view === "ask") {
94
- if (key.name === "escape") {
95
- setView("runs");
96
- }
97
- else if (key.name === "c" && key.ctrl) {
98
- onExit();
99
- }
100
- }
101
- });
102
- return (<box style={{ flexGrow: 1, width: "100%", height: "100%", flexDirection: "column" }}>
103
- {/* Global Tab Header */}
104
- <box style={{ width: "100%", height: 3, borderBottom: true, borderColor: "gray", flexDirection: "row", paddingLeft: 1 }}>
105
- {TABS.map((tab) => {
106
- const isActive = activeTabId === tab.id;
107
- return (<text key={tab.id} style={{ color: isActive ? "#a7f3d0" : "gray", marginRight: 3 }}>
108
- {isActive ? "▶ " : " "}[{tab.access.toUpperCase()}] {tab.label}
109
- </text>);
110
- })}
111
- </box>
112
-
113
- {view === "runs" && (<box style={{ flexGrow: 1, width: "100%", height: "100%", border: true, borderColor: "#34d399", flexDirection: "column" }} title="Smithers Runs - [Enter] View Details | [N] New Run | [Esc] Exit">
114
- <RunsList adapter={adapter} focused={view === "runs"} onChange={setSelectedRunId} onSubmit={(runId) => {
115
- setSelectedRunId(runId);
116
- setView("detail");
117
- }}/>
118
- </box>)}
119
-
120
- {view === "detail" && selectedRunId && (<RunDetailView adapter={adapter} runId={selectedRunId} onBack={() => setView("runs")} onSelectNode={(nodeId) => {
121
- setSelectedNodeId(nodeId);
122
- setView("node");
123
- }}/>)}
124
-
125
- {view === "node" && selectedRunId && (<NodeDetailView adapter={adapter} runId={selectedRunId} nodeId={selectedNodeId} onBack={() => setView("detail")}/>)}
126
-
127
- {view === "launcher" && (<WorkflowLauncher onClose={() => setView("runs")}/>)}
128
- {view === "ask" && (<AskModal onClose={() => setView("runs")}/>)}
129
- {view === "sqlite" && (<box style={{ flexGrow: 1, width: "100%", height: "100%", border: true, borderColor: "#34d399", flexDirection: "column" }} title="Smithers DB - [Esc] Return to Runs | [Tab] Switch Panes | [Up/Down] Query Table">
130
- <SqliteBrowser adapter={adapter} onBack={() => setView("runs")}/>
131
- </box>)}
132
- {view === "crons" && (<box style={{ flexGrow: 1, width: "100%", height: "100%", border: true, borderColor: "#34d399", flexDirection: "column" }} title="Smithers Schedule Triggers - [Esc] Return to Runs | [Up/Down] Select | [Del] Remove">
133
- <CronList adapter={adapter} onBack={() => setView("runs")}/>
134
- </box>)}
135
- {view === "metrics" && (<box style={{ flexGrow: 1, width: "100%", height: "100%", border: true, borderColor: "#34d399", flexDirection: "column" }} title="Smithers Telemetry (Prometheus Rollup) - [Esc] Return to Runs">
136
- <MetricsPane adapter={adapter} onBack={() => setView("runs")}/>
137
- </box>)}
138
- </box>);
139
- }
package/src/tui/app.tsx DELETED
@@ -1,5 +0,0 @@
1
- import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
2
- export declare function TuiApp({ adapter, onExit, }: {
3
- adapter: SmithersDb;
4
- onExit: () => void;
5
- }): import("react/jsx-runtime").JSX.Element;
@@ -1,109 +0,0 @@
1
- // @ts-nocheck
2
- import React, { useState, useEffect } from "react";
3
- import { useKeyboard } from "@opentui/react";
4
- /**
5
- * @param {{ onClose: () => void }} value
6
- */
7
- export function AskModal({ onClose }) {
8
- const [question, setQuestion] = useState("");
9
- const [answer, setAnswer] = useState("");
10
- const [status, setStatus] = useState("input");
11
- useKeyboard((key) => {
12
- if (key.name === "escape" || (key.name === "c" && key.ctrl)) {
13
- if (status !== "streaming") { // Or allow cancel mid-stream if we kill the proc? Keep simple for now
14
- onClose();
15
- return;
16
- }
17
- }
18
- if (status === "input") {
19
- if (key.name === "backspace") {
20
- setQuestion((q) => q.slice(0, -1));
21
- return;
22
- }
23
- if (key.name === "enter" || key.name === "return") {
24
- if (question.trim().length > 0) {
25
- startAsk();
26
- }
27
- return;
28
- }
29
- // Basic typing capture (sequence is the literal ansi char)
30
- if (!key.ctrl && !key.meta && key.sequence && key.sequence.length === 1 && key.name !== "up" && key.name !== "down" && key.name !== "left" && key.name !== "right") {
31
- setQuestion((q) => q + key.sequence);
32
- }
33
- }
34
- });
35
- async function startAsk() {
36
- setStatus("streaming");
37
- setAnswer("");
38
- try {
39
- const proc = Bun.spawn(["bun", "run", "src/index.js", "ask", question], {
40
- stdout: "pipe",
41
- stderr: "pipe",
42
- });
43
- // Stream stdout async
44
- (async () => {
45
- try {
46
- const stream = proc.stdout;
47
- const reader = stream.getReader();
48
- const decoder = new TextDecoder();
49
- while (true) {
50
- const { done, value } = await reader.read();
51
- if (done)
52
- break;
53
- const text = decoder.decode(value);
54
- setAnswer((a) => a + text);
55
- }
56
- }
57
- catch { }
58
- })();
59
- // Stream stderr async (in case the agent prints progress or errors there)
60
- (async () => {
61
- try {
62
- const stream = proc.stderr;
63
- const reader = stream.getReader();
64
- const decoder = new TextDecoder();
65
- while (true) {
66
- const { done, value } = await reader.read();
67
- if (done)
68
- break;
69
- const text = decoder.decode(value);
70
- setAnswer((a) => a + text);
71
- }
72
- }
73
- catch { }
74
- })();
75
- await proc.exited;
76
- if (proc.exitCode !== 0 && answer.trim().length === 0) {
77
- setAnswer("Failed to run ask command. Is your agent installed?");
78
- setStatus("error");
79
- }
80
- else {
81
- setStatus("done");
82
- }
83
- }
84
- catch (err) {
85
- setAnswer(`Spawn error: ${err.message}`);
86
- setStatus("error");
87
- }
88
- }
89
- // Auto scroll logic in OpenTUI: scrollboxes generally stay at the top unless navigated?
90
- // For streaming, we'll just append text.
91
- return (<box style={{
92
- flexGrow: 1,
93
- width: "100%",
94
- height: "100%",
95
- border: true,
96
- borderColor: "magenta",
97
- flexDirection: "column",
98
- }} title={`Ask Smithers ${status === "input" ? "[Type Question, Enter to Submit, Esc to Close]" : "[Streaming... Esc to Close]"}`}>
99
- {status === "input" ? (<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
100
- <text style={{ color: "cyan" }}>What would you like to know about the Smithers orchestrator?</text>
101
- <text style={{ color: "white", marginTop: 1 }}>{"> "}{question}█</text>
102
- </box>) : (<scrollbox style={{ width: "100%", height: "100%", flexDirection: "column", paddingLeft: 1 }}>
103
- <text style={{ color: "cyan", marginBottom: 1 }}>Q: {question}</text>
104
- <text style={{ color: "white" }}>{answer}</text>
105
- {status === "done" && <text style={{ color: "green", marginTop: 1 }}>[ Agent finished. Press Esc to close. ]</text>}
106
- {status === "error" && <text style={{ color: "red", marginTop: 1 }}>[ Agent failed. Press Esc to close. ]</text>}
107
- </scrollbox>)}
108
- </box>);
109
- }
@@ -1,3 +0,0 @@
1
- export declare function AskModal({ onClose }: {
2
- onClose: () => void;
3
- }): import("react/jsx-runtime").JSX.Element;