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.
Files changed (3) hide show
  1. package/README.md +12 -0
  2. package/dist/tui.js +256 -28
  3. 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
  ![Backlog sidebar in OpenCode](docs/images/opencode-backlog.jpg)
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
- const status = await context.ui.dialog.select({
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 (n: new task)",
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
- await context.ui.dialog.alert({
273
- title: item.title,
274
- message: taskDetails(item)
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: () => run(() => browseBacklogWithState(props.context, setBrowseOpen))
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-backlog",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Project backlog tools and sidebar for OpenCode 2",
5
5
  "type": "module",
6
6
  "repository": {