@outputai/cli 0.4.1-next.fb7438a.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/api/generated/api.d.ts +21 -23
  2. package/dist/api/generated/api.js +0 -4
  3. package/dist/assets/docker/docker-compose-dev.yml +1 -4
  4. package/dist/generated/framework_version.json +1 -1
  5. package/dist/utils/format_workflow_result.spec.js +4 -0
  6. package/dist/views/dev/chrome/footer.d.ts +5 -4
  7. package/dist/views/dev/chrome/footer.js +12 -2
  8. package/dist/views/dev/chrome/header.d.ts +2 -1
  9. package/dist/views/dev/chrome/header.js +18 -47
  10. package/dist/views/dev/chrome/header.spec.js +6 -46
  11. package/dist/views/dev/chrome/layout_heights.spec.d.ts +1 -0
  12. package/dist/views/dev/chrome/layout_heights.spec.js +19 -0
  13. package/dist/views/dev/chrome/search_bar.d.ts +1 -1
  14. package/dist/views/dev/chrome/search_bar.js +10 -11
  15. package/dist/views/dev/chrome/tab_bar.d.ts +8 -1
  16. package/dist/views/dev/chrome/tab_bar.js +16 -2
  17. package/dist/views/dev/chrome/toasts.d.ts +1 -0
  18. package/dist/views/dev/chrome/toasts.js +8 -4
  19. package/dist/views/dev/components/content_title.d.ts +6 -0
  20. package/dist/views/dev/components/content_title.js +7 -0
  21. package/dist/views/dev/components/docker_service_status.d.ts +12 -0
  22. package/dist/views/dev/components/docker_service_status.js +19 -0
  23. package/dist/views/dev/components/inline_snippet.d.ts +4 -0
  24. package/dist/views/dev/components/inline_snippet.js +3 -0
  25. package/dist/views/dev/components/master_detail_panel.d.ts +9 -8
  26. package/dist/views/dev/components/master_detail_panel.js +8 -5
  27. package/dist/views/dev/components/run_info_sidebar.d.ts +7 -0
  28. package/dist/views/dev/components/run_info_sidebar.js +19 -0
  29. package/dist/views/dev/components/workflow_status.d.ts +12 -0
  30. package/dist/views/dev/components/workflow_status.js +19 -0
  31. package/dist/views/dev/dev_app.js +107 -31
  32. package/dist/views/dev/hooks/use_run_detail.js +6 -9
  33. package/dist/views/dev/hooks/use_run_detail.spec.js +7 -0
  34. package/dist/views/dev/modals/expanded_json_modal.js +5 -6
  35. package/dist/views/dev/modals/modal_frame.d.ts +13 -0
  36. package/dist/views/dev/modals/modal_frame.js +13 -0
  37. package/dist/views/dev/modals/run_modal.js +23 -13
  38. package/dist/views/dev/{panels/run_detail_view.d.ts → modals/steps_modal.d.ts} +2 -1
  39. package/dist/views/dev/modals/steps_modal.js +102 -0
  40. package/dist/views/dev/panels/help_panel.d.ts +14 -0
  41. package/dist/views/dev/panels/help_panel.js +19 -21
  42. package/dist/views/dev/panels/runs_panel.d.ts +6 -2
  43. package/dist/views/dev/panels/runs_panel.js +82 -83
  44. package/dist/views/dev/panels/runs_panel.spec.js +1 -28
  45. package/dist/views/dev/panels/services_panel.d.ts +6 -0
  46. package/dist/views/dev/panels/services_panel.js +53 -62
  47. package/dist/views/dev/panels/workflows_panel.d.ts +6 -0
  48. package/dist/views/dev/panels/workflows_panel.js +21 -29
  49. package/dist/views/dev/panels/workflows_panel.spec.d.ts +1 -0
  50. package/dist/views/dev/panels/workflows_panel.spec.js +39 -0
  51. package/dist/views/dev/state/ui_state.d.ts +7 -3
  52. package/dist/views/dev/state/ui_state.js +23 -6
  53. package/dist/views/dev/utils/constants.d.ts +2 -2
  54. package/dist/views/dev/utils/constants.js +2 -2
  55. package/dist/views/dev/utils/json_editor.js +3 -3
  56. package/dist/views/dev/utils/json_render.d.ts +2 -0
  57. package/dist/views/dev/utils/json_render.js +48 -6
  58. package/dist/views/dev/utils/json_render.spec.js +9 -1
  59. package/dist/views/dev/utils/panel_helpers.d.ts +15 -0
  60. package/dist/views/dev/utils/panel_helpers.js +30 -0
  61. package/dist/views/dev/utils/panel_helpers.spec.js +46 -1
  62. package/package.json +4 -4
  63. package/dist/components/status_icon.d.ts +0 -11
  64. package/dist/components/status_icon.js +0 -25
  65. package/dist/views/dev/chrome/divider.d.ts +0 -8
  66. package/dist/views/dev/chrome/divider.js +0 -16
  67. package/dist/views/dev/panels/run_detail_view.js +0 -112
@@ -1,17 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useState } from 'react';
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 { StatusIcon } from '#components/status_icon.js';
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
- const VISIBLE_LOG_LINES = 18;
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, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Services" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Box, { width: COL.indicator, children: _jsx(Text, { children: " " }) }), _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" }) })] })] }));
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(StatusIcon, { 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 }) })] }));
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 FailureBanner = ({ services }) => {
36
- const failed = services.filter(isServiceFailed);
37
- if (failed.length === 0) {
38
- return null;
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
- if (lines.length === 0) {
47
- return _jsx(LoadingSpinner, { label: `Waiting for logs from ${serviceName}…` });
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 HINTS = [
62
+ export const SERVICES_HINTS = [
67
63
  { key: '↑/↓', label: 'navigate' },
68
- { key: 'r/R', label: 'restart one/all' },
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 Detail = ({ service, services, lines, paused, banner }) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(FailureBanner, { services: services }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, children: service?.name ?? 'Logs' }) }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(LogPane, { serviceName: service?.name ?? null, lines: lines, paused: paused }) }), banner && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: banner }) }))] }));
81
- export const ServicesPanel = ({ phase, services, dockerComposePath }) => {
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 [selectedIndex, setSelectedIndex] = useState(0);
84
- const [banner, setBanner] = useState(null);
79
+ const lastFailedCountRef = useRef(0);
85
80
  const sortedServices = useMemo(() => [...services].sort(compareService), [services]);
86
- const isActive = ui.tab === 'services' && !ui.search.open && !ui.runModal.open;
87
- const clamped = Math.min(selectedIndex, Math.max(0, sortedServices.length - 1));
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 enabledLogs = isActive && phase === 'running' && Boolean(selectedService);
90
- useEffect(() => {
91
- if (clamped !== selectedIndex) {
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 (!banner) {
98
- return () => { };
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
- const id = setTimeout(() => setBanner(null), 3000);
101
- return () => clearTimeout(id);
102
- }, [banner]);
92
+ lastFailedCountRef.current = failedCount;
93
+ }, [failedCount, pushToast]);
103
94
  useInput((input, key) => {
104
95
  if (key.upArrow) {
105
- setSelectedIndex(i => Math.max(0, i - 1));
96
+ selectPrevious();
106
97
  return;
107
98
  }
108
99
  if (key.downArrow) {
109
- setSelectedIndex(i => Math.min(sortedServices.length - 1, i + 1));
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
- setBanner(`Opened ${url}`);
118
+ pushToast(`Opened ${url}`, 'info');
128
119
  }
129
120
  else {
130
- setBanner(`${selectedService.name} has no known URL`);
121
+ pushToast(`${selectedService.name} has no known URL`, 'error');
131
122
  }
132
123
  return;
133
124
  }
134
- if (input === 'r') {
135
- setBanner(`Restarting ${selectedService.name}…`);
125
+ if (!key.ctrl && input === 'r') {
126
+ pushToast(`Restarting ${selectedService.name}...`, 'info');
136
127
  restartService(dockerComposePath, selectedService.name)
137
- .then(() => setBanner(`Restarted ${selectedService.name}`))
138
- .catch(err => setBanner(`Restart failed: ${err instanceof Error ? err.message : String(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 === 'R') {
142
- setBanner('Restarting all services');
132
+ if (key.ctrl && input === 'r') {
133
+ pushToast('Restarting all services...', 'info');
143
134
  restartStack(dockerComposePath)
144
- .then(() => setBanner('Restarted all services'))
145
- .catch(err => setBanner(`Restart failed: ${err instanceof Error ? err.message : String(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 (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(LoadingSpinner, { label: "Starting Docker services\u2026" }), _jsx(Footer, { hints: HINTS_BOOT })] }));
140
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(LoadingSpinner, { label: "Starting Docker services\u2026" }) }));
150
141
  }
151
142
  if (services.length === 0) {
152
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "No services running." }), _jsx(Footer, { hints: HINTS_BOOT })] }));
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, services: services, lines: logs.lines, paused: logs.paused, banner: banner }), hints: phase === 'running' ? HINTS : HINTS_BOOT, itemLabel: "services" }));
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, useState } from 'react';
2
+ import { useEffect, useMemo } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
- import { StatusIcon, statusColor } from '#components/status_icon.js';
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(StatusIcon, { status: status }), _jsx(Text, { children: " " }), _jsx(Box, { width: 10, children: _jsx(Text, { color: statusColor(status), children: status }) }), _jsx(Box, { width: 9, justifyContent: "flex-end", children: _jsx(Text, { dimColor: true, children: duration }) }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { dimColor: true, children: formatStartedShort(run.startedAt) }) })] }));
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: "column", children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: "white", children: workflow.name }) }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: [stats.total, " runs"] }), stats.running > 0 && _jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: "blue", children: ["\u25CF ", stats.running, " running"] })] }), stats.failed > 0 && _jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: "red", children: ["\u2717 ", stats.failed, " failed"] })] }), stats.completed > 0 && _jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: "green", children: ["\u25CF ", stats.completed, " ok"] })] })] }), _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, paddingRight: 2, children: _jsx(Text, { wrap: "wrap", children: workflow.description ?? 'No description' }) }), _jsxs(Box, { flexDirection: "column", width: 42, borderStyle: "single", borderTop: false, borderBottom: false, borderRight: false, paddingLeft: 2, children: [_jsx(Text, { dimColor: true, bold: true, children: "RECENT RUNS" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: recent.length === 0 ? (_jsx(Text, { dimColor: true, children: "No runs yet" })) : (recent.map((run, i) => _jsx(SidebarRunRow, { run: run }, `${run.runId ?? i}`))) })] })] })] }));
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 HINTS = [
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
- // Lazy initializer runs once on mount. Restores the previously
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 isActive = ui.tab === 'workflows' && !ui.search.open && !ui.runModal.open;
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
- setSelectedIndex(i => Math.max(0, i - 1));
83
+ selectPrevious();
92
84
  }
93
85
  else if (key.downArrow) {
94
- setSelectedIndex(i => Math.min(filtered.length - 1, i + 1));
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 (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Workflows" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Loading catalog\u2026 (waiting for the API to come up)" }) }), _jsx(Footer, { hints: [{ key: 'tab', label: 'next tab' }, { key: '?', label: 'help' }] })] }));
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 (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Workflows" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: ["No workflows match `", ui.search.query, "`. Press "] }), _jsx(Text, { bold: true, children: "esc" }), _jsx(Text, { dimColor: true, children: " to clear." })] }), _jsx(Footer, { hints: HINTS, itemCount: 0, itemLabel: "workflows" })] }));
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 }), hints: HINTS, itemLabel: "workflows" }));
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 RightPaneTab = 'input' | 'output' | 'meta';
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
- rightPaneTab: RightPaneTab;
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
- setRightPaneTab: (tab: RightPaneTab) => void;
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 [rightPaneTab, setRightPaneTab] = useState('output');
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
- rightPaneTab,
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
- setRightPaneTab,
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, rightPaneTab, runsView, runModal, expandedJson, toasts]);
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 RUN_DETAIL_PREVIEW_LINES = 18;
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 RUN_DETAIL_PREVIEW_LINES = 18;
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, { bold: true, children: ["\u270F ", 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) => {
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 raws = Array.from(line.matchAll(RAW_TOKEN_RE), m => classifyRaw(m[0]));
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 JsonView = ({ value, maxLines, offset = 0, truncateLine = true, showOverflowFooter = true }) => {
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 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'] }))] }));
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
  };