opencode-session-todo 0.2.0 → 0.3.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 +1 -1
- package/dist/tui.js +212 -143
- package/index.ts +48 -11
- package/package.json +2 -4
- package/src/instructions.ts +33 -2
- package/src/store.ts +32 -18
- package/tui.tsx +125 -84
- package/USAGE.md +0 -83
- package/docs/development.md +0 -28
package/README.md
CHANGED
|
@@ -31,4 +31,4 @@ Reload OpenCode and reopen the TUI.
|
|
|
31
31
|
- **Keep tasks in view** in the sidebar; open `/todos` for the full list.
|
|
32
32
|
- **Own the file** at `.agents/sessions/<session-id>/todo.txt`.
|
|
33
33
|
|
|
34
|
-
[
|
|
34
|
+
[MIT](LICENSE)
|
package/dist/tui.js
CHANGED
|
@@ -115,6 +115,11 @@ var colorFor = (status, theme) => {
|
|
|
115
115
|
return theme.text.feedback.error.base;
|
|
116
116
|
}
|
|
117
117
|
};
|
|
118
|
+
var priorityBadge = {
|
|
119
|
+
high: "(A)",
|
|
120
|
+
medium: "(B)",
|
|
121
|
+
low: "(C)"
|
|
122
|
+
};
|
|
118
123
|
var Message = Schema4.Struct({
|
|
119
124
|
message: Schema4.String
|
|
120
125
|
});
|
|
@@ -137,16 +142,6 @@ var tui_default = Plugin.define({
|
|
|
137
142
|
open: true
|
|
138
143
|
}
|
|
139
144
|
});
|
|
140
|
-
const notify = api.events.on("updated", (event) => {
|
|
141
|
-
if (event.data.summary === "")
|
|
142
|
-
return;
|
|
143
|
-
ctx.ui.toast.show({
|
|
144
|
-
title: "TODO updated",
|
|
145
|
-
message: event.data.summary,
|
|
146
|
-
variant: "info",
|
|
147
|
-
sessionID: event.data.sessionID
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
145
|
const List = (props) => {
|
|
151
146
|
const [view, setView] = createSignal({
|
|
152
147
|
status: "loading"
|
|
@@ -225,127 +220,215 @@ var tui_default = Plugin.define({
|
|
|
225
220
|
return current.status === "error" ? current.message : undefined;
|
|
226
221
|
};
|
|
227
222
|
const visible = () => Boolean(props.full || snapshot()?.todos.length || error());
|
|
228
|
-
|
|
223
|
+
const Header = () => (() => {
|
|
224
|
+
var _el$ = _$createElement("box"), _el$3 = _$createElement("box"), _el$4 = _$createElement("text"), _el$5 = _$createElement("b"), _el$7 = _$createElement("span"), _el$8 = _$createTextNode(` (`), _el$0 = _$createTextNode(` remaining)`);
|
|
225
|
+
_$insertNode(_el$, _el$3);
|
|
226
|
+
_$setProp(_el$, "flexDirection", "row");
|
|
227
|
+
_$setProp(_el$, "gap", 1);
|
|
228
|
+
_$setProp(_el$, "flexShrink", 0);
|
|
229
|
+
_$setProp(_el$, "onMouseDown", (event) => {
|
|
230
|
+
if (props.full)
|
|
231
|
+
return;
|
|
232
|
+
event.stopPropagation();
|
|
233
|
+
updateSidebarView((draft) => {
|
|
234
|
+
draft.open = !draft.open;
|
|
235
|
+
}).catch((error) => {
|
|
236
|
+
ctx.ui.toast.show({
|
|
237
|
+
title: "Todo sidebar",
|
|
238
|
+
message: `Cannot save expanded state: ${messageFor(error)}`,
|
|
239
|
+
variant: "error"
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
_$insert(_el$, _$createComponent(Show, {
|
|
244
|
+
get when() {
|
|
245
|
+
return !props.full;
|
|
246
|
+
},
|
|
247
|
+
get children() {
|
|
248
|
+
var _el$2 = _$createElement("text");
|
|
249
|
+
_$insert(_el$2, () => sidebarView.open ? "\u25BC" : "\u25B6");
|
|
250
|
+
_$effect((_$p) => _$setProp(_el$2, "fg", ctx.theme.text.base, _$p));
|
|
251
|
+
return _el$2;
|
|
252
|
+
}
|
|
253
|
+
}), _el$3);
|
|
254
|
+
_$insertNode(_el$3, _el$4);
|
|
255
|
+
_$setProp(_el$3, "flexGrow", 1);
|
|
256
|
+
_$setProp(_el$3, "flexShrink", 1);
|
|
257
|
+
_$setProp(_el$3, "minWidth", 0);
|
|
258
|
+
_$insertNode(_el$4, _el$5);
|
|
259
|
+
_$insertNode(_el$4, _el$7);
|
|
260
|
+
_$insertNode(_el$5, _$createTextNode(`TODO`));
|
|
261
|
+
_$insertNode(_el$7, _el$8);
|
|
262
|
+
_$insertNode(_el$7, _el$0);
|
|
263
|
+
_$insert(_el$7, active, _el$0);
|
|
264
|
+
_$insert(_el$, _$createComponent(Show, {
|
|
265
|
+
get when() {
|
|
266
|
+
return _$memo(() => !!props.full)() ? props.onClose : undefined;
|
|
267
|
+
},
|
|
268
|
+
children: (onClose) => (() => {
|
|
269
|
+
var _el$1 = _$createElement("text");
|
|
270
|
+
_$insertNode(_el$1, _$createTextNode(`\xD7`));
|
|
271
|
+
_$setProp(_el$1, "flexShrink", 0);
|
|
272
|
+
_$setProp(_el$1, "selectable", false);
|
|
273
|
+
_$setProp(_el$1, "onMouseDown", (event) => {
|
|
274
|
+
event.stopPropagation();
|
|
275
|
+
onClose()();
|
|
276
|
+
});
|
|
277
|
+
_$effect((_$p) => _$setProp(_el$1, "fg", ctx.theme.text.muted, _$p));
|
|
278
|
+
return _el$1;
|
|
279
|
+
})()
|
|
280
|
+
}), null);
|
|
281
|
+
_$effect((_p$) => {
|
|
282
|
+
var _v$ = props.full ? 1 : 0, _v$2 = ctx.theme.text.base, _v$3 = {
|
|
283
|
+
fg: ctx.theme.text.muted
|
|
284
|
+
};
|
|
285
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "paddingBottom", _v$, _p$.e));
|
|
286
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$4, "fg", _v$2, _p$.t));
|
|
287
|
+
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$7, "style", _v$3, _p$.a));
|
|
288
|
+
return _p$;
|
|
289
|
+
}, {
|
|
290
|
+
e: undefined,
|
|
291
|
+
t: undefined,
|
|
292
|
+
a: undefined
|
|
293
|
+
});
|
|
294
|
+
return _el$;
|
|
295
|
+
})();
|
|
296
|
+
const Body = () => [_$createComponent(Show, {
|
|
229
297
|
get when() {
|
|
230
|
-
return
|
|
298
|
+
return error();
|
|
231
299
|
},
|
|
232
300
|
get children() {
|
|
233
|
-
var _el$ = _$createElement("
|
|
234
|
-
_$insertNode(_el
|
|
235
|
-
_$
|
|
236
|
-
_$setProp(_el
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
_$
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
301
|
+
var _el$11 = _$createElement("text"), _el$12 = _$createTextNode(`Cannot read todos: `);
|
|
302
|
+
_$insertNode(_el$11, _el$12);
|
|
303
|
+
_$insert(_el$11, error, null);
|
|
304
|
+
_$effect((_$p) => _$setProp(_el$11, "fg", ctx.theme.text.feedback.error.base, _$p));
|
|
305
|
+
return _el$11;
|
|
306
|
+
}
|
|
307
|
+
}), _$createComponent(Show, {
|
|
308
|
+
get when() {
|
|
309
|
+
return _$memo(() => !!(props.full && view().status === "ready"))() && todos().length === 0;
|
|
310
|
+
},
|
|
311
|
+
get children() {
|
|
312
|
+
var _el$13 = _$createElement("text");
|
|
313
|
+
_$insertNode(_el$13, _$createTextNode(`No todos for this session.`));
|
|
314
|
+
_$effect((_$p) => _$setProp(_el$13, "fg", ctx.theme.text.muted, _$p));
|
|
315
|
+
return _el$13;
|
|
316
|
+
}
|
|
317
|
+
}), _$createComponent(Show, {
|
|
318
|
+
get when() {
|
|
319
|
+
return props.full;
|
|
320
|
+
},
|
|
321
|
+
get fallback() {
|
|
322
|
+
return _$createComponent(For, {
|
|
323
|
+
get each() {
|
|
324
|
+
return todos();
|
|
325
|
+
},
|
|
326
|
+
children: (todo) => (() => {
|
|
327
|
+
var _el$18 = _$createElement("text");
|
|
328
|
+
_$insert(_el$18, () => formatTodo(todo));
|
|
329
|
+
_$effect((_$p) => _$setProp(_el$18, "fg", colorFor(todo.status, ctx.theme), _$p));
|
|
330
|
+
return _el$18;
|
|
331
|
+
})()
|
|
332
|
+
});
|
|
333
|
+
},
|
|
334
|
+
get children() {
|
|
335
|
+
return _$createComponent(For, {
|
|
336
|
+
get each() {
|
|
337
|
+
return todos();
|
|
338
|
+
},
|
|
339
|
+
children: (todo) => (() => {
|
|
340
|
+
var _el$19 = _$createElement("box"), _el$20 = _$createElement("text"), _el$21 = _$createElement("box"), _el$22 = _$createElement("text"), _el$23 = _$createElement("text");
|
|
341
|
+
_$insertNode(_el$19, _el$20);
|
|
342
|
+
_$insertNode(_el$19, _el$21);
|
|
343
|
+
_$insertNode(_el$19, _el$23);
|
|
344
|
+
_$setProp(_el$19, "flexDirection", "row");
|
|
345
|
+
_$setProp(_el$19, "gap", 1);
|
|
346
|
+
_$setProp(_el$20, "width", 3);
|
|
347
|
+
_$setProp(_el$20, "flexShrink", 0);
|
|
348
|
+
_$insert(_el$20, () => todoSymbols[todo.status]);
|
|
349
|
+
_$insertNode(_el$21, _el$22);
|
|
350
|
+
_$setProp(_el$21, "flexGrow", 1);
|
|
351
|
+
_$setProp(_el$21, "flexShrink", 1);
|
|
352
|
+
_$setProp(_el$21, "minWidth", 0);
|
|
353
|
+
_$setProp(_el$22, "wrapMode", "word");
|
|
354
|
+
_$insert(_el$22, () => todo.content);
|
|
355
|
+
_$setProp(_el$23, "width", 3);
|
|
356
|
+
_$setProp(_el$23, "flexShrink", 0);
|
|
357
|
+
_$insert(_el$23, () => priorityBadge[todo.priority]);
|
|
358
|
+
_$effect((_p$) => {
|
|
359
|
+
var _v$4 = colorFor(todo.status, ctx.theme), _v$5 = colorFor(todo.status, ctx.theme), _v$6 = ctx.theme.text.muted;
|
|
360
|
+
_v$4 !== _p$.e && (_p$.e = _$setProp(_el$20, "fg", _v$4, _p$.e));
|
|
361
|
+
_v$5 !== _p$.t && (_p$.t = _$setProp(_el$22, "fg", _v$5, _p$.t));
|
|
362
|
+
_v$6 !== _p$.a && (_p$.a = _$setProp(_el$23, "fg", _v$6, _p$.a));
|
|
363
|
+
return _p$;
|
|
364
|
+
}, {
|
|
365
|
+
e: undefined,
|
|
366
|
+
t: undefined,
|
|
367
|
+
a: undefined
|
|
252
368
|
});
|
|
253
|
-
|
|
369
|
+
return _el$19;
|
|
370
|
+
})()
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}), _$createComponent(Show, {
|
|
374
|
+
get when() {
|
|
375
|
+
return more();
|
|
376
|
+
},
|
|
377
|
+
get children() {
|
|
378
|
+
var _el$15 = _$createElement("text"), _el$16 = _$createTextNode(`Show all `), _el$17 = _$createTextNode(` todos \u2192`);
|
|
379
|
+
_$insertNode(_el$15, _el$16);
|
|
380
|
+
_$insertNode(_el$15, _el$17);
|
|
381
|
+
_$setProp(_el$15, "onMouseDown", (event) => {
|
|
382
|
+
event.stopPropagation();
|
|
383
|
+
ctx.ui.panel.open("session-todo.list");
|
|
254
384
|
});
|
|
255
|
-
_$insert(_el$
|
|
385
|
+
_$insert(_el$15, more, _el$17);
|
|
386
|
+
_$effect((_$p) => _$setProp(_el$15, "fg", ctx.theme.text.feedback.info.base, _$p));
|
|
387
|
+
return _el$15;
|
|
388
|
+
}
|
|
389
|
+
})];
|
|
390
|
+
return _$createComponent(Show, {
|
|
391
|
+
get when() {
|
|
392
|
+
return visible();
|
|
393
|
+
},
|
|
394
|
+
get children() {
|
|
395
|
+
return _$createComponent(Show, {
|
|
256
396
|
get when() {
|
|
257
|
-
return
|
|
397
|
+
return props.full;
|
|
258
398
|
},
|
|
259
|
-
get
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
399
|
+
get fallback() {
|
|
400
|
+
return (() => {
|
|
401
|
+
var _el$26 = _$createElement("box");
|
|
402
|
+
_$setProp(_el$26, "flexDirection", "column");
|
|
403
|
+
_$setProp(_el$26, "gap", 0);
|
|
404
|
+
_$setProp(_el$26, "paddingTop", 1);
|
|
405
|
+
_$insert(_el$26, _$createComponent(Header, {}), null);
|
|
406
|
+
_$insert(_el$26, _$createComponent(Show, {
|
|
407
|
+
get when() {
|
|
408
|
+
return sidebarView.open;
|
|
409
|
+
},
|
|
410
|
+
get children() {
|
|
411
|
+
return _$createComponent(Body, {});
|
|
412
|
+
}
|
|
413
|
+
}), null);
|
|
414
|
+
return _el$26;
|
|
415
|
+
})();
|
|
275
416
|
},
|
|
276
417
|
get children() {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return error();
|
|
290
|
-
},
|
|
291
|
-
get children() {
|
|
292
|
-
var _el$10 = _$createElement("text"), _el$11 = _$createTextNode(`Cannot read todos: `);
|
|
293
|
-
_$insertNode(_el$10, _el$11);
|
|
294
|
-
_$insert(_el$10, error, null);
|
|
295
|
-
_$effect((_$p) => _$setProp(_el$10, "fg", ctx.theme.text.feedback.error.base, _$p));
|
|
296
|
-
return _el$10;
|
|
297
|
-
}
|
|
298
|
-
}), _$createComponent(Show, {
|
|
299
|
-
get when() {
|
|
300
|
-
return _$memo(() => !!(props.full && view().status === "ready"))() && todos().length === 0;
|
|
301
|
-
},
|
|
302
|
-
get children() {
|
|
303
|
-
var _el$12 = _$createElement("text");
|
|
304
|
-
_$insertNode(_el$12, _$createTextNode(`No todos for this session.`));
|
|
305
|
-
_$effect((_$p) => _$setProp(_el$12, "fg", ctx.theme.text.muted, _$p));
|
|
306
|
-
return _el$12;
|
|
307
|
-
}
|
|
308
|
-
}), _$createComponent(For, {
|
|
309
|
-
get each() {
|
|
310
|
-
return todos();
|
|
311
|
-
},
|
|
312
|
-
children: (todo) => (() => {
|
|
313
|
-
var _el$17 = _$createElement("text");
|
|
314
|
-
_$insert(_el$17, () => formatTodo(todo));
|
|
315
|
-
_$effect((_$p) => _$setProp(_el$17, "fg", colorFor(todo.status, ctx.theme), _$p));
|
|
316
|
-
return _el$17;
|
|
317
|
-
})()
|
|
318
|
-
}), _$createComponent(Show, {
|
|
319
|
-
get when() {
|
|
320
|
-
return more();
|
|
321
|
-
},
|
|
322
|
-
get children() {
|
|
323
|
-
var _el$14 = _$createElement("text"), _el$15 = _$createTextNode(`Show all `), _el$16 = _$createTextNode(` todos \u2192`);
|
|
324
|
-
_$insertNode(_el$14, _el$15);
|
|
325
|
-
_$insertNode(_el$14, _el$16);
|
|
326
|
-
_$setProp(_el$14, "onMouseDown", (event) => {
|
|
327
|
-
event.stopPropagation();
|
|
328
|
-
ctx.ui.panel.open("session-todo.list");
|
|
329
|
-
});
|
|
330
|
-
_$insert(_el$14, more, _el$16);
|
|
331
|
-
_$effect((_$p) => _$setProp(_el$14, "fg", ctx.theme.text.feedback.info.base, _$p));
|
|
332
|
-
return _el$14;
|
|
333
|
-
}
|
|
334
|
-
})];
|
|
418
|
+
var _el$24 = _$createElement("box"), _el$25 = _$createElement("scrollbox");
|
|
419
|
+
_$insertNode(_el$24, _el$25);
|
|
420
|
+
_$setProp(_el$24, "flexDirection", "column");
|
|
421
|
+
_$setProp(_el$24, "flexGrow", 1);
|
|
422
|
+
_$setProp(_el$24, "minHeight", 0);
|
|
423
|
+
_$setProp(_el$24, "padding", 1);
|
|
424
|
+
_$insert(_el$24, _$createComponent(Header, {}), _el$25);
|
|
425
|
+
_$setProp(_el$25, "flexGrow", 1);
|
|
426
|
+
_$setProp(_el$25, "flexBasis", 0);
|
|
427
|
+
_$setProp(_el$25, "minHeight", 0);
|
|
428
|
+
_$insert(_el$25, _$createComponent(Body, {}));
|
|
429
|
+
return _el$24;
|
|
335
430
|
}
|
|
336
|
-
}), null);
|
|
337
|
-
_$effect((_p$) => {
|
|
338
|
-
var _v$ = ctx.theme.text.base, _v$2 = {
|
|
339
|
-
fg: ctx.theme.text.muted
|
|
340
|
-
};
|
|
341
|
-
_v$ !== _p$.e && (_p$.e = _$setProp(_el$4, "fg", _v$, _p$.e));
|
|
342
|
-
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$7, "style", _v$2, _p$.t));
|
|
343
|
-
return _p$;
|
|
344
|
-
}, {
|
|
345
|
-
e: undefined,
|
|
346
|
-
t: undefined
|
|
347
431
|
});
|
|
348
|
-
return _el$;
|
|
349
432
|
}
|
|
350
433
|
});
|
|
351
434
|
};
|
|
@@ -367,28 +450,15 @@ var tui_default = Plugin.define({
|
|
|
367
450
|
run: props.close
|
|
368
451
|
}]
|
|
369
452
|
}));
|
|
370
|
-
return (
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
event.stopPropagation();
|
|
380
|
-
props.close();
|
|
381
|
-
});
|
|
382
|
-
_$setProp(_el$21, "flexGrow", 1);
|
|
383
|
-
_$insert(_el$21, _$createComponent(List, {
|
|
384
|
-
get sessionID() {
|
|
385
|
-
return props.sessionID;
|
|
386
|
-
},
|
|
387
|
-
full: true
|
|
388
|
-
}));
|
|
389
|
-
_$effect((_$p) => _$setProp(_el$19, "fg", ctx.theme.text.base, _$p));
|
|
390
|
-
return _el$18;
|
|
391
|
-
})();
|
|
453
|
+
return _$createComponent(List, {
|
|
454
|
+
get sessionID() {
|
|
455
|
+
return props.sessionID;
|
|
456
|
+
},
|
|
457
|
+
full: true,
|
|
458
|
+
get onClose() {
|
|
459
|
+
return props.close;
|
|
460
|
+
}
|
|
461
|
+
});
|
|
392
462
|
};
|
|
393
463
|
const panel = ctx.ui.slot({
|
|
394
464
|
append: "session.panel",
|
|
@@ -423,7 +493,6 @@ var tui_default = Plugin.define({
|
|
|
423
493
|
return () => {
|
|
424
494
|
sidebar();
|
|
425
495
|
panel();
|
|
426
|
-
notify();
|
|
427
496
|
};
|
|
428
497
|
}
|
|
429
498
|
});
|
package/index.ts
CHANGED
|
@@ -13,7 +13,10 @@ import { diffTodos } from "./src/diff.ts"
|
|
|
13
13
|
import {
|
|
14
14
|
TodoInstructions,
|
|
15
15
|
TodoLifecycleInstructions,
|
|
16
|
+
TodoNamespaceDescription,
|
|
17
|
+
TodoReadAliasDescription,
|
|
16
18
|
TodoReadInstructions,
|
|
19
|
+
TodoWriteAliasDescription,
|
|
17
20
|
TodoWriteInstructions,
|
|
18
21
|
} from "./src/instructions.ts"
|
|
19
22
|
import { Snapshot, TodoError, Todos } from "./src/model.ts"
|
|
@@ -107,6 +110,16 @@ export const register = (ctx: TodoPluginContext): Effect.Effect<void, never, Sco
|
|
|
107
110
|
return session
|
|
108
111
|
})
|
|
109
112
|
|
|
113
|
+
const requireRoot = (session: Session.Info): Effect.Effect<Session.Info, Tool.Error> =>
|
|
114
|
+
session.parentID === undefined
|
|
115
|
+
? Effect.succeed(session)
|
|
116
|
+
: Effect.fail(
|
|
117
|
+
new Tool.Error({
|
|
118
|
+
message:
|
|
119
|
+
"TODO tools are only available in root sessions. Subagent sessions cannot read or update the session's TODO list.",
|
|
120
|
+
}),
|
|
121
|
+
)
|
|
122
|
+
|
|
110
123
|
const directoryFor = (session: Session.Info): Effect.Effect<SessionDirectory, TodoError> =>
|
|
111
124
|
Schema.decodeUnknownEffect(SessionDirectory)({
|
|
112
125
|
directory: session.location.directory,
|
|
@@ -135,6 +148,7 @@ export const register = (ctx: TodoPluginContext): Effect.Effect<void, never, Sco
|
|
|
135
148
|
)(function*(input, context): Effect.fn.Return<Tool.Result<typeof Snapshot>, Tool.Error> {
|
|
136
149
|
const session: Session.Info = yield* getSession(context.sessionID).pipe(
|
|
137
150
|
Effect.mapError(toolError),
|
|
151
|
+
Effect.flatMap(requireRoot),
|
|
138
152
|
)
|
|
139
153
|
const agent: Agent.Info = (yield* ctx.agent
|
|
140
154
|
.get({ agentID: context.agent })
|
|
@@ -186,19 +200,26 @@ export const register = (ctx: TodoPluginContext): Effect.Effect<void, never, Sco
|
|
|
186
200
|
const executeRead: Tool.Info<typeof ReadInput, typeof Snapshot>["execute"] = (
|
|
187
201
|
_input,
|
|
188
202
|
context,
|
|
189
|
-
): Effect.Effect<Tool.Result<typeof Snapshot>, Tool.Error> =>
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
203
|
+
): Effect.Effect<Tool.Result<typeof Snapshot>, Tool.Error> =>
|
|
204
|
+
Effect.gen(function*(): Effect.fn.Return<Tool.Result<typeof Snapshot>, Tool.Error> {
|
|
205
|
+
const session: Session.Info = yield* getSession(context.sessionID).pipe(
|
|
206
|
+
Effect.mapError(toolError),
|
|
207
|
+
Effect.flatMap(requireRoot),
|
|
208
|
+
)
|
|
209
|
+
return yield* directoryFor(session).pipe(
|
|
210
|
+
Effect.flatMap(store.read),
|
|
211
|
+
Effect.map(
|
|
212
|
+
(snapshot): Tool.Result<typeof Snapshot> => ({
|
|
213
|
+
output: snapshot,
|
|
214
|
+
content: JSON.stringify(snapshot),
|
|
215
|
+
}),
|
|
216
|
+
),
|
|
217
|
+
Effect.mapError(toolError),
|
|
218
|
+
)
|
|
219
|
+
})
|
|
200
220
|
|
|
201
221
|
yield* ctx.tool.transform((editor): void => {
|
|
222
|
+
editor.namespace({ name: "todo", description: TodoNamespaceDescription })
|
|
202
223
|
editor.add({
|
|
203
224
|
name: "todowrite",
|
|
204
225
|
description: `${TodoWriteInstructions}\n\n${TodoLifecycleInstructions}`,
|
|
@@ -215,6 +236,22 @@ export const register = (ctx: TodoPluginContext): Effect.Effect<void, never, Sco
|
|
|
215
236
|
options: { codemode: false, permission: "todowrite" },
|
|
216
237
|
execute: executeRead,
|
|
217
238
|
})
|
|
239
|
+
editor.add({
|
|
240
|
+
name: "write",
|
|
241
|
+
description: TodoWriteAliasDescription,
|
|
242
|
+
input: WriteInput,
|
|
243
|
+
output: Snapshot,
|
|
244
|
+
options: { namespace: "todo", codemode: true, pinned: true, permission: "todowrite" },
|
|
245
|
+
execute: executeWrite,
|
|
246
|
+
})
|
|
247
|
+
editor.add({
|
|
248
|
+
name: "read",
|
|
249
|
+
description: TodoReadAliasDescription,
|
|
250
|
+
input: ReadInput,
|
|
251
|
+
output: Snapshot,
|
|
252
|
+
options: { namespace: "todo", codemode: true, pinned: true, permission: "todowrite" },
|
|
253
|
+
execute: executeRead,
|
|
254
|
+
})
|
|
218
255
|
})
|
|
219
256
|
}).pipe(Effect.orDie)
|
|
220
257
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-session-todo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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": {
|
|
@@ -23,10 +23,8 @@
|
|
|
23
23
|
"dist",
|
|
24
24
|
"rpc.ts",
|
|
25
25
|
"src",
|
|
26
|
-
"USAGE.md",
|
|
27
26
|
"LICENSE",
|
|
28
|
-
"README.md"
|
|
29
|
-
"docs/development.md"
|
|
27
|
+
"README.md"
|
|
30
28
|
],
|
|
31
29
|
"scripts": {
|
|
32
30
|
"build": "bun run build.ts",
|
package/src/instructions.ts
CHANGED
|
@@ -4,7 +4,7 @@ Replace this session’s TODO list.
|
|
|
4
4
|
|
|
5
5
|
- You MUST pass the last known saved list as \`previous\`; use \`[]\` when known empty.
|
|
6
6
|
- You SHOULD reuse the returned saved list for subsequent writes.
|
|
7
|
-
- On conflict, you MUST
|
|
7
|
+
- On conflict, you MUST refresh the saved list and retry.`
|
|
8
8
|
|
|
9
9
|
export const TodoReadInstructions: string = `### \`todoread\`
|
|
10
10
|
|
|
@@ -14,6 +14,12 @@ Read this session’s saved TODO list and storage path.
|
|
|
14
14
|
|
|
15
15
|
export const TodoLifecycleInstructions: string = `### TODO lifecycle
|
|
16
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.
|
|
22
|
+
|
|
17
23
|
- You MUST keep this session's TODO list synchronized with the requested work and its actual progress.
|
|
18
24
|
- You MUST preserve existing tasks, order, statuses, and priorities unless actual progress or the user's instructions require a change.
|
|
19
25
|
- You MUST mark a task in_progress before starting it. You MUST keep at most one task in_progress.
|
|
@@ -23,5 +29,30 @@ export const TodoLifecycleInstructions: string = `### TODO lifecycle
|
|
|
23
29
|
- Before your final response, you MUST reconcile the list with what was actually done and accurately retain unfinished work.
|
|
24
30
|
- You SHOULD keep task descriptions concise and actionable.`
|
|
25
31
|
|
|
32
|
+
export const TodoCodeModeInstructions: string = `### Code Mode
|
|
33
|
+
|
|
34
|
+
- You SHOULD prefer Code Mode for TODO updates.
|
|
35
|
+
- When the current list is known, call \`tools.todo.write\` directly with that \`previous\` list; do not read first.
|
|
36
|
+
- Read with \`tools.todo.read()\` only when the saved list is unknown or a write reports a conflict.
|
|
37
|
+
- When you need both, perform them sequentially in one execute call: await the read, pass the returned \`todos\` as \`previous\`, await the write, and return only the final snapshot.
|
|
38
|
+
- You MUST NOT use \`Promise.all\` for dependent TODO operations.
|
|
39
|
+
- You MUST NOT batch lifecycle transitions in one call.
|
|
40
|
+
|
|
41
|
+
\`\`\`ts
|
|
42
|
+
const { todos } = await tools.todo.read()
|
|
43
|
+
return await tools.todo.write({
|
|
44
|
+
previous: todos,
|
|
45
|
+
todos: [...todos, { content: "Run the tests", status: "pending", priority: "high" }],
|
|
46
|
+
})
|
|
47
|
+
\`\`\``
|
|
48
|
+
|
|
49
|
+
export const TodoWriteAliasDescription: string = "Replace this session’s TODO list."
|
|
50
|
+
|
|
51
|
+
export const TodoReadAliasDescription: string =
|
|
52
|
+
"Read this session’s saved TODO list and storage path."
|
|
53
|
+
|
|
54
|
+
export const TodoNamespaceDescription: string =
|
|
55
|
+
"Session TODO list tools. Prefer one execute call per update: write with the last known list; read only when the saved list is unknown or a write reports a conflict."
|
|
56
|
+
|
|
26
57
|
export const TodoInstructions: string =
|
|
27
|
-
`${TodoWriteInstructions}\n\n${TodoReadInstructions}\n\n${TodoLifecycleInstructions}`
|
|
58
|
+
`${TodoWriteInstructions}\n\n${TodoReadInstructions}\n\n${TodoLifecycleInstructions}\n\n${TodoCodeModeInstructions}`
|
package/src/store.ts
CHANGED
|
@@ -65,21 +65,31 @@ const requireDirectory = async (directory: string): Promise<void> => {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
68
|
+
interface LoadedDocument extends Document {
|
|
69
|
+
readonly exists: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const load = Effect.fnUntraced(
|
|
73
|
+
function*(path: string): Effect.fn.Return<LoadedDocument, TodoError> {
|
|
74
|
+
const file: { readonly text: string; readonly exists: boolean } = yield* io(
|
|
75
|
+
`Cannot read ${path}`,
|
|
76
|
+
async (): Promise<{ readonly text: string; readonly exists: boolean }> => {
|
|
77
|
+
try {
|
|
78
|
+
const info: Stats = await lstat(path)
|
|
79
|
+
if (!info.isFile()) {
|
|
80
|
+
throw new Error("todo.txt must be a regular file")
|
|
81
|
+
}
|
|
82
|
+
return { text: await readFile(path, "utf8"), exists: true }
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (missing(error)) return { text: "", exists: false }
|
|
85
|
+
throw error
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
const document: Document = yield* parse(file.text)
|
|
90
|
+
return { ...document, exists: file.exists }
|
|
91
|
+
},
|
|
92
|
+
)
|
|
83
93
|
|
|
84
94
|
const checkDirectory = Effect.fnUntraced(function*(
|
|
85
95
|
session: SessionDirectory,
|
|
@@ -132,13 +142,16 @@ export const makeTodoStore: Effect.Effect<TodoStore> = Effect.gen(
|
|
|
132
142
|
Effect.gen(function*(): Effect.fn.Return<WriteResult, TodoError> {
|
|
133
143
|
const path: string = pathFor(session)
|
|
134
144
|
yield* ensureDirectory(session)
|
|
135
|
-
const previous:
|
|
145
|
+
const previous: LoadedDocument = yield* load(path)
|
|
136
146
|
if (expected !== undefined && !sameTodos(expected, previous.todos)) {
|
|
137
147
|
return yield* new TodoError({
|
|
138
148
|
message:
|
|
139
|
-
"The stored todo list does not match the `previous` list you sent. Use todoread, then retry todowrite with the refreshed `previous` list.",
|
|
149
|
+
"The stored todo list does not match the `previous` list you sent. Use todoread or tools.todo.read(), then retry todowrite with the refreshed `previous` list.",
|
|
140
150
|
})
|
|
141
151
|
}
|
|
152
|
+
if (previous.exists && sameTodos(todos, previous.todos)) {
|
|
153
|
+
return { path, todos, previous: previous.todos }
|
|
154
|
+
}
|
|
142
155
|
const temporary: string = `${path}.${randomUUID()}.tmp`
|
|
143
156
|
const today: string = DateTime.formatIsoDateUtc(yield* DateTime.now)
|
|
144
157
|
yield* io(`Cannot write ${path}`, async (): Promise<void> => {
|
|
@@ -149,8 +162,9 @@ export const makeTodoStore: Effect.Effect<TodoStore> = Effect.gen(
|
|
|
149
162
|
flag: "wx",
|
|
150
163
|
})
|
|
151
164
|
await rename(temporary, path)
|
|
152
|
-
}
|
|
165
|
+
} catch (error) {
|
|
153
166
|
await rm(temporary, { force: true })
|
|
167
|
+
throw error
|
|
154
168
|
}
|
|
155
169
|
})
|
|
156
170
|
return { path, todos, previous: previous.todos }
|
package/tui.tsx
CHANGED
|
@@ -17,17 +17,16 @@ 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
|
+
import { formatTodo, todoSymbols } from "./src/summary.ts"
|
|
21
21
|
|
|
22
22
|
type View =
|
|
23
23
|
| { readonly status: "loading" }
|
|
24
24
|
| { readonly status: "ready"; readonly snapshot: Snapshot }
|
|
25
25
|
| { readonly status: "error"; readonly message: string }
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
readonly sessionID: string
|
|
29
|
-
readonly full?:
|
|
30
|
-
}
|
|
27
|
+
type Props =
|
|
28
|
+
| { readonly sessionID: string; readonly full: true; readonly onClose: () => void }
|
|
29
|
+
| { readonly sessionID: string; readonly full?: false; readonly onClose?: never }
|
|
31
30
|
|
|
32
31
|
const colorFor = (
|
|
33
32
|
status: Todo["status"],
|
|
@@ -45,6 +44,12 @@ const colorFor = (
|
|
|
45
44
|
}
|
|
46
45
|
}
|
|
47
46
|
|
|
47
|
+
const priorityBadge: Readonly<Record<Todo["priority"], string>> = {
|
|
48
|
+
high: "(A)",
|
|
49
|
+
medium: "(B)",
|
|
50
|
+
low: "(C)",
|
|
51
|
+
}
|
|
52
|
+
|
|
48
53
|
const Message = Schema.Struct({ message: Schema.String })
|
|
49
54
|
const messageFor = (error: unknown): string =>
|
|
50
55
|
Schema.is(Message)(error) ? error.message : String(error)
|
|
@@ -65,15 +70,6 @@ export default Plugin.define({
|
|
|
65
70
|
const [sidebarView, updateSidebarView] = ctx.storage.store("sidebar", {
|
|
66
71
|
initial: { open: true },
|
|
67
72
|
})
|
|
68
|
-
const notify: () => void = api.events.on("updated", (event): void => {
|
|
69
|
-
if (event.data.summary === "") return
|
|
70
|
-
ctx.ui.toast.show({
|
|
71
|
-
title: "TODO updated",
|
|
72
|
-
message: event.data.summary,
|
|
73
|
-
variant: "info",
|
|
74
|
-
sessionID: event.data.sessionID,
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
73
|
const List = (props: Props): JSX.Element => {
|
|
78
74
|
const [view, setView] = createSignal<View>({ status: "loading" })
|
|
79
75
|
createEffect((): void => {
|
|
@@ -160,64 +156,125 @@ export default Plugin.define({
|
|
|
160
156
|
}
|
|
161
157
|
const visible: Accessor<boolean> = (): boolean =>
|
|
162
158
|
Boolean(props.full || snapshot()?.todos.length || error())
|
|
163
|
-
|
|
164
|
-
<
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
159
|
+
const Header = (): JSX.Element => (
|
|
160
|
+
<box
|
|
161
|
+
flexDirection="row"
|
|
162
|
+
gap={1}
|
|
163
|
+
flexShrink={0}
|
|
164
|
+
paddingBottom={props.full ? 1 : 0}
|
|
165
|
+
onMouseDown={(event: MouseEvent): void => {
|
|
166
|
+
if (props.full) return
|
|
167
|
+
event.stopPropagation()
|
|
168
|
+
void updateSidebarView((draft): void => {
|
|
169
|
+
draft.open = !draft.open
|
|
170
|
+
}).catch((error: unknown): void => {
|
|
171
|
+
ctx.ui.toast.show({
|
|
172
|
+
title: "Todo sidebar",
|
|
173
|
+
message: `Cannot save expanded state: ${messageFor(error)}`,
|
|
174
|
+
variant: "error",
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
}}
|
|
178
|
+
>
|
|
179
|
+
<Show when={!props.full}>
|
|
180
|
+
<text fg={ctx.theme.text.base}>{sidebarView.open ? "▼" : "▶"}</text>
|
|
181
|
+
</Show>
|
|
182
|
+
<box flexGrow={1} flexShrink={1} minWidth={0}>
|
|
183
|
+
<text fg={ctx.theme.text.base}>
|
|
184
|
+
<b>TODO</b>
|
|
185
|
+
<span style={{ fg: ctx.theme.text.muted }}>{" "}({active()} remaining)</span>
|
|
186
|
+
</text>
|
|
187
|
+
</box>
|
|
188
|
+
<Show when={props.full ? props.onClose : undefined}>
|
|
189
|
+
{(onClose: Accessor<() => void>): JSX.Element => (
|
|
190
|
+
<text
|
|
191
|
+
fg={ctx.theme.text.muted}
|
|
192
|
+
flexShrink={0}
|
|
193
|
+
selectable={false}
|
|
194
|
+
onMouseDown={(event: MouseEvent): void => {
|
|
195
|
+
event.stopPropagation()
|
|
196
|
+
onClose()()
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
×
|
|
189
200
|
</text>
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
)}
|
|
202
|
+
</Show>
|
|
203
|
+
</box>
|
|
204
|
+
)
|
|
205
|
+
const Body = (): JSX.Element => (
|
|
206
|
+
<>
|
|
207
|
+
<Show when={error()}>
|
|
208
|
+
<text fg={ctx.theme.text.feedback.error.base}>Cannot read todos: {error()}</text>
|
|
209
|
+
</Show>
|
|
210
|
+
<Show when={props.full && view().status === "ready" && todos().length === 0}>
|
|
211
|
+
<text fg={ctx.theme.text.muted}>No todos for this session.</text>
|
|
212
|
+
</Show>
|
|
213
|
+
<Show
|
|
214
|
+
when={props.full}
|
|
215
|
+
fallback={
|
|
201
216
|
<For each={todos()}>
|
|
202
217
|
{(todo): JSX.Element => (
|
|
203
|
-
<text fg={colorFor(todo.status, ctx.theme)}>
|
|
204
|
-
{formatTodo(todo)}
|
|
205
|
-
</text>
|
|
218
|
+
<text fg={colorFor(todo.status, ctx.theme)}>{formatTodo(todo)}</text>
|
|
206
219
|
)}
|
|
207
220
|
</For>
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
+
}
|
|
222
|
+
>
|
|
223
|
+
<For each={todos()}>
|
|
224
|
+
{(todo): JSX.Element => (
|
|
225
|
+
<box flexDirection="row" gap={1}>
|
|
226
|
+
<text
|
|
227
|
+
fg={colorFor(todo.status, ctx.theme)}
|
|
228
|
+
width={3}
|
|
229
|
+
flexShrink={0}
|
|
230
|
+
>
|
|
231
|
+
{todoSymbols[todo.status]}
|
|
232
|
+
</text>
|
|
233
|
+
<box flexGrow={1} flexShrink={1} minWidth={0}>
|
|
234
|
+
<text fg={colorFor(todo.status, ctx.theme)} wrapMode="word">
|
|
235
|
+
{todo.content}
|
|
236
|
+
</text>
|
|
237
|
+
</box>
|
|
238
|
+
<text fg={ctx.theme.text.muted} width={3} flexShrink={0}>
|
|
239
|
+
{priorityBadge[todo.priority]}
|
|
240
|
+
</text>
|
|
241
|
+
</box>
|
|
242
|
+
)}
|
|
243
|
+
</For>
|
|
244
|
+
</Show>
|
|
245
|
+
<Show when={more()}>
|
|
246
|
+
<text
|
|
247
|
+
fg={ctx.theme.text.feedback.info.base}
|
|
248
|
+
onMouseDown={(event: MouseEvent): void => {
|
|
249
|
+
event.stopPropagation()
|
|
250
|
+
ctx.ui.panel.open("session-todo.list")
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
Show all {more()} todos →
|
|
254
|
+
</text>
|
|
255
|
+
</Show>
|
|
256
|
+
</>
|
|
257
|
+
)
|
|
258
|
+
return (
|
|
259
|
+
<Show when={visible()}>
|
|
260
|
+
<Show
|
|
261
|
+
when={props.full}
|
|
262
|
+
fallback={
|
|
263
|
+
<box flexDirection="column" gap={0} paddingTop={1}>
|
|
264
|
+
<Header />
|
|
265
|
+
<Show when={sidebarView.open}>
|
|
266
|
+
<Body />
|
|
267
|
+
</Show>
|
|
268
|
+
</box>
|
|
269
|
+
}
|
|
270
|
+
>
|
|
271
|
+
<box flexDirection="column" flexGrow={1} minHeight={0} padding={1}>
|
|
272
|
+
<Header />
|
|
273
|
+
<scrollbox flexGrow={1} flexBasis={0} minHeight={0}>
|
|
274
|
+
<Body />
|
|
275
|
+
</scrollbox>
|
|
276
|
+
</box>
|
|
277
|
+
</Show>
|
|
221
278
|
</Show>
|
|
222
279
|
)
|
|
223
280
|
}
|
|
@@ -240,22 +297,7 @@ export default Plugin.define({
|
|
|
240
297
|
],
|
|
241
298
|
}),
|
|
242
299
|
)
|
|
243
|
-
return
|
|
244
|
-
<box flexDirection="column" flexGrow={1} padding={1}>
|
|
245
|
-
<text
|
|
246
|
-
fg={ctx.theme.text.base}
|
|
247
|
-
onMouseDown={(event: MouseEvent): void => {
|
|
248
|
-
event.stopPropagation()
|
|
249
|
-
props.close()
|
|
250
|
-
}}
|
|
251
|
-
>
|
|
252
|
-
Close × · Esc when focused · Ctrl+P to toggle
|
|
253
|
-
</text>
|
|
254
|
-
<scrollbox flexGrow={1}>
|
|
255
|
-
<List sessionID={props.sessionID} full />
|
|
256
|
-
</scrollbox>
|
|
257
|
-
</box>
|
|
258
|
-
)
|
|
300
|
+
return <List sessionID={props.sessionID} full onClose={props.close} />
|
|
259
301
|
}
|
|
260
302
|
const panel: () => void = ctx.ui.slot({
|
|
261
303
|
append: "session.panel",
|
|
@@ -287,7 +329,6 @@ export default Plugin.define({
|
|
|
287
329
|
return (): void => {
|
|
288
330
|
sidebar()
|
|
289
331
|
panel()
|
|
290
|
-
notify()
|
|
291
332
|
}
|
|
292
333
|
},
|
|
293
334
|
})
|
package/USAGE.md
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
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
|
-
- Update toasts show only added or changed tasks, one line each, with the new status and priority.
|
|
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/docs/development.md
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
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`.
|