opencode-session-todo 0.1.0 → 0.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/dist/tui.js +433 -0
- package/docs/development.md +1 -1
- package/index.ts +4 -2
- package/package.json +5 -2
- package/src/diff.ts +4 -0
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
|
+
};
|
package/docs/development.md
CHANGED
|
@@ -12,7 +12,7 @@ make test
|
|
|
12
12
|
|
|
13
13
|
## Package entry points
|
|
14
14
|
|
|
15
|
-
The package ships TypeScript sources for OpenCode's Bun runtime
|
|
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
16
|
|
|
17
17
|
- `opencode-session-todo`: tools and RPC handlers.
|
|
18
18
|
- `opencode-session-todo/tui`: sidebar and full panel.
|
package/index.ts
CHANGED
|
@@ -164,9 +164,11 @@ export const register = (ctx: TodoPluginContext): Effect.Effect<void, never, Sco
|
|
|
164
164
|
Effect.logWarning("Todo saved, but notification failed", error),
|
|
165
165
|
),
|
|
166
166
|
)
|
|
167
|
+
const snapshot: Snapshot = { path: saved.path, todos: saved.todos }
|
|
168
|
+
const message: string = diff === "" ? "Todo list unchanged." : `Todo list updated:\n${diff}`
|
|
167
169
|
return {
|
|
168
|
-
output:
|
|
169
|
-
content:
|
|
170
|
+
output: snapshot,
|
|
171
|
+
content: `${message}\n\n${JSON.stringify(snapshot)}`,
|
|
170
172
|
metadata: { todos: saved.todos, path: saved.path },
|
|
171
173
|
}
|
|
172
174
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-session-todo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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": {
|
|
@@ -14,12 +14,13 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"exports": {
|
|
16
16
|
".": "./index.ts",
|
|
17
|
-
"./tui": "./tui.
|
|
17
|
+
"./tui": "./dist/tui.js",
|
|
18
18
|
"./rpc": "./rpc.ts"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"index.ts",
|
|
22
22
|
"tui.tsx",
|
|
23
|
+
"dist",
|
|
23
24
|
"rpc.ts",
|
|
24
25
|
"src",
|
|
25
26
|
"USAGE.md",
|
|
@@ -28,10 +29,12 @@
|
|
|
28
29
|
"docs/development.md"
|
|
29
30
|
],
|
|
30
31
|
"scripts": {
|
|
32
|
+
"build": "bun run build.ts",
|
|
31
33
|
"check": "tsc --noEmit",
|
|
32
34
|
"test": "bun test",
|
|
33
35
|
"format": "dprint fmt",
|
|
34
36
|
"format:check": "dprint check",
|
|
37
|
+
"prepack": "bun run build",
|
|
35
38
|
"prepublishOnly": "bun run check && bun run test && bun run format:check"
|
|
36
39
|
},
|
|
37
40
|
"dependencies": {
|
package/src/diff.ts
CHANGED
|
@@ -15,6 +15,10 @@ const advance = (cursor: Iterator<string>): string | undefined => {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const diffLines = (previous: readonly string[], next: readonly string[]): readonly string[] => {
|
|
18
|
+
if (
|
|
19
|
+
previous.length === next.length
|
|
20
|
+
&& previous.every((line: string, index: number): boolean => line === next[index])
|
|
21
|
+
) return []
|
|
18
22
|
if (
|
|
19
23
|
previous.length === 0 || next.length === 0
|
|
20
24
|
|| previous.length * next.length > MAX_CELLS
|