@outputai/cli 0.4.1-next.fb7438a.0 → 0.5.1-next.2dc6fd0.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.
- package/dist/api/generated/api.d.ts +21 -23
- package/dist/api/generated/api.js +0 -4
- package/dist/assets/docker/docker-compose-dev.yml +1 -4
- package/dist/generated/framework_version.json +1 -1
- package/dist/utils/format_workflow_result.spec.js +4 -0
- package/dist/views/dev/chrome/footer.d.ts +5 -4
- package/dist/views/dev/chrome/footer.js +12 -2
- package/dist/views/dev/chrome/header.d.ts +2 -1
- package/dist/views/dev/chrome/header.js +18 -47
- package/dist/views/dev/chrome/header.spec.js +6 -46
- package/dist/views/dev/chrome/layout_heights.spec.d.ts +1 -0
- package/dist/views/dev/chrome/layout_heights.spec.js +19 -0
- package/dist/views/dev/chrome/search_bar.d.ts +1 -1
- package/dist/views/dev/chrome/search_bar.js +10 -11
- package/dist/views/dev/chrome/tab_bar.d.ts +8 -1
- package/dist/views/dev/chrome/tab_bar.js +16 -2
- package/dist/views/dev/chrome/toasts.d.ts +1 -0
- package/dist/views/dev/chrome/toasts.js +8 -4
- package/dist/views/dev/components/content_title.d.ts +6 -0
- package/dist/views/dev/components/content_title.js +7 -0
- package/dist/views/dev/components/docker_service_status.d.ts +12 -0
- package/dist/views/dev/components/docker_service_status.js +19 -0
- package/dist/views/dev/components/inline_snippet.d.ts +4 -0
- package/dist/views/dev/components/inline_snippet.js +3 -0
- package/dist/views/dev/components/master_detail_panel.d.ts +9 -8
- package/dist/views/dev/components/master_detail_panel.js +8 -5
- package/dist/views/dev/components/run_info_sidebar.d.ts +7 -0
- package/dist/views/dev/components/run_info_sidebar.js +19 -0
- package/dist/views/dev/components/workflow_status.d.ts +12 -0
- package/dist/views/dev/components/workflow_status.js +19 -0
- package/dist/views/dev/dev_app.js +107 -31
- package/dist/views/dev/hooks/use_run_detail.js +6 -9
- package/dist/views/dev/hooks/use_run_detail.spec.js +7 -0
- package/dist/views/dev/modals/expanded_json_modal.js +5 -6
- package/dist/views/dev/modals/modal_frame.d.ts +13 -0
- package/dist/views/dev/modals/modal_frame.js +13 -0
- package/dist/views/dev/modals/run_modal.js +23 -13
- package/dist/views/dev/{panels/run_detail_view.d.ts → modals/steps_modal.d.ts} +2 -1
- package/dist/views/dev/modals/steps_modal.js +102 -0
- package/dist/views/dev/panels/help_panel.d.ts +14 -0
- package/dist/views/dev/panels/help_panel.js +19 -21
- package/dist/views/dev/panels/runs_panel.d.ts +6 -2
- package/dist/views/dev/panels/runs_panel.js +82 -83
- package/dist/views/dev/panels/runs_panel.spec.js +1 -28
- package/dist/views/dev/panels/services_panel.d.ts +6 -0
- package/dist/views/dev/panels/services_panel.js +53 -62
- package/dist/views/dev/panels/workflows_panel.d.ts +6 -0
- package/dist/views/dev/panels/workflows_panel.js +21 -29
- package/dist/views/dev/panels/workflows_panel.spec.d.ts +1 -0
- package/dist/views/dev/panels/workflows_panel.spec.js +39 -0
- package/dist/views/dev/state/ui_state.d.ts +7 -3
- package/dist/views/dev/state/ui_state.js +23 -6
- package/dist/views/dev/utils/constants.d.ts +2 -2
- package/dist/views/dev/utils/constants.js +2 -2
- package/dist/views/dev/utils/json_editor.js +3 -3
- package/dist/views/dev/utils/json_render.d.ts +2 -0
- package/dist/views/dev/utils/json_render.js +48 -6
- package/dist/views/dev/utils/json_render.spec.js +9 -1
- package/dist/views/dev/utils/panel_helpers.d.ts +15 -0
- package/dist/views/dev/utils/panel_helpers.js +30 -0
- package/dist/views/dev/utils/panel_helpers.spec.js +46 -1
- package/package.json +4 -4
- package/dist/components/status_icon.d.ts +0 -11
- package/dist/components/status_icon.js +0 -25
- package/dist/views/dev/chrome/divider.d.ts +0 -8
- package/dist/views/dev/chrome/divider.js +0 -16
- 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:
|
|
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.
|
|
80
|
+
image: outputai/api:${OUTPUT_API_VERSION:-0.5.1-next.2dc6fd0.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'
|
|
@@ -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
|
|
7
|
-
hints
|
|
7
|
+
export interface FooterState {
|
|
8
|
+
hints?: CommandHint[];
|
|
8
9
|
itemCount?: number;
|
|
9
10
|
itemLabel?: string;
|
|
10
|
-
}
|
|
11
|
-
export declare const
|
|
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
|
-
|
|
5
|
-
|
|
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
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export const
|
|
19
|
-
const
|
|
20
|
-
|
|
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 = ({
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
72
|
-
const
|
|
73
|
-
return (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", alignItems: "
|
|
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 {
|
|
3
|
-
describe('
|
|
4
|
-
it('
|
|
5
|
-
expect(
|
|
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('
|
|
8
|
-
expect(
|
|
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,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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
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
|
+
}) }));
|
|
@@ -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",
|
|
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,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
|
+
};
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
-
|
|
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, {
|
|
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,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
|
+
};
|