@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,153 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { getWorkflowIdResult, getWorkflowIdRunsRidResult, getWorkflowIdTraceLog, getWorkflowIdRunsRidTraceLog } from '#api/generated/api.js';
4
+ const EMPTY_DETAIL = {
5
+ result: null,
6
+ trace: null,
7
+ steps: [],
8
+ loading: false
9
+ };
10
+ const stepNameOf = (node) => {
11
+ if (node.name) {
12
+ return node.name;
13
+ }
14
+ const kind = node.kind || node.type || 'step';
15
+ const leaf = node.stepName ?? node.activityName ?? '?';
16
+ return `${kind}#${leaf}`;
17
+ };
18
+ const stepStatusOf = (node) => {
19
+ if (node.status) {
20
+ return node.status;
21
+ }
22
+ if (node.phase === 'error' || node.error) {
23
+ return 'failed';
24
+ }
25
+ if (node.phase === 'end') {
26
+ return 'completed';
27
+ }
28
+ return 'running';
29
+ };
30
+ const numericTimestamp = (...candidates) => {
31
+ for (const candidate of candidates) {
32
+ if (typeof candidate === 'number') {
33
+ return candidate;
34
+ }
35
+ }
36
+ return null;
37
+ };
38
+ const stepDurationOf = (node) => {
39
+ if (typeof node.duration === 'number') {
40
+ return node.duration;
41
+ }
42
+ const start = numericTimestamp(node.startTime, node.startedAt);
43
+ const end = numericTimestamp(node.endTime, node.endedAt);
44
+ if (start !== null && end !== null) {
45
+ return end - start;
46
+ }
47
+ return 0;
48
+ };
49
+ export const extractSteps = (trace) => {
50
+ if (!trace?.children) {
51
+ return [];
52
+ }
53
+ return trace.children
54
+ .filter(node => node.phase !== 'start')
55
+ .map((node, idx) => ({
56
+ index: idx + 1,
57
+ name: stepNameOf(node),
58
+ kind: node.kind || node.type || 'step',
59
+ status: stepStatusOf(node),
60
+ durationMs: stepDurationOf(node),
61
+ input: node.input ?? node.details?.input,
62
+ output: node.output ?? node.details?.output,
63
+ error: node.error
64
+ }));
65
+ };
66
+ const readTraceLog = async (source) => {
67
+ if (source.source === 'remote') {
68
+ return source.data;
69
+ }
70
+ const content = await readFile(source.localPath, 'utf-8');
71
+ return JSON.parse(content);
72
+ };
73
+ // Run detail and trace fetches are best-effort. Many statuses (in-progress,
74
+ // failed, canceled) don't have a fully-formed result or trace available at
75
+ // any given moment, and that's expected — the caller falls back to
76
+ // EMPTY_DETAIL and the UI renders whatever's there. Swallow everything.
77
+ const fetchTrace = async (workflowId, runId) => {
78
+ try {
79
+ const response = runId ?
80
+ await getWorkflowIdRunsRidTraceLog(workflowId, runId) :
81
+ await getWorkflowIdTraceLog(workflowId);
82
+ return await readTraceLog(response.data);
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ };
88
+ const fetchResult = async (workflowId, runId) => {
89
+ try {
90
+ const response = runId ?
91
+ await getWorkflowIdRunsRidResult(workflowId, runId) :
92
+ await getWorkflowIdResult(workflowId);
93
+ return response.data;
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ };
99
+ /**
100
+ * Statuses that mean the workflow has stopped advancing. The cache is
101
+ * intentionally only populated for these — partial results from a still-
102
+ * running workflow would otherwise stick and stall the UI when the run
103
+ * eventually finishes.
104
+ */
105
+ const TERMINAL_STATUSES = new Set(['completed', 'failed', 'canceled', 'terminated', 'timed_out']);
106
+ export const isTerminalRunStatus = (status) => Boolean(status && TERMINAL_STATUSES.has(status));
107
+ export const useRunDetail = (workflowId, runId, status) => {
108
+ const [detail, setDetail] = useState(EMPTY_DETAIL);
109
+ const cacheRef = useRef(new Map());
110
+ const fetchIdRef = useRef(0);
111
+ useEffect(() => {
112
+ if (!workflowId) {
113
+ setDetail(EMPTY_DETAIL);
114
+ return;
115
+ }
116
+ // The cache only ever holds terminal-status entries (see below), so
117
+ // a hit here is always safe to reuse without a network roundtrip.
118
+ const key = `${workflowId}:${runId ?? 'latest'}`;
119
+ const cached = cacheRef.current.get(key);
120
+ if (cached) {
121
+ setDetail(cached);
122
+ return;
123
+ }
124
+ const id = ++fetchIdRef.current;
125
+ setDetail({ ...EMPTY_DETAIL, loading: true });
126
+ void Promise.all([
127
+ fetchResult(workflowId, runId),
128
+ fetchTrace(workflowId, runId)
129
+ ]).then(([result, trace]) => {
130
+ if (fetchIdRef.current !== id) {
131
+ return;
132
+ }
133
+ const next = {
134
+ result,
135
+ trace,
136
+ steps: extractSteps(trace),
137
+ loading: false
138
+ };
139
+ // Only memoize the result once the workflow is done. While it's
140
+ // still running the API returns partial data; a follow-up status
141
+ // change re-fires this effect and we re-fetch fresh.
142
+ if (isTerminalRunStatus(result?.status)) {
143
+ cacheRef.current.set(key, next);
144
+ }
145
+ setDetail(next);
146
+ });
147
+ // `status` is in the dep array so a run flipping running → completed
148
+ // (the runs list polls every 2s) re-fires the effect and pulls the
149
+ // fresh output, instead of pinning to the partial result captured
150
+ // mid-run.
151
+ }, [workflowId, runId, status]);
152
+ return detail;
153
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { extractSteps, isTerminalRunStatus } from './use_run_detail.js';
3
+ describe('isTerminalRunStatus', () => {
4
+ it('returns true for completed states', () => {
5
+ expect(isTerminalRunStatus('completed')).toBe(true);
6
+ expect(isTerminalRunStatus('failed')).toBe(true);
7
+ expect(isTerminalRunStatus('canceled')).toBe(true);
8
+ expect(isTerminalRunStatus('terminated')).toBe(true);
9
+ expect(isTerminalRunStatus('timed_out')).toBe(true);
10
+ });
11
+ it('returns false for in-progress states', () => {
12
+ expect(isTerminalRunStatus('running')).toBe(false);
13
+ expect(isTerminalRunStatus('continued')).toBe(false);
14
+ });
15
+ it('returns false for nullish input', () => {
16
+ expect(isTerminalRunStatus(null)).toBe(false);
17
+ expect(isTerminalRunStatus(undefined)).toBe(false);
18
+ expect(isTerminalRunStatus('')).toBe(false);
19
+ });
20
+ });
21
+ const trace = (children) => ({
22
+ root: { workflowName: 'demo', workflowId: 'wf-1', startTime: 0 },
23
+ children
24
+ });
25
+ describe('extractSteps', () => {
26
+ it('returns an empty array when the trace has no children', () => {
27
+ expect(extractSteps(null)).toEqual([]);
28
+ expect(extractSteps(trace([]))).toEqual([]);
29
+ });
30
+ it('filters out start-phase events', () => {
31
+ const t = trace([
32
+ { phase: 'start', name: 'should-skip' },
33
+ { phase: 'end', name: 'kept', status: 'completed', duration: 100 }
34
+ ]);
35
+ const steps = extractSteps(t);
36
+ expect(steps).toHaveLength(1);
37
+ expect(steps[0].name).toBe('kept');
38
+ });
39
+ it('maps phase=error and an error field to status=failed', () => {
40
+ const t = trace([
41
+ { phase: 'error', name: 'boom', error: 'oops' },
42
+ { phase: 'end', name: 'fine', error: 'still-failed' }
43
+ ]);
44
+ const steps = extractSteps(t);
45
+ expect(steps[0].status).toBe('failed');
46
+ expect(steps[1].status).toBe('failed');
47
+ });
48
+ it('falls back to startTime/endTime for duration', () => {
49
+ const t = trace([
50
+ { phase: 'end', name: 'with-times', startTime: 1000, endTime: 1500 }
51
+ ]);
52
+ const steps = extractSteps(t);
53
+ expect(steps[0].durationMs).toBe(500);
54
+ });
55
+ it('prefers explicit duration when present', () => {
56
+ const t = trace([
57
+ { phase: 'end', name: 'has-duration', duration: 250, startTime: 0, endTime: 9999 }
58
+ ]);
59
+ const steps = extractSteps(t);
60
+ expect(steps[0].durationMs).toBe(250);
61
+ });
62
+ it('composes a fallback name from kind and stepName when name is missing', () => {
63
+ const t = trace([
64
+ { phase: 'end', kind: 'activity', stepName: 'extract', status: 'completed' }
65
+ ]);
66
+ const steps = extractSteps(t);
67
+ expect(steps[0].name).toBe('activity#extract');
68
+ });
69
+ it('reads input/output from `details` when not on the node directly', () => {
70
+ const t = trace([
71
+ { phase: 'end', name: 'has-details', details: { input: { x: 1 }, output: { y: 2 } } }
72
+ ]);
73
+ const steps = extractSteps(t);
74
+ expect(steps[0].input).toEqual({ x: 1 });
75
+ expect(steps[0].output).toEqual({ y: 2 });
76
+ });
77
+ it('numbers steps starting at 1', () => {
78
+ const t = trace([
79
+ { phase: 'end', name: 'first' },
80
+ { phase: 'end', name: 'second' },
81
+ { phase: 'end', name: 'third' }
82
+ ]);
83
+ const steps = extractSteps(t);
84
+ expect(steps.map(s => s.index)).toEqual([1, 2, 3]);
85
+ });
86
+ });
@@ -0,0 +1,2 @@
1
+ import { type Workflow } from '#api/generated/api.js';
2
+ export declare const useWorkflowCatalog: (enabled: boolean) => Workflow[];
@@ -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,2 @@
1
+ import React from 'react';
2
+ export declare const ExpandedJsonModal: React.FC;
@@ -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,5 @@
1
+ import React from 'react';
2
+ export declare const RunModal: React.FC<{
3
+ workflowName: string;
4
+ workflowPath?: string;
5
+ }>;
@@ -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, workflowPath }) => {
25
+ const ui = useUiState();
26
+ const scenarios = useMemo(() => listScenariosForWorkflow(workflowName, workflowPath), [workflowName, workflowPath]);
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, workflowPath);
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, workflowPath);
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, workflowPath);
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,2 @@
1
+ import React from 'react';
2
+ export declare const HelpPanel: React.FC;
@@ -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,5 @@
1
+ import React from 'react';
2
+ import type { WorkflowRun } from '#services/workflow_runs.js';
3
+ export declare const RunDetailView: React.FC<{
4
+ run: WorkflowRun;
5
+ }>;