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 +115 -0
- package/dist/board/App.js +57 -18
- package/dist/board/components/ActionButtons.js +12 -12
- package/dist/board/components/ContextHelpModal.js +22 -0
- package/dist/board/components/CreateForm.js +27 -75
- package/dist/board/components/CreateProjectModal.js +8 -17
- package/dist/board/components/EditProjectModal.js +8 -17
- package/dist/board/components/ProjectsPage.js +43 -36
- package/dist/board/components/SearchSelect.js +99 -0
- package/dist/board/components/TaskForm.js +14 -38
- package/dist/board/focus-context.js +98 -0
- package/dist/board/hooks/useBoardKeybindings.js +5 -47
- package/dist/board/hooks/useTabNav.js +60 -0
- package/dist/board/hooks/useTaskKeybindings.js +8 -30
- package/dist/config/constants.js +2 -0
- package/dist/core/command-runner.js +33 -3
- package/dist/core/context-parser.js +55 -0
- package/dist/core/loop-controller.js +20 -2
- package/dist/core/template.js +3 -0
- package/dist/i18n/en.json +8 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
-
|
|
342
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
80
|
-
const
|
|
81
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
106
|
+
if (focusedItem === "taskId" && !isInline) {
|
|
144
107
|
props.onChooseTask();
|
|
145
108
|
key.preventDefault();
|
|
146
109
|
return;
|
|
147
110
|
}
|
|
148
|
-
if (
|
|
111
|
+
if (focusedItem === "save") {
|
|
149
112
|
void submit(valuesRef.current);
|
|
150
113
|
key.preventDefault();
|
|
151
114
|
}
|
|
152
|
-
else if (
|
|
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,
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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)
|
|
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: () => {
|
|
77
|
+
return (_jsx("box", { onMouseDown: () => { setFocusIndex(1); setSelectedColorKey(colorKey); }, style: {
|
|
87
78
|
backgroundColor: isActive ? "#1e3a8a" : undefined,
|
|
88
79
|
paddingLeft: 1,
|
|
89
80
|
paddingRight: 1,
|