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.
Files changed (105) hide show
  1. package/README.md +127 -21
  2. package/dist/board/App.js +1 -1
  3. package/dist/board/components/ActionButtons.js +1 -1
  4. package/dist/board/components/BoardButton.js +1 -1
  5. package/dist/board/components/ConfirmModal.js +1 -1
  6. package/dist/board/components/ContextHelpModal.js +1 -1
  7. package/dist/board/components/CreateForm.js +1 -1
  8. package/dist/board/components/CreateProjectModal.js +1 -1
  9. package/dist/board/components/DeleteProjectConfirm.js +1 -1
  10. package/dist/board/components/DetailView.js +1 -1
  11. package/dist/board/components/EditProjectModal.js +1 -1
  12. package/dist/board/components/FilterBar.js +1 -1
  13. package/dist/board/components/Footer.js +1 -1
  14. package/dist/board/components/Header.js +1 -1
  15. package/dist/board/components/HelpModal.js +1 -1
  16. package/dist/board/components/Inspector.js +1 -1
  17. package/dist/board/components/LogModal.js +1 -1
  18. package/dist/board/components/Navigator.js +1 -1
  19. package/dist/board/components/ProjectsModal.js +1 -1
  20. package/dist/board/components/ProjectsPage.js +1 -1
  21. package/dist/board/components/RunHistory.js +1 -1
  22. package/dist/board/components/SearchBox.js +1 -1
  23. package/dist/board/components/SearchSelect.js +1 -1
  24. package/dist/board/components/TaskBrowser.js +1 -1
  25. package/dist/board/components/TaskFilterBar.js +1 -1
  26. package/dist/board/components/TaskForm.js +1 -1
  27. package/dist/board/components/Timeline.js +1 -1
  28. package/dist/board/focus-context.js +1 -1
  29. package/dist/board/format.js +15 -6
  30. package/dist/board/index.js +1 -1
  31. package/dist/board/toast.js +1 -1
  32. package/dist/cli/import-validator.js +151 -0
  33. package/dist/cli/import-writer.js +77 -0
  34. package/dist/cli.js +111 -17
  35. package/dist/config/constants.js +52 -0
  36. package/dist/daemon/file-watcher.js +171 -0
  37. package/dist/daemon/http-server.js +605 -0
  38. package/dist/daemon/index.js +36 -6
  39. package/dist/daemon/manager.js +94 -13
  40. package/dist/daemon/projects.js +10 -0
  41. package/dist/daemon/server.js +21 -0
  42. package/dist/daemon/task-manager.js +7 -0
  43. package/dist/esm-loader.js +4 -1
  44. package/dist/hooks/useLoopFormValidation.js +140 -0
  45. package/dist/i18n/en.json +237 -17
  46. package/dist/shared/fs-utils.js +34 -1
  47. package/dist/tui/app.js +675 -151
  48. package/dist/tui/commands.js +114 -0
  49. package/dist/tui/components/ActionButtons.js +49 -0
  50. package/dist/tui/components/Button.js +18 -0
  51. package/dist/tui/components/ChainEditor.js +112 -0
  52. package/dist/tui/components/CommandBuilderField.js +143 -0
  53. package/dist/tui/components/CommandEditorModal.js +159 -0
  54. package/dist/tui/components/CommandInput.js +303 -0
  55. package/dist/tui/components/CommandsBrowserModal.js +77 -0
  56. package/dist/tui/components/ConfirmModal.js +9 -0
  57. package/dist/tui/components/ContextHelpModal.js +8 -0
  58. package/dist/tui/components/CreateForm.js +223 -0
  59. package/dist/tui/components/DebugPanel.js +8 -0
  60. package/dist/tui/components/ExportModal.js +37 -0
  61. package/dist/tui/components/FilterBar.js +13 -0
  62. package/dist/tui/components/FocusableButton.js +16 -0
  63. package/dist/tui/components/FocusableInput.js +69 -0
  64. package/dist/tui/components/FocusableList.js +46 -0
  65. package/dist/tui/components/FocusableSearchSelect.js +82 -0
  66. package/dist/tui/components/Footer.js +42 -0
  67. package/dist/tui/components/Header.js +40 -0
  68. package/dist/tui/components/HelpGuideModal.js +21 -0
  69. package/dist/tui/components/HelpModal.js +59 -0
  70. package/dist/tui/components/InlineCommandEditor.js +143 -0
  71. package/dist/tui/components/Inspector.js +31 -0
  72. package/dist/tui/components/LeftPanel.js +33 -0
  73. package/dist/tui/components/LogModal.js +121 -0
  74. package/dist/tui/components/Modal.js +11 -0
  75. package/dist/tui/components/Navigator.js +68 -0
  76. package/dist/tui/components/PatchEditForm.js +78 -0
  77. package/dist/tui/components/ProjectForm.js +64 -0
  78. package/dist/tui/components/ProjectsModal.js +40 -0
  79. package/dist/tui/components/ProjectsPage.js +99 -0
  80. package/dist/tui/components/RightPanel.js +34 -0
  81. package/dist/tui/components/RunHistory.js +108 -0
  82. package/dist/tui/components/SearchSelect.js +67 -0
  83. package/dist/tui/components/SelectModal.js +76 -0
  84. package/dist/tui/components/TabBar.js +20 -0
  85. package/dist/tui/components/TaskBrowser.js +96 -0
  86. package/dist/tui/components/TaskFilterBar.js +9 -0
  87. package/dist/tui/components/TaskForm.js +134 -0
  88. package/dist/tui/components/TaskPickerModal.js +60 -0
  89. package/dist/tui/components/Toast.js +37 -0
  90. package/dist/tui/components/WelcomeScreen.js +70 -0
  91. package/dist/tui/components/WizardForm.js +178 -0
  92. package/dist/tui/daemon.js +142 -0
  93. package/dist/tui/format.js +111 -0
  94. package/dist/tui/hooks/useBreakpoint.js +7 -0
  95. package/dist/tui/hooks/useHoverState.js +11 -0
  96. package/dist/tui/hooks/useLogStream.js +32 -0
  97. package/dist/tui/hooks/useLoopPolling.js +28 -0
  98. package/dist/tui/index.js +24 -0
  99. package/dist/tui/router.js +16 -0
  100. package/dist/tui/state.js +175 -0
  101. package/dist/tui/theme.js +96 -0
  102. package/dist/tui/types.js +1 -0
  103. package/dist/tui/utils/paste.js +10 -0
  104. package/dist/tui/utils/validation.js +56 -0
  105. 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
+ }