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/dist/i18n/en.json CHANGED
@@ -114,7 +114,7 @@
114
114
  "format.hrsAhead": "{hrs}h",
115
115
  "format.daysAhead": "{days}d",
116
116
  "format.timingPaused": "paused",
117
- "format.timingIdle": "stopped",
117
+ "format.timingIdle": "not scheduled",
118
118
  "format.timingNext": "in {timeAgo}",
119
119
  "format.timingLast": "last {timeAgo}",
120
120
  "format.timingNew": "new",
@@ -140,33 +140,44 @@
140
140
  "board.viewLoopsLabel": "View Loops",
141
141
  "board.headerSince": "SINCE",
142
142
  "board.headerRuns": "RUNS",
143
- "board.headerSkipped": "SKP",
143
+ "board.headerSkipped": "SKIP",
144
144
  "board.headerStatus": "STATUS",
145
145
  "board.headerDescription": "DESCRIPTION",
146
146
  "board.headerTiming": "TIMING",
147
147
  "board.headerExitRuns": "EXIT RUNS",
148
148
  "board.navigatorTitle": " Loops {visible}/{total} sort:{sort} status:{status} ",
149
+ "board.navigatorCount": " Loops {visible}/{total} ",
150
+ "board.chipFilter": "filter: {query}",
151
+ "board.chipProject": "project:",
152
+ "board.chipStatus": "status:",
153
+ "board.chipSort": "sort:",
154
+ "board.chipAll": "all",
155
+ "board.chipSearch": "search: {query}",
156
+ "board.chipLoops": "loops:",
157
+ "board.chipType": "type:",
158
+ "board.runHistoryFailStreak": "streak:{count} fail",
159
+ "board.runHistoryLastFail": "last fail:{ago}",
149
160
  "board.noMatch": " No loops match the current view",
150
161
  "board.inspectorTitle": " Inspector ",
151
162
  "board.inspectorEmpty": " Select a loop to view details",
152
- "board.fieldId": "ID: ",
153
- "board.fieldDesc": "Desc: ",
154
- "board.fieldCommand": "Command: ",
155
- "board.fieldTask": "Task: ",
156
- "board.fieldDir": "Dir: ",
157
- "board.fieldInterval": "Interval: ",
158
- "board.fieldStatus": "Status: ",
159
- "board.fieldRuns": "Runs: ",
160
- "board.fieldLastRun": "Last run: ",
163
+ "board.fieldId": "ID:",
164
+ "board.fieldDesc": "Desc:",
165
+ "board.fieldCommand": "Command:",
166
+ "board.fieldTask": "Task:",
167
+ "board.fieldDir": "Dir:",
168
+ "board.fieldInterval": "Interval:",
169
+ "board.fieldStatus": "Status:",
170
+ "board.fieldRuns": "Runs:",
171
+ "board.fieldLastRun": "Last run:",
161
172
  "board.fieldLastExit": "Last exit:",
162
- "board.fieldNextRun": "Next run: ",
163
- "board.fieldPid": "PID: ",
173
+ "board.fieldNextRun": "Next run:",
174
+ "board.fieldPid": "PID:",
164
175
  "board.inherit": "(inherit)",
165
176
  "board.unlimited": "∞",
166
177
  "board.timelineTitle": " Timeline ",
167
178
  "board.timelineEmpty": " No output yet.",
168
179
  "board.runHistoryTitle": " Run History ",
169
- "board.runHistoryTitleHint": " Run History (Press Enter to view log) ",
180
+ "board.runHistoryTitleHint": " Run History (Enter to view log) ",
170
181
  "board.runHistoryEmpty": " No runs yet.",
171
182
  "board.runHistoryTime": "TIME",
172
183
  "board.runHistoryDesc": "DESCRIPTION",
@@ -175,6 +186,12 @@
175
186
  "board.logModalTitle": " Run Log {icon} {time} ",
176
187
  "board.logModalLoading": " Loading...",
177
188
  "board.logModalEmpty": " No output captured.",
189
+ "board.logModalNoMatches": "No matches",
190
+ "board.logModalRunning": "Running",
191
+ "board.logModalExit": "exit {code}",
192
+ "board.logModalDuration": "duration",
193
+ "board.logModalSearchHint": "type to filter, enter to apply, esc to close",
194
+ "board.logModalFooterHints": "/:search c:copy up/down:scroll esc:close",
178
195
  "board.logModalEscClose": "esc to close",
179
196
  "board.logModalRunning": "running…",
180
197
  "board.detailTitle": " Loop Detail ",
@@ -245,11 +262,25 @@
245
262
  "board.hintRunNow": "run once now then every interval, or wait the first interval",
246
263
  "board.hintMaxRuns": "Stop after N runs. Leave blank to run forever",
247
264
  "board.hintTask": "Pick a previously defined task",
265
+ "wizard.taskPickerHint": "Press {action} to browse and search tasks",
266
+ "wizard.cmdEditorHint": "Press {action} to open the command editor",
267
+ "taskPicker.title": "Choose a task",
268
+ "taskPicker.searchPlaceholder": "Type to search tasks...",
269
+ "taskPicker.empty": "No tasks found",
270
+ "taskPicker.hint": "up/down to navigate, enter to select, esc to cancel",
271
+ "selectModal.searchPlaceholder": "Type to filter...",
272
+ "selectModal.empty": "No matches",
273
+ "selectModal.hint": "up/down to navigate, enter to select, esc to cancel",
274
+ "selectModal.fieldHint": "Press {action} to change",
275
+ "cmdEditor.title": "Command Editor",
276
+ "cmdEditor.hint": "enter: new line . ctrl+s: save . esc: cancel",
277
+ "cmdEditor.lines": "lines",
278
+ "cmdEditor.inlineHint": "enter: new line . ctrl+v: paste . ctrl+y: copy . tab: next field . ctrl+s: save",
248
279
  "board.exampleInterval": "30m",
249
280
  "board.exampleCommand": "opencode run \"search missing translations and translate them, 3 maximum\" --model \"opencode/big-pickle\"",
250
281
  "board.exampleDescription": "translate missing strings",
251
- "board.runNowNo": "No - wait the first interval",
252
- "board.runNowYes": "Yes - run now, then every interval",
282
+ "board.runNowNo": "No",
283
+ "board.runNowYes": "Yes - run now",
253
284
  "board.cwdMissing": "Working directory does not exist: {cwd}",
254
285
  "board.createTitle": " New Loop ",
255
286
  "board.editTitle": " Edit Loop (will pause) ",
@@ -298,6 +329,7 @@
298
329
  "board.toastTaskUpdated": "Task updated {id}",
299
330
  "board.toastTaskSelected": "Selected task \"{desc}\"",
300
331
  "board.toastCopied": "Copied to clipboard",
332
+ "board.toastTextCopied": "Text Copied",
301
333
  "board.copyCommand": "Copy",
302
334
  "board.logWaiting": " Waiting for live output...",
303
335
  "board.logStreamError": "Log stream error: {message}",
@@ -339,6 +371,13 @@
339
371
  "board.hintKeyYN": "y/n",
340
372
  "board.taskBrowserTitle": " Tasks {visible}/{total} ",
341
373
  "board.taskBrowserEmpty": " No tasks defined yet. Press n to create one.",
374
+ "board.taskInspectorTitle": " Task Inspector ",
375
+ "board.taskInspectorEmpty": " Select a task to inspect",
376
+ "board.taskFieldName": "Name:",
377
+ "board.taskFieldId": "ID:",
378
+ "board.taskFieldCommand": "Command:",
379
+ "board.taskFieldChain": "Chain:",
380
+ "board.taskFieldCreated": "Created:",
342
381
  "board.taskHeaderName": "NAME",
343
382
  "board.taskHeaderCommand": "COMMAND",
344
383
  "board.taskHeaderChains": "CHAINS",
@@ -383,15 +422,196 @@
383
422
  "project.keyDeleteHint": "d: delete",
384
423
  "project.error.nameRequired": "Project name is required",
385
424
  "project.error.nameEmpty": "Project name cannot be empty",
425
+ "project.wizard.namePrompt": "What is the project name?",
426
+ "project.wizard.nameHint": "A short name for this project",
427
+ "project.wizard.colorPrompt": "Pick a color for this project",
428
+ "project.wizard.colorHint": "Choose a color to identify this project",
386
429
  "project.error.updateFailed": "Failed to update project",
387
430
  "project.error.deleteFailed": "Failed to delete project",
388
431
  "project.toastCreated": "Project \"{name}\" created",
389
432
  "project.toastUpdated": "Project \"{name}\" updated",
390
433
  "project.toastDeleted": "Project \"{name}\" deleted",
434
+ "project.filterHasLoopsTitle": " Loops l ",
435
+ "project.filterHasLoopsAll": "all",
436
+ "project.filterHasLoopsWithLoops": "with loops",
437
+ "project.filterHasLoopsEmpty": "empty",
438
+ "project.filterIsSystemTitle": " Type t ",
439
+ "project.filterIsSystemAll": "all",
440
+ "project.filterIsSystemSystem": "system",
441
+ "project.filterIsSystemUser": "user",
442
+ "project.sortTitle": " Order o ",
443
+ "project.sortName": "name",
444
+ "project.sortLoopCount": "loop count",
445
+ "project.sortCreatedDate": "created",
446
+ "project.navigatorTitle": " Projects {visible}/{total} sort:{sort} loops:{hasLoops} ",
447
+ "project.noMatch": " No projects match the current view",
448
+ "project.headerName": "NAME",
449
+ "project.headerLoops": "LOOPS",
450
+ "project.headerCreated": "CREATED",
451
+ "project.inspectorEmpty": " Select a project to view details",
452
+ "project.fieldId": "ID: ",
453
+ "project.fieldCreated": "Created: ",
454
+ "project.fieldLoops": "Loops: ",
455
+ "project.hintSearch": "search",
391
456
  "context.truncationWarning": "stdout exceeded 1MB - context was truncated",
392
457
  "context.helpTitle": "Chain Context Sharing",
393
458
  "context.helpRules": "JSON output merges its keys. JSONL merges each line as a separate key. Plain text is stored under the \"output\" key. Empty output produces no change.",
394
459
  "context.helpTemplates": "Use {{key}} in commands and args - replaced with context values from earlier tasks in the chain.",
395
460
  "context.helpCaveat": "Plain text stdout overwrites the \"output\" key. Use JSON with named keys when data must survive across tasks.",
396
- "context.helpJqTip": "Tip: use --jq '{key: .field}' to wrap any value in a named JSON key"
461
+ "context.helpJqTip": "Tip: use --jq '{key: .field}' to wrap any value in a named JSON key",
462
+ "cmd.edit": "Edit selected loop",
463
+ "cmd.editTask": "Edit selected task",
464
+ "cmd.editProject": "Edit selected project",
465
+ "cmd.delete": "Delete selected loop",
466
+ "cmd.deleteTask": "Delete selected task",
467
+ "cmd.deleteProject": "Delete selected project",
468
+ "cmd.pause": "Pause selected loop",
469
+ "cmd.play": "Play resume loop",
470
+ "cmd.stop": "Stop selected loop",
471
+ "cmd.clone": "Clone selected loop",
472
+ "cmd.trigger": "Trigger run now",
473
+ "cmd.logs": "Open logs viewer",
474
+ "cmd.newLoop": "New loop",
475
+ "cmd.newTask": "New task",
476
+ "cmd.newProject": "New project",
477
+ "cmd.help": "Help show all commands",
478
+ "cmd.helpMe": "Help me with commands",
479
+ "cmd.allCommands": "All commands",
480
+ "cmd.search": "Find search filter",
481
+ "cmd.searchTasks": "Search tasks",
482
+ "cmd.searchProjects": "Search projects",
483
+ "cmd.setDefault": "Set as default project",
484
+ "cmd.show": "Show a hidden panel",
485
+ "cmd.hide": "Hide a panel",
486
+ "cmd.api": "API endpoints info",
487
+ "cmd.status": "Daemon status info (Soon...)",
488
+ "cmd.filterStatus": "Filter by status",
489
+ "cmd.sort": "Order sort by field",
490
+ "cmd.project": "Filter by project",
491
+ "cmd.debug": "Debug toggle panel",
492
+ "cmd.export": "Export loops JSON",
493
+ "cmd.import": "Import loops JSON",
494
+ "import.missingVersion": "Missing export version. Expected: 2",
495
+ "import.unsupportedVersion": "Unsupported export version: {version}. Expected: 2",
496
+ "import.missingKeys": "Missing required keys: {keys}",
497
+ "import.nonArrayKey": "field '{key}' must be an array",
498
+ "import.invalidFile": "Invalid export file: expected a JSON object",
499
+ "import.writeFailed": "Failed to write store: {error}",
500
+ "import.rolledBack": "Rolled back previously written stores.",
501
+ "import.success": "Imported {loops} loops, {tasks} tasks, {projects} projects",
502
+ "import.hotReload": "Daemon will hot-reload automatically.",
503
+ "export.modalTitle": "Export Loops JSON",
504
+ "export.filePath": "Saved to: {path}",
505
+ "export.error": "Export failed: {message}",
506
+ "export.truncated": "[truncated — {total} lines]",
507
+ "export.hint": "up/down:scroll c:copy esc:close",
508
+ "cmd.quit": "Exit loop-task",
509
+ "cmd.save": "Save changes",
510
+ "cmd.cancel": "Discard changes",
511
+ "cmd.change": "Change a field value",
512
+ "cmd.changeInterval": "Change interval",
513
+ "cmd.changeCommand": "Change command",
514
+ "cmd.changeCwd": "Change working directory",
515
+ "cmd.changeDescription": "Change description",
516
+ "cmd.changeRunNow": "Change run-now setting",
517
+ "cmd.changeMaxRuns": "Change max runs",
518
+ "cmd.changeProject": "Change project",
519
+ "cmd.changeName": "Change name",
520
+ "cmd.changeOnSuccess": "Change on-success chain",
521
+ "cmd.changeOnFailure": "Change on-failure chain",
522
+ "cmdInput.placeholder": "Type a command, or help",
523
+ "cmdInput.welcomeHint": "type 'help' for commands, 'all' to browse",
524
+ "cmdInput.noMatches": "No commands match",
525
+ "commandBuilder.executableLabel": "Executable",
526
+ "commandBuilder.executableHint": "e.g. npm, pnpm, dotnet, docker",
527
+ "commandBuilder.argsLabel": "Arguments",
528
+ "commandBuilder.argsHint": "run, build, test...",
529
+ "commandBuilder.previewLabel": "Preview:",
530
+ "commandBuilder.copyHint": "Ctrl+Y copy",
531
+ "commandBuilder.navHint": "Tab/Enter next field . Esc done",
532
+ "commandBuilder.templatesLabel": "Templates (↑/↓ to select, Enter to apply):",
533
+ "commandBuilder.templateNpmRun": "npm run",
534
+ "commandBuilder.templateNpmTest": "npm test",
535
+ "commandBuilder.templatePnpmBuild": "pnpm build",
536
+ "commandBuilder.templatePnpmTest": "pnpm test",
537
+ "commandBuilder.templateDockerCompose": "docker compose up",
538
+ "wizard.chainNone": "None",
539
+ "cmdInput.searchPlaceholder": "Write something to filter about...",
540
+ "cmdInput.confirmYes": "yes",
541
+ "cmdInput.confirmCancel": "cancel",
542
+ "confirm.deleteLoop": "Delete \"{name}\"? type yes to confirm",
543
+ "confirm.deleteTask": "Delete task \"{id}\"? type yes to confirm",
544
+ "confirm.deleteProject": "Delete project \"{name}\"? type yes to confirm",
545
+ "confirm.stopLoop": "Stop \"{name}\"? type yes to confirm",
546
+ "confirm.quit": "Exit loop-task? Running loops keep running in background.",
547
+ "wizard.newLoop": "New loop",
548
+ "wizard.editLoop": "Edit loop",
549
+ "wizard.newTask": "New task",
550
+ "wizard.stepOf": "Step {current} of {total}",
551
+ "wizard.intervalPrompt": "How often should it run?",
552
+ "wizard.intervalHint": "e.g. 30s, 5m, 30m, 1h, 1d",
553
+ "wizard.commandPrompt": "What command should run?",
554
+ "wizard.commandHint": "Full command line. Quote args with spaces",
555
+ "wizard.taskModePrompt": "Inline command or existing task?",
556
+ "wizard.taskModeInline": "Inline command",
557
+ "wizard.taskModeExisting": "Existing task",
558
+ "wizard.runNowPrompt": "Run immediately or wait for first interval?",
559
+ "wizard.runNowWait": "Wait for the first interval",
560
+ "wizard.runNowNow": "Run now, then every interval",
561
+ "wizard.cwdPrompt": "Working directory? (optional)",
562
+ "wizard.cwdHint": "Directory the command runs in",
563
+ "wizard.descriptionPrompt": "Description? (optional)",
564
+ "wizard.descriptionHint": "Short label shown in the list",
565
+ "wizard.maxRunsPrompt": "Max runs? (optional)",
566
+ "wizard.maxRunsHint": "Stop after N runs. Leave blank to run forever",
567
+ "wizard.projectPrompt": "Which project?",
568
+ "wizard.projectHint": "Assign this loop to a project",
569
+ "wizard.taskNamePrompt": "Name for this task?",
570
+ "wizard.taskNameHint": "Short name to identify this task",
571
+ "wizard.taskCommandPrompt": "What command should this task run?",
572
+ "wizard.onSuccessPrompt": "On success, chain to? (optional)",
573
+ "wizard.onFailurePrompt": "On failure, chain to? (optional)",
574
+ "wizard.created": "Created",
575
+ "wizard.filled": "{filled}/{total} filled",
576
+ "wizard.footerHints": "tab next . ctrl+s save . esc cancel",
577
+ "wizard.ctrlSave": "Ctrl+S to save with defaults",
578
+ "wizard.escBack": "Esc to go back",
579
+ "wizard.validationError": "Invalid value",
580
+ "patch.pendingChanges": "{count} pending changes",
581
+ "patch.noChanges": "No changes",
582
+ "patch.hintNavigation": "↑/↓ navigate fields . Enter to edit . Esc to cancel",
583
+ "patch.hintEditing": "Enter to confirm . Esc to cancel",
584
+ "patch.hintCopy": "Ctrl+Y copy field value",
585
+ "patch.hintNavPending": "↑/↓ navigate . Ctrl+S save . Esc cancel",
586
+ "patch.hintNavIdle": "↑/↓ navigate . Ctrl+Y copy",
587
+ "patch.enterEdit": "Enter to edit",
588
+ "patch.escapeCancel": "Esc to cancel",
589
+ "patch.save": "Save",
590
+ "patch.cancel": "Cancel",
591
+ "patch.field": "Field",
592
+ "patch.value": "Value",
593
+ "patch.validationError": "Invalid: {error}",
594
+ "api.title": "API Endpoints",
595
+ "api.daemonSocket": "Daemon socket",
596
+ "api.statusEndpoint": "Get daemon status",
597
+ "api.exportEndpoint": "Export loops",
598
+ "api.importEndpoint": "Import loops",
599
+ "api.closeHint": "Press Escape or q to close",
600
+ "helpGuide.title": "How loop-task works",
601
+ "helpGuide.intro": "This is a command-first terminal app. Instead of clicking buttons, you type commands at the bottom input.",
602
+ "helpGuide.commands": "Type a command and press Enter. Start typing to see matching commands - like fuzzy search.",
603
+ "helpGuide.examples": "Try: 'new' to create, 'edit' to modify, 'delete' to remove, 'pause' / 'play' to control loops.",
604
+ "helpGuide.confirm": "Destructive commands ask you to type 'yes' to confirm. This prevents accidents.",
605
+ "helpGuide.navigation": "Use Tab to switch between the left list and right detail. Use 1/2/3 to switch tabs (Loops/Tasks/Projects).",
606
+ "helpGuide.ctrl": "Ctrl+E = edit, Ctrl+D = delete - shortcuts that bypass typing.",
607
+ "helpGuide.allCommands": "Type 'all' or 'all commands' to browse every command grouped by category.",
608
+ "helpGuide.esc": "Press Escape or 'q' to close this window.",
609
+ "cmdsBrowser.title": "All Commands",
610
+ "cmdsBrowser.groupGlobal": "Global",
611
+ "cmdsBrowser.groupFilters": "Filters",
612
+ "cmdsBrowser.groupActions": "Actions",
613
+ "cmdsBrowser.groupLoop": "Loop Actions",
614
+ "cmdsBrowser.groupTask": "Task Actions",
615
+ "cmdsBrowser.groupProject": "Project Actions",
616
+ "cmdsBrowser.hint": "Up/Down to navigate, Enter to execute, Esc to close"
397
617
  }
@@ -1,4 +1,13 @@
1
1
  import fs from "node:fs";
2
+ // On Windows, renaming over a file that another handle has open (e.g. the
3
+ // daemon's own fs.watch on loops.json) can transiently fail with EPERM/EBUSY.
4
+ // Retry the rename a few times before falling back to a direct (non-atomic)
5
+ // write so a momentary lock can't take the daemon down.
6
+ const RENAME_RETRIES = 5;
7
+ const RENAME_BACKOFF_MS = 15;
8
+ function sleepSync(ms) {
9
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
10
+ }
2
11
  export function removeIfExists(filePath) {
3
12
  if (fs.existsSync(filePath)) {
4
13
  fs.unlinkSync(filePath);
@@ -7,5 +16,29 @@ export function removeIfExists(filePath) {
7
16
  export function writeFileAtomic(filePath, data) {
8
17
  const tmp = `${filePath}.${process.pid}.tmp`;
9
18
  fs.writeFileSync(tmp, data);
10
- fs.renameSync(tmp, filePath);
19
+ let lastErr;
20
+ for (let attempt = 0; attempt < RENAME_RETRIES; attempt++) {
21
+ try {
22
+ fs.renameSync(tmp, filePath);
23
+ return;
24
+ }
25
+ catch (err) {
26
+ lastErr = err;
27
+ const code = err.code;
28
+ if (code !== "EPERM" && code !== "EBUSY" && code !== "EACCES")
29
+ break;
30
+ sleepSync(RENAME_BACKOFF_MS * (attempt + 1));
31
+ }
32
+ }
33
+ // Fallback: the destination stayed locked. Write in place (non-atomic) so
34
+ // state is still persisted; a partial write is recoverable since corrupt
35
+ // JSON is skipped on load, whereas a crash-looping daemon is not.
36
+ try {
37
+ fs.writeFileSync(filePath, data);
38
+ fs.rmSync(tmp, { force: true });
39
+ }
40
+ catch {
41
+ fs.rmSync(tmp, { force: true });
42
+ throw lastErr;
43
+ }
11
44
  }