@gamflaz/opencode-todolist 1.1.0 → 1.1.2

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.
package/dist/index.d.ts CHANGED
@@ -1,30 +1,10 @@
1
1
  import { Plugin } from "@opencode/plugin";
2
- import { type TodoReadInput, type TodoRootItem } from "./schemas/todo";
3
- export declare function formatReadOutput(todos: readonly TodoRootItem[], options?: {
4
- visibleIds?: readonly number[];
5
- }): string;
2
+ import { type TodoRootItem } from "./schemas/todo";
6
3
  export declare function formatTransitionLines(before: readonly TodoRootItem[], after: readonly TodoRootItem[]): string[];
7
4
  export declare function formatWriteOutput(todos: readonly TodoRootItem[], before?: readonly TodoRootItem[]): string;
8
5
  export declare function assertWriteIsInitialization(existing: readonly TodoRootItem[], incoming: readonly TodoRootItem[]): void;
9
6
  /** Update response: progress, next step, and transition lines without resending the list. */
10
7
  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
- };
28
8
  export declare function registerTodoTools(ctx: Plugin.Context): Promise<void>;
29
9
  export declare function registerTodoPromptHook(ctx: Plugin.Context): Promise<void>;
30
10
  declare const _default: Plugin.Plugin;
package/dist/index.js CHANGED
@@ -3,17 +3,9 @@ import { TodoInputToolSchema, TodoOutputToolSchema, } from "./schemas/todo.js";
3
3
  import { registerTodoSkills } from "./skills/loader.js";
4
4
  import { readTodoData, readTodos, updateTodos } from "./storage/todo-store.js";
5
5
  import { buildTodoHint, consumeTodoHint, findNextTodo, findStuckParent, flattenLeaves, isAnyTodoActive, } from "./todos/hint.js";
6
+ import { runReadOperation } from "./todos/read.js";
6
7
  import { TodoTree } from "./todos/todo-tree.js";
7
8
  import { updateTodoById } from "./todos/update.js";
8
- export function formatReadOutput(todos, options) {
9
- if (todos.length === 0)
10
- return "No todos recorded.";
11
- const tree = new TodoTree(todos).render(options);
12
- const check = new TodoTree(todos).validateAndNormalize();
13
- if (check.valid)
14
- return tree;
15
- return `Warning: the stored list needs fixing — ${check.error}\n${tree}`;
16
- }
17
9
  export function formatTransitionLines(before, after) {
18
10
  const previous = new Map(flattenLeaves(before).map((leaf) => [leaf.id, leaf.status]));
19
11
  const leaves = flattenLeaves(after);
@@ -60,7 +52,7 @@ export function formatWriteOutput(todos, before = []) {
60
52
  export function assertWriteIsInitialization(existing, incoming) {
61
53
  if (existing.length > 0 && incoming.length > 0) {
62
54
  const messages = [
63
- '[manage_todo_list] Invalid operation: "write".',
55
+ 'Invalid operation: "write".',
64
56
  "",
65
57
  'The "write" operation is only allowed during initialization.',
66
58
  'Use "update" to modify an existing todo list.',
@@ -75,38 +67,6 @@ export function formatUpdateOutput(todos, before = []) {
75
67
  const progress = `Todos (${stats.completedSubTasks}/${stats.totalSubTasks}).`;
76
68
  return formatWriteOutput(todos, before).replace(/^Todos \(\d+\/\d+\) updated\./, progress.slice(0, -1));
77
69
  }
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
- }
110
70
  export async function registerTodoTools(ctx) {
111
71
  await ctx.tool.transform((tools) => {
112
72
  tools.add({
@@ -123,18 +83,16 @@ export async function registerTodoTools(ctx) {
123
83
  execute: async (input, toolContext) => {
124
84
  const parsed = TodoInputToolSchema.safeParse(input);
125
85
  if (!parsed.success) {
126
- throw new Error(`[manage_todo_list validation failed] ${parsed.error.issues
86
+ throw new Error(`Invalid manage_todo_list input: ${parsed.error.issues
127
87
  .map((i) => `${i.path.join(".") || "input"}: ${i.message}`)
128
88
  .join(", ")}`);
129
89
  }
130
90
  if (parsed.data.operation === "read") {
131
91
  const todos = await readTodos(toolContext.sessionID);
132
- const scoped = selectReadScope(todos, parsed.data);
92
+ const result = runReadOperation(todos, parsed.data);
133
93
  return {
134
- output: { todos: scoped.todos },
135
- content: formatReadOutput(scoped.todos, {
136
- visibleIds: scoped.visibleIds,
137
- }),
94
+ output: { todos: result.todos },
95
+ content: result.content,
138
96
  };
139
97
  }
140
98
  if (parsed.data.operation === "update") {
@@ -54,7 +54,7 @@ export function parseLegacyTodos(raw) {
54
54
  parsed = JSON.parse(raw);
55
55
  }
56
56
  catch (error) {
57
- throw new InvalidTodoDataError(`[todo store read failed] invalid todo data: ${error instanceof Error ? error.message : String(error)}`);
57
+ throw new InvalidTodoDataError(`invalid todo data: ${error instanceof Error ? error.message : String(error)}`);
58
58
  }
59
59
  const list = Array.isArray(parsed)
60
60
  ? parsed
@@ -62,11 +62,11 @@ export function parseLegacyTodos(raw) {
62
62
  ? parsed.todos
63
63
  : undefined;
64
64
  if (!Array.isArray(list)) {
65
- throw new InvalidTodoDataError("[todo store read failed] invalid todo data: todo list is missing");
65
+ throw new InvalidTodoDataError("invalid todo data: todo list is missing");
66
66
  }
67
67
  const result = TodoListSchema.safeParse(list);
68
68
  if (!result.success) {
69
- throw new InvalidTodoDataError(`[todo store read failed] invalid todo data: ${result.error.issues
69
+ throw new InvalidTodoDataError(`invalid todo data: ${result.error.issues
70
70
  .map((issue) => issue.message)
71
71
  .join(", ")}`);
72
72
  }
@@ -26,7 +26,7 @@ export class TodoStore {
26
26
  static normalizeSessionID(sessionID) {
27
27
  const normalized = sessionID.trim();
28
28
  if (!TodoStore.SESSION_ID_PATTERN.test(normalized)) {
29
- throw new Error("[todo store] invalid session ID");
29
+ throw new Error("invalid session ID");
30
30
  }
31
31
  return normalized;
32
32
  }
@@ -281,7 +281,7 @@ export class TodoStore {
281
281
  if (!result.valid) {
282
282
  // Throwing inside the transaction rolls it back, so an invalid list
283
283
  // never reaches disk and never bumps the revision.
284
- throw new Error(`[todo store validation failed] ${result.error}`);
284
+ throw new Error(result.error);
285
285
  }
286
286
  return result.data;
287
287
  }).todos;
@@ -0,0 +1,44 @@
1
+ import type { TodoReadInput, TodoRootItem } from "../schemas/todo";
2
+ /**
3
+ * Render a list as the tree the `read` operation answers with.
4
+ *
5
+ * A list that fails validation still renders — showing the rows is more useful
6
+ * than refusing — but the warning goes first so the caller fixes it before
7
+ * trusting the shape.
8
+ */
9
+ export declare function formatReadOutput(todos: readonly TodoRootItem[], options?: {
10
+ visibleIds?: readonly number[];
11
+ }): string;
12
+ /**
13
+ * Narrows a list for a scoped read.
14
+ *
15
+ * - `scope: "root"` returns one root with every subtask, so the whole branch is
16
+ * visible.
17
+ * - `scope: "children"` prints the root line as context plus the targeted
18
+ * subtask line, matching a "show me this subtask within its root" read.
19
+ * - `scope: "item"` prints only the targeted row.
20
+ *
21
+ * Either way the root keeps every subtask in the *data*, because the root's
22
+ * `(completed/total)` label is computed from them — dropping them would
23
+ * silently turn `(2/2)` into `(1/1)`.
24
+ */
25
+ export declare function selectReadScope(todos: readonly TodoRootItem[], input: TodoReadInput): {
26
+ todos: TodoRootItem[];
27
+ visibleIds?: readonly number[];
28
+ };
29
+ export interface ReadOperationResult {
30
+ /** Scope-narrowed list, also what the tool returns as structured output. */
31
+ todos: TodoRootItem[];
32
+ /** Rows to print for `children`/`item`; absent means "print everything". */
33
+ visibleIds?: readonly number[];
34
+ /** Rendered tree, ready to hand back as tool content. */
35
+ content: string;
36
+ }
37
+ /**
38
+ * Whole `read` operation, minus storage.
39
+ *
40
+ * Kept pure on purpose: the caller reads the session's list and passes it in,
41
+ * so this module never needs a session id and mirrors `updateTodoById` instead
42
+ * of the storage-facing wrappers.
43
+ */
44
+ export declare function runReadOperation(todos: readonly TodoRootItem[], input: TodoReadInput): ReadOperationResult;
@@ -0,0 +1,67 @@
1
+ import { TodoTree } from "./todo-tree.js";
2
+ /**
3
+ * Render a list as the tree the `read` operation answers with.
4
+ *
5
+ * A list that fails validation still renders — showing the rows is more useful
6
+ * than refusing — but the warning goes first so the caller fixes it before
7
+ * trusting the shape.
8
+ */
9
+ export function formatReadOutput(todos, options) {
10
+ if (todos.length === 0)
11
+ return "No todos recorded.";
12
+ const tree = new TodoTree(todos).render(options);
13
+ const check = new TodoTree(todos).validateAndNormalize();
14
+ if (check.valid)
15
+ return tree;
16
+ return `Warning: the stored list needs fixing — ${check.error}\n${tree}`;
17
+ }
18
+ /**
19
+ * Narrows a list for a scoped read.
20
+ *
21
+ * - `scope: "root"` returns one root with every subtask, so the whole branch is
22
+ * visible.
23
+ * - `scope: "children"` prints the root line as context plus the targeted
24
+ * subtask line, matching a "show me this subtask within its root" read.
25
+ * - `scope: "item"` prints only the targeted row.
26
+ *
27
+ * Either way the root keeps every subtask in the *data*, because the root's
28
+ * `(completed/total)` label is computed from them — dropping them would
29
+ * silently turn `(2/2)` into `(1/1)`.
30
+ */
31
+ export function selectReadScope(todos, input) {
32
+ if (input.scope !== "item" &&
33
+ input.scope !== "children" &&
34
+ input.scope !== "root") {
35
+ return { todos: [...todos] };
36
+ }
37
+ const target = input.target;
38
+ const rootId = Number.isInteger(target) ? target : Math.floor(target);
39
+ const root = todos.find((item) => item.id === rootId);
40
+ const child = Number.isInteger(target)
41
+ ? undefined
42
+ : root?.children?.find((item) => item.id === target);
43
+ if (!root || (!Number.isInteger(target) && !child)) {
44
+ throw new Error(`todo id ${target} was not found`);
45
+ }
46
+ // `root` shows the branch as-is; it needs no row filter at all.
47
+ if (input.scope === "root")
48
+ return { todos: [root] };
49
+ const visibleIds = input.scope === "children" ? [rootId, target] : [target];
50
+ return { todos: [root], visibleIds };
51
+ }
52
+ /**
53
+ * Whole `read` operation, minus storage.
54
+ *
55
+ * Kept pure on purpose: the caller reads the session's list and passes it in,
56
+ * so this module never needs a session id and mirrors `updateTodoById` instead
57
+ * of the storage-facing wrappers.
58
+ */
59
+ export function runReadOperation(todos, input) {
60
+ const scoped = selectReadScope(todos, input);
61
+ const content = formatReadOutput(scoped.todos, {
62
+ visibleIds: scoped.visibleIds,
63
+ });
64
+ return scoped.visibleIds === undefined
65
+ ? { todos: scoped.todos, content }
66
+ : { todos: scoped.todos, visibleIds: scoped.visibleIds, content };
67
+ }
@@ -54,8 +54,26 @@ export declare class TodoTree {
54
54
  /**
55
55
  * Rejects completed parents with unfinished children, and auto-completes a
56
56
  * parent whose children are all completed.
57
+ *
58
+ * The order check runs **after** the auto-complete above, never before: a
59
+ * parent whose children all finished is raised to `completed` here, and that
60
+ * promotion can itself create an out-of-order list. Checking first would let
61
+ * the promotion slip past the check.
57
62
  */
58
63
  private normalize;
64
+ /**
65
+ * Roots run in order, so nothing after the first unfinished root may be
66
+ * finished.
67
+ *
68
+ * Only `error` is allowed to follow an open root — a broken task has to be
69
+ * fixable even while earlier work is still open. Everything else
70
+ * (`completed`, `in_progress`, `cancelled`) has to wait its turn.
71
+ *
72
+ * This lives here rather than in `update.ts` because `write` and the legacy
73
+ * importer reach the database without passing the update rules at all. One
74
+ * check in the shared validator closes every path at once.
75
+ */
76
+ private static findOrderError;
59
77
  /**
60
78
  * Renders the tree.
61
79
  *
@@ -132,6 +132,11 @@ export class TodoTree {
132
132
  /**
133
133
  * Rejects completed parents with unfinished children, and auto-completes a
134
134
  * parent whose children are all completed.
135
+ *
136
+ * The order check runs **after** the auto-complete above, never before: a
137
+ * parent whose children all finished is raised to `completed` here, and that
138
+ * promotion can itself create an out-of-order list. Checking first would let
139
+ * the promotion slip past the check.
135
140
  */
136
141
  normalize() {
137
142
  const normalized = [];
@@ -156,8 +161,39 @@ export class TodoTree {
156
161
  status: allChildrenCompleted ? "completed" : item.status,
157
162
  });
158
163
  }
164
+ const orderError = TodoTree.findOrderError(normalized);
165
+ if (orderError)
166
+ return { valid: false, error: orderError };
159
167
  return { valid: true, data: normalized };
160
168
  }
169
+ /**
170
+ * Roots run in order, so nothing after the first unfinished root may be
171
+ * finished.
172
+ *
173
+ * Only `error` is allowed to follow an open root — a broken task has to be
174
+ * fixable even while earlier work is still open. Everything else
175
+ * (`completed`, `in_progress`, `cancelled`) has to wait its turn.
176
+ *
177
+ * This lives here rather than in `update.ts` because `write` and the legacy
178
+ * importer reach the database without passing the update rules at all. One
179
+ * check in the shared validator closes every path at once.
180
+ */
181
+ static findOrderError(todos) {
182
+ const isFinal = (status) => status === "completed" || status === "cancelled";
183
+ let firstOpen;
184
+ for (const root of todos) {
185
+ if (!firstOpen) {
186
+ if (!isFinal(root.status))
187
+ firstOpen = root;
188
+ continue;
189
+ }
190
+ // Past the first open root: a later root must still be untouched.
191
+ if (root.status === "pending" || root.status === "error")
192
+ continue;
193
+ return `Task '${root.id}' is '${root.status}' but earlier task '${firstOpen.id}' is still '${firstOpen.status}'. Roots run in order: finish or cancel '${firstOpen.id}' first.`;
194
+ }
195
+ return null;
196
+ }
161
197
  // Rendering
162
198
  /**
163
199
  * Renders the tree.
@@ -38,7 +38,7 @@ function assertForwardTransition(from, to, id) {
38
38
  if (from === to)
39
39
  return;
40
40
  if (!(FORWARD_TRANSITIONS[from] ?? []).includes(to)) {
41
- throw new Error(`[manage_todo_list] todo ${id} cannot move from '${from}' to '${to}'. Progress only moves forward; use 'error' to report a failure and fix it from there.`);
41
+ throw new Error(`todo ${id} cannot move from '${from}' to '${to}'. Progress only moves forward; use 'error' to report a failure and fix it from there.`);
42
42
  }
43
43
  }
44
44
  /** Any root still open, an `error` root included. */
@@ -65,11 +65,11 @@ export function assertSequentialScope(todos, input) {
65
65
  const targetRoot = todos.find((root) => root.id === input.id ||
66
66
  root.children?.some((child) => child.id === input.id));
67
67
  if (!targetRoot) {
68
- throw new Error(`[manage_todo_list] todo id ${input.id} was not found`);
68
+ throw new Error(`todo id ${input.id} was not found`);
69
69
  }
70
70
  const firstOpen = todos.findIndex((root) => !isFinal(root.status));
71
71
  if (firstOpen === -1) {
72
- throw new Error("[manage_todo_list] every todo is already finished; nothing can be updated.");
72
+ throw new Error("every todo is already finished; nothing can be updated.");
73
73
  }
74
74
  const targetRootIndex = todos.indexOf(targetRoot);
75
75
  if (targetRootIndex !== firstOpen && !hasError(targetRoot)) {
@@ -80,10 +80,10 @@ export function assertSequentialScope(todos, input) {
80
80
  ? targetRoot
81
81
  : targetRoot.children?.find((child) => child.id === input.id);
82
82
  if (!target) {
83
- throw new Error(`[manage_todo_list] todo id ${input.id} was not found`);
83
+ throw new Error(`todo id ${input.id} was not found`);
84
84
  }
85
85
  if (isFinal(target.status)) {
86
- throw new Error(`[manage_todo_list] todo ${input.id} is '${target.status}' and final; it cannot be changed.`);
86
+ throw new Error(`todo ${input.id} is '${target.status}' and final; it cannot be changed.`);
87
87
  }
88
88
  assertForwardTransition(target.status, input.status, input.id);
89
89
  if (input.status === "in_progress") {
@@ -93,7 +93,7 @@ export function assertSequentialScope(todos, input) {
93
93
  }
94
94
  const busyChild = targetRoot.children?.find((child) => child.status === "in_progress" && child.id !== input.id);
95
95
  if (busyChild) {
96
- throw new Error(`[manage_todo_list] subtask ${busyChild.id} is still in_progress. Finish it (or cancel it) before starting ${input.id}.`);
96
+ throw new Error(`subtask ${busyChild.id} is still in_progress. Finish it (or cancel it) before starting ${input.id}.`);
97
97
  }
98
98
  }
99
99
  return { targetRoot, target };
@@ -134,7 +134,7 @@ export function updateTodoById(todos, input) {
134
134
  return { ...root, status, children };
135
135
  });
136
136
  if (!updated) {
137
- throw new Error(`[manage_todo_list] todo id ${input.id} was not found`);
137
+ throw new Error(`todo id ${input.id} was not found`);
138
138
  }
139
139
  return { todos: nextTodos, updated };
140
140
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamflaz/opencode-todolist",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Hierarchical todo list plugin for OpenCode with a real-time TUI widget.",
5
5
  "type": "module",
6
6
  "files": [