loop-task 1.4.7 → 1.5.1

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
@@ -252,6 +252,121 @@ loop-task (board) ──IPC──► daemon ──► loop 1 ──► task (com
252
252
  - **Persistent** - loop and task state is saved after every run; survives restarts
253
253
  - **Graceful shutdown** - background loops are daemon-managed; foreground loops finish the current execution on Ctrl+C
254
254
 
255
+ ## Chain Context Sharing
256
+
257
+ When tasks are arranged in a chain (on-success or on-failure), context flows between them automatically. This lets later tasks reference output from earlier ones without custom glue.
258
+
259
+ ### How it works
260
+
261
+ 1. **Auto-capture** - stdout from every task in the chain is captured before the next task starts.
262
+ 2. **Parse rules** - captured output is parsed by content type:
263
+ - **JSON object** (`{"key": "value"}`) - each key is merged into the shared context.
264
+ - **JSONL** (one JSON object per line) - each line's keys are merged in order.
265
+ - **Plain text** - stored under a single `output` key.
266
+ - **Empty output** - no change to context.
267
+ 3. **Template interpolation** - use `{{key}}` in the command or arguments of any task. Before spawning, `{{key}}` is replaced with the current value of `key` from the shared context.
268
+ 4. **Merge semantics** - keys accumulate across the chain. Task 1 produces `{ "id": "42" }`, task 2 can use `{{id}}` and also add `{ "status": "ok" }`. Task 3 sees both.
269
+ 5. **Output clobbering** - plain text tasks overwrite the `output` key. Use JSON with named keys when data must survive across multiple downstream tasks.
270
+ 6. **Context lifecycle** - context is built fresh each loop iteration and exists only in memory. It is never persisted to disk.
271
+
272
+ ### Example: Issue Refinement Chain
273
+
274
+ A four-task chain that finds an issue, marks it in-progress, rewrites it with AI, and relabels it - all without re-querying:
275
+
276
+ **Task 1** (primary): Find an issue to refine
277
+
278
+ ```bash
279
+ gh issue list --label "to refine" --limit 1 --json number,title,body --jq '{number: .[0].number, title: .[0].title, body: .[0].body}'
280
+ ```
281
+
282
+ stdout: `{"number":123,"title":"Fix login","body":"It doesn't work"}`
283
+ context: `{ number: 123, title: "Fix login", body: "It doesn't work" }`
284
+
285
+ **Task 2** (chain, onSuccess): Mark as in-progress
286
+
287
+ ```bash
288
+ gh issue edit {{number}} --add-label "refining" --remove-label "to refine"
289
+ ```
290
+
291
+ interpolated: `gh issue edit 123 --add-label "refining" --remove-label "to refine"`
292
+
293
+ **Task 3** (chain, onSuccess): Rewrite with AI (edits the issue directly)
294
+
295
+ ```bash
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
+ ```
298
+
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"`
300
+
301
+ **Task 4** (chain, onSuccess): Relabel as ready to implement
302
+
303
+ ```bash
304
+ gh issue edit {{number}} --remove-label "refining" --add-label "to implement"
305
+ ```
306
+
307
+ interpolated: `gh issue edit 123 --remove-label "refining" --add-label "to implement"`
308
+
309
+ ### How it works
310
+
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.
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.
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.
315
+
316
+ ### Wrapping values with --jq
317
+
318
+ To avoid the plain-text `output` clobbering, wrap any value in a named JSON key using `--jq` (requires `--json` before `--jq`):
319
+
320
+ ```bash
321
+ gh issue list --label "to refine" --json number,title --jq '{number: .[0].number, title: .[0].title}'
322
+ ```
323
+
324
+ This stores `{ "number": 123, "title": "Fix login" }` in context instead of overwriting `output`.
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
+
255
370
  ## Development
256
371
 
257
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,8 +12,9 @@ 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
+ import { ContextHelpModal } from "./components/ContextHelpModal.js";
17
18
  import { Footer } from "./components/Footer.js";
18
19
  import { ConfirmModal } from "./components/ConfirmModal.js";
19
20
  import { CreateView, createInitialValues } from "./components/CreateForm.js";
@@ -27,6 +28,7 @@ import { fetchRunLog, deleteLoop, pauseLoop, resumeLoop, stopLoop, playLoop, tri
27
28
  import { useBreakpoint } from "./hooks/useBreakpoint.js";
28
29
  import { useRouter } from "./router.js";
29
30
  import { POLL_MS } from "../config/constants.js";
31
+ import { useTabNav } from "./hooks/useTabNav.js";
30
32
  const VIEW_TO_MODE = {
31
33
  create: "create",
32
34
  "task-create": "task",
@@ -58,6 +60,7 @@ export function App(props) {
58
60
  const [selectedIndex, setSelectedIndex] = useState(0);
59
61
  const [searchActive, setSearchActive] = useState(false);
60
62
  const [helpOpen, setHelpOpen] = useState(false);
63
+ const [contextHelpOpen, setContextHelpOpen] = useState(false);
61
64
  const [confirm, setConfirm] = useState(null);
62
65
  const [confirmChoice, setConfirmChoice] = useState(0);
63
66
  const [editTarget, setEditTarget] = useState(null);
@@ -66,14 +69,12 @@ export function App(props) {
66
69
  const [pendingTaskSelection, setPendingTaskSelection] = useState(null);
67
70
  const [selectedRunIndex, setSelectedRunIndex] = useState(0);
68
71
  const [selectedAction, setSelectedAction] = useState(0);
69
- const [focusedPanel, setFocusedPanel] = useState("loops");
70
72
  const [logModalRun, setLogModalRun] = useState(null);
71
73
  const [logModalLines, setLogModalLines] = useState([]);
72
74
  const [logModalLoading, setLogModalLoading] = useState(false);
73
75
  const [tasks, setTasks] = useState([]);
74
76
  const [taskSelectedIndex, setTaskSelectedIndex] = useState(0);
75
77
  const [taskSelectedAction, setTaskSelectedAction] = useState(0);
76
- const [taskFocusedPanel, setTaskFocusedPanel] = useState("tasks");
77
78
  const [taskSearchActive, setTaskSearchActive] = useState(false);
78
79
  const [taskQuery, setTaskQuery] = useState("");
79
80
  const [projects, setProjects] = useState([]);
@@ -280,6 +281,30 @@ export function App(props) {
280
281
  pop();
281
282
  pushToast("success", updated ? t("board.toastTaskUpdated", { id }) : t("board.toastTaskCreated", { id }));
282
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";
283
308
  useBoardKeybindings({
284
309
  confirm,
285
310
  confirmChoice,
@@ -307,9 +332,13 @@ export function App(props) {
307
332
  selectedRunIndex,
308
333
  setSelectedRunIndex,
309
334
  selectedRunCount: selected?.runHistory?.length ?? 0,
310
- focusedPanel,
311
- setFocusedPanel,
312
- selectedAction,
335
+ focusedPanel: derivedFocusedPanel,
336
+ setFocusedPanel: ((p) => {
337
+ const navIdx = navItems.indexOf(p);
338
+ if (navIdx >= 0)
339
+ setNavIndex(navIdx);
340
+ }),
341
+ selectedAction: derivedSelectedAction,
313
342
  setSelectedAction,
314
343
  onAction: handleAction,
315
344
  onOpenRunLog: handleOpenRunLog,
@@ -327,10 +356,23 @@ export function App(props) {
327
356
  tasks: filteredTasks,
328
357
  taskSelectedIndex,
329
358
  setTaskSelectedIndex,
330
- taskSelectedAction,
331
- setTaskSelectedAction,
332
- taskFocusedPanel,
333
- 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
+ }),
334
376
  taskSearchActive,
335
377
  setTaskSearchActive,
336
378
  taskQuery,
@@ -338,9 +380,8 @@ export function App(props) {
338
380
  onTaskAction: handleTaskAction,
339
381
  onCancel: cancelTaskList,
340
382
  onCreateTask: handleCreateTask,
341
- onEnterHeader: (direction) => {
342
- setFocusedPanel(direction === "right" ? "header-tasks" : "header-new");
343
- },
383
+ onToggleContextHelp: () => setContextHelpOpen((v) => !v),
384
+ headerFocused: isHeaderFocused,
344
385
  selectable: stack.includes("create") || stack.includes("task-edit"),
345
386
  });
346
387
  const counts = {
@@ -351,16 +392,14 @@ export function App(props) {
351
392
  idle: loops.filter((l) => l.status === "idle").length,
352
393
  };
353
394
  const mode = resolveMode(confirm, searchActive || taskSearchActive, helpOpen, view);
354
- 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") {
355
396
  createProjectTriggerRef.current?.();
356
397
  }
357
398
  else {
358
399
  push("projects");
359
- } } }), 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) => {
360
- setFocusedPanel(direction === "right" ? "header-tasks" : "header-new");
361
- } })) : (_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 })) : 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 }, `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) {
362
401
  setEditTarget(loop);
363
402
  push("create");
364
- } } }), _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 })] }));
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 })] }));
365
404
  }
366
405
  const TASK_FORM_VIEWS = new Set(["task-create", "task-edit"]);
@@ -2,22 +2,22 @@ import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
2
  import { t } from "../../i18n/index.js";
3
3
  import { useHoverState } from "../hooks/useHoverState.js";
4
4
  import { HOVER_BG } from "../../config/constants.js";
5
- export function getActions(status) {
5
+ const STOP_ACTION = { key: "stop", label: t("board.actionStop") };
6
+ const PAUSE_ACTION = { key: "pause", label: t("board.actionPause") };
7
+ const PLAY_ACTION = { key: "play", label: t("board.actionPlay") };
8
+ const STATUS_ACTIONS = {
9
+ running: [STOP_ACTION],
10
+ waiting: [PAUSE_ACTION, STOP_ACTION],
11
+ paused: [STOP_ACTION, PLAY_ACTION],
12
+ idle: [PLAY_ACTION],
13
+ stopped: [PLAY_ACTION],
14
+ };
15
+ function getActions(status) {
6
16
  const actions = [
7
17
  { key: "edit", label: t("board.actionEdit") },
8
18
  { key: "delete", label: t("board.actionDelete") },
9
19
  ];
10
- if (status === "waiting") {
11
- actions.push({ key: "pause", label: t("board.actionPause") });
12
- actions.push({ key: "stop", label: t("board.actionStop") });
13
- }
14
- else if (status === "paused") {
15
- actions.push({ key: "stop", label: t("board.actionStop") });
16
- actions.push({ key: "play", label: t("board.actionPlay") });
17
- }
18
- else if (status === "idle" || status === "stopped") {
19
- actions.push({ key: "play", label: t("board.actionPlay") });
20
- }
20
+ actions.push(...STATUS_ACTIONS[status]);
21
21
  if (status !== "running") {
22
22
  actions.push({ key: "clone", label: t("board.actionClone") });
23
23
  actions.push({ key: "trigger", label: t("board.actionTrigger") });
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
+ import { useTerminalDimensions } from "@opentui/react";
3
+ import { t } from "../../i18n/index.js";
4
+ export function ContextHelpModal(props) {
5
+ const { onClose } = props;
6
+ const { width } = useTerminalDimensions();
7
+ return (_jsx("box", { onMouseDown: onClose, style: {
8
+ position: "absolute",
9
+ top: 0,
10
+ left: 0,
11
+ width: "100%",
12
+ height: "100%",
13
+ justifyContent: "center",
14
+ alignItems: "center",
15
+ zIndex: 90,
16
+ }, children: _jsxs("box", { title: t("context.helpTitle"), border: true, style: {
17
+ flexDirection: "column",
18
+ padding: 1,
19
+ minWidth: Math.min(72, width - 4),
20
+ backgroundColor: "#111827",
21
+ }, children: [_jsx("text", { fg: "#e5e7eb", children: t("context.helpRules") }), _jsx("text", { children: " " }), _jsx("text", { fg: "#38bdf8", children: "{{key}} Templates" }), _jsx("text", { fg: "#e5e7eb", children: t("context.helpTemplates") }), _jsx("text", { children: " " }), _jsx("text", { fg: "#f59e0b", children: "Caveat" }), _jsx("text", { fg: "#e5e7eb", children: t("context.helpCaveat") }), _jsx("text", { children: " " }), _jsx("text", { fg: "#34d399", children: "jq Tip" }), _jsx("text", { fg: "#e5e7eb", children: t("context.helpJqTip") }), _jsx("text", { children: " " }), _jsx("text", { fg: "#9ca3af", children: "Press any key or click to close" })] }) }));
22
+ }
@@ -8,6 +8,8 @@ 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";
12
+ import { SearchSelect } from "./SearchSelect.js";
11
13
  import { HOVER_BG } from "../../config/constants.js";
12
14
  export const TASK_MODE_INLINE = "inline";
13
15
  export const TASK_MODE_EXISTING = "existing";
@@ -44,25 +46,10 @@ export function CreateView(props) {
44
46
  project: props.initial.project ?? props.currentProjectId ?? "default",
45
47
  });
46
48
  const valuesRef = useRef(values);
47
- const [focusIndex, setFocusIndex] = useState(0);
48
49
  const [error, setError] = useState("");
49
50
  const [isSubmitting, setIsSubmitting] = useState(false);
50
51
  const [selectedTaskName, setSelectedTaskName] = useState(props.selectedTaskName ?? null);
51
52
  const inputRef = useRef(null);
52
- useInputShortcuts(() => {
53
- if (focusIndex >= saveIndex)
54
- return null;
55
- return inputRef.current;
56
- });
57
- useState(() => {
58
- if (values.taskId && !selectedTaskName) {
59
- void listTasks().then((tasks) => {
60
- const found = tasks.find((t) => t.id === values.taskId);
61
- if (found)
62
- setSelectedTaskName(found.name);
63
- }).catch(() => { });
64
- }
65
- });
66
53
  const { width: termWidth } = useTerminalDimensions();
67
54
  const btnWidth = Math.max(10, Math.min(14, Math.floor(termWidth / 6)));
68
55
  const isInline = values.taskMode === TASK_MODE_INLINE;
@@ -76,9 +63,22 @@ export function CreateView(props) {
76
63
  return !isInline;
77
64
  return true;
78
65
  });
79
- const chooseTaskIdx = filteredFields.indexOf("taskId");
80
- const saveIndex = filteredFields.length;
81
- const cancelIndex = filteredFields.length + 1;
66
+ const navItems = [...filteredFields, "save", "cancel"];
67
+ const { setFocusIndex, focusedItem, isFocused } = useTabNav(navItems);
68
+ useInputShortcuts(() => {
69
+ if (focusedItem === "save" || focusedItem === "cancel")
70
+ return null;
71
+ return inputRef.current;
72
+ });
73
+ useState(() => {
74
+ if (values.taskId && !selectedTaskName) {
75
+ void listTasks().then((tasks) => {
76
+ const found = tasks.find((t) => t.id === values.taskId);
77
+ if (found)
78
+ setSelectedTaskName(found.name);
79
+ }).catch(() => { });
80
+ }
81
+ });
82
82
  function updateValues(next) {
83
83
  valuesRef.current = next;
84
84
  setValues(next);
@@ -90,66 +90,29 @@ export function CreateView(props) {
90
90
  setSelectedTaskName(props.selectedTaskName ?? props.selectedTaskId);
91
91
  }
92
92
  useKeyboard((key) => {
93
- if (key.name === "tab") {
94
- setFocusIndex((i) => {
95
- const next = key.shift ? i - 1 : i + 1;
96
- if (next < 0)
97
- return cancelIndex;
98
- if (next > cancelIndex)
99
- return 0;
100
- return next;
101
- });
102
- key.preventDefault();
103
- return;
104
- }
105
- if (key.name === "up" || key.name === "down") {
106
- const field = filteredFields[focusIndex];
107
- if (field === "project" && projectOptions.length > 1) {
108
- const currentIdx = projectOptions.findIndex((p) => p.value === valuesRef.current.project);
109
- const nextIdx = key.name === "down"
110
- ? (currentIdx + 1) % projectOptions.length
111
- : (currentIdx - 1 + projectOptions.length) % projectOptions.length;
112
- const next = projectOptions[nextIdx];
113
- if (next)
114
- updateValues({ ...valuesRef.current, project: next.value });
115
- key.preventDefault();
116
- return;
117
- }
118
- setFocusIndex((i) => {
119
- const next = key.name === "up" ? i - 1 : i + 1;
120
- if (next < 0)
121
- return cancelIndex;
122
- if (next > cancelIndex)
123
- return 0;
124
- return next;
125
- });
126
- key.preventDefault();
127
- return;
128
- }
129
93
  if (key.name === "return" || key.name === "enter" || key.name === " ") {
130
- const field = filteredFields[focusIndex];
131
- if (field === "taskMode") {
94
+ if (focusedItem === "taskMode") {
132
95
  const next = valuesRef.current.taskMode === TASK_MODE_INLINE ? TASK_MODE_EXISTING : TASK_MODE_INLINE;
133
96
  updateValues({ ...valuesRef.current, taskMode: next });
134
97
  key.preventDefault();
135
98
  return;
136
99
  }
137
- if (field === "runNow") {
100
+ if (focusedItem === "runNow") {
138
101
  const next = valuesRef.current.runNow === "y" ? "n" : "y";
139
102
  updateValues({ ...valuesRef.current, runNow: next });
140
103
  key.preventDefault();
141
104
  return;
142
105
  }
143
- if (focusIndex === chooseTaskIdx && !isInline) {
106
+ if (focusedItem === "taskId" && !isInline) {
144
107
  props.onChooseTask();
145
108
  key.preventDefault();
146
109
  return;
147
110
  }
148
- if (focusIndex === saveIndex) {
111
+ if (focusedItem === "save") {
149
112
  void submit(valuesRef.current);
150
113
  key.preventDefault();
151
114
  }
152
- else if (focusIndex === cancelIndex) {
115
+ else if (focusedItem === "cancel") {
153
116
  props.onCancel();
154
117
  key.preventDefault();
155
118
  }
@@ -278,8 +241,8 @@ export function CreateView(props) {
278
241
  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) => {
279
242
  const leftField = filteredFields[row * 2];
280
243
  const rightField = row * 2 + 1 < filteredFields.length ? filteredFields[row * 2 + 1] : null;
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));
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] }));
244
+ 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));
245
+ }) }), _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] }));
283
246
  }
284
247
  function HoverButton(props) {
285
248
  const { isHovered, hoverProps } = useHoverState();
@@ -288,9 +251,8 @@ function HoverButton(props) {
288
251
  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 }) }) }));
289
252
  }
290
253
  function FormRow(props) {
291
- const { field, index, focusIndex, values, valuesRef, updateValues, setFocusIndex, submit, labels, hints, examples, taskModeOptions, runNowOptions, projectOptions, selectedTaskName, fields, onChooseTask, inputRef, style } = props;
254
+ const { field, index, isFocused, values, valuesRef, updateValues, setFocusIndex, submit, labels, hints, examples, taskModeOptions, runNowOptions, projectOptions, selectedTaskName, onChooseTask, inputRef, style } = props;
292
255
  const { isHovered, hoverProps } = useHoverState();
293
- const isFocused = focusIndex === index;
294
256
  const isToggleField = field === "taskMode" || field === "runNow";
295
257
  const isTaskButton = field === "taskId";
296
258
  const isProjectField = field === "project";
@@ -301,15 +263,5 @@ function FormRow(props) {
301
263
  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));
302
264
  }) })) : 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
303
265
  ? t("board.selectedTask", { name: selectedTaskName ?? values.taskId })
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) => {
305
- const isActive = values.project === opt.value;
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));
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: () => {
308
- if (index < fields.length - 1) {
309
- setFocusIndex(index + 1);
310
- }
311
- else {
312
- void submit(valuesRef.current);
313
- }
314
- } }) }))] }));
266
+ : 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); } }) }))] }));
315
267
  }
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
2
  import { useState } 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,15 +13,8 @@ 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"]);
16
17
  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
18
  if (key.name === "escape") {
25
19
  onCancel();
26
20
  return;
@@ -34,14 +28,11 @@ export function CreateProjectModal(props) {
34
28
  return;
35
29
  }
36
30
  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") {
31
+ if (key.name === "left" || key.name === "right") {
43
32
  const idx = PROJECT_COLOR_KEYS.indexOf(selectedColorKey);
44
- setSelectedColorKey(PROJECT_COLOR_KEYS[(idx + 1) % PROJECT_COLOR_KEYS.length] ?? selectedColorKey);
33
+ const dir = key.name === "left" ? -1 : 1;
34
+ setSelectedColorKey(PROJECT_COLOR_KEYS[(idx + dir + PROJECT_COLOR_KEYS.length) % PROJECT_COLOR_KEYS.length] ?? selectedColorKey);
35
+ key.preventDefault();
45
36
  return;
46
37
  }
47
38
  }
@@ -81,9 +72,9 @@ export function CreateProjectModal(props) {
81
72
  padding: 1,
82
73
  minWidth: Math.min(44, width - 4),
83
74
  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) => {
75
+ }, 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
76
  const isActive = selectedColorKey === colorKey;
86
- return (_jsx("box", { onMouseDown: () => { setFocusField("color"); setSelectedColorKey(colorKey); }, style: {
77
+ return (_jsx("box", { onMouseDown: () => { setFocusIndex(1); setSelectedColorKey(colorKey); }, style: {
87
78
  backgroundColor: isActive ? "#1e3a8a" : undefined,
88
79
  paddingLeft: 1,
89
80
  paddingRight: 1,