loop-task 1.4.4 → 1.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/board/App.js CHANGED
@@ -310,7 +310,9 @@ export function App(props) {
310
310
  refreshTasks,
311
311
  onViewTasks: () => { void refreshTasks(); push("task-list"); },
312
312
  onViewProjects: () => push("projects"),
313
+ onViewLoops: () => replace("board"),
313
314
  onAddLoop: () => { setEditTarget(null); push("create"); },
315
+ onAddTask: () => { setEditTask(null); push("task-create"); },
314
316
  onSelectProject: () => setProjectsModalOpen(true),
315
317
  });
316
318
  useTaskKeybindings({
@@ -330,6 +332,9 @@ export function App(props) {
330
332
  onTaskAction: handleTaskAction,
331
333
  onCancel: cancelTaskList,
332
334
  onCreateTask: handleCreateTask,
335
+ onEnterHeader: (direction) => {
336
+ setFocusedPanel(direction === "right" ? "header-tasks" : "header-new");
337
+ },
333
338
  selectable: stack.includes("create") || stack.includes("task-edit"),
334
339
  });
335
340
  const counts = {
@@ -345,7 +350,9 @@ export function App(props) {
345
350
  }
346
351
  else {
347
352
  push("projects");
348
- } } }), view === "board" ? (_jsx(FilterBar, { filters: filters, sort: sort, searchActive: searchActive, focusedPanel: focusedPanel, onStatusCycle: () => setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) })), onSortCycle: () => setSort(cycleSortMode(sort)), onSelectProject: () => setProjectsModalOpen(true), currentProjectName: currentProjectId === "all" ? t("project.showAll") : (projects.find(p => p.id === currentProjectId)?.name ?? "Default"), onQueryChange: (value) => setFilters((prev) => ({ ...prev, query: value })), onSearchActivate: () => setSearchActive(true), onSearchDismiss: () => { setSearchActive(false); setFocusedPanel("loops"); } })) : view === "task-list" ? (_jsx(TaskFilterBar, { query: taskQuery, searchActive: taskSearchActive, focusedPanel: taskFocusedPanel, onQueryChange: setTaskQuery, onSearchActivate: () => setTaskSearchActive(true), onSearchDismiss: () => { setTaskSearchActive(false); setTaskFocusedPanel("tasks"); } })) : null, _jsx("box", { style: { flexGrow: 1, backgroundColor: "#0b0b0b" }, children: view === "create" ? (_jsx(CreateView, { mode: editTarget && !cloneMode ? "edit" : "create", editId: editTarget && !cloneMode ? editTarget.id : null, initial: createInitialValues(editTarget, currentProjectId), selectedTaskId: pendingTaskSelection?.id ?? null, selectedTaskName: pendingTaskSelection?.name ?? null, projects: projects, currentProjectId: currentProjectId, onCancel: cancelCreate, onDone: onCreateDone, onChooseTask: handleChooseTask })) : TASK_FORM_VIEWS.has(view) ? (_jsx(TaskForm, { mode: view === "task-edit" ? "edit" : "create", editTask: editTask, onCancel: cancelTask, onDone: onTaskDone })) : view === "task-list" ? (_jsxs("box", { style: { flexDirection: breakpoint === "narrow" ? "column" : "row", flexGrow: 1, backgroundColor: "#0b0b0b" }, children: [_jsx(TaskNavigator, { visible: filteredTasks, total: tasks.length, selectedIndex: taskClampedIndex, focused: taskFocusedPanel === "tasks", query: taskQuery, onSelect: (index) => { setTaskSelectedIndex(index); setTaskFocusedPanel("tasks"); }, onActivate: (index) => { setTaskSelectedIndex(index); setEditTask(filteredTasks[index] ?? null); push("task-edit"); } }), _jsxs("box", { style: { flexDirection: "column", flexGrow: 1, backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx(TaskInspector, { task: selectedTask }, `ti-${selectedTask?.id}`), _jsx(TaskActionButtons, { task: selectedTask, focused: taskFocusedPanel === "actions", selectedAction: taskSelectedAction, selectable: stack.includes("create") || stack.includes("task-edit"), onAction: handleTaskAction }, `tab-${selectedTask?.id}`)] })] })) : view === "projects" ? (_jsx(ProjectsPage, { projects: projects, loops: loops, onClose: () => pop(), onRefresh: refreshProjects, onOpenCreate: (trigger) => { createProjectTriggerRef.current = trigger; } })) : (_jsxs("box", { style: { flexDirection: breakpoint === "narrow" ? "column" : "row", flexGrow: 1, backgroundColor: "#0b0b0b" }, children: [_jsx(Navigator, { visible: visible, total: loops.length, selectedIndex: clampedIndex, filters: filters, sort: sort, breakpoint: breakpoint, focused: focusedPanel === "loops", projects: projects, onSelect: (index) => { setSelectedIndex(index); setFocusedPanel("loops"); }, onActivate: (index) => { setSelectedIndex(index); const loop = visible[index]; if (loop) {
353
+ } } }), view === "board" ? (_jsx(FilterBar, { filters: filters, sort: sort, searchActive: searchActive, focusedPanel: focusedPanel, onStatusCycle: () => setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) })), onSortCycle: () => setSort(cycleSortMode(sort)), onSelectProject: () => setProjectsModalOpen(true), currentProjectName: currentProjectId === "all" ? t("project.showAll") : (projects.find(p => p.id === currentProjectId)?.name ?? "Default"), onQueryChange: (value) => setFilters((prev) => ({ ...prev, query: value })), onSearchActivate: () => setSearchActive(true), onSearchDismiss: () => { setSearchActive(false); setFocusedPanel("loops"); } })) : view === "task-list" ? (_jsx(TaskFilterBar, { query: taskQuery, searchActive: taskSearchActive, focusedPanel: taskFocusedPanel, onQueryChange: setTaskQuery, onSearchActivate: () => setTaskSearchActive(true), onSearchDismiss: () => { setTaskSearchActive(false); setTaskFocusedPanel("tasks"); } })) : null, _jsx("box", { style: { flexGrow: 1, backgroundColor: "#0b0b0b" }, children: view === "create" ? (_jsx(CreateView, { mode: editTarget && !cloneMode ? "edit" : "create", editId: editTarget && !cloneMode ? editTarget.id : null, initial: createInitialValues(editTarget, currentProjectId), selectedTaskId: pendingTaskSelection?.id ?? null, selectedTaskName: pendingTaskSelection?.name ?? null, projects: projects, currentProjectId: currentProjectId, onCancel: cancelCreate, onDone: onCreateDone, onChooseTask: handleChooseTask })) : TASK_FORM_VIEWS.has(view) ? (_jsx(TaskForm, { mode: view === "task-edit" ? "edit" : "create", editTask: editTask, onCancel: cancelTask, onDone: onTaskDone })) : view === "task-list" ? (_jsxs("box", { style: { flexDirection: breakpoint === "narrow" ? "column" : "row", flexGrow: 1, backgroundColor: "#0b0b0b" }, children: [_jsx(TaskNavigator, { visible: filteredTasks, total: tasks.length, selectedIndex: taskClampedIndex, focused: taskFocusedPanel === "tasks", query: taskQuery, onSelect: (index) => { setTaskSelectedIndex(index); setTaskFocusedPanel("tasks"); }, onActivate: (index) => { setTaskSelectedIndex(index); setEditTask(filteredTasks[index] ?? null); push("task-edit"); } }), _jsxs("box", { style: { flexDirection: "column", flexGrow: 1, backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx(TaskInspector, { task: selectedTask }, `ti-${selectedTask?.id}`), _jsx(TaskActionButtons, { task: selectedTask, focused: taskFocusedPanel === "actions", selectedAction: taskSelectedAction, selectable: stack.includes("create") || stack.includes("task-edit"), onAction: handleTaskAction }, `tab-${selectedTask?.id}`)] })] })) : view === "projects" ? (_jsx(ProjectsPage, { projects: projects, loops: loops, onClose: () => pop(), onRefresh: refreshProjects, onOpenCreate: (trigger) => { createProjectTriggerRef.current = trigger; }, onEnterHeader: (direction) => {
354
+ setFocusedPanel(direction === "right" ? "header-tasks" : "header-new");
355
+ } })) : (_jsxs("box", { style: { flexDirection: breakpoint === "narrow" ? "column" : "row", flexGrow: 1, backgroundColor: "#0b0b0b" }, children: [_jsx(Navigator, { visible: visible, total: loops.length, selectedIndex: clampedIndex, filters: filters, sort: sort, breakpoint: breakpoint, focused: focusedPanel === "loops", projects: projects, onSelect: (index) => { setSelectedIndex(index); setFocusedPanel("loops"); }, onActivate: (index) => { setSelectedIndex(index); const loop = visible[index]; if (loop) {
349
356
  setEditTarget(loop);
350
357
  push("create");
351
358
  } } }), _jsxs("box", { style: { flexDirection: "column", flexGrow: 1, backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx(Inspector, { loop: selected }), _jsx(RunHistory, { loop: selected, selectedRunIndex: selectedRunIndex, focused: focusedPanel === "runs", onSelectRun: (index) => { setSelectedRunIndex(index); setFocusedPanel("runs"); }, onOpenRun: handleOpenRunLog }), _jsx(ActionButtons, { loop: selected, focused: focusedPanel === "actions", selectedAction: selectedAction, onAction: handleAction })] })] })) }, viewKey(view, editTarget, editTask)), _jsx(Footer, { mode: mode }), confirm ? (_jsx(ConfirmModal, { message: confirm.message, choice: confirmChoice, onYes: () => { const action = confirm.action; setConfirm(null); void action(); }, onNo: () => setConfirm(null) })) : null, helpOpen ? _jsx(HelpModal, { view: view }) : null, logModalRun ? (_jsx(LogModal, { loopId: selectedId, run: logModalRun, logLines: logModalLines, loading: logModalLoading, onClose: () => setLogModalRun(null) })) : null, projectsModalOpen ? (_jsx(ProjectsModal, { projects: projects, loops: loops, currentProjectId: currentProjectId, onSelect: (id) => { setCurrentProjectId(id); setProjectsModalOpen(false); }, onClose: () => setProjectsModalOpen(false) })) : null, _jsx(ToastStack, { toasts: toasts })] }));
@@ -7,6 +7,7 @@ import { t } from "../../i18n/index.js";
7
7
  import { commandLine } from "../format.js";
8
8
  import { createLoop, updateLoop, listTasks } from "../daemon.js";
9
9
  import { useHoverState } from "../hooks/useHoverState.js";
10
+ import { useInputShortcuts } from "../hooks/useInputShortcuts.js";
10
11
  import { HOVER_BG } from "../../config/constants.js";
11
12
  export const TASK_MODE_INLINE = "inline";
12
13
  export const TASK_MODE_EXISTING = "existing";
@@ -47,6 +48,12 @@ export function CreateView(props) {
47
48
  const [error, setError] = useState("");
48
49
  const [isSubmitting, setIsSubmitting] = useState(false);
49
50
  const [selectedTaskName, setSelectedTaskName] = useState(props.selectedTaskName ?? null);
51
+ const inputRef = useRef(null);
52
+ useInputShortcuts(() => {
53
+ if (focusIndex >= saveIndex)
54
+ return null;
55
+ return inputRef.current;
56
+ });
50
57
  useState(() => {
51
58
  if (values.taskId && !selectedTaskName) {
52
59
  void listTasks().then((tasks) => {
@@ -271,7 +278,7 @@ export function CreateView(props) {
271
278
  return (_jsxs("box", { title: title, border: true, style: { flexDirection: "column", flexGrow: 1, padding: 1, backgroundColor: "#0b0b0b" }, children: [_jsxs("box", { style: { flexDirection: "column", marginBottom: 1 }, children: [_jsx("text", { fg: "#9ca3af", children: t("board.exampleHeading") }), _jsx("text", { fg: "#d1d5db", children: t("board.exampleFull") })] }), _jsx("box", { style: { flexDirection: "column", flexGrow: 1 }, children: Array.from({ length: Math.ceil(filteredFields.length / 2) }, (_, row) => {
272
279
  const leftField = filteredFields[row * 2];
273
280
  const rightField = row * 2 + 1 < filteredFields.length ? filteredFields[row * 2 + 1] : null;
274
- return (_jsxs("box", { style: { flexDirection: "row", marginBottom: 1 }, children: [_jsx(FormRow, { field: leftField, index: row * 2, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, examples: examples, taskModeOptions: taskModeOptions, runNowOptions: runNowOptions, projectOptions: projectOptions, selectedTaskName: selectedTaskName, fields: filteredFields, onChooseTask: props.onChooseTask, style: { width: "50%", paddingRight: 1 } }), rightField ? (_jsx(FormRow, { field: rightField, index: row * 2 + 1, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, examples: examples, taskModeOptions: taskModeOptions, runNowOptions: runNowOptions, projectOptions: projectOptions, selectedTaskName: selectedTaskName, fields: filteredFields, onChooseTask: props.onChooseTask, style: { width: "50%" } })) : (_jsx("box", { style: { width: "50%" } }))] }, row));
281
+ return (_jsxs("box", { style: { flexDirection: "row", marginBottom: 1 }, children: [_jsx(FormRow, { field: leftField, index: row * 2, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, examples: examples, taskModeOptions: taskModeOptions, runNowOptions: runNowOptions, projectOptions: projectOptions, selectedTaskName: selectedTaskName, fields: filteredFields, onChooseTask: props.onChooseTask, inputRef: inputRef, style: { width: "50%", paddingRight: 1 } }), rightField ? (_jsx(FormRow, { field: rightField, index: row * 2 + 1, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, examples: examples, taskModeOptions: taskModeOptions, runNowOptions: runNowOptions, projectOptions: projectOptions, selectedTaskName: selectedTaskName, fields: filteredFields, onChooseTask: props.onChooseTask, inputRef: inputRef, style: { width: "50%" } })) : (_jsx("box", { style: { width: "50%" } }))] }, row));
275
282
  }) }), _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: focusIndex === saveIndex, width: btnWidth, marginRight: 1 }), _jsx(HoverButton, { label: t("board.cancel"), onMouseDown: props.onCancel, selected: focusIndex === cancelIndex, width: btnWidth })] }), _jsx("text", { fg: "#9ca3af", children: t("board.formNav") }), error ? _jsx("text", { fg: "#f87171", children: error }) : null] }));
276
283
  }
277
284
  function HoverButton(props) {
@@ -281,7 +288,7 @@ function HoverButton(props) {
281
288
  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 }) }) }));
282
289
  }
283
290
  function FormRow(props) {
284
- const { field, index, focusIndex, values, valuesRef, updateValues, setFocusIndex, submit, labels, hints, examples, taskModeOptions, runNowOptions, projectOptions, selectedTaskName, fields, onChooseTask, style } = props;
291
+ const { field, index, focusIndex, values, valuesRef, updateValues, setFocusIndex, submit, labels, hints, examples, taskModeOptions, runNowOptions, projectOptions, selectedTaskName, fields, onChooseTask, inputRef, style } = props;
285
292
  const { isHovered, hoverProps } = useHoverState();
286
293
  const isFocused = focusIndex === index;
287
294
  const isToggleField = field === "taskMode" || field === "runNow";
@@ -297,7 +304,7 @@ function FormRow(props) {
297
304
  : t("board.chooseTask") }) })) : isProjectField ? (_jsx("box", { border: true, borderColor: isFocused ? "#38bdf8" : undefined, style: { height: 3, flexDirection: "row", backgroundColor: "#0b0b0b", alignItems: "center", paddingLeft: 1 }, onMouseDown: () => setFocusIndex(index), children: projectOptions.map((opt) => {
298
305
  const isActive = values.project === opt.value;
299
306
  return (_jsx("box", { onMouseDown: () => updateValues({ ...valuesRef.current, project: opt.value }), style: { backgroundColor: isActive ? "#1e3a8a" : undefined, paddingLeft: 1, paddingRight: 1, marginRight: 1 }, children: _jsx("text", { fg: isActive ? "#ffffff" : "#9ca3af", children: `● ${opt.name}` }) }, opt.value));
300
- }) })) : (_jsx("box", { border: true, borderColor: isFocused ? "#38bdf8" : undefined, style: { height: 3, backgroundColor: "#0b0b0b" }, children: _jsx("input", { focused: isFocused, value: values[field], placeholder: examples[field] ? t("board.placeholderExample", { example: examples[field] }) : t("board.placeholderBlank"), onInput: (value) => updateValues({ ...valuesRef.current, [field]: value }), onSubmit: () => {
307
+ }) })) : (_jsx("box", { border: true, borderColor: isFocused ? "#38bdf8" : undefined, style: { height: 3, backgroundColor: "#0b0b0b" }, children: _jsx("input", { ref: inputRef, focused: isFocused, value: values[field], placeholder: examples[field] ? t("board.placeholderExample", { example: examples[field] }) : t("board.placeholderBlank"), onInput: (value) => updateValues({ ...valuesRef.current, [field]: value }), onSubmit: () => {
301
308
  if (index < fields.length - 1) {
302
309
  setFocusIndex(index + 1);
303
310
  }
@@ -5,8 +5,8 @@ export function HelpModal(props) {
5
5
  const { view } = props;
6
6
  const { width } = useTerminalDimensions();
7
7
  const loopRows = [
8
- ["tab/←→", "switch panels"],
9
- ["↑/↓", "navigate list items"],
8
+ ["tab", "switch panels"],
9
+ ["up/down", "navigate list items"],
10
10
  ["enter", t("board.helpEdit")],
11
11
  ["e", "edit loop"],
12
12
  ["d/del", "delete loop"],
@@ -24,8 +24,8 @@ export function HelpModal(props) {
24
24
  ["esc", t("board.helpQuit")],
25
25
  ];
26
26
  const taskRows = [
27
- ["tab/←→", "switch panels"],
28
- ["↑/↓", "navigate tasks"],
27
+ ["tab", "switch panels"],
28
+ ["up/down", "navigate tasks"],
29
29
  ["enter", "focus actions panel"],
30
30
  ["e", "edit task"],
31
31
  ["d/del", "delete task"],
@@ -36,8 +36,8 @@ export function HelpModal(props) {
36
36
  ["esc", t("board.helpQuit")],
37
37
  ];
38
38
  const projectRows = [
39
- ["tab/←→", "switch panel (list/actions)"],
40
- ["↑/↓", "navigate projects"],
39
+ ["tab", "switch panel (list/actions)"],
40
+ ["up/down", "navigate projects"],
41
41
  ["enter", "focus actions"],
42
42
  ["n", "new project"],
43
43
  ["e", "edit project"],
@@ -6,12 +6,31 @@ import { formatRunDuration, formatRunTime } from "../format.js";
6
6
  import { streamRunLog } from "../daemon.js";
7
7
  import { LOG_LINES_MAX } from "../../config/constants.js";
8
8
  import { copyToClipboard } from "../../shared/clipboard.js";
9
+ function colorizeLine(line) {
10
+ if (/^\[Run #/.test(line)) {
11
+ return _jsx("text", { fg: "#38bdf8", children: _jsx("strong", { children: line }) });
12
+ }
13
+ if (/^--- Chain:/.test(line)) {
14
+ return _jsx("text", { fg: "#a78bfa", children: _jsx("strong", { children: line }) });
15
+ }
16
+ if (/^\[exit 0/.test(line)) {
17
+ return _jsx("text", { fg: "#4ade80", children: line });
18
+ }
19
+ if (/^\[exit /.test(line)) {
20
+ return _jsx("text", { fg: "#f87171", children: line });
21
+ }
22
+ if (/^\[error\]/.test(line)) {
23
+ return _jsx("text", { fg: "#f87171", children: line });
24
+ }
25
+ return _jsx("text", { fg: "#e5e7eb", children: line });
26
+ }
9
27
  export function LogModal(props) {
10
28
  const { loopId, run, logLines: staticLines, loading } = props;
11
29
  const { width, height } = useTerminalDimensions();
12
30
  const isRunning = run.status === "running";
13
31
  const [streamLines, setStreamLines] = useState([]);
14
32
  const socketRef = useRef(null);
33
+ const scrollRef = useRef(null);
15
34
  useEffect(() => {
16
35
  if (!isRunning || !loopId)
17
36
  return;
@@ -28,13 +47,18 @@ export function LogModal(props) {
28
47
  }
29
48
  };
30
49
  }, [isRunning, loopId, run.runNumber]);
50
+ const logLines = isRunning ? streamLines : staticLines;
51
+ useEffect(() => {
52
+ if (logLines.length > 0) {
53
+ scrollRef.current?.scrollChildIntoView(`log-line-${logLines.length - 1}`);
54
+ }
55
+ }, [logLines]);
31
56
  useKeyboard((key) => {
32
57
  if (key.ctrl && key.name === "c") {
33
- const all = (isRunning ? streamLines : staticLines).join("\n");
58
+ const all = logLines.join("\n");
34
59
  copyToClipboard(all);
35
60
  }
36
61
  });
37
- const logLines = isRunning ? streamLines : staticLines;
38
62
  const success = isRunning ? true : run.exitCode === 0;
39
63
  const icon = isRunning ? "⟳" : success ? "✓" : "✗";
40
64
  const iconColor = isRunning ? "#facc15" : success ? "#4ade80" : "#f87171";
@@ -54,5 +78,5 @@ export function LogModal(props) {
54
78
  width: Math.min(width - 4, 100),
55
79
  height: Math.min(height - 4, 30),
56
80
  backgroundColor: "#111827",
57
- }, children: [_jsx("scrollbox", { style: { flexGrow: 1, backgroundColor: "#0b0b0b" }, children: loading && !isRunning ? (_jsx("text", { fg: "#9ca3af", children: t("board.logModalLoading") })) : logLines.length === 0 ? (_jsx("text", { fg: "#9ca3af", children: t("board.logModalEmpty") })) : (logLines.map((line, i) => _jsx("text", { children: line }, i))) }), _jsxs("box", { style: { flexDirection: "row", justifyContent: "space-between", backgroundColor: "#111827" }, children: [_jsxs("text", { children: [_jsx("span", { fg: iconColor, children: icon }), " ", isRunning ? (_jsx("span", { fg: "#facc15", children: t("board.logModalRunning") })) : (_jsxs(_Fragment, { children: [_jsx("span", { fg: "#9ca3af", children: formatRunDuration(run.duration) }), " ", _jsxs("span", { fg: success ? "#4ade80" : "#f87171", children: ["exit ", run.exitCode] })] }))] }), _jsx("text", { fg: "#6b7280", children: t("board.logModalEscClose") })] })] }) }));
81
+ }, children: [_jsx("scrollbox", { ref: scrollRef, style: { flexGrow: 1, backgroundColor: "#0b0b0b" }, children: loading && !isRunning ? (_jsx("text", { fg: "#9ca3af", children: t("board.logModalLoading") })) : logLines.length === 0 ? (_jsx("text", { fg: "#9ca3af", children: t("board.logModalEmpty") })) : (logLines.map((line, i) => (_jsx("box", { id: `log-line-${i}`, children: colorizeLine(line) }, i)))) }), _jsxs("box", { style: { flexDirection: "row", justifyContent: "space-between", backgroundColor: "#111827" }, children: [_jsxs("text", { children: [_jsx("span", { fg: iconColor, children: icon }), " ", isRunning ? (_jsx("span", { fg: "#facc15", children: t("board.logModalRunning") })) : (_jsxs(_Fragment, { children: [_jsx("span", { fg: "#9ca3af", children: formatRunDuration(run.duration) }), " ", _jsxs("span", { fg: success ? "#4ade80" : "#f87171", children: ["exit ", run.exitCode] })] }))] }), _jsx("text", { fg: "#6b7280", children: t("board.logModalEscClose") })] })] }) }));
58
82
  }
@@ -15,7 +15,7 @@ function ProjectActionButton(props) {
15
15
  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 }) }) }));
16
16
  }
17
17
  export function ProjectsPage(props) {
18
- const { projects, loops, onClose, onRefresh, onOpenCreate } = props;
18
+ const { projects, loops, onClose, onRefresh, onOpenCreate, onEnterHeader } = props;
19
19
  const [selectedIndex, setSelectedIndex] = useState(0);
20
20
  const [subModal, setSubModal] = useState("none");
21
21
  const [focusedPanel, setFocusedPanel] = useState("list");
@@ -52,7 +52,18 @@ export function ProjectsPage(props) {
52
52
  key.preventDefault();
53
53
  return;
54
54
  }
55
- if (key.name === "left" || key.name === "right" || key.name === "tab") {
55
+ if (key.name === "tab") {
56
+ const direction = key.shift ? "left" : "right";
57
+ if (focusedPanel === "list" && direction === "left") {
58
+ onEnterHeader?.("left");
59
+ key.preventDefault();
60
+ return;
61
+ }
62
+ if (focusedPanel === "actions" && direction === "right") {
63
+ onEnterHeader?.("right");
64
+ key.preventDefault();
65
+ return;
66
+ }
56
67
  setFocusedPanel((p) => (p === "list" ? "actions" : "list"));
57
68
  key.preventDefault();
58
69
  return;
@@ -1,6 +1,10 @@
1
1
  import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ import { useRef } from "react";
2
3
  import { t } from "../../i18n/index.js";
4
+ import { useInputShortcuts } from "../hooks/useInputShortcuts.js";
3
5
  export function SearchBox(props) {
4
6
  const { query, searchActive, focused, onQueryChange, onActivate, onDismiss } = props;
5
- return (_jsx("box", { title: t("board.searchTitle"), border: true, borderColor: focused ? "#38bdf8" : undefined, style: { width: "25%", height: 3, marginRight: 1, paddingLeft: 1, backgroundColor: focused ? "#1e2a4a" : "#0b0b0b" }, children: searchActive ? (_jsx("input", { focused: true, value: query, placeholder: t("board.searchPlaceholder"), onInput: (value) => onQueryChange(value), onSubmit: () => onDismiss() })) : (_jsx("text", { fg: query ? "#e5e7eb" : "#6b7280", onMouseDown: onActivate, children: query || t("board.searchEmpty") })) }));
7
+ const inputRef = useRef(null);
8
+ useInputShortcuts(() => searchActive ? inputRef.current : null);
9
+ return (_jsx("box", { title: t("board.searchTitle"), border: true, borderColor: focused ? "#38bdf8" : undefined, style: { width: "25%", height: 3, marginRight: 1, paddingLeft: 1, backgroundColor: focused ? "#1e2a4a" : "#0b0b0b" }, children: searchActive ? (_jsx("input", { ref: inputRef, focused: true, value: query, placeholder: t("board.searchPlaceholder"), onInput: (value) => onQueryChange(value), onSubmit: () => onDismiss() })) : (_jsx("text", { fg: query ? "#e5e7eb" : "#6b7280", onMouseDown: onActivate, children: query || t("board.searchEmpty") })) }));
6
10
  }
@@ -5,6 +5,7 @@ import { useKeyboard, useTerminalDimensions } from "@opentui/react";
5
5
  import { t } from "../../i18n/index.js";
6
6
  import { createTask, updateTask, listTasks } from "../daemon.js";
7
7
  import { useHoverState } from "../hooks/useHoverState.js";
8
+ import { useInputShortcuts } from "../hooks/useInputShortcuts.js";
8
9
  import { HOVER_BG } from "../../config/constants.js";
9
10
  const taskFields = ["name", "command", "onSuccessTaskId", "onFailureTaskId"];
10
11
  function taskInitialValues(task) {
@@ -25,8 +26,14 @@ export function TaskForm(props) {
25
26
  const [error, setError] = useState("");
26
27
  const [isSubmitting, setIsSubmitting] = useState(false);
27
28
  const [allTasks, setAllTasks] = useState([]);
29
+ const inputRef = useRef(null);
28
30
  const { width: termWidth } = useTerminalDimensions();
29
31
  const btnWidth = Math.max(10, Math.min(14, Math.floor(termWidth / 6)));
32
+ useInputShortcuts(() => {
33
+ if (focusIndex >= saveIndex)
34
+ return null;
35
+ return inputRef.current;
36
+ });
30
37
  const saveIndex = taskFields.length;
31
38
  const cancelIndex = taskFields.length + 1;
32
39
  function updateValues(next) {
@@ -140,16 +147,16 @@ export function TaskForm(props) {
140
147
  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) => {
141
148
  const leftField = taskFields[row * 2];
142
149
  const rightField = row * 2 + 1 < taskFields.length ? taskFields[row * 2 + 1] : null;
143
- return (_jsxs("box", { style: { flexDirection: "row", marginBottom: 1 }, children: [_jsx(TaskFormRow, { field: leftField, index: row * 2, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, chainOptions: chainOptions, style: { width: "50%", paddingRight: 1 } }), rightField ? (_jsx(TaskFormRow, { field: rightField, index: row * 2 + 1, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, chainOptions: chainOptions, style: { width: "50%" } })) : (_jsx("box", { style: { width: "50%" } }))] }, row));
150
+ return (_jsxs("box", { style: { flexDirection: "row", marginBottom: 1 }, children: [_jsx(TaskFormRow, { field: leftField, index: row * 2, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, chainOptions: chainOptions, inputRef: inputRef, style: { width: "50%", paddingRight: 1 } }), rightField ? (_jsx(TaskFormRow, { field: rightField, index: row * 2 + 1, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, chainOptions: chainOptions, inputRef: inputRef, style: { width: "50%" } })) : (_jsx("box", { style: { width: "50%" } }))] }, row));
144
151
  }) }), _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: focusIndex === saveIndex, width: btnWidth, marginRight: 1 }), _jsx(HoverButton, { label: t("board.cancel"), onMouseDown: props.onCancel, selected: focusIndex === cancelIndex, width: btnWidth })] }), _jsx("text", { fg: "#9ca3af", children: t("board.formNav") }), error ? _jsx("text", { fg: "#f87171", children: error }) : null] }));
145
152
  }
146
153
  function TaskFormRow(props) {
147
- const { field, index, focusIndex, values, valuesRef, updateValues, setFocusIndex, submit, labels, hints, chainOptions, style } = props;
154
+ const { field, index, focusIndex, values, valuesRef, updateValues, setFocusIndex, submit, labels, hints, chainOptions, inputRef, style } = props;
148
155
  const isFocused = focusIndex === index;
149
156
  const isSelect = field === "onSuccessTaskId" || field === "onFailureTaskId";
150
157
  const selectOpts = isSelect ? chainOptions : [];
151
158
  const selectedIdx = isSelect ? selectOpts.findIndex((o) => o.value === values[field]) : 0;
152
- return (_jsxs("box", { style: { flexDirection: "column", ...style }, children: [_jsx("text", { fg: isFocused ? "#38bdf8" : "#e5e7eb", children: labels[field] }), _jsx("text", { fg: "#6b7280", children: hints[field] }), isSelect ? (_jsx("box", { border: true, borderColor: isFocused ? "#38bdf8" : undefined, style: { height: Math.min(selectOpts.length + 2, 6), backgroundColor: "#0b0b0b" }, children: _jsx("select", { focused: isFocused, options: selectOpts, selectedIndex: Math.max(0, selectedIdx), showDescription: false, style: { flexGrow: 1 }, onChange: (_i, option) => updateValues({ ...valuesRef.current, [field]: option?.value ?? "" }) }) })) : (_jsx("box", { border: true, borderColor: isFocused ? "#38bdf8" : undefined, style: { height: 3, backgroundColor: "#0b0b0b" }, children: _jsx("input", { focused: isFocused, value: values[field], placeholder: field === "command" ? t("board.exampleCommand") : "", onInput: (value) => updateValues({ ...valuesRef.current, [field]: value }), onSubmit: () => {
159
+ return (_jsxs("box", { style: { flexDirection: "column", ...style }, children: [_jsx("text", { fg: isFocused ? "#38bdf8" : "#e5e7eb", children: labels[field] }), _jsx("text", { fg: "#6b7280", children: hints[field] }), isSelect ? (_jsx("box", { border: true, borderColor: isFocused ? "#38bdf8" : undefined, style: { height: Math.min(selectOpts.length + 2, 6), backgroundColor: "#0b0b0b" }, children: _jsx("select", { focused: isFocused, options: selectOpts, selectedIndex: Math.max(0, selectedIdx), showDescription: false, style: { flexGrow: 1 }, onChange: (_i, option) => updateValues({ ...valuesRef.current, [field]: option?.value ?? "" }) }) })) : (_jsx("box", { border: true, borderColor: isFocused ? "#38bdf8" : undefined, style: { height: 3, backgroundColor: "#0b0b0b" }, children: _jsx("input", { ref: inputRef, focused: isFocused, value: values[field], placeholder: field === "command" ? t("board.exampleCommand") : "", onInput: (value) => updateValues({ ...valuesRef.current, [field]: value }), onSubmit: () => {
153
160
  if (index < taskFields.length - 1) {
154
161
  setFocusIndex(index + 1);
155
162
  }
@@ -195,7 +195,7 @@ const BOARD_SHORTCUTS = {
195
195
  r: (p) => p.onSelectProject?.(),
196
196
  };
197
197
  export function useBoardKeybindings(params) {
198
- const { confirm, confirmChoice, setConfirm, setConfirmChoice, helpOpen, setHelpOpen, searchActive, setSearchActive, view, push, pop, setEditTarget, setEditTask, selected, visibleCount, setSelectedIndex, setFilters, setSort, onQuit, destroyLogSocket, logModalRun, setLogModalRun, logModalLines, selectedRunIndex, setSelectedRunIndex, selectedRunCount, focusedPanel, setFocusedPanel, selectedAction, setSelectedAction, onAction, onOpenRunLog, refreshTasks, onViewTasks, onViewProjects, onAddLoop, onSelectProject, } = params;
198
+ const { confirm, confirmChoice, setConfirm, setConfirmChoice, helpOpen, setHelpOpen, searchActive, setSearchActive, view, push, pop, setEditTarget, setEditTask, selected, visibleCount, setSelectedIndex, setFilters, setSort, onQuit, destroyLogSocket, logModalRun, setLogModalRun, logModalLines, selectedRunIndex, setSelectedRunIndex, selectedRunCount, focusedPanel, setFocusedPanel, selectedAction, setSelectedAction, onAction, onOpenRunLog, refreshTasks, onViewTasks, onViewProjects, onViewLoops, onAddLoop, onAddTask, onSelectProject, } = params;
199
199
  useKeyboard((key) => {
200
200
  const name = key.name;
201
201
  if (confirm) {
@@ -230,6 +230,19 @@ export function useBoardKeybindings(params) {
230
230
  key.preventDefault();
231
231
  return;
232
232
  }
233
+ if (name === "up" || name === "k" || name === "down" || name === "j") {
234
+ const handler = panelHandlers["loops"];
235
+ if (handler) {
236
+ handler(name, {
237
+ setSearchActive, setFilters, setSort, setEditTarget, push,
238
+ setSelectedIndex, setSelectedRunIndex, setFocusedPanel, selectedRunCount, selected,
239
+ selectedRunIndex, setSelectedAction, selectedAction, onAction, onOpenRunLog,
240
+ refreshTasks, onViewTasks, onViewProjects, onAddLoop,
241
+ });
242
+ key.preventDefault();
243
+ }
244
+ return;
245
+ }
233
246
  return;
234
247
  }
235
248
  if (view !== "board" && name === "escape") {
@@ -237,11 +250,78 @@ export function useBoardKeybindings(params) {
237
250
  key.preventDefault();
238
251
  return;
239
252
  }
253
+ if (name === "tab" && view !== "board") {
254
+ const HEADER_PANELS = ["header-tasks", "header-projects", "header-new"];
255
+ const isHeader = HEADER_PANELS.includes(focusedPanel);
256
+ const direction = key.shift ? "left" : "right";
257
+ if (isHeader) {
258
+ const idx = HEADER_PANELS.indexOf(focusedPanel);
259
+ const nextIdx = direction === "right"
260
+ ? (idx + 1) % HEADER_PANELS.length
261
+ : (idx - 1 + HEADER_PANELS.length) % HEADER_PANELS.length;
262
+ setFocusedPanel(HEADER_PANELS[nextIdx]);
263
+ key.preventDefault();
264
+ return;
265
+ }
266
+ }
267
+ if (view !== "board" && (name === "return" || name === "enter")) {
268
+ if (view === "task-list") {
269
+ if (focusedPanel === "header-tasks") {
270
+ onViewProjects?.();
271
+ key.preventDefault();
272
+ return;
273
+ }
274
+ if (focusedPanel === "header-projects") {
275
+ onViewLoops?.();
276
+ key.preventDefault();
277
+ return;
278
+ }
279
+ if (focusedPanel === "header-new") {
280
+ onAddTask?.();
281
+ key.preventDefault();
282
+ return;
283
+ }
284
+ }
285
+ else if (view === "projects") {
286
+ if (focusedPanel === "header-tasks") {
287
+ onViewLoops?.();
288
+ key.preventDefault();
289
+ return;
290
+ }
291
+ if (focusedPanel === "header-projects") {
292
+ onViewTasks?.();
293
+ key.preventDefault();
294
+ return;
295
+ }
296
+ if (focusedPanel === "header-new") {
297
+ onViewProjects?.();
298
+ key.preventDefault();
299
+ return;
300
+ }
301
+ }
302
+ else if (view === "create" || view === "task-create" || view === "task-edit") {
303
+ if (focusedPanel === "header-tasks") {
304
+ onViewProjects?.();
305
+ key.preventDefault();
306
+ return;
307
+ }
308
+ if (focusedPanel === "header-projects") {
309
+ onViewTasks?.();
310
+ key.preventDefault();
311
+ return;
312
+ }
313
+ if (focusedPanel === "header-new") {
314
+ onAddLoop?.();
315
+ key.preventDefault();
316
+ return;
317
+ }
318
+ }
319
+ }
240
320
  if (view !== "board")
241
321
  return;
242
- if (name === "left" || name === "right" || name === "tab") {
243
- const direction = name === "left" || (name === "tab" && key.shift) ? "left" : "right";
244
- if (focusedPanel === "actions" && name !== "tab") {
322
+ if (name === "tab") {
323
+ const direction = key.shift ? "left" : "right";
324
+ if (focusedPanel === "actions") {
245
325
  const actionCount = selected ? getActionCount(selected.status) : 0;
246
326
  if (direction === "left" && selectedAction === 0) {
247
327
  setFocusedPanel((p) => nextPanel(p, "left"));
@@ -0,0 +1,42 @@
1
+ import { useKeyboard } from "@opentui/react";
2
+ import { copyToClipboard, readFromClipboard } from "../../shared/clipboard.js";
3
+ export function useInputShortcuts(getActiveInput) {
4
+ useKeyboard((key) => {
5
+ if (!key.ctrl)
6
+ return;
7
+ const input = getActiveInput();
8
+ if (!input)
9
+ return;
10
+ const name = key.name;
11
+ if (name === "c") {
12
+ const text = input.getSelectedText();
13
+ if (text) {
14
+ copyToClipboard(text);
15
+ }
16
+ key.preventDefault();
17
+ return;
18
+ }
19
+ if (name === "x") {
20
+ const text = input.getSelectedText();
21
+ if (text) {
22
+ copyToClipboard(text);
23
+ input.deleteSelection();
24
+ }
25
+ key.preventDefault();
26
+ return;
27
+ }
28
+ if (name === "v") {
29
+ const text = readFromClipboard();
30
+ if (text) {
31
+ input.insertText(text);
32
+ }
33
+ key.preventDefault();
34
+ return;
35
+ }
36
+ if (name === "a") {
37
+ input.selectAll();
38
+ key.preventDefault();
39
+ return;
40
+ }
41
+ });
42
+ }
@@ -1,7 +1,7 @@
1
1
  import { useKeyboard } from "@opentui/react";
2
2
  import { nextTaskPanel } from "../components/TaskBrowser.js";
3
3
  export function useTaskKeybindings(params) {
4
- const { confirm, view, tasks, taskSelectedIndex, setTaskSelectedIndex, taskSelectedAction, setTaskSelectedAction, taskFocusedPanel, setTaskFocusedPanel, taskSearchActive, setTaskSearchActive, taskQuery, setTaskQuery, onTaskAction, onCancel, onCreateTask, selectable = true, } = params;
4
+ const { confirm, view, tasks, taskSelectedIndex, setTaskSelectedIndex, taskSelectedAction, setTaskSelectedAction, taskFocusedPanel, setTaskFocusedPanel, taskSearchActive, setTaskSearchActive, taskQuery, setTaskQuery, onTaskAction, onCancel, onCreateTask, onEnterHeader, selectable = true, } = params;
5
5
  const taskActions = selectable
6
6
  ? ["select", "edit", "delete"]
7
7
  : ["edit", "delete"];
@@ -19,6 +19,16 @@ export function useTaskKeybindings(params) {
19
19
  key.preventDefault();
20
20
  return;
21
21
  }
22
+ if (name === "up" || name === "k") {
23
+ setTaskSelectedIndex((i) => Math.max(0, i - 1));
24
+ key.preventDefault();
25
+ return;
26
+ }
27
+ if (name === "down" || name === "j") {
28
+ setTaskSelectedIndex((i) => Math.min(tasks.length - 1, i + 1));
29
+ key.preventDefault();
30
+ return;
31
+ }
22
32
  return;
23
33
  }
24
34
  if (name === "escape") {
@@ -36,14 +46,16 @@ export function useTaskKeybindings(params) {
36
46
  key.preventDefault();
37
47
  return;
38
48
  }
39
- if (name === "tab" || name === "left" || name === "right") {
40
- const direction = name === "left" || (name === "tab" && key.shift) ? "left" : "right";
41
- if (taskFocusedPanel === "actions" && name !== "tab") {
49
+ if (name === "tab") {
50
+ const direction = key.shift ? "left" : "right";
51
+ if (taskFocusedPanel === "actions") {
42
52
  if (direction === "left" && taskSelectedAction === 0) {
43
53
  setTaskFocusedPanel((p) => nextTaskPanel(p, "left"));
44
54
  }
45
55
  else if (direction === "right" && taskSelectedAction === taskActionCount - 1) {
46
- setTaskFocusedPanel((p) => nextTaskPanel(p, "right"));
56
+ onEnterHeader("right");
57
+ key.preventDefault();
58
+ return;
47
59
  }
48
60
  else {
49
61
  setTaskSelectedAction((i) => direction === "right"
@@ -51,6 +63,11 @@ export function useTaskKeybindings(params) {
51
63
  : Math.max(0, i - 1));
52
64
  }
53
65
  }
66
+ else if (taskFocusedPanel === "search" && direction === "left") {
67
+ onEnterHeader("left");
68
+ key.preventDefault();
69
+ return;
70
+ }
54
71
  else {
55
72
  setTaskFocusedPanel((p) => nextTaskPanel(p, direction));
56
73
  }
@@ -5,6 +5,7 @@ import { sleep } from "../shared/sleep.js";
5
5
  import { SLEEP_CHUNK_MS } from "../config/constants.js";
6
6
  import { executeCommand } from "./command-runner.js";
7
7
  import { rotateLogIfNeeded } from "./log-rotator.js";
8
+ import { t } from "../i18n/index.js";
8
9
  export class LoopController extends EventEmitter {
9
10
  abortController;
10
11
  runAbortController = null;
@@ -344,6 +345,9 @@ export class LoopController extends EventEmitter {
344
345
  const chainTask = this.taskResolver(currentTargetId);
345
346
  if (!chainTask)
346
347
  break;
348
+ if (this.logStream) {
349
+ this.logStream.write(t("loop.chainHeader", { name: chainTask.name }));
350
+ }
347
351
  const chainStartedAt = new Date().toISOString();
348
352
  const chainOffset = fs.existsSync(this.logPath) ? fs.statSync(this.logPath).size : 0;
349
353
  this.runHistory.push({
@@ -257,8 +257,10 @@ export class IpcServer {
257
257
  send(socket, { type: "error", message: t("errors.loopNotFound", { id }) });
258
258
  return;
259
259
  }
260
- const record = meta.runHistory.find((r) => r.runNumber === runNumber);
261
- if (!record) {
260
+ const records = meta.runHistory
261
+ .filter((r) => r.runNumber === runNumber)
262
+ .sort((a, b) => a.logOffset - b.logOffset);
263
+ if (records.length === 0) {
262
264
  send(socket, { type: "ok", data: "" });
263
265
  return;
264
266
  }
@@ -267,12 +269,12 @@ export class IpcServer {
267
269
  send(socket, { type: "ok", data: "" });
268
270
  return;
269
271
  }
270
- const offset = record.logOffset;
272
+ const firstOffset = records[0].logOffset;
271
273
  const stat = fs.statSync(logPath);
272
- if (stat.size > offset) {
274
+ if (stat.size > firstOffset) {
273
275
  const fd = fs.openSync(logPath, "r");
274
- const buf = Buffer.alloc(stat.size - offset);
275
- fs.readSync(fd, buf, 0, buf.length, offset);
276
+ const buf = Buffer.alloc(stat.size - firstOffset);
277
+ fs.readSync(fd, buf, 0, buf.length, firstOffset);
276
278
  fs.closeSync(fd);
277
279
  for (const line of buf.toString().split("\n")) {
278
280
  if (line) {
@@ -280,7 +282,8 @@ export class IpcServer {
280
282
  }
281
283
  }
282
284
  }
283
- if (record.status === "completed") {
285
+ const allCompleted = records.every((r) => r.status === "completed");
286
+ if (allCompleted) {
284
287
  send(socket, { type: "end" });
285
288
  return;
286
289
  }
package/dist/i18n/en.json CHANGED
@@ -65,6 +65,7 @@
65
65
  "cli.projectAmbiguous": "Multiple projects named \"{name}\" - use the id instead",
66
66
  "cli.projectInvalidColor": "Invalid color \"{color}\". Valid names: {valid}, or a #rrggbb hex",
67
67
  "loop.runHeader": "\n[Run #{runNumber} - {timestamp}]\n",
68
+ "loop.chainHeader": "\n--- Chain: {name} ---\n",
68
69
  "loop.exitMarker": "[exit {code} · {duration}]\n",
69
70
  "loop.cwdMissingLog": "[error] working directory does not exist: {cwd}\n",
70
71
  "loop.cwdMissing": "Working directory does not exist: {cwd}",
@@ -17,3 +17,25 @@ export function copyToClipboard(text) {
17
17
  }
18
18
  }
19
19
  }
20
+ export function readFromClipboard() {
21
+ const os = platform();
22
+ try {
23
+ if (os === "win32") {
24
+ return execFileSync("powershell", ["-NoProfile", "-Command", "Get-Clipboard"], { encoding: "utf-8" }).replace(/\r?\n$/, "");
25
+ }
26
+ else if (os === "darwin") {
27
+ return execFileSync("pbpaste", { encoding: "utf-8" });
28
+ }
29
+ else {
30
+ try {
31
+ return execFileSync("xclip", ["-selection", "clipboard", "-o"], { encoding: "utf-8" });
32
+ }
33
+ catch {
34
+ return execFileSync("xsel", ["--clipboard", "--output"], { encoding: "utf-8" });
35
+ }
36
+ }
37
+ }
38
+ catch {
39
+ return "";
40
+ }
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loop-task",
3
- "version": "1.4.4",
3
+ "version": "1.4.5",
4
4
  "description": "Loop engineering toolkit. Run any command on a cadence, in the background, managed from a terminal board. Schedule tests, builds, syncs, or agent prompts.",
5
5
  "type": "module",
6
6
  "bin": {