@outputai/cli 0.3.2-next.5e221e8.0 → 0.3.3-dev.422151e.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 (94) hide show
  1. package/dist/assets/docker/docker-compose-dev.yml +1 -1
  2. package/dist/commands/dev/index.js +48 -13
  3. package/dist/commands/dev/index.spec.js +1 -2
  4. package/dist/components/status_icon.js +1 -1
  5. package/dist/generated/framework_version.json +1 -1
  6. package/dist/services/docker.js +0 -1
  7. package/dist/templates/project/src/workflows/blog_evaluator/prompts/signal_noise@v1.prompt.template +2 -1
  8. package/dist/templates/workflow/README.md.template +2 -1
  9. package/dist/templates/workflow/prompts/example@v1.prompt.template +2 -1
  10. package/dist/utils/paths.d.ts +11 -0
  11. package/dist/utils/paths.js +14 -0
  12. package/dist/utils/scenario_resolver.js +3 -2
  13. package/dist/views/dev/chrome/divider.d.ts +8 -0
  14. package/dist/views/dev/chrome/divider.js +16 -0
  15. package/dist/views/dev/chrome/footer.d.ts +11 -0
  16. package/dist/views/dev/chrome/footer.js +10 -0
  17. package/dist/views/dev/chrome/header.d.ts +21 -0
  18. package/dist/views/dev/chrome/header.js +74 -0
  19. package/dist/views/dev/chrome/header.spec.d.ts +1 -0
  20. package/dist/views/dev/chrome/header.spec.js +50 -0
  21. package/dist/views/dev/chrome/loading_spinner.d.ts +9 -0
  22. package/dist/views/dev/chrome/loading_spinner.js +9 -0
  23. package/dist/views/dev/chrome/palette.d.ts +16 -0
  24. package/dist/views/dev/chrome/palette.js +16 -0
  25. package/dist/views/dev/chrome/search_bar.d.ts +5 -0
  26. package/dist/views/dev/chrome/search_bar.js +35 -0
  27. package/dist/views/dev/chrome/selection_indicator.d.ts +14 -0
  28. package/dist/views/dev/chrome/selection_indicator.js +13 -0
  29. package/dist/views/dev/chrome/tab_bar.d.ts +5 -0
  30. package/dist/views/dev/chrome/tab_bar.js +4 -0
  31. package/dist/views/dev/chrome/toasts.d.ts +2 -0
  32. package/dist/views/dev/chrome/toasts.js +40 -0
  33. package/dist/views/dev/components/master_detail_panel.d.ts +21 -0
  34. package/dist/views/dev/components/master_detail_panel.js +18 -0
  35. package/dist/views/{dev.d.ts → dev/dev_app.d.ts} +1 -0
  36. package/dist/views/dev/dev_app.js +146 -0
  37. package/dist/views/dev/hooks/use_docker_logs.d.ts +7 -0
  38. package/dist/views/dev/hooks/use_docker_logs.js +69 -0
  39. package/dist/views/dev/hooks/use_poll.d.ts +16 -0
  40. package/dist/views/dev/hooks/use_poll.js +95 -0
  41. package/dist/views/dev/hooks/use_run_detail.d.ts +21 -0
  42. package/dist/views/dev/hooks/use_run_detail.js +153 -0
  43. package/dist/views/dev/hooks/use_run_detail.spec.d.ts +1 -0
  44. package/dist/views/dev/hooks/use_run_detail.spec.js +86 -0
  45. package/dist/views/dev/hooks/use_workflow_catalog.d.ts +2 -0
  46. package/dist/views/dev/hooks/use_workflow_catalog.js +21 -0
  47. package/dist/views/dev/modals/expanded_json_modal.d.ts +2 -0
  48. package/dist/views/dev/modals/expanded_json_modal.js +44 -0
  49. package/dist/views/dev/modals/run_modal.d.ts +4 -0
  50. package/dist/views/dev/modals/run_modal.js +213 -0
  51. package/dist/views/dev/panels/help_panel.d.ts +2 -0
  52. package/dist/views/dev/panels/help_panel.js +53 -0
  53. package/dist/views/dev/panels/run_detail_view.d.ts +5 -0
  54. package/dist/views/dev/panels/run_detail_view.js +112 -0
  55. package/dist/views/dev/panels/runs_panel.d.ts +8 -0
  56. package/dist/views/dev/panels/runs_panel.js +204 -0
  57. package/dist/views/dev/panels/runs_panel.spec.d.ts +1 -0
  58. package/dist/views/dev/panels/runs_panel.spec.js +82 -0
  59. package/dist/views/dev/panels/services_panel.d.ts +14 -0
  60. package/dist/views/dev/panels/services_panel.js +155 -0
  61. package/dist/views/dev/panels/services_panel.spec.d.ts +1 -0
  62. package/dist/views/dev/panels/services_panel.spec.js +28 -0
  63. package/dist/views/dev/panels/workflows_panel.d.ts +7 -0
  64. package/dist/views/dev/panels/workflows_panel.js +111 -0
  65. package/dist/views/dev/services/docker_control.d.ts +5 -0
  66. package/dist/views/dev/services/docker_control.js +25 -0
  67. package/dist/views/dev/services/run_workflow.d.ts +10 -0
  68. package/dist/views/dev/services/run_workflow.js +14 -0
  69. package/dist/views/dev/services/scenario_io.d.ts +2 -0
  70. package/dist/views/dev/services/scenario_io.js +37 -0
  71. package/dist/views/dev/state/ui_state.d.ts +60 -0
  72. package/dist/views/dev/state/ui_state.js +64 -0
  73. package/dist/views/dev/utils/constants.d.ts +17 -0
  74. package/dist/views/dev/utils/constants.js +17 -0
  75. package/dist/views/dev/utils/json_editor.d.ts +21 -0
  76. package/dist/views/dev/utils/json_editor.js +117 -0
  77. package/dist/views/dev/utils/json_editor.spec.d.ts +1 -0
  78. package/dist/views/dev/utils/json_editor.spec.js +57 -0
  79. package/dist/views/dev/utils/json_render.d.ts +15 -0
  80. package/dist/views/dev/utils/json_render.js +77 -0
  81. package/dist/views/dev/utils/json_render.spec.d.ts +1 -0
  82. package/dist/views/dev/utils/json_render.spec.js +65 -0
  83. package/dist/views/dev/utils/panel_helpers.d.ts +16 -0
  84. package/dist/views/dev/utils/panel_helpers.js +32 -0
  85. package/dist/views/dev/utils/panel_helpers.spec.d.ts +1 -0
  86. package/dist/views/dev/utils/panel_helpers.spec.js +47 -0
  87. package/package.json +5 -5
  88. package/dist/components/command_footer.d.ts +0 -8
  89. package/dist/components/command_footer.js +0 -4
  90. package/dist/components/workflow_summary.d.ts +0 -10
  91. package/dist/components/workflow_summary.js +0 -4
  92. package/dist/views/dev.js +0 -187
  93. package/dist/views/workflow/list.d.ts +0 -6
  94. package/dist/views/workflow/list.js +0 -129
@@ -0,0 +1,10 @@
1
+ export interface StartedRun {
2
+ workflowId?: string;
3
+ runId?: string | null;
4
+ }
5
+ export interface StartWorkflowOptions {
6
+ workflowName: string;
7
+ input: unknown;
8
+ taskQueue?: string;
9
+ }
10
+ export declare const startWorkflow: (opts: StartWorkflowOptions) => Promise<StartedRun>;
@@ -0,0 +1,14 @@
1
+ import { postWorkflowStart } from '#api/generated/api.js';
2
+ export const startWorkflow = async (opts) => {
3
+ const response = await postWorkflowStart({
4
+ workflowName: opts.workflowName,
5
+ input: opts.input,
6
+ taskQueue: opts.taskQueue
7
+ });
8
+ if (response.status !== 200) {
9
+ const data = response.data;
10
+ throw new Error(data?.error ?? `Workflow start failed (status ${response.status})`);
11
+ }
12
+ const data = response.data;
13
+ return { workflowId: data.workflowId, runId: data.runId };
14
+ };
@@ -0,0 +1,2 @@
1
+ export declare const readScenario: (workflowName: string, scenarioName: string) => Promise<unknown>;
2
+ export declare const writeScenario: (workflowName: string, scenarioName: string, content: unknown) => Promise<string>;
@@ -0,0 +1,37 @@
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { resolve, dirname } from 'node:path';
4
+ import { resolveScenarioPath } from '#utils/scenario_resolver.js';
5
+ import { getWorkflowsBasePath } from '#utils/paths.js';
6
+ const WORKFLOWS_PATHS = ['src/workflows', 'workflows'];
7
+ export const readScenario = async (workflowName, scenarioName) => {
8
+ const resolution = await resolveScenarioPath(workflowName, scenarioName);
9
+ if (!resolution.found || !resolution.path) {
10
+ throw new Error(`Scenario '${scenarioName}' not found for workflow '${workflowName}'.`);
11
+ }
12
+ const content = await readFile(resolution.path, 'utf-8');
13
+ return JSON.parse(content);
14
+ };
15
+ const findWorkflowDirectory = (workflowName) => {
16
+ const basePath = getWorkflowsBasePath();
17
+ for (const wfDir of WORKFLOWS_PATHS) {
18
+ const candidate = resolve(basePath, wfDir, workflowName);
19
+ if (existsSync(candidate)) {
20
+ return candidate;
21
+ }
22
+ }
23
+ return null;
24
+ };
25
+ export const writeScenario = async (workflowName, scenarioName, content) => {
26
+ const dir = findWorkflowDirectory(workflowName);
27
+ if (!dir) {
28
+ throw new Error(`Workflow directory for '${workflowName}' not found locally.`);
29
+ }
30
+ const targetPath = resolve(dir, 'scenarios', `${scenarioName}.json`);
31
+ if (existsSync(targetPath)) {
32
+ throw new Error(`Scenario '${scenarioName}' already exists at ${targetPath}`);
33
+ }
34
+ await mkdir(dirname(targetPath), { recursive: true });
35
+ await writeFile(targetPath, JSON.stringify(content, null, 2) + '\n', 'utf-8');
36
+ return targetPath;
37
+ };
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ export type Tab = 'workflows' | 'runs' | 'services' | 'help';
3
+ export declare const TAB_ORDER: Tab[];
4
+ export declare const TAB_LABELS: Record<Tab, string>;
5
+ export type RightPaneTab = 'input' | 'output' | 'meta';
6
+ export type RunsView = 'list' | 'detail';
7
+ export interface Selection {
8
+ workflowName?: string;
9
+ runId?: string;
10
+ workflowId?: string;
11
+ serviceName?: string;
12
+ }
13
+ export interface SearchState {
14
+ open: boolean;
15
+ query: string;
16
+ }
17
+ export interface RunModalState {
18
+ open: boolean;
19
+ workflowName: string;
20
+ }
21
+ export interface ExpandedJsonState {
22
+ open: boolean;
23
+ value: unknown;
24
+ title: string;
25
+ }
26
+ export interface Toast {
27
+ id: number;
28
+ message: string;
29
+ tone: 'info' | 'success' | 'error';
30
+ }
31
+ export interface UiState {
32
+ tab: Tab;
33
+ search: SearchState;
34
+ selection: Selection;
35
+ rightPaneTab: RightPaneTab;
36
+ runsView: RunsView;
37
+ runModal: RunModalState;
38
+ expandedJson: ExpandedJsonState;
39
+ toasts: Toast[];
40
+ setTab: (tab: Tab) => void;
41
+ nextTab: () => void;
42
+ prevTab: () => void;
43
+ openSearch: () => void;
44
+ closeSearch: () => void;
45
+ clearSearch: () => void;
46
+ setSearchQuery: (query: string) => void;
47
+ setSelection: (selection: Selection) => void;
48
+ setRightPaneTab: (tab: RightPaneTab) => void;
49
+ setRunsView: (view: RunsView) => void;
50
+ openRunModal: (workflowName: string) => void;
51
+ closeRunModal: () => void;
52
+ openExpandedJson: (value: unknown, title: string) => void;
53
+ closeExpandedJson: () => void;
54
+ pushToast: (message: string, tone?: Toast['tone']) => void;
55
+ dismissToast: (id: number) => void;
56
+ }
57
+ export declare const UiStateProvider: React.FC<{
58
+ children: React.ReactNode;
59
+ }>;
60
+ export declare const useUiState: () => UiState;
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useMemo, useRef, useState } from 'react';
3
+ export const TAB_ORDER = ['workflows', 'runs', 'services', 'help'];
4
+ export const TAB_LABELS = {
5
+ workflows: 'Workflows',
6
+ runs: 'Recent Runs',
7
+ services: 'Services',
8
+ help: 'Help'
9
+ };
10
+ const UiStateContext = createContext(null);
11
+ export const UiStateProvider = ({ children }) => {
12
+ const [tab, setTab] = useState('services');
13
+ const [search, setSearch] = useState({ open: false, query: '' });
14
+ const [selection, setSelection] = useState({});
15
+ const [rightPaneTab, setRightPaneTab] = useState('output');
16
+ const [runsView, setRunsView] = useState('list');
17
+ const [runModal, setRunModal] = useState({ open: false, workflowName: '' });
18
+ const [expandedJson, setExpandedJson] = useState({ open: false, value: null, title: '' });
19
+ const [toasts, setToasts] = useState([]);
20
+ const toastIdRef = useRef(0);
21
+ const value = useMemo(() => ({
22
+ tab,
23
+ search,
24
+ selection,
25
+ rightPaneTab,
26
+ runsView,
27
+ runModal,
28
+ expandedJson,
29
+ toasts,
30
+ setTab,
31
+ nextTab: () => setTab(current => {
32
+ const idx = TAB_ORDER.indexOf(current);
33
+ return TAB_ORDER[(idx + 1) % TAB_ORDER.length];
34
+ }),
35
+ prevTab: () => setTab(current => {
36
+ const idx = TAB_ORDER.indexOf(current);
37
+ return TAB_ORDER[(idx - 1 + TAB_ORDER.length) % TAB_ORDER.length];
38
+ }),
39
+ openSearch: () => setSearch(prev => ({ open: true, query: prev.query })),
40
+ closeSearch: () => setSearch(prev => ({ open: false, query: prev.query })),
41
+ clearSearch: () => setSearch({ open: false, query: '' }),
42
+ setSearchQuery: (query) => setSearch(prev => ({ open: prev.open, query })),
43
+ setSelection,
44
+ setRightPaneTab,
45
+ setRunsView,
46
+ openRunModal: (workflowName) => setRunModal({ open: true, workflowName }),
47
+ closeRunModal: () => setRunModal({ open: false, workflowName: '' }),
48
+ openExpandedJson: (value, title) => setExpandedJson({ open: true, value, title }),
49
+ closeExpandedJson: () => setExpandedJson({ open: false, value: null, title: '' }),
50
+ pushToast: (message, tone = 'info') => {
51
+ const id = ++toastIdRef.current;
52
+ setToasts(prev => [...prev, { id, message, tone }]);
53
+ },
54
+ dismissToast: (id) => setToasts(prev => prev.filter(t => t.id !== id))
55
+ }), [tab, search, selection, rightPaneTab, runsView, runModal, expandedJson, toasts]);
56
+ return _jsx(UiStateContext.Provider, { value: value, children: children });
57
+ };
58
+ export const useUiState = () => {
59
+ const ctx = useContext(UiStateContext);
60
+ if (!ctx) {
61
+ throw new Error('useUiState must be used inside <UiStateProvider>');
62
+ }
63
+ return ctx;
64
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Workflow type emitted by Temporal's catalog refresh, used to drop the
3
+ * routine completed catalog rows from the Recent Runs list while still
4
+ * surfacing failing or running ones for diagnostics.
5
+ */
6
+ export declare const CATALOG_WORKFLOW_NAME = "$catalog";
7
+ /**
8
+ * Visible row counts and per-pane layout knobs. Co-located here so the
9
+ * top-of-file constant blocks across panels stay terse.
10
+ */
11
+ export declare const WORKFLOWS_VISIBLE_ROWS = 8;
12
+ export declare const WORKFLOWS_RECENT_RUNS_LIMIT = 5;
13
+ export declare const RUNS_VISIBLE_ROWS = 8;
14
+ export declare const RUNS_PREVIEW_LINES = 12;
15
+ export declare const RUN_DETAIL_VISIBLE_STEPS = 12;
16
+ export declare const RUN_DETAIL_PREVIEW_LINES = 18;
17
+ export declare const HELP_DOCS_URL = "https://docs.output.ai";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Workflow type emitted by Temporal's catalog refresh, used to drop the
3
+ * routine completed catalog rows from the Recent Runs list while still
4
+ * surfacing failing or running ones for diagnostics.
5
+ */
6
+ export const CATALOG_WORKFLOW_NAME = '$catalog';
7
+ /**
8
+ * Visible row counts and per-pane layout knobs. Co-located here so the
9
+ * top-of-file constant blocks across panels stay terse.
10
+ */
11
+ export const WORKFLOWS_VISIBLE_ROWS = 8;
12
+ export const WORKFLOWS_RECENT_RUNS_LIMIT = 5;
13
+ export const RUNS_VISIBLE_ROWS = 8;
14
+ export const RUNS_PREVIEW_LINES = 12;
15
+ export const RUN_DETAIL_VISIBLE_STEPS = 12;
16
+ export const RUN_DETAIL_PREVIEW_LINES = 18;
17
+ export const HELP_DOCS_URL = 'https://docs.output.ai';
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ interface CursorPos {
3
+ line: number;
4
+ col: number;
5
+ }
6
+ export declare const cursorToPosition: (buffer: string, cursor: number) => CursorPos;
7
+ export declare const positionToCursor: (buffer: string, line: number, col: number) => number;
8
+ export declare const tryParseJson: (text: string) => {
9
+ ok: true;
10
+ } | {
11
+ ok: false;
12
+ error: string;
13
+ };
14
+ export declare const JsonEditor: React.FC<{
15
+ seed: unknown;
16
+ title: string;
17
+ isActive?: boolean;
18
+ onSubmit: (value: unknown) => void;
19
+ onCancel: () => void;
20
+ }>;
21
+ export {};
@@ -0,0 +1,117 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ export const cursorToPosition = (buffer, cursor) => {
5
+ const before = buffer.slice(0, cursor);
6
+ const lines = before.split('\n');
7
+ return { line: lines.length - 1, col: lines[lines.length - 1].length };
8
+ };
9
+ export const positionToCursor = (buffer, line, col) => {
10
+ const allLines = buffer.split('\n');
11
+ const targetLine = Math.max(0, Math.min(line, allLines.length - 1));
12
+ const targetCol = Math.max(0, Math.min(col, allLines[targetLine].length));
13
+ const charsBefore = allLines.slice(0, targetLine).reduce((acc, l) => acc + l.length + 1, 0);
14
+ return charsBefore + targetCol;
15
+ };
16
+ export const tryParseJson = (text) => {
17
+ if (text.trim().length === 0) {
18
+ return { ok: false, error: 'Empty' };
19
+ }
20
+ try {
21
+ JSON.parse(text);
22
+ return { ok: true };
23
+ }
24
+ catch (err) {
25
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
26
+ }
27
+ };
28
+ const VISIBLE_BUFFER = 6;
29
+ export const JsonEditor = ({ seed, title, isActive = true, onSubmit, onCancel }) => {
30
+ const initial = JSON.stringify(seed ?? {}, null, 2);
31
+ const [buffer, setBuffer] = useState(initial);
32
+ const [cursor, setCursor] = useState(initial.length);
33
+ const [status, setStatus] = useState(() => tryParseJson(initial));
34
+ const [submitMessage, setSubmitMessage] = useState(null);
35
+ useEffect(() => {
36
+ setStatus(tryParseJson(buffer));
37
+ }, [buffer]);
38
+ const insert = (text) => {
39
+ setBuffer(b => b.slice(0, cursor) + text + b.slice(cursor));
40
+ setCursor(c => c + text.length);
41
+ };
42
+ const removeBeforeCursor = () => {
43
+ if (cursor === 0) {
44
+ return;
45
+ }
46
+ setBuffer(b => b.slice(0, cursor - 1) + b.slice(cursor));
47
+ setCursor(c => Math.max(0, c - 1));
48
+ };
49
+ useInput((input, key) => {
50
+ if (key.escape) {
51
+ onCancel();
52
+ return;
53
+ }
54
+ if (key.ctrl && input === 's') {
55
+ const parsed = tryParseJson(buffer);
56
+ if (!parsed.ok) {
57
+ setSubmitMessage(`Cannot submit — ${parsed.error}`);
58
+ return;
59
+ }
60
+ try {
61
+ onSubmit(JSON.parse(buffer));
62
+ }
63
+ catch (err) {
64
+ setSubmitMessage(err instanceof Error ? err.message : String(err));
65
+ }
66
+ return;
67
+ }
68
+ if (key.return) {
69
+ insert('\n');
70
+ return;
71
+ }
72
+ if (key.tab) {
73
+ insert(' ');
74
+ return;
75
+ }
76
+ if (key.backspace || key.delete) {
77
+ removeBeforeCursor();
78
+ return;
79
+ }
80
+ if (key.leftArrow) {
81
+ setCursor(c => Math.max(0, c - 1));
82
+ return;
83
+ }
84
+ if (key.rightArrow) {
85
+ setCursor(c => Math.min(buffer.length, c + 1));
86
+ return;
87
+ }
88
+ if (key.upArrow) {
89
+ const pos = cursorToPosition(buffer, cursor);
90
+ setCursor(positionToCursor(buffer, pos.line - 1, pos.col));
91
+ return;
92
+ }
93
+ if (key.downArrow) {
94
+ const pos = cursorToPosition(buffer, cursor);
95
+ setCursor(positionToCursor(buffer, pos.line + 1, pos.col));
96
+ return;
97
+ }
98
+ if (input && !key.ctrl && !key.meta) {
99
+ insert(input);
100
+ }
101
+ }, { isActive });
102
+ const lines = buffer.split('\n');
103
+ const cursorPos = cursorToPosition(buffer, cursor);
104
+ const half = Math.floor(VISIBLE_BUFFER / 2);
105
+ const startLine = Math.max(0, cursorPos.line - half);
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) => {
108
+ const lineIdx = startLine + i;
109
+ if (lineIdx !== cursorPos.line) {
110
+ return _jsx(Text, { children: line.length === 0 ? ' ' : line }, lineIdx);
111
+ }
112
+ const before = line.slice(0, cursorPos.col);
113
+ const at = line[cursorPos.col] ?? ' ';
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));
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
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { cursorToPosition, positionToCursor, tryParseJson } from './json_editor.js';
3
+ describe('cursorToPosition', () => {
4
+ it('returns line 0 col 0 for an empty buffer', () => {
5
+ expect(cursorToPosition('', 0)).toEqual({ line: 0, col: 0 });
6
+ });
7
+ it('tracks column on a single-line buffer', () => {
8
+ expect(cursorToPosition('hello', 3)).toEqual({ line: 0, col: 3 });
9
+ });
10
+ it('crosses newline boundaries', () => {
11
+ // 'ab\ncd' — cursor 3 is just before `c` on line 1
12
+ expect(cursorToPosition('ab\ncd', 3)).toEqual({ line: 1, col: 0 });
13
+ });
14
+ it('lands at the end of a line', () => {
15
+ expect(cursorToPosition('ab\ncd', 5)).toEqual({ line: 1, col: 2 });
16
+ });
17
+ });
18
+ describe('positionToCursor', () => {
19
+ it('returns 0 for an empty buffer', () => {
20
+ expect(positionToCursor('', 0, 0)).toBe(0);
21
+ });
22
+ it('computes index for a column on a given line', () => {
23
+ expect(positionToCursor('ab\ncd', 1, 1)).toBe(4);
24
+ });
25
+ it('clamps the column to the line length', () => {
26
+ expect(positionToCursor('ab\ncd', 0, 99)).toBe(2);
27
+ });
28
+ it('clamps to the last line when over-shooting', () => {
29
+ expect(positionToCursor('ab\ncd', 99, 0)).toBe(3);
30
+ });
31
+ it('clamps to the first line when under-shooting', () => {
32
+ expect(positionToCursor('ab\ncd', -5, 1)).toBe(1);
33
+ });
34
+ it('roundtrips with cursorToPosition', () => {
35
+ const buffer = 'foo\nbar\nbaz';
36
+ for (const cursor of [0, 2, 4, 7, 10]) {
37
+ const pos = cursorToPosition(buffer, cursor);
38
+ expect(positionToCursor(buffer, pos.line, pos.col)).toBe(cursor);
39
+ }
40
+ });
41
+ });
42
+ describe('tryParseJson', () => {
43
+ it('rejects empty input', () => {
44
+ expect(tryParseJson('')).toEqual({ ok: false, error: 'Empty' });
45
+ expect(tryParseJson(' \n ')).toEqual({ ok: false, error: 'Empty' });
46
+ });
47
+ it('accepts valid JSON', () => {
48
+ expect(tryParseJson('{"a":1}')).toEqual({ ok: true });
49
+ });
50
+ it('returns the parser error for invalid JSON', () => {
51
+ const result = tryParseJson('{a:1}');
52
+ expect(result.ok).toBe(false);
53
+ if (!result.ok) {
54
+ expect(result.error.length).toBeGreaterThan(0);
55
+ }
56
+ });
57
+ });
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ export interface ColoredToken {
3
+ text: string;
4
+ color?: string;
5
+ }
6
+ export declare const tokenizeLine: (line: string) => ColoredToken[];
7
+ export declare const formatJsonText: (value: unknown) => string;
8
+ export declare const countJsonLines: (value: unknown) => number;
9
+ export declare const JsonView: React.FC<{
10
+ value: unknown;
11
+ maxLines?: number;
12
+ offset?: number;
13
+ truncateLine?: boolean;
14
+ showOverflowFooter?: boolean;
15
+ }>;
@@ -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 {};