loop-task 1.5.0 → 1.5.2

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/README.md CHANGED
@@ -290,29 +290,28 @@ gh issue edit {{number}} --add-label "refining" --remove-label "to refine"
290
290
 
291
291
  interpolated: `gh issue edit 123 --add-label "refining" --remove-label "to refine"`
292
292
 
293
- **Task 3** (chain, onSuccess): Rewrite with AI
293
+ **Task 3** (chain, onSuccess): Rewrite with AI (edits the issue directly)
294
294
 
295
295
  ```bash
296
- opencode run "Rewrite this GitHub issue as a detailed user story using project context and return only JSON with fields title and body." --model "opencode/big-pickle"
296
+ opencode run "Rewrite this GitHub issue as a detailed user story using project context. Update the issue title and body directly using gh issue edit. Issue number: {{number}} Original title: {{title}} Original body: {{body}}" --model "opencode/big-pickle"
297
297
  ```
298
298
 
299
- stdout: `{"title":"As a user, I want to log in securely","body":"## Acceptance Criteria\n- Login form validates email\n- ..."}`
300
- context: `{ number: 123, title: "As a user, I want to log in securely", body: "## Acceptance Criteria\n- ..." }`
299
+ interpolated: `opencode run "Rewrite this GitHub issue as a detailed user story using project context. Update the issue title and body directly using gh issue edit. Issue number: 123 Original title: Fix login Original body: It doesn't work" --model "opencode/big-pickle"`
301
300
 
302
- **Task 4** (chain, onSuccess): Apply the rewrite and relabel
301
+ **Task 4** (chain, onSuccess): Relabel as ready to implement
303
302
 
304
303
  ```bash
305
- gh issue edit {{number}} --title "{{title}}" --body "{{body}}" --remove-label "refining" --add-label "to implement"
304
+ gh issue edit {{number}} --remove-label "refining" --add-label "to implement"
306
305
  ```
307
306
 
308
- interpolated: `gh issue edit 123 --title "As a user, I want to log in securely" --body "## Acceptance Criteria\n- ..." --remove-label "refining" --add-label "to implement"`
307
+ interpolated: `gh issue edit 123 --remove-label "refining" --add-label "to implement"`
309
308
 
310
309
  ### How it works
311
310
 
312
311
  1. Task 1 queries the issue and emits a JSON object with `number`, `title`, and `body` via `--jq`. The primary task cannot use `{{key}}` interpolation because the chain context is empty when it runs.
313
312
  2. Task 2 receives `{{number}}` interpolated from task 1's context. It relabels the issue from "to refine" to "refining" - no re-query needed.
314
- 3. Task 3 runs opencode, which finds the issue by the "refining" label and rewrites it. It outputs a new JSON object with updated `title` and `body`. Since it uses the same key names, the context is updated with the new values (merge with last-writer-wins).
315
- 4. Task 4 receives `{{number}}` (still 123 from task 1), `{{title}}` and `{{body}}` (now the rewritten versions from task 3). It applies the edits and relabels the issue as "to implement" - no re-query needed.
313
+ 3. Task 3 runs opencode, which finds the issue by the "refining" label and rewrites it in place using `gh issue edit`. The AI agent edits the issue directly - no need to parse its stdout as JSON.
314
+ 4. Task 4 receives `{{number}}` (still 123 from task 1) and relabels the issue as "to implement" - no re-query needed.
316
315
 
317
316
  ### Wrapping values with --jq
318
317
 
@@ -324,6 +323,50 @@ gh issue list --label "to refine" --json number,title --jq '{number: .[0].number
324
323
 
325
324
  This stores `{ "number": 123, "title": "Fix login" }` in context instead of overwriting `output`.
326
325
 
326
+ ### Example: Issue Implementation Chain
327
+
328
+ A four-task chain that finds an issue to implement, marks it in-progress, runs an AI agent to implement it, then closes it - all without re-querying:
329
+
330
+ **Task 1** (primary): Find an issue to implement (or exit if one is already in progress)
331
+
332
+ ```bash
333
+ gh issue list --label "implementing" --limit 1 --json number --jq 'length == 0' | grep -q true && gh issue list --label "to implement" --limit 1 --json number,title,body --jq '{number: .[0].number, title: .[0].title, body: .[0].body}'
334
+ ```
335
+
336
+ stdout: `{"number":456,"title":"Add dark mode toggle","body":"Users want a dark theme"}`
337
+ context: `{ number: 456, title: "Add dark mode toggle", body: "Users want a dark theme" }`
338
+
339
+ If an issue with the "implementing" label already exists, `length == 0` returns `false`, `grep -q true` fails, and the `&&` short-circuits - the chain does not fire. The loop waits for the next iteration.
340
+
341
+ **Task 2** (chain, onSuccess): Mark as in-progress
342
+
343
+ ```bash
344
+ gh issue edit {{number}} --add-label "implementing" --remove-label "to implement"
345
+ ```
346
+
347
+ interpolated: `gh issue edit 456 --add-label "implementing" --remove-label "to implement"`
348
+
349
+ **Task 3** (chain, onSuccess): Implement with AI agent
350
+
351
+ ```bash
352
+ git fetch origin && git checkout main && git reset --hard origin/main && opencode run "Implement this GitHub issue using /ob-autopilot and return only JSON with fields title and body after implementation is completed, merged to main, pushed to origin and the issue has been referenced in GitHub. Issue title: {{title}} Issue body: {{body}}" --model "opencode/big-pickle"
353
+ ```
354
+
355
+ interpolated: `git fetch origin && git checkout main && git reset --hard origin/main && opencode run "Implement this GitHub issue using /ob-autopilot ... Issue title: Add dark mode toggle Issue body: Users want a dark theme" --model "opencode/big-pickle"`
356
+
357
+ stdout: `{"title":"Add dark mode toggle","body":"Implemented dark mode toggle with CSS variables..."}`
358
+ context: `{ number: 456, title: "Add dark mode toggle", body: "Implemented dark mode toggle..." }`
359
+
360
+ **Task 4** (chain, onSuccess): Verify sync and close the issue
361
+
362
+ ```bash
363
+ git push && git fetch origin && [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ] && gh issue edit {{number}} --remove-label "implementing" && gh issue close {{number}}
364
+ ```
365
+
366
+ interpolated: `git push && git fetch origin && [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ] && gh issue edit 456 --remove-label "implementing" && gh issue close 456`
367
+
368
+ The `git rev-parse` check ensures local and remote are in sync before closing - if the push failed or remote is ahead, the command fails and the issue stays open.
369
+
327
370
  ## Development
328
371
 
329
372
  Requires [Bun](https://bun.sh) >= 1.2 for package management and the board, and [Node.js](https://nodejs.org) >= 20 for the CLI and daemon.
package/dist/board/App.js CHANGED
@@ -12,7 +12,7 @@ import { FilterBar } from "./components/FilterBar.js";
12
12
  import { Navigator } from "./components/Navigator.js";
13
13
  import { Inspector } from "./components/Inspector.js";
14
14
  import { RunHistory } from "./components/RunHistory.js";
15
- import { ActionButtons } from "./components/ActionButtons.js";
15
+ import { ActionButtons, getActionKeys } from "./components/ActionButtons.js";
16
16
  import { HelpModal } from "./components/HelpModal.js";
17
17
  import { ContextHelpModal } from "./components/ContextHelpModal.js";
18
18
  import { Footer } from "./components/Footer.js";
@@ -28,6 +28,7 @@ import { fetchRunLog, deleteLoop, pauseLoop, resumeLoop, stopLoop, playLoop, tri
28
28
  import { useBreakpoint } from "./hooks/useBreakpoint.js";
29
29
  import { useRouter } from "./router.js";
30
30
  import { POLL_MS } from "../config/constants.js";
31
+ import { useTabNav } from "./hooks/useTabNav.js";
31
32
  const VIEW_TO_MODE = {
32
33
  create: "create",
33
34
  "task-create": "task",
@@ -68,14 +69,12 @@ export function App(props) {
68
69
  const [pendingTaskSelection, setPendingTaskSelection] = useState(null);
69
70
  const [selectedRunIndex, setSelectedRunIndex] = useState(0);
70
71
  const [selectedAction, setSelectedAction] = useState(0);
71
- const [focusedPanel, setFocusedPanel] = useState("loops");
72
72
  const [logModalRun, setLogModalRun] = useState(null);
73
73
  const [logModalLines, setLogModalLines] = useState([]);
74
74
  const [logModalLoading, setLogModalLoading] = useState(false);
75
75
  const [tasks, setTasks] = useState([]);
76
76
  const [taskSelectedIndex, setTaskSelectedIndex] = useState(0);
77
77
  const [taskSelectedAction, setTaskSelectedAction] = useState(0);
78
- const [taskFocusedPanel, setTaskFocusedPanel] = useState("tasks");
79
78
  const [taskSearchActive, setTaskSearchActive] = useState(false);
80
79
  const [taskQuery, setTaskQuery] = useState("");
81
80
  const [projects, setProjects] = useState([]);
@@ -282,6 +281,30 @@ export function App(props) {
282
281
  pop();
283
282
  pushToast("success", updated ? t("board.toastTaskUpdated", { id }) : t("board.toastTaskCreated", { id }));
284
283
  };
284
+ const FORM_VIEWS = ["create", "task-create", "task-edit"];
285
+ const isFormView = FORM_VIEWS.includes(view);
286
+ const actionKeys = selected ? getActionKeys(selected.status) : [];
287
+ const actionItems = actionKeys.map((k) => `action-${k}`);
288
+ const boardItems = ["search", "project-filter", "status", "sort", "header-1", "header-2", "header-3", "navigator", "run-history", ...actionItems];
289
+ const taskItems = ["task-search", "header-1", "header-2", "header-3", "task-list", ...(["select", "edit", "delete"].map((k) => `task-action-${k}`))];
290
+ const projectItems = ["header-1", "header-2", "header-3"];
291
+ const navItems = isFormView ? [] : view === "task-list" ? taskItems : view === "projects" ? projectItems : boardItems;
292
+ const { focusedItem: navFocused, setFocusIndex: setNavIndex, enabled: navEnabled } = useTabNav(navItems);
293
+ const HEADER_MAP = { "header-1": "header-tasks", "header-2": "header-projects", "header-3": "header-new" };
294
+ const isHeaderFocused = navFocused === "header-1" || navFocused === "header-2" || navFocused === "header-3";
295
+ const derivedFocusedPanel = isHeaderFocused ? HEADER_MAP[navFocused] : navFocused ?? "loops";
296
+ const actionIdx = navFocused?.startsWith("action-") ? actionKeys.indexOf(navFocused.slice("action-".length)) : -1;
297
+ const derivedSelectedAction = actionIdx >= 0 ? actionIdx : selectedAction;
298
+ const taskActionIdx = navFocused?.startsWith("task-action-") ? ["select", "edit", "delete"].indexOf(navFocused.slice("task-action-".length)) : -1;
299
+ const derivedTaskAction = taskActionIdx >= 0 ? taskActionIdx : taskSelectedAction;
300
+ const taskPanelMap = {
301
+ "task-search": "search",
302
+ "task-list": "tasks",
303
+ "task-action-select": "actions",
304
+ "task-action-edit": "actions",
305
+ "task-action-delete": "actions",
306
+ };
307
+ const derivedTaskPanel = (navFocused ? taskPanelMap[navFocused] : undefined) ?? "tasks";
285
308
  useBoardKeybindings({
286
309
  confirm,
287
310
  confirmChoice,
@@ -309,9 +332,13 @@ export function App(props) {
309
332
  selectedRunIndex,
310
333
  setSelectedRunIndex,
311
334
  selectedRunCount: selected?.runHistory?.length ?? 0,
312
- focusedPanel,
313
- setFocusedPanel,
314
- selectedAction,
335
+ focusedPanel: derivedFocusedPanel,
336
+ setFocusedPanel: ((p) => {
337
+ const navIdx = navItems.indexOf(p);
338
+ if (navIdx >= 0)
339
+ setNavIndex(navIdx);
340
+ }),
341
+ selectedAction: derivedSelectedAction,
315
342
  setSelectedAction,
316
343
  onAction: handleAction,
317
344
  onOpenRunLog: handleOpenRunLog,
@@ -329,10 +356,23 @@ export function App(props) {
329
356
  tasks: filteredTasks,
330
357
  taskSelectedIndex,
331
358
  setTaskSelectedIndex,
332
- taskSelectedAction,
333
- setTaskSelectedAction,
334
- taskFocusedPanel,
335
- setTaskFocusedPanel,
359
+ taskSelectedAction: derivedTaskAction,
360
+ setTaskSelectedAction: ((updater) => {
361
+ const nextIdx = typeof updater === "function" ? updater(derivedTaskAction) : updater;
362
+ const taskActions = ["select", "edit", "delete"];
363
+ const actionKey = taskActions[nextIdx];
364
+ if (actionKey) {
365
+ const navIdx = navItems.indexOf(`task-action-${actionKey}`);
366
+ if (navIdx >= 0)
367
+ setNavIndex(navIdx);
368
+ }
369
+ }),
370
+ taskFocusedPanel: derivedTaskPanel,
371
+ setTaskFocusedPanel: ((p) => {
372
+ const navIdx = navItems.indexOf(p === "search" ? "task-search" : p === "actions" ? "task-actions" : "task-list");
373
+ if (navIdx >= 0)
374
+ setNavIndex(navIdx);
375
+ }),
336
376
  taskSearchActive,
337
377
  setTaskSearchActive,
338
378
  taskQuery,
@@ -340,10 +380,8 @@ export function App(props) {
340
380
  onTaskAction: handleTaskAction,
341
381
  onCancel: cancelTaskList,
342
382
  onCreateTask: handleCreateTask,
343
- onEnterHeader: (direction) => {
344
- setFocusedPanel(direction === "right" ? "header-tasks" : "header-new");
345
- },
346
383
  onToggleContextHelp: () => setContextHelpOpen((v) => !v),
384
+ headerFocused: isHeaderFocused,
347
385
  selectable: stack.includes("create") || stack.includes("task-edit"),
348
386
  });
349
387
  const counts = {
@@ -354,16 +392,14 @@ export function App(props) {
354
392
  idle: loops.filter((l) => l.status === "idle").length,
355
393
  };
356
394
  const mode = resolveMode(confirm, searchActive || taskSearchActive, helpOpen, view);
357
- return (_jsxs("box", { style: { flexDirection: "column", width: "100%", height: "100%", backgroundColor: "#0b0b0b" }, children: [_jsx(Header, { daemonStatus: daemonStatus, counts: counts, view: view, focusedPanel: focusedPanel, onViewLoops: () => replace("board"), onViewTasks: () => { void refreshTasks(); push("task-list"); }, onViewProjects: () => push("projects"), onAddLoop: () => { setEditTarget(null); push("create"); }, onAddTask: () => { setEditTask(null); push("task-create"); }, onAddProject: () => { if (view === "projects") {
395
+ return (_jsxs("box", { style: { flexDirection: "column", width: "100%", height: "100%", backgroundColor: "#0b0b0b" }, children: [_jsx(Header, { daemonStatus: daemonStatus, counts: counts, view: view, focusedPanel: derivedFocusedPanel, onViewLoops: () => replace("board"), onViewTasks: () => { void refreshTasks(); push("task-list"); }, onViewProjects: () => push("projects"), onAddLoop: () => { setEditTarget(null); push("create"); }, onAddTask: () => { setEditTask(null); push("task-create"); }, onAddProject: () => { if (view === "projects") {
358
396
  createProjectTriggerRef.current?.();
359
397
  }
360
398
  else {
361
399
  push("projects");
362
- } } }), 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) => {
363
- setFocusedPanel(direction === "right" ? "header-tasks" : "header-new");
364
- } })) : (_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) {
400
+ } } }), view === "board" ? (_jsx(FilterBar, { filters: filters, sort: sort, searchActive: searchActive, focusedPanel: derivedFocusedPanel, 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); } })) : view === "task-list" ? (_jsx(TaskFilterBar, { query: taskQuery, searchActive: taskSearchActive, focusedPanel: derivedTaskPanel, onQueryChange: setTaskQuery, onSearchActivate: () => setTaskSearchActive(true), onSearchDismiss: () => { setTaskSearchActive(false); } })) : 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, onCopy: () => pushToast("success", t("board.toastCopied")) })) : 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: derivedTaskPanel === "tasks", query: taskQuery, onSelect: (index) => { setTaskSelectedIndex(index); }, 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, onCopy: () => pushToast("success", t("board.toastCopied")) }, `ti-${selectedTask?.id}`), _jsx(TaskActionButtons, { task: selectedTask, focused: taskActionIdx >= 0, selectedAction: derivedTaskAction, selectable: stack.includes("create") || stack.includes("task-edit"), onAction: handleTaskAction }, `tab-${selectedTask?.id}`)] })] })) : view === "projects" ? (_jsx(ProjectsPage, { projects: projects, loops: loops, headerFocused: isHeaderFocused, onClose: () => pop(), onRefresh: refreshProjects, onOpenCreate: (trigger) => { createProjectTriggerRef.current = trigger; }, onEnterHeader: () => { } })) : (_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: derivedFocusedPanel === "loops" || derivedFocusedPanel === "navigator", projects: projects, onSelect: (index) => setSelectedIndex(index), onActivate: (index) => { setSelectedIndex(index); const loop = visible[index]; if (loop) {
365
401
  setEditTarget(loop);
366
402
  push("create");
367
- } } }), _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, contextHelpOpen ? _jsx(ContextHelpModal, { onClose: () => setContextHelpOpen(false) }) : 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 })] }));
403
+ } } }), _jsxs("box", { style: { flexDirection: "column", flexGrow: 1, backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx(Inspector, { loop: selected }), _jsx(RunHistory, { loop: selected, selectedRunIndex: selectedRunIndex, focused: derivedFocusedPanel === "runs" || derivedFocusedPanel === "run-history", onSelectRun: (index) => setSelectedRunIndex(index), onOpenRun: handleOpenRunLog }), _jsx(ActionButtons, { loop: selected, focused: actionIdx >= 0, selectedAction: derivedSelectedAction, 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, contextHelpOpen ? _jsx(ContextHelpModal, { onClose: () => setContextHelpOpen(false) }) : 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 })] }));
368
404
  }
369
405
  const TASK_FORM_VIEWS = new Set(["task-create", "task-edit"]);
@@ -8,6 +8,7 @@ import { commandLine } from "../format.js";
8
8
  import { createLoop, updateLoop, listTasks } from "../daemon.js";
9
9
  import { useHoverState } from "../hooks/useHoverState.js";
10
10
  import { useInputShortcuts } from "../hooks/useInputShortcuts.js";
11
+ import { useTabNav } from "../hooks/useTabNav.js";
11
12
  import { SearchSelect } from "./SearchSelect.js";
12
13
  import { HOVER_BG } from "../../config/constants.js";
13
14
  export const TASK_MODE_INLINE = "inline";
@@ -45,25 +46,10 @@ export function CreateView(props) {
45
46
  project: props.initial.project ?? props.currentProjectId ?? "default",
46
47
  });
47
48
  const valuesRef = useRef(values);
48
- const [focusIndex, setFocusIndex] = useState(0);
49
49
  const [error, setError] = useState("");
50
50
  const [isSubmitting, setIsSubmitting] = useState(false);
51
51
  const [selectedTaskName, setSelectedTaskName] = useState(props.selectedTaskName ?? null);
52
52
  const inputRef = useRef(null);
53
- useInputShortcuts(() => {
54
- if (focusIndex >= saveIndex)
55
- return null;
56
- return inputRef.current;
57
- });
58
- useState(() => {
59
- if (values.taskId && !selectedTaskName) {
60
- void listTasks().then((tasks) => {
61
- const found = tasks.find((t) => t.id === values.taskId);
62
- if (found)
63
- setSelectedTaskName(found.name);
64
- }).catch(() => { });
65
- }
66
- });
67
53
  const { width: termWidth } = useTerminalDimensions();
68
54
  const btnWidth = Math.max(10, Math.min(14, Math.floor(termWidth / 6)));
69
55
  const isInline = values.taskMode === TASK_MODE_INLINE;
@@ -77,9 +63,25 @@ export function CreateView(props) {
77
63
  return !isInline;
78
64
  return true;
79
65
  });
80
- const chooseTaskIdx = filteredFields.indexOf("taskId");
81
- const saveIndex = filteredFields.length;
82
- const cancelIndex = filteredFields.length + 1;
66
+ const navItems = [...filteredFields, "save", "cancel"];
67
+ const { setFocusIndex, focusedItem, isFocused } = useTabNav(navItems);
68
+ const focusedItemRef = useRef(focusedItem);
69
+ focusedItemRef.current = focusedItem;
70
+ useInputShortcuts(() => {
71
+ const fi = focusedItemRef.current;
72
+ if (fi === "save" || fi === "cancel" || fi === "project")
73
+ return null;
74
+ return inputRef.current;
75
+ });
76
+ useState(() => {
77
+ if (values.taskId && !selectedTaskName) {
78
+ void listTasks().then((tasks) => {
79
+ const found = tasks.find((t) => t.id === values.taskId);
80
+ if (found)
81
+ setSelectedTaskName(found.name);
82
+ }).catch(() => { });
83
+ }
84
+ });
83
85
  function updateValues(next) {
84
86
  valuesRef.current = next;
85
87
  setValues(next);
@@ -91,60 +93,33 @@ export function CreateView(props) {
91
93
  setSelectedTaskName(props.selectedTaskName ?? props.selectedTaskId);
92
94
  }
93
95
  useKeyboard((key) => {
94
- if (key.name === "tab") {
95
- setFocusIndex((i) => {
96
- const next = key.shift ? i - 1 : i + 1;
97
- if (next < 0)
98
- return cancelIndex;
99
- if (next > cancelIndex)
100
- return 0;
101
- return next;
102
- });
96
+ if (key.name !== "return" && key.name !== "enter" && key.name !== " ")
97
+ return;
98
+ const fi = focusedItemRef.current;
99
+ if (fi === "taskMode") {
100
+ const next = valuesRef.current.taskMode === TASK_MODE_INLINE ? TASK_MODE_EXISTING : TASK_MODE_INLINE;
101
+ updateValues({ ...valuesRef.current, taskMode: next });
103
102
  key.preventDefault();
104
103
  return;
105
104
  }
106
- if (key.name === "up" || key.name === "down") {
107
- const field = filteredFields[focusIndex];
108
- if (field === "project")
109
- return;
110
- setFocusIndex((i) => {
111
- const next = key.name === "up" ? i - 1 : i + 1;
112
- if (next < 0)
113
- return cancelIndex;
114
- if (next > cancelIndex)
115
- return 0;
116
- return next;
117
- });
105
+ if (fi === "runNow") {
106
+ const next = valuesRef.current.runNow === "y" ? "n" : "y";
107
+ updateValues({ ...valuesRef.current, runNow: next });
118
108
  key.preventDefault();
119
109
  return;
120
110
  }
121
- if (key.name === "return" || key.name === "enter" || key.name === " ") {
122
- const field = filteredFields[focusIndex];
123
- if (field === "taskMode") {
124
- const next = valuesRef.current.taskMode === TASK_MODE_INLINE ? TASK_MODE_EXISTING : TASK_MODE_INLINE;
125
- updateValues({ ...valuesRef.current, taskMode: next });
126
- key.preventDefault();
127
- return;
128
- }
129
- if (field === "runNow") {
130
- const next = valuesRef.current.runNow === "y" ? "n" : "y";
131
- updateValues({ ...valuesRef.current, runNow: next });
132
- key.preventDefault();
133
- return;
134
- }
135
- if (focusIndex === chooseTaskIdx && !isInline) {
136
- props.onChooseTask();
137
- key.preventDefault();
138
- return;
139
- }
140
- if (focusIndex === saveIndex) {
141
- void submit(valuesRef.current);
142
- key.preventDefault();
143
- }
144
- else if (focusIndex === cancelIndex) {
145
- props.onCancel();
146
- key.preventDefault();
147
- }
111
+ if (fi === "taskId" && !isInline) {
112
+ props.onChooseTask();
113
+ key.preventDefault();
114
+ return;
115
+ }
116
+ if (fi === "save") {
117
+ void submit(valuesRef.current);
118
+ key.preventDefault();
119
+ }
120
+ else if (fi === "cancel") {
121
+ props.onCancel();
122
+ key.preventDefault();
148
123
  }
149
124
  });
150
125
  async function submit(current) {
@@ -270,8 +245,8 @@ export function CreateView(props) {
270
245
  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) => {
271
246
  const leftField = filteredFields[row * 2];
272
247
  const rightField = row * 2 + 1 < filteredFields.length ? filteredFields[row * 2 + 1] : null;
273
- 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));
274
- }) }), _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] }));
248
+ return (_jsxs("box", { style: { flexDirection: "row", marginBottom: 1 }, children: [_jsx(FormRow, { field: leftField, index: row * 2, isFocused: isFocused(leftField), values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, examples: examples, taskModeOptions: taskModeOptions, runNowOptions: runNowOptions, projectOptions: projectOptions, selectedTaskName: selectedTaskName, onChooseTask: props.onChooseTask, inputRef: inputRef, style: { width: "50%", paddingRight: 1 } }), rightField ? (_jsx(FormRow, { field: rightField, index: row * 2 + 1, isFocused: isFocused(rightField), values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, examples: examples, taskModeOptions: taskModeOptions, runNowOptions: runNowOptions, projectOptions: projectOptions, selectedTaskName: selectedTaskName, onChooseTask: props.onChooseTask, inputRef: inputRef, style: { width: "50%" } })) : (_jsx("box", { style: { width: "50%" } }))] }, row));
249
+ }) }), _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] }));
275
250
  }
276
251
  function HoverButton(props) {
277
252
  const { isHovered, hoverProps } = useHoverState();
@@ -280,9 +255,8 @@ function HoverButton(props) {
280
255
  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 }) }) }));
281
256
  }
282
257
  function FormRow(props) {
283
- const { field, index, focusIndex, values, valuesRef, updateValues, setFocusIndex, submit, labels, hints, examples, taskModeOptions, runNowOptions, projectOptions, selectedTaskName, fields, onChooseTask, inputRef, style } = props;
258
+ const { field, index, isFocused, values, valuesRef, updateValues, setFocusIndex, submit, labels, hints, examples, taskModeOptions, runNowOptions, projectOptions, selectedTaskName, onChooseTask, inputRef, style } = props;
284
259
  const { isHovered, hoverProps } = useHoverState();
285
- const isFocused = focusIndex === index;
286
260
  const isToggleField = field === "taskMode" || field === "runNow";
287
261
  const isTaskButton = field === "taskId";
288
262
  const isProjectField = field === "project";
@@ -293,12 +267,5 @@ function FormRow(props) {
293
267
  return (_jsx("box", { onMouseDown: () => updateValues({ ...valuesRef.current, [field]: opt.value }), style: { backgroundColor: isActive ? "#1e3a8a" : undefined, paddingLeft: 1, paddingRight: 1, marginRight: 1 }, ...hoverProps, children: _jsx("text", { fg: isActive ? "#ffffff" : isHovered ? "#e5e7eb" : "#9ca3af", children: _jsx("strong", { children: opt.name }) }) }, opt.value));
294
268
  }) })) : isTaskButton ? (_jsx("box", { border: true, borderColor: isFocused ? "#38bdf8" : undefined, style: { height: 3, flexDirection: "row", backgroundColor: "#0b0b0b", alignItems: "center", paddingLeft: 1 }, onMouseDown: () => { setFocusIndex(index); onChooseTask(); }, children: _jsx("text", { fg: values.taskId ? "#4ade80" : "#9ca3af", children: values.taskId
295
269
  ? t("board.selectedTask", { name: selectedTaskName ?? values.taskId })
296
- : t("board.chooseTask") }) })) : isProjectField ? (_jsx(SearchSelect, { options: projectOptions, value: values.project, onChange: (v) => updateValues({ ...valuesRef.current, project: v }), focused: isFocused })) : (_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: () => {
297
- if (index < fields.length - 1) {
298
- setFocusIndex(index + 1);
299
- }
300
- else {
301
- void submit(valuesRef.current);
302
- }
303
- } }) }))] }));
270
+ : t("board.chooseTask") }) })) : isProjectField ? (_jsx(SearchSelect, { options: projectOptions, value: values.project, onChange: (v) => updateValues({ ...valuesRef.current, project: v }), focused: isFocused })) : (_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: () => { void submit(valuesRef.current); } }) }))] }));
304
271
  }
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- import { useState } from "react";
2
+ import { useState, useRef } from "react";
3
+ import { useTabNav } from "../hooks/useTabNav.js";
3
4
  import { useKeyboard, useTerminalDimensions } from "@opentui/react";
4
5
  import { t } from "../../i18n/index.js";
5
6
  import { createProject } from "../daemon.js";
@@ -12,36 +13,28 @@ export function CreateProjectModal(props) {
12
13
  const [selectedColorKey, setSelectedColorKey] = useState(defaultColorKey);
13
14
  const [error, setError] = useState("");
14
15
  const [isSubmitting, setIsSubmitting] = useState(false);
15
- const [focusField, setFocusField] = useState("name");
16
+ const { focusedItem: focusField, setFocusIndex } = useTabNav(["name", "color", "save", "cancel"]);
17
+ const focusFieldRef = useRef(focusField);
18
+ focusFieldRef.current = focusField;
16
19
  useKeyboard((key) => {
17
- if (key.name === "tab") {
18
- const order = ["name", "color", "save", "cancel"];
19
- const idx = order.indexOf(focusField);
20
- const next = key.shift ? order[(idx - 1 + order.length) % order.length] : order[(idx + 1) % order.length];
21
- setFocusField(next ?? "name");
22
- return;
23
- }
24
20
  if (key.name === "escape") {
25
21
  onCancel();
26
22
  return;
27
23
  }
28
24
  if (key.name === "return" || key.name === "enter") {
29
- if (focusField === "cancel") {
25
+ if (focusFieldRef.current === "cancel") {
30
26
  onCancel();
31
27
  return;
32
28
  }
33
29
  void submit();
34
30
  return;
35
31
  }
36
- if (focusField === "color") {
37
- if (key.name === "left") {
38
- const idx = PROJECT_COLOR_KEYS.indexOf(selectedColorKey);
39
- setSelectedColorKey(PROJECT_COLOR_KEYS[(idx - 1 + PROJECT_COLOR_KEYS.length) % PROJECT_COLOR_KEYS.length] ?? selectedColorKey);
40
- return;
41
- }
42
- if (key.name === "right") {
32
+ if (focusFieldRef.current === "color") {
33
+ if (key.name === "left" || key.name === "right") {
43
34
  const idx = PROJECT_COLOR_KEYS.indexOf(selectedColorKey);
44
- setSelectedColorKey(PROJECT_COLOR_KEYS[(idx + 1) % PROJECT_COLOR_KEYS.length] ?? selectedColorKey);
35
+ const dir = key.name === "left" ? -1 : 1;
36
+ setSelectedColorKey(PROJECT_COLOR_KEYS[(idx + dir + PROJECT_COLOR_KEYS.length) % PROJECT_COLOR_KEYS.length] ?? selectedColorKey);
37
+ key.preventDefault();
45
38
  return;
46
39
  }
47
40
  }
@@ -81,9 +74,9 @@ export function CreateProjectModal(props) {
81
74
  padding: 1,
82
75
  minWidth: Math.min(44, width - 4),
83
76
  backgroundColor: "#111827",
84
- }, children: [_jsx("text", { fg: focusField === "name" ? "#38bdf8" : "#e5e7eb", children: t("project.labelName") }), _jsx("text", { fg: "#6b7280", children: t("project.hintName") }), _jsx("box", { border: true, borderColor: focusField === "name" ? "#38bdf8" : undefined, style: { height: 3, backgroundColor: "#0b0b0b", marginBottom: 1 }, children: _jsx("input", { focused: focusField === "name", value: name, placeholder: "My Project", onInput: (value) => setName(value), onSubmit: () => setFocusField("color") }) }), _jsx("text", { fg: focusField === "color" ? "#38bdf8" : "#e5e7eb", children: t("project.labelColor") }), _jsx("text", { fg: "#6b7280", children: t("project.hintColor") }), _jsx("box", { border: true, borderColor: focusField === "color" ? "#38bdf8" : undefined, style: { height: 3, flexDirection: "row", backgroundColor: "#0b0b0b", alignItems: "center", marginBottom: 1 }, children: PROJECT_COLOR_KEYS.map((colorKey) => {
77
+ }, children: [_jsx("text", { fg: focusField === "name" ? "#38bdf8" : "#e5e7eb", children: t("project.labelName") }), _jsx("text", { fg: "#6b7280", children: t("project.hintName") }), _jsx("box", { border: true, borderColor: focusField === "name" ? "#38bdf8" : undefined, style: { height: 3, backgroundColor: "#0b0b0b", marginBottom: 1 }, children: _jsx("input", { focused: focusField === "name", value: name, placeholder: "My Project", onInput: (value) => setName(value) }) }), _jsx("text", { fg: focusField === "color" ? "#38bdf8" : "#e5e7eb", children: t("project.labelColor") }), _jsx("text", { fg: "#6b7280", children: t("project.hintColor") }), _jsx("box", { border: true, borderColor: focusField === "color" ? "#38bdf8" : undefined, style: { height: 3, flexDirection: "row", backgroundColor: "#0b0b0b", alignItems: "center", marginBottom: 1 }, children: PROJECT_COLOR_KEYS.map((colorKey) => {
85
78
  const isActive = selectedColorKey === colorKey;
86
- return (_jsx("box", { onMouseDown: () => { setFocusField("color"); setSelectedColorKey(colorKey); }, style: {
79
+ return (_jsx("box", { onMouseDown: () => { setFocusIndex(1); setSelectedColorKey(colorKey); }, style: {
87
80
  backgroundColor: isActive ? "#1e3a8a" : undefined,
88
81
  paddingLeft: 1,
89
82
  paddingRight: 1,
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- import { useState } from "react";
2
+ import { useState, useRef } from "react";
3
+ import { useTabNav } from "../hooks/useTabNav.js";
3
4
  import { useKeyboard, useTerminalDimensions } from "@opentui/react";
4
5
  import { t } from "../../i18n/index.js";
5
6
  import { updateProject } from "../daemon.js";
@@ -14,36 +15,28 @@ export function EditProjectModal(props) {
14
15
  });
15
16
  const [error, setError] = useState("");
16
17
  const [isSubmitting, setIsSubmitting] = useState(false);
17
- const [focusField, setFocusField] = useState("name");
18
+ const { focusedItem: focusField, setFocusIndex } = useTabNav(["name", "color", "save", "cancel"]);
19
+ const focusFieldRef = useRef(focusField);
20
+ focusFieldRef.current = focusField;
18
21
  useKeyboard((key) => {
19
- if (key.name === "tab") {
20
- const order = ["name", "color", "save", "cancel"];
21
- const idx = order.indexOf(focusField);
22
- const next = key.shift ? order[(idx - 1 + order.length) % order.length] : order[(idx + 1) % order.length];
23
- setFocusField(next ?? "name");
24
- return;
25
- }
26
22
  if (key.name === "escape") {
27
23
  onCancel();
28
24
  return;
29
25
  }
30
26
  if (key.name === "return" || key.name === "enter") {
31
- if (focusField === "cancel") {
27
+ if (focusFieldRef.current === "cancel") {
32
28
  onCancel();
33
29
  return;
34
30
  }
35
31
  void submit();
36
32
  return;
37
33
  }
38
- if (focusField === "color") {
39
- if (key.name === "left") {
40
- const idx = PROJECT_COLOR_KEYS.indexOf(selectedColorKey);
41
- setSelectedColorKey(PROJECT_COLOR_KEYS[(idx - 1 + PROJECT_COLOR_KEYS.length) % PROJECT_COLOR_KEYS.length] ?? selectedColorKey);
42
- return;
43
- }
44
- if (key.name === "right") {
34
+ if (focusFieldRef.current === "color") {
35
+ if (key.name === "left" || key.name === "right") {
45
36
  const idx = PROJECT_COLOR_KEYS.indexOf(selectedColorKey);
46
- setSelectedColorKey(PROJECT_COLOR_KEYS[(idx + 1) % PROJECT_COLOR_KEYS.length] ?? selectedColorKey);
37
+ const dir = key.name === "left" ? -1 : 1;
38
+ setSelectedColorKey(PROJECT_COLOR_KEYS[(idx + dir + PROJECT_COLOR_KEYS.length) % PROJECT_COLOR_KEYS.length] ?? selectedColorKey);
39
+ key.preventDefault();
47
40
  return;
48
41
  }
49
42
  }
@@ -83,9 +76,9 @@ export function EditProjectModal(props) {
83
76
  padding: 1,
84
77
  minWidth: Math.min(44, width - 4),
85
78
  backgroundColor: "#111827",
86
- }, children: [_jsx("text", { fg: focusField === "name" ? "#38bdf8" : "#e5e7eb", children: t("project.labelName") }), _jsx("text", { fg: "#6b7280", children: t("project.hintName") }), _jsx("box", { border: true, borderColor: focusField === "name" ? "#38bdf8" : undefined, style: { height: 3, backgroundColor: "#0b0b0b", marginBottom: 1 }, children: _jsx("input", { focused: focusField === "name", value: name, placeholder: project.name, onInput: (value) => setName(value), onSubmit: () => setFocusField("color") }) }), _jsx("text", { fg: focusField === "color" ? "#38bdf8" : "#e5e7eb", children: t("project.labelColor") }), _jsx("text", { fg: "#6b7280", children: t("project.hintColor") }), _jsx("box", { border: true, borderColor: focusField === "color" ? "#38bdf8" : undefined, style: { height: 3, flexDirection: "row", backgroundColor: "#0b0b0b", alignItems: "center", marginBottom: 1 }, children: PROJECT_COLOR_KEYS.map((colorKey) => {
79
+ }, children: [_jsx("text", { fg: focusField === "name" ? "#38bdf8" : "#e5e7eb", children: t("project.labelName") }), _jsx("text", { fg: "#6b7280", children: t("project.hintName") }), _jsx("box", { border: true, borderColor: focusField === "name" ? "#38bdf8" : undefined, style: { height: 3, backgroundColor: "#0b0b0b", marginBottom: 1 }, children: _jsx("input", { focused: focusField === "name", value: name, placeholder: project.name, onInput: (value) => setName(value) }) }), _jsx("text", { fg: focusField === "color" ? "#38bdf8" : "#e5e7eb", children: t("project.labelColor") }), _jsx("text", { fg: "#6b7280", children: t("project.hintColor") }), _jsx("box", { border: true, borderColor: focusField === "color" ? "#38bdf8" : undefined, style: { height: 3, flexDirection: "row", backgroundColor: "#0b0b0b", alignItems: "center", marginBottom: 1 }, children: PROJECT_COLOR_KEYS.map((colorKey) => {
87
80
  const isActive = selectedColorKey === colorKey;
88
- return (_jsx("box", { onMouseDown: () => { setFocusField("color"); setSelectedColorKey(colorKey); }, style: {
81
+ return (_jsx("box", { onMouseDown: () => { setFocusIndex(1); setSelectedColorKey(colorKey); }, style: {
89
82
  backgroundColor: isActive ? "#1e3a8a" : undefined,
90
83
  paddingLeft: 1,
91
84
  paddingRight: 1,