@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,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
+ }>;
@@ -0,0 +1,204 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } 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, formatDate } from '#utils/date_formatter.js';
6
+ import { openUrl } from '#utils/open_url.js';
7
+ import { Footer } from '#views/dev/chrome/footer.js';
8
+ import { LoadingSpinner } from '#views/dev/chrome/loading_spinner.js';
9
+ import { SelectionIndicator } from '#views/dev/chrome/selection_indicator.js';
10
+ import { useUiState } from '#views/dev/state/ui_state.js';
11
+ import { RunDetailView } from '#views/dev/panels/run_detail_view.js';
12
+ import { useRunDetail } from '#views/dev/hooks/use_run_detail.js';
13
+ import { JsonView } from '#views/dev/utils/json_render.js';
14
+ import { MasterDetailPanel } from '#views/dev/components/master_detail_panel.js';
15
+ import { truncate, formatStartedShort } from '#views/dev/utils/panel_helpers.js';
16
+ import { CATALOG_WORKFLOW_NAME, RUNS_VISIBLE_ROWS, RUNS_PREVIEW_LINES } from '#views/dev/utils/constants.js';
17
+ const TEMPORAL_UI_BASE = 'http://localhost:8080';
18
+ const STATUS_ORDER = {
19
+ running: 0,
20
+ failed: 1,
21
+ timed_out: 2,
22
+ terminated: 3,
23
+ canceled: 4,
24
+ continued: 5,
25
+ completed: 6
26
+ };
27
+ const sortRuns = (runs) => [...runs].sort((a, b) => {
28
+ const statusDiff = (STATUS_ORDER[a.status ?? ''] ?? Infinity) - (STATUS_ORDER[b.status ?? ''] ?? Infinity);
29
+ if (statusDiff !== 0) {
30
+ return statusDiff;
31
+ }
32
+ const aTime = a.startedAt ? new Date(a.startedAt).getTime() : 0;
33
+ const bTime = b.startedAt ? new Date(b.startedAt).getTime() : 0;
34
+ return bTime - aTime;
35
+ });
36
+ const matchesFilter = (run, query) => {
37
+ if (!query) {
38
+ return true;
39
+ }
40
+ const q = query.toLowerCase();
41
+ return (run.workflowType ?? '').toLowerCase().includes(q) ||
42
+ (run.workflowId ?? '').toLowerCase().includes(q) ||
43
+ (run.status ?? '').toLowerCase().includes(q);
44
+ };
45
+ export const buildVisibleRuns = (runs, query) => {
46
+ const visible = runs.filter(r => !(r.workflowType === CATALOG_WORKFLOW_NAME && r.status === 'completed'));
47
+ const filtered = query ? visible.filter(r => matchesFilter(r, query)) : visible;
48
+ return sortRuns(filtered);
49
+ };
50
+ export const extractRunInput = (trace) => {
51
+ if (!trace?.children) {
52
+ return null;
53
+ }
54
+ const firstChild = trace.children[0];
55
+ if (!firstChild) {
56
+ return null;
57
+ }
58
+ if (firstChild.input !== undefined) {
59
+ return firstChild.input;
60
+ }
61
+ return firstChild.details?.input ?? null;
62
+ };
63
+ const COL = {
64
+ indicator: 3,
65
+ icon: 3,
66
+ status: 11,
67
+ type: 22,
68
+ id: 26,
69
+ duration: 9,
70
+ started: 14
71
+ };
72
+ const HeaderRow = () => (_jsxs(Box, { children: [_jsx(Box, { width: COL.indicator, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: COL.icon, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: COL.status, children: _jsx(Text, { dimColor: true, bold: true, children: "STATUS" }) }), _jsx(Box, { width: COL.type, children: _jsx(Text, { dimColor: true, bold: true, children: "TYPE" }) }), _jsx(Box, { width: COL.id, children: _jsx(Text, { dimColor: true, bold: true, children: "ID" }) }), _jsx(Box, { width: COL.duration, justifyContent: "flex-end", children: _jsx(Text, { dimColor: true, bold: true, children: "DURATION" }) }), _jsx(Box, { width: COL.started, marginLeft: 2, children: _jsx(Text, { dimColor: true, bold: true, children: "STARTED" }) })] }));
73
+ const RunRow = ({ run, selected }) => {
74
+ const status = run.status ?? 'running';
75
+ const color = statusColor(status);
76
+ const duration = run.startedAt ? formatDurationCompact(elapsedMs(run.startedAt, run.completedAt)) : '-';
77
+ return (_jsxs(Box, { children: [_jsx(Box, { width: COL.indicator, children: _jsx(SelectionIndicator, { selected: selected }) }), _jsx(Box, { width: COL.icon, children: _jsx(StatusIcon, { status: status }) }), _jsx(Box, { width: COL.status, children: _jsx(Text, { color: color, children: status }) }), _jsx(Box, { width: COL.type, children: _jsx(Text, { bold: selected, children: truncate(run.workflowType ?? '-', COL.type - 1) }) }), _jsx(Box, { width: COL.id, children: _jsx(Text, { dimColor: !selected, children: truncate(run.workflowId ?? '-', COL.id - 1) }) }), _jsx(Box, { width: COL.duration, justifyContent: "flex-end", children: _jsx(Text, { dimColor: !selected, children: duration }) }), _jsx(Box, { width: COL.started, marginLeft: 2, children: _jsx(Text, { dimColor: !selected, children: formatStartedShort(run.startedAt) }) })] }));
78
+ };
79
+ const PaneTabs = ({ active }) => (_jsx(Box, { children: ['input', 'output'].map((tab, i) => (_jsx(Box, { marginRight: i === 0 ? 1 : 0, 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)} ` })) }, tab))) }));
80
+ const InlineKV = ({ label, value }) => (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [label, ": "] }), _jsx(Text, { children: value })] }));
81
+ const DetailPane = ({ run, pane }) => {
82
+ const ui = useUiState();
83
+ if (!run || !pane) {
84
+ return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Select a run to see details." }) }));
85
+ }
86
+ const { input: runInput, output: runOutput, error: runError, status, loading } = pane;
87
+ const duration = run.startedAt ? formatDurationCompact(elapsedMs(run.startedAt, run.completedAt)) : '-';
88
+ const activePane = ui.rightPaneTab === 'input' ? 'input' : 'output';
89
+ const renderPane = () => {
90
+ if (activePane === 'input') {
91
+ if (loading && runInput === null) {
92
+ return _jsx(LoadingSpinner, {});
93
+ }
94
+ return _jsx(JsonView, { value: runInput, maxLines: RUNS_PREVIEW_LINES });
95
+ }
96
+ if (runError) {
97
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", bold: true, children: "ERROR" }), _jsx(Text, { color: "red", wrap: "wrap", children: truncate(String(runError), 400) })] }));
98
+ }
99
+ if (runOutput === undefined || runOutput === null) {
100
+ if (loading) {
101
+ return _jsx(LoadingSpinner, {});
102
+ }
103
+ return _jsx(Text, { dimColor: true, children: "No output yet." });
104
+ }
105
+ return _jsx(JsonView, { value: runOutput, maxLines: RUNS_PREVIEW_LINES });
106
+ };
107
+ const heading = `${run.workflowType ?? 'run'} : ${run.runId ?? run.workflowId ?? '-'}`;
108
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsx(Text, { bold: true, children: heading }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(StatusIcon, { status: status }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: statusColor(status), children: status.toUpperCase() }), _jsx(Text, { dimColor: true, children: " " }), _jsx(InlineKV, { label: "DURATION", value: duration }), _jsx(Text, { dimColor: true, children: " " }), _jsx(InlineKV, { label: "STARTED", value: formatDate(run.startedAt) }), _jsx(Text, { dimColor: true, children: " " }), _jsx(InlineKV, { label: "COMPLETED", value: run.completedAt ? formatDate(run.completedAt) : '—' })] }), _jsx(Box, { marginTop: 1, children: _jsx(PaneTabs, { active: activePane }) }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: renderPane() })] }));
109
+ };
110
+ const HINTS = [
111
+ { key: '↑/↓', label: 'navigate' },
112
+ { key: 'enter', label: 'open' },
113
+ { key: '←/→', label: 'switch pane' },
114
+ { key: 'e', label: 'expand' },
115
+ { key: 'o', label: 'temporal' },
116
+ { key: '/', label: 'filter' },
117
+ { key: 'tab', label: 'next tab' }
118
+ ];
119
+ export const RunsPanel = ({ runs }) => {
120
+ const ui = useUiState();
121
+ const filteredRuns = useMemo(() => buildVisibleRuns(runs, ui.search.query), [runs, ui.search.query]);
122
+ // Lazy initializer — runs once on mount. Restores the previously selected
123
+ // run after the expanded-JSON modal unmounts and remounts the panel.
124
+ const [selectedIndex, setSelectedIndex] = useState(() => {
125
+ const previousRunId = ui.selection.runId;
126
+ if (!previousRunId) {
127
+ return 0;
128
+ }
129
+ const initial = buildVisibleRuns(runs, ui.search.query);
130
+ const i = initial.findIndex(r => r.runId === previousRunId);
131
+ return i >= 0 ? i : 0;
132
+ });
133
+ const isActive = ui.tab === 'runs' && ui.runsView === 'list' && !ui.search.open && !ui.runModal.open && !ui.expandedJson.open;
134
+ const clampedIndex = Math.min(selectedIndex, Math.max(0, filteredRuns.length - 1));
135
+ const selectedRun = filteredRuns[clampedIndex];
136
+ const { result, trace, loading } = useRunDetail(selectedRun?.workflowId, selectedRun?.runId, selectedRun?.status);
137
+ const pane = selectedRun ? {
138
+ input: extractRunInput(trace),
139
+ output: result?.output,
140
+ error: result?.error,
141
+ status: result?.status ?? selectedRun.status ?? 'unknown',
142
+ loading
143
+ } : null;
144
+ useEffect(() => {
145
+ if (clampedIndex !== selectedIndex) {
146
+ setSelectedIndex(clampedIndex);
147
+ }
148
+ }, [clampedIndex, selectedIndex]);
149
+ const setSelection = ui.setSelection;
150
+ useEffect(() => {
151
+ setSelection({
152
+ runId: selectedRun?.runId,
153
+ workflowId: selectedRun?.workflowId,
154
+ workflowName: selectedRun?.workflowType
155
+ });
156
+ }, [selectedRun?.runId, selectedRun?.workflowId, selectedRun?.workflowType, setSelection]);
157
+ useInput((input, key) => {
158
+ if (key.upArrow) {
159
+ setSelectedIndex(i => Math.max(0, i - 1));
160
+ return;
161
+ }
162
+ if (key.downArrow) {
163
+ setSelectedIndex(i => Math.min(filteredRuns.length - 1, i + 1));
164
+ return;
165
+ }
166
+ if (input === 'o' && selectedRun?.workflowId) {
167
+ openUrl(`${TEMPORAL_UI_BASE}/namespaces/default/workflows/${selectedRun.workflowId}`);
168
+ return;
169
+ }
170
+ if (key.return && selectedRun?.workflowId) {
171
+ ui.setRunsView('detail');
172
+ return;
173
+ }
174
+ if (key.leftArrow || key.rightArrow) {
175
+ ui.setRightPaneTab(ui.rightPaneTab === 'input' ? 'output' : 'input');
176
+ return;
177
+ }
178
+ if (input === 'e' && pane) {
179
+ const content = ui.rightPaneTab === 'input' ?
180
+ pane.input :
181
+ (pane.error ?? pane.output);
182
+ const label = `${selectedRun?.workflowType ?? 'run'} → ${ui.rightPaneTab}`;
183
+ ui.openExpandedJson(content, label);
184
+ }
185
+ }, { isActive });
186
+ const detailRun = ui.runsView === 'detail' ?
187
+ (runs.find(r => r.runId === ui.selection.runId && r.workflowId === ui.selection.workflowId) ?? selectedRun) :
188
+ undefined;
189
+ useEffect(() => {
190
+ if (ui.runsView === 'detail' && !detailRun) {
191
+ ui.setRunsView('list');
192
+ }
193
+ }, [ui, detailRun]);
194
+ if (ui.runsView === 'detail' && detailRun) {
195
+ return _jsx(RunDetailView, { run: detailRun });
196
+ }
197
+ if (runs.length === 0) {
198
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Recent Runs" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "No runs yet. Trigger one from the Workflows tab or with `output workflow run \u2026`." }) }), _jsx(Footer, { hints: [{ key: 'tab', label: 'next tab' }, { key: '?', label: 'help' }] })] }));
199
+ }
200
+ if (filteredRuns.length === 0) {
201
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Recent Runs" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: ["No runs match `", ui.search.query, "`. Press "] }), _jsx(Text, { bold: true, children: "esc" }), _jsx(Text, { dimColor: true, children: " to clear the filter." })] }), _jsx(Footer, { hints: HINTS, itemCount: 0, itemLabel: "runs" })] }));
202
+ }
203
+ return (_jsx(MasterDetailPanel, { items: filteredRuns, selectedIndex: clampedIndex, visibleRows: RUNS_VISIBLE_ROWS, renderHeader: () => _jsx(HeaderRow, {}), renderRow: (run, selected) => _jsx(RunRow, { run: run, selected: selected }), rowKey: (run, i) => `${run.workflowId}-${run.runId ?? run.startedAt}-${i}`, detail: _jsx(DetailPane, { run: selectedRun, pane: pane }), hints: HINTS, itemLabel: "runs" }));
204
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildVisibleRuns, extractRunInput } from './runs_panel.js';
3
+ const run = (overrides) => ({
4
+ workflowId: 'wf',
5
+ workflowType: 'demo',
6
+ status: 'completed',
7
+ startedAt: '2026-04-28T18:56:53Z',
8
+ completedAt: '2026-04-28T18:56:57Z',
9
+ ...overrides
10
+ });
11
+ describe('buildVisibleRuns', () => {
12
+ it('drops completed $catalog rows', () => {
13
+ const runs = [
14
+ run({ workflowType: '$catalog', status: 'completed' }),
15
+ run({ workflowType: 'demo', status: 'completed' })
16
+ ];
17
+ const visible = buildVisibleRuns(runs, '');
18
+ expect(visible.map(r => r.workflowType)).toEqual(['demo']);
19
+ });
20
+ it('keeps non-completed $catalog rows for diagnostics', () => {
21
+ const runs = [
22
+ run({ workflowType: '$catalog', status: 'running' }),
23
+ run({ workflowType: '$catalog', status: 'failed' })
24
+ ];
25
+ const visible = buildVisibleRuns(runs, '');
26
+ expect(visible).toHaveLength(2);
27
+ });
28
+ it('sorts running before failed before completed', () => {
29
+ const runs = [
30
+ run({ workflowType: 'a', status: 'completed' }),
31
+ run({ workflowType: 'b', status: 'running' }),
32
+ run({ workflowType: 'c', status: 'failed' })
33
+ ];
34
+ const visible = buildVisibleRuns(runs, '');
35
+ expect(visible.map(r => r.status)).toEqual(['running', 'failed', 'completed']);
36
+ });
37
+ it('sorts within the same status by startedAt descending', () => {
38
+ const runs = [
39
+ run({ workflowId: 'old', startedAt: '2026-04-01T00:00:00Z' }),
40
+ run({ workflowId: 'new', startedAt: '2026-04-28T00:00:00Z' })
41
+ ];
42
+ const visible = buildVisibleRuns(runs, '');
43
+ expect(visible.map(r => r.workflowId)).toEqual(['new', 'old']);
44
+ });
45
+ it('filters by query against workflowType, workflowId, and status', () => {
46
+ const runs = [
47
+ run({ workflowType: 'apple', workflowId: 'wf-1' }),
48
+ run({ workflowType: 'banana', workflowId: 'wf-2' })
49
+ ];
50
+ expect(buildVisibleRuns(runs, 'apple')).toHaveLength(1);
51
+ expect(buildVisibleRuns(runs, 'wf-2')).toHaveLength(1);
52
+ expect(buildVisibleRuns(runs, 'completed')).toHaveLength(2);
53
+ expect(buildVisibleRuns(runs, 'no-match')).toHaveLength(0);
54
+ });
55
+ });
56
+ describe('extractRunInput', () => {
57
+ it('returns null when the trace has no children', () => {
58
+ expect(extractRunInput(null)).toBeNull();
59
+ expect(extractRunInput({ root: { workflowName: 'x', workflowId: 'y', startTime: 0 }, children: [] })).toBeNull();
60
+ });
61
+ it('reads from the first child input field directly', () => {
62
+ const trace = {
63
+ root: { workflowName: 'x', workflowId: 'y', startTime: 0 },
64
+ children: [{ input: { foo: 1 } }]
65
+ };
66
+ expect(extractRunInput(trace)).toEqual({ foo: 1 });
67
+ });
68
+ it('falls back to details.input when the top-level input is missing', () => {
69
+ const trace = {
70
+ root: { workflowName: 'x', workflowId: 'y', startTime: 0 },
71
+ children: [{ details: { input: { bar: 2 } } }]
72
+ };
73
+ expect(extractRunInput(trace)).toEqual({ bar: 2 });
74
+ });
75
+ it('returns null when neither input source is set', () => {
76
+ const trace = {
77
+ root: { workflowName: 'x', workflowId: 'y', startTime: 0 },
78
+ children: [{ name: 'first-step' }]
79
+ };
80
+ expect(extractRunInput(trace)).toBeNull();
81
+ });
82
+ });
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { type ServiceStatus } from '#services/docker.js';
3
+ import type { Phase } from '#views/dev/dev_app.js';
4
+ /**
5
+ * Worker first (most-watched in development), then API, then everything
6
+ * else alphabetically. Promoting the priority list to a constant keeps
7
+ * the comparator three lines.
8
+ */
9
+ export declare const compareService: (a: ServiceStatus, b: ServiceStatus) => number;
10
+ export declare const ServicesPanel: React.FC<{
11
+ phase: Phase;
12
+ services: ServiceStatus[];
13
+ dockerComposePath: string;
14
+ }>;
@@ -0,0 +1,155 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { isServiceFailed, SERVICE_HEALTH } from '#services/docker.js';
5
+ import { StatusIcon } from '#components/status_icon.js';
6
+ import { openUrl } from '#utils/open_url.js';
7
+ import { Footer } from '#views/dev/chrome/footer.js';
8
+ import { LoadingSpinner } from '#views/dev/chrome/loading_spinner.js';
9
+ import { SelectionIndicator } from '#views/dev/chrome/selection_indicator.js';
10
+ import { useUiState } from '#views/dev/state/ui_state.js';
11
+ import { useDockerLogs } from '#views/dev/hooks/use_docker_logs.js';
12
+ import { restartService, restartStack } from '#views/dev/services/docker_control.js';
13
+ import { MasterDetailPanel } from '#views/dev/components/master_detail_panel.js';
14
+ const VISIBLE_LOG_LINES = 18;
15
+ const resolveServiceStatus = (service) => service.health === SERVICE_HEALTH.NONE ? service.state : service.health;
16
+ const SERVICE_URLS = {
17
+ 'temporal-ui': 'http://localhost:8080',
18
+ api: 'http://localhost:3001',
19
+ temporal: 'http://localhost:7233',
20
+ redis: 'redis://localhost:6379'
21
+ };
22
+ const COL = {
23
+ indicator: 3,
24
+ icon: 3,
25
+ name: 16,
26
+ status: 11,
27
+ ports: 22
28
+ };
29
+ const HeaderRow = () => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Services" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Box, { width: COL.indicator, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: COL.icon, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: COL.name, children: _jsx(Text, { dimColor: true, bold: true, children: "SERVICE" }) }), _jsx(Box, { width: COL.status, children: _jsx(Text, { dimColor: true, bold: true, children: "STATUS" }) }), _jsx(Box, { width: COL.ports, children: _jsx(Text, { dimColor: true, bold: true, children: "PORTS" }) })] })] }));
30
+ const ServiceRow = ({ service, selected }) => {
31
+ const status = resolveServiceStatus(service);
32
+ const ports = service.ports.length ? service.ports.join(', ') : '-';
33
+ return (_jsxs(Box, { children: [_jsx(Box, { width: COL.indicator, children: _jsx(SelectionIndicator, { selected: selected }) }), _jsx(Box, { width: COL.icon, children: _jsx(StatusIcon, { status: status }) }), _jsx(Box, { width: COL.name, children: _jsx(Text, { bold: selected, wrap: "truncate-end", children: service.name }) }), _jsx(Box, { width: COL.status, children: _jsx(Text, { dimColor: !selected, wrap: "truncate-end", children: status }) }), _jsx(Box, { width: COL.ports, children: _jsx(Text, { dimColor: !selected, wrap: "truncate-end", children: ports }) })] }));
34
+ };
35
+ const FailureBanner = ({ services }) => {
36
+ const failed = services.filter(isServiceFailed);
37
+ if (failed.length === 0) {
38
+ return null;
39
+ }
40
+ return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { backgroundColor: "red", color: "white", bold: true, children: [" \u26A0\uFE0F ", failed.length, " service(s) failing \u2014 see logs and press r to restart "] }) }));
41
+ };
42
+ const LogPane = ({ serviceName, lines, paused }) => {
43
+ if (!serviceName) {
44
+ return _jsx(Text, { dimColor: true, children: "Select a service to tail its logs." });
45
+ }
46
+ if (lines.length === 0) {
47
+ return _jsx(LoadingSpinner, { label: `Waiting for logs from ${serviceName}…` });
48
+ }
49
+ const visible = lines.slice(-VISIBLE_LOG_LINES);
50
+ return (_jsxs(Box, { flexDirection: "column", children: [paused && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { backgroundColor: "yellow", color: "black", children: " PAUSED " }), _jsx(Text, { dimColor: true, children: " press p to resume" })] })), visible.map((line, i) => (_jsx(Text, { wrap: "truncate-end", children: line }, `${i}-${line.length}`)))] }));
51
+ };
52
+ const SERVICE_PRIORITY = ['worker', 'api'];
53
+ /**
54
+ * Worker first (most-watched in development), then API, then everything
55
+ * else alphabetically. Promoting the priority list to a constant keeps
56
+ * the comparator three lines.
57
+ */
58
+ export const compareService = (a, b) => {
59
+ const ai = SERVICE_PRIORITY.indexOf(a.name);
60
+ const bi = SERVICE_PRIORITY.indexOf(b.name);
61
+ if (ai !== -1 || bi !== -1) {
62
+ return (ai === -1 ? Infinity : ai) - (bi === -1 ? Infinity : bi);
63
+ }
64
+ return a.name.localeCompare(b.name);
65
+ };
66
+ const HINTS = [
67
+ { key: '↑/↓', label: 'navigate' },
68
+ { key: 'r/R', label: 'restart one/all' },
69
+ { key: 'p', label: 'pause logs' },
70
+ { key: 'c', label: 'clear' },
71
+ { key: 'o', label: 'open url' },
72
+ { key: 'tab', label: 'next tab' },
73
+ { key: 'ctrl+c', label: 'stop & quit' }
74
+ ];
75
+ const HINTS_BOOT = [
76
+ { key: 'tab', label: 'next tab' },
77
+ { key: '?', label: 'help' },
78
+ { key: 'ctrl+c', label: 'quit' }
79
+ ];
80
+ const Detail = ({ service, services, lines, paused, banner }) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(FailureBanner, { services: services }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, children: service?.name ?? 'Logs' }) }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(LogPane, { serviceName: service?.name ?? null, lines: lines, paused: paused }) }), banner && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: banner }) }))] }));
81
+ export const ServicesPanel = ({ phase, services, dockerComposePath }) => {
82
+ const ui = useUiState();
83
+ const [selectedIndex, setSelectedIndex] = useState(0);
84
+ const [banner, setBanner] = useState(null);
85
+ const sortedServices = useMemo(() => [...services].sort(compareService), [services]);
86
+ const isActive = ui.tab === 'services' && !ui.search.open && !ui.runModal.open;
87
+ const clamped = Math.min(selectedIndex, Math.max(0, sortedServices.length - 1));
88
+ const selectedService = sortedServices[clamped];
89
+ const enabledLogs = isActive && phase === 'running' && Boolean(selectedService);
90
+ useEffect(() => {
91
+ if (clamped !== selectedIndex) {
92
+ setSelectedIndex(clamped);
93
+ }
94
+ }, [clamped, selectedIndex]);
95
+ const logs = useDockerLogs(dockerComposePath, selectedService?.name ?? null, enabledLogs);
96
+ useEffect(() => {
97
+ if (!banner) {
98
+ return () => { };
99
+ }
100
+ const id = setTimeout(() => setBanner(null), 3000);
101
+ return () => clearTimeout(id);
102
+ }, [banner]);
103
+ useInput((input, key) => {
104
+ if (key.upArrow) {
105
+ setSelectedIndex(i => Math.max(0, i - 1));
106
+ return;
107
+ }
108
+ if (key.downArrow) {
109
+ setSelectedIndex(i => Math.min(sortedServices.length - 1, i + 1));
110
+ return;
111
+ }
112
+ if (!selectedService) {
113
+ return;
114
+ }
115
+ if (input === 'p') {
116
+ logs.setPaused(!logs.paused);
117
+ return;
118
+ }
119
+ if (input === 'c') {
120
+ logs.clear();
121
+ return;
122
+ }
123
+ if (input === 'o') {
124
+ const url = SERVICE_URLS[selectedService.name];
125
+ if (url) {
126
+ openUrl(url);
127
+ setBanner(`Opened ${url}`);
128
+ }
129
+ else {
130
+ setBanner(`${selectedService.name} has no known URL`);
131
+ }
132
+ return;
133
+ }
134
+ if (input === 'r') {
135
+ setBanner(`Restarting ${selectedService.name}…`);
136
+ restartService(dockerComposePath, selectedService.name)
137
+ .then(() => setBanner(`Restarted ${selectedService.name}`))
138
+ .catch(err => setBanner(`Restart failed: ${err instanceof Error ? err.message : String(err)}`));
139
+ return;
140
+ }
141
+ if (input === 'R') {
142
+ setBanner('Restarting all services…');
143
+ restartStack(dockerComposePath)
144
+ .then(() => setBanner('Restarted all services'))
145
+ .catch(err => setBanner(`Restart failed: ${err instanceof Error ? err.message : String(err)}`));
146
+ }
147
+ }, { isActive });
148
+ if (phase === 'waiting' && services.length === 0) {
149
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(LoadingSpinner, { label: "Starting Docker services\u2026" }), _jsx(Footer, { hints: HINTS_BOOT })] }));
150
+ }
151
+ if (services.length === 0) {
152
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "No services running." }), _jsx(Footer, { hints: HINTS_BOOT })] }));
153
+ }
154
+ return (_jsx(MasterDetailPanel, { items: sortedServices, selectedIndex: clamped, visibleRows: sortedServices.length, renderHeader: () => _jsx(HeaderRow, {}), renderRow: (service, selected) => _jsx(ServiceRow, { service: service, selected: selected }), rowKey: service => service.name, detail: _jsx(Detail, { service: selectedService, services: services, lines: logs.lines, paused: logs.paused, banner: banner }), hints: phase === 'running' ? HINTS : HINTS_BOOT, itemLabel: "services" }));
155
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { compareService } from './services_panel.js';
3
+ const svc = (name) => ({
4
+ name,
5
+ state: 'running',
6
+ health: 'healthy',
7
+ ports: []
8
+ });
9
+ describe('compareService', () => {
10
+ it('puts worker before api', () => {
11
+ expect(compareService(svc('worker'), svc('api'))).toBeLessThan(0);
12
+ });
13
+ it('puts api before alphabetically-earlier names', () => {
14
+ expect(compareService(svc('api'), svc('aardvark'))).toBeLessThan(0);
15
+ });
16
+ it('puts worker before alphabetically-earlier names', () => {
17
+ expect(compareService(svc('worker'), svc('aardvark'))).toBeLessThan(0);
18
+ });
19
+ it('sorts non-priority services alphabetically', () => {
20
+ expect(compareService(svc('postgres'), svc('redis'))).toBeLessThan(0);
21
+ expect(compareService(svc('temporal'), svc('redis'))).toBeGreaterThan(0);
22
+ });
23
+ it('produces the expected full ordering for a real stack', () => {
24
+ const stack = [svc('temporal-ui'), svc('redis'), svc('postgres'), svc('api'), svc('worker'), svc('temporal')];
25
+ const sorted = [...stack].sort(compareService).map(s => s.name);
26
+ expect(sorted).toEqual(['worker', 'api', 'postgres', 'redis', 'temporal', 'temporal-ui']);
27
+ });
28
+ });
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import type { Workflow } from '#api/generated/api.js';
3
+ import type { WorkflowRun } from '#services/workflow_runs.js';
4
+ export declare const WorkflowsPanel: React.FC<{
5
+ workflows: Workflow[];
6
+ runs: WorkflowRun[];
7
+ }>;