@hacky.software/opencode-todo-supervisor 0.3.6 → 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 +12 -7
- package/package.json +1 -1
- package/src/panel.test.ts +31 -10
- package/src/panel.ts +38 -8
- package/src/tui.tsx +27 -14
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.
|
|
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:
|
|
@@ -174,12 +174,14 @@ effective set is `config.markdownFiles ∪ stored files − removed` per session
|
|
|
174
174
|
|
|
175
175
|
- **Composer strip** (`session.composer.top`): `📋 ███░░░░░ 8/17 open` (plus `⏸`
|
|
176
176
|
while paused), hidden when there is neither a list nor a subscription.
|
|
177
|
-
- **Popover** (`/todos`, palette "Todo: open todos"): a centered overlay dialog
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
- **Popover** (`/todos`, palette "Todo: open todos"): a centered overlay dialog,
|
|
178
|
+
opened at the widest preset. The body scales with the terminal height (up to
|
|
179
|
+
48 rows) so the key legend stays visible on a short terminal and a tall one
|
|
180
180
|
shows more rows; the header shows `open/total` and the selected `row n/m`. `f`
|
|
181
|
-
cycles the dialog
|
|
182
|
-
|
|
181
|
+
cycles the host dialog width — medium (60), large (88), xlarge (116 cells) —
|
|
182
|
+
and `esc` closes it. Rows are laid out to the card width exactly (and render
|
|
183
|
+
with wrapping off), so content never wraps and the card cannot grow past the
|
|
184
|
+
bottom of the screen.
|
|
183
185
|
- header: progress bar, `open/total`, and a supervision chip (`idle · quiet
|
|
184
186
|
12s · nudges 2`, `working: 1 tool · 1 shell · quiet 4s`, `paused
|
|
185
187
|
(no-progress)`, `not supervised`);
|
|
@@ -195,7 +197,10 @@ effective set is `config.markdownFiles ∪ stored files − removed` per session
|
|
|
195
197
|
- it re-reads every 3 s while open, so edits made outside OpenCode show up
|
|
196
198
|
without waiting for an event.
|
|
197
199
|
- **Side panel** (palette "Todo: open side panel"): the same list docked to
|
|
198
|
-
`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.
|
|
199
204
|
- `/todos add <path>` and `/todos remove <path>` manage subscriptions from the
|
|
200
205
|
prompt.
|
|
201
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.
|
|
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,
|
|
@@ -189,16 +191,35 @@ describe("panel cursor", () => {
|
|
|
189
191
|
expect(popoverBodyHeight(200)).toBe(48)
|
|
190
192
|
})
|
|
191
193
|
|
|
192
|
-
test("popover width
|
|
193
|
-
expect(dialogWidth("medium", 200)).toBe(
|
|
194
|
-
expect(dialogWidth("large", 200)).toBe(
|
|
195
|
-
expect(dialogWidth("xlarge", 200)).toBe(
|
|
196
|
-
expect(dialogWidth("medium", 80)).toBe(
|
|
197
|
-
expect(dialogWidth("large", 80)).toBe(
|
|
198
|
-
expect(dialogWidth("xlarge", 80)).toBe(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
194
|
+
test("popover width matches the host dialog card and clamps", () => {
|
|
195
|
+
expect(dialogWidth("medium", 200)).toBe(60)
|
|
196
|
+
expect(dialogWidth("large", 200)).toBe(88)
|
|
197
|
+
expect(dialogWidth("xlarge", 200)).toBe(116)
|
|
198
|
+
expect(dialogWidth("medium", 80)).toBe(60)
|
|
199
|
+
expect(dialogWidth("large", 80)).toBe(78)
|
|
200
|
+
expect(dialogWidth("xlarge", 80)).toBe(78)
|
|
201
|
+
// Never wider than the window (minus the host card's 2-cell inset).
|
|
202
|
+
expect(dialogWidth("xlarge", 20)).toBe(20)
|
|
203
|
+
})
|
|
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)
|
|
202
223
|
})
|
|
203
224
|
|
|
204
225
|
test("id and priority tags and their column widths", () => {
|
package/src/panel.ts
CHANGED
|
@@ -306,15 +306,45 @@ export function popoverBodyHeight(viewportHeight: number, extraRows = 0): number
|
|
|
306
306
|
|
|
307
307
|
export type DialogSizeName = "medium" | "large" | "xlarge"
|
|
308
308
|
|
|
309
|
-
const DIALOG_FACTOR: Record<DialogSizeName, number> = { medium: 0.7, large: 0.9, xlarge: 0.98 }
|
|
310
|
-
|
|
311
309
|
/**
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
310
|
+
* The host draws a dialog inside a card of a *fixed* width per size preset
|
|
311
|
+
* (`medium` 60, `large` 88, `xlarge` 116 cells), capped at the render width
|
|
312
|
+
* minus two columns; the card height hugs its content.
|
|
313
|
+
*
|
|
314
|
+
* Content must be laid out to exactly that width. Text passed a wider budget
|
|
315
|
+
* wraps (`wrapMode` defaults to `"word"`), which grows the content-hugging card
|
|
316
|
+
* past the bottom of the screen. So we mirror the host preset rather than a
|
|
317
|
+
* share of the terminal.
|
|
315
318
|
*/
|
|
319
|
+
const DIALOG_WIDTH: Record<DialogSizeName, number> = { medium: 60, large: 88, xlarge: 116 }
|
|
320
|
+
|
|
321
|
+
/** Content width for the popover dialog: the host card width for `size`. */
|
|
316
322
|
export function dialogWidth(size: DialogSizeName, viewportWidth: number): number {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
323
|
+
return Math.min(DIALOG_WIDTH[size], Math.max(20, viewportWidth - 2))
|
|
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"
|
|
320
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,
|
|
@@ -539,7 +541,7 @@ export default Plugin.define({
|
|
|
539
541
|
const background = () => (isCurrent() ? selectedBg() : undefined)
|
|
540
542
|
if (row.kind === "section") {
|
|
541
543
|
return (
|
|
542
|
-
<text fg={muted()}>
|
|
544
|
+
<text fg={muted()} wrapMode="none">
|
|
543
545
|
{prefix() + truncate(row.label + (row.detail ? " · " + row.detail : ""), width() - 6)}
|
|
544
546
|
</text>
|
|
545
547
|
)
|
|
@@ -549,7 +551,7 @@ export default Plugin.define({
|
|
|
549
551
|
const detail = `${row.open} open of ${row.total}`
|
|
550
552
|
return (
|
|
551
553
|
<box backgroundColor={background()}>
|
|
552
|
-
<text fg={isCurrent() ? accent() : base()}>
|
|
554
|
+
<text fg={isCurrent() ? accent() : base()} wrapMode="none">
|
|
553
555
|
{prefix() + (row.collapsed ? "▸ " : "▾ ") + truncate(`${label} · ${detail}`, width() - 6)}
|
|
554
556
|
</text>
|
|
555
557
|
</box>
|
|
@@ -562,7 +564,7 @@ export default Plugin.define({
|
|
|
562
564
|
parts.push(truncate(row.content + (row.line !== undefined ? ` :${row.line}` : ""), contentWidth()))
|
|
563
565
|
return (
|
|
564
566
|
<box backgroundColor={background()}>
|
|
565
|
-
<text fg={row.status === "completed" ? muted() : isCurrent() ? accent() : base()}>
|
|
567
|
+
<text fg={row.status === "completed" ? muted() : isCurrent() ? accent() : base()} wrapMode="none">
|
|
566
568
|
{parts.join(" ")}
|
|
567
569
|
</text>
|
|
568
570
|
</box>
|
|
@@ -571,14 +573,23 @@ export default Plugin.define({
|
|
|
571
573
|
|
|
572
574
|
return (
|
|
573
575
|
<box flexDirection="column">
|
|
574
|
-
<text fg={base()}>
|
|
575
|
-
{
|
|
576
|
-
|
|
577
|
-
|
|
576
|
+
<text fg={base()} wrapMode="none">
|
|
577
|
+
{truncate(
|
|
578
|
+
"Todos " +
|
|
579
|
+
progressBar(totals().total - totals().open, totals().total, barWidth()) +
|
|
580
|
+
" " +
|
|
581
|
+
`${totals().open}/${totals().total} open` +
|
|
582
|
+
(position() ? ` · row ${position()!.index}/${position()!.total}` : ""),
|
|
583
|
+
width() - 2,
|
|
584
|
+
)}
|
|
585
|
+
</text>
|
|
586
|
+
<text fg={muted()} wrapMode="none">
|
|
587
|
+
{truncate("supervision: " + supervisorChip(snap().supervisor), width() - 2)}
|
|
578
588
|
</text>
|
|
579
|
-
<text fg={muted()}>{"supervision: " + supervisorChip(snap().supervisor)}</text>
|
|
580
589
|
<Show when={(filter() ?? "") !== ""}>
|
|
581
|
-
<text fg={muted()}
|
|
590
|
+
<text fg={muted()} wrapMode="none">
|
|
591
|
+
{truncate(`filter: "${filter()}" (press / to change)`, width() - 2)}
|
|
592
|
+
</text>
|
|
582
593
|
</Show>
|
|
583
594
|
<scrollbox
|
|
584
595
|
ref={(node: unknown) => (scrollNode = node as typeof scrollNode)}
|
|
@@ -593,7 +604,9 @@ export default Plugin.define({
|
|
|
593
604
|
)}
|
|
594
605
|
</For>
|
|
595
606
|
</scrollbox>
|
|
596
|
-
<text fg={muted()}
|
|
607
|
+
<text fg={muted()} wrapMode="none">
|
|
608
|
+
{truncate(footerHints(width()).join(" · "), Math.max(10, width() - 2))}
|
|
609
|
+
</text>
|
|
597
610
|
</box>
|
|
598
611
|
)
|
|
599
612
|
}
|
|
@@ -601,7 +614,7 @@ export default Plugin.define({
|
|
|
601
614
|
// ---- popover (an overlay dialog, like the host's own panes) -----------
|
|
602
615
|
|
|
603
616
|
function TodoPopover(props: { sessionID: string }) {
|
|
604
|
-
const [size, setSize] = createSignal<DialogSize>("
|
|
617
|
+
const [size, setSize] = createSignal<DialogSize>("xlarge")
|
|
605
618
|
const cycleSize = () => {
|
|
606
619
|
const next = SIZE_ORDER[(SIZE_ORDER.indexOf(size()) + 1) % SIZE_ORDER.length]!
|
|
607
620
|
setSize(next)
|
|
@@ -614,7 +627,7 @@ export default Plugin.define({
|
|
|
614
627
|
return (
|
|
615
628
|
<TodoList
|
|
616
629
|
sessionID={props.sessionID}
|
|
617
|
-
active={() =>
|
|
630
|
+
active={() => popoverKeys(context.keymap.mode.current())}
|
|
618
631
|
width={() => dialogWidth(size(), viewportWidth())}
|
|
619
632
|
height={() => popoverBodyHeight(viewportHeight())}
|
|
620
633
|
close={() => context.ui.dialog.clear()}
|
|
@@ -625,7 +638,7 @@ export default Plugin.define({
|
|
|
625
638
|
|
|
626
639
|
const openPopover = (sessionID: string): boolean => {
|
|
627
640
|
try {
|
|
628
|
-
context.ui.dialog.set({ size: "
|
|
641
|
+
context.ui.dialog.set({ size: "xlarge", centered: true })
|
|
629
642
|
context.ui.dialog.show(
|
|
630
643
|
() => <TodoPopover sessionID={sessionID} />,
|
|
631
644
|
() => logFile("tui", "popover closed"),
|
|
@@ -649,7 +662,7 @@ export default Plugin.define({
|
|
|
649
662
|
<Show when={panel.name === "todo.todos"}>
|
|
650
663
|
<TodoList
|
|
651
664
|
sessionID={panel.sessionID}
|
|
652
|
-
active={() => panel.focused}
|
|
665
|
+
active={() => panelKeys(panel.focused, context.keymap.mode.current())}
|
|
653
666
|
width={() => panel.width}
|
|
654
667
|
close={() => panel.close()}
|
|
655
668
|
cycleSize={() => panel.toggleFullscreen()}
|