@outputai/cli 0.3.2-next.5e221e8.0 → 0.3.3-dev.422151e.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.
- package/dist/assets/docker/docker-compose-dev.yml +1 -1
- package/dist/commands/dev/index.js +48 -13
- package/dist/commands/dev/index.spec.js +1 -2
- package/dist/components/status_icon.js +1 -1
- package/dist/generated/framework_version.json +1 -1
- package/dist/services/docker.js +0 -1
- package/dist/templates/project/src/workflows/blog_evaluator/prompts/signal_noise@v1.prompt.template +2 -1
- package/dist/templates/workflow/README.md.template +2 -1
- package/dist/templates/workflow/prompts/example@v1.prompt.template +2 -1
- package/dist/utils/paths.d.ts +11 -0
- package/dist/utils/paths.js +14 -0
- package/dist/utils/scenario_resolver.js +3 -2
- package/dist/views/dev/chrome/divider.d.ts +8 -0
- package/dist/views/dev/chrome/divider.js +16 -0
- package/dist/views/dev/chrome/footer.d.ts +11 -0
- package/dist/views/dev/chrome/footer.js +10 -0
- package/dist/views/dev/chrome/header.d.ts +21 -0
- package/dist/views/dev/chrome/header.js +74 -0
- package/dist/views/dev/chrome/header.spec.d.ts +1 -0
- package/dist/views/dev/chrome/header.spec.js +50 -0
- package/dist/views/dev/chrome/loading_spinner.d.ts +9 -0
- package/dist/views/dev/chrome/loading_spinner.js +9 -0
- package/dist/views/dev/chrome/palette.d.ts +16 -0
- package/dist/views/dev/chrome/palette.js +16 -0
- package/dist/views/dev/chrome/search_bar.d.ts +5 -0
- package/dist/views/dev/chrome/search_bar.js +35 -0
- package/dist/views/dev/chrome/selection_indicator.d.ts +14 -0
- package/dist/views/dev/chrome/selection_indicator.js +13 -0
- package/dist/views/dev/chrome/tab_bar.d.ts +5 -0
- package/dist/views/dev/chrome/tab_bar.js +4 -0
- package/dist/views/dev/chrome/toasts.d.ts +2 -0
- package/dist/views/dev/chrome/toasts.js +40 -0
- package/dist/views/dev/components/master_detail_panel.d.ts +21 -0
- package/dist/views/dev/components/master_detail_panel.js +18 -0
- package/dist/views/{dev.d.ts → dev/dev_app.d.ts} +1 -0
- package/dist/views/dev/dev_app.js +146 -0
- package/dist/views/dev/hooks/use_docker_logs.d.ts +7 -0
- package/dist/views/dev/hooks/use_docker_logs.js +69 -0
- package/dist/views/dev/hooks/use_poll.d.ts +16 -0
- package/dist/views/dev/hooks/use_poll.js +95 -0
- package/dist/views/dev/hooks/use_run_detail.d.ts +21 -0
- package/dist/views/dev/hooks/use_run_detail.js +153 -0
- package/dist/views/dev/hooks/use_run_detail.spec.d.ts +1 -0
- package/dist/views/dev/hooks/use_run_detail.spec.js +86 -0
- package/dist/views/dev/hooks/use_workflow_catalog.d.ts +2 -0
- package/dist/views/dev/hooks/use_workflow_catalog.js +21 -0
- package/dist/views/dev/modals/expanded_json_modal.d.ts +2 -0
- package/dist/views/dev/modals/expanded_json_modal.js +44 -0
- package/dist/views/dev/modals/run_modal.d.ts +4 -0
- package/dist/views/dev/modals/run_modal.js +213 -0
- package/dist/views/dev/panels/help_panel.d.ts +2 -0
- package/dist/views/dev/panels/help_panel.js +53 -0
- package/dist/views/dev/panels/run_detail_view.d.ts +5 -0
- package/dist/views/dev/panels/run_detail_view.js +112 -0
- package/dist/views/dev/panels/runs_panel.d.ts +8 -0
- package/dist/views/dev/panels/runs_panel.js +204 -0
- package/dist/views/dev/panels/runs_panel.spec.d.ts +1 -0
- package/dist/views/dev/panels/runs_panel.spec.js +82 -0
- package/dist/views/dev/panels/services_panel.d.ts +14 -0
- package/dist/views/dev/panels/services_panel.js +155 -0
- package/dist/views/dev/panels/services_panel.spec.d.ts +1 -0
- package/dist/views/dev/panels/services_panel.spec.js +28 -0
- package/dist/views/dev/panels/workflows_panel.d.ts +7 -0
- package/dist/views/dev/panels/workflows_panel.js +111 -0
- package/dist/views/dev/services/docker_control.d.ts +5 -0
- package/dist/views/dev/services/docker_control.js +25 -0
- package/dist/views/dev/services/run_workflow.d.ts +10 -0
- package/dist/views/dev/services/run_workflow.js +14 -0
- package/dist/views/dev/services/scenario_io.d.ts +2 -0
- package/dist/views/dev/services/scenario_io.js +37 -0
- package/dist/views/dev/state/ui_state.d.ts +60 -0
- package/dist/views/dev/state/ui_state.js +64 -0
- package/dist/views/dev/utils/constants.d.ts +17 -0
- package/dist/views/dev/utils/constants.js +17 -0
- package/dist/views/dev/utils/json_editor.d.ts +21 -0
- package/dist/views/dev/utils/json_editor.js +117 -0
- package/dist/views/dev/utils/json_editor.spec.d.ts +1 -0
- package/dist/views/dev/utils/json_editor.spec.js +57 -0
- package/dist/views/dev/utils/json_render.d.ts +15 -0
- package/dist/views/dev/utils/json_render.js +77 -0
- package/dist/views/dev/utils/json_render.spec.d.ts +1 -0
- package/dist/views/dev/utils/json_render.spec.js +65 -0
- package/dist/views/dev/utils/panel_helpers.d.ts +16 -0
- package/dist/views/dev/utils/panel_helpers.js +32 -0
- package/dist/views/dev/utils/panel_helpers.spec.d.ts +1 -0
- package/dist/views/dev/utils/panel_helpers.spec.js +47 -0
- package/package.json +5 -5
- package/dist/components/command_footer.d.ts +0 -8
- package/dist/components/command_footer.js +0 -4
- package/dist/components/workflow_summary.d.ts +0 -10
- package/dist/components/workflow_summary.js +0 -4
- package/dist/views/dev.js +0 -187
- package/dist/views/workflow/list.d.ts +0 -6
- package/dist/views/workflow/list.js +0 -129
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { getWorkflowCatalog } from '#api/generated/api.js';
|
|
3
|
+
import { usePoll } from '#views/dev/hooks/use_poll.js';
|
|
4
|
+
const CATALOG_INTERVAL_MS = 10_000;
|
|
5
|
+
export const useWorkflowCatalog = (enabled) => {
|
|
6
|
+
const [workflows, setWorkflows] = useState([]);
|
|
7
|
+
usePoll(enabled, CATALOG_INTERVAL_MS, async () => {
|
|
8
|
+
try {
|
|
9
|
+
const response = await getWorkflowCatalog();
|
|
10
|
+
const data = response?.data;
|
|
11
|
+
if (data?.workflows) {
|
|
12
|
+
setWorkflows(data.workflows);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// API may not be ready yet
|
|
17
|
+
}
|
|
18
|
+
return 'continue';
|
|
19
|
+
});
|
|
20
|
+
return workflows;
|
|
21
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
|
+
import { useUiState } from '#views/dev/state/ui_state.js';
|
|
5
|
+
import { JsonView, countJsonLines } from '#views/dev/utils/json_render.js';
|
|
6
|
+
import { RULE_PURPLE } from '#views/dev/chrome/palette.js';
|
|
7
|
+
const CHROME_HEIGHT = 4;
|
|
8
|
+
const FALLBACK_ROWS = 24;
|
|
9
|
+
const PAGE_SIZE = 10;
|
|
10
|
+
export const ExpandedJsonModal = () => {
|
|
11
|
+
const ui = useUiState();
|
|
12
|
+
const { stdout } = useStdout();
|
|
13
|
+
const [offset, setOffset] = useState(0);
|
|
14
|
+
const { value, title } = ui.expandedJson;
|
|
15
|
+
const totalLines = countJsonLines(value);
|
|
16
|
+
const cols = stdout?.columns ?? 80;
|
|
17
|
+
const rows = stdout?.rows ?? FALLBACK_ROWS;
|
|
18
|
+
const visibleLines = Math.max(5, rows - CHROME_HEIGHT);
|
|
19
|
+
const maxOffset = Math.max(0, totalLines - visibleLines);
|
|
20
|
+
const clampedOffset = Math.min(offset, maxOffset);
|
|
21
|
+
useInput((input, key) => {
|
|
22
|
+
if (key.escape) {
|
|
23
|
+
ui.closeExpandedJson();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (key.downArrow) {
|
|
27
|
+
setOffset(o => Math.min(maxOffset, o + 1));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (key.upArrow) {
|
|
31
|
+
setOffset(o => Math.max(0, o - 1));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (key.pageDown) {
|
|
35
|
+
setOffset(o => Math.min(maxOffset, o + PAGE_SIZE));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (key.pageUp) {
|
|
39
|
+
setOffset(o => Math.max(0, o - PAGE_SIZE));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
const progress = totalLines === 0 ? 100 : Math.round(((clampedOffset + visibleLines) / totalLines) * 100);
|
|
43
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { bold: true, children: ["\u2922 ", title] }), _jsxs(Text, { dimColor: true, children: [Math.min(100, progress), "% line ", clampedOffset + 1, "-", Math.min(totalLines, clampedOffset + visibleLines), "/", totalLines] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: RULE_PURPLE, children: '─'.repeat(Math.max(1, cols)) }) }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(JsonView, { value: value, maxLines: visibleLines, offset: clampedOffset, truncateLine: true, showOverflowFooter: false }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: RULE_PURPLE, children: '─'.repeat(Math.max(1, cols)) }) }), _jsxs(Box, { columnGap: 2, children: [_jsxs(Box, { columnGap: 1, children: [_jsx(Text, { bold: true, children: "\u2191/\u2193" }), _jsx(Text, { dimColor: true, children: "scroll" })] }), _jsxs(Box, { columnGap: 1, children: [_jsx(Text, { bold: true, children: "pgup/pgdn" }), _jsx(Text, { dimColor: true, children: "page" })] }), _jsxs(Box, { columnGap: 1, children: [_jsx(Text, { bold: true, children: "esc" }), _jsx(Text, { dimColor: true, children: "close" })] })] })] }));
|
|
44
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useMemo, useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
import { listScenariosForWorkflow } from '#utils/scenario_resolver.js';
|
|
6
|
+
import { SelectionIndicator } from '#views/dev/chrome/selection_indicator.js';
|
|
7
|
+
import { useUiState } from '#views/dev/state/ui_state.js';
|
|
8
|
+
import { startWorkflow } from '#views/dev/services/run_workflow.js';
|
|
9
|
+
import { readScenario, writeScenario } from '#views/dev/services/scenario_io.js';
|
|
10
|
+
import { JsonEditor } from '#views/dev/utils/json_editor.js';
|
|
11
|
+
const CUSTOM_SEED = { '': '' };
|
|
12
|
+
const SCENARIO_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
13
|
+
const buildEntries = (scenarios) => {
|
|
14
|
+
const list = scenarios.map(s => ({
|
|
15
|
+
kind: 'scenario',
|
|
16
|
+
label: s,
|
|
17
|
+
scenarioName: s
|
|
18
|
+
}));
|
|
19
|
+
list.push({ kind: 'custom', label: 'Custom input' });
|
|
20
|
+
return list;
|
|
21
|
+
};
|
|
22
|
+
const Frame = ({ title, children }) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "round", paddingX: 1, paddingY: 0, children: [_jsx(Text, { bold: true, children: title }), children] }));
|
|
23
|
+
const TextPrompt = ({ label, value }) => (_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { children: [label, " "] }), _jsx(Text, { children: value }), _jsx(Text, { inverse: true, children: ' ' })] }));
|
|
24
|
+
export const RunModal = ({ workflowName }) => {
|
|
25
|
+
const ui = useUiState();
|
|
26
|
+
const scenarios = useMemo(() => listScenariosForWorkflow(workflowName), [workflowName]);
|
|
27
|
+
const entries = useMemo(() => buildEntries(scenarios), [scenarios]);
|
|
28
|
+
const [mode, setMode] = useState('select');
|
|
29
|
+
const [index, setIndex] = useState(0);
|
|
30
|
+
const [editName, setEditName] = useState('');
|
|
31
|
+
const [editSeed, setEditSeed] = useState(CUSTOM_SEED);
|
|
32
|
+
const [editFrameTitle, setEditFrameTitle] = useState('');
|
|
33
|
+
const [nameError, setNameError] = useState(null);
|
|
34
|
+
const [errorMessage, setErrorMessage] = useState(null);
|
|
35
|
+
const closeWith = (message, tone = 'info') => {
|
|
36
|
+
if (message) {
|
|
37
|
+
ui.pushToast(message, tone);
|
|
38
|
+
}
|
|
39
|
+
ui.closeRunModal();
|
|
40
|
+
};
|
|
41
|
+
const submit = async (input, label) => {
|
|
42
|
+
setMode('submitting');
|
|
43
|
+
try {
|
|
44
|
+
const started = await startWorkflow({ workflowName, input });
|
|
45
|
+
const id = started.workflowId ?? '?';
|
|
46
|
+
ui.setSearchQuery(workflowName);
|
|
47
|
+
ui.setTab('runs');
|
|
48
|
+
closeWith(`Started ${workflowName} (${label}) — ${id}`, 'success');
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
setErrorMessage(err instanceof Error ? err.message : String(err));
|
|
52
|
+
setMode('error');
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const runScenario = async (scenarioName) => {
|
|
56
|
+
try {
|
|
57
|
+
const input = await readScenario(workflowName, scenarioName);
|
|
58
|
+
await submit(input, scenarioName);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
setErrorMessage(err instanceof Error ? err.message : String(err));
|
|
62
|
+
setMode('error');
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const startDuplicate = async (scenarioName) => {
|
|
66
|
+
try {
|
|
67
|
+
const sourceContent = await readScenario(workflowName, scenarioName);
|
|
68
|
+
setEditName(`${scenarioName}_copy`);
|
|
69
|
+
setEditSeed(sourceContent);
|
|
70
|
+
setEditFrameTitle(`Duplicate '${scenarioName}'`);
|
|
71
|
+
setNameError(null);
|
|
72
|
+
setMode('edit_name');
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
setErrorMessage(err instanceof Error ? err.message : String(err));
|
|
76
|
+
setMode('error');
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const startCustom = () => {
|
|
80
|
+
setEditName('');
|
|
81
|
+
setEditSeed(CUSTOM_SEED);
|
|
82
|
+
setEditFrameTitle('New scenario');
|
|
83
|
+
setNameError(null);
|
|
84
|
+
setMode('edit_name');
|
|
85
|
+
};
|
|
86
|
+
const validateName = (raw) => {
|
|
87
|
+
const name = raw.trim();
|
|
88
|
+
if (!name) {
|
|
89
|
+
return 'Scenario name cannot be empty.';
|
|
90
|
+
}
|
|
91
|
+
if (!SCENARIO_NAME_RE.test(name)) {
|
|
92
|
+
return 'Use letters, numbers, dashes, and underscores only.';
|
|
93
|
+
}
|
|
94
|
+
if (scenarios.includes(name)) {
|
|
95
|
+
return `A scenario named '${name}' already exists.`;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
};
|
|
99
|
+
const handleEditorSubmit = async (value) => {
|
|
100
|
+
const name = editName.trim();
|
|
101
|
+
const writeError = validateName(editName);
|
|
102
|
+
if (writeError) {
|
|
103
|
+
setNameError(writeError);
|
|
104
|
+
setMode('edit_name');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
setMode('submitting');
|
|
108
|
+
try {
|
|
109
|
+
const writtenPath = await writeScenario(workflowName, name, value);
|
|
110
|
+
ui.pushToast(`Saved scenario at ${writtenPath}`, 'info');
|
|
111
|
+
await submit(value, name);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
setErrorMessage(err instanceof Error ? err.message : String(err));
|
|
115
|
+
setMode('error');
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const handleEditorCancel = () => {
|
|
119
|
+
// Bring the user back to the name step so they can adjust it or bail.
|
|
120
|
+
setMode('edit_name');
|
|
121
|
+
};
|
|
122
|
+
useInput((input, key) => {
|
|
123
|
+
if (mode === 'edit_content' || mode === 'submitting') {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (mode === 'select') {
|
|
127
|
+
if (key.escape) {
|
|
128
|
+
closeWith();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (key.upArrow) {
|
|
132
|
+
setIndex(i => Math.max(0, i - 1));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (key.downArrow) {
|
|
136
|
+
setIndex(i => Math.min(entries.length - 1, i + 1));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (key.return) {
|
|
140
|
+
const entry = entries[index];
|
|
141
|
+
if (entry?.kind === 'scenario' && entry.scenarioName) {
|
|
142
|
+
void runScenario(entry.scenarioName);
|
|
143
|
+
}
|
|
144
|
+
else if (entry?.kind === 'custom') {
|
|
145
|
+
startCustom();
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (input === 'd') {
|
|
150
|
+
const entry = entries[index];
|
|
151
|
+
if (entry?.kind === 'scenario' && entry.scenarioName) {
|
|
152
|
+
void startDuplicate(entry.scenarioName);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (mode === 'edit_name') {
|
|
158
|
+
if (key.escape) {
|
|
159
|
+
setMode('select');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (key.return) {
|
|
163
|
+
const err = validateName(editName);
|
|
164
|
+
if (err) {
|
|
165
|
+
setNameError(err);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
setNameError(null);
|
|
169
|
+
setMode('edit_content');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (key.backspace || key.delete) {
|
|
173
|
+
setEditName(v => v.slice(0, -1));
|
|
174
|
+
if (nameError) {
|
|
175
|
+
setNameError(null);
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (input && !key.ctrl && !key.meta) {
|
|
180
|
+
setEditName(v => v + input);
|
|
181
|
+
if (nameError) {
|
|
182
|
+
setNameError(null);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (mode === 'error') {
|
|
188
|
+
if (key.escape || key.return) {
|
|
189
|
+
setMode('select');
|
|
190
|
+
setErrorMessage(null);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
if (mode === 'edit_content') {
|
|
195
|
+
return (_jsx(Frame, { title: `${editFrameTitle} → ${editName}.json`, children: _jsx(JsonEditor, { seed: editSeed, title: `${editName}.json`, isActive: true, onSubmit: value => {
|
|
196
|
+
void handleEditorSubmit(value);
|
|
197
|
+
}, onCancel: handleEditorCancel }) }));
|
|
198
|
+
}
|
|
199
|
+
if (mode === 'edit_name') {
|
|
200
|
+
return (_jsxs(Frame, { title: editFrameTitle, children: [_jsx(TextPrompt, { label: "Scenario name:", value: editName }), nameError ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", children: nameError }) })) : null, _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "enter" }), _jsx(Text, { children: " next " }), _jsx(Text, { dimColor: true, children: "esc" }), _jsx(Text, { children: " back" })] })] }));
|
|
201
|
+
}
|
|
202
|
+
if (mode === 'submitting') {
|
|
203
|
+
return (_jsx(Frame, { title: `Run ${workflowName}`, children: _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Starting workflow\u2026" })] }) }));
|
|
204
|
+
}
|
|
205
|
+
if (mode === 'error') {
|
|
206
|
+
return (_jsx(Frame, { title: `Run ${workflowName}`, children: _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", errorMessage ?? 'Something went wrong.'] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press enter or esc to return." }) })] }) }));
|
|
207
|
+
}
|
|
208
|
+
return (_jsxs(Frame, { title: `Run ${workflowName}`, children: [entries.length === 0 ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "No scenarios on disk. Choose Custom input." }) })) : (_jsx(Box, { flexDirection: "column", marginTop: 1, children: entries.map((entry, i) => {
|
|
209
|
+
const prev = i > 0 ? entries[i - 1] : undefined;
|
|
210
|
+
const showSeparator = prev?.kind === 'scenario' && entry.kind !== 'scenario';
|
|
211
|
+
return (_jsxs(React.Fragment, { children: [showSeparator && (_jsx(Box, { marginY: 0, children: _jsx(Text, { dimColor: true, children: '─'.repeat(40) }) })), _jsxs(Box, { children: [_jsx(SelectionIndicator, { selected: i === index }), _jsxs(Text, { bold: i === index, children: [' ', entry.label] })] })] }, `${entry.kind}-${entry.scenarioName ?? i}`));
|
|
212
|
+
}) })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "\u2191/\u2193" }), _jsx(Text, { children: " navigate " }), _jsx(Text, { dimColor: true, children: "enter" }), _jsx(Text, { children: " run " }), _jsx(Text, { dimColor: true, children: "d" }), _jsx(Text, { children: " duplicate " }), _jsx(Text, { dimColor: true, children: "esc" }), _jsx(Text, { children: " cancel" })] })] }));
|
|
213
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { config } from '#config.js';
|
|
5
|
+
import { openUrl } from '#utils/open_url.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
|
+
const DOCS_URL = 'https://docs.output.ai';
|
|
10
|
+
const KV = ({ label, value }) => (_jsxs(Box, { children: [_jsx(Box, { width: 26, children: _jsx(Text, { children: label }) }), _jsx(Text, { bold: true, wrap: "truncate-end", children: value })] }));
|
|
11
|
+
const RunFromCli = () => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Run a workflow from the CLI" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, children: "npx output workflow run blog_evaluator paulgraham_hwh" }) }), _jsx(Box, { children: _jsxs(Text, { bold: true, children: ["npx output workflow run simple --input ", '\'{"values":[1,2,3]}\''] }) }), _jsx(Box, { children: _jsx(Text, { bold: true, children: "npx output workflow run simple --input scenario.json" }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "From the TUI: open Workflows tab, hover a workflow, press " }), _jsx(Text, { bold: true, children: "r" }), _jsx(Text, { dimColor: true, children: "." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Custom input from the TUI uses an in-tui editor with live JSON validation." }) })] }));
|
|
12
|
+
const Hotkeys = () => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Hotkeys" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, bold: true, children: "Global" }) }), _jsx(KV, { label: "Switch tab", value: "tab / shift+tab / 1-4" }), _jsx(KV, { label: "Search / filter", value: "/ (esc clears, enter applies)" }), _jsx(KV, { label: "Open this help", value: "?" }), _jsx(KV, { label: "Open docs.output.ai", value: "d" }), _jsx(KV, { label: "Stop services & quit", value: "ctrl+c" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, bold: true, children: "Workflows tab" }) }), _jsx(KV, { label: "Navigate", value: "\u2191/\u2193" }), _jsx(KV, { label: "Show runs (filtered)", value: "enter" }), _jsx(KV, { label: "Run workflow", value: "r (scenario \u00B7 custom input \u00B7 duplicate)" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, bold: true, children: "Recent Runs tab" }) }), _jsx(KV, { label: "Navigate", value: "\u2191/\u2193" }), _jsx(KV, { label: "Open run detail", value: "enter (esc to go back)" }), _jsx(KV, { label: "Open in Temporal UI", value: "o" }), _jsx(KV, { label: "Switch input/output", value: "\u2190/\u2192" }), _jsx(KV, { label: "Expand JSON pane", value: "e (\u2191/\u2193 scroll, pgup/pgdn page)" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, bold: true, children: "Services tab" }) }), _jsx(KV, { label: "Navigate", value: "\u2191/\u2193" }), _jsx(KV, { label: "Restart one / all", value: "r / R" }), _jsx(KV, { label: "Pause / resume tail", value: "p" }), _jsx(KV, { label: "Clear log buffer", value: "c" }), _jsx(KV, { label: "Open service URL", value: "o" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, bold: true, children: "Run modal" }) }), _jsx(KV, { label: "Navigate", value: "\u2191/\u2193" }), _jsx(KV, { label: "Run scenario", value: "enter" }), _jsx(KV, { label: "Duplicate scenario", value: "d" }), _jsx(KV, { label: "Cancel", value: "esc" })] }));
|
|
13
|
+
const ServiceUrls = () => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Service URLs" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(KV, { label: "Temporal gRPC", value: "localhost:7233" }), _jsx(KV, { label: "Temporal UI", value: "http://localhost:8080" }), _jsx(KV, { label: "API server", value: "localhost:3001" }), _jsx(KV, { label: "Redis", value: "localhost:6379" })] })] }));
|
|
14
|
+
const UpdatingMigrating = () => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Updating / Migrating" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Update the CLI to the latest published version:" }) }), _jsx(Box, { children: _jsx(Text, { bold: true, children: "output update" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Migrate a workflow project to the SDK version this CLI ships with:" }) }), _jsx(Box, { children: _jsx(Text, { bold: true, children: "output migrate" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: "The migration walks `package.json` and project files, updates `@outputai/*` deps, and applies any code-mod steps the SDK ships with the new version." }) })] }));
|
|
15
|
+
const ClaudePlugins = () => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Claude Plugins" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: "`output init` installs the Claude Code plugins (skills, commands, agents) into your project automatically when scaffolding a new workflow." }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "To re-install or refresh the plugins after a CLI update:" }) }), _jsx(Box, { children: _jsx(Text, { bold: true, children: "output update --agents" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "This pulls the latest plugin bundle that ships with the installed CLI version." }) })] }));
|
|
16
|
+
const Troubleshooting = () => {
|
|
17
|
+
const logsCommand = `docker compose -p ${config.dockerServiceName} logs -f <service>`;
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Troubleshooting" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Worker won't start? ", _jsx(Text, { bold: true, children: "output fix" }), " rebuilds the local image."] }) }), _jsx(Box, { children: _jsxs(Text, { children: ["Tail a service log from the shell: ", _jsx(Text, { bold: true, children: logsCommand })] }) }), _jsx(Box, { children: _jsxs(Text, { children: ["Force-pull images: ", _jsx(Text, { bold: true, children: "output dev --image-pull-policy always" })] }) })] }));
|
|
19
|
+
};
|
|
20
|
+
const SECTIONS = [
|
|
21
|
+
{ id: 'cli', title: 'Run from CLI', body: RunFromCli },
|
|
22
|
+
{ id: 'hotkeys', title: 'Hotkeys', body: Hotkeys },
|
|
23
|
+
{ id: 'urls', title: 'Service URLs', body: ServiceUrls },
|
|
24
|
+
{ id: 'updating', title: 'Updating / Migrating', body: UpdatingMigrating },
|
|
25
|
+
{ id: 'claude-plugins', title: 'Claude Plugins', body: ClaudePlugins },
|
|
26
|
+
{ id: 'troubleshooting', title: 'Troubleshooting', body: Troubleshooting }
|
|
27
|
+
];
|
|
28
|
+
const HINTS = [
|
|
29
|
+
{ key: '↑/↓', label: 'navigate' },
|
|
30
|
+
{ key: 'd', label: 'docs' },
|
|
31
|
+
{ key: 'tab', label: 'next tab' },
|
|
32
|
+
{ key: 'ctrl+c', label: 'quit' }
|
|
33
|
+
];
|
|
34
|
+
export const HelpPanel = () => {
|
|
35
|
+
const ui = useUiState();
|
|
36
|
+
const [index, setIndex] = useState(0);
|
|
37
|
+
const isActive = ui.tab === 'help' && !ui.search.open && !ui.runModal.open;
|
|
38
|
+
useInput((input, key) => {
|
|
39
|
+
if (key.upArrow) {
|
|
40
|
+
setIndex(i => Math.max(0, i - 1));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (key.downArrow) {
|
|
44
|
+
setIndex(i => Math.min(SECTIONS.length - 1, i + 1));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (input === 'd') {
|
|
48
|
+
openUrl(DOCS_URL);
|
|
49
|
+
}
|
|
50
|
+
}, { isActive });
|
|
51
|
+
const Section = SECTIONS[index].body;
|
|
52
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { flexDirection: "row", children: [_jsxs(Box, { flexDirection: "column", width: 28, children: [_jsx(Text, { bold: true, children: "Help" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: SECTIONS.map((section, i) => (_jsxs(Box, { children: [_jsx(SelectionIndicator, { selected: i === index }), _jsxs(Text, { bold: i === index, dimColor: i !== index, children: [' ', section.title] })] }, section.id))) })] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "single", borderTop: false, borderBottom: false, borderRight: false, paddingLeft: 2, children: _jsx(Section, {}) })] }), _jsx(Footer, { hints: HINTS, itemCount: SECTIONS.length, itemLabel: "sections" })] }));
|
|
53
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { StatusIcon, statusColor } from '#components/status_icon.js';
|
|
5
|
+
import { formatDurationCompact, formatDate, elapsedMs } from '#utils/date_formatter.js';
|
|
6
|
+
import { Footer } from '#views/dev/chrome/footer.js';
|
|
7
|
+
import { LoadingSpinner } from '#views/dev/chrome/loading_spinner.js';
|
|
8
|
+
import { SelectionIndicator } from '#views/dev/chrome/selection_indicator.js';
|
|
9
|
+
import { useUiState } from '#views/dev/state/ui_state.js';
|
|
10
|
+
import { useRunDetail } from '#views/dev/hooks/use_run_detail.js';
|
|
11
|
+
import { JsonView } from '#views/dev/utils/json_render.js';
|
|
12
|
+
import { truncate, computeWindowStart } from '#views/dev/utils/panel_helpers.js';
|
|
13
|
+
import { RUN_DETAIL_VISIBLE_STEPS, RUN_DETAIL_PREVIEW_LINES } from '#views/dev/utils/constants.js';
|
|
14
|
+
const RIGHT_PANE_ORDER = ['input', 'output', 'meta'];
|
|
15
|
+
const cycleRightPane = (current, direction) => {
|
|
16
|
+
const idx = RIGHT_PANE_ORDER.indexOf(current);
|
|
17
|
+
const next = (idx + direction + RIGHT_PANE_ORDER.length) % RIGHT_PANE_ORDER.length;
|
|
18
|
+
return RIGHT_PANE_ORDER[next];
|
|
19
|
+
};
|
|
20
|
+
const COL = {
|
|
21
|
+
num: 6,
|
|
22
|
+
icon: 3,
|
|
23
|
+
name: 50,
|
|
24
|
+
duration: 8
|
|
25
|
+
};
|
|
26
|
+
const StepRow = ({ step, selected }) => (_jsxs(Box, { children: [_jsxs(Box, { width: COL.num, children: [_jsx(SelectionIndicator, { selected: selected }), _jsx(Text, { bold: selected, children: ` ${step.index}` })] }), _jsx(Box, { width: COL.icon, children: _jsx(StatusIcon, { status: step.status }) }), _jsx(Box, { width: COL.name, children: _jsx(Text, { bold: selected, children: truncate(step.name, COL.name - 1) }) }), _jsx(Box, { width: COL.duration, justifyContent: "flex-end", children: _jsx(Text, { dimColor: !selected, children: formatDurationCompact(step.durationMs) }) })] }));
|
|
27
|
+
const SidebarKV = ({ label, value, color }) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: label }), _jsx(Text, { color: color, wrap: "truncate-end", children: value })] }));
|
|
28
|
+
const Sidebar = ({ run, resultStatus }) => {
|
|
29
|
+
const status = resultStatus ?? run.status ?? 'unknown';
|
|
30
|
+
const duration = run.startedAt ? formatDurationCompact(elapsedMs(run.startedAt, run.completedAt)) : '-';
|
|
31
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { status: status }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: statusColor(status), children: status.toUpperCase() })] }), _jsx(SidebarKV, { label: "RUN ID", value: run.runId ?? '-' }), _jsx(SidebarKV, { label: "WORKFLOW ID", value: run.workflowId ?? '-' }), _jsx(SidebarKV, { label: "TYPE", value: run.workflowType ?? '-' }), _jsx(SidebarKV, { label: "DURATION", value: duration }), _jsx(SidebarKV, { label: "START", value: formatDate(run.startedAt) }), _jsx(SidebarKV, { label: "END", value: run.completedAt ? formatDate(run.completedAt) : '—' })] }));
|
|
32
|
+
};
|
|
33
|
+
const PaneTabs = ({ active }) => (_jsx(Box, { marginTop: 1, children: RIGHT_PANE_ORDER.map((tab, i) => (_jsx(Box, { marginRight: 2, children: tab === active ? (_jsx(Text, { inverse: true, bold: true, children: ` ${tab[0].toUpperCase()}${tab.slice(1)} ` })) : (_jsx(Text, { dimColor: true, children: `${tab[0].toUpperCase()}${tab.slice(1)}${i < RIGHT_PANE_ORDER.length - 1 ? '' : ''}` })) }, tab))) }));
|
|
34
|
+
const stepPaneValue = (step, activeTab) => {
|
|
35
|
+
if (activeTab === 'input') {
|
|
36
|
+
return step.input;
|
|
37
|
+
}
|
|
38
|
+
if (activeTab === 'output') {
|
|
39
|
+
return step.error ?? step.output;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
kind: step.kind,
|
|
43
|
+
status: step.status,
|
|
44
|
+
durationMs: step.durationMs,
|
|
45
|
+
hasError: Boolean(step.error)
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
const StepDetail = ({ step, activeTab }) => {
|
|
49
|
+
if (!step) {
|
|
50
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Select a step to see input/output." }) }));
|
|
51
|
+
}
|
|
52
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(PaneTabs, { active: activeTab }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(JsonView, { value: stepPaneValue(step, activeTab), maxLines: RUN_DETAIL_PREVIEW_LINES }) })] }));
|
|
53
|
+
};
|
|
54
|
+
const HINTS = [
|
|
55
|
+
{ key: '↑/↓', label: 'navigate' },
|
|
56
|
+
{ key: '←/→', label: 'switch pane' },
|
|
57
|
+
{ key: 'e', label: 'expand' },
|
|
58
|
+
{ key: 'esc', label: 'back' },
|
|
59
|
+
{ key: 'tab', label: 'next tab' }
|
|
60
|
+
];
|
|
61
|
+
export const RunDetailView = ({ run }) => {
|
|
62
|
+
const ui = useUiState();
|
|
63
|
+
const { result, steps, loading } = useRunDetail(run.workflowId, run.runId, run.status);
|
|
64
|
+
const [stepIndex, setStepIndex] = useState(0);
|
|
65
|
+
const isActive = ui.tab === 'runs' && ui.runsView === 'detail' && !ui.search.open && !ui.runModal.open && !ui.expandedJson.open;
|
|
66
|
+
const clamped = Math.min(stepIndex, Math.max(0, steps.length - 1));
|
|
67
|
+
const selectedStep = steps[clamped];
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (clamped !== stepIndex) {
|
|
70
|
+
setStepIndex(clamped);
|
|
71
|
+
}
|
|
72
|
+
}, [clamped, stepIndex]);
|
|
73
|
+
useInput((input, key) => {
|
|
74
|
+
if (key.escape) {
|
|
75
|
+
ui.setRunsView('list');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (key.upArrow) {
|
|
79
|
+
setStepIndex(i => Math.max(0, i - 1));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (key.downArrow) {
|
|
83
|
+
setStepIndex(i => Math.min(steps.length - 1, i + 1));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (key.leftArrow) {
|
|
87
|
+
ui.setRightPaneTab(cycleRightPane(ui.rightPaneTab, -1));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (key.rightArrow) {
|
|
91
|
+
ui.setRightPaneTab(cycleRightPane(ui.rightPaneTab, 1));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (input === 'e' && selectedStep) {
|
|
95
|
+
const content = stepPaneValue(selectedStep, ui.rightPaneTab);
|
|
96
|
+
const label = `step ${selectedStep.index}: ${selectedStep.name} → ${ui.rightPaneTab}`;
|
|
97
|
+
ui.openExpandedJson(content, label);
|
|
98
|
+
}
|
|
99
|
+
}, { isActive });
|
|
100
|
+
const windowStart = computeWindowStart(clamped, steps.length, RUN_DETAIL_VISIBLE_STEPS);
|
|
101
|
+
const visibleSteps = steps.slice(windowStart, windowStart + RUN_DETAIL_VISIBLE_STEPS);
|
|
102
|
+
const renderStepList = () => {
|
|
103
|
+
if (loading && steps.length === 0) {
|
|
104
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(LoadingSpinner, { label: "Loading steps\u2026" }) }));
|
|
105
|
+
}
|
|
106
|
+
if (steps.length === 0) {
|
|
107
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "No steps recorded for this run." }) }));
|
|
108
|
+
}
|
|
109
|
+
return (_jsxs(_Fragment, { children: [windowStart > 0 && _jsxs(Text, { dimColor: true, children: [" \u2191 ", windowStart, " more above"] }), visibleSteps.map((step, i) => (_jsx(StepRow, { step: step, selected: windowStart + i === clamped }, `${step.index}-${i}`))), windowStart + RUN_DETAIL_VISIBLE_STEPS < steps.length && (_jsxs(Text, { dimColor: true, children: [" \u2193 ", steps.length - windowStart - RUN_DETAIL_VISIBLE_STEPS, " more below"] }))] }));
|
|
110
|
+
};
|
|
111
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Recent Runs \u203A " }), _jsx(Text, { bold: true, children: run.workflowType }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { children: truncate(run.runId ?? '-', 28) })] }), _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, children: renderStepList() }), _jsx(Box, { flexDirection: "column", width: 40, borderStyle: "single", borderTop: false, borderBottom: false, borderRight: false, paddingLeft: 1, children: _jsx(Sidebar, { run: run, resultStatus: result?.status ?? null }) })] }), _jsx(StepDetail, { step: selectedStep, activeTab: ui.rightPaneTab }), _jsx(Footer, { hints: HINTS, itemCount: steps.length, itemLabel: "steps" })] }));
|
|
112
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { WorkflowRun } from '#services/workflow_runs.js';
|
|
3
|
+
import type { TraceData } from '#types/trace.js';
|
|
4
|
+
export declare const buildVisibleRuns: (runs: WorkflowRun[], query: string) => WorkflowRun[];
|
|
5
|
+
export declare const extractRunInput: (trace: TraceData | null) => unknown;
|
|
6
|
+
export declare const RunsPanel: React.FC<{
|
|
7
|
+
runs: WorkflowRun[];
|
|
8
|
+
}>;
|