loop-task 1.5.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +141 -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 +16 -9
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } 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 { EXPORT_MAX_PREVIEW_LINES } from "../../config/constants.js";
7
+ import { copyToClipboard } from "../../shared/clipboard.js";
8
+ const VISIBLE_LINES = 20;
9
+ export function ExportModal(props) {
10
+ const lines = props.json.split("\n");
11
+ const totalLines = lines.length;
12
+ const truncated = totalLines > EXPORT_MAX_PREVIEW_LINES;
13
+ const displayLines = truncated ? lines.slice(0, EXPORT_MAX_PREVIEW_LINES) : lines;
14
+ const maxScroll = Math.max(0, displayLines.length - VISIBLE_LINES);
15
+ const [scrollOffset, setScrollOffset] = useState(0);
16
+ useInput((input, key) => {
17
+ if (key.escape) {
18
+ props.onClose();
19
+ return;
20
+ }
21
+ if (key.downArrow) {
22
+ setScrollOffset((o) => Math.min(o + 1, maxScroll));
23
+ return;
24
+ }
25
+ if (key.upArrow) {
26
+ setScrollOffset((o) => Math.max(0, o - 1));
27
+ return;
28
+ }
29
+ if (input === "c" && !key.ctrl) {
30
+ copyToClipboard(props.json);
31
+ props.onCopy?.();
32
+ return;
33
+ }
34
+ });
35
+ const visible = displayLines.slice(scrollOffset, scrollOffset + VISIBLE_LINES);
36
+ return (_jsxs(Box, { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", flexDirection: "column", backgroundColor: theme.bg.elevated, borderStyle: "round", borderColor: theme.accent.brand, paddingX: 2, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.accent.brand, bold: true, children: t("export.modalTitle") }) }), props.error ? (_jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: theme.semantic.danger, children: t("export.error", { message: props.error }) }) })) : (_jsxs(_Fragment, { children: [props.filePath ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.accent.brand, children: t("export.filePath", { path: props.filePath }) }) })) : null, _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [visible.map((line, i) => (_jsx(Text, { color: theme.text.primary, wrap: "truncate", children: line }, scrollOffset + i))), truncated ? (_jsx(Text, { color: theme.text.muted, children: t("export.truncated", { total: String(totalLines) }) })) : null] })] })), _jsxs(Box, { marginTop: 1, justifyContent: "space-between", children: [_jsx(Text, { color: theme.text.muted, children: t("export.hint") }), _jsxs(Text, { color: theme.text.muted, children: ["[", scrollOffset, "-", scrollOffset + visible.length, "/", totalLines, "]"] })] })] }));
37
+ }
@@ -0,0 +1,13 @@
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
+ }
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text, useFocus, useInput } from "ink";
3
+ import { darkTheme as theme } from "../theme.js";
4
+ export function FocusableButton(props) {
5
+ const { label, color, onPress, variant = "default" } = props;
6
+ const { isFocused } = useFocus();
7
+ useInput((_input, key) => {
8
+ if (key.return) {
9
+ onPress?.();
10
+ }
11
+ }, { isActive: isFocused });
12
+ const accent = variant === "danger" ? theme.semantic.danger : color;
13
+ const textColor = isFocused ? theme.text.inverse : accent;
14
+ const indicator = isFocused ? "› " : " ";
15
+ return (_jsx(Box, { backgroundColor: isFocused ? theme.bg.active : undefined, paddingX: 1, marginRight: 1, flexShrink: 0, children: _jsx(Text, { color: textColor, bold: true, children: indicator + label }) }));
16
+ }
@@ -0,0 +1,69 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useCallback } from "react";
3
+ import { Box, Text, useFocus, useInput } from "ink";
4
+ import { darkTheme as theme } from "../theme.js";
5
+ import { sanitizePaste } from "../utils/paste.js";
6
+ export function FocusableInput(props) {
7
+ const { value, onChange, onSubmit } = props;
8
+ const placeholder = props.placeholder ?? "type here...";
9
+ const { isFocused } = useFocus();
10
+ const [cursorOffset, setCursorOffset] = useState(value.length);
11
+ const insertText = useCallback((text) => {
12
+ const before = value.slice(0, cursorOffset);
13
+ const after = value.slice(cursorOffset);
14
+ const next = before + text + after;
15
+ onChange(next);
16
+ setCursorOffset(before.length + text.length);
17
+ }, [value, cursorOffset, onChange]);
18
+ useInput((input, key) => {
19
+ // Bracketed paste: content wrapped in ESC[200~ ... ESC[201~
20
+ if (input.includes("\x1b[200~")) {
21
+ insertText(sanitizePaste(input));
22
+ return;
23
+ }
24
+ if (key.ctrl || key.escape)
25
+ return;
26
+ // Multi-char containing CR/LF with no bracketed markers — ignore
27
+ if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
28
+ return;
29
+ if (key.return) {
30
+ onSubmit?.(value);
31
+ return;
32
+ }
33
+ if (key.backspace || key.delete) {
34
+ if (cursorOffset > 0) {
35
+ const next = value.slice(0, cursorOffset - 1) + value.slice(cursorOffset);
36
+ onChange(next);
37
+ setCursorOffset((c) => c - 1);
38
+ }
39
+ return;
40
+ }
41
+ if (key.leftArrow) {
42
+ if (cursorOffset > 0)
43
+ setCursorOffset((c) => c - 1);
44
+ return;
45
+ }
46
+ if (key.rightArrow) {
47
+ if (cursorOffset < value.length)
48
+ setCursorOffset((c) => c + 1);
49
+ return;
50
+ }
51
+ // Multi-char printable input = unbracketed single-line paste (e.g. right-click)
52
+ if (input.length > 1 && !key.meta) {
53
+ insertText(sanitizePaste(input));
54
+ return;
55
+ }
56
+ // Single printable char
57
+ if (input.length === 1 && input >= " " && input <= "~") {
58
+ insertText(input);
59
+ }
60
+ }, { isActive: isFocused });
61
+ const borderColor = isFocused ? theme.accent.brand : theme.border.dim;
62
+ const backgroundColor = isFocused ? theme.bg.input : undefined;
63
+ const showPlaceholder = value.length === 0;
64
+ // Render value with cursor
65
+ const before = value.slice(0, cursorOffset);
66
+ const cursorChar = cursorOffset < value.length ? value[cursorOffset] : " ";
67
+ const after = cursorOffset < value.length ? value.slice(cursorOffset + 1) : "";
68
+ return (_jsxs(Box, { borderStyle: "single", borderColor: borderColor, backgroundColor: backgroundColor, paddingLeft: 1, overflow: "hidden", children: [showPlaceholder ? (_jsx(Text, { color: theme.text.muted, children: placeholder })) : (_jsxs(Text, { color: theme.text.primary, children: [before, _jsx(Text, { inverse: true, children: cursorChar }), after] })), isFocused && showPlaceholder ? _jsx(Text, { inverse: true, children: " " }) : null] }));
69
+ }
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { darkTheme as theme } from "../theme.js";
5
+ export function FocusableList(props) {
6
+ const { items, renderItem, onSelect, onActivate, selectedIndex, isFocused, navActive = true, limit, } = props;
7
+ const n = items.length;
8
+ useInput((input, key) => {
9
+ if (n === 0)
10
+ return;
11
+ if (key.upArrow || input === "k") {
12
+ const next = selectedIndex <= 0 ? n - 1 : selectedIndex - 1;
13
+ onSelect(next);
14
+ return;
15
+ }
16
+ if (key.downArrow || input === "j") {
17
+ const next = selectedIndex >= n - 1 ? 0 : selectedIndex + 1;
18
+ onSelect(next);
19
+ return;
20
+ }
21
+ if (key.return) {
22
+ onActivate(selectedIndex);
23
+ return;
24
+ }
25
+ }, { isActive: isFocused && navActive });
26
+ const { visible, localSelected } = useMemo(() => {
27
+ if (n === 0)
28
+ return { visible: [], localSelected: 0 };
29
+ const eff = Math.min(limit, n);
30
+ if (n <= eff) {
31
+ return { visible: items, localSelected: selectedIndex };
32
+ }
33
+ const offset = Math.floor(eff / 2);
34
+ const r = (((selectedIndex - offset) % n) + n) % n;
35
+ const rotated = [...items.slice(r), ...items.slice(0, r)];
36
+ return { visible: rotated.slice(0, eff), localSelected: offset };
37
+ }, [items, selectedIndex, limit, n]);
38
+ if (n === 0) {
39
+ throw new Error("FocusableList requires at least one item");
40
+ }
41
+ return (_jsx(Box, { flexDirection: "column", children: visible.map((item, i) => {
42
+ const isSelected = i === localSelected;
43
+ const indicator = isSelected ? "\u203a " : " ";
44
+ return (_jsxs(Box, { backgroundColor: isSelected ? (isFocused && navActive ? theme.bg.active : isFocused ? theme.bg.hover : undefined) : undefined, children: [_jsx(Text, { color: isSelected ? theme.text.inverse : theme.text.primary, children: indicator }), renderItem(item, isSelected)] }, i));
45
+ }) }));
46
+ }
@@ -0,0 +1,82 @@
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
+ }
@@ -0,0 +1,42 @@
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
+ }
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text, useStdout } from "ink";
4
+ import { darkTheme as theme } from "../theme.js";
5
+ import { HEADER_COMPACT_WIDTH } from "../../config/constants.js";
6
+ import { t } from "../../i18n/index.js";
7
+ import { TabBar } from "./TabBar.js";
8
+ function daemonSymbol(status) {
9
+ switch (status) {
10
+ case "connected": return "\u25CF";
11
+ case "starting": return "\u25CB";
12
+ case "error": return "\u2717";
13
+ }
14
+ }
15
+ function daemonColor(status) {
16
+ switch (status) {
17
+ case "connected": return theme.semantic.success;
18
+ case "starting": return theme.semantic.warning;
19
+ case "error": return theme.semantic.danger;
20
+ }
21
+ }
22
+ function daemonText(status) {
23
+ switch (status) {
24
+ case "connected": return "connected";
25
+ case "starting": return "starting";
26
+ case "error": return "offline";
27
+ }
28
+ }
29
+ export function Header(props) {
30
+ const { stdout } = useStdout();
31
+ const width = stdout?.columns ?? 80;
32
+ const compact = width < HEADER_COMPACT_WIDTH;
33
+ const entries = [
34
+ { label: t("board.runningLabel"), value: props.counts.running, color: theme.semantic.success },
35
+ { label: t("board.waitingLabel"), value: props.counts.waiting, color: theme.accent.loop },
36
+ { label: t("board.pausedLabel"), value: props.counts.paused, color: theme.semantic.warning },
37
+ { label: t("board.idleLabel"), value: props.counts.idle, color: theme.semantic.idle },
38
+ ];
39
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: theme.accent.brand, bold: true, children: t("board.appName") }), _jsx(Text, { color: daemonColor(props.daemonStatus), children: daemonSymbol(props.daemonStatus) }), _jsx(Text, { color: theme.text.secondary, children: daemonText(props.daemonStatus) }), !compact && entries.map((e) => e.value > 0 ? (_jsxs(React.Fragment, { children: [_jsx(Text, { color: theme.text.muted, children: e.label }), _jsx(Text, { color: e.color, children: e.value })] }, e.label)) : null)] }), _jsx(TabBar, { activeTab: props.activeTab, onTabChange: props.onTabChange, counts: props.tabCounts })] }), _jsx(Box, { children: _jsx(Text, { color: theme.border.default, children: "\u2500".repeat(width) }) })] }));
40
+ }
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { darkTheme as theme } from "../theme.js";
4
+ import { Modal } from "./Modal.js";
5
+ import { t } from "../../i18n/index.js";
6
+ export function HelpGuideModal(props) {
7
+ useInput((_input, key) => {
8
+ if (key.escape || _input === "q") {
9
+ props.onClose();
10
+ }
11
+ });
12
+ const sections = [
13
+ { title: "", body: t("helpGuide.intro") },
14
+ { title: t("helpGuide.commands"), body: t("helpGuide.examples") },
15
+ { title: t("helpGuide.allCommands"), body: t("helpGuide.allCommands") },
16
+ { title: t("helpGuide.confirm"), body: t("helpGuide.confirm") },
17
+ { title: t("helpGuide.navigation"), body: t("helpGuide.navigation") },
18
+ { title: t("helpGuide.ctrl"), body: t("helpGuide.ctrl") },
19
+ ];
20
+ return (_jsxs(Modal, { title: t("helpGuide.title"), onClose: props.onClose, width: "70%", children: [sections.map((section, i) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [section.title.length > 0 ? (_jsx(Text, { color: theme.accent.brand, bold: true, children: section.title })) : null, _jsx(Text, { color: theme.text.primary, children: section.body })] }, i))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.text.muted, children: t("helpGuide.esc") }) })] }));
21
+ }
@@ -0,0 +1,59 @@
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 { Modal } from "./Modal.js";
5
+ import { t } from "../../i18n/index.js";
6
+ function helpEntries(view) {
7
+ switch (view) {
8
+ case "board":
9
+ return [
10
+ { key: t("board.helpKeyMove"), desc: t("board.helpMoveSelection") },
11
+ { key: t("board.helpKeyEnter"), desc: t("board.helpToggleDetail") },
12
+ { key: t("board.helpKeyN"), desc: t("board.helpCreate") },
13
+ { key: t("board.helpKeyE"), desc: t("board.helpEdit") },
14
+ { key: t("board.helpKeyP"), desc: t("board.helpPauseResume") },
15
+ { key: t("board.helpKeyX"), desc: t("board.helpForceRun") },
16
+ { key: t("board.helpKeyD"), desc: t("board.helpDelete") },
17
+ { key: t("board.helpKeySlash"), desc: t("board.helpSearch") },
18
+ { key: t("board.helpKeyF"), desc: t("board.helpCycleFilter") },
19
+ { key: t("board.helpKeyS"), desc: t("board.helpCycleSort") },
20
+ { key: t("board.helpKeyLeftRight"), desc: t("board.helpSwitchPanel") },
21
+ { key: t("board.helpKeyH"), desc: t("board.helpToggleHelp") },
22
+ { key: t("board.helpKeyEsc"), desc: t("board.helpBack") },
23
+ { key: t("board.helpKeyQ"), desc: t("board.helpQuit") },
24
+ ];
25
+ case "tasks":
26
+ return [
27
+ { key: t("board.helpKeyMove"), desc: t("board.helpMoveSelection") },
28
+ { key: t("board.helpKeyEnter"), desc: t("board.taskActionSelect") },
29
+ { key: t("board.helpKeyN"), desc: t("board.helpCreateTask") },
30
+ { key: t("board.helpKeyE"), desc: t("board.taskActionEdit") },
31
+ { key: t("board.helpKeyD"), desc: t("board.actionDelete") },
32
+ { key: t("board.helpKeyH"), desc: t("board.helpToggleHelp") },
33
+ { key: t("board.helpKeyEsc"), desc: t("board.helpBack") },
34
+ { key: t("board.helpKeyQ"), desc: t("board.helpQuit") },
35
+ ];
36
+ case "projects":
37
+ return [
38
+ { key: t("board.helpKeyMove"), desc: t("board.helpMoveSelection") },
39
+ { key: t("board.helpKeyEnter"), desc: t("project.editProjectLabel") },
40
+ { key: t("board.helpKeyN"), desc: t("project.keyNewHint") },
41
+ { key: t("board.helpKeyE"), desc: t("project.keyEditHint") },
42
+ { key: t("board.helpKeyD"), desc: t("project.keyDeleteHint") },
43
+ { key: t("board.helpKeyH"), desc: t("board.helpToggleHelp") },
44
+ { key: t("board.helpKeyEsc"), desc: t("board.helpBack") },
45
+ { key: t("board.helpKeyQ"), desc: t("board.helpQuit") },
46
+ ];
47
+ default:
48
+ return [
49
+ { key: t("board.hintKeyTab"), desc: t("board.hintNextField") },
50
+ { key: t("board.helpKeyEnter"), desc: t("board.hintApply") },
51
+ { key: t("board.helpKeyEsc"), desc: t("board.hintCancel") },
52
+ { key: t("board.helpKeyQ"), desc: t("board.helpQuit") },
53
+ ];
54
+ }
55
+ }
56
+ export function HelpModal(props) {
57
+ const entries = helpEntries(props.view);
58
+ return (_jsx(Modal, { title: "Shortcuts", onClose: props.onClose, width: "60%", children: entries.map((entry, i) => (_jsxs(Box, { children: [_jsx(Text, { color: theme.accent.brand, children: entry.key.padEnd(16) }), _jsx(Text, { color: theme.text.primary, children: entry.desc })] }, i))) }));
59
+ }
@@ -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 { 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
+ }
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { darkTheme as theme, statusColor } from "../theme.js";
4
+ import { describeLoop, commandLine, timeAgo, timeUntil } from "../format.js";
5
+ import { t } from "../../i18n/index.js";
6
+ const LABEL_WIDTH = 11;
7
+ function Field(props) {
8
+ return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(LABEL_WIDTH) }), _jsx(Text, { color: theme.text.primary, children: props.children })] }));
9
+ }
10
+ const DIVIDER = "\u2500".repeat(40);
11
+ /** Muted-label field for identity block (de-emphasized). */
12
+ function MutedField(props) {
13
+ return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(LABEL_WIDTH) }), _jsx(Text, { color: theme.text.muted, children: props.children })] }));
14
+ }
15
+ export function Inspector(props) {
16
+ const { loop } = props;
17
+ if (!loop) {
18
+ return (_jsxs(Box, { flexDirection: "column", paddingY: 0, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorTitle") }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorEmpty") }) })] }));
19
+ }
20
+ const sColor = statusColor(loop.status);
21
+ const maxRunsLabel = loop.maxRuns ? String(loop.maxRuns) : t("board.unlimited");
22
+ const lastRun = loop.lastRunAt ? timeAgo(loop.lastRunAt) : t("format.dash");
23
+ const lastExit = loop.lastExitCode !== null ? String(loop.lastExitCode) : t("format.dash");
24
+ const nextRun = loop.nextRunAt ? t("format.timingNext", { timeAgo: timeUntil(loop.nextRunAt) }) : t("format.dash");
25
+ const pid = loop.pid ? String(loop.pid) : t("format.dash");
26
+ // Dedupe: when description matches the full command line, skip Command field
27
+ const fullCmd = commandLine(loop.command, loop.commandArgs);
28
+ const desc = describeLoop(loop);
29
+ const showCommand = desc !== fullCmd;
30
+ return (_jsxs(Box, { flexDirection: "column", paddingY: 0, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorTitle") }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: t("board.fieldStatus").padEnd(LABEL_WIDTH) }), _jsx(Text, { color: sColor, children: loop.status })] }), _jsx(Field, { label: t("board.fieldLastExit"), children: _jsx(Text, { color: theme.text.primary, children: lastExit }) }), _jsx(Field, { label: t("board.fieldLastRun"), children: _jsx(Text, { color: theme.text.primary, children: lastRun }) }), _jsx(Field, { label: t("board.fieldNextRun"), children: _jsx(Text, { color: theme.text.primary, children: nextRun }) }), _jsx(Field, { label: t("board.fieldRuns"), children: _jsxs(Text, { color: theme.text.primary, children: [loop.runCount, " / ", maxRunsLabel] }) }), _jsx(Field, { label: t("board.fieldInterval"), children: _jsx(Text, { color: theme.text.primary, children: loop.intervalHuman }) }), _jsx(Field, { label: t("board.fieldDir"), children: _jsx(Text, { color: theme.text.primary, children: loop.cwd }) }), _jsx(MutedField, { label: t("board.fieldId"), children: loop.id }), _jsx(MutedField, { label: t("board.fieldDesc"), children: desc }), showCommand && _jsx(MutedField, { label: t("board.fieldCommand"), children: fullCmd }), _jsx(MutedField, { label: t("board.fieldTask"), children: loop.taskId ?? t("format.dash") }), _jsx(MutedField, { label: t("board.fieldPid"), children: pid })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) })] }));
31
+ }