@gamflaz/opencode-todolist 1.0.12 → 1.1.1

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
@@ -8,6 +8,7 @@ plus a TUI widget above the composer that shows progress in real time.
8
8
  - Hierarchical tasks: any number of root tasks, each with up to 9 subtasks.
9
9
  - Numeric IDs only: root `1`, `2`, … and subtasks `1.1`, `1.2`, … (`1.9` max).
10
10
  - Small partial updates: change one item without resending the whole list.
11
+ - Scoped reads: pull the whole list, one branch, or a single item.
11
12
  - Strict forward-only progress with enforced order between root tasks.
12
13
  - Real-time TUI widget with collapse, scroll, `↻ Continue`, and clear controls.
13
14
  - Bundled skills that teach the agent how to drive the tool.
@@ -40,22 +41,36 @@ dependencies, so `bun install` is required before use.
40
41
 
41
42
  #### Operation fields
42
43
 
43
- | Operation | Required fields | Optional fields |
44
- | --------- | ------------------------------- | ------------------------------ |
45
- | `read` | `operation` | None |
46
- | `write` | `operation`, `todos` | None; use `todos: []` to clear |
47
- | `update` | `operation`, `id`, `status` | `content`, `priority`, `note` |
44
+ | Operation | Required fields | Optional fields |
45
+ | --------- | --------------------------- | ------------------------------ |
46
+ | `read` | `operation` | `scope`, `target` |
47
+ | `write` | `operation`, `todos` | None; use `todos: []` to clear |
48
+ | `update` | `operation`, `id`, `status` | `content`, `priority`, `note` |
48
49
 
49
50
  For `update`, send `null` for `priority` or `note` to remove the existing value.
50
51
 
51
52
  ### Read
52
53
 
54
+ By default `read` returns the whole list. An optional `scope` narrows it to one
55
+ branch or one row, which keeps the response small on a long list.
56
+
57
+ | `scope` | `target` | Returns |
58
+ | ---------- | ------------------- | -------------------------------------------- |
59
+ | `all` | — | the whole list (default) |
60
+ | `root` | a root id, e.g. `1` | that root and all of its subtasks |
61
+ | `children` | a subtask id, `1.1` | that subtask, with its root shown as context |
62
+ | `item` | `1` or `1.1` | only that row |
63
+
64
+ `target` is only valid together with `scope: "root"`, `"children"`, or
65
+ `"item"` — passing it without one is rejected, so a scope can never be silently
66
+ ignored.
67
+
68
+ The whole list:
69
+
53
70
  ```json
54
71
  { "operation": "read" }
55
72
  ```
56
73
 
57
- Returns the current list as a progress tree:
58
-
59
74
  ```text
60
75
  [~] 1. Design architecture (1/2)
61
76
  ↳ note: Keep the hierarchy shallow
@@ -64,9 +79,35 @@ Returns the current list as a progress tree:
64
79
  [ ] 2. Write tests
65
80
  ```
66
81
 
82
+ One branch (`root`):
83
+
84
+ ```json
85
+ { "operation": "read", "scope": "root", "target": 1 }
86
+ ```
87
+
88
+ A single subtask, with its root as context (`children`):
89
+
90
+ ```json
91
+ { "operation": "read", "scope": "children", "target": 1.2 }
92
+ ```
93
+
94
+ ```text
95
+ [~] 1. Design architecture (1/2)
96
+ [~] 1.2 API endpoints
97
+ ```
98
+
99
+ One row with no context (`item`):
100
+
101
+ ```json
102
+ { "operation": "read", "scope": "item", "target": 1.2 }
103
+ ```
104
+
67
105
  Markers: `[ ]` pending, `[~]` in progress, `[x]` completed, `[-]` cancelled,
68
106
  `[!]` error. Notes render under their item as `↳ note: …`.
69
107
 
108
+ Even when a scope hides rows, the root keeps its real `(completed/total)` count —
109
+ only the display is narrowed.
110
+
70
111
  ### Write
71
112
 
72
113
  `write` initializes a session or clears it. A non-empty list is only accepted
@@ -100,14 +141,14 @@ Clear the current list:
100
141
 
101
142
  #### Todo item fields
102
143
 
103
- | Field | Type | Required | Description |
104
- | ---------- | --------------------------------- | --------------- | ------------------------------------------------------------------------- |
105
- | `id` | number | Yes | Root: positive integer. Child: `<root>.<n>`, such as `1.1`. |
106
- | `content` | string | Yes | Task summary. |
107
- | `status` | enum | Yes | `pending`, `in_progress`, `completed`, `cancelled`, or `error`. |
108
- | `priority` | `high` \| `medium` \| `low` | No | Optional priority label. |
109
- | `note` | string | No | Technical context or evidence. |
110
- | `children` | array of child items | No, root only | Up to 9 subtasks. Child items cannot contain more children. |
144
+ | Field | Type | Required | Description |
145
+ | ---------- | --------------------------- | ------------- | --------------------------------------------------------------- |
146
+ | `id` | number | Yes | Root: positive integer. Child: `<root>.<n>`, such as `1.1`. |
147
+ | `content` | string | Yes | Task summary. |
148
+ | `status` | enum | Yes | `pending`, `in_progress`, `completed`, `cancelled`, or `error`. |
149
+ | `priority` | `high` \| `medium` \| `low` | No | Optional priority label. |
150
+ | `note` | string | No | Technical context or evidence. |
151
+ | `children` | array of child items | No, root only | Up to 9 subtasks. Child items cannot contain more children. |
111
152
 
112
153
  Root IDs must be unique. A child ID must start with its parent root ID.
113
154
 
@@ -129,11 +170,11 @@ Root IDs must be unique. A child ID must start with its parent root ID.
129
170
 
130
171
  | Field | Required | Notes |
131
172
  | ---------- | -------- | ------------------------------------------------------------ |
132
- | `id` | Yes | Root number or subtask number (`1`, `1.1`). |
133
- | `status` | Yes | `pending`, `in_progress`, `completed`, `cancelled`, `error`. |
134
- | `content` | No | One-line summary shown in the widget. |
135
- | `note` | No | Technical context or evidence. Pass `null` to remove. |
136
- | `priority` | No | `high`, `medium`, or `low`. Pass `null` to remove. |
173
+ | `id` | Yes | Root number or subtask number (`1`, `1.1`). |
174
+ | `status` | Yes | `pending`, `in_progress`, `completed`, `cancelled`, `error`. |
175
+ | `content` | No | One-line summary shown in the widget. |
176
+ | `note` | No | Technical context or evidence. Pass `null` to remove. |
177
+ | `priority` | No | `high`, `medium`, or `low`. Pass `null` to remove. |
137
178
 
138
179
  ## Skills
139
180
 
@@ -152,15 +193,3 @@ bun run compile # rebuild dist/ (tsc + Solid JSX transform)
152
193
  `bun run compile` must be used instead of `bun run build`: the raw `tsc` pass
153
194
  alone leaves intermediate `.jsx` files in `dist/`, which Bun then prefers over
154
195
  the compiled `.js` and reports as `No renderer found` / `ENOENT ... dist/tui.jsx`.
155
-
156
- ## Publishing notes
157
-
158
- `dist/`, `index.js`, and `tui.js` are generated build output and are not
159
- committed. They reach users through the npm tarball: npm runs `prepare`
160
- (`bun run compile`) before packing, and `files` ships the built `dist/`
161
- directory. The `opencode plugin add` installer skips lifecycle scripts
162
- (`ignoreScripts: true`), so the tarball — not a post-install build — must
163
- contain `dist/`.
164
-
165
- Before publishing, run `bun run check && bun run compile` and confirm
166
- `dist/` contains no `.jsx` files.
@@ -0,0 +1,77 @@
1
+ import { type ColorInput } from "@opentui/core";
2
+ /** Cell size accepted by the layout engine. */
3
+ export type GlyphSize = number | "auto" | `${number}%`;
4
+ export type GlyphProps = {
5
+ /**
6
+ * The glyph to draw. Pass a function to keep it reactive (the arrow depends
7
+ * on `isCollapsed()`), or a plain string for a fixed symbol.
8
+ */
9
+ glyph: (() => string) | string;
10
+ color?: ColorInput;
11
+ /** Shortcut: stretch to `100%` on both axes (fill the parent). */
12
+ fill?: boolean;
13
+ /** Shorthand that sets both `width` and `height`. */
14
+ size?: GlyphSize;
15
+ /** Horizontal cells; overrides `fill` and `size`. Defaults to `"auto"`. */
16
+ width?: GlyphSize;
17
+ /** Vertical cells; overrides `fill` and `size`. Defaults to `"auto"`. */
18
+ height?: GlyphSize;
19
+ };
20
+ export type GlyphBox = {
21
+ readonly width: GlyphSize;
22
+ readonly height: GlyphSize;
23
+ };
24
+ /**
25
+ * Resolves the box for a glyph.
26
+ *
27
+ * Precedence per axis: `width`/`height` > `fill` (100%) > `size` > `"auto"`.
28
+ *
29
+ * The default is `"auto"`, so the zero-configuration case fills exactly what
30
+ * the glyph needs. `fill` switches both axes to `100%` in one word, which is
31
+ * the simple "just stretch to whatever space there is" form.
32
+ *
33
+ * Kept pure so the rules are testable without a live renderer.
34
+ */
35
+ export declare function ResolveGlyphBox(props: {
36
+ fill?: boolean;
37
+ size?: GlyphSize;
38
+ width?: GlyphSize;
39
+ height?: GlyphSize;
40
+ }): GlyphBox;
41
+ /**
42
+ * A colored terminal glyph in a single text node.
43
+ *
44
+ * Matches OpenCode's own MCP sidebar, which renders
45
+ * `<text fg={...}>{open ? "▼" : "▶"}</text>`
46
+ * (packages/tui/src/feature-plugins/sidebar/mcp.tsx). A `fg` attribute on
47
+ * `<text>` is reactive: TextBufferRenderable's fg setter calls requestRender().
48
+ *
49
+ * `glyph` accepts either an accessor or a plain string. Solid hands components
50
+ * getters, so an accessor stays live while a string is simply constant.
51
+ *
52
+ * Sizing: `width` and `height` default to `"auto"`. `fill` stretches both axes
53
+ * to `100%` in one word, `size` sets a square box, and explicit `width`/`height`
54
+ * win over both.
55
+ *
56
+ * A terminal cannot scale a character — a glyph always paints in one cell, so
57
+ * `size`/`fill` reserve layout space rather than enlarging the symbol. To make a
58
+ * control read as larger, give it a bigger box (or use the SVG icon path where
59
+ * the terminal supports images).
60
+ */
61
+ export declare function Glyph(props: GlyphProps): import("solid-js").JSX.Element;
62
+ /**
63
+ * The zero-configuration variant: only a glyph and an optional colour, with
64
+ * auto width and height. Use this unless a specific box is required.
65
+ */
66
+ export declare function GlyphAuto(props: {
67
+ glyph: (() => string) | string;
68
+ color?: ColorInput;
69
+ }): import("solid-js").JSX.Element;
70
+ /**
71
+ * The stretch variant: fills `100%` of the parent on both axes, so the size
72
+ * follows the surrounding layout without hand-tuned numbers.
73
+ */
74
+ export declare function GlyphFill(props: {
75
+ glyph: (() => string) | string;
76
+ color?: ColorInput;
77
+ }): import("solid-js").JSX.Element;
@@ -0,0 +1,103 @@
1
+ import { createComponent as _$createComponent } from "@opentui/solid";
2
+ import { effect as _$effect } from "@opentui/solid";
3
+ import { insert as _$insert } from "@opentui/solid";
4
+ import { setProp as _$setProp } from "@opentui/solid";
5
+ import { createElement as _$createElement } from "@opentui/solid";
6
+ /** Cell size accepted by the layout engine. */
7
+
8
+ /**
9
+ * Resolves the box for a glyph.
10
+ *
11
+ * Precedence per axis: `width`/`height` > `fill` (100%) > `size` > `"auto"`.
12
+ *
13
+ * The default is `"auto"`, so the zero-configuration case fills exactly what
14
+ * the glyph needs. `fill` switches both axes to `100%` in one word, which is
15
+ * the simple "just stretch to whatever space there is" form.
16
+ *
17
+ * Kept pure so the rules are testable without a live renderer.
18
+ */
19
+ export function ResolveGlyphBox(props) {
20
+ const fillSize = "100%";
21
+ return {
22
+ width: props.width ?? (props.fill ? fillSize : undefined) ?? props.size ?? "auto",
23
+ height: props.height ?? (props.fill ? fillSize : undefined) ?? props.size ?? "auto"
24
+ };
25
+ }
26
+
27
+ /**
28
+ * A colored terminal glyph in a single text node.
29
+ *
30
+ * Matches OpenCode's own MCP sidebar, which renders
31
+ * `<text fg={...}>{open ? "▼" : "▶"}</text>`
32
+ * (packages/tui/src/feature-plugins/sidebar/mcp.tsx). A `fg` attribute on
33
+ * `<text>` is reactive: TextBufferRenderable's fg setter calls requestRender().
34
+ *
35
+ * `glyph` accepts either an accessor or a plain string. Solid hands components
36
+ * getters, so an accessor stays live while a string is simply constant.
37
+ *
38
+ * Sizing: `width` and `height` default to `"auto"`. `fill` stretches both axes
39
+ * to `100%` in one word, `size` sets a square box, and explicit `width`/`height`
40
+ * win over both.
41
+ *
42
+ * A terminal cannot scale a character — a glyph always paints in one cell, so
43
+ * `size`/`fill` reserve layout space rather than enlarging the symbol. To make a
44
+ * control read as larger, give it a bigger box (or use the SVG icon path where
45
+ * the terminal supports images).
46
+ */
47
+ export function Glyph(props) {
48
+ const text = () => {
49
+ const glyph = props.glyph;
50
+ return typeof glyph === "function" ? glyph() : glyph;
51
+ };
52
+ const box = () => ResolveGlyphBox(props);
53
+ return (() => {
54
+ var _el$ = _$createElement("text");
55
+ _$setProp(_el$, "wrapMode", "none");
56
+ _$insert(_el$, text);
57
+ _$effect(_p$ => {
58
+ var _v$ = props.color,
59
+ _v$2 = box().width,
60
+ _v$3 = box().height;
61
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$, "fg", _v$, _p$.e));
62
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "width", _v$2, _p$.t));
63
+ _v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "height", _v$3, _p$.a));
64
+ return _p$;
65
+ }, {
66
+ e: undefined,
67
+ t: undefined,
68
+ a: undefined
69
+ });
70
+ return _el$;
71
+ })();
72
+ }
73
+
74
+ /**
75
+ * The zero-configuration variant: only a glyph and an optional colour, with
76
+ * auto width and height. Use this unless a specific box is required.
77
+ */
78
+ export function GlyphAuto(props) {
79
+ return _$createComponent(Glyph, {
80
+ get glyph() {
81
+ return props.glyph;
82
+ },
83
+ get color() {
84
+ return props.color;
85
+ }
86
+ });
87
+ }
88
+
89
+ /**
90
+ * The stretch variant: fills `100%` of the parent on both axes, so the size
91
+ * follows the surrounding layout without hand-tuned numbers.
92
+ */
93
+ export function GlyphFill(props) {
94
+ return _$createComponent(Glyph, {
95
+ get glyph() {
96
+ return props.glyph;
97
+ },
98
+ get color() {
99
+ return props.color;
100
+ },
101
+ fill: true
102
+ });
103
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Terminal glyphs for the TUI widget.
3
+ *
4
+ * These are plain Unicode characters rather than rendered SVG images. Terminal
5
+ * image protocols (Kitty, Sixel) are advertised inconsistently — Termius, for
6
+ * example, advertises Kitty support but fails to load the payload — so a
7
+ * capability probe can select an image branch that never actually renders and
8
+ * never falls back.
9
+ *
10
+ * Reference: OpenCode's own MCP sidebar uses exactly this shape
11
+ * (`packages/tui/src/feature-plugins/sidebar/mcp.tsx`):
12
+ *
13
+ * <text fg={theme.text.base}>{view.open ? "▼" : "▶"}</text>
14
+ *
15
+ * A terminal cannot scale a character: one code point paints in one cell, so
16
+ * `size`/`width`/`height` on a glyph reserve layout space and never enlarge the
17
+ * symbol. To make an indicator look bigger it must be drawn from several cells
18
+ * (block art) or rendered as an image on terminals that support it.
19
+ */
20
+ /** Collapsed indicator. */
21
+ export declare const ARROW_RIGHT = "\u25B6";
22
+ /** Expanded indicator. */
23
+ export declare const ARROW_DOWN = "\u25BC";
24
+ /** Continue action. */
25
+ export declare const CONTINUE = "\u21BB";
26
+ /** Clear action. */
27
+ export declare const CLEAR = "\u2A2F";
28
+ export declare const ALL_GLYPHS: {
29
+ readonly ARROW_RIGHT: "▶";
30
+ readonly ARROW_DOWN: "▼";
31
+ readonly CONTINUE: "↻";
32
+ readonly CLEAR: "⨯";
33
+ };
34
+ /**
35
+ * Exact codepoints shipped. Pinned so tests catch an accidental swap to a
36
+ * visually similar character that may not exist in every terminal font.
37
+ *
38
+ * Update these whenever a glyph above changes; the test compares the two.
39
+ */
40
+ export declare const GLYPH_CODEPOINTS: {
41
+ readonly ARROW_RIGHT: 9654;
42
+ readonly ARROW_DOWN: 9660;
43
+ readonly CONTINUE: 8635;
44
+ readonly CLEAR: 10799;
45
+ };
46
+ /**
47
+ * Rejected look-alikes: characters that are visually similar but too small or
48
+ * not universally available in terminal fonts. Kept so a regression cannot
49
+ * quietly reintroduce a shrunk indicator.
50
+ */
51
+ export declare const REJECTED_GLYPHS: readonly ["▸", "▾", "▷", "◁"];
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Terminal glyphs for the TUI widget.
3
+ *
4
+ * These are plain Unicode characters rather than rendered SVG images. Terminal
5
+ * image protocols (Kitty, Sixel) are advertised inconsistently — Termius, for
6
+ * example, advertises Kitty support but fails to load the payload — so a
7
+ * capability probe can select an image branch that never actually renders and
8
+ * never falls back.
9
+ *
10
+ * Reference: OpenCode's own MCP sidebar uses exactly this shape
11
+ * (`packages/tui/src/feature-plugins/sidebar/mcp.tsx`):
12
+ *
13
+ * <text fg={theme.text.base}>{view.open ? "▼" : "▶"}</text>
14
+ *
15
+ * A terminal cannot scale a character: one code point paints in one cell, so
16
+ * `size`/`width`/`height` on a glyph reserve layout space and never enlarge the
17
+ * symbol. To make an indicator look bigger it must be drawn from several cells
18
+ * (block art) or rendered as an image on terminals that support it.
19
+ */
20
+ /** Collapsed indicator. */
21
+ export const ARROW_RIGHT = "▶";
22
+ /** Expanded indicator. */
23
+ export const ARROW_DOWN = "▼";
24
+ /** Continue action. */
25
+ export const CONTINUE = "↻";
26
+ /** Clear action. */
27
+ export const CLEAR = "⨯";
28
+ export const ALL_GLYPHS = {
29
+ ARROW_RIGHT,
30
+ ARROW_DOWN,
31
+ CONTINUE,
32
+ CLEAR,
33
+ };
34
+ /**
35
+ * Exact codepoints shipped. Pinned so tests catch an accidental swap to a
36
+ * visually similar character that may not exist in every terminal font.
37
+ *
38
+ * Update these whenever a glyph above changes; the test compares the two.
39
+ */
40
+ export const GLYPH_CODEPOINTS = {
41
+ ARROW_RIGHT: 0x25b6, // ▶
42
+ ARROW_DOWN: 0x25bc, // ▼
43
+ CONTINUE: 0x21bb, // ↻
44
+ CLEAR: 0x2a2f, // ⨯
45
+ };
46
+ /**
47
+ * Rejected look-alikes: characters that are visually similar but too small or
48
+ * not universally available in terminal fonts. Kept so a regression cannot
49
+ * quietly reintroduce a shrunk indicator.
50
+ */
51
+ export const REJECTED_GLYPHS = [
52
+ "▸", // U+25B8 small triangle, too small at this size
53
+ "▾", // U+25BE small triangle, too small at this size
54
+ "▷", // U+25B7 outline triangle, too small
55
+ "◁", // U+25C1 outline triangle, too small
56
+ ];
package/dist/index.d.ts CHANGED
@@ -1,11 +1,30 @@
1
1
  import { Plugin } from "@opencode/plugin";
2
- import { type TodoRootItem } from "./schemas/todo";
3
- export declare function formatReadOutput(todos: readonly TodoRootItem[]): string;
2
+ import { type TodoReadInput, type TodoRootItem } from "./schemas/todo";
3
+ export declare function formatReadOutput(todos: readonly TodoRootItem[], options?: {
4
+ visibleIds?: readonly number[];
5
+ }): string;
4
6
  export declare function formatTransitionLines(before: readonly TodoRootItem[], after: readonly TodoRootItem[]): string[];
5
7
  export declare function formatWriteOutput(todos: readonly TodoRootItem[], before?: readonly TodoRootItem[]): string;
6
8
  export declare function assertWriteIsInitialization(existing: readonly TodoRootItem[], incoming: readonly TodoRootItem[]): void;
7
9
  /** Update response: progress, next step, and transition lines without resending the list. */
8
10
  export declare function formatUpdateOutput(todos: readonly TodoRootItem[], before?: readonly TodoRootItem[]): string;
11
+ /**
12
+ * Narrows a list for a scoped read.
13
+ *
14
+ * - `scope: "root"` returns one root with every subtask, so the whole branch is
15
+ * visible.
16
+ * - `scope: "children"` prints the root line as context plus the targeted
17
+ * subtask line, matching a "show me this subtask within its root" read.
18
+ * - `scope: "item"` prints only the targeted row.
19
+ *
20
+ * Either way the root keeps every subtask in the *data*, because the root's
21
+ * `(completed/total)` label is computed from them — dropping them would
22
+ * silently turn `(2/2)` into `(1/1)`.
23
+ */
24
+ export declare function selectReadScope(todos: readonly TodoRootItem[], input: TodoReadInput): {
25
+ todos: TodoRootItem[];
26
+ visibleIds?: readonly number[];
27
+ };
9
28
  export declare function registerTodoTools(ctx: Plugin.Context): Promise<void>;
10
29
  export declare function registerTodoPromptHook(ctx: Plugin.Context): Promise<void>;
11
30
  declare const _default: Plugin.Plugin;
package/dist/index.js CHANGED
@@ -5,10 +5,10 @@ import { readTodoData, readTodos, updateTodos } from "./storage/todo-store.js";
5
5
  import { buildTodoHint, consumeTodoHint, findNextTodo, findStuckParent, flattenLeaves, isAnyTodoActive, } from "./todos/hint.js";
6
6
  import { TodoTree } from "./todos/todo-tree.js";
7
7
  import { updateTodoById } from "./todos/update.js";
8
- export function formatReadOutput(todos) {
8
+ export function formatReadOutput(todos, options) {
9
9
  if (todos.length === 0)
10
10
  return "No todos recorded.";
11
- const tree = new TodoTree(todos).render();
11
+ const tree = new TodoTree(todos).render(options);
12
12
  const check = new TodoTree(todos).validateAndNormalize();
13
13
  if (check.valid)
14
14
  return tree;
@@ -75,6 +75,38 @@ export function formatUpdateOutput(todos, before = []) {
75
75
  const progress = `Todos (${stats.completedSubTasks}/${stats.totalSubTasks}).`;
76
76
  return formatWriteOutput(todos, before).replace(/^Todos \(\d+\/\d+\) updated\./, progress.slice(0, -1));
77
77
  }
78
+ /**
79
+ * Narrows a list for a scoped read.
80
+ *
81
+ * - `scope: "root"` returns one root with every subtask, so the whole branch is
82
+ * visible.
83
+ * - `scope: "children"` prints the root line as context plus the targeted
84
+ * subtask line, matching a "show me this subtask within its root" read.
85
+ * - `scope: "item"` prints only the targeted row.
86
+ *
87
+ * Either way the root keeps every subtask in the *data*, because the root's
88
+ * `(completed/total)` label is computed from them — dropping them would
89
+ * silently turn `(2/2)` into `(1/1)`.
90
+ */
91
+ export function selectReadScope(todos, input) {
92
+ if (input.scope !== "item" && input.scope !== "children" && input.scope !== "root") {
93
+ return { todos: [...todos] };
94
+ }
95
+ const target = input.target;
96
+ const rootId = Number.isInteger(target) ? target : Math.floor(target);
97
+ const root = todos.find((item) => item.id === rootId);
98
+ const child = Number.isInteger(target)
99
+ ? undefined
100
+ : root?.children?.find((item) => item.id === target);
101
+ if (!root || (!Number.isInteger(target) && !child)) {
102
+ throw new Error(`[manage_todo_list] todo id ${target} was not found`);
103
+ }
104
+ // `root` shows the branch as-is; it needs no row filter at all.
105
+ if (input.scope === "root")
106
+ return { todos: [root] };
107
+ const visibleIds = input.scope === "children" ? [rootId, target] : [target];
108
+ return { todos: [root], visibleIds };
109
+ }
78
110
  export async function registerTodoTools(ctx) {
79
111
  await ctx.tool.transform((tools) => {
80
112
  tools.add({
@@ -97,7 +129,13 @@ export async function registerTodoTools(ctx) {
97
129
  }
98
130
  if (parsed.data.operation === "read") {
99
131
  const todos = await readTodos(toolContext.sessionID);
100
- return { output: { todos }, content: formatReadOutput(todos) };
132
+ const scoped = selectReadScope(todos, parsed.data);
133
+ return {
134
+ output: { todos: scoped.todos },
135
+ content: formatReadOutput(scoped.todos, {
136
+ visibleIds: scoped.visibleIds,
137
+ }),
138
+ };
101
139
  }
102
140
  if (parsed.data.operation === "update") {
103
141
  const updateInput = parsed.data;