opencode-session-todo 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +31 -2
- package/USAGE.md +83 -0
- package/dist/tui.js +433 -0
- package/docs/development.md +28 -0
- package/index.ts +211 -0
- package/package.json +63 -4
- package/rpc.ts +22 -0
- package/src/diff.ts +64 -0
- package/src/instructions.ts +10 -0
- package/src/model.ts +51 -0
- package/src/sidebar.ts +44 -0
- package/src/store.ts +162 -0
- package/src/todotxt.ts +150 -0
- package/tui.tsx +299 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexandru Nedelcu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
-
#
|
|
1
|
+
# OpenCode TODO
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**A task list that stays with your session.**
|
|
4
|
+
|
|
5
|
+
Session-local [todo.txt](https://github.com/todotxt/todo.txt) for OpenCode V2.
|
|
6
|
+
Tools for the agent. A sidebar for you. Plain text you can edit.
|
|
7
|
+
|
|
8
|
+
## Get started
|
|
9
|
+
|
|
10
|
+
Add to `opencode.jsonc`:
|
|
11
|
+
|
|
12
|
+
```jsonc
|
|
13
|
+
{
|
|
14
|
+
"$schema": "https://opencode.ai/config.json",
|
|
15
|
+
"plugins": [
|
|
16
|
+
{
|
|
17
|
+
"package": "opencode-session-todo",
|
|
18
|
+
"options": {
|
|
19
|
+
"sidebarLimit": 5,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Reload OpenCode and reopen the TUI.
|
|
27
|
+
|
|
28
|
+
- **Read and write** with `todoread` and `todowrite`, with conflict detection and change diffs.
|
|
29
|
+
- **Keep tasks in view** in the sidebar; open `/todos` for the full list.
|
|
30
|
+
- **Own the file** at `.agents/sessions/<session-id>/todo.txt`.
|
|
31
|
+
|
|
32
|
+
[Usage & permissions](USAGE.md) · [Development & publishing](docs/development.md) · [MIT](LICENSE)
|
package/USAGE.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Session TODOs
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
Configure the plugin in `opencode.jsonc`:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"$schema": "https://opencode.ai/config.json",
|
|
10
|
+
"plugins": [{ "package": "opencode-session-todo", "options": { "sidebarLimit": 5 } }]
|
|
11
|
+
}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Then reload OpenCode:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
opencode reload
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Reopen the TUI after configuration changes. `sidebarLimit` is a positive integer, default **5**.
|
|
21
|
+
|
|
22
|
+
## Tools
|
|
23
|
+
|
|
24
|
+
Only root sessions expose these tools and receive the TODO lifecycle instructions. Subagents see neither tool.
|
|
25
|
+
|
|
26
|
+
- `todoread({})` reads the current session's tasks and file path. Missing files return an empty list.
|
|
27
|
+
- `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
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"previous": [],
|
|
32
|
+
"todos": [
|
|
33
|
+
{ "content": "Run the tests", "status": "in_progress", "priority": "high" }
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Statuses: `pending`, `in_progress`, `completed`, `cancelled`. Priorities: `high`, `medium`, `low`.
|
|
39
|
+
At most one task may be `in_progress`. Content must be one nonempty trimmed line,
|
|
40
|
+
cannot begin with a priority/date, and cannot contain `status:` or `pri:` tokens.
|
|
41
|
+
|
|
42
|
+
## TUI
|
|
43
|
+
|
|
44
|
+
- Click **TODO** or its `▼` / `▶` arrow to expand or collapse the sidebar list.
|
|
45
|
+
- Above `sidebarLimit` total tasks, the sidebar shows only unfinished tasks, capped at the limit,
|
|
46
|
+
with the active task first.
|
|
47
|
+
- **Show all …**, `/todos`, or **Ctrl+P → Toggle session todos** opens the full scrollable panel.
|
|
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.
|
|
50
|
+
- Manual file edits appear within two seconds. Collapsing does not change the tasks.
|
|
51
|
+
|
|
52
|
+
## Storage
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
<project/worktree>/.agents/sessions/<session-id>/todo.txt
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
(A) Run the tests status:in_progress
|
|
60
|
+
(B) Review the changes
|
|
61
|
+
x Read the docs pri:C
|
|
62
|
+
x Superseded approach pri:B status:cancelled
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`A` / `B` / `C` map to high / medium / low; other priority letters are unsupported.
|
|
66
|
+
Add `.agents/sessions/` to your project's ignore rules to keep these files out of version control.
|
|
67
|
+
|
|
68
|
+
## Permissions
|
|
69
|
+
|
|
70
|
+
Both tools use `todowrite`. Writes require agent/session rules resolving to `allow`; `ask` and
|
|
71
|
+
`deny` are rejected. Approval prompts, saved approvals, and experimental policies are unsupported.
|
|
72
|
+
Read-only agents should deny `todowrite`.
|
|
73
|
+
|
|
74
|
+
## Commands
|
|
75
|
+
|
|
76
|
+
From the repository root:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
bun run check
|
|
80
|
+
bun test
|
|
81
|
+
bun run format # dprint fmt
|
|
82
|
+
bun run format:check # dprint check
|
|
83
|
+
```
|
package/dist/tui.js
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// tui.tsx
|
|
3
|
+
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
4
|
+
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
5
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
6
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
7
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
8
|
+
import { memo as _$memo } from "@opentui/solid";
|
|
9
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
10
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
11
|
+
import { Plugin } from "@opencode/plugin/tui";
|
|
12
|
+
import { Session as Session2 } from "@opencode/schema/session";
|
|
13
|
+
import { Effect as Effect2, Result, Schema as Schema4 } from "effect";
|
|
14
|
+
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js";
|
|
15
|
+
|
|
16
|
+
// rpc.ts
|
|
17
|
+
import { Rpc } from "@opencode/schema/rpc";
|
|
18
|
+
import { Session } from "@opencode/schema/session";
|
|
19
|
+
import { Schema as Schema2 } from "effect";
|
|
20
|
+
|
|
21
|
+
// src/model.ts
|
|
22
|
+
import { Schema } from "effect";
|
|
23
|
+
var Content = Schema.String.check(Schema.isMinLength(1), Schema.isTrimmed(), Schema.isPattern(/^[^\r\n\x00]+$/), Schema.makeFilter((value) => !/^(?:\([A-Z]\) |\d{4}-\d{2}-\d{2}(?: |$))/.test(value) && !/(?:^|\s)(?:status|pri):/.test(value), {
|
|
24
|
+
message: "Task text must not start with a priority/date or contain reserved status:/pri: tokens"
|
|
25
|
+
})).pipe(Schema.brand("TodoContent"));
|
|
26
|
+
var Status = Schema.Literals(["pending", "in_progress", "completed", "cancelled"]);
|
|
27
|
+
var Priority = Schema.Literals(["high", "medium", "low"]);
|
|
28
|
+
var Todo = Schema.Struct({ content: Content, status: Status, priority: Priority });
|
|
29
|
+
var Todos = Schema.Array(Todo).check(Schema.makeFilter((todos) => {
|
|
30
|
+
let active = false;
|
|
31
|
+
for (const todo of todos) {
|
|
32
|
+
if (todo.status !== "in_progress")
|
|
33
|
+
continue;
|
|
34
|
+
if (active)
|
|
35
|
+
return false;
|
|
36
|
+
active = true;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}, { message: "At most one todo may be in_progress" })).pipe(Schema.brand("TodoList"));
|
|
40
|
+
var SessionName = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9][A-Za-z0-9_-]*$/)).pipe(Schema.brand("TodoSessionName"));
|
|
41
|
+
var Snapshot = Schema.Struct({ path: Schema.String, todos: Todos });
|
|
42
|
+
|
|
43
|
+
class TodoError extends Schema.TaggedError()("TodoError", {
|
|
44
|
+
message: Schema.String
|
|
45
|
+
}) {
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// rpc.ts
|
|
49
|
+
var TodoRpc = Rpc.define({
|
|
50
|
+
id: "session-todo",
|
|
51
|
+
methods: {
|
|
52
|
+
read: {
|
|
53
|
+
input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Session.ID })),
|
|
54
|
+
output: Schema2.toStandardSchemaV1(Snapshot),
|
|
55
|
+
errors: { storage: Schema2.toStandardSchemaV1(Schema2.Struct({ message: Schema2.String })) }
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
events: {
|
|
59
|
+
updated: {
|
|
60
|
+
schema: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Session.ID, diff: Schema2.String }))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// src/sidebar.ts
|
|
66
|
+
import { Effect, Schema as Schema3 } from "effect";
|
|
67
|
+
var SidebarLimit = Schema3.Int.check(Schema3.isGreaterThan(0)).pipe(Schema3.brand("TodoSidebarLimit"));
|
|
68
|
+
var SidebarOptions = Schema3.Struct({
|
|
69
|
+
sidebarLimit: SidebarLimit.pipe(Schema3.withDecodingDefaultKey(Effect.succeed(5)))
|
|
70
|
+
});
|
|
71
|
+
var incomplete = (todo) => todo.status === "pending" || todo.status === "in_progress";
|
|
72
|
+
var projectTodos = (todos, mode) => {
|
|
73
|
+
if (mode === "full" || todos.length <= mode) {
|
|
74
|
+
return {
|
|
75
|
+
kind: "all",
|
|
76
|
+
items: todos,
|
|
77
|
+
remaining: todos.reduce((count, todo) => count + Number(incomplete(todo)), 0)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const items = [];
|
|
81
|
+
let remaining = 0;
|
|
82
|
+
for (const todo of todos) {
|
|
83
|
+
if (!incomplete(todo))
|
|
84
|
+
continue;
|
|
85
|
+
remaining += 1;
|
|
86
|
+
if (todo.status === "in_progress") {
|
|
87
|
+
if (items.length === mode)
|
|
88
|
+
items.pop();
|
|
89
|
+
items.unshift(todo);
|
|
90
|
+
} else if (items.length < mode)
|
|
91
|
+
items.push(todo);
|
|
92
|
+
}
|
|
93
|
+
return { kind: "limited", items, remaining, total: todos.length };
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// tui.tsx
|
|
97
|
+
var symbols = {
|
|
98
|
+
pending: "[ ]",
|
|
99
|
+
in_progress: "[\u2022]",
|
|
100
|
+
completed: "[\u2713]",
|
|
101
|
+
cancelled: "[\u2013]"
|
|
102
|
+
};
|
|
103
|
+
var colorFor = (status, theme) => {
|
|
104
|
+
switch (status) {
|
|
105
|
+
case "pending":
|
|
106
|
+
return theme.text.base;
|
|
107
|
+
case "in_progress":
|
|
108
|
+
return theme.text.feedback.warning.base;
|
|
109
|
+
case "completed":
|
|
110
|
+
return theme.text.muted;
|
|
111
|
+
case "cancelled":
|
|
112
|
+
return theme.text.feedback.error.base;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var Message = Schema4.Struct({
|
|
116
|
+
message: Schema4.String
|
|
117
|
+
});
|
|
118
|
+
var messageFor = (error) => Schema4.is(Message)(error) ? error.message : String(error);
|
|
119
|
+
var tui_default = Plugin.define({
|
|
120
|
+
id: "session-todo",
|
|
121
|
+
setup(ctx) {
|
|
122
|
+
const options = Schema4.decodeUnknownResult(SidebarOptions)(ctx.options);
|
|
123
|
+
if (Result.isFailure(options)) {
|
|
124
|
+
ctx.ui.toast.show({
|
|
125
|
+
title: "Invalid TODO sidebar options",
|
|
126
|
+
message: options.failure.message,
|
|
127
|
+
variant: "error"
|
|
128
|
+
});
|
|
129
|
+
return () => {};
|
|
130
|
+
}
|
|
131
|
+
const api = ctx.client.rpc(TodoRpc);
|
|
132
|
+
const [sidebarView, updateSidebarView] = ctx.storage.store("sidebar", {
|
|
133
|
+
initial: {
|
|
134
|
+
open: true
|
|
135
|
+
}
|
|
136
|
+
});
|
|
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
|
+
const List = (props) => {
|
|
148
|
+
const [view, setView] = createSignal({
|
|
149
|
+
status: "loading"
|
|
150
|
+
});
|
|
151
|
+
createEffect(() => {
|
|
152
|
+
const sessionID = props.sessionID;
|
|
153
|
+
setView({
|
|
154
|
+
status: "loading"
|
|
155
|
+
});
|
|
156
|
+
const controller = new AbortController;
|
|
157
|
+
let running = false;
|
|
158
|
+
const refresh = () => {
|
|
159
|
+
if (running || controller.signal.aborted)
|
|
160
|
+
return;
|
|
161
|
+
running = true;
|
|
162
|
+
const request = Schema4.decodeUnknownEffect(Session2.ID)(sessionID).pipe(Effect2.flatMap((id) => Effect2.tryPromise({
|
|
163
|
+
try: () => api.read({
|
|
164
|
+
sessionID: id
|
|
165
|
+
}, {
|
|
166
|
+
signal: controller.signal,
|
|
167
|
+
location: ctx.data.session.get(sessionID)?.location ?? ctx.location ?? ctx.data.location.default()
|
|
168
|
+
}),
|
|
169
|
+
catch: (error) => error
|
|
170
|
+
})));
|
|
171
|
+
Effect2.runPromise(request.pipe(Effect2.match({
|
|
172
|
+
onSuccess: (snapshot) => ({
|
|
173
|
+
status: "ready",
|
|
174
|
+
snapshot
|
|
175
|
+
}),
|
|
176
|
+
onFailure: (error) => ({
|
|
177
|
+
status: "error",
|
|
178
|
+
message: messageFor(error)
|
|
179
|
+
})
|
|
180
|
+
}))).then((next) => {
|
|
181
|
+
if (!controller.signal.aborted)
|
|
182
|
+
setView(next);
|
|
183
|
+
}).catch((error) => {
|
|
184
|
+
if (!controller.signal.aborted) {
|
|
185
|
+
setView({
|
|
186
|
+
status: "error",
|
|
187
|
+
message: messageFor(error)
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}).finally(() => {
|
|
191
|
+
running = false;
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
refresh();
|
|
195
|
+
const timer = setInterval(refresh, 2000);
|
|
196
|
+
const unsubscribe = api.events.on("updated", (event) => {
|
|
197
|
+
if (event.data.sessionID === sessionID)
|
|
198
|
+
refresh();
|
|
199
|
+
});
|
|
200
|
+
onCleanup(() => {
|
|
201
|
+
controller.abort();
|
|
202
|
+
clearInterval(timer);
|
|
203
|
+
unsubscribe();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
const snapshot = () => {
|
|
207
|
+
const current = view();
|
|
208
|
+
return current.status === "ready" ? current.snapshot : undefined;
|
|
209
|
+
};
|
|
210
|
+
const projection = createMemo(() => {
|
|
211
|
+
const current = snapshot();
|
|
212
|
+
return current === undefined ? undefined : projectTodos(current.todos, props.full ? "full" : options.success.sidebarLimit);
|
|
213
|
+
});
|
|
214
|
+
const todos = () => projection()?.items ?? [];
|
|
215
|
+
const active = () => projection()?.remaining ?? 0;
|
|
216
|
+
const more = () => {
|
|
217
|
+
const current = projection();
|
|
218
|
+
return current?.kind === "limited" ? current.total : undefined;
|
|
219
|
+
};
|
|
220
|
+
const error = () => {
|
|
221
|
+
const current = view();
|
|
222
|
+
return current.status === "error" ? current.message : undefined;
|
|
223
|
+
};
|
|
224
|
+
const visible = () => Boolean(props.full || snapshot()?.todos.length || error());
|
|
225
|
+
return _$createComponent(Show, {
|
|
226
|
+
get when() {
|
|
227
|
+
return visible();
|
|
228
|
+
},
|
|
229
|
+
get children() {
|
|
230
|
+
var _el$ = _$createElement("box"), _el$2 = _$createElement("box"), _el$4 = _$createElement("text"), _el$5 = _$createElement("b"), _el$7 = _$createElement("span"), _el$8 = _$createTextNode(` (`), _el$0 = _$createTextNode(` remaining)`);
|
|
231
|
+
_$insertNode(_el$, _el$2);
|
|
232
|
+
_$setProp(_el$, "flexDirection", "column");
|
|
233
|
+
_$setProp(_el$, "gap", 0);
|
|
234
|
+
_$setProp(_el$, "paddingTop", 1);
|
|
235
|
+
_$insertNode(_el$2, _el$4);
|
|
236
|
+
_$setProp(_el$2, "flexDirection", "row");
|
|
237
|
+
_$setProp(_el$2, "gap", 1);
|
|
238
|
+
_$setProp(_el$2, "onMouseDown", (event) => {
|
|
239
|
+
if (props.full)
|
|
240
|
+
return;
|
|
241
|
+
event.stopPropagation();
|
|
242
|
+
updateSidebarView((draft) => {
|
|
243
|
+
draft.open = !draft.open;
|
|
244
|
+
}).catch((error) => {
|
|
245
|
+
ctx.ui.toast.show({
|
|
246
|
+
title: "Todo sidebar",
|
|
247
|
+
message: `Cannot save expanded state: ${messageFor(error)}`,
|
|
248
|
+
variant: "error"
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
_$insert(_el$2, _$createComponent(Show, {
|
|
253
|
+
get when() {
|
|
254
|
+
return !props.full;
|
|
255
|
+
},
|
|
256
|
+
get children() {
|
|
257
|
+
var _el$3 = _$createElement("text");
|
|
258
|
+
_$insert(_el$3, () => sidebarView.open ? "\u25BC" : "\u25B6");
|
|
259
|
+
_$effect((_$p) => _$setProp(_el$3, "fg", ctx.theme.text.base, _$p));
|
|
260
|
+
return _el$3;
|
|
261
|
+
}
|
|
262
|
+
}), _el$4);
|
|
263
|
+
_$insertNode(_el$4, _el$5);
|
|
264
|
+
_$insertNode(_el$4, _el$7);
|
|
265
|
+
_$insertNode(_el$5, _$createTextNode(`TODO`));
|
|
266
|
+
_$insertNode(_el$7, _el$8);
|
|
267
|
+
_$insertNode(_el$7, _el$0);
|
|
268
|
+
_$insert(_el$7, active, _el$0);
|
|
269
|
+
_$insert(_el$, _$createComponent(Show, {
|
|
270
|
+
get when() {
|
|
271
|
+
return props.full || sidebarView.open;
|
|
272
|
+
},
|
|
273
|
+
get children() {
|
|
274
|
+
return [_$createComponent(Show, {
|
|
275
|
+
get when() {
|
|
276
|
+
return props.full;
|
|
277
|
+
},
|
|
278
|
+
get children() {
|
|
279
|
+
var _el$1 = _$createElement("text");
|
|
280
|
+
_$insert(_el$1, () => snapshot()?.path ?? "Loading todo.txt\u2026");
|
|
281
|
+
_$effect((_$p) => _$setProp(_el$1, "fg", ctx.theme.text.muted, _$p));
|
|
282
|
+
return _el$1;
|
|
283
|
+
}
|
|
284
|
+
}), _$createComponent(Show, {
|
|
285
|
+
get when() {
|
|
286
|
+
return error();
|
|
287
|
+
},
|
|
288
|
+
get children() {
|
|
289
|
+
var _el$10 = _$createElement("text"), _el$11 = _$createTextNode(`Cannot read todos: `);
|
|
290
|
+
_$insertNode(_el$10, _el$11);
|
|
291
|
+
_$insert(_el$10, error, null);
|
|
292
|
+
_$effect((_$p) => _$setProp(_el$10, "fg", ctx.theme.text.feedback.error.base, _$p));
|
|
293
|
+
return _el$10;
|
|
294
|
+
}
|
|
295
|
+
}), _$createComponent(Show, {
|
|
296
|
+
get when() {
|
|
297
|
+
return _$memo(() => !!(props.full && view().status === "ready"))() && todos().length === 0;
|
|
298
|
+
},
|
|
299
|
+
get children() {
|
|
300
|
+
var _el$12 = _$createElement("text");
|
|
301
|
+
_$insertNode(_el$12, _$createTextNode(`No todos for this session.`));
|
|
302
|
+
_$effect((_$p) => _$setProp(_el$12, "fg", ctx.theme.text.muted, _$p));
|
|
303
|
+
return _el$12;
|
|
304
|
+
}
|
|
305
|
+
}), _$createComponent(For, {
|
|
306
|
+
get each() {
|
|
307
|
+
return todos();
|
|
308
|
+
},
|
|
309
|
+
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);
|
|
316
|
+
_$effect((_$p) => _$setProp(_el$17, "fg", colorFor(todo.status, ctx.theme), _$p));
|
|
317
|
+
return _el$17;
|
|
318
|
+
})()
|
|
319
|
+
}), _$createComponent(Show, {
|
|
320
|
+
get when() {
|
|
321
|
+
return more();
|
|
322
|
+
},
|
|
323
|
+
get children() {
|
|
324
|
+
var _el$14 = _$createElement("text"), _el$15 = _$createTextNode(`Show all `), _el$16 = _$createTextNode(` todos \u2192`);
|
|
325
|
+
_$insertNode(_el$14, _el$15);
|
|
326
|
+
_$insertNode(_el$14, _el$16);
|
|
327
|
+
_$setProp(_el$14, "onMouseDown", (event) => {
|
|
328
|
+
event.stopPropagation();
|
|
329
|
+
ctx.ui.panel.open("session-todo.list");
|
|
330
|
+
});
|
|
331
|
+
_$insert(_el$14, more, _el$16);
|
|
332
|
+
_$effect((_$p) => _$setProp(_el$14, "fg", ctx.theme.text.feedback.info.base, _$p));
|
|
333
|
+
return _el$14;
|
|
334
|
+
}
|
|
335
|
+
})];
|
|
336
|
+
}
|
|
337
|
+
}), null);
|
|
338
|
+
_$effect((_p$) => {
|
|
339
|
+
var _v$ = ctx.theme.text.base, _v$2 = {
|
|
340
|
+
fg: ctx.theme.text.muted
|
|
341
|
+
};
|
|
342
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$4, "fg", _v$, _p$.e));
|
|
343
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$7, "style", _v$2, _p$.t));
|
|
344
|
+
return _p$;
|
|
345
|
+
}, {
|
|
346
|
+
e: undefined,
|
|
347
|
+
t: undefined
|
|
348
|
+
});
|
|
349
|
+
return _el$;
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
};
|
|
353
|
+
const sidebar = ctx.ui.slot({
|
|
354
|
+
append: "sidebar.content",
|
|
355
|
+
render: (props) => _$createComponent(List, {
|
|
356
|
+
get sessionID() {
|
|
357
|
+
return props.sessionID;
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
});
|
|
361
|
+
const Panel = (props) => {
|
|
362
|
+
ctx.keymap.layer(() => ({
|
|
363
|
+
mode: "global",
|
|
364
|
+
enabled: props.focused,
|
|
365
|
+
commands: [{
|
|
366
|
+
bind: "escape",
|
|
367
|
+
title: "Close session todos",
|
|
368
|
+
run: props.close
|
|
369
|
+
}]
|
|
370
|
+
}));
|
|
371
|
+
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) => {
|
|
380
|
+
event.stopPropagation();
|
|
381
|
+
props.close();
|
|
382
|
+
});
|
|
383
|
+
_$setProp(_el$23, "flexGrow", 1);
|
|
384
|
+
_$insert(_el$23, _$createComponent(List, {
|
|
385
|
+
get sessionID() {
|
|
386
|
+
return props.sessionID;
|
|
387
|
+
},
|
|
388
|
+
full: true
|
|
389
|
+
}));
|
|
390
|
+
_$effect((_$p) => _$setProp(_el$21, "fg", ctx.theme.text.base, _$p));
|
|
391
|
+
return _el$20;
|
|
392
|
+
})();
|
|
393
|
+
};
|
|
394
|
+
const panel = ctx.ui.slot({
|
|
395
|
+
append: "session.panel",
|
|
396
|
+
render: (props) => _$createComponent(Show, {
|
|
397
|
+
get when() {
|
|
398
|
+
return props.name === "session-todo.list";
|
|
399
|
+
},
|
|
400
|
+
get children() {
|
|
401
|
+
return _$createComponent(Panel, props);
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
});
|
|
405
|
+
ctx.keymap.layer(() => ({
|
|
406
|
+
mode: "global",
|
|
407
|
+
commands: [{
|
|
408
|
+
id: "session-todo.list",
|
|
409
|
+
title: "Toggle session todos",
|
|
410
|
+
group: "Session",
|
|
411
|
+
palette: true,
|
|
412
|
+
slash: {
|
|
413
|
+
name: "todos"
|
|
414
|
+
},
|
|
415
|
+
enabled: ctx.ui.router.current().type === "session",
|
|
416
|
+
run: () => {
|
|
417
|
+
if (ctx.ui.panel.current()?.name === "session-todo.list")
|
|
418
|
+
ctx.ui.panel.close();
|
|
419
|
+
else
|
|
420
|
+
ctx.ui.panel.open("session-todo.list");
|
|
421
|
+
}
|
|
422
|
+
}]
|
|
423
|
+
}));
|
|
424
|
+
return () => {
|
|
425
|
+
sidebar();
|
|
426
|
+
panel();
|
|
427
|
+
notify();
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
export {
|
|
432
|
+
tui_default as default
|
|
433
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
Requires Bun >= 1.4.2.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
bun ci
|
|
7
|
+
make test
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`make test` runs typecheck, tests, and formatting checks.
|
|
11
|
+
`make upgrade-dependencies` updates pinned dependencies and the lockfile.
|
|
12
|
+
|
|
13
|
+
## Package entry points
|
|
14
|
+
|
|
15
|
+
The package ships TypeScript sources for OpenCode's Bun runtime and precompiled Solid ESM for the TUI. Run `bun run build` before loading the local package; packing builds the TUI automatically.
|
|
16
|
+
|
|
17
|
+
- `opencode-session-todo`: tools and RPC handlers.
|
|
18
|
+
- `opencode-session-todo/tui`: sidebar and full panel.
|
|
19
|
+
- `opencode-session-todo/rpc`: shared RPC definitions.
|
|
20
|
+
|
|
21
|
+
## Publishing
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm login
|
|
25
|
+
make publish
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`make publish` runs the checks and a package dry-run before `npm publish --access public`.
|