opencode-session-todo 0.1.2 → 0.2.0

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
@@ -5,6 +5,8 @@
5
5
  Session-local [todo.txt](https://github.com/todotxt/todo.txt) for OpenCode V2.
6
6
  Tools for the agent. A sidebar for you. Plain text you can edit.
7
7
 
8
+ [![npm version](https://img.shields.io/npm/v/opencode-session-todo)](https://www.npmjs.com/package/opencode-session-todo)
9
+
8
10
  ## Get started
9
11
 
10
12
  Add to `opencode.jsonc`:
package/USAGE.md CHANGED
@@ -46,7 +46,7 @@ cannot begin with a priority/date, and cannot contain `status:` or `pri:` tokens
46
46
  with the active task first.
47
47
  - **Show all …**, `/todos`, or **Ctrl+P → Toggle session todos** opens the full scrollable panel.
48
48
  - Close with **Close ×**, **Esc** while focused, or the same command-palette action.
49
- - Each `todowrite` pops a toast with the before/after diff of the change.
49
+ - Update toasts show only added or changed tasks, one line each, with the new status and priority.
50
50
  - Manual file edits appear within two seconds. Collapsing does not change the tasks.
51
51
 
52
52
  ## Storage
package/dist/tui.js CHANGED
@@ -57,7 +57,7 @@ var TodoRpc = Rpc.define({
57
57
  },
58
58
  events: {
59
59
  updated: {
60
- schema: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Session.ID, diff: Schema2.String }))
60
+ schema: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Session.ID, diff: Schema2.String, summary: Schema2.String }))
61
61
  }
62
62
  }
63
63
  });
@@ -93,13 +93,16 @@ var projectTodos = (todos, mode) => {
93
93
  return { kind: "limited", items, remaining, total: todos.length };
94
94
  };
95
95
 
96
- // tui.tsx
97
- var symbols = {
96
+ // src/summary.ts
97
+ var todoSymbols = {
98
98
  pending: "[ ]",
99
99
  in_progress: "[\u2022]",
100
100
  completed: "[\u2713]",
101
101
  cancelled: "[\u2013]"
102
102
  };
103
+ var formatTodo = (todo) => `${todoSymbols[todo.status]} ${todo.content} \xB7 ${todo.priority}`;
104
+
105
+ // tui.tsx
103
106
  var colorFor = (status, theme) => {
104
107
  switch (status) {
105
108
  case "pending":
@@ -135,11 +138,11 @@ var tui_default = Plugin.define({
135
138
  }
136
139
  });
137
140
  const notify = api.events.on("updated", (event) => {
138
- if (event.data.diff === "")
141
+ if (event.data.summary === "")
139
142
  return;
140
143
  ctx.ui.toast.show({
141
144
  title: "TODO updated",
142
- message: event.data.diff,
145
+ message: event.data.summary,
143
146
  variant: "info",
144
147
  sessionID: event.data.sessionID
145
148
  });
@@ -307,12 +310,8 @@ var tui_default = Plugin.define({
307
310
  return todos();
308
311
  },
309
312
  children: (todo) => (() => {
310
- var _el$17 = _$createElement("text"), _el$18 = _$createTextNode(` `), _el$19 = _$createTextNode(` \xB7 `);
311
- _$insertNode(_el$17, _el$18);
312
- _$insertNode(_el$17, _el$19);
313
- _$insert(_el$17, () => symbols[todo.status], _el$18);
314
- _$insert(_el$17, () => todo.content, _el$19);
315
- _$insert(_el$17, () => todo.priority, null);
313
+ var _el$17 = _$createElement("text");
314
+ _$insert(_el$17, () => formatTodo(todo));
316
315
  _$effect((_$p) => _$setProp(_el$17, "fg", colorFor(todo.status, ctx.theme), _$p));
317
316
  return _el$17;
318
317
  })()
@@ -369,26 +368,26 @@ var tui_default = Plugin.define({
369
368
  }]
370
369
  }));
371
370
  return (() => {
372
- var _el$20 = _$createElement("box"), _el$21 = _$createElement("text"), _el$23 = _$createElement("scrollbox");
373
- _$insertNode(_el$20, _el$21);
374
- _$insertNode(_el$20, _el$23);
375
- _$setProp(_el$20, "flexDirection", "column");
376
- _$setProp(_el$20, "flexGrow", 1);
377
- _$setProp(_el$20, "padding", 1);
378
- _$insertNode(_el$21, _$createTextNode(`Close \xD7 \xB7 Esc when focused \xB7 Ctrl+P to toggle`));
379
- _$setProp(_el$21, "onMouseDown", (event) => {
371
+ var _el$18 = _$createElement("box"), _el$19 = _$createElement("text"), _el$21 = _$createElement("scrollbox");
372
+ _$insertNode(_el$18, _el$19);
373
+ _$insertNode(_el$18, _el$21);
374
+ _$setProp(_el$18, "flexDirection", "column");
375
+ _$setProp(_el$18, "flexGrow", 1);
376
+ _$setProp(_el$18, "padding", 1);
377
+ _$insertNode(_el$19, _$createTextNode(`Close \xD7 \xB7 Esc when focused \xB7 Ctrl+P to toggle`));
378
+ _$setProp(_el$19, "onMouseDown", (event) => {
380
379
  event.stopPropagation();
381
380
  props.close();
382
381
  });
383
- _$setProp(_el$23, "flexGrow", 1);
384
- _$insert(_el$23, _$createComponent(List, {
382
+ _$setProp(_el$21, "flexGrow", 1);
383
+ _$insert(_el$21, _$createComponent(List, {
385
384
  get sessionID() {
386
385
  return props.sessionID;
387
386
  },
388
387
  full: true
389
388
  }));
390
- _$effect((_$p) => _$setProp(_el$21, "fg", ctx.theme.text.base, _$p));
391
- return _el$20;
389
+ _$effect((_$p) => _$setProp(_el$19, "fg", ctx.theme.text.base, _$p));
390
+ return _el$18;
392
391
  })();
393
392
  };
394
393
  const panel = ctx.ui.slot({
package/index.ts CHANGED
@@ -10,9 +10,15 @@ import { Effect, Schema, type Scope } from "effect"
10
10
  import { resolve } from "node:path"
11
11
  import { TodoRpc } from "./rpc.ts"
12
12
  import { diffTodos } from "./src/diff.ts"
13
- import { TodoInstructions } from "./src/instructions.ts"
13
+ import {
14
+ TodoInstructions,
15
+ TodoLifecycleInstructions,
16
+ TodoReadInstructions,
17
+ TodoWriteInstructions,
18
+ } from "./src/instructions.ts"
14
19
  import { Snapshot, TodoError, Todos } from "./src/model.ts"
15
20
  import { makeTodoStore, SessionDirectory, type TodoStore, type WriteResult } from "./src/store.ts"
21
+ import { summarizeTodos } from "./src/summary.ts"
16
22
 
17
23
  const WriteInput = Schema.Struct({ previous: Todos, todos: Todos })
18
24
  const ReadInput = Schema.Struct({})
@@ -157,7 +163,11 @@ export const register = (ctx: TodoPluginContext): Effect.Effect<void, never, Sco
157
163
  )
158
164
  const diff: string = diffTodos(saved.previous, saved.todos)
159
165
  yield* rpc.events
160
- .emit("updated", { sessionID: context.sessionID, diff })
166
+ .emit("updated", {
167
+ sessionID: context.sessionID,
168
+ diff,
169
+ summary: summarizeTodos(saved.previous, saved.todos),
170
+ })
161
171
  .pipe(
162
172
  Effect.catch(
163
173
  (error): Effect.Effect<void> =>
@@ -191,8 +201,7 @@ export const register = (ctx: TodoPluginContext): Effect.Effect<void, never, Sco
191
201
  yield* ctx.tool.transform((editor): void => {
192
202
  editor.add({
193
203
  name: "todowrite",
194
- description:
195
- `Replace this session's ordered todo list when it still equals the \`previous\` list you send; take \`previous\` from the latest todoread or todowrite result in this conversation. A mismatch fails the write: use todoread, then retry. Status: pending, in_progress, completed, cancelled; at most one in_progress. Priority: high, medium, low. Text is one trimmed line; status:/pri: are reserved. An empty todos array clears the list. Persists as project-local todo.txt and updates the TUI.\n\n${TodoInstructions}`,
204
+ description: `${TodoWriteInstructions}\n\n${TodoLifecycleInstructions}`,
196
205
  input: WriteInput,
197
206
  output: Snapshot,
198
207
  options: { codemode: false, permission: "todowrite" },
@@ -200,8 +209,7 @@ export const register = (ctx: TodoPluginContext): Effect.Effect<void, never, Sco
200
209
  })
201
210
  editor.add({
202
211
  name: "todoread",
203
- description:
204
- `Read this session's todo.txt list and storage path. Use it to obtain the \`previous\` list todowrite expects, or to retry after a failed write; missing files mean an empty list.\n\n${TodoInstructions}`,
212
+ description: `${TodoReadInstructions}\n\n${TodoLifecycleInstructions}`,
205
213
  input: ReadInput,
206
214
  output: Snapshot,
207
215
  options: { codemode: false, permission: "todowrite" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-session-todo",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Session-local todo.txt tools and TUI for OpenCode V2",
5
5
  "homepage": "https://github.com/alexandru/opencode-todo#readme",
6
6
  "bugs": {
package/rpc.ts CHANGED
@@ -15,7 +15,7 @@ export const TodoRpc = Rpc.define({
15
15
  events: {
16
16
  updated: {
17
17
  schema: Schema.toStandardSchemaV1(
18
- Schema.Struct({ sessionID: Session.ID, diff: Schema.String }),
18
+ Schema.Struct({ sessionID: Session.ID, diff: Schema.String, summary: Schema.String }),
19
19
  ),
20
20
  },
21
21
  },
@@ -1,10 +1,27 @@
1
- export const TodoInstructions: string = `TODO lifecycle
1
+ export const TodoWriteInstructions: string = `### \`todowrite\`
2
+
3
+ Replace this session’s TODO list.
4
+
5
+ - You MUST pass the last known saved list as \`previous\`; use \`[]\` when known empty.
6
+ - You SHOULD reuse the returned saved list for subsequent writes.
7
+ - On conflict, you MUST call \`todoread\` and retry.`
8
+
9
+ export const TodoReadInstructions: string = `### \`todoread\`
10
+
11
+ Read this session’s saved TODO list and storage path.
12
+
13
+ - You SHOULD use \`todoread\` only when the saved list is unknown or \`todowrite\` reports a conflict.`
14
+
15
+ export const TodoLifecycleInstructions: string = `### TODO lifecycle
2
16
 
3
17
  - You MUST keep this session's TODO list synchronized with the requested work and its actual progress.
4
- - You MUST pass the list you expect to be stored as \`previous\` to every todowrite; when the write reports the stored list changed, you MUST use todoread and retry with the refreshed list. You MUST preserve existing tasks, order, statuses, and priorities unless actual progress or the user's instructions require a change.
18
+ - You MUST preserve existing tasks, order, statuses, and priorities unless actual progress or the user's instructions require a change.
5
19
  - You MUST mark a task in_progress before starting it. You MUST keep at most one task in_progress.
6
20
  - You MUST mark a task completed immediately after finishing its work and required verification.
7
21
  - You MUST NOT mark blocked, failed, skipped, unfinished, or unverified work completed. Unstarted or deferred work MUST remain pending; cancelled work MUST be represented as cancelled.
8
22
  - You MUST update the list after each meaningful status transition. You MUST NOT batch status updates until the end of the conversation or leave finished work in_progress.
9
23
  - Before your final response, you MUST reconcile the list with what was actually done and accurately retain unfinished work.
10
24
  - You SHOULD keep task descriptions concise and actionable.`
25
+
26
+ export const TodoInstructions: string =
27
+ `${TodoWriteInstructions}\n\n${TodoReadInstructions}\n\n${TodoLifecycleInstructions}`
package/src/summary.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { Todo, Todos } from "./model.ts"
2
+
3
+ export const todoSymbols: Readonly<Record<Todo["status"], string>> = {
4
+ pending: "[ ]",
5
+ in_progress: "[•]",
6
+ completed: "[✓]",
7
+ cancelled: "[–]",
8
+ }
9
+
10
+ export const formatTodo = (todo: Todo): string =>
11
+ `${todoSymbols[todo.status]} ${todo.content} · ${todo.priority}`
12
+
13
+ const keyFor = (todo: Todo): string => `${todo.status}|${todo.priority}|${todo.content}`
14
+
15
+ export const summarizeTodos = (previous: Todos, next: Todos): string => {
16
+ const unchanged: Map<string, number> = new Map()
17
+ for (const todo of previous) {
18
+ const key: string = keyFor(todo)
19
+ unchanged.set(key, (unchanged.get(key) ?? 0) + 1)
20
+ }
21
+ const lines: string[] = []
22
+ for (const todo of next) {
23
+ const key: string = keyFor(todo)
24
+ const remaining: number = unchanged.get(key) ?? 0
25
+ if (remaining > 0) {
26
+ unchanged.set(key, remaining - 1)
27
+ } else {
28
+ lines.push(formatTodo(todo))
29
+ }
30
+ }
31
+ return lines.join("\n")
32
+ }
package/tui.tsx CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  import { TodoRpc } from "./rpc.ts"
18
18
  import { type Snapshot, type Todo } from "./src/model.ts"
19
19
  import { projectTodos, SidebarOptions, type TodoProjection } from "./src/sidebar.ts"
20
+ import { formatTodo } from "./src/summary.ts"
20
21
 
21
22
  type View =
22
23
  | { readonly status: "loading" }
@@ -28,13 +29,6 @@ interface Props {
28
29
  readonly full?: boolean
29
30
  }
30
31
 
31
- const symbols: Readonly<Record<Todo["status"], string>> = {
32
- pending: "[ ]",
33
- in_progress: "[•]",
34
- completed: "[✓]",
35
- cancelled: "[–]",
36
- }
37
-
38
32
  const colorFor = (
39
33
  status: Todo["status"],
40
34
  theme: ResolvedTheme,
@@ -72,10 +66,10 @@ export default Plugin.define({
72
66
  initial: { open: true },
73
67
  })
74
68
  const notify: () => void = api.events.on("updated", (event): void => {
75
- if (event.data.diff === "") return
69
+ if (event.data.summary === "") return
76
70
  ctx.ui.toast.show({
77
71
  title: "TODO updated",
78
- message: event.data.diff,
72
+ message: event.data.summary,
79
73
  variant: "info",
80
74
  sessionID: event.data.sessionID,
81
75
  })
@@ -207,7 +201,7 @@ export default Plugin.define({
207
201
  <For each={todos()}>
208
202
  {(todo): JSX.Element => (
209
203
  <text fg={colorFor(todo.status, ctx.theme)}>
210
- {symbols[todo.status]} {todo.content} · {todo.priority}
204
+ {formatTodo(todo)}
211
205
  </text>
212
206
  )}
213
207
  </For>