opencode-session-todo 0.2.1 → 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 -132
- package/index.ts +48 -11
- package/package.json +2 -4
- package/src/instructions.ts +27 -2
- package/src/store.ts +32 -18
- package/tui.tsx +125 -74
- package/USAGE.md +0 -85
- 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
|
});
|
|
@@ -215,127 +220,215 @@ var tui_default = Plugin.define({
|
|
|
215
220
|
return current.status === "error" ? current.message : undefined;
|
|
216
221
|
};
|
|
217
222
|
const visible = () => Boolean(props.full || snapshot()?.todos.length || error());
|
|
218
|
-
|
|
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, {
|
|
219
297
|
get when() {
|
|
220
|
-
return
|
|
298
|
+
return error();
|
|
221
299
|
},
|
|
222
300
|
get children() {
|
|
223
|
-
var _el$ = _$createElement("
|
|
224
|
-
_$insertNode(_el
|
|
225
|
-
_$
|
|
226
|
-
_$setProp(_el
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
_$
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
242
368
|
});
|
|
243
|
-
|
|
369
|
+
return _el$19;
|
|
370
|
+
})()
|
|
244
371
|
});
|
|
245
|
-
|
|
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");
|
|
384
|
+
});
|
|
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, {
|
|
246
396
|
get when() {
|
|
247
|
-
return
|
|
397
|
+
return props.full;
|
|
248
398
|
},
|
|
249
|
-
get
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
+
})();
|
|
265
416
|
},
|
|
266
417
|
get children() {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
return error();
|
|
280
|
-
},
|
|
281
|
-
get children() {
|
|
282
|
-
var _el$10 = _$createElement("text"), _el$11 = _$createTextNode(`Cannot read todos: `);
|
|
283
|
-
_$insertNode(_el$10, _el$11);
|
|
284
|
-
_$insert(_el$10, error, null);
|
|
285
|
-
_$effect((_$p) => _$setProp(_el$10, "fg", ctx.theme.text.feedback.error.base, _$p));
|
|
286
|
-
return _el$10;
|
|
287
|
-
}
|
|
288
|
-
}), _$createComponent(Show, {
|
|
289
|
-
get when() {
|
|
290
|
-
return _$memo(() => !!(props.full && view().status === "ready"))() && todos().length === 0;
|
|
291
|
-
},
|
|
292
|
-
get children() {
|
|
293
|
-
var _el$12 = _$createElement("text");
|
|
294
|
-
_$insertNode(_el$12, _$createTextNode(`No todos for this session.`));
|
|
295
|
-
_$effect((_$p) => _$setProp(_el$12, "fg", ctx.theme.text.muted, _$p));
|
|
296
|
-
return _el$12;
|
|
297
|
-
}
|
|
298
|
-
}), _$createComponent(For, {
|
|
299
|
-
get each() {
|
|
300
|
-
return todos();
|
|
301
|
-
},
|
|
302
|
-
children: (todo) => (() => {
|
|
303
|
-
var _el$17 = _$createElement("text");
|
|
304
|
-
_$insert(_el$17, () => formatTodo(todo));
|
|
305
|
-
_$effect((_$p) => _$setProp(_el$17, "fg", colorFor(todo.status, ctx.theme), _$p));
|
|
306
|
-
return _el$17;
|
|
307
|
-
})()
|
|
308
|
-
}), _$createComponent(Show, {
|
|
309
|
-
get when() {
|
|
310
|
-
return more();
|
|
311
|
-
},
|
|
312
|
-
get children() {
|
|
313
|
-
var _el$14 = _$createElement("text"), _el$15 = _$createTextNode(`Show all `), _el$16 = _$createTextNode(` todos \u2192`);
|
|
314
|
-
_$insertNode(_el$14, _el$15);
|
|
315
|
-
_$insertNode(_el$14, _el$16);
|
|
316
|
-
_$setProp(_el$14, "onMouseDown", (event) => {
|
|
317
|
-
event.stopPropagation();
|
|
318
|
-
ctx.ui.panel.open("session-todo.list");
|
|
319
|
-
});
|
|
320
|
-
_$insert(_el$14, more, _el$16);
|
|
321
|
-
_$effect((_$p) => _$setProp(_el$14, "fg", ctx.theme.text.feedback.info.base, _$p));
|
|
322
|
-
return _el$14;
|
|
323
|
-
}
|
|
324
|
-
})];
|
|
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;
|
|
325
430
|
}
|
|
326
|
-
}), null);
|
|
327
|
-
_$effect((_p$) => {
|
|
328
|
-
var _v$ = ctx.theme.text.base, _v$2 = {
|
|
329
|
-
fg: ctx.theme.text.muted
|
|
330
|
-
};
|
|
331
|
-
_v$ !== _p$.e && (_p$.e = _$setProp(_el$4, "fg", _v$, _p$.e));
|
|
332
|
-
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$7, "style", _v$2, _p$.t));
|
|
333
|
-
return _p$;
|
|
334
|
-
}, {
|
|
335
|
-
e: undefined,
|
|
336
|
-
t: undefined
|
|
337
431
|
});
|
|
338
|
-
return _el$;
|
|
339
432
|
}
|
|
340
433
|
});
|
|
341
434
|
};
|
|
@@ -357,28 +450,15 @@ var tui_default = Plugin.define({
|
|
|
357
450
|
run: props.close
|
|
358
451
|
}]
|
|
359
452
|
}));
|
|
360
|
-
return (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
event.stopPropagation();
|
|
370
|
-
props.close();
|
|
371
|
-
});
|
|
372
|
-
_$setProp(_el$21, "flexGrow", 1);
|
|
373
|
-
_$insert(_el$21, _$createComponent(List, {
|
|
374
|
-
get sessionID() {
|
|
375
|
-
return props.sessionID;
|
|
376
|
-
},
|
|
377
|
-
full: true
|
|
378
|
-
}));
|
|
379
|
-
_$effect((_$p) => _$setProp(_el$19, "fg", ctx.theme.text.base, _$p));
|
|
380
|
-
return _el$18;
|
|
381
|
-
})();
|
|
453
|
+
return _$createComponent(List, {
|
|
454
|
+
get sessionID() {
|
|
455
|
+
return props.sessionID;
|
|
456
|
+
},
|
|
457
|
+
full: true,
|
|
458
|
+
get onClose() {
|
|
459
|
+
return props.close;
|
|
460
|
+
}
|
|
461
|
+
});
|
|
382
462
|
};
|
|
383
463
|
const panel = ctx.ui.slot({
|
|
384
464
|
append: "session.panel",
|
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
|
|
|
@@ -29,5 +29,30 @@ The following rules apply only when a TODO list is needed or already tracks unfi
|
|
|
29
29
|
- Before your final response, you MUST reconcile the list with what was actually done and accurately retain unfinished work.
|
|
30
30
|
- You SHOULD keep task descriptions concise and actionable.`
|
|
31
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
|
+
|
|
32
57
|
export const TodoInstructions: string =
|
|
33
|
-
`${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)
|
|
@@ -151,64 +156,125 @@ export default Plugin.define({
|
|
|
151
156
|
}
|
|
152
157
|
const visible: Accessor<boolean> = (): boolean =>
|
|
153
158
|
Boolean(props.full || snapshot()?.todos.length || error())
|
|
154
|
-
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
×
|
|
180
200
|
</text>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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={
|
|
192
216
|
<For each={todos()}>
|
|
193
217
|
{(todo): JSX.Element => (
|
|
194
|
-
<text fg={colorFor(todo.status, ctx.theme)}>
|
|
195
|
-
{formatTodo(todo)}
|
|
196
|
-
</text>
|
|
218
|
+
<text fg={colorFor(todo.status, ctx.theme)}>{formatTodo(todo)}</text>
|
|
197
219
|
)}
|
|
198
220
|
</For>
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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>
|
|
212
278
|
</Show>
|
|
213
279
|
)
|
|
214
280
|
}
|
|
@@ -231,22 +297,7 @@ export default Plugin.define({
|
|
|
231
297
|
],
|
|
232
298
|
}),
|
|
233
299
|
)
|
|
234
|
-
return
|
|
235
|
-
<box flexDirection="column" flexGrow={1} padding={1}>
|
|
236
|
-
<text
|
|
237
|
-
fg={ctx.theme.text.base}
|
|
238
|
-
onMouseDown={(event: MouseEvent): void => {
|
|
239
|
-
event.stopPropagation()
|
|
240
|
-
props.close()
|
|
241
|
-
}}
|
|
242
|
-
>
|
|
243
|
-
Close × · Esc when focused · Ctrl+P to toggle
|
|
244
|
-
</text>
|
|
245
|
-
<scrollbox flexGrow={1}>
|
|
246
|
-
<List sessionID={props.sessionID} full />
|
|
247
|
-
</scrollbox>
|
|
248
|
-
</box>
|
|
249
|
-
)
|
|
300
|
+
return <List sessionID={props.sessionID} full onClose={props.close} />
|
|
250
301
|
}
|
|
251
302
|
const panel: () => void = ctx.ui.slot({
|
|
252
303
|
append: "session.panel",
|
package/USAGE.md
DELETED
|
@@ -1,85 +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
|
-
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
|
-
|
|
29
|
-
- `todoread({})` reads the current session's tasks and file path. Missing files return an empty list.
|
|
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.
|
|
31
|
-
|
|
32
|
-
```json
|
|
33
|
-
{
|
|
34
|
-
"previous": [],
|
|
35
|
-
"todos": [
|
|
36
|
-
{ "content": "Run the tests", "status": "in_progress", "priority": "high" }
|
|
37
|
-
]
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Statuses: `pending`, `in_progress`, `completed`, `cancelled`. Priorities: `high`, `medium`, `low`.
|
|
42
|
-
At most one task may be `in_progress`. Content must be one nonempty trimmed line,
|
|
43
|
-
cannot begin with a priority/date, and cannot contain `status:` or `pri:` tokens.
|
|
44
|
-
|
|
45
|
-
## TUI
|
|
46
|
-
|
|
47
|
-
- Click **TODO** or its `▼` / `▶` arrow to expand or collapse the sidebar list.
|
|
48
|
-
- Above `sidebarLimit` total tasks, the sidebar shows only unfinished tasks, capped at the limit,
|
|
49
|
-
with the active task first.
|
|
50
|
-
- **Show all …**, `/todos`, or **Ctrl+P → Toggle session todos** opens the full scrollable panel.
|
|
51
|
-
- Close with **Close ×**, **Esc** while focused, or the same command-palette action.
|
|
52
|
-
- Manual file edits appear within two seconds. Collapsing does not change the tasks.
|
|
53
|
-
|
|
54
|
-
## Storage
|
|
55
|
-
|
|
56
|
-
```text
|
|
57
|
-
<project/worktree>/.agents/sessions/<session-id>/todo.txt
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
```text
|
|
61
|
-
(A) Run the tests status:in_progress
|
|
62
|
-
(B) Review the changes
|
|
63
|
-
x Read the docs pri:C
|
|
64
|
-
x Superseded approach pri:B status:cancelled
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
`A` / `B` / `C` map to high / medium / low; other priority letters are unsupported.
|
|
68
|
-
Add `.agents/sessions/` to your project's ignore rules to keep these files out of version control.
|
|
69
|
-
|
|
70
|
-
## Permissions
|
|
71
|
-
|
|
72
|
-
Both tools use `todowrite`. Writes require agent/session rules resolving to `allow`; `ask` and
|
|
73
|
-
`deny` are rejected. Approval prompts, saved approvals, and experimental policies are unsupported.
|
|
74
|
-
Read-only agents should deny `todowrite`.
|
|
75
|
-
|
|
76
|
-
## Commands
|
|
77
|
-
|
|
78
|
-
From the repository root:
|
|
79
|
-
|
|
80
|
-
```sh
|
|
81
|
-
bun run check
|
|
82
|
-
bun test
|
|
83
|
-
bun run format # dprint fmt
|
|
84
|
-
bun run format:check # dprint check
|
|
85
|
-
```
|
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`.
|