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

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.8
27
27
  ```
28
28
 
29
29
  This also loads the bundled TUI entry. To pass options, use the object form:
@@ -197,7 +197,10 @@ 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, completion
202
+ list), so a docked panel never steals the arrow keys from the model picker,
203
+ command palette or the prompt's completions.
201
204
  - `/todos add <path>` and `/todos remove <path>` manage subscriptions from the
202
205
  prompt.
203
206
  - **Toasts** on `nudged` (idle vs stalled wording) and on the all-done transition.
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.8",
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, confirmations and other dialogs run in `modal`.
210
+ expect(panelKeys(true, "modal")).toBe(false)
211
+ expect(panelKeys(true, "menu")).toBe(false)
212
+ expect(panelKeys(true, "autocomplete")).toBe(false)
213
+ // An unknown mode is not treated as an overlay.
214
+ expect(panelKeys(true, "composer")).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("composer")).toBe(true)
221
+ expect(popoverKeys("menu")).toBe(false)
222
+ expect(popoverKeys("autocomplete")).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) and
329
+ * `autocomplete` (the prompt's completion list). They all drive themselves with
330
+ * the arrow keys.
331
+ */
332
+ const OVERLAY_MODES: ReadonlySet<string> = new Set(["modal", "menu", "autocomplete"])
333
+
334
+ /**
335
+ * Whether the side panel's keys should be live. The layer binds the arrows
336
+ * globally at a high priority, so it must yield the moment an overlay opens —
337
+ * otherwise a focused docked panel swallows the model picker's up/down.
338
+ */
339
+ export function panelKeys(focused: boolean, mode: string): boolean {
340
+ return focused && !OVERLAY_MODES.has(mode)
341
+ }
342
+
343
+ /**
344
+ * Whether the popover's keys should be live. The popover *is* a dialog, so
345
+ * `modal` is ours; only the non-dialog overlays (palette, completion list) take
346
+ * the keyboard away from it.
347
+ */
348
+ export function popoverKeys(mode: string): boolean {
349
+ return mode !== "menu" && mode !== "autocomplete"
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,
@@ -625,7 +627,7 @@ export default Plugin.define({
625
627
  return (
626
628
  <TodoList
627
629
  sessionID={props.sessionID}
628
- active={() => true}
630
+ active={() => popoverKeys(context.keymap.mode.current())}
629
631
  width={() => dialogWidth(size(), viewportWidth())}
630
632
  height={() => popoverBodyHeight(viewportHeight())}
631
633
  close={() => context.ui.dialog.clear()}
@@ -660,7 +662,7 @@ export default Plugin.define({
660
662
  <Show when={panel.name === "todo.todos"}>
661
663
  <TodoList
662
664
  sessionID={panel.sessionID}
663
- active={() => panel.focused}
665
+ active={() => panelKeys(panel.focused, context.keymap.mode.current())}
664
666
  width={() => panel.width}
665
667
  close={() => panel.close()}
666
668
  cycleSize={() => panel.toggleFullscreen()}