@outputai/cli 0.3.2-next.5e221e8.0 → 0.3.3-dev.2650161.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 (96) hide show
  1. package/dist/assets/docker/docker-compose-dev.yml +1 -1
  2. package/dist/commands/dev/index.js +48 -13
  3. package/dist/commands/dev/index.spec.js +1 -2
  4. package/dist/components/status_icon.js +1 -1
  5. package/dist/generated/framework_version.json +1 -1
  6. package/dist/services/docker.js +0 -1
  7. package/dist/templates/project/src/workflows/blog_evaluator/prompts/signal_noise@v1.prompt.template +2 -1
  8. package/dist/templates/workflow/README.md.template +2 -1
  9. package/dist/templates/workflow/prompts/example@v1.prompt.template +2 -1
  10. package/dist/utils/paths.d.ts +11 -0
  11. package/dist/utils/paths.js +14 -0
  12. package/dist/utils/scenario_resolver.d.ts +2 -1
  13. package/dist/utils/scenario_resolver.js +57 -25
  14. package/dist/utils/scenario_resolver.spec.js +30 -1
  15. package/dist/views/dev/chrome/divider.d.ts +8 -0
  16. package/dist/views/dev/chrome/divider.js +16 -0
  17. package/dist/views/dev/chrome/footer.d.ts +11 -0
  18. package/dist/views/dev/chrome/footer.js +10 -0
  19. package/dist/views/dev/chrome/header.d.ts +21 -0
  20. package/dist/views/dev/chrome/header.js +74 -0
  21. package/dist/views/dev/chrome/header.spec.d.ts +1 -0
  22. package/dist/views/dev/chrome/header.spec.js +50 -0
  23. package/dist/views/dev/chrome/loading_spinner.d.ts +9 -0
  24. package/dist/views/dev/chrome/loading_spinner.js +9 -0
  25. package/dist/views/dev/chrome/palette.d.ts +16 -0
  26. package/dist/views/dev/chrome/palette.js +16 -0
  27. package/dist/views/dev/chrome/search_bar.d.ts +5 -0
  28. package/dist/views/dev/chrome/search_bar.js +35 -0
  29. package/dist/views/dev/chrome/selection_indicator.d.ts +14 -0
  30. package/dist/views/dev/chrome/selection_indicator.js +13 -0
  31. package/dist/views/dev/chrome/tab_bar.d.ts +5 -0
  32. package/dist/views/dev/chrome/tab_bar.js +4 -0
  33. package/dist/views/dev/chrome/toasts.d.ts +2 -0
  34. package/dist/views/dev/chrome/toasts.js +40 -0
  35. package/dist/views/dev/components/master_detail_panel.d.ts +21 -0
  36. package/dist/views/dev/components/master_detail_panel.js +18 -0
  37. package/dist/views/{dev.d.ts → dev/dev_app.d.ts} +1 -0
  38. package/dist/views/dev/dev_app.js +146 -0
  39. package/dist/views/dev/hooks/use_docker_logs.d.ts +7 -0
  40. package/dist/views/dev/hooks/use_docker_logs.js +69 -0
  41. package/dist/views/dev/hooks/use_poll.d.ts +16 -0
  42. package/dist/views/dev/hooks/use_poll.js +95 -0
  43. package/dist/views/dev/hooks/use_run_detail.d.ts +21 -0
  44. package/dist/views/dev/hooks/use_run_detail.js +153 -0
  45. package/dist/views/dev/hooks/use_run_detail.spec.d.ts +1 -0
  46. package/dist/views/dev/hooks/use_run_detail.spec.js +86 -0
  47. package/dist/views/dev/hooks/use_workflow_catalog.d.ts +2 -0
  48. package/dist/views/dev/hooks/use_workflow_catalog.js +21 -0
  49. package/dist/views/dev/modals/expanded_json_modal.d.ts +2 -0
  50. package/dist/views/dev/modals/expanded_json_modal.js +44 -0
  51. package/dist/views/dev/modals/run_modal.d.ts +5 -0
  52. package/dist/views/dev/modals/run_modal.js +213 -0
  53. package/dist/views/dev/panels/help_panel.d.ts +2 -0
  54. package/dist/views/dev/panels/help_panel.js +53 -0
  55. package/dist/views/dev/panels/run_detail_view.d.ts +5 -0
  56. package/dist/views/dev/panels/run_detail_view.js +112 -0
  57. package/dist/views/dev/panels/runs_panel.d.ts +8 -0
  58. package/dist/views/dev/panels/runs_panel.js +204 -0
  59. package/dist/views/dev/panels/runs_panel.spec.d.ts +1 -0
  60. package/dist/views/dev/panels/runs_panel.spec.js +82 -0
  61. package/dist/views/dev/panels/services_panel.d.ts +14 -0
  62. package/dist/views/dev/panels/services_panel.js +155 -0
  63. package/dist/views/dev/panels/services_panel.spec.d.ts +1 -0
  64. package/dist/views/dev/panels/services_panel.spec.js +28 -0
  65. package/dist/views/dev/panels/workflows_panel.d.ts +7 -0
  66. package/dist/views/dev/panels/workflows_panel.js +111 -0
  67. package/dist/views/dev/services/docker_control.d.ts +5 -0
  68. package/dist/views/dev/services/docker_control.js +25 -0
  69. package/dist/views/dev/services/run_workflow.d.ts +10 -0
  70. package/dist/views/dev/services/run_workflow.js +14 -0
  71. package/dist/views/dev/services/scenario_io.d.ts +2 -0
  72. package/dist/views/dev/services/scenario_io.js +41 -0
  73. package/dist/views/dev/state/ui_state.d.ts +61 -0
  74. package/dist/views/dev/state/ui_state.js +64 -0
  75. package/dist/views/dev/utils/constants.d.ts +17 -0
  76. package/dist/views/dev/utils/constants.js +17 -0
  77. package/dist/views/dev/utils/json_editor.d.ts +21 -0
  78. package/dist/views/dev/utils/json_editor.js +117 -0
  79. package/dist/views/dev/utils/json_editor.spec.d.ts +1 -0
  80. package/dist/views/dev/utils/json_editor.spec.js +57 -0
  81. package/dist/views/dev/utils/json_render.d.ts +15 -0
  82. package/dist/views/dev/utils/json_render.js +77 -0
  83. package/dist/views/dev/utils/json_render.spec.d.ts +1 -0
  84. package/dist/views/dev/utils/json_render.spec.js +65 -0
  85. package/dist/views/dev/utils/panel_helpers.d.ts +16 -0
  86. package/dist/views/dev/utils/panel_helpers.js +32 -0
  87. package/dist/views/dev/utils/panel_helpers.spec.d.ts +1 -0
  88. package/dist/views/dev/utils/panel_helpers.spec.js +47 -0
  89. package/package.json +5 -5
  90. package/dist/components/command_footer.d.ts +0 -8
  91. package/dist/components/command_footer.js +0 -4
  92. package/dist/components/workflow_summary.d.ts +0 -10
  93. package/dist/components/workflow_summary.js +0 -4
  94. package/dist/views/dev.js +0 -187
  95. package/dist/views/workflow/list.d.ts +0 -6
  96. package/dist/views/workflow/list.js +0 -129
@@ -0,0 +1,111 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { StatusIcon, statusColor } from '#components/status_icon.js';
5
+ import { elapsedMs, formatDurationCompact } from '#utils/date_formatter.js';
6
+ import { Footer } from '#views/dev/chrome/footer.js';
7
+ import { SelectionIndicator } from '#views/dev/chrome/selection_indicator.js';
8
+ import { useUiState } from '#views/dev/state/ui_state.js';
9
+ import { MasterDetailPanel } from '#views/dev/components/master_detail_panel.js';
10
+ import { formatStartedShort } from '#views/dev/utils/panel_helpers.js';
11
+ import { WORKFLOWS_VISIBLE_ROWS, WORKFLOWS_RECENT_RUNS_LIMIT } from '#views/dev/utils/constants.js';
12
+ const COL = {
13
+ indicator: 3,
14
+ name: 30
15
+ };
16
+ const matchesQuery = (workflow, query) => {
17
+ if (!query) {
18
+ return true;
19
+ }
20
+ const q = query.toLowerCase();
21
+ if ((workflow.name ?? '').toLowerCase().includes(q)) {
22
+ return true;
23
+ }
24
+ if ((workflow.description ?? '').toLowerCase().includes(q)) {
25
+ return true;
26
+ }
27
+ return (workflow.aliases ?? []).some(a => a.toLowerCase().includes(q));
28
+ };
29
+ const sortByName = (workflows) => [...workflows].sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
30
+ const buildVisibleWorkflows = (workflows, query) => {
31
+ const list = query ? workflows.filter(w => matchesQuery(w, query)) : workflows;
32
+ return sortByName(list);
33
+ };
34
+ const HeaderRow = () => (_jsxs(Box, { children: [_jsx(Box, { width: COL.indicator, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: COL.name, children: _jsx(Text, { dimColor: true, bold: true, children: "WORKFLOW" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { dimColor: true, bold: true, children: "DESCRIPTION" }) })] }));
35
+ const WorkflowRow = ({ workflow, selected }) => (_jsxs(Box, { children: [_jsx(Box, { width: COL.indicator, children: _jsx(SelectionIndicator, { selected: selected }) }), _jsx(Box, { width: COL.name, children: _jsx(Text, { bold: selected, wrap: "truncate-end", children: workflow.name ?? '-' }) }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { dimColor: !selected, wrap: "truncate-end", children: workflow.description ?? 'No description' }) })] }));
36
+ const SidebarRunRow = ({ run }) => {
37
+ const status = run.status ?? 'unknown';
38
+ const duration = run.startedAt ? formatDurationCompact(elapsedMs(run.startedAt, run.completedAt)) : '-';
39
+ return (_jsxs(Box, { children: [_jsx(StatusIcon, { status: status }), _jsx(Text, { children: " " }), _jsx(Box, { width: 10, children: _jsx(Text, { color: statusColor(status), children: status }) }), _jsx(Box, { width: 9, justifyContent: "flex-end", children: _jsx(Text, { dimColor: true, children: duration }) }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { dimColor: true, children: formatStartedShort(run.startedAt) }) })] }));
40
+ };
41
+ const DetailPane = ({ workflow, runs }) => {
42
+ if (!workflow) {
43
+ return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Select a workflow to see details." }) }));
44
+ }
45
+ const wfRuns = runs.filter(r => r.workflowType === workflow.name);
46
+ const stats = {
47
+ total: wfRuns.length,
48
+ running: wfRuns.filter(r => r.status === 'running').length,
49
+ failed: wfRuns.filter(r => r.status === 'failed').length,
50
+ completed: wfRuns.filter(r => r.status === 'completed').length
51
+ };
52
+ const recent = wfRuns.slice(0, WORKFLOWS_RECENT_RUNS_LIMIT);
53
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: "white", children: workflow.name }) }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: [stats.total, " runs"] }), stats.running > 0 && _jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: "blue", children: ["\u25CF ", stats.running, " running"] })] }), stats.failed > 0 && _jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: "red", children: ["\u2717 ", stats.failed, " failed"] })] }), stats.completed > 0 && _jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: "green", children: ["\u25CF ", stats.completed, " ok"] })] })] }), _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, paddingRight: 2, children: _jsx(Text, { wrap: "wrap", children: workflow.description ?? 'No description' }) }), _jsxs(Box, { flexDirection: "column", width: 42, borderStyle: "single", borderTop: false, borderBottom: false, borderRight: false, paddingLeft: 2, children: [_jsx(Text, { dimColor: true, bold: true, children: "RECENT RUNS" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: recent.length === 0 ? (_jsx(Text, { dimColor: true, children: "No runs yet" })) : (recent.map((run, i) => _jsx(SidebarRunRow, { run: run }, `${run.runId ?? i}`))) })] })] })] }));
54
+ };
55
+ const HINTS = [
56
+ { key: '↑/↓', label: 'navigate' },
57
+ { key: 'enter', label: 'show runs' },
58
+ { key: 'r', label: 'run' },
59
+ { key: '/', label: 'search' },
60
+ { key: 'tab', label: 'next tab' }
61
+ ];
62
+ export const WorkflowsPanel = ({ workflows, runs }) => {
63
+ const ui = useUiState();
64
+ const filtered = useMemo(() => buildVisibleWorkflows(workflows, ui.search.query), [workflows, ui.search.query]);
65
+ // Lazy initializer — runs once on mount. Restores the previously
66
+ // highlighted workflow after the run modal unmounts and remounts the
67
+ // panel.
68
+ const [selectedIndex, setSelectedIndex] = useState(() => {
69
+ const previousName = ui.selection.workflowName;
70
+ if (!previousName) {
71
+ return 0;
72
+ }
73
+ const initial = buildVisibleWorkflows(workflows, ui.search.query);
74
+ const i = initial.findIndex(w => w.name === previousName);
75
+ return i >= 0 ? i : 0;
76
+ });
77
+ const isActive = ui.tab === 'workflows' && !ui.search.open && !ui.runModal.open;
78
+ const clamped = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
79
+ const selectedWorkflow = filtered[clamped];
80
+ useEffect(() => {
81
+ if (clamped !== selectedIndex) {
82
+ setSelectedIndex(clamped);
83
+ }
84
+ }, [clamped, selectedIndex]);
85
+ const setSelection = ui.setSelection;
86
+ useEffect(() => {
87
+ setSelection({ workflowName: selectedWorkflow?.name });
88
+ }, [selectedWorkflow?.name, setSelection]);
89
+ useInput((input, key) => {
90
+ if (key.upArrow) {
91
+ setSelectedIndex(i => Math.max(0, i - 1));
92
+ }
93
+ else if (key.downArrow) {
94
+ setSelectedIndex(i => Math.min(filtered.length - 1, i + 1));
95
+ }
96
+ else if (key.return && selectedWorkflow?.name) {
97
+ ui.setSearchQuery(selectedWorkflow.name);
98
+ ui.setTab('runs');
99
+ }
100
+ else if (input === 'r' && selectedWorkflow?.name) {
101
+ ui.openRunModal(selectedWorkflow.name, selectedWorkflow.path);
102
+ }
103
+ }, { isActive });
104
+ if (workflows.length === 0) {
105
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Workflows" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Loading catalog\u2026 (waiting for the API to come up)" }) }), _jsx(Footer, { hints: [{ key: 'tab', label: 'next tab' }, { key: '?', label: 'help' }] })] }));
106
+ }
107
+ if (filtered.length === 0) {
108
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Workflows" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: ["No workflows match `", ui.search.query, "`. Press "] }), _jsx(Text, { bold: true, children: "esc" }), _jsx(Text, { dimColor: true, children: " to clear." })] }), _jsx(Footer, { hints: HINTS, itemCount: 0, itemLabel: "workflows" })] }));
109
+ }
110
+ return (_jsx(MasterDetailPanel, { items: filtered, selectedIndex: clamped, visibleRows: WORKFLOWS_VISIBLE_ROWS, renderHeader: () => _jsx(HeaderRow, {}), renderRow: (wf, selected) => _jsx(WorkflowRow, { workflow: wf, selected: selected }), rowKey: (wf, i) => wf.name ?? `row-${i}`, detail: _jsx(DetailPane, { workflow: selectedWorkflow, runs: runs }), hints: HINTS, itemLabel: "workflows" }));
111
+ };
@@ -0,0 +1,5 @@
1
+ import { type ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ export declare const restartService: (dockerComposePath: string, serviceName: string) => Promise<void>;
3
+ export declare const restartStack: (dockerComposePath: string) => Promise<void>;
4
+ export declare const stopService: (dockerComposePath: string, serviceName: string) => Promise<void>;
5
+ export declare const tailLogs: (dockerComposePath: string, serviceName: string, tailLines?: number) => ChildProcessWithoutNullStreams;
@@ -0,0 +1,25 @@
1
+ import { spawn } from 'node:child_process';
2
+ // stdio: 'pipe' so docker's stderr/stdout never reach the host terminal
3
+ // while INK owns the screen. Stderr is buffered and surfaced through the
4
+ // rejected Promise so the panel can present it as a banner instead of
5
+ // having garbled output collide with the rendered TUI.
6
+ const run = (args) => new Promise((resolve, reject) => {
7
+ const child = spawn('docker', args, { stdio: 'pipe' });
8
+ const stderrChunks = [];
9
+ child.stderr.on('data', chunk => {
10
+ stderrChunks.push(chunk.toString());
11
+ });
12
+ child.on('error', reject);
13
+ child.on('exit', code => {
14
+ if (code === 0) {
15
+ resolve();
16
+ return;
17
+ }
18
+ const stderr = stderrChunks.join('').trim();
19
+ reject(new Error(stderr || `docker ${args.join(' ')} exited with code ${code}`));
20
+ });
21
+ });
22
+ export const restartService = (dockerComposePath, serviceName) => run(['compose', '-f', dockerComposePath, 'restart', serviceName]);
23
+ export const restartStack = (dockerComposePath) => run(['compose', '-f', dockerComposePath, 'restart']);
24
+ export const stopService = (dockerComposePath, serviceName) => run(['compose', '-f', dockerComposePath, 'stop', serviceName]);
25
+ export const tailLogs = (dockerComposePath, serviceName, tailLines = 200) => spawn('docker', ['compose', '-f', dockerComposePath, 'logs', '-f', '--no-color', '--tail', String(tailLines), serviceName], { stdio: 'pipe' });
@@ -0,0 +1,10 @@
1
+ export interface StartedRun {
2
+ workflowId?: string;
3
+ runId?: string | null;
4
+ }
5
+ export interface StartWorkflowOptions {
6
+ workflowName: string;
7
+ input: unknown;
8
+ taskQueue?: string;
9
+ }
10
+ export declare const startWorkflow: (opts: StartWorkflowOptions) => Promise<StartedRun>;
@@ -0,0 +1,14 @@
1
+ import { postWorkflowStart } from '#api/generated/api.js';
2
+ export const startWorkflow = async (opts) => {
3
+ const response = await postWorkflowStart({
4
+ workflowName: opts.workflowName,
5
+ input: opts.input,
6
+ taskQueue: opts.taskQueue
7
+ });
8
+ if (response.status !== 200) {
9
+ const data = response.data;
10
+ throw new Error(data?.error ?? `Workflow start failed (status ${response.status})`);
11
+ }
12
+ const data = response.data;
13
+ return { workflowId: data.workflowId, runId: data.runId };
14
+ };
@@ -0,0 +1,2 @@
1
+ export declare const readScenario: (workflowName: string, scenarioName: string, workflowPath?: string) => Promise<unknown>;
2
+ export declare const writeScenario: (workflowName: string, scenarioName: string, content: unknown, workflowPath?: string) => Promise<string>;
@@ -0,0 +1,41 @@
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { resolve, dirname } from 'node:path';
4
+ import { findWorkflowDirectoryFromPath, resolveScenarioPath } from '#utils/scenario_resolver.js';
5
+ import { getWorkflowsBasePath } from '#utils/paths.js';
6
+ const WORKFLOWS_PATHS = ['src/workflows', 'workflows'];
7
+ export const readScenario = async (workflowName, scenarioName, workflowPath) => {
8
+ const resolution = await resolveScenarioPath(workflowName, scenarioName, getWorkflowsBasePath(), workflowPath);
9
+ if (!resolution.found || !resolution.path) {
10
+ throw new Error(`Scenario '${scenarioName}' not found for workflow '${workflowName}'.`);
11
+ }
12
+ const content = await readFile(resolution.path, 'utf-8');
13
+ return JSON.parse(content);
14
+ };
15
+ const findWorkflowDirectory = (workflowName, workflowPath) => {
16
+ const basePath = getWorkflowsBasePath();
17
+ const pathDir = findWorkflowDirectoryFromPath(workflowPath, basePath);
18
+ if (pathDir) {
19
+ return pathDir;
20
+ }
21
+ for (const wfDir of WORKFLOWS_PATHS) {
22
+ const candidate = resolve(basePath, wfDir, workflowName);
23
+ if (existsSync(candidate)) {
24
+ return candidate;
25
+ }
26
+ }
27
+ return null;
28
+ };
29
+ export const writeScenario = async (workflowName, scenarioName, content, workflowPath) => {
30
+ const dir = findWorkflowDirectory(workflowName, workflowPath);
31
+ if (!dir) {
32
+ throw new Error(`Workflow directory for '${workflowName}' not found locally.`);
33
+ }
34
+ const targetPath = resolve(dir, 'scenarios', `${scenarioName}.json`);
35
+ if (existsSync(targetPath)) {
36
+ throw new Error(`Scenario '${scenarioName}' already exists at ${targetPath}`);
37
+ }
38
+ await mkdir(dirname(targetPath), { recursive: true });
39
+ await writeFile(targetPath, JSON.stringify(content, null, 2) + '\n', 'utf-8');
40
+ return targetPath;
41
+ };
@@ -0,0 +1,61 @@
1
+ import React from 'react';
2
+ export type Tab = 'workflows' | 'runs' | 'services' | 'help';
3
+ export declare const TAB_ORDER: Tab[];
4
+ export declare const TAB_LABELS: Record<Tab, string>;
5
+ export type RightPaneTab = 'input' | 'output' | 'meta';
6
+ export type RunsView = 'list' | 'detail';
7
+ export interface Selection {
8
+ workflowName?: string;
9
+ runId?: string;
10
+ workflowId?: string;
11
+ serviceName?: string;
12
+ }
13
+ export interface SearchState {
14
+ open: boolean;
15
+ query: string;
16
+ }
17
+ export interface RunModalState {
18
+ open: boolean;
19
+ workflowName: string;
20
+ workflowPath?: string;
21
+ }
22
+ export interface ExpandedJsonState {
23
+ open: boolean;
24
+ value: unknown;
25
+ title: string;
26
+ }
27
+ export interface Toast {
28
+ id: number;
29
+ message: string;
30
+ tone: 'info' | 'success' | 'error';
31
+ }
32
+ export interface UiState {
33
+ tab: Tab;
34
+ search: SearchState;
35
+ selection: Selection;
36
+ rightPaneTab: RightPaneTab;
37
+ runsView: RunsView;
38
+ runModal: RunModalState;
39
+ expandedJson: ExpandedJsonState;
40
+ toasts: Toast[];
41
+ setTab: (tab: Tab) => void;
42
+ nextTab: () => void;
43
+ prevTab: () => void;
44
+ openSearch: () => void;
45
+ closeSearch: () => void;
46
+ clearSearch: () => void;
47
+ setSearchQuery: (query: string) => void;
48
+ setSelection: (selection: Selection) => void;
49
+ setRightPaneTab: (tab: RightPaneTab) => void;
50
+ setRunsView: (view: RunsView) => void;
51
+ openRunModal: (workflowName: string, workflowPath?: string) => void;
52
+ closeRunModal: () => void;
53
+ openExpandedJson: (value: unknown, title: string) => void;
54
+ closeExpandedJson: () => void;
55
+ pushToast: (message: string, tone?: Toast['tone']) => void;
56
+ dismissToast: (id: number) => void;
57
+ }
58
+ export declare const UiStateProvider: React.FC<{
59
+ children: React.ReactNode;
60
+ }>;
61
+ export declare const useUiState: () => UiState;
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useMemo, useRef, useState } from 'react';
3
+ export const TAB_ORDER = ['workflows', 'runs', 'services', 'help'];
4
+ export const TAB_LABELS = {
5
+ workflows: 'Workflows',
6
+ runs: 'Recent Runs',
7
+ services: 'Services',
8
+ help: 'Help'
9
+ };
10
+ const UiStateContext = createContext(null);
11
+ export const UiStateProvider = ({ children }) => {
12
+ const [tab, setTab] = useState('services');
13
+ const [search, setSearch] = useState({ open: false, query: '' });
14
+ const [selection, setSelection] = useState({});
15
+ const [rightPaneTab, setRightPaneTab] = useState('output');
16
+ const [runsView, setRunsView] = useState('list');
17
+ const [runModal, setRunModal] = useState({ open: false, workflowName: '' });
18
+ const [expandedJson, setExpandedJson] = useState({ open: false, value: null, title: '' });
19
+ const [toasts, setToasts] = useState([]);
20
+ const toastIdRef = useRef(0);
21
+ const value = useMemo(() => ({
22
+ tab,
23
+ search,
24
+ selection,
25
+ rightPaneTab,
26
+ runsView,
27
+ runModal,
28
+ expandedJson,
29
+ toasts,
30
+ setTab,
31
+ nextTab: () => setTab(current => {
32
+ const idx = TAB_ORDER.indexOf(current);
33
+ return TAB_ORDER[(idx + 1) % TAB_ORDER.length];
34
+ }),
35
+ prevTab: () => setTab(current => {
36
+ const idx = TAB_ORDER.indexOf(current);
37
+ return TAB_ORDER[(idx - 1 + TAB_ORDER.length) % TAB_ORDER.length];
38
+ }),
39
+ openSearch: () => setSearch(prev => ({ open: true, query: prev.query })),
40
+ closeSearch: () => setSearch(prev => ({ open: false, query: prev.query })),
41
+ clearSearch: () => setSearch({ open: false, query: '' }),
42
+ setSearchQuery: (query) => setSearch(prev => ({ open: prev.open, query })),
43
+ setSelection,
44
+ setRightPaneTab,
45
+ setRunsView,
46
+ openRunModal: (workflowName, workflowPath) => setRunModal({ open: true, workflowName, workflowPath }),
47
+ closeRunModal: () => setRunModal({ open: false, workflowName: '' }),
48
+ openExpandedJson: (value, title) => setExpandedJson({ open: true, value, title }),
49
+ closeExpandedJson: () => setExpandedJson({ open: false, value: null, title: '' }),
50
+ pushToast: (message, tone = 'info') => {
51
+ const id = ++toastIdRef.current;
52
+ setToasts(prev => [...prev, { id, message, tone }]);
53
+ },
54
+ dismissToast: (id) => setToasts(prev => prev.filter(t => t.id !== id))
55
+ }), [tab, search, selection, rightPaneTab, runsView, runModal, expandedJson, toasts]);
56
+ return _jsx(UiStateContext.Provider, { value: value, children: children });
57
+ };
58
+ export const useUiState = () => {
59
+ const ctx = useContext(UiStateContext);
60
+ if (!ctx) {
61
+ throw new Error('useUiState must be used inside <UiStateProvider>');
62
+ }
63
+ return ctx;
64
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Workflow type emitted by Temporal's catalog refresh, used to drop the
3
+ * routine completed catalog rows from the Recent Runs list while still
4
+ * surfacing failing or running ones for diagnostics.
5
+ */
6
+ export declare const CATALOG_WORKFLOW_NAME = "$catalog";
7
+ /**
8
+ * Visible row counts and per-pane layout knobs. Co-located here so the
9
+ * top-of-file constant blocks across panels stay terse.
10
+ */
11
+ export declare const WORKFLOWS_VISIBLE_ROWS = 8;
12
+ export declare const WORKFLOWS_RECENT_RUNS_LIMIT = 5;
13
+ export declare const RUNS_VISIBLE_ROWS = 8;
14
+ export declare const RUNS_PREVIEW_LINES = 12;
15
+ export declare const RUN_DETAIL_VISIBLE_STEPS = 12;
16
+ export declare const RUN_DETAIL_PREVIEW_LINES = 18;
17
+ export declare const HELP_DOCS_URL = "https://docs.output.ai";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Workflow type emitted by Temporal's catalog refresh, used to drop the
3
+ * routine completed catalog rows from the Recent Runs list while still
4
+ * surfacing failing or running ones for diagnostics.
5
+ */
6
+ export const CATALOG_WORKFLOW_NAME = '$catalog';
7
+ /**
8
+ * Visible row counts and per-pane layout knobs. Co-located here so the
9
+ * top-of-file constant blocks across panels stay terse.
10
+ */
11
+ export const WORKFLOWS_VISIBLE_ROWS = 8;
12
+ export const WORKFLOWS_RECENT_RUNS_LIMIT = 5;
13
+ export const RUNS_VISIBLE_ROWS = 8;
14
+ export const RUNS_PREVIEW_LINES = 12;
15
+ export const RUN_DETAIL_VISIBLE_STEPS = 12;
16
+ export const RUN_DETAIL_PREVIEW_LINES = 18;
17
+ export const HELP_DOCS_URL = 'https://docs.output.ai';
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ interface CursorPos {
3
+ line: number;
4
+ col: number;
5
+ }
6
+ export declare const cursorToPosition: (buffer: string, cursor: number) => CursorPos;
7
+ export declare const positionToCursor: (buffer: string, line: number, col: number) => number;
8
+ export declare const tryParseJson: (text: string) => {
9
+ ok: true;
10
+ } | {
11
+ ok: false;
12
+ error: string;
13
+ };
14
+ export declare const JsonEditor: React.FC<{
15
+ seed: unknown;
16
+ title: string;
17
+ isActive?: boolean;
18
+ onSubmit: (value: unknown) => void;
19
+ onCancel: () => void;
20
+ }>;
21
+ export {};
@@ -0,0 +1,117 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ export const cursorToPosition = (buffer, cursor) => {
5
+ const before = buffer.slice(0, cursor);
6
+ const lines = before.split('\n');
7
+ return { line: lines.length - 1, col: lines[lines.length - 1].length };
8
+ };
9
+ export const positionToCursor = (buffer, line, col) => {
10
+ const allLines = buffer.split('\n');
11
+ const targetLine = Math.max(0, Math.min(line, allLines.length - 1));
12
+ const targetCol = Math.max(0, Math.min(col, allLines[targetLine].length));
13
+ const charsBefore = allLines.slice(0, targetLine).reduce((acc, l) => acc + l.length + 1, 0);
14
+ return charsBefore + targetCol;
15
+ };
16
+ export const tryParseJson = (text) => {
17
+ if (text.trim().length === 0) {
18
+ return { ok: false, error: 'Empty' };
19
+ }
20
+ try {
21
+ JSON.parse(text);
22
+ return { ok: true };
23
+ }
24
+ catch (err) {
25
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
26
+ }
27
+ };
28
+ const VISIBLE_BUFFER = 6;
29
+ export const JsonEditor = ({ seed, title, isActive = true, onSubmit, onCancel }) => {
30
+ const initial = JSON.stringify(seed ?? {}, null, 2);
31
+ const [buffer, setBuffer] = useState(initial);
32
+ const [cursor, setCursor] = useState(initial.length);
33
+ const [status, setStatus] = useState(() => tryParseJson(initial));
34
+ const [submitMessage, setSubmitMessage] = useState(null);
35
+ useEffect(() => {
36
+ setStatus(tryParseJson(buffer));
37
+ }, [buffer]);
38
+ const insert = (text) => {
39
+ setBuffer(b => b.slice(0, cursor) + text + b.slice(cursor));
40
+ setCursor(c => c + text.length);
41
+ };
42
+ const removeBeforeCursor = () => {
43
+ if (cursor === 0) {
44
+ return;
45
+ }
46
+ setBuffer(b => b.slice(0, cursor - 1) + b.slice(cursor));
47
+ setCursor(c => Math.max(0, c - 1));
48
+ };
49
+ useInput((input, key) => {
50
+ if (key.escape) {
51
+ onCancel();
52
+ return;
53
+ }
54
+ if (key.ctrl && input === 's') {
55
+ const parsed = tryParseJson(buffer);
56
+ if (!parsed.ok) {
57
+ setSubmitMessage(`Cannot submit — ${parsed.error}`);
58
+ return;
59
+ }
60
+ try {
61
+ onSubmit(JSON.parse(buffer));
62
+ }
63
+ catch (err) {
64
+ setSubmitMessage(err instanceof Error ? err.message : String(err));
65
+ }
66
+ return;
67
+ }
68
+ if (key.return) {
69
+ insert('\n');
70
+ return;
71
+ }
72
+ if (key.tab) {
73
+ insert(' ');
74
+ return;
75
+ }
76
+ if (key.backspace || key.delete) {
77
+ removeBeforeCursor();
78
+ return;
79
+ }
80
+ if (key.leftArrow) {
81
+ setCursor(c => Math.max(0, c - 1));
82
+ return;
83
+ }
84
+ if (key.rightArrow) {
85
+ setCursor(c => Math.min(buffer.length, c + 1));
86
+ return;
87
+ }
88
+ if (key.upArrow) {
89
+ const pos = cursorToPosition(buffer, cursor);
90
+ setCursor(positionToCursor(buffer, pos.line - 1, pos.col));
91
+ return;
92
+ }
93
+ if (key.downArrow) {
94
+ const pos = cursorToPosition(buffer, cursor);
95
+ setCursor(positionToCursor(buffer, pos.line + 1, pos.col));
96
+ return;
97
+ }
98
+ if (input && !key.ctrl && !key.meta) {
99
+ insert(input);
100
+ }
101
+ }, { isActive });
102
+ const lines = buffer.split('\n');
103
+ const cursorPos = cursorToPosition(buffer, cursor);
104
+ const half = Math.floor(VISIBLE_BUFFER / 2);
105
+ const startLine = Math.max(0, cursorPos.line - half);
106
+ const visibleLines = lines.slice(startLine, startLine + (VISIBLE_BUFFER * 2) + 1);
107
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { bold: true, children: ["\u270F ", title] }), _jsx(Text, { bold: true, color: status.ok ? 'green' : 'red', children: status.ok ? '✓ valid JSON' : '✗ invalid JSON' })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: visibleLines.map((line, i) => {
108
+ const lineIdx = startLine + i;
109
+ if (lineIdx !== cursorPos.line) {
110
+ return _jsx(Text, { children: line.length === 0 ? ' ' : line }, lineIdx);
111
+ }
112
+ const before = line.slice(0, cursorPos.col);
113
+ const at = line[cursorPos.col] ?? ' ';
114
+ const after = line.slice(cursorPos.col + 1);
115
+ return (_jsxs(Text, { children: [_jsx(Text, { children: before }), _jsx(Text, { inverse: true, children: at }), _jsx(Text, { children: after })] }, lineIdx));
116
+ }) }), !status.ok && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", wrap: "truncate-end", children: status.error }) })), submitMessage && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: submitMessage }) })), _jsxs(Box, { marginTop: 1, columnGap: 2, children: [_jsxs(Box, { columnGap: 1, children: [_jsx(Text, { bold: true, children: "ctrl+s" }), _jsx(Text, { dimColor: true, children: "submit" })] }), _jsxs(Box, { columnGap: 1, children: [_jsx(Text, { bold: true, children: "esc" }), _jsx(Text, { dimColor: true, children: "cancel" })] }), _jsxs(Box, { columnGap: 1, children: [_jsx(Text, { bold: true, children: "\u2191\u2193\u2190\u2192" }), _jsx(Text, { dimColor: true, children: "move" })] }), _jsxs(Box, { columnGap: 1, children: [_jsx(Text, { bold: true, children: "tab" }), _jsx(Text, { dimColor: true, children: "indent" })] })] })] }));
117
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { cursorToPosition, positionToCursor, tryParseJson } from './json_editor.js';
3
+ describe('cursorToPosition', () => {
4
+ it('returns line 0 col 0 for an empty buffer', () => {
5
+ expect(cursorToPosition('', 0)).toEqual({ line: 0, col: 0 });
6
+ });
7
+ it('tracks column on a single-line buffer', () => {
8
+ expect(cursorToPosition('hello', 3)).toEqual({ line: 0, col: 3 });
9
+ });
10
+ it('crosses newline boundaries', () => {
11
+ // 'ab\ncd' — cursor 3 is just before `c` on line 1
12
+ expect(cursorToPosition('ab\ncd', 3)).toEqual({ line: 1, col: 0 });
13
+ });
14
+ it('lands at the end of a line', () => {
15
+ expect(cursorToPosition('ab\ncd', 5)).toEqual({ line: 1, col: 2 });
16
+ });
17
+ });
18
+ describe('positionToCursor', () => {
19
+ it('returns 0 for an empty buffer', () => {
20
+ expect(positionToCursor('', 0, 0)).toBe(0);
21
+ });
22
+ it('computes index for a column on a given line', () => {
23
+ expect(positionToCursor('ab\ncd', 1, 1)).toBe(4);
24
+ });
25
+ it('clamps the column to the line length', () => {
26
+ expect(positionToCursor('ab\ncd', 0, 99)).toBe(2);
27
+ });
28
+ it('clamps to the last line when over-shooting', () => {
29
+ expect(positionToCursor('ab\ncd', 99, 0)).toBe(3);
30
+ });
31
+ it('clamps to the first line when under-shooting', () => {
32
+ expect(positionToCursor('ab\ncd', -5, 1)).toBe(1);
33
+ });
34
+ it('roundtrips with cursorToPosition', () => {
35
+ const buffer = 'foo\nbar\nbaz';
36
+ for (const cursor of [0, 2, 4, 7, 10]) {
37
+ const pos = cursorToPosition(buffer, cursor);
38
+ expect(positionToCursor(buffer, pos.line, pos.col)).toBe(cursor);
39
+ }
40
+ });
41
+ });
42
+ describe('tryParseJson', () => {
43
+ it('rejects empty input', () => {
44
+ expect(tryParseJson('')).toEqual({ ok: false, error: 'Empty' });
45
+ expect(tryParseJson(' \n ')).toEqual({ ok: false, error: 'Empty' });
46
+ });
47
+ it('accepts valid JSON', () => {
48
+ expect(tryParseJson('{"a":1}')).toEqual({ ok: true });
49
+ });
50
+ it('returns the parser error for invalid JSON', () => {
51
+ const result = tryParseJson('{a:1}');
52
+ expect(result.ok).toBe(false);
53
+ if (!result.ok) {
54
+ expect(result.error.length).toBeGreaterThan(0);
55
+ }
56
+ });
57
+ });
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ export interface ColoredToken {
3
+ text: string;
4
+ color?: string;
5
+ }
6
+ export declare const tokenizeLine: (line: string) => ColoredToken[];
7
+ export declare const formatJsonText: (value: unknown) => string;
8
+ export declare const countJsonLines: (value: unknown) => number;
9
+ export declare const JsonView: React.FC<{
10
+ value: unknown;
11
+ maxLines?: number;
12
+ offset?: number;
13
+ truncateLine?: boolean;
14
+ showOverflowFooter?: boolean;
15
+ }>;