@gamflaz/opencode-todolist 1.0.9 → 1.0.11
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 +21 -6
- package/dist/icons/svg-icon.js +1 -1
- package/dist/index.js +7 -7
- package/dist/schemas/todo.js +1 -1
- package/dist/storage/todo-store.js +3 -3
- package/dist/todos/display.js +1 -1
- package/dist/todos/hint.js +2 -2
- package/dist/tui/layout.js +1 -1
- package/dist/tui/todo-panel.d.ts +2 -2
- package/dist/tui/todo-panel.js +66 -66
- package/dist/tui.js +2 -2
- package/index.js +1 -1
- package/package.json +1 -1
- package/tui.js +1 -1
- package/dist/icons/svg-icon.jsx +0 -37
- package/dist/tui/todo-panel.jsx +0 -503
- package/dist/tui.jsx +0 -23
package/README.md
CHANGED
|
@@ -21,17 +21,20 @@ git clone https://github.com/MomoPi-Dark/opencode-todolist.git \
|
|
|
21
21
|
~/.config/opencode/plugins/todolist
|
|
22
22
|
cd ~/.config/opencode/plugins/todolist
|
|
23
23
|
bun install
|
|
24
|
-
bun run build
|
|
25
24
|
```
|
|
26
25
|
|
|
26
|
+
`bun install` runs the `prepare` script, which compiles `dist/` from `src/`
|
|
27
|
+
(TypeScript declarations plus Solid-compiled JSX). No extra step is required.
|
|
28
|
+
|
|
27
29
|
Or add it directly through the OpenCode CLI:
|
|
28
30
|
|
|
29
31
|
```bash
|
|
30
32
|
opencode plugin add @gamflaz/opencode-todolist
|
|
31
33
|
```
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
The npm package ships the prebuilt `dist/`, so no local build runs. OpenCode
|
|
36
|
+
loads local plugins from the plugins directory but does not install their
|
|
37
|
+
dependencies, so `bun install` is required before use.
|
|
35
38
|
|
|
36
39
|
## Tool: `manage_todo_list`
|
|
37
40
|
|
|
@@ -141,11 +144,23 @@ tracked work.
|
|
|
141
144
|
## Development
|
|
142
145
|
|
|
143
146
|
```bash
|
|
144
|
-
bun install
|
|
147
|
+
bun install # runs prepare -> compiles dist/
|
|
145
148
|
bun run check # typecheck + tests
|
|
146
|
-
bun run
|
|
149
|
+
bun run compile # rebuild dist/ (tsc + Solid JSX transform)
|
|
147
150
|
```
|
|
148
151
|
|
|
152
|
+
`bun run compile` must be used instead of `bun run build`: the raw `tsc` pass
|
|
153
|
+
alone leaves intermediate `.jsx` files in `dist/`, which Bun then prefers over
|
|
154
|
+
the compiled `.js` and reports as `No renderer found` / `ENOENT ... dist/tui.jsx`.
|
|
155
|
+
|
|
149
156
|
## Publishing notes
|
|
150
157
|
|
|
151
|
-
|
|
158
|
+
`dist/`, `index.js`, and `tui.js` are generated build output and are not
|
|
159
|
+
committed. They reach users through the npm tarball: npm runs `prepare`
|
|
160
|
+
(`bun run compile`) before packing, and `files` ships the built `dist/`
|
|
161
|
+
directory. The `opencode plugin add` installer skips lifecycle scripts
|
|
162
|
+
(`ignoreScripts: true`), so the tarball — not a post-install build — must
|
|
163
|
+
contain `dist/`.
|
|
164
|
+
|
|
165
|
+
Before publishing, run `bun run check && bun run compile` and confirm
|
|
166
|
+
`dist/` contains no `.jsx` files.
|
package/dist/icons/svg-icon.js
CHANGED
|
@@ -11,7 +11,7 @@ import { usePlugin } from "@opencode/plugin/tui";
|
|
|
11
11
|
import { rgbToHex } from "@opentui/core";
|
|
12
12
|
import { Resvg } from "@resvg/resvg-js";
|
|
13
13
|
import { createMemo, createSignal, onCleanup, Show } from "solid-js";
|
|
14
|
-
import { supportsImageRendering } from "./image-capabilities";
|
|
14
|
+
import { supportsImageRendering } from "./image-capabilities.js";
|
|
15
15
|
export function SvgIcon(props) {
|
|
16
16
|
const {
|
|
17
17
|
source: _,
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Plugin } from "@opencode/plugin";
|
|
2
|
-
import { TodoInputToolSchema, TodoOutputToolSchema, } from "./schemas/todo";
|
|
3
|
-
import { registerTodoSkills } from "./skills/loader";
|
|
4
|
-
import { readTodoData, readTodos, updateTodos } from "./storage/todo-store";
|
|
5
|
-
import { buildTodoHint, consumeTodoHint, findNextTodo, findStuckParent, flattenLeaves, isAnyTodoActive, } from "./todos/hint";
|
|
6
|
-
import { TodoTree } from "./todos/todo-tree";
|
|
7
|
-
import { updateTodoById } from "./todos/update";
|
|
2
|
+
import { TodoInputToolSchema, TodoOutputToolSchema, } from "./schemas/todo.js";
|
|
3
|
+
import { registerTodoSkills } from "./skills/loader.js";
|
|
4
|
+
import { readTodoData, readTodos, updateTodos } from "./storage/todo-store.js";
|
|
5
|
+
import { buildTodoHint, consumeTodoHint, findNextTodo, findStuckParent, flattenLeaves, isAnyTodoActive, } from "./todos/hint.js";
|
|
6
|
+
import { TodoTree } from "./todos/todo-tree.js";
|
|
7
|
+
import { updateTodoById } from "./todos/update.js";
|
|
8
8
|
export function formatReadOutput(todos) {
|
|
9
9
|
if (todos.length === 0)
|
|
10
10
|
return "No todos recorded.";
|
|
@@ -181,6 +181,6 @@ export async function registerTodoPromptHook(ctx) {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
export default Plugin.define({
|
|
184
|
-
id: "opencode.todolist.
|
|
184
|
+
id: "opencode.todolist.tool",
|
|
185
185
|
setup: registerTodoTools,
|
|
186
186
|
});
|
package/dist/schemas/todo.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import z from "zod";
|
|
2
|
-
import { PrioritySchema, TodoStatusSchema } from "./status";
|
|
2
|
+
import { PrioritySchema, TodoStatusSchema } from "./status.js";
|
|
3
3
|
// Child id format: <root_id>.<n> as a number -> 1.1, 2.3, etc.
|
|
4
4
|
// Only a single decimal digit (1-9) is allowed, see the note below.
|
|
5
5
|
const ChildIdSchema = z
|
|
@@ -4,9 +4,9 @@ import { chmod, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises
|
|
|
4
4
|
import { homedir, tmpdir } from "node:os";
|
|
5
5
|
import { join, posix, win32 } from "node:path";
|
|
6
6
|
import * as lockfile from "proper-lockfile";
|
|
7
|
-
import { TodoRootItemSchema } from "../schemas/todo";
|
|
8
|
-
import { TodoTree } from "../todos/todo-tree";
|
|
9
|
-
import { InvalidTodoDataError } from "./errors";
|
|
7
|
+
import { TodoRootItemSchema } from "../schemas/todo.js";
|
|
8
|
+
import { TodoTree } from "../todos/todo-tree.js";
|
|
9
|
+
import { InvalidTodoDataError } from "./errors.js";
|
|
10
10
|
/**
|
|
11
11
|
* Per-session todo persistence.
|
|
12
12
|
*
|
package/dist/todos/display.js
CHANGED
package/dist/todos/hint.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { getHintMarkerPath, readTodoData, TodoStore } from "../storage/todo-store";
|
|
3
|
-
import { TodoTree } from "./todo-tree";
|
|
2
|
+
import { getHintMarkerPath, readTodoData, TodoStore } from "../storage/todo-store.js";
|
|
3
|
+
import { TodoTree } from "./todo-tree.js";
|
|
4
4
|
export const DEFAULT_STALE_MS = 30 * 60 * 1000; // 30 minutes (prompt fallback)
|
|
5
5
|
export const CONTEXT_STALE_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
6
6
|
const UPCOMING_LIMIT = 3;
|
package/dist/tui/layout.js
CHANGED
package/dist/tui/todo-panel.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Plugin } from "@opencode/plugin/tui";
|
|
2
|
-
import { ColorInput, ScrollBoxRenderable } from "@opentui/core";
|
|
3
|
-
import { TodoStatus } from "../schemas/status";
|
|
2
|
+
import { type ColorInput, ScrollBoxRenderable } from "@opentui/core";
|
|
3
|
+
import { type TodoStatus } from "../schemas/status";
|
|
4
4
|
/** How many open items the Continue prompt lists before collapsing the rest. */
|
|
5
5
|
export declare const CONTINUE_PROMPT_MAX_ITEMS = 3;
|
|
6
6
|
/** Per-field caps so one long note cannot flood the prompt. */
|
package/dist/tui/todo-panel.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
1
2
|
import { memo as _$memo } from "@opentui/solid";
|
|
2
3
|
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
3
|
-
import { effect as _$effect } from "@opentui/solid";
|
|
4
4
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
5
5
|
import { insert as _$insert } from "@opentui/solid";
|
|
6
6
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
7
7
|
import { setProp as _$setProp } from "@opentui/solid";
|
|
8
8
|
import { use as _$use } from "@opentui/solid";
|
|
9
|
+
import { spread as _$spread } from "@opentui/solid";
|
|
10
|
+
import { mergeProps as _$mergeProps } from "@opentui/solid";
|
|
9
11
|
import { createElement as _$createElement } from "@opentui/solid";
|
|
10
12
|
import { mkdirSync, watch } from "node:fs";
|
|
11
13
|
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js";
|
|
12
|
-
import { supportsImageRendering } from "../icons/image-capabilities";
|
|
13
|
-
import { SvgIcon } from "../icons/svg-icon";
|
|
14
|
-
import { arrowDownSvgIcon, arrowRightSvgIcon, continuePromptSvgIcon, removeSvgIcon } from "../icons/svg-icons";
|
|
15
|
-
import { StatusLabel } from "../schemas/status";
|
|
16
|
-
import { getTodosDirectory, hashSessionID, readTodosSync, writeTodos } from "../storage/todo-store";
|
|
17
|
-
import { getTodoDisplaySummary, hasOpenTodos } from "../todos/display";
|
|
18
|
-
import { TodoTree } from "../todos/todo-tree";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
14
|
+
import { supportsImageRendering } from "../icons/image-capabilities.js";
|
|
15
|
+
import { SvgIcon } from "../icons/svg-icon.js";
|
|
16
|
+
import { arrowDownSvgIcon, arrowRightSvgIcon, continuePromptSvgIcon, removeSvgIcon } from "../icons/svg-icons.js";
|
|
17
|
+
import { StatusLabel } from "../schemas/status.js";
|
|
18
|
+
import { getTodosDirectory, hashSessionID, readTodosSync, writeTodos } from "../storage/todo-store.js";
|
|
19
|
+
import { getTodoDisplaySummary, hasOpenTodos } from "../todos/display.js";
|
|
20
|
+
import { TodoTree } from "../todos/todo-tree.js";
|
|
21
|
+
import { ERROR_PREFIX, ErrorRowWidth, RowWidth } from "./layout.js";
|
|
22
|
+
import { shorten } from "./text.js";
|
|
21
23
|
|
|
22
24
|
/** How many open items the Continue prompt lists before collapsing the rest. */
|
|
23
25
|
export const CONTINUE_PROMPT_MAX_ITEMS = 3;
|
|
@@ -167,21 +169,29 @@ export function TodoList(props) {
|
|
|
167
169
|
if (el) followActiveRow(el);
|
|
168
170
|
});
|
|
169
171
|
const scrollbarOptions = () => props.scrollbarColor ? {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
verticalScrollbarOptions: {
|
|
173
|
+
trackOptions: {
|
|
174
|
+
backgroundColor: props.scrollbarColor,
|
|
175
|
+
foregroundColor: props.scrollbarColor
|
|
176
|
+
}
|
|
173
177
|
}
|
|
174
|
-
} :
|
|
178
|
+
} : {};
|
|
175
179
|
const isExpanded = id => (props.expandedIds ?? []).includes(id);
|
|
176
180
|
return (() => {
|
|
177
181
|
var _el$ = _$createElement("scrollbox"),
|
|
178
182
|
_el$2 = _$createElement("box");
|
|
179
183
|
_$insertNode(_el$, _el$2);
|
|
180
184
|
_$use(attach, _el$);
|
|
181
|
-
_$setProp(_el$, "width", "
|
|
182
|
-
_$
|
|
183
|
-
|
|
184
|
-
|
|
185
|
+
_$setProp(_el$, "width", "auto");
|
|
186
|
+
_$spread(_el$, _$mergeProps({
|
|
187
|
+
get height() {
|
|
188
|
+
return Math.min(entries().length, TODO_PANEL_MAX_ROWS);
|
|
189
|
+
}
|
|
190
|
+
}, scrollbarOptions, {
|
|
191
|
+
"horizontalScrollbarOptions": {
|
|
192
|
+
visible: false
|
|
193
|
+
}
|
|
194
|
+
}), true);
|
|
185
195
|
_$setProp(_el$2, "flexDirection", "column");
|
|
186
196
|
_$setProp(_el$2, "flexShrink", 0);
|
|
187
197
|
_$insert(_el$2, _$createComponent(For, {
|
|
@@ -239,23 +249,23 @@ export function TodoList(props) {
|
|
|
239
249
|
}
|
|
240
250
|
}), null);
|
|
241
251
|
_$effect(_p$ => {
|
|
242
|
-
var _v$
|
|
243
|
-
_v$
|
|
244
|
-
_v$
|
|
252
|
+
var _v$ = rowId(entry.row.id),
|
|
253
|
+
_v$2 = RowWidth(entry.row),
|
|
254
|
+
_v$3 = {
|
|
245
255
|
fg: entry.row.iconColor
|
|
246
256
|
},
|
|
247
|
-
_v$
|
|
257
|
+
_v$4 = {
|
|
248
258
|
fg: entry.row.textColor
|
|
249
259
|
},
|
|
250
|
-
_v$
|
|
260
|
+
_v$5 = {
|
|
251
261
|
fg: entry.row.textColor,
|
|
252
262
|
strikethrough: entry.row.status === "completed"
|
|
253
263
|
};
|
|
254
|
-
_v$
|
|
255
|
-
_v$
|
|
256
|
-
_v$
|
|
257
|
-
_v$
|
|
258
|
-
_v$
|
|
264
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$3, "id", _v$, _p$.e));
|
|
265
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$3, "width", _v$2, _p$.t));
|
|
266
|
+
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$7, "style", _v$3, _p$.a));
|
|
267
|
+
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$9, "style", _v$4, _p$.o));
|
|
268
|
+
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$0, "style", _v$5, _p$.i));
|
|
259
269
|
return _p$;
|
|
260
270
|
}, {
|
|
261
271
|
e: undefined,
|
|
@@ -287,21 +297,21 @@ export function TodoList(props) {
|
|
|
287
297
|
_$insert(_el$13, ERROR_PREFIX, null);
|
|
288
298
|
_$insert(_el$14, () => entry.row.content);
|
|
289
299
|
_$effect(_p$ => {
|
|
290
|
-
var _v$
|
|
291
|
-
_v$
|
|
300
|
+
var _v$6 = `${rowId(entry.row.id)}-note`,
|
|
301
|
+
_v$7 = ErrorRowWidth({
|
|
292
302
|
error: entry.row.content,
|
|
293
303
|
depth: entry.row.depth
|
|
294
304
|
}),
|
|
295
|
-
_v$
|
|
305
|
+
_v$8 = {
|
|
296
306
|
fg: props.scrollbarColor
|
|
297
307
|
},
|
|
298
|
-
_v$
|
|
308
|
+
_v$9 = {
|
|
299
309
|
fg: entry.row.textColor
|
|
300
310
|
};
|
|
301
|
-
_v$
|
|
302
|
-
_v$
|
|
303
|
-
_v$
|
|
304
|
-
_v$
|
|
311
|
+
_v$6 !== _p$.e && (_p$.e = _$setProp(_el$11, "id", _v$6, _p$.e));
|
|
312
|
+
_v$7 !== _p$.t && (_p$.t = _$setProp(_el$11, "width", _v$7, _p$.t));
|
|
313
|
+
_v$8 !== _p$.a && (_p$.a = _$setProp(_el$13, "style", _v$8, _p$.a));
|
|
314
|
+
_v$9 !== _p$.o && (_p$.o = _$setProp(_el$14, "style", _v$9, _p$.o));
|
|
305
315
|
return _p$;
|
|
306
316
|
}, {
|
|
307
317
|
e: undefined,
|
|
@@ -312,16 +322,6 @@ export function TodoList(props) {
|
|
|
312
322
|
return _el$11;
|
|
313
323
|
})()
|
|
314
324
|
}));
|
|
315
|
-
_$effect(_p$ => {
|
|
316
|
-
var _v$ = Math.min(entries().length, TODO_PANEL_MAX_ROWS),
|
|
317
|
-
_v$2 = scrollbarOptions();
|
|
318
|
-
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "height", _v$, _p$.e));
|
|
319
|
-
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "verticalScrollbarOptions", _v$2, _p$.t));
|
|
320
|
-
return _p$;
|
|
321
|
-
}, {
|
|
322
|
-
e: undefined,
|
|
323
|
-
t: undefined
|
|
324
|
-
});
|
|
325
325
|
return _el$;
|
|
326
326
|
})();
|
|
327
327
|
}
|
|
@@ -654,12 +654,12 @@ export function TodoProgress(props) {
|
|
|
654
654
|
_$insertNode(_el$25, _$createTextNode(`Continue`));
|
|
655
655
|
_$setProp(_el$25, "wrapMode", "none");
|
|
656
656
|
_$effect(_p$ => {
|
|
657
|
-
var _v$
|
|
658
|
-
_v$
|
|
659
|
-
_v$
|
|
660
|
-
_v$
|
|
661
|
-
_v$
|
|
662
|
-
_v$
|
|
657
|
+
var _v$0 = continueHover.onMouseOver,
|
|
658
|
+
_v$1 = continueHover.onMouseOut,
|
|
659
|
+
_v$10 = hoverColor(continueHover.hovered(), theme().text.action.primary.base);
|
|
660
|
+
_v$0 !== _p$.e && (_p$.e = _$setProp(_el$24, "onMouseOver", _v$0, _p$.e));
|
|
661
|
+
_v$1 !== _p$.t && (_p$.t = _$setProp(_el$24, "onMouseOut", _v$1, _p$.t));
|
|
662
|
+
_v$10 !== _p$.a && (_p$.a = _$setProp(_el$25, "fg", _v$10, _p$.a));
|
|
663
663
|
return _p$;
|
|
664
664
|
}, {
|
|
665
665
|
e: undefined,
|
|
@@ -708,24 +708,24 @@ export function TodoProgress(props) {
|
|
|
708
708
|
}
|
|
709
709
|
}), null);
|
|
710
710
|
_$effect(_p$ => {
|
|
711
|
-
var _v$
|
|
712
|
-
_v$
|
|
713
|
-
_v$
|
|
711
|
+
var _v$11 = theme().border.base,
|
|
712
|
+
_v$12 = imageSupported() ? 0 : 1,
|
|
713
|
+
_v$13 = {
|
|
714
714
|
fg: theme().text.base
|
|
715
715
|
},
|
|
716
|
-
_v$
|
|
716
|
+
_v$14 = {
|
|
717
717
|
fg: theme().text.muted
|
|
718
718
|
},
|
|
719
|
-
_v$
|
|
720
|
-
_v$
|
|
721
|
-
_v$
|
|
722
|
-
_v$
|
|
723
|
-
_v$
|
|
724
|
-
_v$
|
|
725
|
-
_v$
|
|
726
|
-
_v$
|
|
727
|
-
_v$
|
|
728
|
-
_v$
|
|
719
|
+
_v$15 = !imageSupported() ? 1.5 : 0,
|
|
720
|
+
_v$16 = clearHover.onMouseOver,
|
|
721
|
+
_v$17 = clearHover.onMouseOut;
|
|
722
|
+
_v$11 !== _p$.e && (_p$.e = _$setProp(_el$15, "borderColor", _v$11, _p$.e));
|
|
723
|
+
_v$12 !== _p$.t && (_p$.t = _$setProp(_el$18, "marginLeft", _v$12, _p$.t));
|
|
724
|
+
_v$13 !== _p$.a && (_p$.a = _$setProp(_el$21, "style", _v$13, _p$.a));
|
|
725
|
+
_v$14 !== _p$.o && (_p$.o = _$setProp(_el$23, "style", _v$14, _p$.o));
|
|
726
|
+
_v$15 !== _p$.i && (_p$.i = _$setProp(_el$27, "marginRight", _v$15, _p$.i));
|
|
727
|
+
_v$16 !== _p$.n && (_p$.n = _$setProp(_el$27, "onMouseOver", _v$16, _p$.n));
|
|
728
|
+
_v$17 !== _p$.s && (_p$.s = _$setProp(_el$27, "onMouseOut", _v$17, _p$.s));
|
|
729
729
|
return _p$;
|
|
730
730
|
}, {
|
|
731
731
|
e: undefined,
|
package/dist/tui.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
2
2
|
import { Plugin } from "@opencode/plugin/tui";
|
|
3
3
|
import { RendererContext } from "@opentui/solid";
|
|
4
|
-
import { TodoProgress } from "./tui/todo-panel";
|
|
4
|
+
import { TodoProgress } from "./tui/todo-panel.js";
|
|
5
5
|
export default Plugin.define({
|
|
6
|
-
id: "opencode.todolist.
|
|
6
|
+
id: "opencode.todolist.tui",
|
|
7
7
|
setup(context) {
|
|
8
8
|
const releaseSlot = context.ui.slot({
|
|
9
9
|
append: "session.composer.top",
|
package/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default } from "./dist/index";
|
|
1
|
+
export { default } from "./dist/index.js";
|
package/package.json
CHANGED
package/tui.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default } from "./dist/tui";
|
|
1
|
+
export { default } from "./dist/tui.js";
|
package/dist/icons/svg-icon.jsx
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { usePlugin } from "@opencode/plugin/tui";
|
|
2
|
-
import { rgbToHex } from "@opentui/core";
|
|
3
|
-
import { Resvg } from "@resvg/resvg-js";
|
|
4
|
-
import { createMemo, createSignal, onCleanup, Show } from "solid-js";
|
|
5
|
-
import { supportsImageRendering } from "./image-capabilities";
|
|
6
|
-
export function SvgIcon(props) {
|
|
7
|
-
const { source: _, color: __, fallback, ...prop } = props;
|
|
8
|
-
const context = usePlugin();
|
|
9
|
-
const [imageSupported, setImageSupported] = createSignal(supportsImageRendering(context.renderer.capabilities));
|
|
10
|
-
const updateImageSupport = (capabilities) => {
|
|
11
|
-
setImageSupported(supportsImageRendering(capabilities));
|
|
12
|
-
};
|
|
13
|
-
context.renderer.on("capabilities", updateImageSupport);
|
|
14
|
-
onCleanup(() => context.renderer.off("capabilities", updateImageSupport));
|
|
15
|
-
const iconColor = () => props.color
|
|
16
|
-
? typeof props.color === "string"
|
|
17
|
-
? props.color
|
|
18
|
-
: rgbToHex(props.color)
|
|
19
|
-
: "currentColor";
|
|
20
|
-
const pngBuffer = createMemo(() => {
|
|
21
|
-
const bgToken = context.theme.background.base;
|
|
22
|
-
const bg = rgbToHex(bgToken);
|
|
23
|
-
let svg = props.source
|
|
24
|
-
.replace(/stroke="[^"]*"/g, `stroke="${iconColor()}"`)
|
|
25
|
-
.replace(/color="[^"]*"/g, `color="${iconColor()}"`);
|
|
26
|
-
const resvg = new Resvg(svg, {
|
|
27
|
-
fitTo: { mode: "width", value: 64 },
|
|
28
|
-
background: bg,
|
|
29
|
-
});
|
|
30
|
-
return resvg.render().asPng();
|
|
31
|
-
});
|
|
32
|
-
return (<Show when={imageSupported()} fallback={<text>
|
|
33
|
-
<span style={{ fg: iconColor() }}>{fallback}</span>
|
|
34
|
-
</text>}>
|
|
35
|
-
<image source={pngBuffer()} width={props.width ?? 2} height={props.height ?? 1} {...prop}/>
|
|
36
|
-
</Show>);
|
|
37
|
-
}
|
package/dist/tui/todo-panel.jsx
DELETED
|
@@ -1,503 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, watch } from "node:fs";
|
|
2
|
-
import { createEffect, createMemo, createSignal, For, onCleanup, Show, } from "solid-js";
|
|
3
|
-
import { supportsImageRendering } from "../icons/image-capabilities";
|
|
4
|
-
import { SvgIcon } from "../icons/svg-icon";
|
|
5
|
-
import { arrowDownSvgIcon, arrowRightSvgIcon, continuePromptSvgIcon, removeSvgIcon, } from "../icons/svg-icons";
|
|
6
|
-
import { StatusLabel } from "../schemas/status";
|
|
7
|
-
import { getTodosDirectory, hashSessionID, readTodosSync, writeTodos, } from "../storage/todo-store";
|
|
8
|
-
import { getTodoDisplaySummary, hasOpenTodos, } from "../todos/display";
|
|
9
|
-
import { TodoTree } from "../todos/todo-tree";
|
|
10
|
-
import { shorten } from "./text";
|
|
11
|
-
import { ERROR_PREFIX, ErrorRowWidth, RowWidth } from "./layout";
|
|
12
|
-
/** How many open items the Continue prompt lists before collapsing the rest. */
|
|
13
|
-
export const CONTINUE_PROMPT_MAX_ITEMS = 3;
|
|
14
|
-
/** Per-field caps so one long note cannot flood the prompt. */
|
|
15
|
-
export const CONTINUE_PROMPT_MAX_CONTENT = 60;
|
|
16
|
-
export const CONTINUE_PROMPT_MAX_NOTE = 60;
|
|
17
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
18
|
-
const SPINNER_INTERVAL_MS = 100;
|
|
19
|
-
const WATCH_DEBOUNCE_MS = 50;
|
|
20
|
-
/**
|
|
21
|
-
* Maximum number of todo rows the panel shows before it starts scrolling.
|
|
22
|
-
* Keeping the panel compact protects the composer; scrolling guarantees no
|
|
23
|
-
* row is ever silently dropped.
|
|
24
|
-
*/
|
|
25
|
-
export const TODO_PANEL_MAX_ROWS = 8;
|
|
26
|
-
const isOpen = (status) => status !== "completed" && status !== "cancelled";
|
|
27
|
-
function ContinuePrompt(summary) {
|
|
28
|
-
const open = summary?.items.filter((item) => isOpen(item.status)) ?? [];
|
|
29
|
-
const shown = open.slice(0, CONTINUE_PROMPT_MAX_ITEMS);
|
|
30
|
-
const hidden = open.length - shown.length;
|
|
31
|
-
const lines = [
|
|
32
|
-
"Continue the current todolist.",
|
|
33
|
-
"First call manage_todo_list [read] and reconcile the stored list with this summary.",
|
|
34
|
-
];
|
|
35
|
-
if (summary) {
|
|
36
|
-
lines.push(`Progress: ${summary.completed}/${summary.total} completed.`);
|
|
37
|
-
}
|
|
38
|
-
if (open.length) {
|
|
39
|
-
lines.push("Open items that still need work (short summary):");
|
|
40
|
-
for (const item of shown) {
|
|
41
|
-
const note = item.note
|
|
42
|
-
? ` — ${shorten(item.note, CONTINUE_PROMPT_MAX_NOTE)}`
|
|
43
|
-
: "";
|
|
44
|
-
lines.push(`${item.depth ? " " : ""}- ${item.id} (${StatusLabel[item.status]}) ${shorten(item.content, CONTINUE_PROMPT_MAX_CONTENT)}${note}`);
|
|
45
|
-
}
|
|
46
|
-
if (hidden > 0)
|
|
47
|
-
lines.push(`... (${hidden} more open item(s))`);
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
lines.push("No open items are listed. Verify the stored state before stopping.");
|
|
51
|
-
}
|
|
52
|
-
lines.push("Continue the next open item immediately. Keep exactly one item in_progress and update the todo list after verification.");
|
|
53
|
-
return lines.join("\n");
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Flattens rows into renderable lines, inserting an error detail right below
|
|
57
|
-
* an expanded error row (click to toggle).
|
|
58
|
-
*/
|
|
59
|
-
export function TodoListEntries(rows, expandedIds) {
|
|
60
|
-
const expanded = new Set(expandedIds);
|
|
61
|
-
const entries = [];
|
|
62
|
-
for (const row of rows) {
|
|
63
|
-
entries.push({ kind: "row", row });
|
|
64
|
-
if (row.status === "error" && expanded.has(row.id)) {
|
|
65
|
-
entries.push({ kind: "error", row });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return entries;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Follow the most advanced active row: the last `in_progress` entry is the
|
|
72
|
-
* task currently being executed. Error lines are skipped.
|
|
73
|
-
*/
|
|
74
|
-
export function FindActiveEntryIndex(entries) {
|
|
75
|
-
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
76
|
-
const entry = entries[index];
|
|
77
|
-
if (entry.kind === "row" && entry.row.status === "in_progress") {
|
|
78
|
-
return index;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return -1;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Scroll offset needed to reveal the active row, or `undefined` when nothing
|
|
85
|
-
* should move (no active row, already visible, or the list fits).
|
|
86
|
-
*
|
|
87
|
-
* Kept pure so the follow behaviour is testable without a live renderer.
|
|
88
|
-
*/
|
|
89
|
-
export function NextFollowScrollTop(input) {
|
|
90
|
-
const { currentTop, activeIndex, rowCount, viewportHeight } = input;
|
|
91
|
-
if (activeIndex < 0)
|
|
92
|
-
return undefined;
|
|
93
|
-
const viewport = Math.max(1, viewportHeight);
|
|
94
|
-
const maxTop = Math.max(0, rowCount - viewport);
|
|
95
|
-
if (maxTop === 0)
|
|
96
|
-
return undefined;
|
|
97
|
-
const activeBottom = activeIndex + 1;
|
|
98
|
-
const visibleBottom = currentTop + viewport;
|
|
99
|
-
if (activeIndex >= currentTop && activeBottom <= visibleBottom) {
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
102
|
-
const targetTop = activeIndex >= visibleBottom ? activeBottom - viewport : activeIndex;
|
|
103
|
-
return Math.min(Math.max(0, targetTop), maxTop);
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Presentational, context-free todo list.
|
|
107
|
-
*
|
|
108
|
-
* The panel is deliberately chrome-free: no indicator row, and the horizontal
|
|
109
|
-
* scrollbar stays hidden. Long titles never wrap — they are reached by
|
|
110
|
-
* scrolling left/right, and the vertical list scrolls when it exceeds
|
|
111
|
-
* `TODO_PANEL_MAX_ROWS`. When a row is `in_progress` the panel follows it.
|
|
112
|
-
*
|
|
113
|
-
* `activeIcon` is an accessor so the spinner updates only the icon text node
|
|
114
|
-
* instead of forcing every row object to be rebuilt on each animation frame.
|
|
115
|
-
*/
|
|
116
|
-
export function TodoList(props) {
|
|
117
|
-
const rowId = (id) => `todo-row-${id}`;
|
|
118
|
-
const [box, setBox] = createSignal();
|
|
119
|
-
const entries = createMemo(() => TodoListEntries(props.rows, props.expandedIds ?? []));
|
|
120
|
-
const iconOf = (row) => row.status === "in_progress"
|
|
121
|
-
? (props.activeIcon?.() ?? row.icon)
|
|
122
|
-
: row.icon;
|
|
123
|
-
const followActiveRow = (el) => {
|
|
124
|
-
if (el.isDestroyed)
|
|
125
|
-
return;
|
|
126
|
-
const target = NextFollowScrollTop({
|
|
127
|
-
currentTop: el.scrollTop,
|
|
128
|
-
activeIndex: FindActiveEntryIndex(entries()),
|
|
129
|
-
rowCount: entries().length,
|
|
130
|
-
viewportHeight: el.viewport.height,
|
|
131
|
-
});
|
|
132
|
-
if (target !== undefined)
|
|
133
|
-
el.scrollTo(target);
|
|
134
|
-
};
|
|
135
|
-
const attach = (el) => {
|
|
136
|
-
setBox(el);
|
|
137
|
-
el.content.maxWidth = undefined;
|
|
138
|
-
const internalOnSizeChange = el.content.onSizeChange;
|
|
139
|
-
el.content.onSizeChange = () => {
|
|
140
|
-
internalOnSizeChange?.call(el.content);
|
|
141
|
-
followActiveRow(el);
|
|
142
|
-
};
|
|
143
|
-
props.ref?.(el);
|
|
144
|
-
};
|
|
145
|
-
// `entries()` already changes whenever rows or expanded ids change.
|
|
146
|
-
createEffect(() => {
|
|
147
|
-
entries();
|
|
148
|
-
const el = box();
|
|
149
|
-
if (el)
|
|
150
|
-
followActiveRow(el);
|
|
151
|
-
});
|
|
152
|
-
const scrollbarOptions = () => props.scrollbarColor
|
|
153
|
-
? {
|
|
154
|
-
trackOptions: {
|
|
155
|
-
backgroundColor: props.scrollbarColor,
|
|
156
|
-
foregroundColor: props.scrollbarColor,
|
|
157
|
-
},
|
|
158
|
-
}
|
|
159
|
-
: undefined;
|
|
160
|
-
const isExpanded = (id) => (props.expandedIds ?? []).includes(id);
|
|
161
|
-
return (<scrollbox ref={attach} width="100%" height={Math.min(entries().length, TODO_PANEL_MAX_ROWS)} verticalScrollbarOptions={scrollbarOptions()} horizontalScrollbarOptions={{ visible: false }}>
|
|
162
|
-
<box flexDirection="column" flexShrink={0}>
|
|
163
|
-
<For each={entries()}>
|
|
164
|
-
{(entry) => entry.kind === "row" ? (<box id={rowId(entry.row.id)} flexDirection="row" height={1} flexShrink={0} width={RowWidth(entry.row)} onMouseDown={(e) => {
|
|
165
|
-
if (e.button === 0 && entry.row.status === "error") {
|
|
166
|
-
e.stopPropagation?.();
|
|
167
|
-
props.onToggleRow?.(entry.row.id);
|
|
168
|
-
}
|
|
169
|
-
}}>
|
|
170
|
-
<text wrapMode="none">
|
|
171
|
-
<Show when={entry.row.depth > 0}>
|
|
172
|
-
<span>{" "}</span>
|
|
173
|
-
</Show>
|
|
174
|
-
<span style={{ fg: entry.row.iconColor }}>
|
|
175
|
-
{iconOf(entry.row)}{" "}
|
|
176
|
-
</span>
|
|
177
|
-
<span style={{ fg: entry.row.textColor }}>
|
|
178
|
-
{TodoTree.formatIdPrefix(entry.row.id, entry.row.depth)}
|
|
179
|
-
</span>
|
|
180
|
-
<span style={{
|
|
181
|
-
fg: entry.row.textColor,
|
|
182
|
-
strikethrough: entry.row.status === "completed",
|
|
183
|
-
}}>
|
|
184
|
-
{entry.row.content}
|
|
185
|
-
</span>
|
|
186
|
-
<Show when={entry.row.status === "error"}>
|
|
187
|
-
<span style={{ fg: props.scrollbarColor }}>
|
|
188
|
-
{" "}
|
|
189
|
-
{isExpanded(entry.row.id) ? "⌄" : "›"}
|
|
190
|
-
</span>
|
|
191
|
-
</Show>
|
|
192
|
-
</text>
|
|
193
|
-
</box>) : (<box id={`${rowId(entry.row.id)}-note`} flexDirection="row" height={1} flexShrink={0} width={ErrorRowWidth({
|
|
194
|
-
error: entry.row.content,
|
|
195
|
-
depth: entry.row.depth,
|
|
196
|
-
})} onMouseDown={(e) => {
|
|
197
|
-
if (e.button === 0) {
|
|
198
|
-
e.stopPropagation?.();
|
|
199
|
-
props.onToggleRow?.(entry.row.id);
|
|
200
|
-
}
|
|
201
|
-
}}>
|
|
202
|
-
<text wrapMode="none">
|
|
203
|
-
<span style={{ fg: props.scrollbarColor }}>
|
|
204
|
-
{entry.row.depth > 0 ? " " : " "}
|
|
205
|
-
{ERROR_PREFIX}
|
|
206
|
-
</span>
|
|
207
|
-
<span style={{ fg: entry.row.textColor }}>
|
|
208
|
-
{entry.row.content}
|
|
209
|
-
</span>
|
|
210
|
-
</text>
|
|
211
|
-
</box>)}
|
|
212
|
-
</For>
|
|
213
|
-
</box>
|
|
214
|
-
</scrollbox>);
|
|
215
|
-
}
|
|
216
|
-
/** Small helper so each hover target does not repeat three lines. */
|
|
217
|
-
function createHover() {
|
|
218
|
-
const [hovered, setHovered] = createSignal(false);
|
|
219
|
-
return {
|
|
220
|
-
hovered,
|
|
221
|
-
onMouseOver: () => setHovered(true),
|
|
222
|
-
onMouseOut: () => setHovered(false),
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Watches the todos directory and calls `onChange` (debounced) when a file
|
|
227
|
-
* belonging to the current session changes. Cleans up with the owner scope.
|
|
228
|
-
*/
|
|
229
|
-
function watchSessionTodos(sessionID, onChange) {
|
|
230
|
-
let timer;
|
|
231
|
-
let watcher;
|
|
232
|
-
const hashed = createMemo(() => {
|
|
233
|
-
const sid = sessionID();
|
|
234
|
-
if (!sid)
|
|
235
|
-
return "";
|
|
236
|
-
try {
|
|
237
|
-
return hashSessionID(sid);
|
|
238
|
-
}
|
|
239
|
-
catch {
|
|
240
|
-
return "";
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
const notify = () => {
|
|
244
|
-
clearTimeout(timer);
|
|
245
|
-
timer = setTimeout(onChange, WATCH_DEBOUNCE_MS);
|
|
246
|
-
};
|
|
247
|
-
const belongsToSession = (name) => {
|
|
248
|
-
const sid = sessionID();
|
|
249
|
-
if (!sid)
|
|
250
|
-
return true;
|
|
251
|
-
const hash = hashed();
|
|
252
|
-
return ((hash !== "" && name.startsWith(hash)) ||
|
|
253
|
-
name.startsWith(sid) ||
|
|
254
|
-
name.startsWith(`todos-${sid}-`));
|
|
255
|
-
};
|
|
256
|
-
try {
|
|
257
|
-
const baseDir = getTodosDirectory();
|
|
258
|
-
mkdirSync(baseDir, { recursive: true });
|
|
259
|
-
watcher = watch(baseDir, { recursive: true, persistent: false }, (_, filename) => {
|
|
260
|
-
if (!filename || belongsToSession(String(filename)))
|
|
261
|
-
notify();
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
catch (err) {
|
|
265
|
-
console.error("[todos] watcher initialization failed:", err);
|
|
266
|
-
}
|
|
267
|
-
onCleanup(() => {
|
|
268
|
-
clearTimeout(timer);
|
|
269
|
-
watcher?.close();
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
export function TodoProgress(props) {
|
|
273
|
-
const { context } = props;
|
|
274
|
-
const sessionID = () => props.sessionID ?? "";
|
|
275
|
-
const theme = () => context.theme;
|
|
276
|
-
const [refresh, setRefresh] = createSignal(0);
|
|
277
|
-
const [isCollapsed, setIsCollapsed] = createSignal(true);
|
|
278
|
-
const [isContinuePending, setIsContinuePending] = createSignal(false);
|
|
279
|
-
const [animFrame, setAnimFrame] = createSignal(0);
|
|
280
|
-
const [expandedIds, setExpandedIds] = createSignal([]);
|
|
281
|
-
const clearHover = createHover();
|
|
282
|
-
const continueHover = createHover();
|
|
283
|
-
// Image capability
|
|
284
|
-
const [imageSupported, setImageSupported] = createSignal(supportsImageRendering(context.renderer?.capabilities));
|
|
285
|
-
const onCapabilities = (caps) => setImageSupported(supportsImageRendering(caps));
|
|
286
|
-
context.renderer.on("capabilities", onCapabilities);
|
|
287
|
-
onCleanup(() => context.renderer.off("capabilities", onCapabilities));
|
|
288
|
-
// Running state
|
|
289
|
-
const readRunning = () => {
|
|
290
|
-
const sid = sessionID();
|
|
291
|
-
if (!sid)
|
|
292
|
-
return false;
|
|
293
|
-
try {
|
|
294
|
-
return context.data.session.status(sid) === "running";
|
|
295
|
-
}
|
|
296
|
-
catch {
|
|
297
|
-
return false;
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
const [isRunning, setIsRunning] = createSignal(readRunning());
|
|
301
|
-
createEffect(() => setIsRunning(readRunning()));
|
|
302
|
-
const unsubscribe = context.data.listen(({ details }) => {
|
|
303
|
-
const sid = sessionID();
|
|
304
|
-
switch (details.type) {
|
|
305
|
-
case "session.execution.started":
|
|
306
|
-
case "session.execution.interrupted":
|
|
307
|
-
case "session.execution.succeeded":
|
|
308
|
-
case "session.execution.failed":
|
|
309
|
-
case "session.idle":
|
|
310
|
-
if (!sid || details.data.sessionID === sid) {
|
|
311
|
-
setIsRunning(details.type === "session.execution.started");
|
|
312
|
-
setIsContinuePending(false);
|
|
313
|
-
}
|
|
314
|
-
break;
|
|
315
|
-
case "session.status":
|
|
316
|
-
if (!sid || details.data.sessionID === sid) {
|
|
317
|
-
setIsRunning(details.data.status.type !== "idle");
|
|
318
|
-
setIsContinuePending(false);
|
|
319
|
-
}
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
onCleanup(() => {
|
|
324
|
-
try {
|
|
325
|
-
unsubscribe();
|
|
326
|
-
}
|
|
327
|
-
catch { }
|
|
328
|
-
});
|
|
329
|
-
// Todo data
|
|
330
|
-
watchSessionTodos(sessionID, () => setRefresh((v) => v + 1));
|
|
331
|
-
const summary = createMemo(() => {
|
|
332
|
-
refresh();
|
|
333
|
-
const sid = sessionID();
|
|
334
|
-
if (!sid)
|
|
335
|
-
return null;
|
|
336
|
-
try {
|
|
337
|
-
return getTodoDisplaySummary(readTodosSync(sid));
|
|
338
|
-
}
|
|
339
|
-
catch {
|
|
340
|
-
return null;
|
|
341
|
-
}
|
|
342
|
-
});
|
|
343
|
-
// Spinner: one interval, only while something is actually running.
|
|
344
|
-
const hasInProgress = createMemo(() => summary()?.items.some((i) => i.status === "in_progress") ?? false);
|
|
345
|
-
createEffect(() => {
|
|
346
|
-
if (!(hasInProgress() && isRunning()))
|
|
347
|
-
return;
|
|
348
|
-
const id = setInterval(() => setAnimFrame((prev) => (prev + 1) % SPINNER_FRAMES.length), SPINNER_INTERVAL_MS);
|
|
349
|
-
onCleanup(() => clearInterval(id));
|
|
350
|
-
});
|
|
351
|
-
const activeIcon = () => (isRunning() ? SPINNER_FRAMES[animFrame()] : "-");
|
|
352
|
-
const itemIcon = (status, fallback) => status === "in_progress" ? activeIcon() : fallback;
|
|
353
|
-
const activeItem = createMemo(() => {
|
|
354
|
-
const items = summary()?.items;
|
|
355
|
-
if (!items)
|
|
356
|
-
return null;
|
|
357
|
-
return (items.find((i) => i.status === "in_progress" && i.depth > 0) ??
|
|
358
|
-
items.find((i) => i.status === "in_progress") ??
|
|
359
|
-
null);
|
|
360
|
-
});
|
|
361
|
-
// Colors
|
|
362
|
-
const getStatusColor = (status) => {
|
|
363
|
-
const t = theme();
|
|
364
|
-
switch (status) {
|
|
365
|
-
case "in_progress":
|
|
366
|
-
return isRunning()
|
|
367
|
-
? (t.text.action?.primary?.base ??
|
|
368
|
-
t.text.feedback.info?.base ??
|
|
369
|
-
t.text.base)
|
|
370
|
-
: t.text.muted;
|
|
371
|
-
case "completed":
|
|
372
|
-
return t.text.feedback.success.base;
|
|
373
|
-
case "error":
|
|
374
|
-
case "cancelled":
|
|
375
|
-
return t.text.feedback.error.base;
|
|
376
|
-
default:
|
|
377
|
-
return t.text.muted;
|
|
378
|
-
}
|
|
379
|
-
};
|
|
380
|
-
const getTextColor = (status) => status === "completed" || status === "cancelled" || status === "error"
|
|
381
|
-
? theme().text.muted
|
|
382
|
-
: theme().text.base;
|
|
383
|
-
/**
|
|
384
|
-
* Rows are built only while the panel is expanded and do NOT depend on the
|
|
385
|
-
* spinner frame, so the row objects stay stable between animation ticks.
|
|
386
|
-
*/
|
|
387
|
-
const rows = createMemo(() => {
|
|
388
|
-
if (isCollapsed())
|
|
389
|
-
return [];
|
|
390
|
-
return (summary()?.items ?? []).map((item) => ({
|
|
391
|
-
id: item.id,
|
|
392
|
-
content: item.content,
|
|
393
|
-
icon: item.icon,
|
|
394
|
-
iconColor: getStatusColor(item.status),
|
|
395
|
-
textColor: getTextColor(item.status),
|
|
396
|
-
depth: item.depth,
|
|
397
|
-
status: item.status,
|
|
398
|
-
}));
|
|
399
|
-
});
|
|
400
|
-
// Actions
|
|
401
|
-
const toggleRowNote = (id) => setExpandedIds((current) => current.includes(id)
|
|
402
|
-
? current.filter((entry) => entry !== id)
|
|
403
|
-
: [...current, id]);
|
|
404
|
-
const continueTodos = async () => {
|
|
405
|
-
if (isRunning() || isContinuePending())
|
|
406
|
-
return;
|
|
407
|
-
const sid = sessionID();
|
|
408
|
-
if (!sid)
|
|
409
|
-
return;
|
|
410
|
-
setIsContinuePending(true);
|
|
411
|
-
try {
|
|
412
|
-
await context.client.session.prompt({
|
|
413
|
-
sessionID: sid,
|
|
414
|
-
text: ContinuePrompt(summary()),
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
catch (error) {
|
|
418
|
-
setIsContinuePending(false);
|
|
419
|
-
console.error("[todos] continue failed:", error);
|
|
420
|
-
}
|
|
421
|
-
};
|
|
422
|
-
const clearTodos = async () => {
|
|
423
|
-
const sid = sessionID();
|
|
424
|
-
if (!sid)
|
|
425
|
-
return;
|
|
426
|
-
try {
|
|
427
|
-
await writeTodos(sid, []);
|
|
428
|
-
setRefresh((v) => v + 1);
|
|
429
|
-
}
|
|
430
|
-
catch (error) {
|
|
431
|
-
console.error("[todos] clear failed:", error);
|
|
432
|
-
}
|
|
433
|
-
};
|
|
434
|
-
const hoverColor = (hovered, active) => hovered ? active : theme().text.muted;
|
|
435
|
-
return (<Show when={summary()}>
|
|
436
|
-
{(data) => (<box flexDirection="column" border={true} borderStyle="rounded" borderColor={theme().border.base} paddingLeft={1} paddingRight={1}>
|
|
437
|
-
<box flexDirection="row" justifyContent="space-between">
|
|
438
|
-
<box flexDirection="row" flexGrow={1} onMouseDown={(e) => {
|
|
439
|
-
if (e.button === 0) {
|
|
440
|
-
e.stopPropagation?.();
|
|
441
|
-
setIsCollapsed((v) => !v);
|
|
442
|
-
}
|
|
443
|
-
}}>
|
|
444
|
-
<box flexDirection="row" width={2} height={1} marginRight={1} marginLeft={imageSupported() ? 0 : 1} alignItems="flex-start">
|
|
445
|
-
<SvgIcon source={isCollapsed() ? arrowRightSvgIcon : arrowDownSvgIcon} color={theme().text.muted} fallback={isCollapsed() ? "▶" : "▼"} width={2} height={1}/>
|
|
446
|
-
</box>
|
|
447
|
-
|
|
448
|
-
<box flexDirection="row" flexGrow={1} flexShrink={1} overflow="hidden" marginRight={1}>
|
|
449
|
-
<text wrapMode="none">
|
|
450
|
-
<span style={{ fg: theme().text.base }}>{"Todos "}</span>
|
|
451
|
-
<span style={{ fg: theme().text.muted }}>
|
|
452
|
-
{`(${data().completed}/${data().total})`}
|
|
453
|
-
</span>
|
|
454
|
-
<Show when={isCollapsed() && activeItem()}>
|
|
455
|
-
{(active) => (<>
|
|
456
|
-
<span style={{ fg: theme().text.muted }}>{" "}</span>
|
|
457
|
-
<span style={{ fg: getStatusColor(active().status) }}>
|
|
458
|
-
{itemIcon(active().status, active().icon)}{" "}
|
|
459
|
-
</span>
|
|
460
|
-
<span style={{ fg: getTextColor(active().status) }}>
|
|
461
|
-
{TodoTree.formatIdPrefix(active().id, active().depth)}
|
|
462
|
-
{active().content}
|
|
463
|
-
</span>
|
|
464
|
-
</>)}
|
|
465
|
-
</Show>
|
|
466
|
-
</text>
|
|
467
|
-
</box>
|
|
468
|
-
</box>
|
|
469
|
-
|
|
470
|
-
<Show when={hasOpenTodos(data().items) &&
|
|
471
|
-
!isRunning() &&
|
|
472
|
-
!isContinuePending()}>
|
|
473
|
-
<box flexDirection="row" gap={1} marginRight={3} onMouseOver={continueHover.onMouseOver} onMouseOut={continueHover.onMouseOut} onMouseDown={(e) => {
|
|
474
|
-
if (e.button === 0) {
|
|
475
|
-
e.stopPropagation();
|
|
476
|
-
void continueTodos();
|
|
477
|
-
}
|
|
478
|
-
}}>
|
|
479
|
-
<SvgIcon source={continuePromptSvgIcon} color={hoverColor(continueHover.hovered(), theme().text.action.primary.base)} fallback="↻" width={2} height={1}/>
|
|
480
|
-
<text wrapMode="none" fg={hoverColor(continueHover.hovered(), theme().text.action.primary.base)}>
|
|
481
|
-
Continue
|
|
482
|
-
</text>
|
|
483
|
-
</box>
|
|
484
|
-
</Show>
|
|
485
|
-
|
|
486
|
-
<box marginRight={!imageSupported() ? 1.5 : 0} flexDirection="row" onMouseOver={clearHover.onMouseOver} onMouseOut={clearHover.onMouseOut} onMouseDown={(e) => {
|
|
487
|
-
if (e.button === 0) {
|
|
488
|
-
e.stopPropagation();
|
|
489
|
-
void clearTodos();
|
|
490
|
-
}
|
|
491
|
-
}}>
|
|
492
|
-
<SvgIcon color={hoverColor(clearHover.hovered(), theme().text.feedback.error.base)} source={removeSvgIcon} fallback="✖" height={1} width={2}/>
|
|
493
|
-
</box>
|
|
494
|
-
</box>
|
|
495
|
-
|
|
496
|
-
<Show when={!isCollapsed()}>
|
|
497
|
-
<box flexDirection="column" paddingTop={1} paddingLeft={0.5}>
|
|
498
|
-
<TodoList rows={rows()} expandedIds={expandedIds()} activeIcon={activeIcon} onToggleRow={toggleRowNote}/>
|
|
499
|
-
</box>
|
|
500
|
-
</Show>
|
|
501
|
-
</box>)}
|
|
502
|
-
</Show>);
|
|
503
|
-
}
|
package/dist/tui.jsx
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { Plugin } from "@opencode/plugin/tui";
|
|
2
|
-
import { RendererContext } from "@opentui/solid";
|
|
3
|
-
import { TodoProgress } from "./tui/todo-panel";
|
|
4
|
-
export default Plugin.define({
|
|
5
|
-
id: "opencode.todolist.tui1",
|
|
6
|
-
setup(context) {
|
|
7
|
-
const releaseSlot = context.ui.slot({
|
|
8
|
-
append: "session.composer.top",
|
|
9
|
-
render: (props) => {
|
|
10
|
-
const directory = context.data.session.get(props.sessionID)?.location
|
|
11
|
-
.directory;
|
|
12
|
-
if (!directory)
|
|
13
|
-
return null;
|
|
14
|
-
return (<RendererContext.Provider value={context.renderer}>
|
|
15
|
-
<TodoProgress context={context} sessionID={props.sessionID}/>
|
|
16
|
-
</RendererContext.Provider>);
|
|
17
|
-
},
|
|
18
|
-
});
|
|
19
|
-
return () => {
|
|
20
|
-
releaseSlot();
|
|
21
|
-
};
|
|
22
|
-
},
|
|
23
|
-
});
|