@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.
- package/dist/agent-detection.d.ts +20 -3
- package/package.json +17 -19
- package/src/AgentAvailability.ts +3 -0
- package/src/agent-detection.js +226 -14
- package/src/ask.js +4 -6
- package/src/index.js +89 -45
- package/src/workflow-pack.js +48 -10
- package/src/tui/app.jsx +0 -139
- package/src/tui/app.tsx +0 -5
- package/src/tui/components/AskModal.jsx +0 -109
- package/src/tui/components/AskModal.tsx +0 -3
- package/src/tui/components/AttentionPane.jsx +0 -112
- package/src/tui/components/AttentionPane.tsx +0 -6
- package/src/tui/components/ChatPane.jsx +0 -57
- package/src/tui/components/ChatPane.tsx +0 -7
- package/src/tui/components/CronList.jsx +0 -87
- package/src/tui/components/CronList.tsx +0 -5
- package/src/tui/components/DetailsPane.jsx +0 -96
- package/src/tui/components/DetailsPane.tsx +0 -7
- package/src/tui/components/FramesPane.jsx +0 -147
- package/src/tui/components/FramesPane.tsx +0 -8
- package/src/tui/components/LogsPane.jsx +0 -46
- package/src/tui/components/LogsPane.tsx +0 -6
- package/src/tui/components/MetricsPane.jsx +0 -108
- package/src/tui/components/MetricsPane.tsx +0 -5
- package/src/tui/components/NodeDetailView.jsx +0 -284
- package/src/tui/components/NodeDetailView.tsx +0 -7
- package/src/tui/components/NodeInspector.jsx +0 -51
- package/src/tui/components/NodeInspector.tsx +0 -7
- package/src/tui/components/RunDetailView.jsx +0 -190
- package/src/tui/components/RunDetailView.tsx +0 -7
- package/src/tui/components/RunsList.jsx +0 -184
- package/src/tui/components/RunsList.tsx +0 -7
- package/src/tui/components/SqliteBrowser.jsx +0 -131
- package/src/tui/components/SqliteBrowser.tsx +0 -5
- package/src/tui/components/WorkflowLauncher.jsx +0 -63
- 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)
|
package/src/workflow-pack.js
CHANGED
|
@@ -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 "
|
|
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: [
|
|
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
|
|
3392
|
-
*
|
|
3393
|
-
*
|
|
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,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
|
-
}
|