loop-task 1.5.5 → 2.0.1
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/README.md +127 -21
- package/dist/board/App.js +1 -1
- package/dist/board/components/ActionButtons.js +1 -1
- package/dist/board/components/BoardButton.js +1 -1
- package/dist/board/components/ConfirmModal.js +1 -1
- package/dist/board/components/ContextHelpModal.js +1 -1
- package/dist/board/components/CreateForm.js +1 -1
- package/dist/board/components/CreateProjectModal.js +1 -1
- package/dist/board/components/DeleteProjectConfirm.js +1 -1
- package/dist/board/components/DetailView.js +1 -1
- package/dist/board/components/EditProjectModal.js +1 -1
- package/dist/board/components/FilterBar.js +1 -1
- package/dist/board/components/Footer.js +1 -1
- package/dist/board/components/Header.js +1 -1
- package/dist/board/components/HelpModal.js +1 -1
- package/dist/board/components/Inspector.js +1 -1
- package/dist/board/components/LogModal.js +1 -1
- package/dist/board/components/Navigator.js +1 -1
- package/dist/board/components/ProjectsModal.js +1 -1
- package/dist/board/components/ProjectsPage.js +1 -1
- package/dist/board/components/RunHistory.js +1 -1
- package/dist/board/components/SearchBox.js +1 -1
- package/dist/board/components/SearchSelect.js +1 -1
- package/dist/board/components/TaskBrowser.js +1 -1
- package/dist/board/components/TaskFilterBar.js +1 -1
- package/dist/board/components/TaskForm.js +1 -1
- package/dist/board/components/Timeline.js +1 -1
- package/dist/board/focus-context.js +1 -1
- package/dist/board/format.js +15 -6
- package/dist/board/index.js +1 -1
- package/dist/board/toast.js +1 -1
- package/dist/cli/import-validator.js +151 -0
- package/dist/cli/import-writer.js +77 -0
- package/dist/cli.js +111 -17
- package/dist/config/constants.js +52 -0
- package/dist/daemon/file-watcher.js +171 -0
- package/dist/daemon/http-server.js +605 -0
- package/dist/daemon/index.js +36 -6
- package/dist/daemon/manager.js +94 -13
- package/dist/daemon/projects.js +10 -0
- package/dist/daemon/server.js +21 -0
- package/dist/daemon/task-manager.js +7 -0
- package/dist/esm-loader.js +4 -1
- package/dist/hooks/useLoopFormValidation.js +140 -0
- package/dist/i18n/en.json +237 -17
- package/dist/shared/fs-utils.js +34 -1
- package/dist/tui/app.js +675 -151
- package/dist/tui/commands.js +114 -0
- package/dist/tui/components/ActionButtons.js +49 -0
- package/dist/tui/components/Button.js +18 -0
- package/dist/tui/components/ChainEditor.js +112 -0
- package/dist/tui/components/CommandBuilderField.js +143 -0
- package/dist/tui/components/CommandEditorModal.js +159 -0
- package/dist/tui/components/CommandInput.js +303 -0
- package/dist/tui/components/CommandsBrowserModal.js +77 -0
- package/dist/tui/components/ConfirmModal.js +9 -0
- package/dist/tui/components/ContextHelpModal.js +8 -0
- package/dist/tui/components/CreateForm.js +223 -0
- package/dist/tui/components/DebugPanel.js +8 -0
- package/dist/tui/components/ExportModal.js +37 -0
- package/dist/tui/components/FilterBar.js +13 -0
- package/dist/tui/components/FocusableButton.js +16 -0
- package/dist/tui/components/FocusableInput.js +69 -0
- package/dist/tui/components/FocusableList.js +46 -0
- package/dist/tui/components/FocusableSearchSelect.js +82 -0
- package/dist/tui/components/Footer.js +42 -0
- package/dist/tui/components/Header.js +40 -0
- package/dist/tui/components/HelpGuideModal.js +21 -0
- package/dist/tui/components/HelpModal.js +59 -0
- package/dist/tui/components/InlineCommandEditor.js +143 -0
- package/dist/tui/components/Inspector.js +31 -0
- package/dist/tui/components/LeftPanel.js +33 -0
- package/dist/tui/components/LogModal.js +121 -0
- package/dist/tui/components/Modal.js +11 -0
- package/dist/tui/components/Navigator.js +68 -0
- package/dist/tui/components/PatchEditForm.js +78 -0
- package/dist/tui/components/ProjectForm.js +64 -0
- package/dist/tui/components/ProjectsModal.js +40 -0
- package/dist/tui/components/ProjectsPage.js +99 -0
- package/dist/tui/components/RightPanel.js +34 -0
- package/dist/tui/components/RunHistory.js +108 -0
- package/dist/tui/components/SearchSelect.js +67 -0
- package/dist/tui/components/SelectModal.js +76 -0
- package/dist/tui/components/TabBar.js +20 -0
- package/dist/tui/components/TaskBrowser.js +96 -0
- package/dist/tui/components/TaskFilterBar.js +9 -0
- package/dist/tui/components/TaskForm.js +134 -0
- package/dist/tui/components/TaskPickerModal.js +60 -0
- package/dist/tui/components/Toast.js +37 -0
- package/dist/tui/components/WelcomeScreen.js +70 -0
- package/dist/tui/components/WizardForm.js +178 -0
- package/dist/tui/daemon.js +142 -0
- package/dist/tui/format.js +111 -0
- package/dist/tui/hooks/useBreakpoint.js +7 -0
- package/dist/tui/hooks/useHoverState.js +11 -0
- package/dist/tui/hooks/useLogStream.js +32 -0
- package/dist/tui/hooks/useLoopPolling.js +28 -0
- package/dist/tui/index.js +24 -0
- package/dist/tui/router.js +16 -0
- package/dist/tui/state.js +175 -0
- package/dist/tui/theme.js +96 -0
- package/dist/tui/types.js +1 -0
- package/dist/tui/utils/paste.js +10 -0
- package/dist/tui/utils/validation.js +56 -0
- package/package.json +13 -8
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { COMMAND_TIER_ACTION, COMMAND_TIER_CONFIRM, COMMAND_TIER_GLOBAL, COMMAND_CATEGORY_GLOBAL, COMMAND_CATEGORY_FILTERS, COMMAND_CATEGORY_LOOP, COMMAND_CATEGORY_TASK, COMMAND_CATEGORY_PROJECT, } from '../config/constants.js';
|
|
2
|
+
import { t } from '../i18n/index.js';
|
|
3
|
+
// ── Shared helpers ───────────────────────────────────────────────────
|
|
4
|
+
function globalCommands() {
|
|
5
|
+
return [
|
|
6
|
+
{ label: t('cmd.help'), value: 'help', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL, shortcut: 'ctrl+p' },
|
|
7
|
+
{ label: t('cmd.debug'), value: 'debug', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL, shortcut: 'ctrl+b' },
|
|
8
|
+
{ label: t('cmd.api'), value: 'api', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL, shortcut: 'ctrl+g' },
|
|
9
|
+
{ label: t('cmd.export'), value: 'export', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL, shortcut: 'ctrl+x' },
|
|
10
|
+
{ label: t('cmd.import'), value: 'import', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL, shortcut: 'ctrl+i' },
|
|
11
|
+
{ label: t('cmd.status'), value: 'status', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL, shortcut: 'ctrl+y' },
|
|
12
|
+
];
|
|
13
|
+
}
|
|
14
|
+
function loopFilterCommands() {
|
|
15
|
+
return [
|
|
16
|
+
{ label: t('cmd.search'), value: 'search', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_FILTERS, shortcut: 'ctrl+f+s' },
|
|
17
|
+
{ label: t('cmd.filterStatus'), value: 'filter-status', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_FILTERS, shortcut: 'ctrl+f+t' },
|
|
18
|
+
{ label: t('cmd.sort'), value: 'sort', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_FILTERS, shortcut: 'ctrl+f+o' },
|
|
19
|
+
{ label: t('cmd.project'), value: 'filter-project', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_FILTERS, shortcut: 'ctrl+f+p' },
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
function taskFilterCommands() {
|
|
23
|
+
return [
|
|
24
|
+
{ label: t('cmd.searchTasks'), value: 'search', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_FILTERS, shortcut: 'ctrl+f+s' },
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
function projectFilterCommands() {
|
|
28
|
+
return [
|
|
29
|
+
{ label: t('cmd.searchProjects'), value: 'search', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_FILTERS, shortcut: 'ctrl+f+s' },
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
// ── Command ranking (exact > prefix > fuzzy) ─────────────────────────
|
|
33
|
+
/**
|
|
34
|
+
* Re-rank autocomplete options: exact match → prefix match → fuzzy (existing order).
|
|
35
|
+
* Stable within each group. Exported for testing.
|
|
36
|
+
*/
|
|
37
|
+
export function rankCommands(input, options) {
|
|
38
|
+
if (input.length === 0)
|
|
39
|
+
return options;
|
|
40
|
+
const q = input.toLowerCase();
|
|
41
|
+
const exact = [];
|
|
42
|
+
const prefix = [];
|
|
43
|
+
const fuzzy = [];
|
|
44
|
+
for (const opt of options) {
|
|
45
|
+
const l = opt.label.toLowerCase();
|
|
46
|
+
const v = opt.value.toLowerCase();
|
|
47
|
+
if (l === q || v === q) {
|
|
48
|
+
exact.push(opt);
|
|
49
|
+
}
|
|
50
|
+
else if (l.startsWith(q) || v.startsWith(q)) {
|
|
51
|
+
prefix.push(opt);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
fuzzy.push(opt);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return [...exact, ...prefix, ...fuzzy];
|
|
58
|
+
}
|
|
59
|
+
// ── Autocomplete dropdown (context-sensitive, shown while typing) ────
|
|
60
|
+
export function buildCommands(context) {
|
|
61
|
+
const commands = [...globalCommands()];
|
|
62
|
+
if (context.activeTab === 'loops') {
|
|
63
|
+
commands.push(...loopFilterCommands());
|
|
64
|
+
commands.push({
|
|
65
|
+
label: t('cmd.newLoop'), value: 'new-loop', hint: '',
|
|
66
|
+
tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+n',
|
|
67
|
+
});
|
|
68
|
+
if (context.selectedLoop !== null) {
|
|
69
|
+
const loop = context.selectedLoop;
|
|
70
|
+
const desc = loop.description || loop.id;
|
|
71
|
+
commands.push({ label: t('cmd.edit'), value: 'edit', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+e' }, { label: t('cmd.pause'), value: 'pause', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+p' }, { label: t('cmd.play'), value: 'play', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+r' }, { label: t('cmd.stop'), value: 'stop', hint: t('confirm.stopLoop', { name: desc }), tier: COMMAND_TIER_CONFIRM, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+s' }, { label: t('cmd.trigger'), value: 'trigger', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+t' }, { label: t('cmd.clone'), value: 'clone', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+c' }, { label: t('cmd.delete'), value: 'delete', hint: t('confirm.deleteLoop', { name: desc }), tier: COMMAND_TIER_CONFIRM, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+d' }, { label: t('cmd.logs'), value: 'logs', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+o' });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (context.activeTab === 'tasks') {
|
|
75
|
+
commands.push(...taskFilterCommands());
|
|
76
|
+
commands.push({
|
|
77
|
+
label: t('cmd.newTask'), value: 'new-task', hint: '',
|
|
78
|
+
tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_TASK, shortcut: 'ctrl+a+n',
|
|
79
|
+
});
|
|
80
|
+
if (context.selectedTask !== null) {
|
|
81
|
+
const task = context.selectedTask;
|
|
82
|
+
commands.push({ label: t('cmd.editTask'), value: 'edit', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_TASK, shortcut: 'ctrl+a+e' }, { label: t('cmd.deleteTask'), value: 'delete', hint: t('confirm.deleteTask', { id: task.id }), tier: COMMAND_TIER_CONFIRM, category: COMMAND_CATEGORY_TASK, shortcut: 'ctrl+a+d' });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (context.activeTab === 'projects') {
|
|
86
|
+
commands.push(...projectFilterCommands());
|
|
87
|
+
commands.push({
|
|
88
|
+
label: t('cmd.newProject'), value: 'new-project', hint: '',
|
|
89
|
+
tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_PROJECT, shortcut: 'ctrl+a+n',
|
|
90
|
+
});
|
|
91
|
+
if (context.selectedProject !== null) {
|
|
92
|
+
const project = context.selectedProject;
|
|
93
|
+
commands.push({ label: t('cmd.editProject'), value: 'edit', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_PROJECT, shortcut: 'ctrl+a+e' }, { label: t('cmd.deleteProject'), value: 'delete', hint: t('confirm.deleteProject', { name: project.name }), tier: COMMAND_TIER_CONFIRM, category: COMMAND_CATEGORY_PROJECT, shortcut: 'ctrl+a+d' });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return commands;
|
|
97
|
+
}
|
|
98
|
+
// ── Ctrl+P commands browser (shows all available commands for this tab) ─
|
|
99
|
+
export function buildTabCommands(context) {
|
|
100
|
+
const commands = [...globalCommands()];
|
|
101
|
+
if (context.activeTab === 'loops') {
|
|
102
|
+
commands.push(...loopFilterCommands());
|
|
103
|
+
commands.push({ label: t('cmd.newLoop'), value: 'new-loop', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+n' }, { label: t('cmd.edit'), value: 'edit', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+e' }, { label: t('cmd.pause'), value: 'pause', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+p' }, { label: t('cmd.play'), value: 'play', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+r' }, { label: t('cmd.stop'), value: 'stop', hint: '', tier: COMMAND_TIER_CONFIRM, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+s' }, { label: t('cmd.trigger'), value: 'trigger', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+t' }, { label: t('cmd.clone'), value: 'clone', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+c' }, { label: t('cmd.delete'), value: 'delete', hint: '', tier: COMMAND_TIER_CONFIRM, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+d' }, { label: t('cmd.logs'), value: 'logs', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+o' });
|
|
104
|
+
}
|
|
105
|
+
if (context.activeTab === 'tasks') {
|
|
106
|
+
commands.push(...taskFilterCommands());
|
|
107
|
+
commands.push({ label: t('cmd.newTask'), value: 'new-task', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_TASK, shortcut: 'ctrl+a+n' }, { label: t('cmd.editTask'), value: 'edit', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_TASK, shortcut: 'ctrl+a+e' }, { label: t('cmd.deleteTask'), value: 'delete', hint: '', tier: COMMAND_TIER_CONFIRM, category: COMMAND_CATEGORY_TASK, shortcut: 'ctrl+a+d' });
|
|
108
|
+
}
|
|
109
|
+
if (context.activeTab === 'projects') {
|
|
110
|
+
commands.push(...projectFilterCommands());
|
|
111
|
+
commands.push({ label: t('cmd.newProject'), value: 'new-project', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_PROJECT, shortcut: 'ctrl+a+n' }, { label: t('cmd.editProject'), value: 'edit', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_PROJECT, shortcut: 'ctrl+a+e' }, { label: t('cmd.deleteProject'), value: 'delete', hint: '', tier: COMMAND_TIER_CONFIRM, category: COMMAND_CATEGORY_PROJECT, shortcut: 'ctrl+a+d' });
|
|
112
|
+
}
|
|
113
|
+
return commands;
|
|
114
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { darkTheme as theme } from "../theme.js";
|
|
4
|
+
import { t } from "../../i18n/index.js";
|
|
5
|
+
import { FocusableButton } from "./FocusableButton.js";
|
|
6
|
+
const STOP_ACTION = { key: "stop", label: t("board.actionStop") };
|
|
7
|
+
const PAUSE_ACTION = { key: "pause", label: t("board.actionPause") };
|
|
8
|
+
const PLAY_ACTION = { key: "play", label: t("board.actionPlay") };
|
|
9
|
+
const STATUS_ACTIONS = {
|
|
10
|
+
running: [STOP_ACTION],
|
|
11
|
+
waiting: [PAUSE_ACTION, STOP_ACTION],
|
|
12
|
+
paused: [STOP_ACTION, PLAY_ACTION],
|
|
13
|
+
idle: [PLAY_ACTION],
|
|
14
|
+
stopped: [PLAY_ACTION],
|
|
15
|
+
};
|
|
16
|
+
export function getActions(status) {
|
|
17
|
+
const actions = [
|
|
18
|
+
{ key: "edit", label: t("board.actionEdit") },
|
|
19
|
+
{ key: "delete", label: t("board.actionDelete") },
|
|
20
|
+
];
|
|
21
|
+
actions.push(...STATUS_ACTIONS[status]);
|
|
22
|
+
if (status !== "running") {
|
|
23
|
+
actions.push({ key: "clone", label: t("board.actionClone") });
|
|
24
|
+
actions.push({ key: "trigger", label: t("board.actionTrigger") });
|
|
25
|
+
}
|
|
26
|
+
return actions;
|
|
27
|
+
}
|
|
28
|
+
export function getActionKeys(status) {
|
|
29
|
+
return getActions(status).map((a) => a.key);
|
|
30
|
+
}
|
|
31
|
+
export function getActionCount(status) {
|
|
32
|
+
return getActions(status).length;
|
|
33
|
+
}
|
|
34
|
+
export function ActionButtons(props) {
|
|
35
|
+
const { loop, onAction } = props;
|
|
36
|
+
const actions = loop ? getActions(loop.status) : [];
|
|
37
|
+
if (!loop) {
|
|
38
|
+
return (_jsx(Box, { borderStyle: "single", borderColor: theme.border.default, flexDirection: "row", height: 3, flexShrink: 0, backgroundColor: theme.bg.surface, alignItems: "center", children: _jsx(Text, { color: theme.text.muted, children: t("board.noActions") }) }));
|
|
39
|
+
}
|
|
40
|
+
return (_jsx(Box, { borderStyle: "single", borderColor: theme.border.default, flexDirection: "row", flexShrink: 0, backgroundColor: theme.bg.surface, alignItems: "center", paddingX: 1, children: actions.map((action) => {
|
|
41
|
+
const color = action.key === "delete" || action.key === "stop"
|
|
42
|
+
? theme.semantic.danger
|
|
43
|
+
: theme.accent.focus;
|
|
44
|
+
const variant = action.key === "delete" || action.key === "stop"
|
|
45
|
+
? "danger"
|
|
46
|
+
: "default";
|
|
47
|
+
return (_jsx(FocusableButton, { label: action.label, color: color, variant: variant, bordered: false, onPress: () => onAction(action.key) }, action.key));
|
|
48
|
+
}) }));
|
|
49
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { darkTheme as theme } from "../theme.js";
|
|
4
|
+
export function Button(props) {
|
|
5
|
+
const { label, focused, variant = "default" } = props;
|
|
6
|
+
const bgColor = focused
|
|
7
|
+
? variant === "danger"
|
|
8
|
+
? theme.semantic.danger
|
|
9
|
+
: theme.bg.active
|
|
10
|
+
: undefined;
|
|
11
|
+
const fgColor = focused
|
|
12
|
+
? theme.text.inverse
|
|
13
|
+
: variant === "danger"
|
|
14
|
+
? theme.semantic.danger
|
|
15
|
+
: theme.text.primary;
|
|
16
|
+
const indicator = focused ? "› " : " ";
|
|
17
|
+
return (_jsx(Box, { backgroundColor: bgColor, paddingX: 1, marginRight: 1, flexShrink: 0, children: _jsx(Text, { color: fgColor, bold: true, children: indicator + label }) }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo, useEffect } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { darkTheme as theme } from "../theme.js";
|
|
5
|
+
import { listTasks } from "../daemon.js";
|
|
6
|
+
import { commandLine } from "../format.js";
|
|
7
|
+
function buildChains(tasks) {
|
|
8
|
+
const taskMap = new Map();
|
|
9
|
+
for (const task of tasks) {
|
|
10
|
+
taskMap.set(task.id, task);
|
|
11
|
+
}
|
|
12
|
+
const referenced = new Set();
|
|
13
|
+
for (const task of tasks) {
|
|
14
|
+
if (task.onSuccessTaskId)
|
|
15
|
+
referenced.add(task.onSuccessTaskId);
|
|
16
|
+
if (task.onFailureTaskId)
|
|
17
|
+
referenced.add(task.onFailureTaskId);
|
|
18
|
+
}
|
|
19
|
+
const roots = tasks.filter((task) => !referenced.has(task.id));
|
|
20
|
+
const visited = new Set();
|
|
21
|
+
function buildNode(task, branch, depth, isLast) {
|
|
22
|
+
if (visited.has(task.id)) {
|
|
23
|
+
return { task, children: [], branch, depth, isLast };
|
|
24
|
+
}
|
|
25
|
+
visited.add(task.id);
|
|
26
|
+
const children = [];
|
|
27
|
+
if (task.onSuccessTaskId) {
|
|
28
|
+
const child = taskMap.get(task.onSuccessTaskId);
|
|
29
|
+
if (child)
|
|
30
|
+
children.push(buildNode(child, "success", depth + 1, false));
|
|
31
|
+
}
|
|
32
|
+
if (task.onFailureTaskId) {
|
|
33
|
+
const child = taskMap.get(task.onFailureTaskId);
|
|
34
|
+
if (child) {
|
|
35
|
+
if (children.length > 0)
|
|
36
|
+
children[0].isLast = true;
|
|
37
|
+
children.push(buildNode(child, "failure", depth + 1, true));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (children.length > 0)
|
|
41
|
+
children[children.length - 1].isLast = true;
|
|
42
|
+
if (children.length === 0 && branch !== null)
|
|
43
|
+
isLast = true;
|
|
44
|
+
return { task, children, branch, depth, isLast };
|
|
45
|
+
}
|
|
46
|
+
return roots.map((root) => ({
|
|
47
|
+
root: buildNode(root, null, 0, true),
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
function flattenTree(node, result = [], prefix = "") {
|
|
51
|
+
result.push({ node, prefix });
|
|
52
|
+
for (const child of node.children) {
|
|
53
|
+
const childPrefix = prefix + (node.isLast ? " " : "\u2502 ");
|
|
54
|
+
flattenTree(child, result, childPrefix);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
export function ChainEditor(props) {
|
|
59
|
+
const [tasks, setTasks] = useState([]);
|
|
60
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
listTasks().then(setTasks).catch(() => setTasks([]));
|
|
63
|
+
}, []);
|
|
64
|
+
const trees = useMemo(() => buildChains(tasks), [tasks]);
|
|
65
|
+
const flatNodes = useMemo(() => {
|
|
66
|
+
const all = [];
|
|
67
|
+
for (const tree of trees) {
|
|
68
|
+
flattenTree(tree.root, all);
|
|
69
|
+
}
|
|
70
|
+
return all;
|
|
71
|
+
}, [trees]);
|
|
72
|
+
useInput((_input, key) => {
|
|
73
|
+
if (key.escape) {
|
|
74
|
+
props.onClose();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (key.upArrow) {
|
|
78
|
+
setSelectedIndex((i) => i <= 0 ? flatNodes.length - 1 : i - 1);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (key.downArrow) {
|
|
82
|
+
setSelectedIndex((i) => i >= flatNodes.length - 1 ? 0 : i + 1);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (key.return) {
|
|
86
|
+
const node = flatNodes[selectedIndex];
|
|
87
|
+
if (node && props.onEditTask) {
|
|
88
|
+
props.onEditTask(node.node.task);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
if (flatNodes.length === 0) {
|
|
94
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: theme.accent.task, flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: theme.accent.task, bold: true, children: "Chain Editor" }), _jsx(Text, { color: theme.text.muted, children: "No tasks found. Create tasks first to build chains." }), _jsx(Text, { color: theme.text.muted, children: "Press Esc to go back." })] }));
|
|
95
|
+
}
|
|
96
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: theme.accent.task, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: theme.accent.task, bold: true, children: "Chain Editor" }), _jsx(Text, { color: theme.text.muted, children: " - up/down: navigate, enter: edit task, esc: back" })] }), flatNodes.map((item, i) => {
|
|
97
|
+
const isSelected = i === selectedIndex;
|
|
98
|
+
const bg = isSelected ? theme.bg.active : undefined;
|
|
99
|
+
const fg = isSelected ? theme.text.inverse : theme.text.primary;
|
|
100
|
+
const branchLabel = item.node.branch
|
|
101
|
+
? item.node.branch === "success"
|
|
102
|
+
? "[\u2713] "
|
|
103
|
+
: "[\u2717] "
|
|
104
|
+
: "";
|
|
105
|
+
const branchColor = item.node.branch === "success"
|
|
106
|
+
? theme.semantic.success
|
|
107
|
+
: item.node.branch === "failure"
|
|
108
|
+
? theme.semantic.danger
|
|
109
|
+
: theme.text.primary;
|
|
110
|
+
return (_jsxs(Box, { backgroundColor: bg, children: [_jsx(Text, { color: theme.text.muted, children: item.prefix }), _jsx(Text, { color: isSelected ? theme.text.inverse : branchColor, children: branchLabel }), _jsx(Text, { color: fg, children: item.node.task.name }), _jsxs(Text, { color: isSelected ? theme.text.inverse : theme.text.muted, children: [" - ", commandLine(item.node.task.command, item.node.task.commandArgs).slice(0, 40)] })] }, `${item.node.task.id}-${i}`));
|
|
111
|
+
}), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.text.muted, children: [trees.length, " root(s) | ", tasks.length, " total tasks | ", flatNodes.length, " nodes"] }) })] }));
|
|
112
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { darkTheme as theme } from "../theme.js";
|
|
5
|
+
import { t } from "../../i18n/index.js";
|
|
6
|
+
import { copyToClipboard } from "../../shared/clipboard.js";
|
|
7
|
+
import { COMMAND_TEMPLATES } from "../../config/constants.js";
|
|
8
|
+
function hasUnbalancedQuotes(text) {
|
|
9
|
+
let single = 0;
|
|
10
|
+
let double = 0;
|
|
11
|
+
for (const ch of text) {
|
|
12
|
+
if (ch === "'")
|
|
13
|
+
single++;
|
|
14
|
+
if (ch === '"')
|
|
15
|
+
double++;
|
|
16
|
+
}
|
|
17
|
+
return single % 2 !== 0 || double % 2 !== 0;
|
|
18
|
+
}
|
|
19
|
+
function parseParts(command) {
|
|
20
|
+
const trimmed = command.trim();
|
|
21
|
+
if (!trimmed)
|
|
22
|
+
return { executable: "", args: "" };
|
|
23
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
24
|
+
if (firstSpace === -1)
|
|
25
|
+
return { executable: trimmed, args: "" };
|
|
26
|
+
return { executable: trimmed.slice(0, firstSpace), args: trimmed.slice(firstSpace + 1) };
|
|
27
|
+
}
|
|
28
|
+
export function CommandBuilderField(props) {
|
|
29
|
+
const { isActive, onChange, onAdvance } = props;
|
|
30
|
+
const initial = parseParts(props.value);
|
|
31
|
+
const [executable, setExecutable] = useState(initial.executable);
|
|
32
|
+
const [args, setArgs] = useState(initial.args);
|
|
33
|
+
const [activeSub, setActiveSub] = useState(initial.executable ? "args" : "executable");
|
|
34
|
+
const [templateIdx, setTemplateIdx] = useState(0);
|
|
35
|
+
const [validationMsg, setValidationMsg] = useState(null);
|
|
36
|
+
const assembled = [executable, args].filter(Boolean).join(" ");
|
|
37
|
+
const commitValue = useCallback(() => {
|
|
38
|
+
onChange(assembled);
|
|
39
|
+
}, [assembled, onChange]);
|
|
40
|
+
const handleTemplateSelect = useCallback(() => {
|
|
41
|
+
const tmpl = COMMAND_TEMPLATES[templateIdx];
|
|
42
|
+
if (!tmpl)
|
|
43
|
+
return;
|
|
44
|
+
setExecutable(tmpl.command);
|
|
45
|
+
setArgs(tmpl.args);
|
|
46
|
+
setActiveSub("args");
|
|
47
|
+
const next = [tmpl.command, tmpl.args].filter(Boolean).join(" ");
|
|
48
|
+
onChange(next);
|
|
49
|
+
}, [templateIdx, onChange]);
|
|
50
|
+
useInput((input, key) => {
|
|
51
|
+
if (!isActive)
|
|
52
|
+
return;
|
|
53
|
+
if (activeSub === "templates") {
|
|
54
|
+
if (key.upArrow) {
|
|
55
|
+
setTemplateIdx((prev) => (prev > 0 ? prev - 1 : COMMAND_TEMPLATES.length - 1));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (key.downArrow) {
|
|
59
|
+
setTemplateIdx((prev) => (prev < COMMAND_TEMPLATES.length - 1 ? prev + 1 : 0));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (key.return) {
|
|
63
|
+
handleTemplateSelect();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (key.escape) {
|
|
67
|
+
setActiveSub("executable");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (key.ctrl && input === "y") {
|
|
73
|
+
copyToClipboard(assembled);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (key.escape) {
|
|
77
|
+
commitValue();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (activeSub === "executable") {
|
|
81
|
+
if (key.return || key.tab) {
|
|
82
|
+
if (!executable.trim()) {
|
|
83
|
+
setValidationMsg(t("errors.commandEmpty"));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
setValidationMsg(null);
|
|
87
|
+
setActiveSub("args");
|
|
88
|
+
commitValue();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (key.delete || key.backspace) {
|
|
92
|
+
const next = executable.slice(0, -1);
|
|
93
|
+
setExecutable(next);
|
|
94
|
+
if (next.trim())
|
|
95
|
+
setValidationMsg(null);
|
|
96
|
+
onChange([next, args].filter(Boolean).join(" "));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (input.length === 1 && input >= " " && input <= "~") {
|
|
100
|
+
const next = executable + input;
|
|
101
|
+
setExecutable(next);
|
|
102
|
+
if (next.trim())
|
|
103
|
+
setValidationMsg(null);
|
|
104
|
+
if (!next.trim())
|
|
105
|
+
setActiveSub("templates");
|
|
106
|
+
onChange([next, args].filter(Boolean).join(" "));
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (activeSub === "args") {
|
|
111
|
+
if (key.return) {
|
|
112
|
+
if (hasUnbalancedQuotes(args)) {
|
|
113
|
+
setValidationMsg(t("errors.unbalancedQuote"));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
setValidationMsg(null);
|
|
117
|
+
commitValue();
|
|
118
|
+
onAdvance();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (key.delete || key.backspace) {
|
|
122
|
+
const next = args.slice(0, -1);
|
|
123
|
+
setArgs(next);
|
|
124
|
+
onChange([executable, next].filter(Boolean).join(" "));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (input.length === 1 && input >= " " && input <= "~") {
|
|
128
|
+
const next = args + input;
|
|
129
|
+
setArgs(next);
|
|
130
|
+
onChange([executable, next].filter(Boolean).join(" "));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}, { isActive });
|
|
134
|
+
const execBorderColor = isActive && activeSub === "executable" ? theme.accent.brand : theme.border.dim;
|
|
135
|
+
const execBgColor = isActive && activeSub === "executable" ? theme.bg.input : undefined;
|
|
136
|
+
const argsBorderColor = isActive && activeSub === "args" ? theme.accent.brand : theme.border.dim;
|
|
137
|
+
const argsBgColor = isActive && activeSub === "args" ? theme.bg.input : undefined;
|
|
138
|
+
const showTemplates = isActive && activeSub === "executable" && !executable.trim();
|
|
139
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Text, { color: theme.text.muted, children: t("commandBuilder.executableLabel") }), _jsxs(Box, { borderStyle: "single", borderColor: execBorderColor, backgroundColor: execBgColor, paddingLeft: 1, children: [_jsx(Text, { color: executable ? theme.text.primary : theme.text.muted, children: executable || t("commandBuilder.executableHint") }), isActive && activeSub === "executable" ? _jsx(Text, { inverse: true, children: " " }) : null] })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 2, children: [_jsx(Text, { color: theme.text.muted, children: t("commandBuilder.argsLabel") }), _jsxs(Box, { borderStyle: "single", borderColor: argsBorderColor, backgroundColor: argsBgColor, paddingLeft: 1, children: [_jsx(Text, { color: args ? theme.text.primary : theme.text.muted, children: args || t("commandBuilder.argsHint") }), isActive && activeSub === "args" ? _jsx(Text, { inverse: true, children: " " }) : null] })] })] }), assembled ? (_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.text.muted, children: [t("commandBuilder.previewLabel"), " "] }), _jsx(Text, { color: theme.accent.task, bold: true, children: assembled })] })) : null, isActive ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.text.muted, children: [t("commandBuilder.copyHint"), " . ", t("commandBuilder.navHint")] }) })) : null, validationMsg ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.semantic.danger, children: validationMsg }) })) : null, showTemplates ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.text.muted, children: t("commandBuilder.templatesLabel") }), COMMAND_TEMPLATES.map((tmpl, i) => {
|
|
140
|
+
const isSel = i === templateIdx;
|
|
141
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isSel ? theme.accent.brand : theme.text.muted, children: isSel ? "\u276F " : " " }), _jsx(Text, { color: isSel ? theme.accent.brand : theme.text.secondary, children: tmpl.label }), _jsxs(Text, { color: theme.text.muted, children: [" \u2014 ", tmpl.command, " ", tmpl.args] })] }, tmpl.label));
|
|
142
|
+
})] })) : null] }));
|
|
143
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { darkTheme as theme } from "../theme.js";
|
|
5
|
+
import { t } from "../../i18n/index.js";
|
|
6
|
+
const MAX_VISIBLE_LINES = 14;
|
|
7
|
+
export function CommandEditorModal(props) {
|
|
8
|
+
const [lines, setLines] = useState(() => {
|
|
9
|
+
const initial = props.initial.trim();
|
|
10
|
+
if (!initial)
|
|
11
|
+
return [""];
|
|
12
|
+
return initial.split(/\r?\n/);
|
|
13
|
+
});
|
|
14
|
+
const [cursorRow, setCursorRow] = useState(0);
|
|
15
|
+
const [cursorCol, setCursorCol] = useState(0);
|
|
16
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
17
|
+
const currentLine = lines[cursorRow] ?? "";
|
|
18
|
+
const commit = useCallback(() => {
|
|
19
|
+
const value = lines.join("\n").trim();
|
|
20
|
+
props.onSelect(value);
|
|
21
|
+
}, [lines, props]);
|
|
22
|
+
const insertChar = useCallback((ch) => {
|
|
23
|
+
setLines((prev) => {
|
|
24
|
+
const next = [...prev];
|
|
25
|
+
const line = next[cursorRow] ?? "";
|
|
26
|
+
next[cursorRow] = line.slice(0, cursorCol) + ch + line.slice(cursorCol);
|
|
27
|
+
return next;
|
|
28
|
+
});
|
|
29
|
+
setCursorCol((c) => c + 1);
|
|
30
|
+
}, [cursorRow, cursorCol]);
|
|
31
|
+
const handleBackspace = useCallback(() => {
|
|
32
|
+
if (cursorCol > 0) {
|
|
33
|
+
setLines((prev) => {
|
|
34
|
+
const next = [...prev];
|
|
35
|
+
const line = next[cursorRow] ?? "";
|
|
36
|
+
next[cursorRow] = line.slice(0, cursorCol - 1) + line.slice(cursorCol);
|
|
37
|
+
return next;
|
|
38
|
+
});
|
|
39
|
+
setCursorCol((c) => c - 1);
|
|
40
|
+
}
|
|
41
|
+
else if (cursorRow > 0) {
|
|
42
|
+
const prevLine = lines[cursorRow - 1] ?? "";
|
|
43
|
+
const merged = prevLine + currentLine;
|
|
44
|
+
setLines((prev) => {
|
|
45
|
+
const next = [...prev];
|
|
46
|
+
next[cursorRow - 1] = merged;
|
|
47
|
+
next.splice(cursorRow, 1);
|
|
48
|
+
return next;
|
|
49
|
+
});
|
|
50
|
+
setCursorRow((r) => r - 1);
|
|
51
|
+
setCursorCol(prevLine.length);
|
|
52
|
+
}
|
|
53
|
+
}, [cursorCol, cursorRow, currentLine, lines]);
|
|
54
|
+
const handleEnter = useCallback(() => {
|
|
55
|
+
setLines((prev) => {
|
|
56
|
+
const next = [...prev];
|
|
57
|
+
const line = next[cursorRow] ?? "";
|
|
58
|
+
next.splice(cursorRow, 1, line.slice(0, cursorCol), line.slice(cursorCol));
|
|
59
|
+
return next;
|
|
60
|
+
});
|
|
61
|
+
setCursorRow((r) => r + 1);
|
|
62
|
+
setCursorCol(0);
|
|
63
|
+
}, [cursorRow, cursorCol]);
|
|
64
|
+
const moveUp = useCallback(() => {
|
|
65
|
+
if (cursorRow > 0) {
|
|
66
|
+
setCursorRow((r) => r - 1);
|
|
67
|
+
setCursorCol((c) => Math.min(c, (lines[cursorRow - 1] ?? "").length));
|
|
68
|
+
if (cursorRow - 1 < scrollOffset) {
|
|
69
|
+
setScrollOffset((s) => Math.max(0, s - 1));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}, [cursorRow, cursorCol, lines, scrollOffset]);
|
|
73
|
+
const moveDown = useCallback(() => {
|
|
74
|
+
if (cursorRow < lines.length - 1) {
|
|
75
|
+
setCursorRow((r) => r + 1);
|
|
76
|
+
setCursorCol((c) => Math.min(c, (lines[cursorRow + 1] ?? "").length));
|
|
77
|
+
if (cursorRow + 1 >= scrollOffset + MAX_VISIBLE_LINES) {
|
|
78
|
+
setScrollOffset((s) => s + 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}, [cursorRow, cursorCol, lines, scrollOffset]);
|
|
82
|
+
const moveLeft = useCallback(() => {
|
|
83
|
+
if (cursorCol > 0) {
|
|
84
|
+
setCursorCol((c) => c - 1);
|
|
85
|
+
}
|
|
86
|
+
else if (cursorRow > 0) {
|
|
87
|
+
const prevLine = lines[cursorRow - 1] ?? "";
|
|
88
|
+
setCursorRow((r) => r - 1);
|
|
89
|
+
setCursorCol(prevLine.length);
|
|
90
|
+
}
|
|
91
|
+
}, [cursorCol, cursorRow, lines]);
|
|
92
|
+
const moveRight = useCallback(() => {
|
|
93
|
+
if (cursorCol < currentLine.length) {
|
|
94
|
+
setCursorCol((c) => c + 1);
|
|
95
|
+
}
|
|
96
|
+
else if (cursorRow < lines.length - 1) {
|
|
97
|
+
setCursorRow((r) => r + 1);
|
|
98
|
+
setCursorCol(0);
|
|
99
|
+
}
|
|
100
|
+
}, [cursorCol, cursorRow, currentLine, lines]);
|
|
101
|
+
useInput((input, key) => {
|
|
102
|
+
if (key.ctrl) {
|
|
103
|
+
if (input === "s") {
|
|
104
|
+
commit();
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (key.escape) {
|
|
109
|
+
props.onClose();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (key.return) {
|
|
113
|
+
handleEnter();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (key.upArrow) {
|
|
117
|
+
moveUp();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (key.downArrow) {
|
|
121
|
+
moveDown();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (key.leftArrow) {
|
|
125
|
+
moveLeft();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (key.rightArrow) {
|
|
129
|
+
moveRight();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (key.backspace || key.delete) {
|
|
133
|
+
handleBackspace();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (input.length === 1 && input >= " " && input <= "~") {
|
|
137
|
+
insertChar(input);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
const totalLines = lines.length;
|
|
141
|
+
const visibleStart = scrollOffset;
|
|
142
|
+
const visibleEnd = Math.min(totalLines, visibleStart + MAX_VISIBLE_LINES);
|
|
143
|
+
const visibleLines = lines.slice(visibleStart, visibleEnd);
|
|
144
|
+
const lineNumWidth = String(totalLines).length;
|
|
145
|
+
function renderLine(lineText, rowIdx) {
|
|
146
|
+
const isCursor = rowIdx === cursorRow;
|
|
147
|
+
const displayRow = rowIdx + 1;
|
|
148
|
+
const prefix = String(displayRow).padStart(lineNumWidth, " ");
|
|
149
|
+
const lineNumColor = isCursor ? theme.accent.brand : theme.text.muted;
|
|
150
|
+
if (isCursor) {
|
|
151
|
+
const before = lineText.slice(0, cursorCol);
|
|
152
|
+
const char = cursorCol < lineText.length ? lineText[cursorCol] : " ";
|
|
153
|
+
const after = cursorCol < lineText.length ? lineText.slice(cursorCol + 1) : "";
|
|
154
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: lineNumColor, children: [prefix, " "] }), _jsx(Text, { color: theme.text.primary, children: before }), _jsx(Text, { inverse: true, children: char }), _jsx(Text, { color: theme.text.primary, children: after })] }, rowIdx));
|
|
155
|
+
}
|
|
156
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: lineNumColor, children: [prefix, " "] }), _jsx(Text, { color: theme.text.secondary, children: lineText })] }, rowIdx));
|
|
157
|
+
}
|
|
158
|
+
return (_jsx(Box, { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", justifyContent: "center", alignItems: "center", children: _jsxs(Box, { width: 70, flexDirection: "column", backgroundColor: theme.bg.elevated, borderStyle: "round", borderColor: theme.accent.brand, paddingX: 1, paddingY: 0, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: theme.accent.brand, bold: true, children: t("cmdEditor.title") }), _jsxs(Text, { color: theme.text.muted, children: [totalLines, " ", t("cmdEditor.lines")] })] }), _jsx(Box, { flexDirection: "column", marginTop: 0, children: visibleLines.map((line, visIdx) => renderLine(line, visibleStart + visIdx)) }), visibleEnd < totalLines ? (_jsx(Text, { color: theme.text.muted, children: "\u2193 " + (totalLines - visibleEnd) + " more" })) : null, _jsxs(Box, { marginTop: 1, justifyContent: "space-between", children: [_jsx(Text, { color: theme.text.muted, children: t("cmdEditor.hint") }), _jsxs(Text, { color: theme.text.muted, children: [visibleEnd, "/", totalLines] })] })] }) }));
|
|
159
|
+
}
|