@hacky.software/opencode-todo-supervisor 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,7 @@ Tested against OpenCode `2.0.24`.
23
23
  ## Install
24
24
 
25
25
  ```sh
26
- opencode plugin add @hacky.software/opencode-todo-supervisor@0.3.7
26
+ opencode plugin add @hacky.software/opencode-todo-supervisor@0.3.9
27
27
  ```
28
28
 
29
29
  This also loads the bundled TUI entry. To pass options, use the object form:
@@ -197,7 +197,11 @@ effective set is `config.markdownFiles ∪ stored files − removed` per session
197
197
  - it re-reads every 3 s while open, so edits made outside OpenCode show up
198
198
  without waiting for an event.
199
199
  - **Side panel** (palette "Todo: open side panel"): the same list docked to
200
- `session.panel` (`todo.todos`), with `f` for fullscreen.
200
+ `session.panel` (`todo.todos`), with `f` for fullscreen. Its keys are live only
201
+ while the panel is focused *and* no overlay is up (dialog, menu, completions,
202
+ composer), so a docked panel never steals the arrow keys from the model picker,
203
+ command palette or the prompt's completions. The gate is applied per key at
204
+ dispatch, not on the layer: the host ignores a layer-level `enabled`.
201
205
  - `/todos add <path>` and `/todos remove <path>` manage subscriptions from the
202
206
  prompt.
203
207
  - **Toasts** on `nudged` (idle vs stalled wording) and on the all-done transition.
@@ -380,7 +384,7 @@ There is no filesystem watcher. Instead:
380
384
  (source edits and config content changes; `touch` alone does not).
381
385
  - `opencode2 reload` forces reconciliation; `opencode2 service restart` only as a
382
386
  last resort (it interrupts sessions).
383
- - Tests: `bun test` (121 tests across parser, storage, tools, RPC, state machine,
387
+ - Tests: `bun test` (123 tests across parser, storage, tools, RPC, state machine,
384
388
  panel helpers).
385
389
  - Watch logs: `tail -f ~/.local/share/opencode/log/todo-supervisor.log`.
386
390
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hacky.software/opencode-todo-supervisor",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "OpenCode V2 plugin that gives every session a todo list (todo_write/todo_read), supervises idle sessions until the list is done, and can drive checklists in markdown files from a TUI panel.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/panel.test.ts CHANGED
@@ -10,7 +10,9 @@ import {
10
10
  isSelectable,
11
11
  itemColumns,
12
12
  moveCursor,
13
+ panelKeys,
13
14
  popoverBodyHeight,
15
+ popoverKeys,
14
16
  priorityTag,
15
17
  progressBar,
16
18
  quietLabel,
@@ -200,6 +202,26 @@ describe("panel cursor", () => {
200
202
  expect(dialogWidth("xlarge", 20)).toBe(20)
201
203
  })
202
204
 
205
+ test("the panel layer yields to overlays that own the keyboard", () => {
206
+ // Normal session view: live while focused.
207
+ expect(panelKeys(true, "base")).toBe(true)
208
+ expect(panelKeys(false, "base")).toBe(false)
209
+ // The model picker, dialogs, menus and completions all take the keyboard.
210
+ for (const mode of ["modal", "menu", "autocomplete", "composer"]) {
211
+ expect(panelKeys(true, mode)).toBe(false)
212
+ }
213
+ // Any other mode (including the plain session view) keeps the panel live.
214
+ expect(panelKeys(true, "base")).toBe(true)
215
+ })
216
+
217
+ test("the popover keeps the keyboard in its own dialog", () => {
218
+ expect(popoverKeys("modal")).toBe(true)
219
+ expect(popoverKeys("base")).toBe(true)
220
+ expect(popoverKeys("menu")).toBe(false)
221
+ expect(popoverKeys("autocomplete")).toBe(false)
222
+ expect(popoverKeys("composer")).toBe(false)
223
+ })
224
+
203
225
  test("id and priority tags and their column widths", () => {
204
226
  expect(idTag(undefined)).toBe("")
205
227
  expect(idTag("12")).toBe("#12")
package/src/panel.ts CHANGED
@@ -322,3 +322,29 @@ const DIALOG_WIDTH: Record<DialogSizeName, number> = { medium: 60, large: 88, xl
322
322
  export function dialogWidth(size: DialogSizeName, viewportWidth: number): number {
323
323
  return Math.min(DIALOG_WIDTH[size], Math.max(20, viewportWidth - 2))
324
324
  }
325
+
326
+ /**
327
+ * Keymap modes that own the keyboard while they are up: `modal` (dialogs, the
328
+ * model picker, confirmations), `menu` (the command/palette menus),
329
+ * `autocomplete` (the prompt's completion list) and `composer` (composer
330
+ * overlays). They all drive themselves with the arrow keys.
331
+ */
332
+ const OVERLAY_MODES: ReadonlySet<string> = new Set(["modal", "menu", "autocomplete", "composer"])
333
+
334
+ /**
335
+ * Whether the side panel's keys should be live. Only the overlay modes take the
336
+ * keyboard away; any other mode (the plain session view included) keeps the
337
+ * panel live while it is focused. Without this a focused docked panel swallows
338
+ * the model picker's up/down.
339
+ */
340
+ export function panelKeys(focused: boolean, mode: string): boolean {
341
+ return focused && !OVERLAY_MODES.has(mode)
342
+ }
343
+
344
+ /**
345
+ * Whether the popover's keys should be live. The popover *is* a dialog, so
346
+ * `modal` is ours; any other overlay takes the keyboard away from it.
347
+ */
348
+ export function popoverKeys(mode: string): boolean {
349
+ return mode === "modal" || !OVERLAY_MODES.has(mode)
350
+ }
package/src/tui.tsx CHANGED
@@ -26,7 +26,9 @@ import {
26
26
  idTag,
27
27
  itemColumns,
28
28
  moveCursor,
29
+ panelKeys,
29
30
  popoverBodyHeight,
31
+ popoverKeys,
30
32
  priorityTag,
31
33
  progressBar,
32
34
  rowIndex,
@@ -409,8 +411,9 @@ export default Plugin.define({
409
411
  })
410
412
 
411
413
  context.keymap.layer(() => ({
414
+ // "global" keeps the layer mode-agnostic; the actual gating is done
415
+ // per command below.
412
416
  mode: "global",
413
- enabled: () => props.active(),
414
417
  priority: 10,
415
418
  commands: [
416
419
  { title: "Todo: select next", bind: "down", run: () => move(1) },
@@ -491,7 +494,12 @@ export default Plugin.define({
491
494
  }),
492
495
  },
493
496
  { title: "Todo: close", bind: "escape", run: () => props.close() },
494
- ],
497
+ // The host drops a *layer*-level `enabled`; the *command*-level one
498
+ // is consulted on every dispatch. Gate each key here so it yields —
499
+ // and falls through — the moment an overlay (model picker, palette,
500
+ // completion list, composer) owns the keyboard while a todo surface
501
+ // happens to still be focused.
502
+ ].map((command) => ({ ...command, enabled: () => props.active() })),
495
503
  }))
496
504
 
497
505
  const totals = () => {
@@ -625,7 +633,7 @@ export default Plugin.define({
625
633
  return (
626
634
  <TodoList
627
635
  sessionID={props.sessionID}
628
- active={() => true}
636
+ active={() => popoverKeys(context.keymap.mode.current())}
629
637
  width={() => dialogWidth(size(), viewportWidth())}
630
638
  height={() => popoverBodyHeight(viewportHeight())}
631
639
  close={() => context.ui.dialog.clear()}
@@ -660,7 +668,7 @@ export default Plugin.define({
660
668
  <Show when={panel.name === "todo.todos"}>
661
669
  <TodoList
662
670
  sessionID={panel.sessionID}
663
- active={() => panel.focused}
671
+ active={() => panelKeys(panel.focused, context.keymap.mode.current())}
664
672
  width={() => panel.width}
665
673
  close={() => panel.close()}
666
674
  cycleSize={() => panel.toggleFullscreen()}