@outputai/cli 0.4.1-next.6bc541c.0 → 0.4.1-next.94453f5.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 (67) hide show
  1. package/dist/api/generated/api.d.ts +21 -23
  2. package/dist/api/generated/api.js +0 -4
  3. package/dist/assets/docker/docker-compose-dev.yml +1 -4
  4. package/dist/generated/framework_version.json +1 -1
  5. package/dist/utils/format_workflow_result.spec.js +4 -0
  6. package/dist/views/dev/chrome/footer.d.ts +5 -4
  7. package/dist/views/dev/chrome/footer.js +12 -2
  8. package/dist/views/dev/chrome/header.d.ts +2 -1
  9. package/dist/views/dev/chrome/header.js +18 -47
  10. package/dist/views/dev/chrome/header.spec.js +6 -46
  11. package/dist/views/dev/chrome/layout_heights.spec.d.ts +1 -0
  12. package/dist/views/dev/chrome/layout_heights.spec.js +19 -0
  13. package/dist/views/dev/chrome/search_bar.d.ts +1 -1
  14. package/dist/views/dev/chrome/search_bar.js +10 -11
  15. package/dist/views/dev/chrome/tab_bar.d.ts +8 -1
  16. package/dist/views/dev/chrome/tab_bar.js +16 -2
  17. package/dist/views/dev/chrome/toasts.d.ts +1 -0
  18. package/dist/views/dev/chrome/toasts.js +8 -4
  19. package/dist/views/dev/components/content_title.d.ts +6 -0
  20. package/dist/views/dev/components/content_title.js +7 -0
  21. package/dist/views/dev/components/docker_service_status.d.ts +12 -0
  22. package/dist/views/dev/components/docker_service_status.js +19 -0
  23. package/dist/views/dev/components/inline_snippet.d.ts +4 -0
  24. package/dist/views/dev/components/inline_snippet.js +3 -0
  25. package/dist/views/dev/components/master_detail_panel.d.ts +9 -8
  26. package/dist/views/dev/components/master_detail_panel.js +8 -5
  27. package/dist/views/dev/components/run_info_sidebar.d.ts +7 -0
  28. package/dist/views/dev/components/run_info_sidebar.js +19 -0
  29. package/dist/views/dev/components/workflow_status.d.ts +12 -0
  30. package/dist/views/dev/components/workflow_status.js +19 -0
  31. package/dist/views/dev/dev_app.js +107 -31
  32. package/dist/views/dev/hooks/use_run_detail.js +6 -9
  33. package/dist/views/dev/hooks/use_run_detail.spec.js +7 -0
  34. package/dist/views/dev/modals/expanded_json_modal.js +5 -6
  35. package/dist/views/dev/modals/modal_frame.d.ts +13 -0
  36. package/dist/views/dev/modals/modal_frame.js +13 -0
  37. package/dist/views/dev/modals/run_modal.js +23 -13
  38. package/dist/views/dev/{panels/run_detail_view.d.ts → modals/steps_modal.d.ts} +2 -1
  39. package/dist/views/dev/modals/steps_modal.js +102 -0
  40. package/dist/views/dev/panels/help_panel.d.ts +14 -0
  41. package/dist/views/dev/panels/help_panel.js +19 -21
  42. package/dist/views/dev/panels/runs_panel.d.ts +6 -2
  43. package/dist/views/dev/panels/runs_panel.js +82 -83
  44. package/dist/views/dev/panels/runs_panel.spec.js +1 -28
  45. package/dist/views/dev/panels/services_panel.d.ts +6 -0
  46. package/dist/views/dev/panels/services_panel.js +53 -62
  47. package/dist/views/dev/panels/workflows_panel.d.ts +6 -0
  48. package/dist/views/dev/panels/workflows_panel.js +21 -29
  49. package/dist/views/dev/panels/workflows_panel.spec.d.ts +1 -0
  50. package/dist/views/dev/panels/workflows_panel.spec.js +39 -0
  51. package/dist/views/dev/state/ui_state.d.ts +7 -3
  52. package/dist/views/dev/state/ui_state.js +23 -6
  53. package/dist/views/dev/utils/constants.d.ts +2 -2
  54. package/dist/views/dev/utils/constants.js +2 -2
  55. package/dist/views/dev/utils/json_editor.js +3 -3
  56. package/dist/views/dev/utils/json_render.d.ts +2 -0
  57. package/dist/views/dev/utils/json_render.js +48 -6
  58. package/dist/views/dev/utils/json_render.spec.js +9 -1
  59. package/dist/views/dev/utils/panel_helpers.d.ts +15 -0
  60. package/dist/views/dev/utils/panel_helpers.js +30 -0
  61. package/dist/views/dev/utils/panel_helpers.spec.js +46 -1
  62. package/package.json +4 -4
  63. package/dist/components/status_icon.d.ts +0 -11
  64. package/dist/components/status_icon.js +0 -25
  65. package/dist/views/dev/chrome/divider.d.ts +0 -8
  66. package/dist/views/dev/chrome/divider.js +0 -16
  67. package/dist/views/dev/panels/run_detail_view.js +0 -112
@@ -207,6 +207,16 @@ export interface WorkflowStatusResponse {
207
207
  /** An epoch timestamp representing when the workflow ended */
208
208
  completedAt?: number;
209
209
  }
210
+ export type WorkflowResultResponseAttributesItem = {
211
+ [key: string]: unknown;
212
+ };
213
+ /**
214
+ * Convenience totals derived from attributes
215
+ * @nullable
216
+ */
217
+ export type WorkflowResultResponseAggregations = {
218
+ [key: string]: unknown;
219
+ } | null;
210
220
  /**
211
221
  * The workflow execution status
212
222
  */
@@ -229,6 +239,16 @@ export interface WorkflowResultResponse {
229
239
  /** The result of workflow, null if workflow failed */
230
240
  output?: unknown;
231
241
  trace?: TraceInfo;
242
+ /**
243
+ * Durable workflow attributes collected during execution
244
+ * @nullable
245
+ */
246
+ attributes?: WorkflowResultResponseAttributesItem[] | null;
247
+ /**
248
+ * Convenience totals derived from attributes
249
+ * @nullable
250
+ */
251
+ aggregations?: WorkflowResultResponseAggregations;
232
252
  /** The workflow execution status */
233
253
  status?: WorkflowResultResponseStatus;
234
254
  /**
@@ -303,28 +323,6 @@ export type PostWorkflowRunBody = {
303
323
  /** (Optional) The max time to wait for the execution, defaults to 30s */
304
324
  timeout?: number;
305
325
  };
306
- /**
307
- * The workflow execution status
308
- */
309
- export type PostWorkflowRun200Status = typeof PostWorkflowRun200Status[keyof typeof PostWorkflowRun200Status];
310
- export declare const PostWorkflowRun200Status: {
311
- readonly completed: "completed";
312
- readonly failed: "failed";
313
- };
314
- export type PostWorkflowRun200 = {
315
- /** The workflow execution id */
316
- workflowId?: string;
317
- /** The output of the workflow, null if workflow failed */
318
- output?: unknown;
319
- trace?: TraceInfo;
320
- /** The workflow execution status */
321
- status?: PostWorkflowRun200Status;
322
- /**
323
- * Error message if workflow failed, null otherwise
324
- * @nullable
325
- */
326
- error?: string | null;
327
- };
328
326
  export type PostWorkflowStartBody = {
329
327
  /** The name of the workflow to execute */
330
328
  workflowName: string;
@@ -518,7 +516,7 @@ export declare const getHealth: (options?: ApiRequestOptions) => Promise<getHeal
518
516
  * @summary Execute a workflow synchronously
519
517
  */
520
518
  export type postWorkflowRunResponse200 = {
521
- data: PostWorkflowRun200;
519
+ data: WorkflowResultResponse;
522
520
  status: 200;
523
521
  };
524
522
  export type postWorkflowRunResponse400 = {
@@ -42,10 +42,6 @@ export const WorkflowResultResponseStatus = {
42
42
  timed_out: 'timed_out',
43
43
  continued: 'continued',
44
44
  };
45
- export const PostWorkflowRun200Status = {
46
- completed: 'completed',
47
- failed: 'failed',
48
- };
49
45
  ;
50
46
  export const getGetHealthUrl = () => {
51
47
  return `/health`;
@@ -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.6bc541c.0}
80
+ image: outputai/api:${OUTPUT_API_VERSION:-0.4.1-next.94453f5.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.6bc541c.0"
2
+ "framework": "0.4.1-next.94453f5.0"
3
3
  }
@@ -71,11 +71,15 @@ describe('formatWorkflowResult', () => {
71
71
  workflowId: 'wf-456',
72
72
  status: 'failed',
73
73
  output: null,
74
+ attributes: [{ type: 'llm:usage', total: 0.4 }],
75
+ aggregations: { cost: { total: 0.4 }, tokens: { total: 20 }, httpRequests: { total: 0 } },
74
76
  error: 'Activity task failed'
75
77
  };
76
78
  const output = formatOutput(data, 'json', formatWorkflowResult);
77
79
  const parsed = JSON.parse(output);
78
80
  expect(parsed.status).toBe('failed');
81
+ expect(parsed.attributes).toEqual([{ type: 'llm:usage', total: 0.4 }]);
82
+ expect(parsed.aggregations).toEqual({ cost: { total: 0.4 }, tokens: { total: 20 }, httpRequests: { total: 0 } });
79
83
  expect(parsed.error).toBe('Activity task failed');
80
84
  });
81
85
  });
@@ -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
+ };