opencode-backlog 0.1.5 → 0.1.6
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 +12 -0
- package/dist/tui.js +256 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ A persistent project backlog for OpenCode V2 agents and the TUI.
|
|
|
4
4
|
|
|
5
5
|
`opencode-backlog` gives the agent tools to manage project tasks. It also adds an interactive backlog to the OpenCode sidebar and command palette.
|
|
6
6
|
|
|
7
|
+
Published package: [opencode-backlog on npm](https://www.npmjs.com/package/opencode-backlog).
|
|
8
|
+
|
|
7
9
|

|
|
8
10
|
|
|
9
11
|
## What It Does
|
|
@@ -129,6 +131,16 @@ Open the command palette and select `Browse backlog` to inspect tasks. Select a
|
|
|
129
131
|
|
|
130
132
|
Run `/backlog` or `/tasks` to open the same browser directly.
|
|
131
133
|
|
|
134
|
+
The backlog browser provides these shortcuts:
|
|
135
|
+
|
|
136
|
+
- `Enter` opens the selected task details.
|
|
137
|
+
- `n` creates a task.
|
|
138
|
+
- `c` changes the selected task state.
|
|
139
|
+
- `e` edits the selected task.
|
|
140
|
+
- `d` deletes the selected task after confirmation.
|
|
141
|
+
|
|
142
|
+
Click a sidebar task to open its details. The detail dialog provides `c`, `e`, and `d` for the same actions.
|
|
143
|
+
|
|
132
144
|
## Agent Tools
|
|
133
145
|
|
|
134
146
|
| Tool | Purpose |
|
package/dist/tui.js
CHANGED
|
@@ -10,7 +10,7 @@ import { randomUUID } from "node:crypto";
|
|
|
10
10
|
import { watch } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { Plugin } from "@opencode-ai/plugin/tui";
|
|
13
|
-
import { createMemo, createSignal, For, onCleanup, Show } from "solid-js";
|
|
13
|
+
import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js";
|
|
14
14
|
import { BACKLOG_FILE, EMPTY_BACKLOG, STATUSES, moveItem } from "./backlog.js";
|
|
15
15
|
import { readBacklog, readBacklogSync, updateBacklog } from "./store.js";
|
|
16
16
|
function statusLabel(status) {
|
|
@@ -116,6 +116,7 @@ function BacklogView(props) {
|
|
|
116
116
|
_$setProp(_el$11, "flexDirection", "row");
|
|
117
117
|
_$setProp(_el$11, "gap", 1);
|
|
118
118
|
_$setProp(_el$11, "minWidth", 0);
|
|
119
|
+
_$setProp(_el$11, "onMouseUp", () => void showTaskDetails(props.context, item));
|
|
119
120
|
_$setProp(_el$12, "flexShrink", 0);
|
|
120
121
|
_$insert(_el$12, status === "done" ? "✓" : status === "doing" ? "●" : "○");
|
|
121
122
|
_$setProp(_el$13, "wrapMode", "none");
|
|
@@ -149,6 +150,140 @@ function BacklogView(props) {
|
|
|
149
150
|
function taskDetails(item) {
|
|
150
151
|
return [`Status: ${statusLabel(item.status)}`, `ID: ${item.id}`, "", item.notes ?? "No notes"].join("\n");
|
|
151
152
|
}
|
|
153
|
+
function TaskAction(props) {
|
|
154
|
+
return (() => {
|
|
155
|
+
var _el$14 = _$createElement("text"),
|
|
156
|
+
_el$15 = _$createElement("b"),
|
|
157
|
+
_el$16 = _$createTextNode(` `);
|
|
158
|
+
_$insertNode(_el$14, _el$15);
|
|
159
|
+
_$insertNode(_el$14, _el$16);
|
|
160
|
+
_$insert(_el$15, () => props.shortcut);
|
|
161
|
+
_$insert(_el$14, () => props.label, null);
|
|
162
|
+
_$effect(_p$ => {
|
|
163
|
+
var _v$3 = props.danger ? props.context.theme.text.feedback.error.default : props.context.theme.text.subdued,
|
|
164
|
+
_v$4 = props.run;
|
|
165
|
+
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$14, "fg", _v$3, _p$.e));
|
|
166
|
+
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$14, "onMouseUp", _v$4, _p$.t));
|
|
167
|
+
return _p$;
|
|
168
|
+
}, {
|
|
169
|
+
e: undefined,
|
|
170
|
+
t: undefined
|
|
171
|
+
});
|
|
172
|
+
return _el$14;
|
|
173
|
+
})();
|
|
174
|
+
}
|
|
175
|
+
function TaskDetailsDialog(props) {
|
|
176
|
+
const run = operation => {
|
|
177
|
+
props.context.ui.dialog.clear();
|
|
178
|
+
void operation().catch(cause => props.context.ui.toast.show({
|
|
179
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
180
|
+
variant: "error"
|
|
181
|
+
}));
|
|
182
|
+
};
|
|
183
|
+
const changeStatus = () => run(() => changeTaskStatus(props.context, props.item));
|
|
184
|
+
const edit = () => run(() => editBacklogItem(props.context, props.item));
|
|
185
|
+
const remove = () => run(() => removeBacklogItem(props.context, props.item));
|
|
186
|
+
onMount(() => props.context.ui.dialog.set({
|
|
187
|
+
size: "medium"
|
|
188
|
+
}));
|
|
189
|
+
props.context.keymap.layer(() => ({
|
|
190
|
+
mode: "modal",
|
|
191
|
+
priority: 100,
|
|
192
|
+
commands: [{
|
|
193
|
+
bind: "c",
|
|
194
|
+
title: "Change backlog task status",
|
|
195
|
+
group: "Backlog",
|
|
196
|
+
run: changeStatus
|
|
197
|
+
}, {
|
|
198
|
+
bind: "e",
|
|
199
|
+
title: "Edit backlog task",
|
|
200
|
+
group: "Backlog",
|
|
201
|
+
run: edit
|
|
202
|
+
}, {
|
|
203
|
+
bind: "d",
|
|
204
|
+
title: "Delete backlog task",
|
|
205
|
+
group: "Backlog",
|
|
206
|
+
run: remove
|
|
207
|
+
}]
|
|
208
|
+
}));
|
|
209
|
+
return (() => {
|
|
210
|
+
var _el$17 = _$createElement("box"),
|
|
211
|
+
_el$18 = _$createElement("box"),
|
|
212
|
+
_el$19 = _$createElement("text"),
|
|
213
|
+
_el$20 = _$createElement("b"),
|
|
214
|
+
_el$21 = _$createElement("text"),
|
|
215
|
+
_el$23 = _$createElement("text"),
|
|
216
|
+
_el$24 = _$createElement("box");
|
|
217
|
+
_$insertNode(_el$17, _el$18);
|
|
218
|
+
_$insertNode(_el$17, _el$23);
|
|
219
|
+
_$insertNode(_el$17, _el$24);
|
|
220
|
+
_$setProp(_el$17, "paddingLeft", 2);
|
|
221
|
+
_$setProp(_el$17, "paddingRight", 2);
|
|
222
|
+
_$setProp(_el$17, "gap", 1);
|
|
223
|
+
_$insertNode(_el$18, _el$19);
|
|
224
|
+
_$insertNode(_el$18, _el$21);
|
|
225
|
+
_$setProp(_el$18, "flexDirection", "row");
|
|
226
|
+
_$setProp(_el$18, "justifyContent", "space-between");
|
|
227
|
+
_$setProp(_el$18, "gap", 2);
|
|
228
|
+
_$insertNode(_el$19, _el$20);
|
|
229
|
+
_$setProp(_el$19, "wrapMode", "word");
|
|
230
|
+
_$setProp(_el$19, "flexGrow", 1);
|
|
231
|
+
_$insert(_el$20, () => props.item.title);
|
|
232
|
+
_$insertNode(_el$21, _$createTextNode(`esc`));
|
|
233
|
+
_$setProp(_el$21, "onMouseUp", () => props.context.ui.dialog.clear());
|
|
234
|
+
_$setProp(_el$23, "wrapMode", "word");
|
|
235
|
+
_$insert(_el$23, () => taskDetails(props.item));
|
|
236
|
+
_$setProp(_el$24, "flexDirection", "row");
|
|
237
|
+
_$setProp(_el$24, "justifyContent", "flex-end");
|
|
238
|
+
_$setProp(_el$24, "gap", 2);
|
|
239
|
+
_$setProp(_el$24, "paddingBottom", 1);
|
|
240
|
+
_$insert(_el$24, _$createComponent(TaskAction, {
|
|
241
|
+
get context() {
|
|
242
|
+
return props.context;
|
|
243
|
+
},
|
|
244
|
+
shortcut: "c",
|
|
245
|
+
label: "status",
|
|
246
|
+
run: changeStatus
|
|
247
|
+
}), null);
|
|
248
|
+
_$insert(_el$24, _$createComponent(TaskAction, {
|
|
249
|
+
get context() {
|
|
250
|
+
return props.context;
|
|
251
|
+
},
|
|
252
|
+
shortcut: "e",
|
|
253
|
+
label: "edit",
|
|
254
|
+
run: edit
|
|
255
|
+
}), null);
|
|
256
|
+
_$insert(_el$24, _$createComponent(TaskAction, {
|
|
257
|
+
get context() {
|
|
258
|
+
return props.context;
|
|
259
|
+
},
|
|
260
|
+
shortcut: "d",
|
|
261
|
+
label: "delete",
|
|
262
|
+
danger: true,
|
|
263
|
+
run: remove
|
|
264
|
+
}), null);
|
|
265
|
+
_$effect(_p$ => {
|
|
266
|
+
var _v$5 = props.context.theme.text.default,
|
|
267
|
+
_v$6 = props.context.theme.text.subdued,
|
|
268
|
+
_v$7 = props.context.theme.text.subdued;
|
|
269
|
+
_v$5 !== _p$.e && (_p$.e = _$setProp(_el$19, "fg", _v$5, _p$.e));
|
|
270
|
+
_v$6 !== _p$.t && (_p$.t = _$setProp(_el$21, "fg", _v$6, _p$.t));
|
|
271
|
+
_v$7 !== _p$.a && (_p$.a = _$setProp(_el$23, "fg", _v$7, _p$.a));
|
|
272
|
+
return _p$;
|
|
273
|
+
}, {
|
|
274
|
+
e: undefined,
|
|
275
|
+
t: undefined,
|
|
276
|
+
a: undefined
|
|
277
|
+
});
|
|
278
|
+
return _el$17;
|
|
279
|
+
})();
|
|
280
|
+
}
|
|
281
|
+
function showTaskDetails(context, item) {
|
|
282
|
+
context.ui.dialog.show(() => _$createComponent(TaskDetailsDialog, {
|
|
283
|
+
context: context,
|
|
284
|
+
item: item
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
152
287
|
async function browseBacklog(context) {
|
|
153
288
|
return browseBacklogWithState(context, () => {});
|
|
154
289
|
}
|
|
@@ -193,6 +328,89 @@ async function addBacklogItem(context) {
|
|
|
193
328
|
variant: "success"
|
|
194
329
|
});
|
|
195
330
|
}
|
|
331
|
+
async function changeTaskStatus(context, item) {
|
|
332
|
+
const status = await context.ui.dialog.select({
|
|
333
|
+
title: `Change status: ${item.title}`,
|
|
334
|
+
current: item.status,
|
|
335
|
+
options: STATUSES.map(candidate => ({
|
|
336
|
+
title: statusLabel(candidate),
|
|
337
|
+
value: candidate,
|
|
338
|
+
...(candidate === item.status ? {
|
|
339
|
+
description: "Current status"
|
|
340
|
+
} : {})
|
|
341
|
+
}))
|
|
342
|
+
});
|
|
343
|
+
if (!status || status === item.status) return;
|
|
344
|
+
const {
|
|
345
|
+
path
|
|
346
|
+
} = backlogLocation(context);
|
|
347
|
+
await updateBacklog(path, current => ({
|
|
348
|
+
version: 1,
|
|
349
|
+
items: moveItem(current.items, item.id, status, undefined)
|
|
350
|
+
}));
|
|
351
|
+
context.ui.toast.show({
|
|
352
|
+
message: `Moved "${item.title}" to ${statusLabel(status)}.`,
|
|
353
|
+
variant: "success"
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
async function editBacklogItem(context, item) {
|
|
357
|
+
const title = await context.ui.dialog.prompt({
|
|
358
|
+
title: "Edit backlog task",
|
|
359
|
+
placeholder: "Task title",
|
|
360
|
+
value: item.title
|
|
361
|
+
});
|
|
362
|
+
if (!title?.trim()) return;
|
|
363
|
+
const notes = await context.ui.dialog.prompt({
|
|
364
|
+
title: title.trim(),
|
|
365
|
+
description: "Optional notes",
|
|
366
|
+
placeholder: "Leave empty for no notes",
|
|
367
|
+
value: item.notes ?? ""
|
|
368
|
+
});
|
|
369
|
+
if (notes === undefined) return;
|
|
370
|
+
const {
|
|
371
|
+
path
|
|
372
|
+
} = backlogLocation(context);
|
|
373
|
+
await updateBacklog(path, current => ({
|
|
374
|
+
version: 1,
|
|
375
|
+
items: current.items.map(candidate => {
|
|
376
|
+
if (candidate.id !== item.id) return candidate;
|
|
377
|
+
return {
|
|
378
|
+
id: candidate.id,
|
|
379
|
+
title: title.trim(),
|
|
380
|
+
status: candidate.status,
|
|
381
|
+
...(notes.trim() ? {
|
|
382
|
+
notes: notes.trim()
|
|
383
|
+
} : {})
|
|
384
|
+
};
|
|
385
|
+
})
|
|
386
|
+
}));
|
|
387
|
+
context.ui.toast.show({
|
|
388
|
+
message: `Updated "${title.trim()}".`,
|
|
389
|
+
variant: "success"
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
async function removeBacklogItem(context, item) {
|
|
393
|
+
const confirmed = await context.ui.dialog.confirm({
|
|
394
|
+
title: "Delete backlog task",
|
|
395
|
+
message: item.title,
|
|
396
|
+
label: {
|
|
397
|
+
confirm: "Delete",
|
|
398
|
+
cancel: "Cancel"
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
if (!confirmed) return;
|
|
402
|
+
const {
|
|
403
|
+
path
|
|
404
|
+
} = backlogLocation(context);
|
|
405
|
+
await updateBacklog(path, current => ({
|
|
406
|
+
version: 1,
|
|
407
|
+
items: current.items.filter(candidate => candidate.id !== item.id)
|
|
408
|
+
}));
|
|
409
|
+
context.ui.toast.show({
|
|
410
|
+
message: `Deleted "${item.title}".`,
|
|
411
|
+
variant: "success"
|
|
412
|
+
});
|
|
413
|
+
}
|
|
196
414
|
async function moveBacklogItem(context) {
|
|
197
415
|
const {
|
|
198
416
|
path
|
|
@@ -220,28 +438,9 @@ async function moveBacklogItem(context) {
|
|
|
220
438
|
if (!id) return;
|
|
221
439
|
const item = backlog.items.find(candidate => candidate.id === id);
|
|
222
440
|
if (!item) return;
|
|
223
|
-
|
|
224
|
-
title: `Change status: ${item.title}`,
|
|
225
|
-
current: item.status,
|
|
226
|
-
options: STATUSES.map(candidate => ({
|
|
227
|
-
title: statusLabel(candidate),
|
|
228
|
-
value: candidate,
|
|
229
|
-
...(candidate === item.status ? {
|
|
230
|
-
description: "Current status"
|
|
231
|
-
} : {})
|
|
232
|
-
}))
|
|
233
|
-
});
|
|
234
|
-
if (!status || status === item.status) return;
|
|
235
|
-
await updateBacklog(path, current => ({
|
|
236
|
-
version: 1,
|
|
237
|
-
items: moveItem(current.items, item.id, status, undefined)
|
|
238
|
-
}));
|
|
239
|
-
context.ui.toast.show({
|
|
240
|
-
message: `Moved "${item.title}" to ${statusLabel(status)}.`,
|
|
241
|
-
variant: "success"
|
|
242
|
-
});
|
|
441
|
+
await changeTaskStatus(context, item);
|
|
243
442
|
}
|
|
244
|
-
async function browseBacklogWithState(context, setOpen) {
|
|
443
|
+
async function browseBacklogWithState(context, setOpen, action = () => "details") {
|
|
245
444
|
const {
|
|
246
445
|
path
|
|
247
446
|
} = backlogLocation(context);
|
|
@@ -255,7 +454,7 @@ async function browseBacklogWithState(context, setOpen) {
|
|
|
255
454
|
}
|
|
256
455
|
setOpen(true);
|
|
257
456
|
const id = await context.ui.dialog.select({
|
|
258
|
-
title: "Backlog
|
|
457
|
+
title: "Backlog",
|
|
259
458
|
placeholder: "Select a task",
|
|
260
459
|
options: backlog.items.map(item => ({
|
|
261
460
|
title: item.title,
|
|
@@ -269,13 +468,15 @@ async function browseBacklogWithState(context, setOpen) {
|
|
|
269
468
|
if (!id) return;
|
|
270
469
|
const item = backlog.items.find(candidate => candidate.id === id);
|
|
271
470
|
if (!item) return;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
471
|
+
const selectedAction = action();
|
|
472
|
+
if (selectedAction === "status") return changeTaskStatus(context, item);
|
|
473
|
+
if (selectedAction === "edit") return editBacklogItem(context, item);
|
|
474
|
+
if (selectedAction === "delete") return removeBacklogItem(context, item);
|
|
475
|
+
showTaskDetails(context, item);
|
|
276
476
|
}
|
|
277
477
|
function Commands(props) {
|
|
278
478
|
const [browseOpen, setBrowseOpen] = createSignal(false);
|
|
479
|
+
const [browseAction, setBrowseAction] = createSignal("details");
|
|
279
480
|
const run = async operation => {
|
|
280
481
|
try {
|
|
281
482
|
await operation();
|
|
@@ -298,7 +499,10 @@ function Commands(props) {
|
|
|
298
499
|
name: "backlog",
|
|
299
500
|
aliases: ["tasks"]
|
|
300
501
|
},
|
|
301
|
-
run: () =>
|
|
502
|
+
run: () => {
|
|
503
|
+
setBrowseAction("details");
|
|
504
|
+
return run(() => browseBacklogWithState(props.context, setBrowseOpen, browseAction));
|
|
505
|
+
}
|
|
302
506
|
}, {
|
|
303
507
|
id: "backlog.add",
|
|
304
508
|
title: "Add backlog task",
|
|
@@ -335,6 +539,30 @@ function Commands(props) {
|
|
|
335
539
|
setBrowseOpen(false);
|
|
336
540
|
return run(() => addBacklogItem(props.context));
|
|
337
541
|
}
|
|
542
|
+
}, {
|
|
543
|
+
bind: "c",
|
|
544
|
+
title: "Change selected task status",
|
|
545
|
+
group: "Backlog",
|
|
546
|
+
run() {
|
|
547
|
+
setBrowseAction("status");
|
|
548
|
+
props.context.keymap.dispatch("dialog.select.submit");
|
|
549
|
+
}
|
|
550
|
+
}, {
|
|
551
|
+
bind: "d",
|
|
552
|
+
title: "Delete selected task",
|
|
553
|
+
group: "Backlog",
|
|
554
|
+
run() {
|
|
555
|
+
setBrowseAction("delete");
|
|
556
|
+
props.context.keymap.dispatch("dialog.select.submit");
|
|
557
|
+
}
|
|
558
|
+
}, {
|
|
559
|
+
bind: "e",
|
|
560
|
+
title: "Edit selected task",
|
|
561
|
+
group: "Backlog",
|
|
562
|
+
run() {
|
|
563
|
+
setBrowseAction("edit");
|
|
564
|
+
props.context.keymap.dispatch("dialog.select.submit");
|
|
565
|
+
}
|
|
338
566
|
}]
|
|
339
567
|
}));
|
|
340
568
|
return null;
|