@outputai/cli 0.4.1-next.d43aa3d.0 → 0.5.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 (64) hide show
  1. package/dist/assets/docker/docker-compose-dev.yml +1 -4
  2. package/dist/generated/framework_version.json +1 -1
  3. package/dist/views/dev/chrome/footer.d.ts +5 -4
  4. package/dist/views/dev/chrome/footer.js +12 -2
  5. package/dist/views/dev/chrome/header.d.ts +2 -1
  6. package/dist/views/dev/chrome/header.js +18 -47
  7. package/dist/views/dev/chrome/header.spec.js +6 -46
  8. package/dist/views/dev/chrome/layout_heights.spec.d.ts +1 -0
  9. package/dist/views/dev/chrome/layout_heights.spec.js +19 -0
  10. package/dist/views/dev/chrome/search_bar.d.ts +1 -1
  11. package/dist/views/dev/chrome/search_bar.js +10 -11
  12. package/dist/views/dev/chrome/tab_bar.d.ts +8 -1
  13. package/dist/views/dev/chrome/tab_bar.js +16 -2
  14. package/dist/views/dev/chrome/toasts.d.ts +1 -0
  15. package/dist/views/dev/chrome/toasts.js +8 -4
  16. package/dist/views/dev/components/content_title.d.ts +6 -0
  17. package/dist/views/dev/components/content_title.js +7 -0
  18. package/dist/views/dev/components/docker_service_status.d.ts +12 -0
  19. package/dist/views/dev/components/docker_service_status.js +19 -0
  20. package/dist/views/dev/components/inline_snippet.d.ts +4 -0
  21. package/dist/views/dev/components/inline_snippet.js +3 -0
  22. package/dist/views/dev/components/master_detail_panel.d.ts +9 -8
  23. package/dist/views/dev/components/master_detail_panel.js +8 -5
  24. package/dist/views/dev/components/run_info_sidebar.d.ts +7 -0
  25. package/dist/views/dev/components/run_info_sidebar.js +19 -0
  26. package/dist/views/dev/components/workflow_status.d.ts +12 -0
  27. package/dist/views/dev/components/workflow_status.js +19 -0
  28. package/dist/views/dev/dev_app.js +107 -31
  29. package/dist/views/dev/hooks/use_run_detail.js +6 -9
  30. package/dist/views/dev/hooks/use_run_detail.spec.js +7 -0
  31. package/dist/views/dev/modals/expanded_json_modal.js +5 -6
  32. package/dist/views/dev/modals/modal_frame.d.ts +13 -0
  33. package/dist/views/dev/modals/modal_frame.js +13 -0
  34. package/dist/views/dev/modals/run_modal.js +23 -13
  35. package/dist/views/dev/{panels/run_detail_view.d.ts → modals/steps_modal.d.ts} +2 -1
  36. package/dist/views/dev/modals/steps_modal.js +102 -0
  37. package/dist/views/dev/panels/help_panel.d.ts +14 -0
  38. package/dist/views/dev/panels/help_panel.js +19 -21
  39. package/dist/views/dev/panels/runs_panel.d.ts +6 -2
  40. package/dist/views/dev/panels/runs_panel.js +82 -83
  41. package/dist/views/dev/panels/runs_panel.spec.js +1 -28
  42. package/dist/views/dev/panels/services_panel.d.ts +6 -0
  43. package/dist/views/dev/panels/services_panel.js +53 -62
  44. package/dist/views/dev/panels/workflows_panel.d.ts +6 -0
  45. package/dist/views/dev/panels/workflows_panel.js +21 -29
  46. package/dist/views/dev/panels/workflows_panel.spec.d.ts +1 -0
  47. package/dist/views/dev/panels/workflows_panel.spec.js +39 -0
  48. package/dist/views/dev/state/ui_state.d.ts +7 -3
  49. package/dist/views/dev/state/ui_state.js +23 -6
  50. package/dist/views/dev/utils/constants.d.ts +2 -2
  51. package/dist/views/dev/utils/constants.js +2 -2
  52. package/dist/views/dev/utils/json_editor.js +3 -3
  53. package/dist/views/dev/utils/json_render.d.ts +2 -0
  54. package/dist/views/dev/utils/json_render.js +48 -6
  55. package/dist/views/dev/utils/json_render.spec.js +9 -1
  56. package/dist/views/dev/utils/panel_helpers.d.ts +15 -0
  57. package/dist/views/dev/utils/panel_helpers.js +30 -0
  58. package/dist/views/dev/utils/panel_helpers.spec.js +46 -1
  59. package/package.json +4 -4
  60. package/dist/components/status_icon.d.ts +0 -11
  61. package/dist/components/status_icon.js +0 -25
  62. package/dist/views/dev/chrome/divider.d.ts +0 -8
  63. package/dist/views/dev/chrome/divider.js +0 -16
  64. package/dist/views/dev/panels/run_detail_view.js +0 -112
@@ -1,53 +1,51 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
3
2
  import { Box, Text, useInput } from 'ink';
4
3
  import { config } from '#config.js';
5
4
  import { openUrl } from '#utils/open_url.js';
6
- import { Footer } from '#views/dev/chrome/footer.js';
7
5
  import { SelectionIndicator } from '#views/dev/chrome/selection_indicator.js';
8
6
  import { useUiState } from '#views/dev/state/ui_state.js';
7
+ import { useListSelection } from '#views/dev/utils/panel_helpers.js';
8
+ import { InlineSnippet } from '../components/inline_snippet.js';
9
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." }) })] }));
10
+ export const Section = ({ children, title, direction = 'v' }) => (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: title }), _jsx(Box, { flexDirection: direction === 'v' ? 'column' : 'row', gap: 1, flexWrap: 'wrap', children: children })] }));
11
+ export const SubSection = ({ children, title }) => (_jsxs(Box, { flexDirection: "column", gap: 1, borderStyle: "single", borderColor: "blackBright", paddingLeft: 1, paddingRight: 1, children: [_jsx(Text, { italic: true, dimColor: true, children: title }), _jsx(Box, { flexDirection: "column", children: children })] }));
12
+ const KV = ({ label, value }) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 26, children: _jsx(Text, { children: label }) }), _jsx(Text, { bold: true, children: value })] }));
13
+ const RunFromCli = () => (_jsxs(Section, { title: "Running a workflow", children: [_jsxs(SubSection, { title: "From the CLI", children: [_jsx(InlineSnippet, { content: "npx output workflow run blog_evaluator paulgraham_hwh" }), _jsx(InlineSnippet, { content: 'npx output workflow run simple --input {"values":[1,2,3]}' }), _jsx(InlineSnippet, { content: "npx output workflow run simple --input scenario.json" })] }), _jsxs(SubSection, { title: "From the TUI", children: [_jsxs(Text, { children: ["Open Workflows tab, hover a workflow, press ", _jsx(Text, { bold: true, children: "r" }), "."] }), _jsx(Text, { children: "Create a custom input from the TUI using the editor with live JSON validation or select an existing scenario." })] })] }));
14
+ const ServiceUrls = () => (_jsx(Section, { title: "Service URLs", children: _jsxs(SubSection, { title: "Where the services are available", 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" })] }) }));
15
+ const UpdatingMigrating = () => (_jsxs(Section, { title: "Updating / Migrating", children: [_jsxs(SubSection, { title: "Update", children: [_jsx(Text, { wrap: "wrap", children: "Update the CLI to the latest published version:" }), _jsx(InlineSnippet, { content: "output update" })] }), _jsxs(SubSection, { title: "Migrate", children: [_jsx(Text, { wrap: "wrap", children: "Migrate a workflow project to the SDK version this CLI ships with:" }), _jsx(InlineSnippet, { content: "output migrate" }), _jsx(Text, { 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." })] })] }));
16
+ const ClaudePlugins = () => (_jsx(Section, { title: "Claude Plugins", children: _jsxs(SubSection, { title: "Reinstall", children: [_jsx(Text, { wrap: "wrap", children: "Command `output init` already installs the Claude Code plugins (skills, commands, agents) into your project during scaffolding." }), _jsx(Text, { wrap: "wrap", children: "But if it is necessary to reinstall it, use the update command:" }), _jsx(InlineSnippet, { content: "output update --agents" }), _jsx(Text, { children: "This pulls the latest plugin bundle that ships with the installed CLI version." })] }) }));
16
17
  const Troubleshooting = () => {
17
18
  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
+ return (_jsxs(Section, { title: "Troubleshooting", children: [_jsxs(SubSection, { title: "Worker won't start", children: [_jsx(Text, { children: "If the worker won't start, rebuild the local image:" }), _jsx(InlineSnippet, { content: "output fix" })] }), _jsxs(SubSection, { title: "I need to see more logs", children: [_jsx(Text, { children: "Tail a service log from the shell:" }), _jsx(InlineSnippet, { content: logsCommand })] }), _jsxs(SubSection, { title: "Force pull-images", children: [_jsx(Text, { children: "If the images get stale and a fresh start is necessary, force pull with:" }), _jsx(InlineSnippet, { content: "output dev --image-pull-policy always" })] })] }));
19
20
  };
20
21
  const SECTIONS = [
21
22
  { id: 'cli', title: 'Run from CLI', body: RunFromCli },
22
- { id: 'hotkeys', title: 'Hotkeys', body: Hotkeys },
23
23
  { id: 'urls', title: 'Service URLs', body: ServiceUrls },
24
24
  { id: 'updating', title: 'Updating / Migrating', body: UpdatingMigrating },
25
25
  { id: 'claude-plugins', title: 'Claude Plugins', body: ClaudePlugins },
26
26
  { id: 'troubleshooting', title: 'Troubleshooting', body: Troubleshooting }
27
27
  ];
28
- const HINTS = [
28
+ export const HELP_HINTS = [
29
29
  { key: '↑/↓', label: 'navigate' },
30
- { key: 'd', label: 'docs' },
31
- { key: 'tab', label: 'next tab' },
32
- { key: 'ctrl+c', label: 'quit' }
30
+ { key: 'd', label: 'docs' }
33
31
  ];
32
+ export const HELP_SECTION_COUNT = SECTIONS.length;
34
33
  export const HelpPanel = () => {
35
34
  const ui = useUiState();
36
- const [index, setIndex] = useState(0);
37
- const isActive = ui.tab === 'help' && !ui.search.open && !ui.runModal.open;
35
+ const { selectedIndex: index, selectPrevious, selectNext } = useListSelection(SECTIONS.length);
38
36
  useInput((input, key) => {
39
37
  if (key.upArrow) {
40
- setIndex(i => Math.max(0, i - 1));
38
+ selectPrevious();
41
39
  return;
42
40
  }
43
41
  if (key.downArrow) {
44
- setIndex(i => Math.min(SECTIONS.length - 1, i + 1));
42
+ selectNext();
45
43
  return;
46
44
  }
47
45
  if (input === 'd') {
48
46
  openUrl(DOCS_URL);
49
47
  }
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" })] }));
48
+ }, { isActive: ui.tab === 'help' && !ui.search.open });
49
+ const ActiveSection = SECTIONS[index].body;
50
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingRight: 2, 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(ActiveSection, {})] }));
53
51
  };
@@ -1,8 +1,12 @@
1
1
  import React from 'react';
2
2
  import type { WorkflowRun } from '#services/workflow_runs.js';
3
- import type { TraceData } from '#types/trace.js';
4
3
  export declare const buildVisibleRuns: (runs: WorkflowRun[], query: string) => WorkflowRun[];
5
- export declare const extractRunInput: (trace: TraceData | null) => unknown;
4
+ export declare const RUNS_HINTS: {
5
+ key: string;
6
+ label: string;
7
+ }[];
8
+ export declare const RUNS_EMPTY_HINTS: never[];
6
9
  export declare const RunsPanel: React.FC<{
7
10
  runs: WorkflowRun[];
11
+ height: number;
8
12
  }>;
@@ -1,19 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useState } from 'react';
2
+ import { useEffect, useMemo } from 'react';
3
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';
4
+ import { WorkflowStatusIcon, workflowStatusColor } from '#views/dev/components/workflow_status.js';
5
+ import { elapsedMs, formatDurationCompact } from '#utils/date_formatter.js';
6
6
  import { openUrl } from '#utils/open_url.js';
7
- import { Footer } from '#views/dev/chrome/footer.js';
7
+ import { TabBar, getHeight as getTabBarHeight } from '#views/dev/chrome/tab_bar.js';
8
+ import { ContentTitle, getHeight as getContentTitleHeight } from '#views/dev/components/content_title.js';
8
9
  import { LoadingSpinner } from '#views/dev/chrome/loading_spinner.js';
9
10
  import { SelectionIndicator } from '#views/dev/chrome/selection_indicator.js';
10
11
  import { useUiState } from '#views/dev/state/ui_state.js';
11
- import { RunDetailView } from '#views/dev/panels/run_detail_view.js';
12
12
  import { useRunDetail } from '#views/dev/hooks/use_run_detail.js';
13
13
  import { JsonView } from '#views/dev/utils/json_render.js';
14
+ import { RunInfoSidebar } from '#views/dev/components/run_info_sidebar.js';
14
15
  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';
16
+ import { capitalize, cycleValue, formatContentTitle, formatStartedShort, hasJsonValue, truncate, useListSelection } from '#views/dev/utils/panel_helpers.js';
17
+ import { CATALOG_WORKFLOW_NAME, RUNS_VISIBLE_ROWS } from '#views/dev/utils/constants.js';
17
18
  const TEMPORAL_UI_BASE = 'http://localhost:8080';
18
19
  const STATUS_ORDER = {
19
20
  running: 0,
@@ -47,19 +48,6 @@ export const buildVisibleRuns = (runs, query) => {
47
48
  const filtered = query ? visible.filter(r => matchesFilter(r, query)) : visible;
48
49
  return sortRuns(filtered);
49
50
  };
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
51
  const COL = {
64
52
  indicator: 3,
65
53
  icon: 3,
@@ -69,59 +57,86 @@ const COL = {
69
57
  duration: 9,
70
58
  started: 14
71
59
  };
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" }) })] }));
60
+ const RUN_INFO_TABS = [
61
+ { id: 'status', label: 'Status' },
62
+ { id: 'input', label: 'Input' },
63
+ { id: 'output', label: 'Output' },
64
+ { id: 'attributes', label: 'Attributes' },
65
+ { id: 'aggregations', label: 'Aggregations' }
66
+ ];
67
+ const RUN_INFO_TAB_ORDER = ['status', 'input', 'output', 'attributes', 'aggregations'];
68
+ const HeaderRow = () => (_jsxs(Box, { children: [_jsx(Box, { width: COL.indicator, children: _jsx(Text, { children: "\u00A0" }) }), _jsx(Box, { width: COL.icon, children: _jsx(Text, { children: "\u00A0" }) }), _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
69
  const RunRow = ({ run, selected }) => {
74
70
  const status = run.status ?? 'running';
75
- const color = statusColor(status);
71
+ const color = workflowStatusColor(status);
76
72
  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) }) })] }));
73
+ return (_jsxs(Box, { children: [_jsx(Box, { width: COL.indicator, children: _jsx(SelectionIndicator, { selected: selected }) }), _jsx(Box, { width: COL.icon, children: _jsx(WorkflowStatusIcon, { 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) }) })] }));
74
+ };
75
+ const statusPaneValue = (run, pane) => ({
76
+ status: pane.status,
77
+ runId: run.runId,
78
+ workflowId: run.workflowId,
79
+ workflowType: run.workflowType,
80
+ startedAt: run.startedAt,
81
+ completedAt: run.completedAt
82
+ });
83
+ const runPaneValue = (run, pane, activePane) => {
84
+ if (activePane === 'status') {
85
+ return statusPaneValue(run, pane);
86
+ }
87
+ if (activePane === 'input') {
88
+ return pane.input;
89
+ }
90
+ if (activePane === 'output') {
91
+ return pane.error ?? pane.output;
92
+ }
93
+ if (activePane === 'attributes') {
94
+ return pane.attributes;
95
+ }
96
+ return pane.aggregations;
78
97
  };
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 }) => {
98
+ const DetailPane = ({ run, pane, rows }) => {
82
99
  const ui = useUiState();
83
100
  if (!run || !pane) {
84
101
  return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Select a run to see details." }) }));
85
102
  }
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';
103
+ const { loading } = pane;
104
+ const activePane = ui.runListPaneTab;
105
+ const tabContentRows = Math.max(1, rows - getContentTitleHeight() - getTabBarHeight());
106
+ const tabs = hasJsonValue(pane.error) ?
107
+ RUN_INFO_TABS.map(tab => tab.id === 'output' ? { ...tab, label: 'Error' } : tab) :
108
+ RUN_INFO_TABS;
89
109
  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 });
110
+ if (activePane === 'status') {
111
+ return _jsx(RunInfoSidebar, { run: run, resultStatus: pane.status, maxRows: tabContentRows });
95
112
  }
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) {
113
+ const value = runPaneValue(run, pane, activePane);
114
+ if (value === undefined || value === null) {
100
115
  if (loading) {
101
116
  return _jsx(LoadingSpinner, {});
102
117
  }
103
- return _jsx(Text, { dimColor: true, children: "No output yet." });
118
+ return _jsx(Text, { dimColor: true, children: "\u2014" });
119
+ }
120
+ if (activePane === 'output' && hasJsonValue(pane.error)) {
121
+ const lines = String(pane.error).split('\n').slice(0, tabContentRows);
122
+ return (_jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { color: "red", wrap: "truncate-end", children: line }, i))) }));
104
123
  }
105
- return _jsx(JsonView, { value: runOutput, maxLines: RUNS_PREVIEW_LINES });
124
+ return _jsx(JsonView, { value: value, maxLines: tabContentRows, truncateLine: true });
106
125
  };
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() })] }));
126
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(ContentTitle, { title: formatContentTitle([`Workflow "${run.workflowType}"`, 'Result']) }), _jsx(TabBar, { active: activePane, items: tabs }), renderPane()] }));
109
127
  };
110
- const HINTS = [
128
+ export const RUNS_HINTS = [
111
129
  { key: '↑/↓', label: 'navigate' },
112
130
  { key: 'enter', label: 'open' },
113
131
  { key: '←/→', label: 'switch pane' },
114
132
  { key: 'e', label: 'expand' },
115
- { key: 'o', label: 'temporal' },
116
- { key: '/', label: 'filter' },
117
- { key: 'tab', label: 'next tab' }
133
+ { key: 'o', label: 'temporal' }
118
134
  ];
119
- export const RunsPanel = ({ runs }) => {
135
+ export const RUNS_EMPTY_HINTS = [];
136
+ export const RunsPanel = ({ runs, height }) => {
120
137
  const ui = useUiState();
121
138
  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(() => {
139
+ const initialIndex = () => {
125
140
  const previousRunId = ui.selection.runId;
126
141
  if (!previousRunId) {
127
142
  return 0;
@@ -129,23 +144,19 @@ export const RunsPanel = ({ runs }) => {
129
144
  const initial = buildVisibleRuns(runs, ui.search.query);
130
145
  const i = initial.findIndex(r => r.runId === previousRunId);
131
146
  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));
147
+ };
148
+ const { selectedIndex: clampedIndex, selectPrevious, selectNext } = useListSelection(filteredRuns.length, initialIndex);
135
149
  const selectedRun = filteredRuns[clampedIndex];
136
- const { result, trace, loading } = useRunDetail(selectedRun?.workflowId, selectedRun?.runId, selectedRun?.status);
150
+ const { result, loading } = useRunDetail(selectedRun?.workflowId, selectedRun?.runId, selectedRun?.status);
137
151
  const pane = selectedRun ? {
138
- input: extractRunInput(trace),
152
+ input: result?.input,
139
153
  output: result?.output,
140
154
  error: result?.error,
155
+ attributes: result?.attributes,
156
+ aggregations: result?.aggregations,
141
157
  status: result?.status ?? selectedRun.status ?? 'unknown',
142
158
  loading
143
159
  } : null;
144
- useEffect(() => {
145
- if (clampedIndex !== selectedIndex) {
146
- setSelectedIndex(clampedIndex);
147
- }
148
- }, [clampedIndex, selectedIndex]);
149
160
  const setSelection = ui.setSelection;
150
161
  useEffect(() => {
151
162
  setSelection({
@@ -156,11 +167,11 @@ export const RunsPanel = ({ runs }) => {
156
167
  }, [selectedRun?.runId, selectedRun?.workflowId, selectedRun?.workflowType, setSelection]);
157
168
  useInput((input, key) => {
158
169
  if (key.upArrow) {
159
- setSelectedIndex(i => Math.max(0, i - 1));
170
+ selectPrevious();
160
171
  return;
161
172
  }
162
173
  if (key.downArrow) {
163
- setSelectedIndex(i => Math.min(filteredRuns.length - 1, i + 1));
174
+ selectNext();
164
175
  return;
165
176
  }
166
177
  if (input === 'o' && selectedRun?.workflowId) {
@@ -172,33 +183,21 @@ export const RunsPanel = ({ runs }) => {
172
183
  return;
173
184
  }
174
185
  if (key.leftArrow || key.rightArrow) {
175
- ui.setRightPaneTab(ui.rightPaneTab === 'input' ? 'output' : 'input');
186
+ ui.setRunListPaneTab(cycleValue(RUN_INFO_TAB_ORDER, ui.runListPaneTab, key.rightArrow ? 1 : -1));
176
187
  return;
177
188
  }
178
189
  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);
190
+ const activePane = ui.runListPaneTab;
191
+ const content = selectedRun ? runPaneValue(selectedRun, pane, activePane) : null;
192
+ const title = formatContentTitle(['Recent Runs', `Workflow "${selectedRun?.workflowType ?? ''}"`, capitalize(activePane)]);
193
+ ui.openExpandedJson(content, title);
184
194
  }
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
- }
195
+ }, { isActive: ui.tab === 'runs' && ui.runsView === 'list' && !ui.search.open });
197
196
  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' }] })] }));
197
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "No runs yet. Trigger one from the Workflows tab or with `output workflow run \u2026`." }) }));
199
198
  }
200
199
  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" })] }));
200
+ return (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { dimColor: true, children: ["No runs match `", ui.search.query, "`. Press ", _jsx(Text, { bold: true, children: "esc" }), " to clear the filter."] }) }));
202
201
  }
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" }));
202
+ return (_jsx(MasterDetailPanel, { items: filteredRuns, selectedIndex: clampedIndex, height: height, 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: ({ detailRows }) => _jsx(DetailPane, { run: selectedRun, pane: pane, rows: detailRows }) }));
204
203
  };
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { buildVisibleRuns, extractRunInput } from './runs_panel.js';
2
+ import { buildVisibleRuns } from './runs_panel.js';
3
3
  const run = (overrides) => ({
4
4
  workflowId: 'wf',
5
5
  workflowType: 'demo',
@@ -53,30 +53,3 @@ describe('buildVisibleRuns', () => {
53
53
  expect(buildVisibleRuns(runs, 'no-match')).toHaveLength(0);
54
54
  });
55
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
- });
@@ -7,7 +7,13 @@ import type { Phase } from '#views/dev/dev_app.js';
7
7
  * the comparator three lines.
8
8
  */
9
9
  export declare const compareService: (a: ServiceStatus, b: ServiceStatus) => number;
10
+ export declare const SERVICES_HINTS: {
11
+ key: string;
12
+ label: string;
13
+ }[];
14
+ export declare const SERVICES_BOOT_HINTS: never[];
10
15
  export declare const ServicesPanel: React.FC<{
16
+ height: number;
11
17
  phase: Phase;
12
18
  services: ServiceStatus[];
13
19
  dockerComposePath: string;