@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
@@ -77,7 +77,7 @@ services:
77
77
  condition: service_healthy
78
78
  worker:
79
79
  condition: service_healthy
80
- image: outputai/api:${OUTPUT_API_VERSION:-0.4.1-next.d43aa3d.0}
80
+ image: outputai/api:${OUTPUT_API_VERSION:-0.5.0}
81
81
  init: true
82
82
  networks:
83
83
  - main
@@ -88,9 +88,6 @@ services:
88
88
  - NODE_ENV=development
89
89
  - OUTPUT_API_PORT=3001
90
90
  - OUTPUT_CATALOG_ID=${OUTPUT_CATALOG_ID:-main}
91
- - OUTPUT_AWS_REGION=${AWS_REGION:-us-west-1}
92
- - OUTPUT_AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
93
- - OUTPUT_AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
94
91
  - TEMPORAL_ADDRESS=temporal:7233
95
92
  ports:
96
93
  - '${OUTPUT_API_HOST_PORT:-3001}:3001'
@@ -1,3 +1,3 @@
1
1
  {
2
- "framework": "0.4.1-next.d43aa3d.0"
2
+ "framework": "0.5.0"
3
3
  }
@@ -1,11 +1,12 @@
1
1
  import React from 'react';
2
+ export declare const getHeight: () => number;
2
3
  export interface CommandHint {
3
4
  key: string;
4
5
  label: string;
5
6
  }
6
- export declare const Footer: React.FC<{
7
- hints: CommandHint[];
7
+ export interface FooterState {
8
+ hints?: CommandHint[];
8
9
  itemCount?: number;
9
10
  itemLabel?: string;
10
- }>;
11
- export declare const GLOBAL_HINTS: CommandHint[];
11
+ }
12
+ export declare const Footer: React.FC<FooterState>;
@@ -1,10 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { Box, Text } from 'ink';
4
- export const Footer = ({ hints, itemCount, itemLabel = 'items' }) => (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginTop: 1, children: [_jsx(Box, { flexDirection: "row", children: hints.map((hint, i) => (_jsxs(React.Fragment, { children: [i > 0 && _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { bold: true, children: hint.key }), _jsx(Text, { dimColor: true, children: ` ${hint.label}` })] }, hint.key))) }), typeof itemCount === 'number' && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [itemCount, " ", itemLabel] }) }))] }));
5
- export const GLOBAL_HINTS = [
4
+ import packageJson from '../../../../package.json' with { type: 'json' };
5
+ const GLOBAL_HINT_ROWS = 1;
6
+ const LOCAL_HINT_ROWS = 1;
7
+ export const getHeight = () => GLOBAL_HINT_ROWS + LOCAL_HINT_ROWS;
8
+ const GLOBAL_HINTS = [
6
9
  { key: 'tab', label: 'next tab' },
10
+ { key: 'shift-tab', label: 'prev tab' },
11
+ { key: '1-4', label: 'tabs' },
7
12
  { key: '/', label: 'search' },
8
13
  { key: '?', label: 'help' },
9
14
  { key: 'ctrl+c', label: 'quit' }
10
15
  ];
16
+ const VERSION = packageJson.version;
17
+ const HintRow = ({ hints }) => (_jsx(Box, { flexDirection: "row", children: hints.length === 0 ? (_jsx(Text, { children: " " })) : hints.map((hint, i) => (_jsxs(React.Fragment, { children: [i > 0 && _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { bold: true, children: hint.key }), _jsx(Text, { dimColor: true, children: ` ${hint.label}` })] }, hint.key))) }));
18
+ export const Footer = ({ hints = [], itemCount, itemLabel }) => {
19
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(HintRow, { hints: GLOBAL_HINTS }), typeof itemCount === 'number' && itemLabel && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [itemCount, " ", itemLabel] }) }))] }), _jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(HintRow, { hints: hints }), _jsxs(Text, { color: "blackBright", children: ["v", VERSION] })] })] }));
20
+ };
@@ -5,7 +5,8 @@ export interface WorkflowSummary {
5
5
  failed: number;
6
6
  total: number;
7
7
  }
8
- export declare const compressPixels: (rows: string[]) => string[];
8
+ export declare const getHeight: (terminalRows: number) => number;
9
+ export declare const useHeaderRows: () => number;
9
10
  export type ServiceBadge = 'healthy' | 'starting' | 'failed';
10
11
  export interface HeaderCounters {
11
12
  running: number;
@@ -3,46 +3,20 @@ import { Box, Text, useStdout } from 'ink';
3
3
  import Spinner from 'ink-spinner';
4
4
  import { LOGO_GRADIENT, PURPLE_100 } from '#views/dev/chrome/palette.js';
5
5
  const LOGO_PIXELS = [
6
- ' ██████ ██ ██ ████████ ██████ ██ ██ ████████',
7
- '██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
8
- '██ ██ ██ ██ ██ ██████ ██ ██ ██ ',
9
- '██ ██ ██ ██ ██ ██ ██ ██ ██ ',
10
- ' ██████ ██████ ██ ██ ██████ ██ '
6
+ '▟▀▀▙▐▌ ▐▌▀▜▛▀▐▛▀▙▐▌ ▐▌▀▜▛▀',
7
+ '█ █▐▌ ▐▌ ▐▌ ▐▛▀▘▐▌ ▐▌ ▐▌ ',
8
+ '▝▀▀▘ ▀▀▀ ▝▘ ▝▘ ▀▀▀ ▝▘ '
11
9
  ];
12
- const QUADRANT_CHARS = [
13
- ' ', '▗', '▖', '▄',
14
- '▝', '▐', '▞', '▟',
15
- '▘', '▚', '▌', '▙',
16
- '▀', '▜', '▛', '█'
17
- ];
18
- export const compressPixels = (rows) => {
19
- const maxCol = Math.max(...rows.map(r => r.length));
20
- const evenCol = maxCol + (maxCol % 2);
21
- const padded = rows.map(r => r.padEnd(evenCol, ' '));
22
- const fullPadded = padded.length % 2 === 1 ?
23
- [...padded, ' '.repeat(evenCol)] :
24
- padded;
25
- const rowPairs = fullPadded.reduce((acc, row, i) => {
26
- if (i % 2 === 0) {
27
- acc.push([row, fullPadded[i + 1]]);
28
- }
29
- return acc;
30
- }, []);
31
- const colCount = Math.floor(evenCol / 2);
32
- return rowPairs.map(([top, bot]) => {
33
- const chars = Array.from({ length: colCount }, (_, k) => {
34
- const j = k * 2;
35
- const tl = top[j] === '█' ? 8 : 0;
36
- const tr = top[j + 1] === '█' ? 4 : 0;
37
- const bl = bot[j] === '█' ? 2 : 0;
38
- const br = bot[j + 1] === '█' ? 1 : 0;
39
- return QUADRANT_CHARS[tl + tr + bl + br];
40
- });
41
- return chars.join('');
42
- });
10
+ const HEADER_MARGIN = 1;
11
+ const COMPACT_HEIGHT_THRESHOLD = 50;
12
+ const COMPACT_HEADER_ROWS = 1;
13
+ const FULL_HEADER_ROWS = 3;
14
+ const getLogoHeight = (terminalRows) => terminalRows < COMPACT_HEIGHT_THRESHOLD ? COMPACT_HEADER_ROWS : FULL_HEADER_ROWS;
15
+ export const getHeight = (terminalRows) => getLogoHeight(terminalRows) + HEADER_MARGIN;
16
+ export const useHeaderRows = () => {
17
+ const { stdout } = useStdout();
18
+ return getLogoHeight(stdout?.rows ?? 60);
43
19
  };
44
- const LOGO_COMPRESSED = compressPixels(LOGO_PIXELS);
45
- const FULL_WIDTH_THRESHOLD = 60;
46
20
  const ServicesBadge = ({ badge, failingCount }) => {
47
21
  if (badge === 'failed') {
48
22
  return (_jsxs(Text, { color: "red", bold: true, children: ["\u26A0 ", failingCount, " service", failingCount === 1 ? '' : 's', " down"] }));
@@ -53,12 +27,9 @@ const ServicesBadge = ({ badge, failingCount }) => {
53
27
  return (_jsxs(_Fragment, { children: [_jsx(Text, { color: "green", children: "\u25CF " }), _jsx(Text, { children: "services" })] }));
54
28
  };
55
29
  const Counters = ({ counters }) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { marginRight: 3, children: _jsx(ServicesBadge, { badge: counters.serviceBadge, failingCount: counters.failingServices }) }), counters.running > 0 && (_jsxs(Box, { marginRight: 3, children: [_jsx(Text, { color: "blue", children: "\u25CF " }), _jsxs(Text, { bold: true, children: [counters.running, " "] }), _jsx(Text, { children: "running" })] })), counters.failed > 0 && (_jsxs(Box, { marginRight: 3, children: [_jsx(Text, { color: "red", children: "\u2717 " }), _jsxs(Text, { color: "red", bold: true, children: [counters.failed, " "] }), _jsx(Text, { color: "red", children: "failed" })] })), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [counters.totalWorkflows, " workflows \u00B7 ", counters.totalRuns, " runs"] }) })] }));
56
- const Logo = ({ cols }) => {
57
- if (cols < FULL_WIDTH_THRESHOLD) {
58
- return _jsx(Text, { color: PURPLE_100, bold: true, children: "OUTPUT" });
59
- }
60
- return (_jsx(Box, { flexDirection: "column", children: LOGO_COMPRESSED.map((line, i) => (_jsx(Text, { color: LOGO_GRADIENT[i] ?? PURPLE_100, bold: true, children: line }, i))) }));
61
- };
30
+ const Logo = ({ compact }) => (_jsx(Box, { flexDirection: "column", children: compact ?
31
+ _jsx(Text, { color: PURPLE_100, bold: true, children: "OUTPUT" }) :
32
+ LOGO_PIXELS.map((line, i) => (_jsx(Text, { color: LOGO_GRADIENT[i] ?? PURPLE_100, bold: true, children: line }, i))) }));
62
33
  export const buildSummaryCounters = (summary, totalWorkflows, serviceBadge = 'starting', failingServices = 0) => ({
63
34
  running: summary?.running ?? 0,
64
35
  failed: summary?.failed ?? 0,
@@ -68,7 +39,7 @@ export const buildSummaryCounters = (summary, totalWorkflows, serviceBadge = 'st
68
39
  failingServices
69
40
  });
70
41
  export const Header = ({ counters }) => {
71
- const { stdout } = useStdout();
72
- const cols = stdout?.columns ?? 120;
73
- return (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", alignItems: "flex-start", children: [_jsx(Logo, { cols: cols }), _jsx(Box, { flexDirection: "column", paddingTop: 1, children: _jsx(Counters, { counters: counters }) })] }));
42
+ const headerRows = useHeaderRows();
43
+ const compact = headerRows === COMPACT_HEADER_ROWS;
44
+ return (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: HEADER_MARGIN, children: [_jsx(Logo, { compact: compact }), _jsx(Box, { flexDirection: "column", children: _jsx(Counters, { counters: counters }) })] }));
74
45
  };
@@ -1,50 +1,10 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { compressPixels } from './header.js';
3
- describe('compressPixels', () => {
4
- it('returns an empty array for empty input', () => {
5
- expect(compressPixels([])).toEqual([]);
2
+ import { getHeight as getHeaderHeight } from './header.js';
3
+ describe('getHeaderHeight', () => {
4
+ it('includes margin with compact height for terminals under 50 rows', () => {
5
+ expect(getHeaderHeight(49)).toBe(2);
6
6
  });
7
- it('compresses a 2x2 full block to a single full block', () => {
8
- expect(compressPixels(['██', '██'])).toEqual(['█']);
9
- });
10
- it('maps each quadrant character correctly', () => {
11
- // Each row pair decodes to: top-left, top-right, bottom-left, bottom-right
12
- const cases = [
13
- [[' ', ' '], ' '], // 0000
14
- [[' ', ' █'], '▗'], // 0001 bottom-right
15
- [[' ', '█ '], '▖'], // 0010 bottom-left
16
- [[' ', '██'], '▄'], // 0011 bottom half
17
- [[' █', ' '], '▝'], // 0100 top-right
18
- [[' █', ' █'], '▐'], // 0101 right half
19
- [[' █', '█ '], '▞'], // 0110 anti-diagonal
20
- [[' █', '██'], '▟'], // 0111
21
- [['█ ', ' '], '▘'], // 1000 top-left
22
- [['█ ', ' █'], '▚'], // 1001 diagonal
23
- [['█ ', '█ '], '▌'], // 1010 left half
24
- [['█ ', '██'], '▙'], // 1011
25
- [['██', ' '], '▀'], // 1100 top half
26
- [['██', ' █'], '▜'], // 1101
27
- [['██', '█ '], '▛'], // 1110
28
- [['██', '██'], '█'] // 1111 full
29
- ];
30
- for (const [input, expected] of cases) {
31
- expect(compressPixels(input)).toEqual([expected]);
32
- }
33
- });
34
- it('pads odd-length rows with an empty bottom row', () => {
35
- // Single row of 4 cols compresses to 1 row of 2 cols
36
- expect(compressPixels(['████'])).toEqual(['▀▀']);
37
- });
38
- it('pads odd-width columns with a trailing space', () => {
39
- // 3 cols compresses to 2 cols (even-padded)
40
- expect(compressPixels(['███', '███'])).toEqual(['█▌']);
41
- });
42
- it('compresses 5-row input to 3 rows', () => {
43
- const five = ['██', '██', '██', '██', '██'];
44
- const result = compressPixels(five);
45
- expect(result.length).toBe(3);
46
- expect(result[0]).toBe('█');
47
- expect(result[1]).toBe('█');
48
- expect(result[2]).toBe('▀');
7
+ it('includes margin with full height for terminals with at least 50 rows', () => {
8
+ expect(getHeaderHeight(50)).toBe(4);
49
9
  });
50
10
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getHeight as getFooterHeight } from './footer.js';
3
+ import { getHeight as getTabBarHeight } from './tab_bar.js';
4
+ import { getHeight as getContentTitleHeight } from '../components/content_title.js';
5
+ import { getHeight as getModalFrameHeight } from '../modals/modal_frame.js';
6
+ describe('static layout heights', () => {
7
+ it('reports the tab bar height including its border row', () => {
8
+ expect(getTabBarHeight()).toBe(2);
9
+ });
10
+ it('reports the two-line footer height', () => {
11
+ expect(getFooterHeight()).toBe(2);
12
+ });
13
+ it('reports content title height including bottom margin', () => {
14
+ expect(getContentTitleHeight()).toBe(2);
15
+ });
16
+ it('reports modal frame chrome height', () => {
17
+ expect(getModalFrameHeight()).toBe(6);
18
+ });
19
+ });
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
+ export declare const useHeight: () => number;
2
3
  export declare const SearchBar: React.FC<{
3
4
  active: boolean;
4
- onSubmit?: (query: string) => void;
5
5
  }>;
@@ -1,16 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect } from 'react';
3
2
  import { Box, Text, useInput } from 'ink';
4
3
  import { useUiState } from '#views/dev/state/ui_state.js';
5
- export const SearchBar = ({ active, onSubmit }) => {
4
+ const SEARCH_CONTENT_ROWS = 1;
5
+ const SEARCH_BORDER_ROWS = 2;
6
+ export const useHeight = () => {
6
7
  const ui = useUiState();
8
+ return ui.search.open || Boolean(ui.search.query) ? SEARCH_CONTENT_ROWS + SEARCH_BORDER_ROWS : 0;
9
+ };
10
+ export const SearchBar = ({ active }) => {
11
+ const ui = useUiState();
12
+ const visible = useHeight() > 0;
7
13
  useInput((input, key) => {
8
14
  if (key.escape) {
9
15
  ui.clearSearch();
10
16
  return;
11
17
  }
12
18
  if (key.return) {
13
- onSubmit?.(ui.search.query);
14
19
  ui.closeSearch();
15
20
  return;
16
21
  }
@@ -22,14 +27,8 @@ export const SearchBar = ({ active, onSubmit }) => {
22
27
  ui.setSearchQuery(ui.search.query + input);
23
28
  }
24
29
  }, { isActive: active });
25
- useEffect(() => {
26
- if (!active) {
27
- return;
28
- }
29
- onSubmit?.(ui.search.query);
30
- }, [active, ui.search.query, onSubmit]);
31
- if (!active) {
30
+ if (!visible) {
32
31
  return null;
33
32
  }
34
- return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { bold: true, children: "/ " }), _jsx(Text, { children: ui.search.query }), _jsx(Text, { inverse: true, children: ' ' })] }));
33
+ return (_jsxs(Box, { borderColor: "white", borderStyle: "double", flexGrow: 1, children: [_jsx(Text, { dimColor: true, children: "FILTER WORKFLOWS:\u00A0" }), _jsx(Text, { children: ui.search.query }), active && _jsx(Text, { inverse: true, children: ' ' })] }));
35
34
  };
@@ -1,5 +1,12 @@
1
1
  import React from 'react';
2
2
  import { type Tab } from '#views/dev/state/ui_state.js';
3
+ export declare const getHeight: () => number;
4
+ export interface TabBarItem {
5
+ id: string;
6
+ label: string;
7
+ }
3
8
  export declare const TabBar: React.FC<{
4
- active: Tab;
9
+ active: Tab | string;
10
+ items?: readonly TabBarItem[];
11
+ borderColor?: string;
5
12
  }>;
@@ -1,4 +1,18 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { TAB_LABELS, TAB_ORDER } from '#views/dev/state/ui_state.js';
4
- export const TabBar = ({ active }) => (_jsx(Box, { flexDirection: "row", marginTop: 1, children: TAB_ORDER.map(tab => (_jsx(Box, { marginRight: 3, children: tab === active ? (_jsx(Text, { inverse: true, bold: true, children: ` ${TAB_LABELS[tab]} ` })) : (_jsx(Text, { dimColor: true, children: TAB_LABELS[tab] })) }, tab))) }));
4
+ const TAB_LABEL_ROWS = 1;
5
+ const TAB_BORDER_BOTTOM_ROWS = 1;
6
+ export const getHeight = () => TAB_LABEL_ROWS + TAB_BORDER_BOTTOM_ROWS;
7
+ const DEFAULT_ITEMS = TAB_ORDER.map(tab => ({
8
+ id: tab,
9
+ label: TAB_LABELS[tab]
10
+ }));
11
+ export const TabBar = ({ active, items = DEFAULT_ITEMS, borderColor }) => (_jsx(Box, { flexDirection: "row", borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, borderBottom: true, borderColor: borderColor ?? 'blackBright', gap: 3, children: items.map(tab => {
12
+ const activeTab = tab.id === active;
13
+ const content = _jsxs(_Fragment, { children: ["\u00A0", tab.label, "\u00A0"] });
14
+ if (activeTab) {
15
+ return (_jsx(Box, { children: _jsx(Text, { inverse: true, bold: true, children: content }) }, tab.id));
16
+ }
17
+ return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: content }) }, tab.id));
18
+ }) }));
@@ -1,2 +1,3 @@
1
1
  import React from 'react';
2
+ export declare const useHeight: () => number;
2
3
  export declare const Toasts: React.FC;
@@ -3,6 +3,10 @@ import { useEffect } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import { useUiState } from '#views/dev/state/ui_state.js';
5
5
  const TOAST_TTL_MS = 4000;
6
+ export const useHeight = () => {
7
+ const ui = useUiState();
8
+ return ui.toasts.length > 0 ? ui.toasts.length : 0;
9
+ };
6
10
  const toneColor = (tone) => {
7
11
  if (tone === 'success') {
8
12
  return 'green';
@@ -14,12 +18,12 @@ const toneColor = (tone) => {
14
18
  };
15
19
  const tonePrefix = (tone) => {
16
20
  if (tone === 'success') {
17
- return '';
21
+ return '[+]';
18
22
  }
19
23
  if (tone === 'error') {
20
- return '';
24
+ return '[!]';
21
25
  }
22
- return '';
26
+ return '[i]';
23
27
  };
24
28
  export const Toasts = () => {
25
29
  const ui = useUiState();
@@ -36,5 +40,5 @@ export const Toasts = () => {
36
40
  if (toasts.length === 0) {
37
41
  return null;
38
42
  }
39
- return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: toasts.map(toast => (_jsxs(Box, { children: [_jsxs(Text, { color: toneColor(toast.tone), bold: true, children: [tonePrefix(toast.tone), " "] }), _jsx(Text, { children: toast.message })] }, toast.id))) }));
43
+ return (_jsx(Box, { flexDirection: "column", flexGrow: 1, children: toasts.map(toast => (_jsx(Box, { backgroundColor: toneColor(toast.tone), flexGrow: 1, children: _jsxs(Text, { color: "black", wrap: "truncate-end", children: ["\u00A0", tonePrefix(toast.tone), "\u00A0", toast.message, "\u00A0"] }) }, toast.id))) }));
40
44
  };
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ export declare const getHeight: () => number;
3
+ /** Renders a bold title for panel content sections. */
4
+ export declare const ContentTitle: React.FC<{
5
+ title: string;
6
+ }>;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ const TITLE_ROWS = 1;
4
+ const TITLE_MARGIN_BOTTOM = 1;
5
+ export const getHeight = () => TITLE_ROWS + TITLE_MARGIN_BOTTOM;
6
+ /** Renders a bold title for panel content sections. */
7
+ export const ContentTitle = ({ title }) => (_jsx(Box, { marginBottom: TITLE_MARGIN_BOTTOM, children: _jsx(Text, { bold: true, children: title }) }));
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ interface StatusDisplay {
3
+ icon: string;
4
+ color: string;
5
+ }
6
+ export declare const resolveDockerServiceStatus: (status: string) => StatusDisplay;
7
+ export declare const dockerServiceStatusColor: (status: string) => string;
8
+ /** Renders Docker service health/state without workflow status semantics. */
9
+ export declare const DockerServiceStatusIcon: React.FC<{
10
+ status: string;
11
+ }>;
12
+ export {};
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ const DOCKER_SERVICE_STATUS_MAP = {
4
+ healthy: { icon: '●', color: 'green' },
5
+ unhealthy: { icon: '○', color: 'red' },
6
+ starting: { icon: '◐', color: 'yellow' },
7
+ none: { icon: '●', color: 'blue' },
8
+ running: { icon: '●', color: 'green' },
9
+ created: { icon: '◐', color: 'yellow' },
10
+ exited: { icon: '✗', color: 'red' }
11
+ };
12
+ const DEFAULT_DISPLAY = { icon: '?', color: 'white' };
13
+ export const resolveDockerServiceStatus = (status) => DOCKER_SERVICE_STATUS_MAP[status] ?? DEFAULT_DISPLAY;
14
+ export const dockerServiceStatusColor = (status) => resolveDockerServiceStatus(status).color;
15
+ /** Renders Docker service health/state without workflow status semantics. */
16
+ export const DockerServiceStatusIcon = ({ status }) => {
17
+ const { icon, color } = resolveDockerServiceStatus(status);
18
+ return _jsx(Text, { color: color, children: icon });
19
+ };
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ export declare const InlineSnippet: React.FC<{
3
+ content: string;
4
+ }>;
@@ -0,0 +1,3 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ export const InlineSnippet = ({ content }) => (_jsx(Text, { color: "magenta", italic: true, children: content }));
@@ -1,21 +1,22 @@
1
1
  import React from 'react';
2
- import { type CommandHint } from '#views/dev/chrome/footer.js';
2
+ interface DetailRenderInfo {
3
+ detailRows: number;
4
+ }
3
5
  /**
4
6
  * Generic two-pane shell shared by every panel that has a windowed list
5
- * on top, a horizontal rule, a detail pane below, and a footer at the
6
- * bottom. Panels keep their own selection state and detail rendering;
7
- * the shell owns the layout invariant (windowing, overflow indicators,
8
- * separator, footer) so it lives in one place.
7
+ * on top and a detail pane below. Panels keep their own selection state
8
+ * and detail rendering; the shell owns the layout invariant (windowing,
9
+ * overflow indicators, separator) so it lives in one place.
9
10
  */
10
11
  export interface MasterDetailPanelProps<T> {
11
12
  items: T[];
12
13
  selectedIndex: number;
14
+ height?: number;
13
15
  visibleRows: number;
14
16
  renderHeader: () => React.ReactNode;
15
17
  renderRow: (item: T, selected: boolean, absoluteIndex: number) => React.ReactNode;
16
18
  rowKey: (item: T, absoluteIndex: number) => string;
17
- detail: React.ReactNode;
18
- hints: CommandHint[];
19
- itemLabel: string;
19
+ detail: React.ReactNode | ((info: DetailRenderInfo) => React.ReactNode);
20
20
  }
21
21
  export declare const MasterDetailPanel: <T extends object>(props: MasterDetailPanelProps<T>) => React.ReactElement;
22
+ export {};
@@ -1,18 +1,21 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { Box, Text } from 'ink';
4
- import { Footer } from '#views/dev/chrome/footer.js';
5
- import { HorizontalRule } from '#views/dev/chrome/divider.js';
6
4
  import { computeWindowStart } from '#views/dev/utils/panel_helpers.js';
5
+ const HEADER_ROWS = 1;
6
+ const DETAIL_BORDER_ROWS = 2;
7
7
  const OverflowIndicator = ({ direction, count }) => (_jsxs(Text, { dimColor: true, children: [" ", direction === 'up' ? '↑' : '↓', " ", count, " more ", direction === 'up' ? 'above' : 'below'] }));
8
8
  export const MasterDetailPanel = (props) => {
9
- const { items, selectedIndex, visibleRows, renderHeader, renderRow, rowKey, detail, hints, itemLabel } = props;
9
+ const { items, selectedIndex, height, visibleRows, renderHeader, renderRow, rowKey, detail } = props;
10
10
  const windowStart = computeWindowStart(selectedIndex, items.length, visibleRows);
11
11
  const visible = items.slice(windowStart, windowStart + visibleRows);
12
12
  const overflowAbove = windowStart;
13
13
  const overflowBelow = items.length - (windowStart + visible.length);
14
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { flexDirection: "column", children: [renderHeader(), overflowAbove > 0 && _jsx(OverflowIndicator, { direction: "up", count: overflowAbove }), visible.map((item, i) => {
14
+ const listRows = HEADER_ROWS + visible.length + (overflowAbove > 0 ? 1 : 0) + (overflowBelow > 0 ? 1 : 0);
15
+ const detailRows = Math.max(1, (height ?? 1) - listRows - DETAIL_BORDER_ROWS);
16
+ const renderedDetail = typeof detail === 'function' ? detail({ detailRows }) : detail;
17
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [renderHeader(), overflowAbove > 0 && _jsx(OverflowIndicator, { direction: "up", count: overflowAbove }), visible.map((item, i) => {
15
18
  const absoluteIndex = windowStart + i;
16
19
  return (_jsx(React.Fragment, { children: renderRow(item, absoluteIndex === selectedIndex, absoluteIndex) }, rowKey(item, absoluteIndex)));
17
- }), overflowBelow > 0 && _jsx(OverflowIndicator, { direction: "down", count: overflowBelow })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(HorizontalRule, {}) }), detail, _jsx(Footer, { hints: hints, itemCount: items.length, itemLabel: itemLabel })] }));
20
+ }), overflowBelow > 0 && _jsx(OverflowIndicator, { direction: "down", count: overflowBelow })] }), _jsx(Box, { borderStyle: "single", borderColor: "blackBright", paddingX: 1, flexGrow: 1, children: renderedDetail })] }));
18
21
  };
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import type { WorkflowRun } from '#services/workflow_runs.js';
3
+ export declare const RunInfoSidebar: React.FC<{
4
+ run: WorkflowRun;
5
+ resultStatus?: string | null;
6
+ maxRows?: number;
7
+ }>;
@@ -0,0 +1,19 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { WorkflowStatusIcon, workflowStatusColor } from '#views/dev/components/workflow_status.js';
4
+ import { elapsedMs, formatDate, formatDurationCompact } from '#utils/date_formatter.js';
5
+ const SidebarKV = ({ label, value }) => (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { dimColor: true, bold: true, children: [label, ":\u00A0"] }), _jsx(Text, { bold: true, wrap: "wrap", children: value })] }));
6
+ export const RunInfoSidebar = ({ run, resultStatus, maxRows }) => {
7
+ const status = resultStatus ?? run.status ?? 'unknown';
8
+ const duration = run.startedAt ? formatDurationCompact(elapsedMs(run.startedAt, run.completedAt)) : '-';
9
+ const rows = [
10
+ _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "RUN STATUS\u00A0" }), _jsx(WorkflowStatusIcon, { status: status }), _jsx(Text, { children: "\u00A0" }), _jsx(Text, { bold: true, color: workflowStatusColor(status), children: status.toUpperCase() })] }, "status"),
11
+ _jsx(SidebarKV, { label: "RUN ID", value: run.runId ?? '-' }, "run-id"),
12
+ _jsx(SidebarKV, { label: "WORKFLOW ID", value: run.workflowId ?? '-' }, "workflow-id"),
13
+ _jsx(SidebarKV, { label: "TYPE", value: run.workflowType ?? '-' }, "type"),
14
+ _jsx(SidebarKV, { label: "DURATION", value: duration }, "duration"),
15
+ _jsx(SidebarKV, { label: "START", value: formatDate(run.startedAt) }, "start"),
16
+ _jsx(SidebarKV, { label: "END", value: run.completedAt ? formatDate(run.completedAt) : '' }, "end")
17
+ ];
18
+ return (_jsx(Box, { flexDirection: "column", gap: maxRows === undefined ? 1 : 0, children: rows.slice(0, maxRows) }));
19
+ };
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ interface StatusDisplay {
3
+ icon: string;
4
+ color: string;
5
+ }
6
+ export declare const resolveWorkflowStatus: (status: string) => StatusDisplay;
7
+ export declare const workflowStatusColor: (status: string) => string;
8
+ /** Renders workflow, run, and step status without Docker status semantics. */
9
+ export declare const WorkflowStatusIcon: React.FC<{
10
+ status: string;
11
+ }>;
12
+ export {};
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ const WORKFLOW_STATUS_MAP = {
4
+ running: { icon: '●', color: 'yellow' },
5
+ completed: { icon: '●', color: 'green' },
6
+ failed: { icon: '✗', color: 'red' },
7
+ canceled: { icon: '○', color: 'gray' },
8
+ terminated: { icon: '✗', color: 'gray' },
9
+ timed_out: { icon: '✗', color: 'red' },
10
+ continued: { icon: '↻', color: 'blue' }
11
+ };
12
+ const DEFAULT_DISPLAY = { icon: '?', color: 'white' };
13
+ export const resolveWorkflowStatus = (status) => WORKFLOW_STATUS_MAP[status] ?? DEFAULT_DISPLAY;
14
+ export const workflowStatusColor = (status) => resolveWorkflowStatus(status).color;
15
+ /** Renders workflow, run, and step status without Docker status semantics. */
16
+ export const WorkflowStatusIcon = ({ status }) => {
17
+ const { icon, color } = resolveWorkflowStatus(status);
18
+ return _jsx(Text, { color: color, children: icon });
19
+ };