@outputai/cli 0.3.2-next.5e221e8.0 → 0.3.3-dev.2650161.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/docker/docker-compose-dev.yml +1 -1
- package/dist/commands/dev/index.js +48 -13
- package/dist/commands/dev/index.spec.js +1 -2
- package/dist/components/status_icon.js +1 -1
- package/dist/generated/framework_version.json +1 -1
- package/dist/services/docker.js +0 -1
- package/dist/templates/project/src/workflows/blog_evaluator/prompts/signal_noise@v1.prompt.template +2 -1
- package/dist/templates/workflow/README.md.template +2 -1
- package/dist/templates/workflow/prompts/example@v1.prompt.template +2 -1
- package/dist/utils/paths.d.ts +11 -0
- package/dist/utils/paths.js +14 -0
- package/dist/utils/scenario_resolver.d.ts +2 -1
- package/dist/utils/scenario_resolver.js +57 -25
- package/dist/utils/scenario_resolver.spec.js +30 -1
- package/dist/views/dev/chrome/divider.d.ts +8 -0
- package/dist/views/dev/chrome/divider.js +16 -0
- package/dist/views/dev/chrome/footer.d.ts +11 -0
- package/dist/views/dev/chrome/footer.js +10 -0
- package/dist/views/dev/chrome/header.d.ts +21 -0
- package/dist/views/dev/chrome/header.js +74 -0
- package/dist/views/dev/chrome/header.spec.d.ts +1 -0
- package/dist/views/dev/chrome/header.spec.js +50 -0
- package/dist/views/dev/chrome/loading_spinner.d.ts +9 -0
- package/dist/views/dev/chrome/loading_spinner.js +9 -0
- package/dist/views/dev/chrome/palette.d.ts +16 -0
- package/dist/views/dev/chrome/palette.js +16 -0
- package/dist/views/dev/chrome/search_bar.d.ts +5 -0
- package/dist/views/dev/chrome/search_bar.js +35 -0
- package/dist/views/dev/chrome/selection_indicator.d.ts +14 -0
- package/dist/views/dev/chrome/selection_indicator.js +13 -0
- package/dist/views/dev/chrome/tab_bar.d.ts +5 -0
- package/dist/views/dev/chrome/tab_bar.js +4 -0
- package/dist/views/dev/chrome/toasts.d.ts +2 -0
- package/dist/views/dev/chrome/toasts.js +40 -0
- package/dist/views/dev/components/master_detail_panel.d.ts +21 -0
- package/dist/views/dev/components/master_detail_panel.js +18 -0
- package/dist/views/{dev.d.ts → dev/dev_app.d.ts} +1 -0
- package/dist/views/dev/dev_app.js +146 -0
- package/dist/views/dev/hooks/use_docker_logs.d.ts +7 -0
- package/dist/views/dev/hooks/use_docker_logs.js +69 -0
- package/dist/views/dev/hooks/use_poll.d.ts +16 -0
- package/dist/views/dev/hooks/use_poll.js +95 -0
- package/dist/views/dev/hooks/use_run_detail.d.ts +21 -0
- package/dist/views/dev/hooks/use_run_detail.js +153 -0
- package/dist/views/dev/hooks/use_run_detail.spec.d.ts +1 -0
- package/dist/views/dev/hooks/use_run_detail.spec.js +86 -0
- package/dist/views/dev/hooks/use_workflow_catalog.d.ts +2 -0
- package/dist/views/dev/hooks/use_workflow_catalog.js +21 -0
- package/dist/views/dev/modals/expanded_json_modal.d.ts +2 -0
- package/dist/views/dev/modals/expanded_json_modal.js +44 -0
- package/dist/views/dev/modals/run_modal.d.ts +5 -0
- package/dist/views/dev/modals/run_modal.js +213 -0
- package/dist/views/dev/panels/help_panel.d.ts +2 -0
- package/dist/views/dev/panels/help_panel.js +53 -0
- package/dist/views/dev/panels/run_detail_view.d.ts +5 -0
- package/dist/views/dev/panels/run_detail_view.js +112 -0
- package/dist/views/dev/panels/runs_panel.d.ts +8 -0
- package/dist/views/dev/panels/runs_panel.js +204 -0
- package/dist/views/dev/panels/runs_panel.spec.d.ts +1 -0
- package/dist/views/dev/panels/runs_panel.spec.js +82 -0
- package/dist/views/dev/panels/services_panel.d.ts +14 -0
- package/dist/views/dev/panels/services_panel.js +155 -0
- package/dist/views/dev/panels/services_panel.spec.d.ts +1 -0
- package/dist/views/dev/panels/services_panel.spec.js +28 -0
- package/dist/views/dev/panels/workflows_panel.d.ts +7 -0
- package/dist/views/dev/panels/workflows_panel.js +111 -0
- package/dist/views/dev/services/docker_control.d.ts +5 -0
- package/dist/views/dev/services/docker_control.js +25 -0
- package/dist/views/dev/services/run_workflow.d.ts +10 -0
- package/dist/views/dev/services/run_workflow.js +14 -0
- package/dist/views/dev/services/scenario_io.d.ts +2 -0
- package/dist/views/dev/services/scenario_io.js +41 -0
- package/dist/views/dev/state/ui_state.d.ts +61 -0
- package/dist/views/dev/state/ui_state.js +64 -0
- package/dist/views/dev/utils/constants.d.ts +17 -0
- package/dist/views/dev/utils/constants.js +17 -0
- package/dist/views/dev/utils/json_editor.d.ts +21 -0
- package/dist/views/dev/utils/json_editor.js +117 -0
- package/dist/views/dev/utils/json_editor.spec.d.ts +1 -0
- package/dist/views/dev/utils/json_editor.spec.js +57 -0
- package/dist/views/dev/utils/json_render.d.ts +15 -0
- package/dist/views/dev/utils/json_render.js +77 -0
- package/dist/views/dev/utils/json_render.spec.d.ts +1 -0
- package/dist/views/dev/utils/json_render.spec.js +65 -0
- package/dist/views/dev/utils/panel_helpers.d.ts +16 -0
- package/dist/views/dev/utils/panel_helpers.js +32 -0
- package/dist/views/dev/utils/panel_helpers.spec.d.ts +1 -0
- package/dist/views/dev/utils/panel_helpers.spec.js +47 -0
- package/package.json +5 -5
- package/dist/components/command_footer.d.ts +0 -8
- package/dist/components/command_footer.js +0 -4
- package/dist/components/workflow_summary.d.ts +0 -10
- package/dist/components/workflow_summary.js +0 -4
- package/dist/views/dev.js +0 -187
- package/dist/views/workflow/list.d.ts +0 -6
- package/dist/views/workflow/list.js +0 -129
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
const RAW_TOKEN_RE = /"(?:\\.|[^"\\])*"|true|false|null|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|[{}\[\]:,]|\s+/g;
|
|
4
|
+
const KEY_COLOR = 'cyan';
|
|
5
|
+
const STRING_COLOR = 'green';
|
|
6
|
+
const NUMBER_COLOR = 'yellow';
|
|
7
|
+
const BOOLEAN_COLOR = 'magenta';
|
|
8
|
+
const NULL_COLOR = 'red';
|
|
9
|
+
const PUNCT_COLOR = 'gray';
|
|
10
|
+
const classifyRaw = (text) => {
|
|
11
|
+
if (/^\s+$/.test(text)) {
|
|
12
|
+
return { kind: 'ws', text };
|
|
13
|
+
}
|
|
14
|
+
if (text.startsWith('"')) {
|
|
15
|
+
return { kind: 'string', text };
|
|
16
|
+
}
|
|
17
|
+
if (text === 'true' || text === 'false' || text === 'null') {
|
|
18
|
+
return { kind: 'lit', text };
|
|
19
|
+
}
|
|
20
|
+
if (/^-?\d/.test(text)) {
|
|
21
|
+
return { kind: 'num', text };
|
|
22
|
+
}
|
|
23
|
+
return { kind: 'punct', text };
|
|
24
|
+
};
|
|
25
|
+
export const tokenizeLine = (line) => {
|
|
26
|
+
const raws = Array.from(line.matchAll(RAW_TOKEN_RE), m => classifyRaw(m[0]));
|
|
27
|
+
return raws.map((raw, idx) => {
|
|
28
|
+
if (raw.kind === 'ws') {
|
|
29
|
+
return { text: raw.text };
|
|
30
|
+
}
|
|
31
|
+
if (raw.kind === 'string') {
|
|
32
|
+
const next = raws.slice(idx + 1).find(r => r.kind !== 'ws');
|
|
33
|
+
const isKey = next?.kind === 'punct' && next.text === ':';
|
|
34
|
+
return { text: raw.text, color: isKey ? KEY_COLOR : STRING_COLOR };
|
|
35
|
+
}
|
|
36
|
+
if (raw.kind === 'lit') {
|
|
37
|
+
return { text: raw.text, color: raw.text === 'null' ? NULL_COLOR : BOOLEAN_COLOR };
|
|
38
|
+
}
|
|
39
|
+
if (raw.kind === 'num') {
|
|
40
|
+
return { text: raw.text, color: NUMBER_COLOR };
|
|
41
|
+
}
|
|
42
|
+
return { text: raw.text, color: PUNCT_COLOR };
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
export const formatJsonText = (value) => {
|
|
46
|
+
if (value === undefined || value === null) {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return JSON.stringify(value, null, 2);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return String(value);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const renderTokens = (tokens) => tokens.map((token, i) => token.color ?
|
|
57
|
+
_jsx(Text, { color: token.color, children: token.text }, i) :
|
|
58
|
+
_jsx(Text, { children: token.text }, i));
|
|
59
|
+
export const countJsonLines = (value) => {
|
|
60
|
+
const text = formatJsonText(value);
|
|
61
|
+
return text ? text.split('\n').length : 0;
|
|
62
|
+
};
|
|
63
|
+
export const JsonView = ({ value, maxLines, offset = 0, truncateLine = true, showOverflowFooter = true }) => {
|
|
64
|
+
if (value === undefined || value === null) {
|
|
65
|
+
return _jsx(Text, { dimColor: true, children: "\u2014" });
|
|
66
|
+
}
|
|
67
|
+
const text = formatJsonText(value);
|
|
68
|
+
if (!text) {
|
|
69
|
+
return _jsx(Text, { dimColor: true, children: "\u2014" });
|
|
70
|
+
}
|
|
71
|
+
const allLines = text.split('\n');
|
|
72
|
+
const start = Math.max(0, offset);
|
|
73
|
+
const end = typeof maxLines === 'number' ? start + maxLines : undefined;
|
|
74
|
+
const visible = allLines.slice(start, end);
|
|
75
|
+
const overflowBelow = end !== undefined ? Math.max(0, allLines.length - end) : 0;
|
|
76
|
+
return (_jsxs(Box, { flexDirection: "column", children: [visible.map((line, i) => (_jsx(Text, { wrap: truncateLine ? 'truncate-end' : 'wrap', children: renderTokens(tokenizeLine(line)) }, i))), showOverflowFooter && overflowBelow > 0 && (_jsxs(Text, { dimColor: true, children: ["\u2026 ", overflowBelow, " more line", overflowBelow === 1 ? '' : 's'] }))] }));
|
|
77
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { tokenizeLine, formatJsonText, countJsonLines } from './json_render.js';
|
|
3
|
+
describe('tokenizeLine', () => {
|
|
4
|
+
it('classifies an object key (followed by colon) as a key', () => {
|
|
5
|
+
const tokens = tokenizeLine(' "name": "ada"');
|
|
6
|
+
const key = tokens.find(t => t.text === '"name"');
|
|
7
|
+
const value = tokens.find(t => t.text === '"ada"');
|
|
8
|
+
expect(key?.color).toBe('cyan');
|
|
9
|
+
expect(value?.color).toBe('green');
|
|
10
|
+
});
|
|
11
|
+
it('classifies a numeric value', () => {
|
|
12
|
+
const tokens = tokenizeLine(' "age": 42');
|
|
13
|
+
const num = tokens.find(t => t.text === '42');
|
|
14
|
+
expect(num?.color).toBe('yellow');
|
|
15
|
+
});
|
|
16
|
+
it('classifies booleans as magenta', () => {
|
|
17
|
+
const tokens = tokenizeLine(' "active": true');
|
|
18
|
+
expect(tokens.find(t => t.text === 'true')?.color).toBe('magenta');
|
|
19
|
+
});
|
|
20
|
+
it('classifies null as red', () => {
|
|
21
|
+
const tokens = tokenizeLine(' "owner": null');
|
|
22
|
+
expect(tokens.find(t => t.text === 'null')?.color).toBe('red');
|
|
23
|
+
});
|
|
24
|
+
it('colours punctuation gray', () => {
|
|
25
|
+
const tokens = tokenizeLine('{},[]:');
|
|
26
|
+
for (const t of tokens) {
|
|
27
|
+
expect(t.color).toBe('gray');
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
it('preserves whitespace tokens with no colour', () => {
|
|
31
|
+
const tokens = tokenizeLine(' "x"');
|
|
32
|
+
const ws = tokens.find(t => /^\s+$/.test(t.text));
|
|
33
|
+
expect(ws?.color).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
it('handles escaped quotes inside strings', () => {
|
|
36
|
+
const tokens = tokenizeLine(' "msg": "she said \\"hi\\""');
|
|
37
|
+
const value = tokens.find(t => t.text.includes('hi'));
|
|
38
|
+
expect(value?.color).toBe('green');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('formatJsonText', () => {
|
|
42
|
+
it('returns an empty string for null', () => {
|
|
43
|
+
expect(formatJsonText(null)).toBe('');
|
|
44
|
+
});
|
|
45
|
+
it('returns an empty string for undefined', () => {
|
|
46
|
+
expect(formatJsonText(undefined)).toBe('');
|
|
47
|
+
});
|
|
48
|
+
it('pretty-prints with two-space indent', () => {
|
|
49
|
+
expect(formatJsonText({ a: 1 })).toBe('{\n "a": 1\n}');
|
|
50
|
+
});
|
|
51
|
+
it('falls back to String() when stringify throws', () => {
|
|
52
|
+
const cyclic = {};
|
|
53
|
+
cyclic.self = cyclic;
|
|
54
|
+
expect(formatJsonText(cyclic)).toBe(String(cyclic));
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('countJsonLines', () => {
|
|
58
|
+
it('returns 0 for nullish input', () => {
|
|
59
|
+
expect(countJsonLines(null)).toBe(0);
|
|
60
|
+
expect(countJsonLines(undefined)).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
it('counts lines in pretty-printed output', () => {
|
|
63
|
+
expect(countJsonLines({ a: 1, b: 2 })).toBe(4);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Truncate a string to fit a column, appending an ellipsis when clipped.
|
|
3
|
+
*/
|
|
4
|
+
export declare const truncate: (str: string, max: number) => string;
|
|
5
|
+
/**
|
|
6
|
+
* Format an ISO timestamp into the short `MMM d HH:mm` form used in panel
|
|
7
|
+
* row tables (e.g. `Apr 28 18:56`). Returns `-` when the input is missing
|
|
8
|
+
* or unparseable.
|
|
9
|
+
*/
|
|
10
|
+
export declare const formatStartedShort: (iso: string | undefined) => string;
|
|
11
|
+
/**
|
|
12
|
+
* Compute the index of the first visible row for a windowed list. Keeps
|
|
13
|
+
* the selected row centred when possible and clamps so the window never
|
|
14
|
+
* runs off the end of the array.
|
|
15
|
+
*/
|
|
16
|
+
export declare const computeWindowStart: (selectedIndex: number, total: number, visibleRows: number) => number;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { format, parseISO } from 'date-fns';
|
|
2
|
+
/**
|
|
3
|
+
* Truncate a string to fit a column, appending an ellipsis when clipped.
|
|
4
|
+
*/
|
|
5
|
+
export const truncate = (str, max) => str.length > max ? `${str.slice(0, max - 1)}…` : str;
|
|
6
|
+
/**
|
|
7
|
+
* Format an ISO timestamp into the short `MMM d HH:mm` form used in panel
|
|
8
|
+
* row tables (e.g. `Apr 28 18:56`). Returns `-` when the input is missing
|
|
9
|
+
* or unparseable.
|
|
10
|
+
*/
|
|
11
|
+
export const formatStartedShort = (iso) => {
|
|
12
|
+
if (!iso) {
|
|
13
|
+
return '-';
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
return format(parseISO(iso), 'MMM d HH:mm');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return '-';
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Compute the index of the first visible row for a windowed list. Keeps
|
|
24
|
+
* the selected row centred when possible and clamps so the window never
|
|
25
|
+
* runs off the end of the array.
|
|
26
|
+
*/
|
|
27
|
+
export const computeWindowStart = (selectedIndex, total, visibleRows) => {
|
|
28
|
+
const half = Math.floor(visibleRows / 2);
|
|
29
|
+
const start = Math.max(0, selectedIndex - half);
|
|
30
|
+
const maxStart = Math.max(0, total - visibleRows);
|
|
31
|
+
return Math.min(start, maxStart);
|
|
32
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { truncate, formatStartedShort, computeWindowStart } from './panel_helpers.js';
|
|
3
|
+
describe('truncate', () => {
|
|
4
|
+
it('returns the input unchanged when shorter than max', () => {
|
|
5
|
+
expect(truncate('short', 10)).toBe('short');
|
|
6
|
+
});
|
|
7
|
+
it('returns the input unchanged when exactly equal to max', () => {
|
|
8
|
+
expect(truncate('abcde', 5)).toBe('abcde');
|
|
9
|
+
});
|
|
10
|
+
it('replaces the last character with an ellipsis when too long', () => {
|
|
11
|
+
expect(truncate('abcdefgh', 5)).toBe('abcd…');
|
|
12
|
+
});
|
|
13
|
+
it('handles empty input', () => {
|
|
14
|
+
expect(truncate('', 4)).toBe('');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe('formatStartedShort', () => {
|
|
18
|
+
it('returns `-` when the input is undefined', () => {
|
|
19
|
+
expect(formatStartedShort(undefined)).toBe('-');
|
|
20
|
+
});
|
|
21
|
+
it('returns `-` when the input is empty', () => {
|
|
22
|
+
expect(formatStartedShort('')).toBe('-');
|
|
23
|
+
});
|
|
24
|
+
it('returns `-` when the input is unparseable', () => {
|
|
25
|
+
expect(formatStartedShort('not-an-iso')).toBe('-');
|
|
26
|
+
});
|
|
27
|
+
it('formats a valid ISO timestamp into `MMM d HH:mm`', () => {
|
|
28
|
+
expect(formatStartedShort('2026-04-28T18:56:53Z')).toMatch(/^Apr 28 \d{2}:\d{2}$/);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('computeWindowStart', () => {
|
|
32
|
+
it('centres the selected row in the viewport when possible', () => {
|
|
33
|
+
expect(computeWindowStart(10, 30, 8)).toBe(6);
|
|
34
|
+
});
|
|
35
|
+
it('clamps to 0 when the selected row is near the top', () => {
|
|
36
|
+
expect(computeWindowStart(1, 30, 8)).toBe(0);
|
|
37
|
+
});
|
|
38
|
+
it('clamps so the window never runs off the end', () => {
|
|
39
|
+
expect(computeWindowStart(28, 30, 8)).toBe(22);
|
|
40
|
+
});
|
|
41
|
+
it('returns 0 when the list is shorter than the viewport', () => {
|
|
42
|
+
expect(computeWindowStart(2, 5, 8)).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
it('returns 0 for an empty list', () => {
|
|
45
|
+
expect(computeWindowStart(0, 0, 8)).toBe(0);
|
|
46
|
+
});
|
|
47
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3-dev.2650161.0",
|
|
4
4
|
"description": "CLI for Output.ai workflow generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"debug": "4.4.3",
|
|
28
28
|
"dotenv": "17.4.2",
|
|
29
29
|
"handlebars": "4.7.9",
|
|
30
|
-
"ink": "
|
|
30
|
+
"ink": "7.0.1",
|
|
31
31
|
"ink-spinner": "5.0.0",
|
|
32
32
|
"js-yaml": "4.1.1",
|
|
33
33
|
"json-schema-library": "11.4.0",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"semver": "7.7.4",
|
|
37
37
|
"undici": "8.1.0",
|
|
38
38
|
"yaml": "^2.8.3",
|
|
39
|
-
"@outputai/credentials": "0.3.
|
|
40
|
-
"@outputai/evals": "0.3.
|
|
41
|
-
"@outputai/llm": "0.3.
|
|
39
|
+
"@outputai/credentials": "0.3.3-dev.2650161.0",
|
|
40
|
+
"@outputai/evals": "0.3.3-dev.2650161.0",
|
|
41
|
+
"@outputai/llm": "0.3.3-dev.2650161.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/cli-progress": "3.11.6",
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
export const CommandFooter = ({ hints }) => (_jsx(Box, { marginTop: 1, children: hints.map((hint, i) => (_jsxs(React.Fragment, { children: [i > 0 && _jsx(Text, { dimColor: true, children: ' | ' }), _jsx(Text, { dimColor: true, children: '(' }), _jsx(Text, { dimColor: true, bold: true, children: hint.key }), _jsx(Text, { dimColor: true, children: ')' }), _jsx(Text, { dimColor: true, children: ` ${hint.label}` })] }, hint.key))) }));
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import { statusColor } from '#components/status_icon.js';
|
|
4
|
-
export const WorkflowSummarySection = ({ summary }) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "\uD83D\uDCCB Workflows" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: statusColor('running'), children: [summary.running, " running"] }), _jsx(Text, { children: ", " }), _jsxs(Text, { color: statusColor('failed'), children: [summary.failed, " failed"] }), _jsx(Text, { children: ", " }), _jsxs(Text, { color: statusColor('completed'), children: [summary.completed, " complete"] })] })] }));
|
package/dist/views/dev.js
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect, useRef } from 'react';
|
|
3
|
-
import { Box, Text, Static, useApp, useInput } from 'ink';
|
|
4
|
-
import Spinner from 'ink-spinner';
|
|
5
|
-
import { getServiceStatus, isServiceHealthy, isServiceFailed, SERVICE_HEALTH } from '#services/docker.js';
|
|
6
|
-
import { config } from '#config.js';
|
|
7
|
-
import { fetchWorkflowRuns } from '#services/workflow_runs.js';
|
|
8
|
-
import { openUrl } from '#utils/open_url.js';
|
|
9
|
-
import { StatusIcon } from '#components/status_icon.js';
|
|
10
|
-
import { WorkflowSummarySection } from '#components/workflow_summary.js';
|
|
11
|
-
import { CommandFooter } from '#components/command_footer.js';
|
|
12
|
-
import { WorkflowListView } from '#views/workflow/list.js';
|
|
13
|
-
const POLL_INTERVAL_MS = 2000;
|
|
14
|
-
const HEALTH_TIMEOUT_MS = 120_000;
|
|
15
|
-
const resolveServiceStatus = (service) => service.health === SERVICE_HEALTH.NONE ? service.state : service.health;
|
|
16
|
-
const fetchServices = async (dockerComposePath) => {
|
|
17
|
-
try {
|
|
18
|
-
return await getServiceStatus(dockerComposePath);
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
const usePoll = (enabled, onTick) => {
|
|
25
|
-
const onTickRef = useRef(onTick);
|
|
26
|
-
onTickRef.current = onTick;
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
const state = {
|
|
29
|
-
active: true,
|
|
30
|
-
timeout: undefined
|
|
31
|
-
};
|
|
32
|
-
const run = async () => {
|
|
33
|
-
if (!state.active) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
const result = await onTickRef.current();
|
|
37
|
-
if (!state.active || result === 'done') {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
state.timeout = setTimeout(run, POLL_INTERVAL_MS);
|
|
41
|
-
};
|
|
42
|
-
if (enabled) {
|
|
43
|
-
void run();
|
|
44
|
-
}
|
|
45
|
-
return () => {
|
|
46
|
-
state.active = false;
|
|
47
|
-
clearTimeout(state.timeout);
|
|
48
|
-
};
|
|
49
|
-
}, [enabled]);
|
|
50
|
-
};
|
|
51
|
-
const useHealthPolling = (dockerComposePath, enabled, callbacks) => {
|
|
52
|
-
const callbacksRef = useRef(callbacks);
|
|
53
|
-
callbacksRef.current = callbacks;
|
|
54
|
-
const startTimeRef = useRef(Date.now());
|
|
55
|
-
usePoll(enabled, async () => {
|
|
56
|
-
if (Date.now() - startTimeRef.current > HEALTH_TIMEOUT_MS) {
|
|
57
|
-
callbacksRef.current.onTimeout();
|
|
58
|
-
return 'done';
|
|
59
|
-
}
|
|
60
|
-
const svcs = await fetchServices(dockerComposePath);
|
|
61
|
-
if (svcs === null) {
|
|
62
|
-
return 'continue';
|
|
63
|
-
}
|
|
64
|
-
callbacksRef.current.onServices(svcs);
|
|
65
|
-
if (svcs.length > 0 && svcs.every(isServiceHealthy)) {
|
|
66
|
-
callbacksRef.current.onAllHealthy(svcs);
|
|
67
|
-
return 'done';
|
|
68
|
-
}
|
|
69
|
-
if (svcs.length > 0 && svcs.find(isServiceFailed)) {
|
|
70
|
-
callbacksRef.current.onFailure(svcs);
|
|
71
|
-
return 'done';
|
|
72
|
-
}
|
|
73
|
-
return 'continue';
|
|
74
|
-
});
|
|
75
|
-
};
|
|
76
|
-
const useStatusRefresh = (dockerComposePath, enabled, onServices) => {
|
|
77
|
-
const onServicesRef = useRef(onServices);
|
|
78
|
-
onServicesRef.current = onServices;
|
|
79
|
-
usePoll(enabled, async () => {
|
|
80
|
-
const svcs = await fetchServices(dockerComposePath);
|
|
81
|
-
if (svcs !== null) {
|
|
82
|
-
onServicesRef.current(svcs);
|
|
83
|
-
}
|
|
84
|
-
return 'continue';
|
|
85
|
-
});
|
|
86
|
-
};
|
|
87
|
-
const useWorkflowPolling = (enabled, onRuns) => {
|
|
88
|
-
const onRunsRef = useRef(onRuns);
|
|
89
|
-
onRunsRef.current = onRuns;
|
|
90
|
-
usePoll(enabled, async () => {
|
|
91
|
-
try {
|
|
92
|
-
const { runs } = await fetchWorkflowRuns({ limit: 100 });
|
|
93
|
-
onRunsRef.current(runs);
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
// API may not be ready yet
|
|
97
|
-
}
|
|
98
|
-
return 'continue';
|
|
99
|
-
});
|
|
100
|
-
};
|
|
101
|
-
const useMainViewInput = (isActive, callbacks) => {
|
|
102
|
-
const callbacksRef = useRef(callbacks);
|
|
103
|
-
callbacksRef.current = callbacks;
|
|
104
|
-
useInput(input => {
|
|
105
|
-
if (input === 'o') {
|
|
106
|
-
callbacksRef.current.onOpenTemporal();
|
|
107
|
-
}
|
|
108
|
-
if (input === 'w') {
|
|
109
|
-
callbacksRef.current.onOpenWorkflows();
|
|
110
|
-
}
|
|
111
|
-
}, { isActive });
|
|
112
|
-
};
|
|
113
|
-
const useCtrlC = (onCleanup) => {
|
|
114
|
-
const { exit } = useApp();
|
|
115
|
-
const isExitingRef = useRef(false);
|
|
116
|
-
useInput((input, key) => {
|
|
117
|
-
if (key.ctrl && input === 'c' && !isExitingRef.current) {
|
|
118
|
-
isExitingRef.current = true;
|
|
119
|
-
void onCleanup().then(() => exit()).catch(err => exit(err instanceof Error ? err : new Error(String(err))));
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
};
|
|
123
|
-
const ServiceRow = ({ service }) => {
|
|
124
|
-
const status = resolveServiceStatus(service);
|
|
125
|
-
const ports = service.ports.length ? service.ports.join(', ') : '-';
|
|
126
|
-
return (_jsxs(Box, { children: [_jsx(Box, { width: 3, children: _jsx(StatusIcon, { status: status }) }), _jsx(Box, { width: 16, children: _jsx(Text, { children: service.name }) }), _jsx(Text, { dimColor: true, children: status.padEnd(10) }), _jsx(Text, { dimColor: true, children: ports })] }));
|
|
127
|
-
};
|
|
128
|
-
const FailureWarning = ({ services }) => {
|
|
129
|
-
const failed = services.filter(isServiceFailed);
|
|
130
|
-
if (failed.length === 0) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
const failedNames = failed.map(s => s.name).join(', ');
|
|
134
|
-
const hasWorker = failed.some(s => s.name.toLowerCase().includes('worker'));
|
|
135
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Box, { children: _jsx(Text, { backgroundColor: "red", color: "white", bold: true, children: " \u26A0\uFE0F SERVICE FAILURE DETECTED " }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "red", bold: true, children: "Failed services: " }), _jsx(Text, { children: failedNames })] }), hasWorker ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "\u26A1 The worker is not running!" }), _jsx(Text, { color: "yellow", children: ' Workflows will fail until the worker is restarted.' }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsxs(Text, { children: ["\uD83D\uDD0D Check the logs with: ", _jsx(Text, { color: "magenta", children: "docker compose logs worker" })] }) }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsxs(Text, { children: ["\uD83D\uDD27 If you just updated ", _jsx(Text, { italic: true, children: "@outputai/cli" }), ", try: ", _jsx(Text, { color: "magenta", children: "output fix" })] }) })] })) : (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["\uD83D\uDD0D Check the logs with: ", _jsx(Text, { color: "magenta", children: "docker compose logs <service-name>" })] }) }))] }));
|
|
136
|
-
};
|
|
137
|
-
const DevSuccessMessage = ({ services }) => {
|
|
138
|
-
const divider = '─'.repeat(80);
|
|
139
|
-
const sortedNames = services.map(s => s.name).sort().join('|');
|
|
140
|
-
const logsCommand = `docker compose -p ${config.dockerServiceName} logs -f <${sortedNames}>`;
|
|
141
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { dimColor: true, children: divider }) }), _jsxs(Box, { children: [_jsx(Text, { color: "green", bold: true, children: '✅ SUCCESS! ' }), _jsx(Text, { bold: true, children: "Development services are running" })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { dimColor: true, children: divider }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "\uD83D\uDC33 SERVICES" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { color: "white", children: 'Temporal UI: ' }), _jsx(Text, { color: "cyan", children: config.temporalUiUrl })] }), _jsxs(Box, { children: [_jsx(Text, { color: "white", children: 'API Server: ' }), _jsxs(Text, { color: "yellow", children: ["localhost:", config.ports.api] })] })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { dimColor: true, children: divider }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "\uD83D\uDE80 RUN A WORKFLOW" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsx(Text, { color: "white", children: "In a new terminal, execute:" }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "cyan", children: "npx output workflow run blog_evaluator paulgraham_hwh" }) })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { dimColor: true, children: divider }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "\u26A1 USEFUL COMMANDS" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { color: "white", children: 'Open Temporal UI: ' }), _jsxs(Text, { color: "cyan", children: ["open ", config.temporalUiUrl] })] }), _jsxs(Box, { children: [_jsx(Text, { color: "white", children: 'View logs: ' }), _jsx(Text, { color: "cyan", children: logsCommand })] }), _jsxs(Box, { children: [_jsx(Text, { color: "white", children: 'Stop services: ' }), _jsx(Text, { color: "cyan", children: "Press Ctrl+C" })] })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { dimColor: true, children: divider }) }), _jsx(Text, { dimColor: true, children: "\uD83D\uDCA1 Tip: The Temporal UI lets you monitor workflow executions in real-time" })] }));
|
|
142
|
-
};
|
|
143
|
-
const WaitingView = ({ services }) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Waiting for services to become healthy..." })] }), services.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: services.map(s => _jsx(ServiceRow, { service: s }, s.name)) }))] }));
|
|
144
|
-
const RunningView = ({ services, workflowSummary }) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "\uD83D\uDCCA Service Status" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: services.map(s => _jsx(ServiceRow, { service: s }, s.name)) }), _jsx(FailureWarning, { services: services }), workflowSummary && _jsx(WorkflowSummarySection, { summary: workflowSummary }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: '🌐 Temporal UI: ' }), _jsx(Text, { bold: true, children: config.temporalUiUrl })] }), _jsx(CommandFooter, { hints: [
|
|
145
|
-
{ key: 'o', label: 'open ui' },
|
|
146
|
-
{ key: 'w', label: 'view workflow runs' },
|
|
147
|
-
{ key: 'ctrl+c', label: 'stop' }
|
|
148
|
-
] })] }));
|
|
149
|
-
const MainDevView = ({ phase, services, workflowSummary }) => {
|
|
150
|
-
if (phase === 'waiting') {
|
|
151
|
-
return _jsx(WaitingView, { services: services });
|
|
152
|
-
}
|
|
153
|
-
return _jsx(RunningView, { services: services, workflowSummary: workflowSummary });
|
|
154
|
-
};
|
|
155
|
-
export const DevApp = ({ dockerComposePath, onCleanup }) => {
|
|
156
|
-
const { exit } = useApp();
|
|
157
|
-
const [phase, setPhase] = useState('waiting');
|
|
158
|
-
const [services, setServices] = useState([]);
|
|
159
|
-
const [successItems, setSuccessItems] = useState([]);
|
|
160
|
-
const [activeView, setActiveView] = useState('main');
|
|
161
|
-
const [workflowRuns, setWorkflowRuns] = useState([]);
|
|
162
|
-
useHealthPolling(dockerComposePath, phase === 'waiting', {
|
|
163
|
-
onServices: setServices,
|
|
164
|
-
onAllHealthy: svcs => {
|
|
165
|
-
setSuccessItems([{ id: 'success', services: svcs }]);
|
|
166
|
-
setPhase('running');
|
|
167
|
-
},
|
|
168
|
-
onFailure: () => {
|
|
169
|
-
setPhase('failed');
|
|
170
|
-
},
|
|
171
|
-
onTimeout: () => exit(new Error('Timeout waiting for services to become healthy'))
|
|
172
|
-
});
|
|
173
|
-
useStatusRefresh(dockerComposePath, phase === 'running', setServices);
|
|
174
|
-
useWorkflowPolling(phase === 'running' || phase === 'failed', setWorkflowRuns);
|
|
175
|
-
useMainViewInput(activeView === 'main' && phase !== 'waiting', {
|
|
176
|
-
onOpenTemporal: () => openUrl(config.temporalUiUrl),
|
|
177
|
-
onOpenWorkflows: () => setActiveView('workflows')
|
|
178
|
-
});
|
|
179
|
-
useCtrlC(onCleanup);
|
|
180
|
-
const workflowSummary = workflowRuns.length > 0 ? {
|
|
181
|
-
running: workflowRuns.filter(r => r.status === 'running').length,
|
|
182
|
-
completed: workflowRuns.filter(r => r.status === 'completed').length,
|
|
183
|
-
failed: workflowRuns.filter(r => r.status === 'failed').length,
|
|
184
|
-
total: workflowRuns.length
|
|
185
|
-
} : null;
|
|
186
|
-
return (_jsxs(_Fragment, { children: [_jsx(Static, { items: successItems, children: item => _jsx(DevSuccessMessage, { services: item.services }, item.id) }), _jsxs(Box, { flexDirection: "column", children: [activeView === 'main' && (_jsx(MainDevView, { phase: phase, services: services, workflowSummary: workflowSummary })), activeView === 'workflows' && (_jsx(WorkflowListView, { runs: workflowRuns, onBack: () => setActiveView('main') }))] })] }));
|
|
187
|
-
};
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
3
|
-
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import Spinner from 'ink-spinner';
|
|
5
|
-
import { getWorkflowIdRunsRidResult } from '#api/generated/api.js';
|
|
6
|
-
import { StatusIcon, statusColor } from '#components/status_icon.js';
|
|
7
|
-
import { elapsedMs, formatDurationCompact } from '#utils/date_formatter.js';
|
|
8
|
-
import { CommandFooter } from '#components/command_footer.js';
|
|
9
|
-
import { openUrl } from '#utils/open_url.js';
|
|
10
|
-
import { config } from '#config.js';
|
|
11
|
-
const VISIBLE_ROWS = 15;
|
|
12
|
-
const STATUS_ORDER = {
|
|
13
|
-
running: 0,
|
|
14
|
-
failed: 1,
|
|
15
|
-
timed_out: 2,
|
|
16
|
-
terminated: 3,
|
|
17
|
-
canceled: 4,
|
|
18
|
-
continued: 5,
|
|
19
|
-
completed: 6
|
|
20
|
-
};
|
|
21
|
-
const sortRuns = (runs) => [...runs].sort((a, b) => {
|
|
22
|
-
const statusDiff = (STATUS_ORDER[a.status ?? ''] ?? Infinity) - (STATUS_ORDER[b.status ?? ''] ?? Infinity);
|
|
23
|
-
if (statusDiff !== 0) {
|
|
24
|
-
return statusDiff;
|
|
25
|
-
}
|
|
26
|
-
const aTime = a.startedAt ? new Date(a.startedAt).getTime() : 0;
|
|
27
|
-
const bTime = b.startedAt ? new Date(b.startedAt).getTime() : 0;
|
|
28
|
-
return bTime - aTime;
|
|
29
|
-
});
|
|
30
|
-
const truncate = (str, max) => str.length > max ? str.slice(0, max - 1) + '…' : str;
|
|
31
|
-
const COL = {
|
|
32
|
-
indicator: 2,
|
|
33
|
-
icon: 3,
|
|
34
|
-
status: 12,
|
|
35
|
-
type: 20,
|
|
36
|
-
id: 32,
|
|
37
|
-
duration: 10
|
|
38
|
-
};
|
|
39
|
-
const WorkflowRow = ({ run, selected }) => {
|
|
40
|
-
const status = run.status ?? 'running';
|
|
41
|
-
const color = statusColor(status);
|
|
42
|
-
const duration = run.startedAt ? formatDurationCompact(elapsedMs(run.startedAt, run.completedAt)) : '-';
|
|
43
|
-
return (_jsxs(Box, { children: [_jsx(Box, { width: COL.indicator, children: _jsx(Text, { color: selected ? 'cyan' : undefined, bold: selected, children: selected ? '▸' : ' ' }) }), _jsx(Box, { width: COL.icon, children: _jsx(StatusIcon, { status: status }) }), _jsx(Box, { width: COL.status, children: _jsx(Text, { color: color, children: status }) }), _jsx(Box, { width: COL.type, children: _jsx(Text, { bold: selected, children: truncate(run.workflowType ?? '-', COL.type - 2) }) }), _jsx(Box, { width: COL.id, children: _jsx(Text, { dimColor: !selected, children: truncate(run.workflowId ?? '-', COL.id - 2) }) }), _jsx(Box, { width: COL.duration, justifyContent: "flex-end", children: _jsx(Text, { dimColor: true, children: duration }) })] }));
|
|
44
|
-
};
|
|
45
|
-
const HeaderRow = () => (_jsxs(Box, { children: [_jsx(Box, { width: COL.indicator, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: COL.icon, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: COL.status, children: _jsx(Text, { dimColor: true, bold: true, children: "STATUS" }) }), _jsx(Box, { width: COL.type, children: _jsx(Text, { dimColor: true, bold: true, children: "TYPE" }) }), _jsx(Box, { width: COL.id, children: _jsx(Text, { dimColor: true, bold: true, children: "WORKFLOW ID" }) }), _jsx(Box, { width: COL.duration, justifyContent: "flex-end", children: _jsx(Text, { dimColor: true, bold: true, children: "DURATION" }) })] }));
|
|
46
|
-
const WorkflowDetailPane = ({ detail, loading }) => {
|
|
47
|
-
if (loading) {
|
|
48
|
-
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Loading details..." })] }));
|
|
49
|
-
}
|
|
50
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Status: " }), _jsx(Text, { color: statusColor(detail.status ?? ''), children: detail.status })] }), detail.error && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: "red", children: "Error:" }), _jsx(Text, { color: "red", children: truncate(detail.error, 300) })] })), detail.output !== undefined && detail.output !== null && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Output:" }), _jsx(Text, { children: truncate(JSON.stringify(detail.output, null, 2), 400) })] }))] }));
|
|
51
|
-
};
|
|
52
|
-
export const WorkflowListView = ({ runs, onBack }) => {
|
|
53
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
54
|
-
const [detail, setDetail] = useState(null);
|
|
55
|
-
const [detailLoading, setDetailLoading] = useState(false);
|
|
56
|
-
const cacheRef = useRef(new Map());
|
|
57
|
-
const fetchIdRef = useRef(0);
|
|
58
|
-
const sortedRuns = useMemo(() => sortRuns(runs), [runs]);
|
|
59
|
-
const clampedIndex = Math.min(selectedIndex, Math.max(0, sortedRuns.length - 1));
|
|
60
|
-
const selectedRun = sortedRuns[clampedIndex];
|
|
61
|
-
const selectedWorkflowId = selectedRun?.workflowId;
|
|
62
|
-
const selectedRunId = selectedRun?.runId;
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
if (clampedIndex !== selectedIndex) {
|
|
65
|
-
setSelectedIndex(clampedIndex);
|
|
66
|
-
}
|
|
67
|
-
}, [clampedIndex, selectedIndex]);
|
|
68
|
-
useEffect(() => {
|
|
69
|
-
if (!selectedWorkflowId || !selectedRunId) {
|
|
70
|
-
setDetail(null);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const cacheKey = `${selectedWorkflowId}:${selectedRunId}`;
|
|
74
|
-
const cached = cacheRef.current.get(cacheKey);
|
|
75
|
-
if (cached) {
|
|
76
|
-
setDetail(cached);
|
|
77
|
-
setDetailLoading(false);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const currentFetchId = ++fetchIdRef.current;
|
|
81
|
-
setDetailLoading(true);
|
|
82
|
-
getWorkflowIdRunsRidResult(selectedWorkflowId, selectedRunId)
|
|
83
|
-
.then(response => {
|
|
84
|
-
if (fetchIdRef.current !== currentFetchId) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const data = response.data;
|
|
88
|
-
cacheRef.current.set(cacheKey, data);
|
|
89
|
-
setDetail(data);
|
|
90
|
-
setDetailLoading(false);
|
|
91
|
-
})
|
|
92
|
-
.catch(() => {
|
|
93
|
-
if (fetchIdRef.current !== currentFetchId) {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
setDetail(null);
|
|
97
|
-
setDetailLoading(false);
|
|
98
|
-
});
|
|
99
|
-
}, [selectedWorkflowId, selectedRunId]);
|
|
100
|
-
useInput((input, key) => {
|
|
101
|
-
if (key.upArrow) {
|
|
102
|
-
setSelectedIndex(i => Math.max(0, i - 1));
|
|
103
|
-
}
|
|
104
|
-
else if (key.downArrow) {
|
|
105
|
-
setSelectedIndex(i => Math.min(sortedRuns.length - 1, i + 1));
|
|
106
|
-
}
|
|
107
|
-
else if (key.escape || input === 'q') {
|
|
108
|
-
onBack();
|
|
109
|
-
}
|
|
110
|
-
else if (input === 'o' && selectedWorkflowId) {
|
|
111
|
-
openUrl(`${config.temporalUiUrl}/namespaces/default/workflows/${selectedWorkflowId}`);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
const windowStart = useMemo(() => {
|
|
115
|
-
const half = Math.floor(VISIBLE_ROWS / 2);
|
|
116
|
-
const start = Math.max(0, clampedIndex - half);
|
|
117
|
-
const maxStart = Math.max(0, sortedRuns.length - VISIBLE_ROWS);
|
|
118
|
-
return Math.min(start, maxStart);
|
|
119
|
-
}, [clampedIndex, sortedRuns.length]);
|
|
120
|
-
const visibleRuns = sortedRuns.slice(windowStart, windowStart + VISIBLE_ROWS);
|
|
121
|
-
if (sortedRuns.length === 0) {
|
|
122
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Workflow Runs" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "No workflow runs found." }) }), _jsx(CommandFooter, { hints: [{ key: 'q', label: 'back' }] })] }));
|
|
123
|
-
}
|
|
124
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Workflow Runs (", sortedRuns.length, ")"] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(HeaderRow, {}), windowStart > 0 && _jsxs(Text, { dimColor: true, children: [" \u2191 ", windowStart, " more above"] }), visibleRuns.map((run, i) => (_jsx(WorkflowRow, { run: run, selected: windowStart + i === clampedIndex }, `${run.workflowId}-${run.startedAt}-${windowStart + i}`))), windowStart + VISIBLE_ROWS < sortedRuns.length && (_jsxs(Text, { dimColor: true, children: [" \u2193 ", sortedRuns.length - windowStart - VISIBLE_ROWS, " more below"] }))] }), detail && _jsx(WorkflowDetailPane, { detail: detail, loading: detailLoading }), detailLoading && !detail && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Loading details..." })] })), _jsx(CommandFooter, { hints: [
|
|
125
|
-
{ key: '↑/↓', label: 'navigate' },
|
|
126
|
-
{ key: 'o', label: 'open in temporal' },
|
|
127
|
-
{ key: 'q', label: 'back' }
|
|
128
|
-
] })] }));
|
|
129
|
-
};
|