opencode-session-todo 0.1.2 → 0.2.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 +2 -0
- package/USAGE.md +3 -1
- package/dist/tui.js +20 -32
- package/index.ts +14 -6
- package/package.json +1 -1
- package/rpc.ts +1 -1
- package/src/instructions.ts +25 -2
- package/src/summary.ts +32 -0
- package/tui.tsx +2 -18
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
|
+
[](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
|
@@ -23,6 +23,9 @@ Reopen the TUI after configuration changes. `sidebarLimit` is a positive integer
|
|
|
23
23
|
|
|
24
24
|
Only root sessions expose these tools and receive the TODO lifecycle instructions. Subagents see neither tool.
|
|
25
25
|
|
|
26
|
+
Agents start a TODO list when they have more than one work item to track, not for a straightforward
|
|
27
|
+
question or a single work item. Existing unfinished work remains tracked through completion.
|
|
28
|
+
|
|
26
29
|
- `todoread({})` reads the current session's tasks and file path. Missing files return an empty list.
|
|
27
30
|
- `todowrite({ previous, todos })` replaces the ordered list only when the stored list still equals `previous`; take `previous` from the latest `todoread` or `todowrite` result. A mismatch fails with an instruction to `todoread` and retry. The result reports a before/after diff. An empty `todos` array clears the list.
|
|
28
31
|
|
|
@@ -46,7 +49,6 @@ cannot begin with a priority/date, and cannot contain `status:` or `pri:` tokens
|
|
|
46
49
|
with the active task first.
|
|
47
50
|
- **Show all …**, `/todos`, or **Ctrl+P → Toggle session todos** opens the full scrollable panel.
|
|
48
51
|
- 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.
|
|
50
52
|
- Manual file edits appear within two seconds. Collapsing does not change the tasks.
|
|
51
53
|
|
|
52
54
|
## 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
|
-
//
|
|
97
|
-
var
|
|
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":
|
|
@@ -134,16 +137,6 @@ var tui_default = Plugin.define({
|
|
|
134
137
|
open: true
|
|
135
138
|
}
|
|
136
139
|
});
|
|
137
|
-
const notify = api.events.on("updated", (event) => {
|
|
138
|
-
if (event.data.diff === "")
|
|
139
|
-
return;
|
|
140
|
-
ctx.ui.toast.show({
|
|
141
|
-
title: "TODO updated",
|
|
142
|
-
message: event.data.diff,
|
|
143
|
-
variant: "info",
|
|
144
|
-
sessionID: event.data.sessionID
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
140
|
const List = (props) => {
|
|
148
141
|
const [view, setView] = createSignal({
|
|
149
142
|
status: "loading"
|
|
@@ -307,12 +300,8 @@ var tui_default = Plugin.define({
|
|
|
307
300
|
return todos();
|
|
308
301
|
},
|
|
309
302
|
children: (todo) => (() => {
|
|
310
|
-
var _el$17 = _$createElement("text")
|
|
311
|
-
_$
|
|
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);
|
|
303
|
+
var _el$17 = _$createElement("text");
|
|
304
|
+
_$insert(_el$17, () => formatTodo(todo));
|
|
316
305
|
_$effect((_$p) => _$setProp(_el$17, "fg", colorFor(todo.status, ctx.theme), _$p));
|
|
317
306
|
return _el$17;
|
|
318
307
|
})()
|
|
@@ -369,26 +358,26 @@ var tui_default = Plugin.define({
|
|
|
369
358
|
}]
|
|
370
359
|
}));
|
|
371
360
|
return (() => {
|
|
372
|
-
var _el$
|
|
373
|
-
_$insertNode(_el$
|
|
374
|
-
_$insertNode(_el$
|
|
375
|
-
_$setProp(_el$
|
|
376
|
-
_$setProp(_el$
|
|
377
|
-
_$setProp(_el$
|
|
378
|
-
_$insertNode(_el$
|
|
379
|
-
_$setProp(_el$
|
|
361
|
+
var _el$18 = _$createElement("box"), _el$19 = _$createElement("text"), _el$21 = _$createElement("scrollbox");
|
|
362
|
+
_$insertNode(_el$18, _el$19);
|
|
363
|
+
_$insertNode(_el$18, _el$21);
|
|
364
|
+
_$setProp(_el$18, "flexDirection", "column");
|
|
365
|
+
_$setProp(_el$18, "flexGrow", 1);
|
|
366
|
+
_$setProp(_el$18, "padding", 1);
|
|
367
|
+
_$insertNode(_el$19, _$createTextNode(`Close \xD7 \xB7 Esc when focused \xB7 Ctrl+P to toggle`));
|
|
368
|
+
_$setProp(_el$19, "onMouseDown", (event) => {
|
|
380
369
|
event.stopPropagation();
|
|
381
370
|
props.close();
|
|
382
371
|
});
|
|
383
|
-
_$setProp(_el$
|
|
384
|
-
_$insert(_el$
|
|
372
|
+
_$setProp(_el$21, "flexGrow", 1);
|
|
373
|
+
_$insert(_el$21, _$createComponent(List, {
|
|
385
374
|
get sessionID() {
|
|
386
375
|
return props.sessionID;
|
|
387
376
|
},
|
|
388
377
|
full: true
|
|
389
378
|
}));
|
|
390
|
-
_$effect((_$p) => _$setProp(_el$
|
|
391
|
-
return _el$
|
|
379
|
+
_$effect((_$p) => _$setProp(_el$19, "fg", ctx.theme.text.base, _$p));
|
|
380
|
+
return _el$18;
|
|
392
381
|
})();
|
|
393
382
|
};
|
|
394
383
|
const panel = ctx.ui.slot({
|
|
@@ -424,7 +413,6 @@ var tui_default = Plugin.define({
|
|
|
424
413
|
return () => {
|
|
425
414
|
sidebar();
|
|
426
415
|
panel();
|
|
427
|
-
notify();
|
|
428
416
|
};
|
|
429
417
|
}
|
|
430
418
|
});
|
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 {
|
|
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", {
|
|
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
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
|
},
|
package/src/instructions.ts
CHANGED
|
@@ -1,10 +1,33 @@
|
|
|
1
|
-
export const
|
|
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
|
|
16
|
+
|
|
17
|
+
- You MUST create a TODO list when you have more than one work item to track.
|
|
18
|
+
- You MUST NOT create a TODO list for a straightforward question or a single work item.
|
|
19
|
+
- You MUST NOT split a single work item into artificial steps just to justify a TODO list.
|
|
20
|
+
|
|
21
|
+
The following rules apply only when a TODO list is needed or already tracks unfinished work.
|
|
2
22
|
|
|
3
23
|
- You MUST keep this session's TODO list synchronized with the requested work and its actual progress.
|
|
4
|
-
- You MUST
|
|
24
|
+
- You MUST preserve existing tasks, order, statuses, and priorities unless actual progress or the user's instructions require a change.
|
|
5
25
|
- You MUST mark a task in_progress before starting it. You MUST keep at most one task in_progress.
|
|
6
26
|
- You MUST mark a task completed immediately after finishing its work and required verification.
|
|
7
27
|
- 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
28
|
- 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
29
|
- Before your final response, you MUST reconcile the list with what was actually done and accurately retain unfinished work.
|
|
10
30
|
- You SHOULD keep task descriptions concise and actionable.`
|
|
31
|
+
|
|
32
|
+
export const TodoInstructions: string =
|
|
33
|
+
`${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,
|
|
@@ -71,15 +65,6 @@ export default Plugin.define({
|
|
|
71
65
|
const [sidebarView, updateSidebarView] = ctx.storage.store("sidebar", {
|
|
72
66
|
initial: { open: true },
|
|
73
67
|
})
|
|
74
|
-
const notify: () => void = api.events.on("updated", (event): void => {
|
|
75
|
-
if (event.data.diff === "") return
|
|
76
|
-
ctx.ui.toast.show({
|
|
77
|
-
title: "TODO updated",
|
|
78
|
-
message: event.data.diff,
|
|
79
|
-
variant: "info",
|
|
80
|
-
sessionID: event.data.sessionID,
|
|
81
|
-
})
|
|
82
|
-
})
|
|
83
68
|
const List = (props: Props): JSX.Element => {
|
|
84
69
|
const [view, setView] = createSignal<View>({ status: "loading" })
|
|
85
70
|
createEffect((): void => {
|
|
@@ -207,7 +192,7 @@ export default Plugin.define({
|
|
|
207
192
|
<For each={todos()}>
|
|
208
193
|
{(todo): JSX.Element => (
|
|
209
194
|
<text fg={colorFor(todo.status, ctx.theme)}>
|
|
210
|
-
{
|
|
195
|
+
{formatTodo(todo)}
|
|
211
196
|
</text>
|
|
212
197
|
)}
|
|
213
198
|
</For>
|
|
@@ -293,7 +278,6 @@ export default Plugin.define({
|
|
|
293
278
|
return (): void => {
|
|
294
279
|
sidebar()
|
|
295
280
|
panel()
|
|
296
|
-
notify()
|
|
297
281
|
}
|
|
298
282
|
},
|
|
299
283
|
})
|