loop-task 2.0.2 → 2.0.3

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 (121) hide show
  1. package/package.json +2 -2
  2. package/dist/board/App.js +0 -405
  3. package/dist/board/board-log.js +0 -18
  4. package/dist/board/components/ActionButtons.js +0 -46
  5. package/dist/board/components/BoardButton.js +0 -58
  6. package/dist/board/components/ConfirmModal.js +0 -36
  7. package/dist/board/components/ContextHelpModal.js +0 -22
  8. package/dist/board/components/CreateForm.js +0 -271
  9. package/dist/board/components/CreateProjectModal.js +0 -97
  10. package/dist/board/components/DeleteProjectConfirm.js +0 -53
  11. package/dist/board/components/DetailView.js +0 -25
  12. package/dist/board/components/EditProjectModal.js +0 -99
  13. package/dist/board/components/FilterBar.js +0 -17
  14. package/dist/board/components/Footer.js +0 -52
  15. package/dist/board/components/Header.js +0 -46
  16. package/dist/board/components/HelpModal.js +0 -65
  17. package/dist/board/components/Inspector.js +0 -12
  18. package/dist/board/components/LogModal.js +0 -82
  19. package/dist/board/components/Navigator.js +0 -84
  20. package/dist/board/components/ProjectsModal.js +0 -70
  21. package/dist/board/components/ProjectsPage.js +0 -134
  22. package/dist/board/components/RunHistory.js +0 -83
  23. package/dist/board/components/SearchBox.js +0 -10
  24. package/dist/board/components/SearchSelect.js +0 -132
  25. package/dist/board/components/TaskBrowser.js +0 -95
  26. package/dist/board/components/TaskFilterBar.js +0 -6
  27. package/dist/board/components/TaskForm.js +0 -155
  28. package/dist/board/components/Timeline.js +0 -5
  29. package/dist/board/daemon.js +0 -117
  30. package/dist/board/focus-context.js +0 -98
  31. package/dist/board/hooks/useBoardKeybindings.js +0 -344
  32. package/dist/board/hooks/useBreakpoint.js +0 -6
  33. package/dist/board/hooks/useHoverState.js +0 -11
  34. package/dist/board/hooks/useInputShortcuts.js +0 -61
  35. package/dist/board/hooks/useLogStream.js +0 -32
  36. package/dist/board/hooks/useLoopPolling.js +0 -28
  37. package/dist/board/hooks/useTabNav.js +0 -60
  38. package/dist/board/hooks/useTaskKeybindings.js +0 -120
  39. package/dist/board/index.js +0 -32
  40. package/dist/board/router.js +0 -16
  41. package/dist/board/state.js +0 -99
  42. package/dist/board/toast.js +0 -65
  43. package/dist/board/types.js +0 -1
  44. package/dist/cli.d.ts +0 -3
  45. package/dist/cli.d.ts.map +0 -1
  46. package/dist/cli.js.map +0 -1
  47. package/dist/client/commands.d.ts +0 -11
  48. package/dist/client/commands.d.ts.map +0 -1
  49. package/dist/client/commands.js.map +0 -1
  50. package/dist/client/ipc.d.ts +0 -5
  51. package/dist/client/ipc.d.ts.map +0 -1
  52. package/dist/client/ipc.js.map +0 -1
  53. package/dist/daemon/index.d.ts +0 -2
  54. package/dist/daemon/index.d.ts.map +0 -1
  55. package/dist/daemon/index.js.map +0 -1
  56. package/dist/daemon/manager.d.ts +0 -17
  57. package/dist/daemon/manager.d.ts.map +0 -1
  58. package/dist/daemon/manager.js.map +0 -1
  59. package/dist/daemon/server.d.ts +0 -15
  60. package/dist/daemon/server.d.ts.map +0 -1
  61. package/dist/daemon/server.js.map +0 -1
  62. package/dist/daemon/spawner.d.ts +0 -3
  63. package/dist/daemon/spawner.d.ts.map +0 -1
  64. package/dist/daemon/spawner.js.map +0 -1
  65. package/dist/daemon/state.d.ts +0 -15
  66. package/dist/daemon/state.d.ts.map +0 -1
  67. package/dist/daemon/state.js.map +0 -1
  68. package/dist/duration.d.ts +0 -3
  69. package/dist/duration.d.ts.map +0 -1
  70. package/dist/duration.js.map +0 -1
  71. package/dist/logger.d.ts +0 -11
  72. package/dist/logger.d.ts.map +0 -1
  73. package/dist/logger.js.map +0 -1
  74. package/dist/loop-config.d.ts +0 -13
  75. package/dist/loop-config.d.ts.map +0 -1
  76. package/dist/loop-config.js.map +0 -1
  77. package/dist/loop.d.ts +0 -46
  78. package/dist/loop.d.ts.map +0 -1
  79. package/dist/loop.js +0 -363
  80. package/dist/loop.js.map +0 -1
  81. package/dist/open-board.d.ts +0 -2
  82. package/dist/open-board.d.ts.map +0 -1
  83. package/dist/open-board.js +0 -19
  84. package/dist/open-board.js.map +0 -1
  85. package/dist/tui/app.d.ts +0 -57
  86. package/dist/tui/app.d.ts.map +0 -1
  87. package/dist/tui/app.js.map +0 -1
  88. package/dist/tui/components/ActionButtons.js +0 -49
  89. package/dist/tui/components/CommandBuilderField.js +0 -143
  90. package/dist/tui/components/CommandEditorModal.js +0 -159
  91. package/dist/tui/components/ConfirmModal.js +0 -9
  92. package/dist/tui/components/FilterBar.js +0 -13
  93. package/dist/tui/components/FocusableSearchSelect.js +0 -82
  94. package/dist/tui/components/Footer.js +0 -42
  95. package/dist/tui/components/PatchEditForm.js +0 -78
  96. package/dist/tui/components/SearchSelect.js +0 -67
  97. package/dist/tui/components/TaskFilterBar.js +0 -9
  98. package/dist/tui/create-loop.d.ts +0 -3
  99. package/dist/tui/create-loop.d.ts.map +0 -1
  100. package/dist/tui/create-loop.js +0 -90
  101. package/dist/tui/create-loop.js.map +0 -1
  102. package/dist/tui/dashboard.d.ts +0 -2
  103. package/dist/tui/dashboard.d.ts.map +0 -1
  104. package/dist/tui/dashboard.js +0 -45
  105. package/dist/tui/dashboard.js.map +0 -1
  106. package/dist/tui/events.d.ts +0 -3
  107. package/dist/tui/events.d.ts.map +0 -1
  108. package/dist/tui/events.js +0 -244
  109. package/dist/tui/events.js.map +0 -1
  110. package/dist/tui/popup.d.ts +0 -3
  111. package/dist/tui/popup.d.ts.map +0 -1
  112. package/dist/tui/popup.js +0 -58
  113. package/dist/tui/popup.js.map +0 -1
  114. package/dist/tui/render.d.ts +0 -9
  115. package/dist/tui/render.d.ts.map +0 -1
  116. package/dist/tui/render.js +0 -305
  117. package/dist/tui/render.js.map +0 -1
  118. package/dist/types.d.ts +0 -94
  119. package/dist/types.d.ts.map +0 -1
  120. package/dist/types.js.map +0 -1
  121. /package/dist/tui/{app.js → App.js} +0 -0
@@ -1,132 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRef, useState, useMemo, useEffect } from "react";
3
- import { useKeyboard } from "@opentui/react";
4
- import { t } from "../../i18n/index.js";
5
- import { SEARCH_SELECT_HEIGHT } from "../../config/constants.js";
6
- export function SearchSelect(props) {
7
- const { options, value, onChange, focused, height } = props;
8
- const placeholder = props.placeholder ?? t("board.searchSelectPlaceholder");
9
- const maxHeight = height ?? SEARCH_SELECT_HEIGHT;
10
- const [filter, setFilter] = useState("");
11
- const [selectedIndex, setSelectedIndex] = useState(0);
12
- const filterRef = useRef(filter);
13
- filterRef.current = filter;
14
- const filtered = useMemo(() => {
15
- if (!filter)
16
- return options;
17
- const q = filter.toLowerCase();
18
- return options.filter((o) => o.name.toLowerCase().includes(q) || o.value.toLowerCase().includes(q));
19
- }, [options, filter]);
20
- const clampedSelected = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
21
- const filteredRef = useRef(filtered);
22
- filteredRef.current = filtered;
23
- const selectedRef = useRef(clampedSelected);
24
- selectedRef.current = clampedSelected;
25
- const focusedRef = useRef(focused);
26
- focusedRef.current = focused;
27
- const onChangeRef = useRef(onChange);
28
- onChangeRef.current = onChange;
29
- const scrollRef = useRef(null);
30
- useEffect(() => {
31
- if (focused) {
32
- const idx = filtered.findIndex((o) => o.value === value);
33
- const target = idx >= 0 ? idx : 0;
34
- setSelectedIndex(target);
35
- requestAnimationFrame(() => {
36
- scrollRef.current?.scrollChildIntoView(`select-option-${target}`);
37
- });
38
- }
39
- else {
40
- setFilter("");
41
- }
42
- }, [focused]);
43
- useEffect(() => {
44
- scrollRef.current?.scrollChildIntoView(`select-option-${clampedSelected}`);
45
- }, [clampedSelected, filter]);
46
- const computeFiltered = (f) => {
47
- if (!f)
48
- return options;
49
- const q = f.toLowerCase();
50
- return options.filter((o) => o.name.toLowerCase().includes(q) || o.value.toLowerCase().includes(q));
51
- };
52
- useKeyboard((key) => {
53
- if (!focusedRef.current)
54
- return;
55
- const name = key.name;
56
- if (name === "up" || name === "k") {
57
- const list = computeFiltered(filterRef.current);
58
- if (list.length > 0) {
59
- const cur = Math.min(selectedRef.current, list.length - 1);
60
- setSelectedIndex(cur <= 0 ? list.length - 1 : cur - 1);
61
- }
62
- key.preventDefault();
63
- key.stopPropagation();
64
- return;
65
- }
66
- if (name === "down" || name === "j") {
67
- const list = computeFiltered(filterRef.current);
68
- if (list.length > 0) {
69
- const cur = Math.min(selectedRef.current, list.length - 1);
70
- setSelectedIndex(cur >= list.length - 1 ? 0 : cur + 1);
71
- }
72
- key.preventDefault();
73
- key.stopPropagation();
74
- return;
75
- }
76
- if (name === "return" || name === "enter") {
77
- const list = computeFiltered(filterRef.current);
78
- const cur = Math.min(selectedRef.current, list.length - 1);
79
- const option = list[cur];
80
- if (option) {
81
- onChangeRef.current(option.value);
82
- }
83
- key.preventDefault();
84
- key.stopPropagation();
85
- return;
86
- }
87
- if (name === "escape") {
88
- setFilter("");
89
- setSelectedIndex(0);
90
- key.preventDefault();
91
- key.stopPropagation();
92
- return;
93
- }
94
- if (name === "backspace") {
95
- const newFilter = filterRef.current.slice(0, -1);
96
- filterRef.current = newFilter;
97
- setFilter(newFilter);
98
- setSelectedIndex(0);
99
- key.preventDefault();
100
- key.stopPropagation();
101
- return;
102
- }
103
- if (key.ctrl)
104
- return;
105
- let char = null;
106
- if (name && name.length === 1 && /[a-z0-9 _\-./]/i.test(name)) {
107
- char = name;
108
- }
109
- else if (key.sequence && key.sequence.length === 1 && /[a-z0-9 _\-./]/i.test(key.sequence)) {
110
- char = key.sequence;
111
- }
112
- if (char) {
113
- const newFilter = filterRef.current + char;
114
- filterRef.current = newFilter;
115
- setFilter(newFilter);
116
- setSelectedIndex(0);
117
- key.preventDefault();
118
- key.stopPropagation();
119
- return;
120
- }
121
- });
122
- const listHeight = Math.min(filtered.length, maxHeight);
123
- return (_jsxs("box", { border: true, borderColor: focused ? "#38bdf8" : undefined, style: { flexDirection: "column", backgroundColor: focused ? "#0f172a" : "#0b0b0b" }, children: [_jsxs("box", { style: { height: 3, flexDirection: "row", alignItems: "center", paddingLeft: 1 }, children: [_jsx("text", { fg: "#6b7280", children: "/ " }), _jsx("text", { fg: filter ? "#e5e7eb" : "#6b7280", children: filter || placeholder })] }), _jsx("scrollbox", { ref: scrollRef, style: { height: listHeight, backgroundColor: "#0b0b0b" }, children: filtered.map((option, i) => {
124
- const isSelected = i === clampedSelected;
125
- const isActive = option.value === value;
126
- const prefix = isSelected ? "› " : " ";
127
- const colorIndicator = option.color ? `\u25cf ` : "";
128
- const bg = isSelected ? "#1e3a8a" : undefined;
129
- const fg = isSelected ? "#ffffff" : isActive ? "#38bdf8" : "#9ca3af";
130
- return (_jsx("box", { id: `select-option-${i}`, style: { flexDirection: "row", backgroundColor: bg }, children: _jsx("text", { fg: fg, children: `${prefix}${colorIndicator}${option.name}` }) }, option.value));
131
- }) })] }));
132
- }
@@ -1,95 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef } from "react";
3
- import { useTerminalDimensions } from "@opentui/react";
4
- import { t } from "../../i18n/index.js";
5
- import { commandLine, truncate } from "../format.js";
6
- import { useHoverState } from "../hooks/useHoverState.js";
7
- import { HOVER_BG, ENTITY_COLORS } from "../../config/constants.js";
8
- function fit(text, width) {
9
- if (width <= 0)
10
- return "";
11
- if (text.length <= width)
12
- return text.padEnd(width);
13
- return truncate(text, width);
14
- }
15
- const TASK_PANEL_ORDER = ["search", "new", "tasks", "actions"];
16
- const TASK_ACTIONS = ["select", "edit", "delete"];
17
- export const TASK_ACTION_COUNT = TASK_ACTIONS.length;
18
- export const TASK_ACTION_KEYS = [...TASK_ACTIONS];
19
- export function nextTaskPanel(current, direction) {
20
- const idx = TASK_PANEL_ORDER.indexOf(current);
21
- return TASK_PANEL_ORDER[(idx + (direction === "right" ? 1 : TASK_PANEL_ORDER.length - 1)) % TASK_PANEL_ORDER.length];
22
- }
23
- export function TaskNavigator(props) {
24
- const { visible, total, selectedIndex, focused, query, onSelect, onActivate } = props;
25
- const { width, height } = useTerminalDimensions();
26
- const scrollRef = useRef(null);
27
- const panelWidth = width * 0.55 - 4;
28
- const panelHeight = height - 7;
29
- const nameW = 20;
30
- const chainsW = 14;
31
- const fixedOverhead = 2 + nameW + 2 + chainsW;
32
- const cmdW = Math.max(10, panelWidth - fixedOverhead);
33
- const header = " " +
34
- fit(t("board.taskHeaderName"), nameW) +
35
- " " +
36
- fit(t("board.taskHeaderCommand"), cmdW) +
37
- " " +
38
- t("board.taskHeaderChains");
39
- useEffect(() => {
40
- const id = `task-row-${selectedIndex}`;
41
- scrollRef.current?.scrollChildIntoView(id);
42
- }, [selectedIndex]);
43
- return (_jsxs("box", { title: t("board.taskBrowserTitle", { visible: visible.length, total }), border: true, borderColor: focused ? ENTITY_COLORS.task : "#2e2545", style: { width: "55%", flexShrink: 0, flexDirection: "column", backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx("text", { fg: "#6b7280", children: header }), visible.length === 0 ? (_jsx("text", { fg: "#9ca3af", children: t("board.taskBrowserEmpty") })) : (_jsx("scrollbox", { ref: scrollRef, style: { flexGrow: 1, maxHeight: panelHeight, backgroundColor: "#0b0b0b" }, children: visible.map((task, index) => (_jsx(TaskNavRow, { id: `task-row-${index}`, task: task, index: index, isSelected: index === selectedIndex, focused: focused, nameW: nameW, cmdW: cmdW, chainsW: chainsW, onSelect: onSelect, onActivate: onActivate }, task.id))) }))] }));
44
- }
45
- function TaskNavRow(props) {
46
- const { id, task, index, isSelected, focused, nameW, cmdW, chainsW, onSelect, onActivate } = props;
47
- const { isHovered, hoverProps } = useHoverState();
48
- const bg = isSelected ? (focused ? "#1e3a8a" : "#1e2a4a") : isHovered ? HOVER_BG : undefined;
49
- const fg = isSelected ? "#ffffff" : "#e5e7eb";
50
- const lastClickRef = useRef(0);
51
- function handleClick() {
52
- const now = Date.now();
53
- if (now - lastClickRef.current < 400) {
54
- onActivate(index);
55
- }
56
- else {
57
- onSelect(index);
58
- }
59
- lastClickRef.current = now;
60
- }
61
- const cmd = commandLine(task.command, task.commandArgs);
62
- const chains = task.onSuccessTaskId || task.onFailureTaskId
63
- ? t("board.taskChainsFormat", { success: task.onSuccessTaskId ?? "-", failure: task.onFailureTaskId ?? "-" })
64
- : t("board.taskChainsNone");
65
- const name = task.name.length > nameW ? task.name.slice(0, nameW - 3) + "..." : task.name;
66
- const truncCmd = cmd.length > cmdW ? cmd.slice(0, cmdW - 3) + "..." : cmd;
67
- return (_jsx("box", { id: id, onMouseDown: handleClick, backgroundColor: bg, ...hoverProps, children: _jsxs("text", { fg: fg, children: [isSelected ? "›" : " ", " ", fit(name, nameW), " ", fit(truncCmd, cmdW), " ", fit(chains, chainsW)] }) }));
68
- }
69
- export function TaskInspector(props) {
70
- const { task } = props;
71
- if (!task) {
72
- return (_jsx("box", { title: t("board.inspectorTitle"), border: true, style: { backgroundColor: "#0b0b0b" }, children: _jsx("text", { fg: "#9ca3af", children: t("board.inspectorEmpty") }) }));
73
- }
74
- const cmd = commandLine(task.command, task.commandArgs);
75
- return (_jsxs("box", { title: t("board.inspectorTitle"), border: true, style: { flexDirection: "column", backgroundColor: "#0b0b0b" }, children: [_jsxs("text", { children: [_jsx("strong", { children: t("board.fieldId") }), " ", task.id] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.taskLabelName") }), " ", task.name] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.fieldCommand") }), " ", cmd] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.taskLabelOnSuccess") }), " ", task.onSuccessTaskId ?? t("board.taskNone")] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.taskLabelOnFailure") }), " ", task.onFailureTaskId ?? t("board.taskNone")] })] }));
76
- }
77
- export function TaskActionButtons(props) {
78
- const { task, focused, selectedAction, selectable = true, onAction } = props;
79
- if (!task) {
80
- return (_jsx("box", { border: true, borderColor: focused ? "#38bdf8" : undefined, style: { flexDirection: "row", height: 3, flexShrink: 0, backgroundColor: "#0b0b0b", justifyContent: "center", alignItems: "center" }, children: _jsx("text", { fg: "#9ca3af", children: t("board.noActions") }) }));
81
- }
82
- const allActions = [
83
- { key: "select", label: t("board.taskActionSelect") },
84
- { key: "edit", label: t("board.taskActionEdit") },
85
- { key: "delete", label: t("board.taskActionDelete") },
86
- ];
87
- const actions = selectable ? allActions : allActions.filter((a) => a.key !== "select");
88
- return (_jsx("box", { border: true, borderColor: focused ? "#38bdf8" : undefined, style: { flexDirection: "row", height: 3, flexShrink: 0, paddingLeft: 1, backgroundColor: "#0b0b0b", alignItems: "center" }, children: actions.map((action, i) => (_jsx(TaskActionButton, { label: action.label, selected: focused && selectedAction === i, onMouseDown: () => onAction(action.key) }, action.key))) }));
89
- }
90
- function TaskActionButton(props) {
91
- const { isHovered, hoverProps } = useHoverState();
92
- const bg = props.selected ? "#1e3a8a" : isHovered ? HOVER_BG : undefined;
93
- const fg = props.selected ? "#ffffff" : isHovered ? "#e5e7eb" : "#9ca3af";
94
- return (_jsx("box", { onMouseDown: props.onMouseDown, style: { backgroundColor: bg, paddingLeft: 1, paddingRight: 1, marginRight: 1 }, ...hoverProps, children: _jsx("text", { fg: fg, children: _jsx("strong", { children: props.label }) }) }));
95
- }
@@ -1,6 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { SearchBox } from "./SearchBox.js";
3
- export function TaskFilterBar(props) {
4
- const { query, searchActive, focusedPanel, onQueryChange, onSearchActivate, onSearchDismiss } = props;
5
- return (_jsx("box", { style: { flexDirection: "row", height: 3, paddingLeft: 1, paddingRight: 1, backgroundColor: "#0b0b0b" }, children: _jsx("box", { style: { flexGrow: 1 }, children: _jsx(SearchBox, { query: query, searchActive: searchActive, focused: focusedPanel === "search", onQueryChange: onQueryChange, onActivate: onSearchActivate, onDismiss: onSearchDismiss }) }) }));
6
- }
@@ -1,155 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRef, useState } from "react";
3
- import crypto from "node:crypto";
4
- import { useKeyboard, useTerminalDimensions } from "@opentui/react";
5
- import { t } from "../../i18n/index.js";
6
- import { createTask, updateTask, listTasks } from "../daemon.js";
7
- import { useHoverState } from "../hooks/useHoverState.js";
8
- import { useInputShortcuts } from "../hooks/useInputShortcuts.js";
9
- import { useTabNav } from "../hooks/useTabNav.js";
10
- import { copyToClipboard } from "../../shared/clipboard.js";
11
- import { HOVER_BG } from "../../config/constants.js";
12
- import { SearchSelect } from "./SearchSelect.js";
13
- const taskFields = ["name", "command", "onSuccessTaskId", "onFailureTaskId"];
14
- function taskInitialValues(task) {
15
- if (!task) {
16
- return { name: "", command: "", onSuccessTaskId: "", onFailureTaskId: "" };
17
- }
18
- return {
19
- name: task.name,
20
- command: [task.command, ...task.commandArgs].join(" "),
21
- onSuccessTaskId: task.onSuccessTaskId ?? "",
22
- onFailureTaskId: task.onFailureTaskId ?? "",
23
- };
24
- }
25
- export function TaskForm(props) {
26
- const [values, setValues] = useState(taskInitialValues(props.editTask));
27
- const valuesRef = useRef(values);
28
- const [error, setError] = useState("");
29
- const [isSubmitting, setIsSubmitting] = useState(false);
30
- const [allTasks, setAllTasks] = useState([]);
31
- const inputRef = useRef(null);
32
- const { width: termWidth } = useTerminalDimensions();
33
- const btnWidth = Math.max(10, Math.min(14, Math.floor(termWidth / 6)));
34
- const navItems = [...taskFields, "save", "cancel"];
35
- const { setFocusIndex, focusedItem, isFocused } = useTabNav(navItems);
36
- const focusedItemRef = useRef(focusedItem);
37
- focusedItemRef.current = focusedItem;
38
- useInputShortcuts(() => {
39
- const fi = focusedItemRef.current;
40
- if (fi != null && fi !== "save" && fi !== "cancel" && fi !== "onSuccessTaskId" && fi !== "onFailureTaskId") {
41
- return inputRef.current;
42
- }
43
- return null;
44
- });
45
- function updateValues(next) {
46
- valuesRef.current = next;
47
- setValues(next);
48
- }
49
- useState(() => {
50
- void listTasks().then(setAllTasks).catch(() => { });
51
- });
52
- useKeyboard((key) => {
53
- if (key.name === "return" || key.name === "enter") {
54
- const fi = focusedItemRef.current;
55
- if (fi === "save") {
56
- void submit(valuesRef.current);
57
- key.preventDefault();
58
- }
59
- else if (fi === "cancel") {
60
- props.onCancel();
61
- key.preventDefault();
62
- }
63
- }
64
- });
65
- async function submit(current) {
66
- if (isSubmitting)
67
- return;
68
- try {
69
- setIsSubmitting(true);
70
- setError("");
71
- const cmdLine = current.command.trim();
72
- if (!cmdLine) {
73
- setError(t("errors.commandEmpty"));
74
- return;
75
- }
76
- const tokens = cmdLine.split(/\s+/);
77
- const command = tokens[0] ?? "";
78
- const commandArgs = tokens.slice(1);
79
- const onSuccessTaskId = current.onSuccessTaskId || null;
80
- const onFailureTaskId = current.onFailureTaskId || null;
81
- if (props.mode === "edit" && props.editTask) {
82
- await updateTask(props.editTask.id, {
83
- name: current.name.trim() || command,
84
- command,
85
- commandArgs,
86
- onSuccessTaskId,
87
- onFailureTaskId,
88
- });
89
- props.onDone(true, props.editTask.id);
90
- }
91
- else {
92
- const id = crypto.randomUUID().slice(0, 8);
93
- await createTask({
94
- id,
95
- name: current.name.trim() || command,
96
- command,
97
- commandArgs,
98
- onSuccessTaskId,
99
- onFailureTaskId,
100
- });
101
- props.onDone(false, id);
102
- }
103
- }
104
- catch (e) {
105
- setError(e instanceof Error ? e.message : String(e));
106
- }
107
- finally {
108
- setIsSubmitting(false);
109
- }
110
- }
111
- const labels = {
112
- name: t("board.taskLabelName"),
113
- command: t("board.taskLabelCommand"),
114
- onSuccessTaskId: t("board.taskLabelOnSuccess"),
115
- onFailureTaskId: t("board.taskLabelOnFailure"),
116
- };
117
- const hints = {
118
- name: t("board.taskHintName"),
119
- command: t("board.hintCommand"),
120
- onSuccessTaskId: t("board.taskHintOnSuccess"),
121
- onFailureTaskId: t("board.taskHintOnFailure"),
122
- };
123
- const chainOptions = [
124
- { name: t("board.taskNone"), description: "", value: "" },
125
- ...allTasks
126
- .filter((t) => t.id !== props.editTask?.id)
127
- .map((t) => ({ name: `${t.id} ${t.name}`, description: "", value: t.id })),
128
- ];
129
- const title = props.mode === "edit" ? t("board.taskEditTitle") : t("board.taskCreateTitle");
130
- return (_jsxs("box", { title: title, border: true, style: { flexDirection: "column", flexGrow: 1, padding: 1, backgroundColor: "#0b0b0b" }, children: [_jsx("box", { style: { flexDirection: "column", flexGrow: 1 }, children: Array.from({ length: Math.ceil(taskFields.length / 2) }, (_, row) => {
131
- const leftField = taskFields[row * 2];
132
- const rightField = row * 2 + 1 < taskFields.length ? taskFields[row * 2 + 1] : null;
133
- return (_jsxs("box", { style: { flexDirection: "row", marginBottom: 1 }, children: [_jsx(TaskFormRow, { field: leftField, index: row * 2, focused: isFocused(leftField), values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, chainOptions: chainOptions, inputRef: inputRef, onCopy: props.onCopy, style: { width: "50%", paddingRight: 1 } }), rightField ? (_jsx(TaskFormRow, { field: rightField, index: row * 2 + 1, focused: isFocused(rightField), values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, chainOptions: chainOptions, inputRef: inputRef, onCopy: props.onCopy, style: { width: "50%" } })) : (_jsx("box", { style: { width: "50%" } }))] }, row));
134
- }) }), _jsxs("box", { style: { flexDirection: "row", height: 3, marginBottom: 1, backgroundColor: "#0b0b0b" }, children: [_jsx(HoverButton, { label: isSubmitting ? t("board.saving") : props.mode === "edit" ? t("board.save") : t("board.create"), onMouseDown: () => void submit(valuesRef.current), selected: isFocused("save"), width: btnWidth, marginRight: 1 }), _jsx(HoverButton, { label: t("board.cancel"), onMouseDown: props.onCancel, selected: isFocused("cancel"), width: btnWidth })] }), _jsx("text", { fg: "#9ca3af", children: t("board.formNav") }), error ? _jsx("text", { fg: "#f87171", children: error }) : null] }));
135
- }
136
- function TaskFormRow(props) {
137
- const { field, index, focused, values, valuesRef, updateValues, setFocusIndex, submit, labels, hints, chainOptions, inputRef, onCopy, style } = props;
138
- const isSelect = field === "onSuccessTaskId" || field === "onFailureTaskId";
139
- const selectOpts = isSelect ? chainOptions : [];
140
- const copyHover = useHoverState();
141
- return (_jsxs("box", { style: { flexDirection: "column", ...style }, children: [_jsx("text", { fg: focused ? "#38bdf8" : "#e5e7eb", children: labels[field] }), _jsx("text", { fg: "#6b7280", children: hints[field] }), isSelect ? (_jsx(SearchSelect, { options: selectOpts, value: values[field], onChange: (v) => updateValues({ ...valuesRef.current, [field]: v }), focused: focused })) : (_jsxs("box", { style: { flexDirection: "row" }, children: [_jsx("box", { border: true, borderColor: focused ? "#38bdf8" : undefined, style: { height: 3, width: field === "command" ? "92%" : "100%", backgroundColor: "#0b0b0b" }, children: _jsx("input", { ref: inputRef, focused: focused, value: values[field], placeholder: field === "command" ? t("board.exampleCommand") : "", onInput: (value) => updateValues({ ...valuesRef.current, [field]: value }), onSubmit: () => {
142
- if (index < taskFields.length - 1) {
143
- setFocusIndex(index + 1);
144
- }
145
- else {
146
- void submit(valuesRef.current);
147
- }
148
- } }) }), field === "command" ? (_jsx("box", { border: true, borderColor: copyHover.isHovered ? "#38bdf8" : "#374151", onMouseDown: () => { copyToClipboard(valuesRef.current.command); onCopy?.(valuesRef.current.command); }, style: { width: "8%", height: 3, justifyContent: "center", alignItems: "center", backgroundColor: copyHover.isHovered ? HOVER_BG : "#0b0b0b" }, ...copyHover.hoverProps, children: _jsx("text", { fg: copyHover.isHovered ? "#38bdf8" : "#6b7280", children: "\u2398" }) })) : null] }))] }));
149
- }
150
- function HoverButton(props) {
151
- const { isHovered, hoverProps } = useHoverState();
152
- const bg = props.selected ? "#1e3a8a" : isHovered ? HOVER_BG : undefined;
153
- const borderColor = props.selected ? "#38bdf8" : undefined;
154
- return (_jsx("box", { border: true, onMouseDown: props.onMouseDown, borderColor: borderColor, style: { width: props.width, justifyContent: "center", alignItems: "center", marginRight: props.marginRight, backgroundColor: bg }, ...hoverProps, children: _jsx("text", { fg: "#e5e7eb", children: _jsx("strong", { children: props.label }) }) }));
155
- }
@@ -1,5 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { t } from "../../i18n/index.js";
3
- export function Timeline(props) {
4
- return (_jsx("scrollbox", { title: t("board.timelineTitle"), border: true, style: { flexGrow: 1, backgroundColor: "#0b0b0b" }, stickyScroll: true, stickyStart: "bottom", children: props.logLines.length === 0 ? (_jsx("text", { fg: "#9ca3af", children: t("board.timelineEmpty") })) : (props.logLines.map((line, index) => _jsx("text", { children: line }, index))) }));
5
- }
@@ -1,117 +0,0 @@
1
- import { sendRequest, streamRequest } from "../client/ipc.js";
2
- import { t } from "../i18n/index.js";
3
- import { LOG_TAIL_DEFAULT } from "../config/constants.js";
4
- function expectOk(message, type) {
5
- if (type !== "ok") {
6
- throw new Error(message);
7
- }
8
- }
9
- export async function listLoops() {
10
- const response = await sendRequest({ type: "list" });
11
- if (response.type !== "ok") {
12
- throw new Error(response.message);
13
- }
14
- return response.data;
15
- }
16
- export async function pauseLoop(id) {
17
- const response = await sendRequest({ type: "pause", payload: { id } });
18
- expectOk(response.message ?? t("errors.pauseFailed"), response.type);
19
- }
20
- export async function resumeLoop(id) {
21
- const response = await sendRequest({ type: "resume", payload: { id } });
22
- expectOk(response.message ?? t("errors.resumeFailed"), response.type);
23
- }
24
- export async function stopLoop(id) {
25
- const response = await sendRequest({ type: "stop-loop", payload: { id } });
26
- expectOk(response.message ?? "Stop failed", response.type);
27
- }
28
- export async function playLoop(id) {
29
- const response = await sendRequest({ type: "play-loop", payload: { id } });
30
- expectOk(response.message ?? "Play failed", response.type);
31
- }
32
- export async function triggerLoop(id) {
33
- const response = await sendRequest({ type: "trigger", payload: { id } });
34
- expectOk(response.message ?? t("errors.forceRunFailed"), response.type);
35
- }
36
- export async function deleteLoop(id) {
37
- const response = await sendRequest({ type: "delete", payload: { id } });
38
- expectOk(response.message ?? t("errors.deleteFailed"), response.type);
39
- }
40
- export async function createLoop(options, intervalHuman) {
41
- const response = await sendRequest({
42
- type: "start",
43
- payload: { ...options, intervalHuman },
44
- });
45
- if (response.type !== "ok") {
46
- throw new Error(response.message);
47
- }
48
- return response.data.id;
49
- }
50
- export async function updateLoop(id, options, intervalHuman) {
51
- const response = await sendRequest({
52
- type: "update",
53
- payload: { id, ...options, intervalHuman },
54
- });
55
- if (response.type !== "ok") {
56
- throw new Error(response.message);
57
- }
58
- return response.data.id;
59
- }
60
- export function streamLogs(id, onLine, onError) {
61
- return streamRequest({ type: "logs", payload: { id, follow: true, tail: LOG_TAIL_DEFAULT } }, onLine, () => { }, onError);
62
- }
63
- export async function fetchRunLog(id, runNumber) {
64
- const response = await sendRequest({ type: "run-log", payload: { id, runNumber } });
65
- if (response.type !== "ok") {
66
- throw new Error(response.message ?? "Failed to fetch run log");
67
- }
68
- return response.data ?? "";
69
- }
70
- export function streamRunLog(id, runNumber, onLine, onEnd, onError) {
71
- return streamRequest({ type: "run-log-stream", payload: { id, runNumber } }, onLine, onEnd, onError);
72
- }
73
- export async function listTasks() {
74
- const response = await sendRequest({ type: "task-list" });
75
- if (response.type !== "ok") {
76
- throw new Error(response.message);
77
- }
78
- return response.data;
79
- }
80
- export async function createTask(payload) {
81
- const response = await sendRequest({ type: "task-create", payload });
82
- if (response.type !== "ok") {
83
- throw new Error(response.message);
84
- }
85
- return response.data;
86
- }
87
- export async function updateTask(id, payload) {
88
- const response = await sendRequest({ type: "task-update", payload: { id, ...payload } });
89
- if (response.type !== "ok") {
90
- throw new Error(response.message);
91
- }
92
- return response.data;
93
- }
94
- export async function deleteTask(id) {
95
- const response = await sendRequest({ type: "task-delete", payload: { id } });
96
- expectOk(response.message ?? "Task delete failed", response.type);
97
- }
98
- export async function listProjects() {
99
- const response = await sendRequest({ type: "project-list" });
100
- if (response.type !== "ok")
101
- throw new Error(response.message);
102
- return response.data;
103
- }
104
- export async function createProject(name, color) {
105
- const response = await sendRequest({ type: "project-create", payload: { name, color } });
106
- if (response.type !== "ok")
107
- throw new Error(response.message);
108
- return response.data;
109
- }
110
- export async function updateProject(id, name, color) {
111
- const response = await sendRequest({ type: "project-update", payload: { id, name, color } });
112
- expectOk(response.message ?? t("project.error.updateFailed"), response.type);
113
- }
114
- export async function deleteProject(id) {
115
- const response = await sendRequest({ type: "project-delete", payload: { id } });
116
- expectOk(response.message ?? t("project.error.deleteFailed"), response.type);
117
- }
@@ -1,98 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useContext, useState, useCallback, useRef, useEffect } from "react";
3
- import { useKeyboard } from "@opentui/react";
4
- const FocusContext = createContext(null);
5
- export function useFocusContext() {
6
- return useContext(FocusContext);
7
- }
8
- export function FocusProvider(props) {
9
- const itemsRef = useRef(new Map());
10
- const [focusedOrder, setFocusedOrderState] = useState(null);
11
- const [active, setActive] = useState(true);
12
- useEffect(() => {
13
- if (focusedOrder === null && itemsRef.current.size > 0) {
14
- const minOrder = Math.min(...itemsRef.current.keys());
15
- setFocusedOrderState(minOrder);
16
- }
17
- }, [itemsRef.current.size]);
18
- const registerItem = useCallback((order, id) => {
19
- itemsRef.current.set(order, { order, id });
20
- if (focusedOrder === null) {
21
- setFocusedOrderState(order);
22
- }
23
- }, [focusedOrder]);
24
- const unregisterItem = useCallback((id) => {
25
- for (const [order, entry] of itemsRef.current) {
26
- if (entry.id === id) {
27
- itemsRef.current.delete(order);
28
- break;
29
- }
30
- }
31
- }, []);
32
- const setFocusedOrder = useCallback((order) => {
33
- setFocusedOrderState(order);
34
- }, []);
35
- const isFocused = useCallback((order) => {
36
- return focusedOrder === order;
37
- }, [focusedOrder]);
38
- const focusNext = useCallback(() => {
39
- const orders = Array.from(itemsRef.current.keys()).sort((a, b) => a - b);
40
- if (orders.length === 0)
41
- return;
42
- const currentIdx = orders.indexOf(focusedOrder ?? orders[0]);
43
- const nextIdx = (currentIdx + 1) % orders.length;
44
- setFocusedOrderState(orders[nextIdx]);
45
- }, [focusedOrder]);
46
- const focusPrev = useCallback(() => {
47
- const orders = Array.from(itemsRef.current.keys()).sort((a, b) => a - b);
48
- if (orders.length === 0)
49
- return;
50
- const currentIdx = orders.indexOf(focusedOrder ?? orders[0]);
51
- const prevIdx = (currentIdx - 1 + orders.length) % orders.length;
52
- setFocusedOrderState(orders[prevIdx]);
53
- }, [focusedOrder]);
54
- useKeyboard((key) => {
55
- if (!active)
56
- return;
57
- if (key.name === "tab") {
58
- if (key.shift) {
59
- focusPrev();
60
- }
61
- else {
62
- focusNext();
63
- }
64
- key.preventDefault();
65
- }
66
- });
67
- const value = {
68
- registerItem,
69
- unregisterItem,
70
- focusedOrder,
71
- setFocusedOrder,
72
- isFocused,
73
- focusNext,
74
- focusPrev,
75
- active,
76
- };
77
- return _jsx(FocusContext.Provider, { value: value, children: props.children });
78
- }
79
- export function useFocusable(order, id) {
80
- const ctx = useContext(FocusContext);
81
- const registered = useRef(false);
82
- useEffect(() => {
83
- if (!ctx)
84
- return;
85
- ctx.registerItem(order, id);
86
- registered.current = true;
87
- return () => {
88
- ctx.unregisterItem(id);
89
- };
90
- }, [ctx, order, id]);
91
- if (!ctx) {
92
- return { isFocused: false, setFocused: () => { } };
93
- }
94
- return {
95
- isFocused: ctx.isFocused(order),
96
- setFocused: () => ctx.setFocusedOrder(order),
97
- };
98
- }