loop-task 1.5.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +141 -21
  2. package/dist/board/App.js +1 -1
  3. package/dist/board/components/ActionButtons.js +1 -1
  4. package/dist/board/components/BoardButton.js +1 -1
  5. package/dist/board/components/ConfirmModal.js +1 -1
  6. package/dist/board/components/ContextHelpModal.js +1 -1
  7. package/dist/board/components/CreateForm.js +1 -1
  8. package/dist/board/components/CreateProjectModal.js +1 -1
  9. package/dist/board/components/DeleteProjectConfirm.js +1 -1
  10. package/dist/board/components/DetailView.js +1 -1
  11. package/dist/board/components/EditProjectModal.js +1 -1
  12. package/dist/board/components/FilterBar.js +1 -1
  13. package/dist/board/components/Footer.js +1 -1
  14. package/dist/board/components/Header.js +1 -1
  15. package/dist/board/components/HelpModal.js +1 -1
  16. package/dist/board/components/Inspector.js +1 -1
  17. package/dist/board/components/LogModal.js +1 -1
  18. package/dist/board/components/Navigator.js +1 -1
  19. package/dist/board/components/ProjectsModal.js +1 -1
  20. package/dist/board/components/ProjectsPage.js +1 -1
  21. package/dist/board/components/RunHistory.js +1 -1
  22. package/dist/board/components/SearchBox.js +1 -1
  23. package/dist/board/components/SearchSelect.js +1 -1
  24. package/dist/board/components/TaskBrowser.js +1 -1
  25. package/dist/board/components/TaskFilterBar.js +1 -1
  26. package/dist/board/components/TaskForm.js +1 -1
  27. package/dist/board/components/Timeline.js +1 -1
  28. package/dist/board/focus-context.js +1 -1
  29. package/dist/board/format.js +15 -6
  30. package/dist/board/index.js +1 -1
  31. package/dist/board/toast.js +1 -1
  32. package/dist/cli/import-validator.js +151 -0
  33. package/dist/cli/import-writer.js +77 -0
  34. package/dist/cli.js +111 -17
  35. package/dist/config/constants.js +52 -0
  36. package/dist/daemon/file-watcher.js +171 -0
  37. package/dist/daemon/http-server.js +605 -0
  38. package/dist/daemon/index.js +36 -6
  39. package/dist/daemon/manager.js +94 -13
  40. package/dist/daemon/projects.js +10 -0
  41. package/dist/daemon/server.js +21 -0
  42. package/dist/daemon/task-manager.js +7 -0
  43. package/dist/esm-loader.js +4 -1
  44. package/dist/hooks/useLoopFormValidation.js +140 -0
  45. package/dist/i18n/en.json +237 -17
  46. package/dist/shared/fs-utils.js +34 -1
  47. package/dist/tui/app.js +675 -151
  48. package/dist/tui/commands.js +114 -0
  49. package/dist/tui/components/ActionButtons.js +49 -0
  50. package/dist/tui/components/Button.js +18 -0
  51. package/dist/tui/components/ChainEditor.js +112 -0
  52. package/dist/tui/components/CommandBuilderField.js +143 -0
  53. package/dist/tui/components/CommandEditorModal.js +159 -0
  54. package/dist/tui/components/CommandInput.js +303 -0
  55. package/dist/tui/components/CommandsBrowserModal.js +77 -0
  56. package/dist/tui/components/ConfirmModal.js +9 -0
  57. package/dist/tui/components/ContextHelpModal.js +8 -0
  58. package/dist/tui/components/CreateForm.js +223 -0
  59. package/dist/tui/components/DebugPanel.js +8 -0
  60. package/dist/tui/components/ExportModal.js +37 -0
  61. package/dist/tui/components/FilterBar.js +13 -0
  62. package/dist/tui/components/FocusableButton.js +16 -0
  63. package/dist/tui/components/FocusableInput.js +69 -0
  64. package/dist/tui/components/FocusableList.js +46 -0
  65. package/dist/tui/components/FocusableSearchSelect.js +82 -0
  66. package/dist/tui/components/Footer.js +42 -0
  67. package/dist/tui/components/Header.js +40 -0
  68. package/dist/tui/components/HelpGuideModal.js +21 -0
  69. package/dist/tui/components/HelpModal.js +59 -0
  70. package/dist/tui/components/InlineCommandEditor.js +143 -0
  71. package/dist/tui/components/Inspector.js +31 -0
  72. package/dist/tui/components/LeftPanel.js +33 -0
  73. package/dist/tui/components/LogModal.js +121 -0
  74. package/dist/tui/components/Modal.js +11 -0
  75. package/dist/tui/components/Navigator.js +68 -0
  76. package/dist/tui/components/PatchEditForm.js +78 -0
  77. package/dist/tui/components/ProjectForm.js +64 -0
  78. package/dist/tui/components/ProjectsModal.js +40 -0
  79. package/dist/tui/components/ProjectsPage.js +99 -0
  80. package/dist/tui/components/RightPanel.js +34 -0
  81. package/dist/tui/components/RunHistory.js +108 -0
  82. package/dist/tui/components/SearchSelect.js +67 -0
  83. package/dist/tui/components/SelectModal.js +76 -0
  84. package/dist/tui/components/TabBar.js +20 -0
  85. package/dist/tui/components/TaskBrowser.js +96 -0
  86. package/dist/tui/components/TaskFilterBar.js +9 -0
  87. package/dist/tui/components/TaskForm.js +134 -0
  88. package/dist/tui/components/TaskPickerModal.js +60 -0
  89. package/dist/tui/components/Toast.js +37 -0
  90. package/dist/tui/components/WelcomeScreen.js +70 -0
  91. package/dist/tui/components/WizardForm.js +178 -0
  92. package/dist/tui/daemon.js +142 -0
  93. package/dist/tui/format.js +111 -0
  94. package/dist/tui/hooks/useBreakpoint.js +7 -0
  95. package/dist/tui/hooks/useHoverState.js +11 -0
  96. package/dist/tui/hooks/useLogStream.js +32 -0
  97. package/dist/tui/hooks/useLoopPolling.js +28 -0
  98. package/dist/tui/index.js +24 -0
  99. package/dist/tui/router.js +16 -0
  100. package/dist/tui/state.js +175 -0
  101. package/dist/tui/theme.js +96 -0
  102. package/dist/tui/types.js +1 -0
  103. package/dist/tui/utils/paste.js +10 -0
  104. package/dist/tui/utils/validation.js +56 -0
  105. package/package.json +13 -8
package/README.md CHANGED
@@ -12,6 +12,22 @@
12
12
  [![license](https://img.shields.io/npm/l/loop-task?style=flat-square&color=black)](./LICENSE)
13
13
  [![node](https://img.shields.io/node/v/loop-task?style=flat-square&color=black)](https://nodejs.org)
14
14
 
15
+ ## What's new in v2.0.0
16
+
17
+ - **Ink 7 + React 19 TUI**: Rebuilt the entire terminal UI on [Ink 7](https://github.com/vadimdemedes/ink) (React 19 for CLI). No more Bun dependency — runs on any Node >= 20.
18
+ - **HTTP API + Swagger**: The daemon now exposes a REST + SSE API on `localhost:8845`. Browse it at `http://127.0.0.1:8845/api/docs` (Swagger UI) or fetch the OpenAPI spec at `/api/openapi.json`. Manage loops, tasks, projects, and logs from any HTTP client.
19
+ - **Hot-reloading configs**: Edit `~/.loop-cli/loops.json`, `tasks.json`, or `projects.json` in any editor and the daemon auto-reloads in real-time.
20
+ - **Export/Import**: `loop-task export > backup.json` and `loop-task import backup.json` for backup and sharing.
21
+ - **CLI API**: `loop-task status --json` for scripting and automation. `loop-task api` prints HTTP API endpoints.
22
+ - **Polished loop forms**: Task mode toggle (inline command vs. existing task), per-field validation, smart CWD defaults, clipboard copy, and direct edit navigation — across both board and TUI.
23
+ - **Rich log viewer**: Search/filter log output, fold chain sections, scroll lock, copy to clipboard.
24
+ - **Run history trends**: Sparklines of durations, success/failure streaks, average duration.
25
+ - **Chain visual editor**: Tree view of task chains with success/failure branches.
26
+ - **First-run onboarding**: Welcome screen with example loops when no loops exist.
27
+ - **Daemon push notifications**: Subscribe to real-time events via IPC or SSE (`GET /api/events`).
28
+ - **Docker support**: `docker run -v ~/.loop-cli:/root/.loop-cli loop-task`.
29
+ - **GitHub Actions CI**: Runs typecheck, lint, test, build on ubuntu/macos/windows.
30
+
15
31
  </div>
16
32
 
17
33
  ## Loop engineering
@@ -21,7 +37,7 @@
21
37
  `loop-task` is that heartbeat as a tiny local primitive. Some examples:
22
38
 
23
39
  <div align="center">
24
- <img src="https://raw.githubusercontent.com/CKGrafico/loop-cli/refs/heads/main/demo.gif" alt="opencode-task demo" width="700" />
40
+ <img src="https://raw.githubusercontent.com/CKGrafico/loop-task/refs/heads/main/demo.gif" alt="loop-task demo" width="700" />
25
41
  </div>
26
42
 
27
43
  ```bash
@@ -46,12 +62,13 @@ No cron files to maintain and no daemon to babysit: loops persist across reboots
46
62
 
47
63
  ```bash
48
64
  npm install -g loop-task
49
- loop-task # open the board (requires Bun)
65
+ loop-task # open the board
50
66
  loop-task start # start the daemon, restore persisted loops
51
67
  loop-task new 30m -- npm test # create a background loop
52
68
  loop-task run --now 10s -- echo hi # run a loop in the foreground
53
69
  loop-task stop <id> # stop a frozen loop and kill its child process
54
70
  loop-task restart # kill daemon + all loops, restart fresh
71
+ loop-task api # show HTTP API endpoints (Swagger, OpenAPI)
55
72
  ```
56
73
 
57
74
  Or run it directly:
@@ -63,16 +80,13 @@ npx loop-task new 30m -- npm test
63
80
 
64
81
  ## Requirements
65
82
 
66
- - **Node.js >= 20** - required for all commands
67
- - **Bun >= 1.2** - required for the interactive board only
68
-
69
- Install Bun:
83
+ - **Node.js >= 20** - required for all commands including the board
70
84
 
71
85
  ```bash
72
- npm install -g bun
86
+ npm install -g loop-task
73
87
  ```
74
88
 
75
- `start`, `new`, and `run` work with Node alone. The board auto-delegates to Bun when needed.
89
+ All commands (`start`, `new`, `run`, `board`) work with Node alone.
76
90
 
77
91
  ## Concepts
78
92
 
@@ -134,13 +148,17 @@ Colors can be a name (`white`, `cyan`, `green`, `yellow`, `orange`, `pink`) or a
134
148
 
135
149
  | Command | Description |
136
150
  | ------- | ----------- |
137
- | `loop-task` | Open the interactive board (requires Bun) |
151
+ | `loop-task` | Open the interactive board |
138
152
  | `loop-task start` | Start the background daemon, restore persisted loops |
139
153
  | `loop-task new <interval> -- <command>` | Create a background loop (creates an inline task) |
140
154
  | `loop-task new <interval> --project <name> -- <command>` | Create a loop assigned to a project |
141
155
  | `loop-task run <interval> -- <command>` | Run a loop in the foreground |
142
156
  | `loop-task stop <id>` | Stop a loop and interrupt its running child process |
143
157
  | `loop-task restart` | Kill the daemon and all running loops, then restart fresh |
158
+ | `loop-task status [--json]` | Show status of all loops (JSON optional for scripting) |
159
+ | `loop-task export [file]` | Export all configs to JSON file (or stdout) |
160
+ | `loop-task import <file>` | Import configs from file (triggers hot-reload) |
161
+ | `loop-task api` | Show HTTP API endpoints (base URL, Swagger UI, OpenAPI spec) |
144
162
  | `loop-task project list` | List all projects |
145
163
  | `loop-task project new <name> [--color <color>]` | Create a project |
146
164
  | `loop-task project rename <id\|name> <new-name>` | Rename a project |
@@ -209,6 +227,15 @@ esc quit
209
227
 
210
228
  Destructive actions (pause, force run, delete) prompt a confirmation before executing.
211
229
 
230
+ ### Copy & paste in the command bar
231
+
232
+ The bottom command bar is a normal terminal input, so use your terminal's own clipboard gestures — they work in every terminal (including the VS Code integrated terminal, where Ctrl+C/V are captured by the editor):
233
+
234
+ - **Paste** with **Ctrl+Shift+V** (Windows/Linux), **Cmd+V** (macOS), or **right-click**. Multi-line pastes collapse to a single line.
235
+ - **Ctrl+U** clears the command bar (select-all + delete).
236
+ - **`c`** copies the selected loop/task's command to the clipboard.
237
+ - To copy arbitrary text, drag-select in the terminal and use its native copy (Ctrl+Shift+C / right-click).
238
+
212
239
  ### Pause vs Stop
213
240
 
214
241
  - **Pause** (`p`) - temporarily halts the loop. Resuming continues the original schedule (e.g., a loop that runs every 6h at :00 paused at 12:00 and resumed at 14:00 will still fire at 16:00).
@@ -217,9 +244,9 @@ Destructive actions (pause, force run, delete) prompt a confirmation before exec
217
244
  ## How it works
218
245
 
219
246
  ```
220
- loop-task (board) ──IPC──► daemon ──► loop 1 ──► task (command)
221
- ├──► loop 2 ──► task ──► on-success task
222
- └──► loop 3 ──► task ──► on-failure task
247
+ loop-task (board) ──IPC──► daemon ──► loop 1 ──► task (command)
248
+ HTTP client (curl) ──HTTP──► ├──► loop 2 ──► task ──► on-success task
249
+ browser (Swagger) ──HTTP──► └──► loop 3 ──► task ──► on-failure task
223
250
  ```
224
251
 
225
252
  - The **daemon** is a background process that manages all loops and tasks. It starts automatically when you run `loop-task start` or any command that needs it.
@@ -276,7 +303,7 @@ A four-task chain that finds an issue, marks it in-progress, rewrites it with AI
276
303
  **Task 1** (primary): Find an issue to refine
277
304
 
278
305
  ```bash
279
- gh issue list --label "to refine" --limit 1 --json number,title,body --jq '{number: .[0].number, title: .[0].title, body: .[0].body}'
306
+ gh issue list --label "to refine" --json number,title,body --jq '{number: .[0].number, title: .[0].title, body: .[0].body}'
280
307
  ```
281
308
 
282
309
  stdout: `{"number":123,"title":"Fix login","body":"It doesn't work"}`
@@ -330,7 +357,7 @@ A four-task chain that finds an issue to implement, marks it in-progress, runs a
330
357
  **Task 1** (primary): Find an issue to implement (or exit if one is already in progress)
331
358
 
332
359
  ```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}'
360
+ gh issue list --label "implementing" --json number --jq 'length == 0' | grep -q true && gh issue list --label "to implement" --json number,title,body --jq '{number: .[0].number, title: .[0].title, body: .[0].body}'
334
361
  ```
335
362
 
336
363
  stdout: `{"number":456,"title":"Add dark mode toggle","body":"Users want a dark theme"}`
@@ -367,19 +394,92 @@ interpolated: `git push && git fetch origin && [ "$(git rev-parse HEAD)" = "$(gi
367
394
 
368
395
  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
396
 
397
+ ## Docker
398
+
399
+ ```bash
400
+ # Run the board in a container
401
+ docker run -it -v ~/.loop-cli:/root/.loop-cli loop-task
402
+
403
+ # Check status
404
+ docker run -v ~/.loop-cli:/root/.loop-cli loop-task status --json
405
+
406
+ # Create a background loop
407
+ docker run -v ~/.loop-cli:/root/.loop-cli loop-task new 30m -- npm test
408
+ ```
409
+
410
+ ## HTTP API
411
+
412
+ The daemon exposes a REST + SSE API on `localhost:8845` (configurable via `LOOP_CLI_HTTP_PORT`). It starts automatically with the daemon — no extra flags needed.
413
+
414
+ ### Quick reference
415
+
416
+ ```bash
417
+ # List all loops
418
+ curl http://127.0.0.1:8845/api/loops
419
+
420
+ # Create a loop
421
+ curl -X POST http://127.0.0.1:8845/api/loops \
422
+ -H "Content-Type: application/json" \
423
+ -d '{"command":"echo hi","intervalHuman":"5m","description":"test"}'
424
+
425
+ # Get a single loop
426
+ curl http://127.0.0.1:8845/api/loops/abc123
427
+
428
+ # Pause / resume / trigger / stop / delete
429
+ curl -X POST http://127.0.0.1:8845/api/loops/abc123/pause
430
+ curl -X POST http://127.0.0.1:8845/api/loops/abc123/resume
431
+ curl -X POST http://127.0.0.1:8845/api/loops/abc123/trigger
432
+ curl -X POST http://127.0.0.1:8845/api/loops/abc123/stop
433
+ curl -X DELETE http://127.0.0.1:8845/api/loops/abc123
434
+
435
+ # Fetch logs (last 50 lines)
436
+ curl http://127.0.0.1:8845/api/loops/abc123/logs?tail=50
437
+
438
+ # Stream logs in real-time (SSE)
439
+ curl -N http://127.0.0.1:8845/api/loops/abc123/logs/stream
440
+
441
+ # Subscribe to daemon events (SSE)
442
+ curl -N http://127.0.0.1:8845/api/events
443
+
444
+ # Tasks and projects
445
+ curl http://127.0.0.1:8845/api/tasks
446
+ curl http://127.0.0.1:8845/api/projects
447
+ ```
448
+
449
+ ### Interactive docs
450
+
451
+ - **Swagger UI**: `http://127.0.0.1:8845/api/docs`
452
+ - **OpenAPI 3.0 spec**: `http://127.0.0.1:8845/api/openapi.json`
453
+
454
+ ### From the CLI/TUI
455
+
456
+ - `loop-task api` — prints all API endpoints to stdout
457
+ - Board: press **Ctrl+G** or type `api` — shows a toast with API info
458
+
459
+ ### Response format
460
+
461
+ All responses use a consistent JSON envelope:
462
+
463
+ ```json
464
+ {"ok": true, "data": ...} // success (200/201)
465
+ {"ok": false, "error": {"message": "..."}} // error (400/404/405/500)
466
+ ```
467
+
468
+ The API binds to `127.0.0.1` only — it is not reachable from the network. If the port is already in use, the daemon skips the HTTP server and continues with IPC only.
469
+
370
470
  ## Development
371
471
 
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.
472
+ Requires [Node.js](https://nodejs.org) >= 20. Uses [pnpm](https://pnpm.io) for package management.
373
473
 
374
474
  ```bash
375
- bun install
475
+ pnpm install
376
476
  npm run build
377
477
  ```
378
478
 
379
479
  Run locally:
380
480
 
381
481
  ```bash
382
- bun run dev # board
482
+ tsx src/cli.ts # board
383
483
  node dist/entry.js new --now 30m -- npm test # background loop
384
484
  node dist/entry.js run --now --max-runs 1 10s -- echo hello # foreground
385
485
  ```
@@ -387,12 +487,32 @@ node dist/entry.js run --now --max-runs 1 10s -- echo hello # foreground
387
487
  Quality gates:
388
488
 
389
489
  ```bash
390
- bun run typecheck
391
- bun run lint
392
- bun run test
393
- npm run build
490
+ pnpm run typecheck # tsc --noEmit
491
+ pnpm run lint # eslint src/ tests/
492
+ pnpm run test # vitest run
493
+ pnpm run build # tsc -p tsconfig.build.json
394
494
  ```
395
495
 
496
+ ### Testing the board in a browser (ttyd)
497
+
498
+ > **Agents: Do NOT use ttyd unless the user explicitly asks you to check the CLI in a browser.** It is never the default. Do not start a ttyd server on your own for "manual pass" tasks or visual QA — those are for the human. Reach for ttyd only when the user says "check the board in the browser", "use ttyd", or similar.
499
+
500
+ The board is an interactive TUI, so it needs a real terminal — you can't drive it from a piped/captured shell (and neither can an AI agent). [`ttyd`](https://github.com/tsl0922/ttyd) shares a terminal over HTTP, which makes the board reachable from a browser and scriptable by browser-automation agents — but only when explicitly requested.
501
+
502
+ Install ttyd (see the [ttyd README](https://github.com/tsl0922/ttyd#installation) — e.g. `winget install tsl0922.ttyd`, `brew install ttyd`, or `apt install ttyd`), then serve the board from an interactive terminal:
503
+
504
+ ```bash
505
+ # Point -w at the repo (absolute path) and run the dev board:
506
+ ttyd -W -w "C:\Projects\Personal\loop-cli" -p 7681 pnpm run dev
507
+
508
+ # The built entry works too (after `npm run build`), with the same -w:
509
+ ttyd -W -w "C:\Projects\Personal\loop-cli" -p 7681 node dist/entry.js
510
+ ```
511
+
512
+ Open `http://localhost:7681` in a browser and use the board as normal. `-W` makes it writable so keystrokes reach the TUI. Handy for demos, for testing on a machine without a good local terminal, and for letting an AI agent drive the board (navigate, send keys, screenshot; ttyd renders via xterm.js on a `<canvas>`, so read state from screenshots, not page text).
513
+
514
+ > **Windows note:** always pass `-w "<absolute repo path>"`. Without it, ttyd gives the spawned command no valid working directory and it fails with `CreateProcessW failed with error 267` — for *every* command (`pnpm`, `npx`, `node` all fail the same way; it is not a `.cmd`-shim issue). On macOS/Linux `-w` is optional but harmless. Start ttyd from a real interactive terminal; a detached/console-less launch can crash its ConPTY on Windows.
515
+
396
516
  ## License
397
517
 
398
518
  MIT
package/dist/board/App.js CHANGED
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useRef, useState } from "react";
3
3
  import { applyLoopFilters, cycleSortMode, cycleStatusFilter, defaultFilters, } from "./state.js";
4
4
  import { ToastStack, useToasts } from "./toast.js";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx } from "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";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useHoverState } from "../hooks/useHoverState.js";
3
3
  const PRIMARY_REST = "#1e3a5f";
4
4
  const PRIMARY_HOVER = "#2a4f7a";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useTerminalDimensions } from "@opentui/react";
3
3
  import { t } from "../../i18n/index.js";
4
4
  import { useHoverState } from "../hooks/useHoverState.js";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useTerminalDimensions } from "@opentui/react";
3
3
  import { t } from "../../i18n/index.js";
4
4
  export function ContextHelpModal(props) {
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useRef, useState } from "react";
3
3
  import { useKeyboard, useTerminalDimensions } from "@opentui/react";
4
4
  import fs from "node:fs";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useRef } from "react";
3
3
  import { useTabNav } from "../hooks/useTabNav.js";
4
4
  import { useKeyboard, useTerminalDimensions } from "@opentui/react";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import { useKeyboard, useTerminalDimensions } from "@opentui/react";
4
4
  import { t } from "../../i18n/index.js";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from "react";
3
3
  import { t } from "../../i18n/index.js";
4
4
  import { commandLine, describeLoop, statusColor, statusLabel, timingLabel } from "../format.js";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useRef } from "react";
3
3
  import { useTabNav } from "../hooks/useTabNav.js";
4
4
  import { useKeyboard, useTerminalDimensions } from "@opentui/react";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "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";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { t } from "../../i18n/index.js";
3
3
  import { ENTITY_COLORS } from "../../config/constants.js";
4
4
  export function Footer(props) {
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useTerminalDimensions } from "@opentui/react";
3
3
  import { HEADER_COMPACT_WIDTH, ENTITY_COLORS } from "../../config/constants.js";
4
4
  import { t } from "../../i18n/index.js";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useTerminalDimensions } from "@opentui/react";
3
3
  import { t } from "../../i18n/index.js";
4
4
  export function HelpModal(props) {
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { t } from "../../i18n/index.js";
3
3
  import { commandLine, describeLoop, statusColor, statusLabel, timeAgo } from "../format.js";
4
4
  export function Inspector(props) {
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import { useKeyboard, useTerminalDimensions } from "@opentui/react";
4
4
  import { t } from "../../i18n/index.js";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import { useTerminalDimensions } from "@opentui/react";
4
4
  import { t } from "../../i18n/index.js";
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import { useKeyboard, useTerminalDimensions } from "@opentui/react";
4
4
  import { t } from "../../i18n/index.js";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useEffect, useRef } from "react";
3
3
  import { useKeyboard } from "@opentui/react";
4
4
  import { t } from "../../i18n/index.js";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useMemo } from "react";
3
3
  import { useTerminalDimensions } from "@opentui/react";
4
4
  import { t } from "../../i18n/index.js";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useRef } from "react";
3
3
  import { t } from "../../i18n/index.js";
4
4
  import { useInputShortcuts } from "../hooks/useInputShortcuts.js";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useRef, useState, useMemo, useEffect } from "react";
3
3
  import { useKeyboard } from "@opentui/react";
4
4
  import { t } from "../../i18n/index.js";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef } from "react";
3
3
  import { useTerminalDimensions } from "@opentui/react";
4
4
  import { t } from "../../i18n/index.js";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { SearchBox } from "./SearchBox.js";
3
3
  export function TaskFilterBar(props) {
4
4
  const { query, searchActive, focusedPanel, onQueryChange, onSearchActivate, onSearchDismiss } = props;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useRef, useState } from "react";
3
3
  import crypto from "node:crypto";
4
4
  import { useKeyboard, useTerminalDimensions } from "@opentui/react";
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { t } from "../../i18n/index.js";
3
3
  export function Timeline(props) {
4
4
  return (_jsx("scrollbox", { title: t("board.timelineTitle"), border: true, style: { flexGrow: 1, backgroundColor: "#0b0b0b" }, stickyScroll: true, stickyStart: "bottom", children: props.logLines.length === 0 ? (_jsx("text", { fg: "#9ca3af", children: t("board.timelineEmpty") })) : (props.logLines.map((line, index) => _jsx("text", { children: line }, index))) }));
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useState, useCallback, useRef, useEffect } from "react";
3
3
  import { useKeyboard } from "@opentui/react";
4
4
  const FocusContext = createContext(null);
@@ -1,9 +1,15 @@
1
1
  import { t } from "../i18n/index.js";
2
+ export function unescapeCommand(str) {
3
+ return str
4
+ .replace(/\\\\/g, "\x00")
5
+ .replace(/\\"/g, '"')
6
+ .replace(/\x00/g, "\\");
7
+ }
2
8
  export function quoteArg(arg) {
3
9
  return /[\s"]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
4
10
  }
5
11
  export function commandLine(command, args) {
6
- return [command, ...args.map(quoteArg)].join(" ").trim();
12
+ return [unescapeCommand(command), ...args.map((a) => quoteArg(unescapeCommand(a)))].join(" ").trim();
7
13
  }
8
14
  export function formatCmd(command, args, max = 24) {
9
15
  const full = commandLine(command, args);
@@ -51,7 +57,7 @@ export function timeUntil(iso) {
51
57
  }
52
58
  const STATUS_COLORS = {
53
59
  running: "#4ade80",
54
- waiting: "#38bdf8",
60
+ waiting: "#6b7280",
55
61
  paused: "#facc15",
56
62
  idle: "#fb923c",
57
63
  stopped: "#f87171",
@@ -91,12 +97,15 @@ export function formatRunTime(iso) {
91
97
  const d = new Date(iso);
92
98
  return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false });
93
99
  }
100
+ export function formatDate(iso) {
101
+ const d = new Date(iso);
102
+ const date = d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
103
+ const time = d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
104
+ return `${date} ${time}`;
105
+ }
94
106
  export function sinceLabel(loop) {
95
107
  const ts = loop.sessionStartedAt ?? loop.createdAt;
96
108
  if (!ts)
97
109
  return t("format.dash");
98
- const d = new Date(ts);
99
- const date = d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
100
- const time = d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
101
- return `${date} ${time}`;
110
+ return formatDate(ts);
102
111
  }
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createCliRenderer } from "@opentui/core";
3
3
  import { createRoot } from "@opentui/react";
4
4
  import { App } from "./App.js";
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useCallback, useRef, useState } from "react";
3
3
  import { TOAST_MAX, TOAST_TIMEOUT_MS } from "../config/constants.js";
4
4
  export function useToasts() {