loop-task 2.0.2 → 2.0.4

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 (131) hide show
  1. package/dist/config/constants.js +10 -0
  2. package/dist/i18n/en.json +12 -0
  3. package/dist/loop-config.js +59 -0
  4. package/dist/shared/useUndoRedo.js +79 -0
  5. package/dist/tui/components/CodeEditorModal.js +245 -0
  6. package/dist/tui/components/CodeEditorPreview.js +14 -0
  7. package/dist/tui/components/CreateForm.js +12 -10
  8. package/dist/tui/components/TaskForm.js +12 -12
  9. package/dist/tui/utils/syntax.js +128 -0
  10. package/package.json +2 -2
  11. package/dist/board/App.js +0 -405
  12. package/dist/board/board-log.js +0 -18
  13. package/dist/board/components/ActionButtons.js +0 -46
  14. package/dist/board/components/BoardButton.js +0 -58
  15. package/dist/board/components/ConfirmModal.js +0 -36
  16. package/dist/board/components/ContextHelpModal.js +0 -22
  17. package/dist/board/components/CreateForm.js +0 -271
  18. package/dist/board/components/CreateProjectModal.js +0 -97
  19. package/dist/board/components/DeleteProjectConfirm.js +0 -53
  20. package/dist/board/components/DetailView.js +0 -25
  21. package/dist/board/components/EditProjectModal.js +0 -99
  22. package/dist/board/components/FilterBar.js +0 -17
  23. package/dist/board/components/Footer.js +0 -52
  24. package/dist/board/components/Header.js +0 -46
  25. package/dist/board/components/HelpModal.js +0 -65
  26. package/dist/board/components/Inspector.js +0 -12
  27. package/dist/board/components/LogModal.js +0 -82
  28. package/dist/board/components/Navigator.js +0 -84
  29. package/dist/board/components/ProjectsModal.js +0 -70
  30. package/dist/board/components/ProjectsPage.js +0 -134
  31. package/dist/board/components/RunHistory.js +0 -83
  32. package/dist/board/components/SearchBox.js +0 -10
  33. package/dist/board/components/SearchSelect.js +0 -132
  34. package/dist/board/components/TaskBrowser.js +0 -95
  35. package/dist/board/components/TaskFilterBar.js +0 -6
  36. package/dist/board/components/TaskForm.js +0 -155
  37. package/dist/board/components/Timeline.js +0 -5
  38. package/dist/board/daemon.js +0 -117
  39. package/dist/board/focus-context.js +0 -98
  40. package/dist/board/hooks/useBoardKeybindings.js +0 -344
  41. package/dist/board/hooks/useBreakpoint.js +0 -6
  42. package/dist/board/hooks/useHoverState.js +0 -11
  43. package/dist/board/hooks/useInputShortcuts.js +0 -61
  44. package/dist/board/hooks/useLogStream.js +0 -32
  45. package/dist/board/hooks/useLoopPolling.js +0 -28
  46. package/dist/board/hooks/useTabNav.js +0 -60
  47. package/dist/board/hooks/useTaskKeybindings.js +0 -120
  48. package/dist/board/index.js +0 -32
  49. package/dist/board/router.js +0 -16
  50. package/dist/board/state.js +0 -99
  51. package/dist/board/toast.js +0 -65
  52. package/dist/board/types.js +0 -1
  53. package/dist/cli.d.ts +0 -3
  54. package/dist/cli.d.ts.map +0 -1
  55. package/dist/cli.js.map +0 -1
  56. package/dist/client/commands.d.ts +0 -11
  57. package/dist/client/commands.d.ts.map +0 -1
  58. package/dist/client/commands.js.map +0 -1
  59. package/dist/client/ipc.d.ts +0 -5
  60. package/dist/client/ipc.d.ts.map +0 -1
  61. package/dist/client/ipc.js.map +0 -1
  62. package/dist/daemon/index.d.ts +0 -2
  63. package/dist/daemon/index.d.ts.map +0 -1
  64. package/dist/daemon/index.js.map +0 -1
  65. package/dist/daemon/manager.d.ts +0 -17
  66. package/dist/daemon/manager.d.ts.map +0 -1
  67. package/dist/daemon/manager.js.map +0 -1
  68. package/dist/daemon/server.d.ts +0 -15
  69. package/dist/daemon/server.d.ts.map +0 -1
  70. package/dist/daemon/server.js.map +0 -1
  71. package/dist/daemon/spawner.d.ts +0 -3
  72. package/dist/daemon/spawner.d.ts.map +0 -1
  73. package/dist/daemon/spawner.js.map +0 -1
  74. package/dist/daemon/state.d.ts +0 -15
  75. package/dist/daemon/state.d.ts.map +0 -1
  76. package/dist/daemon/state.js.map +0 -1
  77. package/dist/duration.d.ts +0 -3
  78. package/dist/duration.d.ts.map +0 -1
  79. package/dist/duration.js.map +0 -1
  80. package/dist/logger.d.ts +0 -11
  81. package/dist/logger.d.ts.map +0 -1
  82. package/dist/logger.js.map +0 -1
  83. package/dist/loop-config.d.ts +0 -13
  84. package/dist/loop-config.d.ts.map +0 -1
  85. package/dist/loop-config.js.map +0 -1
  86. package/dist/loop.d.ts +0 -46
  87. package/dist/loop.d.ts.map +0 -1
  88. package/dist/loop.js +0 -363
  89. package/dist/loop.js.map +0 -1
  90. package/dist/open-board.d.ts +0 -2
  91. package/dist/open-board.d.ts.map +0 -1
  92. package/dist/open-board.js +0 -19
  93. package/dist/open-board.js.map +0 -1
  94. package/dist/tui/app.d.ts +0 -57
  95. package/dist/tui/app.d.ts.map +0 -1
  96. package/dist/tui/app.js.map +0 -1
  97. package/dist/tui/components/ActionButtons.js +0 -49
  98. package/dist/tui/components/CommandBuilderField.js +0 -143
  99. package/dist/tui/components/CommandEditorModal.js +0 -159
  100. package/dist/tui/components/ConfirmModal.js +0 -9
  101. package/dist/tui/components/FilterBar.js +0 -13
  102. package/dist/tui/components/FocusableSearchSelect.js +0 -82
  103. package/dist/tui/components/Footer.js +0 -42
  104. package/dist/tui/components/InlineCommandEditor.js +0 -143
  105. package/dist/tui/components/PatchEditForm.js +0 -78
  106. package/dist/tui/components/SearchSelect.js +0 -67
  107. package/dist/tui/components/TaskFilterBar.js +0 -9
  108. package/dist/tui/create-loop.d.ts +0 -3
  109. package/dist/tui/create-loop.d.ts.map +0 -1
  110. package/dist/tui/create-loop.js +0 -90
  111. package/dist/tui/create-loop.js.map +0 -1
  112. package/dist/tui/dashboard.d.ts +0 -2
  113. package/dist/tui/dashboard.d.ts.map +0 -1
  114. package/dist/tui/dashboard.js +0 -45
  115. package/dist/tui/dashboard.js.map +0 -1
  116. package/dist/tui/events.d.ts +0 -3
  117. package/dist/tui/events.d.ts.map +0 -1
  118. package/dist/tui/events.js +0 -244
  119. package/dist/tui/events.js.map +0 -1
  120. package/dist/tui/popup.d.ts +0 -3
  121. package/dist/tui/popup.d.ts.map +0 -1
  122. package/dist/tui/popup.js +0 -58
  123. package/dist/tui/popup.js.map +0 -1
  124. package/dist/tui/render.d.ts +0 -9
  125. package/dist/tui/render.d.ts.map +0 -1
  126. package/dist/tui/render.js +0 -305
  127. package/dist/tui/render.js.map +0 -1
  128. package/dist/types.d.ts +0 -94
  129. package/dist/types.d.ts.map +0 -1
  130. package/dist/types.js.map +0 -1
  131. /package/dist/tui/{app.js → App.js} +0 -0
@@ -1,49 +0,0 @@
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
- }
@@ -1,143 +0,0 @@
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
- }
@@ -1,159 +0,0 @@
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
- }
@@ -1,9 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box } from "ink";
3
- import { darkTheme as theme } from "../theme.js";
4
- import { Modal } from "./Modal.js";
5
- import { FocusableButton } from "./FocusableButton.js";
6
- import { t } from "../../i18n/index.js";
7
- export function ConfirmModal(props) {
8
- return (_jsx(Modal, { title: props.message, onClose: props.onNo, width: "50%", children: _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(FocusableButton, { label: t("board.yes"), color: theme.accent.focus, onPress: props.onYes }), _jsx(FocusableButton, { label: t("board.no"), color: theme.semantic.danger, onPress: props.onNo, variant: "danger" })] }) }));
9
- }
@@ -1,13 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } 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
- import { FocusableInput } from "./FocusableInput.js";
7
- export function FilterBar(props) {
8
- const { filters, sort, onStatusCycle, onSortCycle, onSelectProject, currentProjectName, onQueryChange, onSearchDismiss, } = props;
9
- const statusLabel = `${t("board.statusFilterTitle")} ${filters.status}`;
10
- const sortLabel = `${t("board.sortTitle")} ${sort}`;
11
- const projectLabel = currentProjectName ?? t("project.showAll");
12
- return (_jsxs(Box, { flexDirection: "column", height: 4, children: [_jsxs(Box, { children: [_jsx(Box, { width: currentProjectName ? 36 : 48, children: _jsx(FocusableInput, { value: filters.query, onChange: onQueryChange, onSubmit: onSearchDismiss, placeholder: t("board.searchPlaceholder") }) }), onSelectProject ? (_jsx(Box, { marginLeft: 1, children: _jsx(FocusableButton, { label: projectLabel, color: theme.accent.project, onPress: onSelectProject }) })) : null, _jsx(Box, { marginLeft: 1, children: _jsx(FocusableButton, { label: statusLabel, color: theme.accent.focus, onPress: onStatusCycle }) }), _jsx(Box, { marginLeft: 1, children: _jsx(FocusableButton, { label: sortLabel, color: theme.accent.focus, onPress: onSortCycle }) })] }), _jsx(Box, { children: _jsx(Text, { color: theme.text.muted, children: t("board.searchEmpty") }) })] }));
13
- }
@@ -1,82 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useMemo } from "react";
3
- import { Box, Text, useFocus, useInput } from "ink";
4
- import { darkTheme as theme } from "../theme.js";
5
- const MAX_VISIBLE = 6;
6
- export function FocusableSearchSelect(props) {
7
- const { options, value, onChange } = props;
8
- const placeholder = props.placeholder ?? "type to filter...";
9
- const { isFocused } = useFocus();
10
- const [filter, setFilter] = useState("");
11
- const [selectedIndex, setSelectedIndex] = useState(0);
12
- const filtered = useMemo(() => {
13
- if (!filter)
14
- return options;
15
- const q = filter.toLowerCase();
16
- return options.filter((o) => o.name.toLowerCase().includes(q) ||
17
- o.value.toLowerCase().includes(q));
18
- }, [options, filter]);
19
- const clampedIndex = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
20
- const visibleStart = clampedIndex < MAX_VISIBLE
21
- ? 0
22
- : clampedIndex - MAX_VISIBLE + 1;
23
- const visible = filtered.slice(visibleStart, visibleStart + MAX_VISIBLE);
24
- useInput((input, key) => {
25
- if (key.upArrow) {
26
- setSelectedIndex((prev) => prev <= 0 ? filtered.length - 1 : prev - 1);
27
- return;
28
- }
29
- if (key.downArrow) {
30
- setSelectedIndex((prev) => prev >= filtered.length - 1 ? 0 : prev + 1);
31
- return;
32
- }
33
- if (key.return) {
34
- const option = filtered[clampedIndex];
35
- if (option) {
36
- onChange(option.value);
37
- }
38
- return;
39
- }
40
- if (key.escape) {
41
- setFilter("");
42
- setSelectedIndex(0);
43
- return;
44
- }
45
- if (key.backspace || key.delete) {
46
- setFilter((f) => f.slice(0, -1));
47
- setSelectedIndex(0);
48
- return;
49
- }
50
- if (input &&
51
- !key.ctrl &&
52
- !key.meta &&
53
- input.length === 1 &&
54
- /[a-z0-9 _\-./]/i.test(input)) {
55
- setFilter((f) => f + input);
56
- setSelectedIndex(0);
57
- return;
58
- }
59
- }, { isActive: isFocused });
60
- const borderColor = isFocused ? theme.accent.focus : theme.border.default;
61
- const backgroundColor = isFocused
62
- ? theme.bg.input
63
- : theme.bg.surface;
64
- const selectedOption = options.find((o) => o.value === value);
65
- const displayLabel = selectedOption?.name ?? placeholder;
66
- if (!isFocused) {
67
- return (_jsx(Box, { borderStyle: "single", borderColor: theme.border.dim, paddingLeft: 1, children: _jsx(Text, { color: value ? theme.text.primary : theme.text.muted, children: displayLabel }) }));
68
- }
69
- return (_jsxs(Box, { borderStyle: "single", borderColor: borderColor, backgroundColor: backgroundColor, flexDirection: "column", children: [_jsxs(Box, { paddingLeft: 1, children: [_jsx(Text, { color: theme.text.muted, children: "/ " }), _jsx(Text, { color: filter ? theme.text.primary : theme.text.muted, children: filter || placeholder })] }), filtered.length > 0 ? (_jsx(Box, { flexDirection: "column", children: visible.map((option, i) => {
70
- const realIndex = visibleStart + i;
71
- const isSelected = realIndex === clampedIndex;
72
- const isActive = option.value === value;
73
- const prefix = isSelected ? "\u203a " : " ";
74
- const bg = isSelected ? theme.bg.active : undefined;
75
- const fg = isSelected
76
- ? theme.text.inverse
77
- : isActive
78
- ? theme.accent.focus
79
- : option.color ?? theme.text.secondary;
80
- return (_jsx(Box, { backgroundColor: bg, children: _jsxs(Text, { color: fg, children: [prefix, option.name] }) }, option.value));
81
- }) })) : null] }));
82
- }
@@ -1,42 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { Box, Text } from "ink";
3
- import { darkTheme as theme } from "../theme.js";
4
- function modeBadgeColor(mode) {
5
- switch (mode) {
6
- case "normal": return "#38bdf8";
7
- case "search": return "#38bdf8";
8
- case "create": return "#38bdf8";
9
- case "task": return "#a78bfa";
10
- case "help": return "#38bdf8";
11
- case "confirm": return "#f87171";
12
- case "projects": return "#34d399";
13
- }
14
- }
15
- function modeLabelText(mode) {
16
- switch (mode) {
17
- case "normal": return "Loops";
18
- case "search": return "Search";
19
- case "create": return "Create";
20
- case "task": return "Tasks";
21
- case "help": return "Help";
22
- case "confirm": return "Confirm";
23
- case "projects": return "Projects";
24
- }
25
- }
26
- function modeHints(mode) {
27
- switch (mode) {
28
- case "normal": return "Tab:panel /:search h:help Ctrl+C:quit";
29
- case "search": return "Enter:apply Esc:cancel";
30
- case "create": return "Tab:next Enter:save Esc:cancel";
31
- case "task": return "Tab:next Enter:save Esc:cancel";
32
- case "help": return "Esc:close";
33
- case "confirm": return "arrows:choose Enter:confirm y/n Esc:cancel";
34
- case "projects": return "Tab:navigate n:new e:edit d:delete Esc:back";
35
- }
36
- }
37
- export function Footer(props) {
38
- const badgeColor = modeBadgeColor(props.mode);
39
- const label = modeLabelText(props.mode);
40
- const hints = modeHints(props.mode);
41
- return (_jsxs(Box, { height: 1, justifyContent: "space-between", children: [_jsx(Box, { children: _jsxs(Text, { backgroundColor: badgeColor, color: theme.bg.base, bold: true, children: [" ", label, " "] }) }), _jsx(Box, { children: _jsx(Text, { color: theme.text.muted, children: hints }) })] }));
42
- }
@@ -1,143 +0,0 @@
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 { sanitizePaste } from "../utils/paste.js";
8
- const MAX_VISIBLE = 8;
9
- export function InlineCommandEditor({ value, hint, isActive, onChange, }) {
10
- const [lines, setLines] = useState(() => value ? value.split("\n") : [""]);
11
- const [cursorRow, setCursorRow] = useState(0);
12
- const [cursorCol, setCursorCol] = useState(0);
13
- const emit = useCallback((nextLines) => {
14
- onChange(nextLines.join("\n"));
15
- }, [onChange]);
16
- useInput((input, key) => {
17
- // Ctrl+Y: copy full command text
18
- if (key.ctrl && input === "y") {
19
- copyToClipboard(value);
20
- return;
21
- }
22
- // Let WizardForm handle these
23
- if (key.ctrl || key.escape || key.tab)
24
- return;
25
- if (key.return) {
26
- const next = [...lines];
27
- const line = next[cursorRow] ?? "";
28
- next.splice(cursorRow, 1, line.slice(0, cursorCol), line.slice(cursorCol));
29
- setLines(next);
30
- setCursorRow((r) => r + 1);
31
- setCursorCol(0);
32
- emit(next);
33
- return;
34
- }
35
- if (key.upArrow) {
36
- if (cursorRow > 0) {
37
- setCursorRow((r) => r - 1);
38
- setCursorCol((c) => Math.min(c, (lines[cursorRow - 1] ?? "").length));
39
- }
40
- return;
41
- }
42
- if (key.downArrow) {
43
- if (cursorRow < lines.length - 1) {
44
- setCursorRow((r) => r + 1);
45
- setCursorCol((c) => Math.min(c, (lines[cursorRow + 1] ?? "").length));
46
- }
47
- return;
48
- }
49
- if (key.leftArrow) {
50
- if (cursorCol > 0)
51
- setCursorCol((c) => c - 1);
52
- else if (cursorRow > 0) {
53
- setCursorRow((r) => r - 1);
54
- setCursorCol((lines[cursorRow - 1] ?? "").length);
55
- }
56
- return;
57
- }
58
- if (key.rightArrow) {
59
- const curLen = (lines[cursorRow] ?? "").length;
60
- if (cursorCol < curLen)
61
- setCursorCol((c) => c + 1);
62
- else if (cursorRow < lines.length - 1) {
63
- setCursorRow((r) => r + 1);
64
- setCursorCol(0);
65
- }
66
- return;
67
- }
68
- if (key.backspace || key.delete) {
69
- if (cursorCol > 0) {
70
- const next = [...lines];
71
- const line = next[cursorRow] ?? "";
72
- next[cursorRow] = line.slice(0, cursorCol - 1) + line.slice(cursorCol);
73
- setLines(next);
74
- setCursorCol((c) => c - 1);
75
- emit(next);
76
- }
77
- else if (cursorRow > 0) {
78
- const next = [...lines];
79
- const prev = next[cursorRow - 1] ?? "";
80
- const cur = next[cursorRow] ?? "";
81
- next.splice(cursorRow - 1, 2, prev + cur);
82
- setLines(next);
83
- setCursorRow((r) => r - 1);
84
- setCursorCol(prev.length);
85
- emit(next);
86
- }
87
- return;
88
- }
89
- // Bracketed paste: content wrapped in ESC[200~ ... ESC[201~
90
- if (input.includes("\x1b[200~")) {
91
- const pasted = sanitizePaste(input);
92
- if (pasted) {
93
- const next = [...lines];
94
- const line = next[cursorRow] ?? "";
95
- next[cursorRow] = line.slice(0, cursorCol) + pasted + line.slice(cursorCol);
96
- setLines(next);
97
- setCursorCol((c) => c + pasted.length);
98
- emit(next);
99
- }
100
- return;
101
- }
102
- // Multi-char containing CR/LF with no bracketed markers — ignore
103
- if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
104
- return;
105
- // Multi-char printable input = unbracketed single-line paste (e.g. right-click)
106
- if (input.length > 1 && !key.meta) {
107
- const pasted = sanitizePaste(input);
108
- if (pasted) {
109
- const next = [...lines];
110
- const line = next[cursorRow] ?? "";
111
- next[cursorRow] = line.slice(0, cursorCol) + pasted + line.slice(cursorCol);
112
- setLines(next);
113
- setCursorCol((c) => c + pasted.length);
114
- emit(next);
115
- }
116
- return;
117
- }
118
- if (input.length === 1 && input >= " " && input <= "~") {
119
- const next = [...lines];
120
- const line = next[cursorRow] ?? "";
121
- next[cursorRow] = line.slice(0, cursorCol) + input + line.slice(cursorCol);
122
- setLines(next);
123
- setCursorCol((c) => c + 1);
124
- emit(next);
125
- }
126
- }, { isActive });
127
- const scrollStart = Math.max(0, cursorRow - MAX_VISIBLE + 1);
128
- const visible = lines.slice(scrollStart, scrollStart + MAX_VISIBLE);
129
- const lineNumWidth = String(lines.length).length;
130
- const isEmpty = lines.length === 1 && lines[0] === "";
131
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: isActive ? theme.accent.brand : theme.border.dim, backgroundColor: isActive ? theme.bg.input : undefined, width: "100%", paddingX: 1, children: [isEmpty && !isActive ? (_jsx(Text, { color: theme.text.muted, children: hint })) : (visible.map((line, visIdx) => {
132
- const rowIdx = scrollStart + visIdx;
133
- const isCursor = isActive && rowIdx === cursorRow;
134
- const lineNum = String(rowIdx + 1).padStart(lineNumWidth, " ");
135
- if (isCursor) {
136
- const before = line.slice(0, cursorCol);
137
- const cur = cursorCol < line.length ? line[cursorCol] : " ";
138
- const after = cursorCol < line.length ? line.slice(cursorCol + 1) : "";
139
- return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.text.muted, children: [lineNum, " "] }), _jsx(Text, { color: theme.text.primary, children: before }), _jsx(Text, { inverse: true, children: cur }), _jsx(Text, { color: theme.text.primary, children: after })] }, rowIdx));
140
- }
141
- return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.text.muted, children: [lineNum, " "] }), _jsx(Text, { color: theme.text.secondary, children: line })] }, rowIdx));
142
- })), isActive ? (_jsx(Box, { marginTop: 0, children: _jsx(Text, { color: theme.text.muted, children: t("cmdEditor.inlineHint") }) })) : null] }));
143
- }