@outputai/cli 0.4.1-next.fb7438a.0 → 0.5.1-dev.45fb889.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
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo,
|
|
3
|
-
import { Box, Text, useInput } from 'ink';
|
|
2
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
4
|
import { isServiceFailed, SERVICE_HEALTH } from '#services/docker.js';
|
|
5
|
-
import {
|
|
5
|
+
import { DockerServiceStatusIcon } from '#views/dev/components/docker_service_status.js';
|
|
6
6
|
import { openUrl } from '#utils/open_url.js';
|
|
7
|
-
import { Footer } from '#views/dev/chrome/footer.js';
|
|
8
7
|
import { LoadingSpinner } from '#views/dev/chrome/loading_spinner.js';
|
|
9
8
|
import { SelectionIndicator } from '#views/dev/chrome/selection_indicator.js';
|
|
10
9
|
import { useUiState } from '#views/dev/state/ui_state.js';
|
|
11
10
|
import { useDockerLogs } from '#views/dev/hooks/use_docker_logs.js';
|
|
12
11
|
import { restartService, restartStack } from '#views/dev/services/docker_control.js';
|
|
12
|
+
import { ContentTitle, getHeight as getContentTitleHeight } from '#views/dev/components/content_title.js';
|
|
13
13
|
import { MasterDetailPanel } from '#views/dev/components/master_detail_panel.js';
|
|
14
|
-
|
|
14
|
+
import { formatContentTitle, truncate, useListSelection } from '#views/dev/utils/panel_helpers.js';
|
|
15
|
+
const LOG_WIDTH_PADDING = 8;
|
|
16
|
+
const PAUSED_ROWS = 1;
|
|
15
17
|
const resolveServiceStatus = (service) => service.health === SERVICE_HEALTH.NONE ? service.state : service.health;
|
|
16
18
|
const SERVICE_URLS = {
|
|
17
19
|
'temporal-ui': 'http://localhost:8080',
|
|
@@ -26,28 +28,22 @@ const COL = {
|
|
|
26
28
|
status: 11,
|
|
27
29
|
ports: 22
|
|
28
30
|
};
|
|
29
|
-
const HeaderRow = () => (_jsxs(Box, {
|
|
31
|
+
const HeaderRow = () => (_jsxs(Box, { children: [_jsx(Box, { width: COL.indicator, children: _jsx(Text, { children: "\u00A0" }) }), _jsx(Box, { width: COL.icon, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: COL.name, children: _jsx(Text, { dimColor: true, bold: true, children: "SERVICE" }) }), _jsx(Box, { width: COL.status, children: _jsx(Text, { dimColor: true, bold: true, children: "STATUS" }) }), _jsx(Box, { width: COL.ports, children: _jsx(Text, { dimColor: true, bold: true, children: "PORTS" }) })] }));
|
|
30
32
|
const ServiceRow = ({ service, selected }) => {
|
|
31
33
|
const status = resolveServiceStatus(service);
|
|
32
34
|
const ports = service.ports.length ? service.ports.join(', ') : '-';
|
|
33
|
-
return (_jsxs(Box, { children: [_jsx(Box, { width: COL.indicator, children: _jsx(SelectionIndicator, { selected: selected }) }), _jsx(Box, { width: COL.icon, children: _jsx(
|
|
35
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: COL.indicator, children: _jsx(SelectionIndicator, { selected: selected }) }), _jsx(Box, { width: COL.icon, children: _jsx(DockerServiceStatusIcon, { status: status }) }), _jsx(Box, { width: COL.name, children: _jsx(Text, { bold: selected, wrap: "truncate-end", children: service.name }) }), _jsx(Box, { width: COL.status, children: _jsx(Text, { dimColor: !selected, wrap: "truncate-end", children: status }) }), _jsx(Box, { width: COL.ports, children: _jsx(Text, { dimColor: !selected, wrap: "truncate-end", children: ports }) })] }));
|
|
34
36
|
};
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { backgroundColor: "red", color: "white", bold: true, children: [" \u26A0\uFE0F ", failed.length, " service(s) failing \u2014 see logs and press r to restart "] }) }));
|
|
41
|
-
};
|
|
42
|
-
const LogPane = ({ serviceName, lines, paused }) => {
|
|
37
|
+
const formatLogLine = (line, maxWidth) => truncate(line.replace(/\r/g, '').replace(/\t/g, ' ').trimEnd(), maxWidth);
|
|
38
|
+
const serviceLogRowsFor = (detailRows, paused) => Math.max(1, detailRows - getContentTitleHeight() - (paused ? PAUSED_ROWS : 0));
|
|
39
|
+
const LogPane = ({ serviceName, lines, maxLines, paused }) => {
|
|
40
|
+
const { stdout } = useStdout();
|
|
41
|
+
const maxLineWidth = Math.max(20, (stdout?.columns ?? 120) - LOG_WIDTH_PADDING);
|
|
43
42
|
if (!serviceName) {
|
|
44
43
|
return _jsx(Text, { dimColor: true, children: "Select a service to tail its logs." });
|
|
45
44
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
const visible = lines.slice(-VISIBLE_LOG_LINES);
|
|
50
|
-
return (_jsxs(Box, { flexDirection: "column", children: [paused && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { backgroundColor: "yellow", color: "black", children: " PAUSED " }), _jsx(Text, { dimColor: true, children: " press p to resume" })] })), visible.map((line, i) => (_jsx(Text, { wrap: "truncate-end", children: line }, `${i}-${line.length}`)))] }));
|
|
45
|
+
const visible = lines.slice(-maxLines);
|
|
46
|
+
return (_jsxs(Box, { flexDirection: "column", children: [lines.length === 0 && (_jsx(LoadingSpinner, { label: `Waiting for logs from ${serviceName}...` })), visible.map((line, i) => (_jsx(Text, { wrap: "truncate-end", children: formatLogLine(line, maxLineWidth) }, `${i}-${line.length}`))), paused && (_jsxs(Box, { children: [_jsx(Text, { backgroundColor: "yellow", color: "black", children: " PAUSED " }), _jsx(Text, { dimColor: true, children: " press p to resume" })] }))] }));
|
|
51
47
|
};
|
|
52
48
|
const SERVICE_PRIORITY = ['worker', 'api'];
|
|
53
49
|
/**
|
|
@@ -63,50 +59,45 @@ export const compareService = (a, b) => {
|
|
|
63
59
|
}
|
|
64
60
|
return a.name.localeCompare(b.name);
|
|
65
61
|
};
|
|
66
|
-
const
|
|
62
|
+
export const SERVICES_HINTS = [
|
|
67
63
|
{ key: '↑/↓', label: 'navigate' },
|
|
68
|
-
{ key: 'r
|
|
64
|
+
{ key: 'r', label: 'restart' },
|
|
65
|
+
{ key: 'ctrl+r', label: 'restart all' },
|
|
69
66
|
{ key: 'p', label: 'pause logs' },
|
|
70
67
|
{ key: 'c', label: 'clear' },
|
|
71
|
-
{ key: 'o', label: 'open url' }
|
|
72
|
-
{ key: 'tab', label: 'next tab' },
|
|
73
|
-
{ key: 'ctrl+c', label: 'stop & quit' }
|
|
74
|
-
];
|
|
75
|
-
const HINTS_BOOT = [
|
|
76
|
-
{ key: 'tab', label: 'next tab' },
|
|
77
|
-
{ key: '?', label: 'help' },
|
|
78
|
-
{ key: 'ctrl+c', label: 'quit' }
|
|
68
|
+
{ key: 'o', label: 'open url' }
|
|
79
69
|
];
|
|
80
|
-
const
|
|
81
|
-
|
|
70
|
+
export const SERVICES_BOOT_HINTS = [];
|
|
71
|
+
const Detail = ({ service, lines, maxLogLines, paused }) => {
|
|
72
|
+
if (!service) {
|
|
73
|
+
return _jsx(Box, {});
|
|
74
|
+
}
|
|
75
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ContentTitle, { title: formatContentTitle([`Service "${service?.name}"`, 'Logs']) }), _jsx(Box, { flexDirection: "column", children: _jsx(LogPane, { serviceName: service.name ?? null, lines: lines, maxLines: maxLogLines, paused: paused }) })] }));
|
|
76
|
+
};
|
|
77
|
+
export const ServicesPanel = ({ height, phase, services, dockerComposePath }) => {
|
|
82
78
|
const ui = useUiState();
|
|
83
|
-
const
|
|
84
|
-
const [banner, setBanner] = useState(null);
|
|
79
|
+
const lastFailedCountRef = useRef(0);
|
|
85
80
|
const sortedServices = useMemo(() => [...services].sort(compareService), [services]);
|
|
86
|
-
const
|
|
87
|
-
const
|
|
81
|
+
const failedCount = useMemo(() => services.filter(isServiceFailed).length, [services]);
|
|
82
|
+
const { selectedIndex: clamped, selectPrevious, selectNext } = useListSelection(sortedServices.length);
|
|
88
83
|
const selectedService = sortedServices[clamped];
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
setSelectedIndex(clamped);
|
|
93
|
-
}
|
|
94
|
-
}, [clamped, selectedIndex]);
|
|
84
|
+
const isActive = ui.tab === 'services' && !ui.search.open;
|
|
85
|
+
const enabledLogs = isActive && Boolean(selectedService);
|
|
86
|
+
const pushToast = ui.pushToast;
|
|
95
87
|
const logs = useDockerLogs(dockerComposePath, selectedService?.name ?? null, enabledLogs);
|
|
96
88
|
useEffect(() => {
|
|
97
|
-
if (
|
|
98
|
-
|
|
89
|
+
if (failedCount > 0 && failedCount !== lastFailedCountRef.current) {
|
|
90
|
+
pushToast(`${failedCount} service${failedCount === 1 ? '' : 's'} failing. See logs and press r to restart.`, 'error');
|
|
99
91
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}, [banner]);
|
|
92
|
+
lastFailedCountRef.current = failedCount;
|
|
93
|
+
}, [failedCount, pushToast]);
|
|
103
94
|
useInput((input, key) => {
|
|
104
95
|
if (key.upArrow) {
|
|
105
|
-
|
|
96
|
+
selectPrevious();
|
|
106
97
|
return;
|
|
107
98
|
}
|
|
108
99
|
if (key.downArrow) {
|
|
109
|
-
|
|
100
|
+
selectNext();
|
|
110
101
|
return;
|
|
111
102
|
}
|
|
112
103
|
if (!selectedService) {
|
|
@@ -124,32 +115,32 @@ export const ServicesPanel = ({ phase, services, dockerComposePath }) => {
|
|
|
124
115
|
const url = SERVICE_URLS[selectedService.name];
|
|
125
116
|
if (url) {
|
|
126
117
|
openUrl(url);
|
|
127
|
-
|
|
118
|
+
pushToast(`Opened ${url}`, 'info');
|
|
128
119
|
}
|
|
129
120
|
else {
|
|
130
|
-
|
|
121
|
+
pushToast(`${selectedService.name} has no known URL`, 'error');
|
|
131
122
|
}
|
|
132
123
|
return;
|
|
133
124
|
}
|
|
134
|
-
if (input === 'r') {
|
|
135
|
-
|
|
125
|
+
if (!key.ctrl && input === 'r') {
|
|
126
|
+
pushToast(`Restarting ${selectedService.name}...`, 'info');
|
|
136
127
|
restartService(dockerComposePath, selectedService.name)
|
|
137
|
-
.then(() =>
|
|
138
|
-
.catch(err =>
|
|
128
|
+
.then(() => pushToast(`Restarted ${selectedService.name}`, 'success'))
|
|
129
|
+
.catch(err => pushToast(`Restart failed: ${err instanceof Error ? err.message : String(err)}`, 'error'));
|
|
139
130
|
return;
|
|
140
131
|
}
|
|
141
|
-
if (input === '
|
|
142
|
-
|
|
132
|
+
if (key.ctrl && input === 'r') {
|
|
133
|
+
pushToast('Restarting all services...', 'info');
|
|
143
134
|
restartStack(dockerComposePath)
|
|
144
|
-
.then(() =>
|
|
145
|
-
.catch(err =>
|
|
135
|
+
.then(() => pushToast('Restarted all services', 'success'))
|
|
136
|
+
.catch(err => pushToast(`Restart failed: ${err instanceof Error ? err.message : String(err)}`, 'error'));
|
|
146
137
|
}
|
|
147
138
|
}, { isActive });
|
|
148
139
|
if (phase === 'waiting' && services.length === 0) {
|
|
149
|
-
return (
|
|
140
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(LoadingSpinner, { label: "Starting Docker services\u2026" }) }));
|
|
150
141
|
}
|
|
151
142
|
if (services.length === 0) {
|
|
152
|
-
return (
|
|
143
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "No services running." }) }));
|
|
153
144
|
}
|
|
154
|
-
return (_jsx(MasterDetailPanel, { items: sortedServices, selectedIndex: clamped, visibleRows: sortedServices.length, renderHeader: () => _jsx(HeaderRow, {}), renderRow: (service, selected) => _jsx(ServiceRow, { service: service, selected: selected }), rowKey: service => service.name, detail: _jsx(Detail, { service: selectedService,
|
|
145
|
+
return (_jsx(MasterDetailPanel, { items: sortedServices, selectedIndex: clamped, height: height, visibleRows: sortedServices.length, renderHeader: () => _jsx(HeaderRow, {}), renderRow: (service, selected) => _jsx(ServiceRow, { service: service, selected: selected }), rowKey: service => service.name, detail: ({ detailRows }) => (_jsx(Detail, { service: selectedService, lines: logs.lines, maxLogLines: serviceLogRowsFor(detailRows, logs.paused), paused: logs.paused })) }));
|
|
155
146
|
};
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { Workflow } from '#api/generated/api.js';
|
|
3
3
|
import type { WorkflowRun } from '#services/workflow_runs.js';
|
|
4
|
+
export declare const buildVisibleWorkflows: (workflows: Workflow[], query: string) => Workflow[];
|
|
5
|
+
export declare const WORKFLOWS_HINTS: {
|
|
6
|
+
key: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}[];
|
|
9
|
+
export declare const WORKFLOWS_LOADING_HINTS: never[];
|
|
4
10
|
export declare const WorkflowsPanel: React.FC<{
|
|
5
11
|
workflows: Workflow[];
|
|
6
12
|
runs: WorkflowRun[];
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo
|
|
2
|
+
import { useEffect, useMemo } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import {
|
|
4
|
+
import { WorkflowStatusIcon, workflowStatusColor } from '#views/dev/components/workflow_status.js';
|
|
5
|
+
import { ContentTitle } from '#views/dev/components/content_title.js';
|
|
5
6
|
import { elapsedMs, formatDurationCompact } from '#utils/date_formatter.js';
|
|
6
|
-
import { Footer } from '#views/dev/chrome/footer.js';
|
|
7
7
|
import { SelectionIndicator } from '#views/dev/chrome/selection_indicator.js';
|
|
8
8
|
import { useUiState } from '#views/dev/state/ui_state.js';
|
|
9
9
|
import { MasterDetailPanel } from '#views/dev/components/master_detail_panel.js';
|
|
10
|
-
import { formatStartedShort } from '#views/dev/utils/panel_helpers.js';
|
|
10
|
+
import { formatStartedShort, useListSelection } from '#views/dev/utils/panel_helpers.js';
|
|
11
11
|
import { WORKFLOWS_VISIBLE_ROWS, WORKFLOWS_RECENT_RUNS_LIMIT } from '#views/dev/utils/constants.js';
|
|
12
12
|
const COL = {
|
|
13
13
|
indicator: 3,
|
|
@@ -27,7 +27,7 @@ const matchesQuery = (workflow, query) => {
|
|
|
27
27
|
return (workflow.aliases ?? []).some(a => a.toLowerCase().includes(q));
|
|
28
28
|
};
|
|
29
29
|
const sortByName = (workflows) => [...workflows].sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
|
|
30
|
-
const buildVisibleWorkflows = (workflows, query) => {
|
|
30
|
+
export const buildVisibleWorkflows = (workflows, query) => {
|
|
31
31
|
const list = query ? workflows.filter(w => matchesQuery(w, query)) : workflows;
|
|
32
32
|
return sortByName(list);
|
|
33
33
|
};
|
|
@@ -36,7 +36,7 @@ const WorkflowRow = ({ workflow, selected }) => (_jsxs(Box, { children: [_jsx(Bo
|
|
|
36
36
|
const SidebarRunRow = ({ run }) => {
|
|
37
37
|
const status = run.status ?? 'unknown';
|
|
38
38
|
const duration = run.startedAt ? formatDurationCompact(elapsedMs(run.startedAt, run.completedAt)) : '-';
|
|
39
|
-
return (_jsxs(Box, { children: [_jsx(
|
|
39
|
+
return (_jsxs(Box, { children: [_jsx(WorkflowStatusIcon, { status: status }), _jsx(Box, { width: 11, paddingLeft: 1, children: _jsx(Text, { color: workflowStatusColor(status), children: status }) }), _jsx(Box, { width: 9, justifyContent: "flex-end", children: _jsx(Text, { dimColor: true, children: duration }) }), _jsx(Box, { width: 14, justifyContent: "flex-end", children: _jsx(Text, { dimColor: true, children: formatStartedShort(run.startedAt) }) })] }));
|
|
40
40
|
};
|
|
41
41
|
const DetailPane = ({ workflow, runs }) => {
|
|
42
42
|
if (!workflow) {
|
|
@@ -50,22 +50,20 @@ const DetailPane = ({ workflow, runs }) => {
|
|
|
50
50
|
completed: wfRuns.filter(r => r.status === 'completed').length
|
|
51
51
|
};
|
|
52
52
|
const recent = wfRuns.slice(0, WORKFLOWS_RECENT_RUNS_LIMIT);
|
|
53
|
-
return (_jsxs(Box, { flexDirection: "
|
|
53
|
+
return (_jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(ContentTitle, { title: `Workflow "${workflow.name}"` }), _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [stats.total, " runs"] }), stats.running > 0 && _jsxs(_Fragment, { children: [_jsx(Text, { children: "\u2003\u2003" }), _jsxs(Text, { color: "blue", children: ["\u25CF ", stats.running, " running"] })] }), stats.failed > 0 && _jsxs(_Fragment, { children: [_jsx(Text, { children: "\u2003\u2003" }), _jsxs(Text, { color: "red", children: ["\u2717 ", stats.failed, " failed"] })] }), stats.completed > 0 && _jsxs(_Fragment, { children: [_jsx(Text, { children: "\u2003\u2003" }), _jsxs(Text, { color: "green", children: ["\u25CF ", stats.completed, " ok"] })] })] }), _jsx(Text, { wrap: "wrap", children: workflow.description ?? 'No description' })] })] }), recent.length > 0 ?
|
|
54
|
+
_jsxs(Box, { flexDirection: "column", flexShrink: 0, borderStyle: "single", borderColor: "blackBright", borderTop: false, borderBottom: false, borderRight: false, paddingY: 1, paddingLeft: 1, gap: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: "RECENT RUNS" }), _jsx(Box, { flexDirection: "column", children: recent.map((run, i) => _jsx(SidebarRunRow, { run: run }, `${run.runId ?? i}`)) })] }) :
|
|
55
|
+
_jsx(_Fragment, {})] }));
|
|
54
56
|
};
|
|
55
|
-
const
|
|
57
|
+
export const WORKFLOWS_HINTS = [
|
|
56
58
|
{ key: '↑/↓', label: 'navigate' },
|
|
57
59
|
{ key: 'enter', label: 'show runs' },
|
|
58
|
-
{ key: 'r', label: 'run' }
|
|
59
|
-
{ key: '/', label: 'search' },
|
|
60
|
-
{ key: 'tab', label: 'next tab' }
|
|
60
|
+
{ key: 'r', label: 'run' }
|
|
61
61
|
];
|
|
62
|
+
export const WORKFLOWS_LOADING_HINTS = [];
|
|
62
63
|
export const WorkflowsPanel = ({ workflows, runs }) => {
|
|
63
64
|
const ui = useUiState();
|
|
64
65
|
const filtered = useMemo(() => buildVisibleWorkflows(workflows, ui.search.query), [workflows, ui.search.query]);
|
|
65
|
-
|
|
66
|
-
// highlighted workflow after the run modal unmounts and remounts the
|
|
67
|
-
// panel.
|
|
68
|
-
const [selectedIndex, setSelectedIndex] = useState(() => {
|
|
66
|
+
const initialIndex = () => {
|
|
69
67
|
const previousName = ui.selection.workflowName;
|
|
70
68
|
if (!previousName) {
|
|
71
69
|
return 0;
|
|
@@ -73,25 +71,19 @@ export const WorkflowsPanel = ({ workflows, runs }) => {
|
|
|
73
71
|
const initial = buildVisibleWorkflows(workflows, ui.search.query);
|
|
74
72
|
const i = initial.findIndex(w => w.name === previousName);
|
|
75
73
|
return i >= 0 ? i : 0;
|
|
76
|
-
}
|
|
77
|
-
const
|
|
78
|
-
const clamped = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
|
|
74
|
+
};
|
|
75
|
+
const { selectedIndex: clamped, selectPrevious, selectNext } = useListSelection(filtered.length, initialIndex);
|
|
79
76
|
const selectedWorkflow = filtered[clamped];
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
if (clamped !== selectedIndex) {
|
|
82
|
-
setSelectedIndex(clamped);
|
|
83
|
-
}
|
|
84
|
-
}, [clamped, selectedIndex]);
|
|
85
77
|
const setSelection = ui.setSelection;
|
|
86
78
|
useEffect(() => {
|
|
87
79
|
setSelection({ workflowName: selectedWorkflow?.name });
|
|
88
80
|
}, [selectedWorkflow?.name, setSelection]);
|
|
89
81
|
useInput((input, key) => {
|
|
90
82
|
if (key.upArrow) {
|
|
91
|
-
|
|
83
|
+
selectPrevious();
|
|
92
84
|
}
|
|
93
85
|
else if (key.downArrow) {
|
|
94
|
-
|
|
86
|
+
selectNext();
|
|
95
87
|
}
|
|
96
88
|
else if (key.return && selectedWorkflow?.name) {
|
|
97
89
|
ui.setSearchQuery(selectedWorkflow.name);
|
|
@@ -100,12 +92,12 @@ export const WorkflowsPanel = ({ workflows, runs }) => {
|
|
|
100
92
|
else if (input === 'r' && selectedWorkflow?.name) {
|
|
101
93
|
ui.openRunModal(selectedWorkflow.name, selectedWorkflow.path);
|
|
102
94
|
}
|
|
103
|
-
}, { isActive });
|
|
95
|
+
}, { isActive: ui.tab === 'workflows' && !ui.search.open });
|
|
104
96
|
if (workflows.length === 0) {
|
|
105
|
-
return (
|
|
97
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "Loading catalog\u2026 (waiting for the API to come up)" }) }));
|
|
106
98
|
}
|
|
107
99
|
if (filtered.length === 0) {
|
|
108
|
-
return (
|
|
100
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { dimColor: true, children: ["No workflows match `", ui.search.query, "`. Press ", _jsx(Text, { bold: true, children: "esc" }), " to clear the filter."] }) }));
|
|
109
101
|
}
|
|
110
|
-
return (_jsx(MasterDetailPanel, { items: filtered, selectedIndex: clamped, visibleRows: WORKFLOWS_VISIBLE_ROWS, renderHeader: () => _jsx(HeaderRow, {}), renderRow: (wf, selected) => _jsx(WorkflowRow, { workflow: wf, selected: selected }), rowKey: (wf, i) => wf.name ?? `row-${i}`, detail: _jsx(DetailPane, { workflow: selectedWorkflow, runs: runs })
|
|
102
|
+
return (_jsx(MasterDetailPanel, { items: filtered, selectedIndex: clamped, visibleRows: WORKFLOWS_VISIBLE_ROWS, renderHeader: () => _jsx(HeaderRow, {}), renderRow: (wf, selected) => _jsx(WorkflowRow, { workflow: wf, selected: selected }), rowKey: (wf, i) => wf.name ?? `row-${i}`, detail: _jsx(DetailPane, { workflow: selectedWorkflow, runs: runs }) }));
|
|
111
103
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildVisibleWorkflows } from './workflows_panel.js';
|
|
3
|
+
const workflow = (overrides) => ({
|
|
4
|
+
name: 'demo',
|
|
5
|
+
description: 'Demo workflow',
|
|
6
|
+
aliases: [],
|
|
7
|
+
...overrides
|
|
8
|
+
});
|
|
9
|
+
describe('buildVisibleWorkflows', () => {
|
|
10
|
+
it('sorts workflows by name', () => {
|
|
11
|
+
const visible = buildVisibleWorkflows([
|
|
12
|
+
workflow({ name: 'zebra' }),
|
|
13
|
+
workflow({ name: 'apple' }),
|
|
14
|
+
workflow({ name: 'middle' })
|
|
15
|
+
], '');
|
|
16
|
+
expect(visible.map(w => w.name)).toEqual(['apple', 'middle', 'zebra']);
|
|
17
|
+
});
|
|
18
|
+
it('filters by name, description, and aliases', () => {
|
|
19
|
+
const workflows = [
|
|
20
|
+
workflow({ name: 'invoice', description: 'Billing workflow', aliases: ['money'] }),
|
|
21
|
+
workflow({ name: 'support', description: 'Ticket triage', aliases: ['helpdesk'] })
|
|
22
|
+
];
|
|
23
|
+
expect(buildVisibleWorkflows(workflows, 'invoice').map(w => w.name)).toEqual(['invoice']);
|
|
24
|
+
expect(buildVisibleWorkflows(workflows, 'ticket').map(w => w.name)).toEqual(['support']);
|
|
25
|
+
expect(buildVisibleWorkflows(workflows, 'money').map(w => w.name)).toEqual(['invoice']);
|
|
26
|
+
});
|
|
27
|
+
it('matches queries case-insensitively', () => {
|
|
28
|
+
const visible = buildVisibleWorkflows([
|
|
29
|
+
workflow({ name: 'Invoice' }),
|
|
30
|
+
workflow({ name: 'Support' })
|
|
31
|
+
], 'invoice');
|
|
32
|
+
expect(visible.map(w => w.name)).toEqual(['Invoice']);
|
|
33
|
+
});
|
|
34
|
+
it('returns an empty list when no workflow matches', () => {
|
|
35
|
+
expect(buildVisibleWorkflows([
|
|
36
|
+
workflow({ name: 'invoice' })
|
|
37
|
+
], 'missing')).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -2,7 +2,8 @@ import React from 'react';
|
|
|
2
2
|
export type Tab = 'workflows' | 'runs' | 'services' | 'help';
|
|
3
3
|
export declare const TAB_ORDER: Tab[];
|
|
4
4
|
export declare const TAB_LABELS: Record<Tab, string>;
|
|
5
|
-
export type
|
|
5
|
+
export type RunListPaneTab = 'status' | 'input' | 'output' | 'attributes' | 'aggregations';
|
|
6
|
+
export type RunStepPaneTab = 'input' | 'output' | 'meta';
|
|
6
7
|
export type RunsView = 'list' | 'detail';
|
|
7
8
|
export interface Selection {
|
|
8
9
|
workflowName?: string;
|
|
@@ -29,11 +30,13 @@ export interface Toast {
|
|
|
29
30
|
message: string;
|
|
30
31
|
tone: 'info' | 'success' | 'error';
|
|
31
32
|
}
|
|
33
|
+
export declare const MAX_VISIBLE_TOASTS = 3;
|
|
32
34
|
export interface UiState {
|
|
33
35
|
tab: Tab;
|
|
34
36
|
search: SearchState;
|
|
35
37
|
selection: Selection;
|
|
36
|
-
|
|
38
|
+
runListPaneTab: RunListPaneTab;
|
|
39
|
+
runStepPaneTab: RunStepPaneTab;
|
|
37
40
|
runsView: RunsView;
|
|
38
41
|
runModal: RunModalState;
|
|
39
42
|
expandedJson: ExpandedJsonState;
|
|
@@ -46,7 +49,8 @@ export interface UiState {
|
|
|
46
49
|
clearSearch: () => void;
|
|
47
50
|
setSearchQuery: (query: string) => void;
|
|
48
51
|
setSelection: (selection: Selection) => void;
|
|
49
|
-
|
|
52
|
+
setRunListPaneTab: (tab: RunListPaneTab) => void;
|
|
53
|
+
setRunStepPaneTab: (tab: RunStepPaneTab) => void;
|
|
50
54
|
setRunsView: (view: RunsView) => void;
|
|
51
55
|
openRunModal: (workflowName: string, workflowPath?: string) => void;
|
|
52
56
|
closeRunModal: () => void;
|
|
@@ -7,12 +7,14 @@ export const TAB_LABELS = {
|
|
|
7
7
|
services: 'Services',
|
|
8
8
|
help: 'Help'
|
|
9
9
|
};
|
|
10
|
+
export const MAX_VISIBLE_TOASTS = 3;
|
|
10
11
|
const UiStateContext = createContext(null);
|
|
11
12
|
export const UiStateProvider = ({ children }) => {
|
|
12
13
|
const [tab, setTab] = useState('services');
|
|
13
14
|
const [search, setSearch] = useState({ open: false, query: '' });
|
|
14
15
|
const [selection, setSelection] = useState({});
|
|
15
|
-
const [
|
|
16
|
+
const [runListPaneTab, setRunListPaneTab] = useState('status');
|
|
17
|
+
const [runStepPaneTab, setRunStepPaneTab] = useState('output');
|
|
16
18
|
const [runsView, setRunsView] = useState('list');
|
|
17
19
|
const [runModal, setRunModal] = useState({ open: false, workflowName: '' });
|
|
18
20
|
const [expandedJson, setExpandedJson] = useState({ open: false, value: null, title: '' });
|
|
@@ -22,18 +24,32 @@ export const UiStateProvider = ({ children }) => {
|
|
|
22
24
|
tab,
|
|
23
25
|
search,
|
|
24
26
|
selection,
|
|
25
|
-
|
|
27
|
+
runListPaneTab,
|
|
28
|
+
runStepPaneTab,
|
|
26
29
|
runsView,
|
|
27
30
|
runModal,
|
|
28
31
|
expandedJson,
|
|
29
32
|
toasts,
|
|
30
|
-
setTab
|
|
33
|
+
setTab: next => {
|
|
34
|
+
setTab(current => {
|
|
35
|
+
if (current === 'runs' && next !== 'runs') {
|
|
36
|
+
setRunsView('list');
|
|
37
|
+
}
|
|
38
|
+
return next;
|
|
39
|
+
});
|
|
40
|
+
},
|
|
31
41
|
nextTab: () => setTab(current => {
|
|
32
42
|
const idx = TAB_ORDER.indexOf(current);
|
|
43
|
+
if (current === 'runs') {
|
|
44
|
+
setRunsView('list');
|
|
45
|
+
}
|
|
33
46
|
return TAB_ORDER[(idx + 1) % TAB_ORDER.length];
|
|
34
47
|
}),
|
|
35
48
|
prevTab: () => setTab(current => {
|
|
36
49
|
const idx = TAB_ORDER.indexOf(current);
|
|
50
|
+
if (current === 'runs') {
|
|
51
|
+
setRunsView('list');
|
|
52
|
+
}
|
|
37
53
|
return TAB_ORDER[(idx - 1 + TAB_ORDER.length) % TAB_ORDER.length];
|
|
38
54
|
}),
|
|
39
55
|
openSearch: () => setSearch(prev => ({ open: true, query: prev.query })),
|
|
@@ -41,7 +57,8 @@ export const UiStateProvider = ({ children }) => {
|
|
|
41
57
|
clearSearch: () => setSearch({ open: false, query: '' }),
|
|
42
58
|
setSearchQuery: (query) => setSearch(prev => ({ open: prev.open, query })),
|
|
43
59
|
setSelection,
|
|
44
|
-
|
|
60
|
+
setRunListPaneTab,
|
|
61
|
+
setRunStepPaneTab,
|
|
45
62
|
setRunsView,
|
|
46
63
|
openRunModal: (workflowName, workflowPath) => setRunModal({ open: true, workflowName, workflowPath }),
|
|
47
64
|
closeRunModal: () => setRunModal({ open: false, workflowName: '' }),
|
|
@@ -49,10 +66,10 @@ export const UiStateProvider = ({ children }) => {
|
|
|
49
66
|
closeExpandedJson: () => setExpandedJson({ open: false, value: null, title: '' }),
|
|
50
67
|
pushToast: (message, tone = 'info') => {
|
|
51
68
|
const id = ++toastIdRef.current;
|
|
52
|
-
setToasts(prev => [...prev, { id, message, tone }]);
|
|
69
|
+
setToasts(prev => [...prev, { id, message, tone }].slice(-MAX_VISIBLE_TOASTS));
|
|
53
70
|
},
|
|
54
71
|
dismissToast: (id) => setToasts(prev => prev.filter(t => t.id !== id))
|
|
55
|
-
}), [tab, search, selection,
|
|
72
|
+
}), [tab, search, selection, runListPaneTab, runStepPaneTab, runsView, runModal, expandedJson, toasts]);
|
|
56
73
|
return _jsx(UiStateContext.Provider, { value: value, children: children });
|
|
57
74
|
};
|
|
58
75
|
export const useUiState = () => {
|
|
@@ -11,7 +11,7 @@ export declare const CATALOG_WORKFLOW_NAME = "$catalog";
|
|
|
11
11
|
export declare const WORKFLOWS_VISIBLE_ROWS = 8;
|
|
12
12
|
export declare const WORKFLOWS_RECENT_RUNS_LIMIT = 5;
|
|
13
13
|
export declare const RUNS_VISIBLE_ROWS = 8;
|
|
14
|
-
export declare const RUNS_PREVIEW_LINES = 12;
|
|
15
14
|
export declare const RUN_DETAIL_VISIBLE_STEPS = 12;
|
|
16
|
-
export declare const
|
|
15
|
+
export declare const MIN_TERMINAL_COLUMNS = 100;
|
|
16
|
+
export declare const MIN_TERMINAL_ROWS = 40;
|
|
17
17
|
export declare const HELP_DOCS_URL = "https://docs.output.ai";
|
|
@@ -11,7 +11,7 @@ export const CATALOG_WORKFLOW_NAME = '$catalog';
|
|
|
11
11
|
export const WORKFLOWS_VISIBLE_ROWS = 8;
|
|
12
12
|
export const WORKFLOWS_RECENT_RUNS_LIMIT = 5;
|
|
13
13
|
export const RUNS_VISIBLE_ROWS = 8;
|
|
14
|
-
export const RUNS_PREVIEW_LINES = 12;
|
|
15
14
|
export const RUN_DETAIL_VISIBLE_STEPS = 12;
|
|
16
|
-
export const
|
|
15
|
+
export const MIN_TERMINAL_COLUMNS = 100;
|
|
16
|
+
export const MIN_TERMINAL_ROWS = 40;
|
|
17
17
|
export const HELP_DOCS_URL = 'https://docs.output.ai';
|
|
@@ -104,14 +104,14 @@ export const JsonEditor = ({ seed, title, isActive = true, onSubmit, onCancel })
|
|
|
104
104
|
const half = Math.floor(VISIBLE_BUFFER / 2);
|
|
105
105
|
const startLine = Math.max(0, cursorPos.line - half);
|
|
106
106
|
const visibleLines = lines.slice(startLine, startLine + (VISIBLE_BUFFER * 2) + 1);
|
|
107
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, {
|
|
107
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { children: ["File: ", title] }), _jsx(Text, { bold: true, color: status.ok ? 'green' : 'red', children: status.ok ? '✓ valid JSON' : '✗ invalid JSON' })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: visibleLines.map((line, i) => {
|
|
108
108
|
const lineIdx = startLine + i;
|
|
109
109
|
if (lineIdx !== cursorPos.line) {
|
|
110
|
-
return _jsx(Text, { children: line.length === 0 ? ' ' : line }, lineIdx);
|
|
110
|
+
return _jsx(Text, { dimColor: true, children: line.length === 0 ? ' ' : line }, lineIdx);
|
|
111
111
|
}
|
|
112
112
|
const before = line.slice(0, cursorPos.col);
|
|
113
113
|
const at = line[cursorPos.col] ?? ' ';
|
|
114
114
|
const after = line.slice(cursorPos.col + 1);
|
|
115
|
-
return (_jsxs(Text, { children: [_jsx(Text, { children: before }), _jsx(Text, { inverse: true, children: at }), _jsx(Text, { children: after })] }, lineIdx));
|
|
115
|
+
return (_jsxs(Text, { bold: true, children: [_jsx(Text, { children: before }), _jsx(Text, { inverse: true, children: at }), _jsx(Text, { children: after })] }, lineIdx));
|
|
116
116
|
}) }), !status.ok && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", wrap: "truncate-end", children: status.error }) })), submitMessage && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: submitMessage }) })), _jsxs(Box, { marginTop: 1, columnGap: 2, children: [_jsxs(Box, { columnGap: 1, children: [_jsx(Text, { bold: true, children: "ctrl+s" }), _jsx(Text, { dimColor: true, children: "submit" })] }), _jsxs(Box, { columnGap: 1, children: [_jsx(Text, { bold: true, children: "esc" }), _jsx(Text, { dimColor: true, children: "cancel" })] }), _jsxs(Box, { columnGap: 1, children: [_jsx(Text, { bold: true, children: "\u2191\u2193\u2190\u2192" }), _jsx(Text, { dimColor: true, children: "move" })] }), _jsxs(Box, { columnGap: 1, children: [_jsx(Text, { bold: true, children: "tab" }), _jsx(Text, { dimColor: true, children: "indent" })] })] })] }));
|
|
117
117
|
};
|
|
@@ -6,10 +6,12 @@ export interface ColoredToken {
|
|
|
6
6
|
export declare const tokenizeLine: (line: string) => ColoredToken[];
|
|
7
7
|
export declare const formatJsonText: (value: unknown) => string;
|
|
8
8
|
export declare const countJsonLines: (value: unknown) => number;
|
|
9
|
+
export declare const wrapTokens: (tokens: ColoredToken[], maxWidth: number | undefined) => ColoredToken[][];
|
|
9
10
|
export declare const JsonView: React.FC<{
|
|
10
11
|
value: unknown;
|
|
11
12
|
maxLines?: number;
|
|
12
13
|
offset?: number;
|
|
13
14
|
truncateLine?: boolean;
|
|
14
15
|
showOverflowFooter?: boolean;
|
|
16
|
+
maxWidth?: number;
|
|
15
17
|
}>;
|
|
@@ -23,7 +23,19 @@ const classifyRaw = (text) => {
|
|
|
23
23
|
return { kind: 'punct', text };
|
|
24
24
|
};
|
|
25
25
|
export const tokenizeLine = (line) => {
|
|
26
|
-
const
|
|
26
|
+
const matches = Array.from(line.matchAll(RAW_TOKEN_RE));
|
|
27
|
+
const matchedRaws = matches.flatMap((match, idx) => {
|
|
28
|
+
const index = match.index ?? 0;
|
|
29
|
+
const previous = matches[idx - 1];
|
|
30
|
+
const previousEnd = previous ? (previous.index ?? 0) + previous[0].length : 0;
|
|
31
|
+
const gap = index > previousEnd ? [{ kind: 'string', text: line.slice(previousEnd, index) }] : [];
|
|
32
|
+
return [...gap, classifyRaw(match[0])];
|
|
33
|
+
});
|
|
34
|
+
const lastMatch = matches[matches.length - 1];
|
|
35
|
+
const lastIndex = lastMatch ? (lastMatch.index ?? 0) + lastMatch[0].length : 0;
|
|
36
|
+
const raws = lastIndex < line.length ?
|
|
37
|
+
[...matchedRaws, { kind: 'string', text: line.slice(lastIndex) }] :
|
|
38
|
+
matchedRaws;
|
|
27
39
|
return raws.map((raw, idx) => {
|
|
28
40
|
if (raw.kind === 'ws') {
|
|
29
41
|
return { text: raw.text };
|
|
@@ -56,11 +68,35 @@ export const formatJsonText = (value) => {
|
|
|
56
68
|
const renderTokens = (tokens) => tokens.map((token, i) => token.color ?
|
|
57
69
|
_jsx(Text, { color: token.color, children: token.text }, i) :
|
|
58
70
|
_jsx(Text, { children: token.text }, i));
|
|
71
|
+
const tokenLength = (tokens) => tokens.reduce((total, token) => total + token.text.length, 0);
|
|
59
72
|
export const countJsonLines = (value) => {
|
|
60
73
|
const text = formatJsonText(value);
|
|
61
74
|
return text ? text.split('\n').length : 0;
|
|
62
75
|
};
|
|
63
|
-
export const
|
|
76
|
+
export const wrapTokens = (tokens, maxWidth) => {
|
|
77
|
+
if (maxWidth === undefined || maxWidth <= 0 || tokenLength(tokens) <= maxWidth) {
|
|
78
|
+
return [tokens];
|
|
79
|
+
}
|
|
80
|
+
const state = tokens.reduce((acc, token) => {
|
|
81
|
+
const chunks = Array.from({ length: Math.ceil(token.text.length / maxWidth) }, (_, i) => token.text.slice(i * maxWidth, (i + 1) * maxWidth));
|
|
82
|
+
return chunks.reduce((chunkAcc, chunk) => {
|
|
83
|
+
if (chunkAcc.width > 0 && chunkAcc.width + chunk.length > maxWidth) {
|
|
84
|
+
return {
|
|
85
|
+
lines: [...chunkAcc.lines, chunkAcc.current],
|
|
86
|
+
current: [{ ...token, text: chunk }],
|
|
87
|
+
width: chunk.length
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
...chunkAcc,
|
|
92
|
+
current: [...chunkAcc.current, { ...token, text: chunk }],
|
|
93
|
+
width: chunkAcc.width + chunk.length
|
|
94
|
+
};
|
|
95
|
+
}, acc);
|
|
96
|
+
}, { lines: [], current: [], width: 0 });
|
|
97
|
+
return state.current.length > 0 ? [...state.lines, state.current] : state.lines;
|
|
98
|
+
};
|
|
99
|
+
export const JsonView = ({ value, maxLines, offset = 0, truncateLine = true, showOverflowFooter = true, maxWidth }) => {
|
|
64
100
|
if (value === undefined || value === null) {
|
|
65
101
|
return _jsx(Text, { dimColor: true, children: "\u2014" });
|
|
66
102
|
}
|
|
@@ -70,8 +106,14 @@ export const JsonView = ({ value, maxLines, offset = 0, truncateLine = true, sho
|
|
|
70
106
|
}
|
|
71
107
|
const allLines = text.split('\n');
|
|
72
108
|
const start = Math.max(0, offset);
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
109
|
+
const displayLines = allLines
|
|
110
|
+
.slice(start)
|
|
111
|
+
.flatMap(line => truncateLine ? [tokenizeLine(line)] : wrapTokens(tokenizeLine(line), maxWidth));
|
|
112
|
+
const hasLineLimit = typeof maxLines === 'number';
|
|
113
|
+
const overflowBelow = hasLineLimit ? Math.max(0, displayLines.length - maxLines) : 0;
|
|
114
|
+
const footerRows = showOverflowFooter && overflowBelow > 0 ? 1 : 0;
|
|
115
|
+
const end = hasLineLimit ? Math.max(0, maxLines - footerRows) : undefined;
|
|
116
|
+
const visible = displayLines.slice(0, end);
|
|
117
|
+
const omittedLines = hasLineLimit ? Math.max(0, displayLines.length - visible.length) : 0;
|
|
118
|
+
return (_jsxs(Box, { flexDirection: "column", children: [visible.map((tokens, i) => (_jsx(Text, { wrap: truncateLine ? 'truncate-end' : 'wrap', children: renderTokens(tokens) }, i))), showOverflowFooter && omittedLines > 0 && (_jsxs(Text, { dimColor: true, children: ["\u2026 ", omittedLines, " more line", omittedLines === 1 ? '' : 's'] }))] }));
|
|
77
119
|
};
|