@gamflaz/opencode-todolist 1.0.4 → 1.0.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 +32 -31
- package/dist/icons/{capabilities.d.ts → image-capabilities.d.ts} +1 -1
- package/dist/icons/{convert.js → svg-icon.js} +1 -1
- package/dist/icons/{icons.d.ts → svg-icons.d.ts} +1 -0
- package/dist/icons/{icons.js → svg-icons.js} +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -6
- package/dist/{schema/store.schema.d.ts → schemas/platform.d.ts} +1 -0
- package/dist/{schema/store.schema.js → schemas/platform.js} +1 -0
- package/dist/{schema/status.schema.d.ts → schemas/status.d.ts} +2 -0
- package/dist/{schema/status.schema.js → schemas/status.js} +8 -0
- package/dist/{schema/todo.schema.js → schemas/todo.js} +1 -1
- package/dist/{store → storage}/errors.d.ts +1 -0
- package/dist/{store → storage}/errors.js +1 -0
- package/dist/{store/store.d.ts → storage/todo-store.d.ts} +1 -1
- package/dist/{store/store.js → storage/todo-store.js} +2 -2
- package/dist/{todo → todos}/display.d.ts +2 -2
- package/dist/{todo → todos}/display.js +1 -1
- package/dist/{todo → todos}/hint.d.ts +1 -1
- package/dist/{todo → todos}/hint.js +2 -2
- package/dist/{todo/todo.d.ts → todos/todo-tree.d.ts} +2 -2
- package/dist/{todo/todo.js → todos/todo-tree.js} +1 -1
- package/dist/{todo → todos}/update.d.ts +2 -2
- package/dist/tui/layout.d.ts +18 -0
- package/dist/tui/layout.js +22 -0
- package/dist/tui/text.d.ts +2 -0
- package/dist/tui/text.js +4 -0
- package/dist/tui/todo-panel.d.ts +77 -0
- package/dist/tui/todo-panel.js +742 -0
- package/dist/tui.d.ts +0 -97
- package/dist/tui.js +16 -776
- package/package.json +7 -2
- /package/dist/icons/{capabilities.js → image-capabilities.js} +0 -0
- /package/dist/icons/{convert.d.ts → svg-icon.d.ts} +0 -0
- /package/dist/{schema/todo.schema.d.ts → schemas/todo.d.ts} +0 -0
- /package/dist/skills/{skill.d.ts → loader.d.ts} +0 -0
- /package/dist/skills/{skill.js → loader.js} +0 -0
- /package/dist/{todo → todos}/update.js +0 -0
package/dist/tui.js
CHANGED
|
@@ -1,784 +1,24 @@
|
|
|
1
|
-
import { memo as _$memo } from "@opentui/solid";
|
|
2
|
-
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
3
|
-
import { effect as _$effect } from "@opentui/solid";
|
|
4
|
-
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
5
|
-
import { insert as _$insert } from "@opentui/solid";
|
|
6
1
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
7
|
-
import { setProp as _$setProp } from "@opentui/solid";
|
|
8
|
-
import { use as _$use } from "@opentui/solid";
|
|
9
|
-
import { createElement as _$createElement } from "@opentui/solid";
|
|
10
2
|
import { Plugin } from "@opencode/plugin/tui";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export const CONTINUE_PROMPT_MAX_CONTENT = 60;
|
|
25
|
-
export const CONTINUE_PROMPT_MAX_NOTE = 60;
|
|
26
|
-
|
|
27
|
-
/** Truncates to `max` chars, marking the cut with "...". */
|
|
28
|
-
function shorten(text, max) {
|
|
29
|
-
return text.length <= max ? text : `${text.slice(0, max - 3).trimEnd()}...`;
|
|
30
|
-
}
|
|
31
|
-
export function buildContinuePrompt(summary) {
|
|
32
|
-
const statusLabel = {
|
|
33
|
-
pending: "not started",
|
|
34
|
-
in_progress: "currently being worked on",
|
|
35
|
-
completed: "completed",
|
|
36
|
-
cancelled: "cancelled",
|
|
37
|
-
error: "needs fixing"
|
|
38
|
-
};
|
|
39
|
-
const open = summary?.items.filter(({
|
|
40
|
-
status
|
|
41
|
-
}) => status !== "completed" && status !== "cancelled") ?? [];
|
|
42
|
-
const shown = open.slice(0, CONTINUE_PROMPT_MAX_ITEMS);
|
|
43
|
-
const hidden = open.length - shown.length;
|
|
44
|
-
const lines = ["Continue the current todolist.", "First call manage_todo_list [read] and reconcile the stored list with this summary."];
|
|
45
|
-
if (summary) lines.push(`Progress: ${summary.completed}/${summary.total} completed.`);
|
|
46
|
-
if (open.length) {
|
|
47
|
-
lines.push("Open items that still need work (short summary):");
|
|
48
|
-
lines.push(...shown.map(item => `${item.depth ? " " : ""}- ${item.id} (${statusLabel[item.status]}) ${shorten(item.content, CONTINUE_PROMPT_MAX_CONTENT)}${item.note ? ` — ${shorten(item.note, CONTINUE_PROMPT_MAX_NOTE)}` : ""}`));
|
|
49
|
-
if (hidden > 0) lines.push(`... (${hidden} more open item(s))`);
|
|
50
|
-
} else {
|
|
51
|
-
lines.push("No open items are listed. Verify the stored state before stopping.");
|
|
52
|
-
}
|
|
53
|
-
lines.push("Continue the next open item immediately. Keep exactly one item in_progress and update the todo list after verification.");
|
|
54
|
-
return lines.join("\n");
|
|
55
|
-
}
|
|
56
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Maximum number of todo rows the panel shows before it starts scrolling.
|
|
60
|
-
* Keeping the panel compact protects the composer; scrolling guarantees no
|
|
61
|
-
* row is ever silently dropped.
|
|
62
|
-
*/
|
|
63
|
-
export const TODO_PANEL_MAX_ROWS = 8;
|
|
64
|
-
/** Rendered prefix of an error detail row. */
|
|
65
|
-
export const ERROR_PREFIX = "↳ error: ";
|
|
66
|
-
|
|
67
|
-
/** A renderable line: either a task row or its on-demand note row. */
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Flattens rows into renderable lines, inserting an error detail right below
|
|
71
|
-
* an expanded error row (click to toggle).
|
|
72
|
-
*/
|
|
73
|
-
export function buildTodoListEntries(rows, expandedIds) {
|
|
74
|
-
const expanded = new Set(expandedIds);
|
|
75
|
-
const entries = [];
|
|
76
|
-
for (const row of rows) {
|
|
77
|
-
entries.push({
|
|
78
|
-
kind: "row",
|
|
79
|
-
row
|
|
80
|
-
});
|
|
81
|
-
if (row.status === "error" && expanded.has(row.id)) {
|
|
82
|
-
entries.push({
|
|
83
|
-
kind: "error",
|
|
84
|
-
row
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return entries;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Follow the most advanced active row: the last `in_progress` entry is the
|
|
93
|
-
* task currently being executed. Note lines are skipped, they follow their
|
|
94
|
-
* own row.
|
|
95
|
-
*/
|
|
96
|
-
export function findActiveEntryIndex(entries) {
|
|
97
|
-
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
98
|
-
const entry = entries[index];
|
|
99
|
-
if (entry.kind === "row" && entry.row.status === "in_progress") return index;
|
|
100
|
-
}
|
|
101
|
-
return -1;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Rendered width of a row: the indentation, the icon cell and the title.
|
|
106
|
-
*
|
|
107
|
-
* Rows must be sized explicitly, otherwise the scroll content never grows
|
|
108
|
-
* wider than the viewport and horizontal scrolling has nothing to move.
|
|
109
|
-
*/
|
|
110
|
-
export function rowWidth(row) {
|
|
111
|
-
const indent = row.depth > 0 ? 2 : 0;
|
|
112
|
-
const label = row.id !== undefined ? `${TodoTree.formatIdPrefix(row.id, row.depth)}${row.content}` : row.content;
|
|
113
|
-
// icon glyph + 1 space, then the title.
|
|
114
|
-
return indent + 1 + 1 + [...label].length;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Rendered width of an expanded error line under its row. */
|
|
118
|
-
export function errorRowWidth(row) {
|
|
119
|
-
const indent = row.depth > 0 ? 4 : 2;
|
|
120
|
-
return indent + [...(ERROR_PREFIX + row.error)].length;
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Scroll offset needed to reveal the active row, or `undefined` when nothing
|
|
124
|
-
* should move (no active row, already visible, or the list fits).
|
|
125
|
-
*
|
|
126
|
-
* Kept pure so the follow behaviour is testable without a live renderer.
|
|
127
|
-
*/
|
|
128
|
-
export function nextFollowScrollTop(input) {
|
|
129
|
-
const {
|
|
130
|
-
currentTop,
|
|
131
|
-
activeIndex,
|
|
132
|
-
rowCount,
|
|
133
|
-
viewportHeight
|
|
134
|
-
} = input;
|
|
135
|
-
if (activeIndex < 0) return undefined;
|
|
136
|
-
const viewport = Math.max(1, viewportHeight);
|
|
137
|
-
const maxTop = Math.max(0, rowCount - viewport);
|
|
138
|
-
if (maxTop === 0) return undefined;
|
|
139
|
-
const activeBottom = activeIndex + 1;
|
|
140
|
-
const visibleTop = currentTop;
|
|
141
|
-
const visibleBottom = currentTop + viewport;
|
|
142
|
-
if (activeIndex >= visibleTop && activeBottom <= visibleBottom) {
|
|
143
|
-
return undefined;
|
|
144
|
-
}
|
|
145
|
-
const targetTop = activeIndex >= visibleBottom ? activeBottom - viewport : activeIndex;
|
|
146
|
-
return Math.min(Math.max(0, targetTop), maxTop);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Follow the most advanced active row: the last `in_progress` entry is the
|
|
151
|
-
* task currently being executed.
|
|
152
|
-
*/
|
|
153
|
-
export function findActiveRowIndex(rows) {
|
|
154
|
-
for (let index = rows.length - 1; index >= 0; index -= 1) {
|
|
155
|
-
if (rows[index]?.status === "in_progress") return index;
|
|
156
|
-
}
|
|
157
|
-
return -1;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Presentational, context-free todo list.
|
|
162
|
-
*
|
|
163
|
-
* The panel is deliberately chrome-free: no indicator row, and the horizontal
|
|
164
|
-
* scrollbar stays hidden. Long titles never wrap (that would break the
|
|
165
|
-
* one-line-per-row layout) — they are reached by scrolling left/right, and the
|
|
166
|
-
* vertical list scrolls when it exceeds `TODO_PANEL_MAX_ROWS`. When a row is
|
|
167
|
-
* `in_progress` the panel follows it so the active task is never out of sight.
|
|
168
|
-
*/
|
|
169
|
-
export function TodoList(props) {
|
|
170
|
-
const rowId = id => `todo-row-${id}`;
|
|
171
|
-
const [scrollYBox, setScrollYBox] = createSignal();
|
|
172
|
-
const [scrollXBox, setScrollXBox] = createSignal();
|
|
173
|
-
const entries = createMemo(() => buildTodoListEntries(props.rows, props.expandedIds ?? []));
|
|
174
|
-
const followActiveRow = box => {
|
|
175
|
-
if (box.isDestroyed) return;
|
|
176
|
-
const target = nextFollowScrollTop({
|
|
177
|
-
currentTop: box.scrollTop,
|
|
178
|
-
activeIndex: findActiveEntryIndex(entries()),
|
|
179
|
-
rowCount: entries().length,
|
|
180
|
-
viewportHeight: box.viewport.height
|
|
181
|
-
});
|
|
182
|
-
if (target === undefined) return;
|
|
183
|
-
box.scrollTo(target);
|
|
184
|
-
};
|
|
185
|
-
const attach = box => {
|
|
186
|
-
setScrollYBox(box);
|
|
187
|
-
box.content.maxWidth = undefined;
|
|
188
|
-
const internalOnSizeChange = box.content.onSizeChange;
|
|
189
|
-
box.content.onSizeChange = () => {
|
|
190
|
-
internalOnSizeChange?.call(box.content);
|
|
191
|
-
followActiveRow(box);
|
|
192
|
-
};
|
|
193
|
-
props.ref?.(box);
|
|
194
|
-
};
|
|
195
|
-
createEffect(() => {
|
|
196
|
-
void props.rows.map(row => `${row.id}:${row.status}`).join("|");
|
|
197
|
-
void props.expandedIds?.join("|");
|
|
198
|
-
const box = scrollYBox();
|
|
199
|
-
if (box) followActiveRow(box);
|
|
200
|
-
});
|
|
201
|
-
const scrollbarOptions = () => props.scrollbarColor ? {
|
|
202
|
-
trackOptions: {
|
|
203
|
-
backgroundColor: props.scrollbarColor,
|
|
204
|
-
foregroundColor: props.scrollbarColor
|
|
205
|
-
}
|
|
206
|
-
} : undefined;
|
|
207
|
-
const viewportHeight = () => Math.min(entries().length, TODO_PANEL_MAX_ROWS);
|
|
208
|
-
const toggle = id => {
|
|
209
|
-
props.onToggleRow?.(id);
|
|
210
|
-
};
|
|
211
|
-
return (() => {
|
|
212
|
-
var _el$ = _$createElement("scrollbox"),
|
|
213
|
-
_el$2 = _$createElement("box");
|
|
214
|
-
_$insertNode(_el$, _el$2);
|
|
215
|
-
_$use(el => attach(el), _el$);
|
|
216
|
-
_$setProp(_el$, "width", "100%");
|
|
217
|
-
_$setProp(_el$, "horizontalScrollbarOptions", {
|
|
218
|
-
visible: false
|
|
219
|
-
});
|
|
220
|
-
_$setProp(_el$2, "flexDirection", "column");
|
|
221
|
-
_$setProp(_el$2, "flexShrink", 0);
|
|
222
|
-
_$insert(_el$2, _$createComponent(For, {
|
|
223
|
-
get each() {
|
|
224
|
-
return entries();
|
|
225
|
-
},
|
|
226
|
-
children: entry => entry.kind === "row" ? (() => {
|
|
227
|
-
var _el$3 = _$createElement("box"),
|
|
228
|
-
_el$4 = _$createElement("text"),
|
|
229
|
-
_el$7 = _$createElement("span"),
|
|
230
|
-
_el$8 = _$createTextNode(` `),
|
|
231
|
-
_el$9 = _$createElement("span"),
|
|
232
|
-
_el$0 = _$createElement("span");
|
|
233
|
-
_$insertNode(_el$3, _el$4);
|
|
234
|
-
_$setProp(_el$3, "flexDirection", "row");
|
|
235
|
-
_$setProp(_el$3, "height", 1);
|
|
236
|
-
_$setProp(_el$3, "flexShrink", 0);
|
|
237
|
-
_$setProp(_el$3, "onMouseDown", e => {
|
|
238
|
-
if (e.button === 0 && entry.row.status === "error") {
|
|
239
|
-
e.stopPropagation?.();
|
|
240
|
-
toggle(entry.row.id);
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
_$insertNode(_el$4, _el$7);
|
|
244
|
-
_$insertNode(_el$4, _el$9);
|
|
245
|
-
_$insertNode(_el$4, _el$0);
|
|
246
|
-
_$setProp(_el$4, "wrapMode", "none");
|
|
247
|
-
_$insert(_el$4, _$createComponent(Show, {
|
|
248
|
-
get when() {
|
|
249
|
-
return entry.row.depth > 0;
|
|
250
|
-
},
|
|
251
|
-
get children() {
|
|
252
|
-
var _el$5 = _$createElement("span");
|
|
253
|
-
_$insertNode(_el$5, _$createTextNode(` `));
|
|
254
|
-
return _el$5;
|
|
255
|
-
}
|
|
256
|
-
}), _el$7);
|
|
257
|
-
_$insertNode(_el$7, _el$8);
|
|
258
|
-
_$insert(_el$7, () => entry.row.icon, _el$8);
|
|
259
|
-
_$insert(_el$9, () => TodoTree.formatIdPrefix(entry.row.id, entry.row.depth));
|
|
260
|
-
_$insert(_el$0, () => entry.row.content);
|
|
261
|
-
_$insert(_el$4, _$createComponent(Show, {
|
|
262
|
-
get when() {
|
|
263
|
-
return entry.row.status === "error";
|
|
264
|
-
},
|
|
265
|
-
get children() {
|
|
266
|
-
var _el$1 = _$createElement("span"),
|
|
267
|
-
_el$10 = _$createTextNode(` `);
|
|
268
|
-
_$insertNode(_el$1, _el$10);
|
|
269
|
-
_$insert(_el$1, () => (props.expandedIds ?? []).includes(entry.row.id) ? "⌄" : "›", null);
|
|
270
|
-
_$effect(_$p => _$setProp(_el$1, "style", {
|
|
271
|
-
fg: props.scrollbarColor
|
|
272
|
-
}, _$p));
|
|
273
|
-
return _el$1;
|
|
274
|
-
}
|
|
275
|
-
}), null);
|
|
276
|
-
_$effect(_p$ => {
|
|
277
|
-
var _v$3 = rowId(entry.row.id),
|
|
278
|
-
_v$4 = rowWidth(entry.row),
|
|
279
|
-
_v$5 = {
|
|
280
|
-
fg: entry.row.iconColor
|
|
281
|
-
},
|
|
282
|
-
_v$6 = {
|
|
283
|
-
fg: entry.row.textColor
|
|
284
|
-
},
|
|
285
|
-
_v$7 = {
|
|
286
|
-
fg: entry.row.textColor,
|
|
287
|
-
strikethrough: entry.row.status === "completed"
|
|
288
|
-
};
|
|
289
|
-
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$3, "id", _v$3, _p$.e));
|
|
290
|
-
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$3, "width", _v$4, _p$.t));
|
|
291
|
-
_v$5 !== _p$.a && (_p$.a = _$setProp(_el$7, "style", _v$5, _p$.a));
|
|
292
|
-
_v$6 !== _p$.o && (_p$.o = _$setProp(_el$9, "style", _v$6, _p$.o));
|
|
293
|
-
_v$7 !== _p$.i && (_p$.i = _$setProp(_el$0, "style", _v$7, _p$.i));
|
|
294
|
-
return _p$;
|
|
295
|
-
}, {
|
|
296
|
-
e: undefined,
|
|
297
|
-
t: undefined,
|
|
298
|
-
a: undefined,
|
|
299
|
-
o: undefined,
|
|
300
|
-
i: undefined
|
|
301
|
-
});
|
|
302
|
-
return _el$3;
|
|
303
|
-
})() : (() => {
|
|
304
|
-
var _el$11 = _$createElement("box"),
|
|
305
|
-
_el$12 = _$createElement("text"),
|
|
306
|
-
_el$13 = _$createElement("span"),
|
|
307
|
-
_el$14 = _$createTextNode(`↳ error: `),
|
|
308
|
-
_el$15 = _$createElement("span");
|
|
309
|
-
_$insertNode(_el$11, _el$12);
|
|
310
|
-
_$setProp(_el$11, "flexDirection", "row");
|
|
311
|
-
_$setProp(_el$11, "height", 1);
|
|
312
|
-
_$setProp(_el$11, "flexShrink", 0);
|
|
313
|
-
_$setProp(_el$11, "onMouseDown", e => {
|
|
314
|
-
if (e.button === 0) {
|
|
315
|
-
e.stopPropagation?.();
|
|
316
|
-
toggle(entry.row.id);
|
|
3
|
+
import { TodoProgress } from "./tui/todo-panel";
|
|
4
|
+
export default Plugin.define({
|
|
5
|
+
id: "opencode.todolist.tui",
|
|
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.directory;
|
|
11
|
+
if (!directory) return null;
|
|
12
|
+
return _$createComponent(TodoProgress, {
|
|
13
|
+
context: context,
|
|
14
|
+
get sessionID() {
|
|
15
|
+
return props.sessionID;
|
|
317
16
|
}
|
|
318
17
|
});
|
|
319
|
-
_$insertNode(_el$12, _el$13);
|
|
320
|
-
_$insertNode(_el$12, _el$15);
|
|
321
|
-
_$setProp(_el$12, "wrapMode", "none");
|
|
322
|
-
_$insertNode(_el$13, _el$14);
|
|
323
|
-
_$insert(_el$13, () => entry.row.depth > 0 ? " " : " ", _el$14);
|
|
324
|
-
_$insert(_el$15, () => entry.row.content);
|
|
325
|
-
_$effect(_p$ => {
|
|
326
|
-
var _v$8 = `${rowId(entry.row.id)}-note`,
|
|
327
|
-
_v$9 = errorRowWidth({
|
|
328
|
-
error: entry.row.content,
|
|
329
|
-
depth: entry.row.depth
|
|
330
|
-
}),
|
|
331
|
-
_v$0 = {
|
|
332
|
-
fg: props.scrollbarColor
|
|
333
|
-
},
|
|
334
|
-
_v$1 = {
|
|
335
|
-
fg: entry.row.textColor
|
|
336
|
-
};
|
|
337
|
-
_v$8 !== _p$.e && (_p$.e = _$setProp(_el$11, "id", _v$8, _p$.e));
|
|
338
|
-
_v$9 !== _p$.t && (_p$.t = _$setProp(_el$11, "width", _v$9, _p$.t));
|
|
339
|
-
_v$0 !== _p$.a && (_p$.a = _$setProp(_el$13, "style", _v$0, _p$.a));
|
|
340
|
-
_v$1 !== _p$.o && (_p$.o = _$setProp(_el$15, "style", _v$1, _p$.o));
|
|
341
|
-
return _p$;
|
|
342
|
-
}, {
|
|
343
|
-
e: undefined,
|
|
344
|
-
t: undefined,
|
|
345
|
-
a: undefined,
|
|
346
|
-
o: undefined
|
|
347
|
-
});
|
|
348
|
-
return _el$11;
|
|
349
|
-
})()
|
|
350
|
-
}));
|
|
351
|
-
_$effect(_p$ => {
|
|
352
|
-
var _v$ = viewportHeight(),
|
|
353
|
-
_v$2 = scrollbarOptions();
|
|
354
|
-
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "height", _v$, _p$.e));
|
|
355
|
-
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "verticalScrollbarOptions", _v$2, _p$.t));
|
|
356
|
-
return _p$;
|
|
357
|
-
}, {
|
|
358
|
-
e: undefined,
|
|
359
|
-
t: undefined
|
|
360
|
-
});
|
|
361
|
-
return _el$;
|
|
362
|
-
})();
|
|
363
|
-
}
|
|
364
|
-
function TodoProgress(props) {
|
|
365
|
-
const [refresh, setRefresh] = createSignal(0);
|
|
366
|
-
|
|
367
|
-
// Hover
|
|
368
|
-
const [isClearHovered, setIsClearHovered] = createSignal(false);
|
|
369
|
-
const [isContinueHovered, setIsContinueHovered] = createSignal(false);
|
|
370
|
-
const [isCollapsed, setIsCollapsed] = createSignal(true);
|
|
371
|
-
const [isContinuePending, setIsContinuePending] = createSignal(false);
|
|
372
|
-
const [animFrame, setAnimFrame] = createSignal(0);
|
|
373
|
-
const [expandedIds, setExpandedIds] = createSignal([]);
|
|
374
|
-
let timer;
|
|
375
|
-
let watcher;
|
|
376
|
-
let animTimer;
|
|
377
|
-
const [imageSupported, setImageSupported] = createSignal(supportsImageRendering(props.context.renderer?.capabilities));
|
|
378
|
-
const updateCapabilities = caps => {
|
|
379
|
-
setImageSupported(supportsImageRendering(caps));
|
|
380
|
-
};
|
|
381
|
-
props.context.renderer?.on?.("capabilities", updateCapabilities);
|
|
382
|
-
onCleanup(() => {
|
|
383
|
-
props.context.renderer?.off?.("capabilities", updateCapabilities);
|
|
384
|
-
});
|
|
385
|
-
const getSessionID = () => props.sessionID ?? "";
|
|
386
|
-
const checkRunning = () => {
|
|
387
|
-
const sid = getSessionID();
|
|
388
|
-
if (!sid) return false;
|
|
389
|
-
try {
|
|
390
|
-
return props.context.data.session.status(sid) === "running";
|
|
391
|
-
} catch {
|
|
392
|
-
return false;
|
|
393
|
-
}
|
|
394
|
-
};
|
|
395
|
-
const [isRunning, setIsRunning] = createSignal(checkRunning());
|
|
396
|
-
const unsubscribe = props.context.data.listen(({
|
|
397
|
-
details
|
|
398
|
-
}) => {
|
|
399
|
-
const sid = getSessionID();
|
|
400
|
-
switch (details.type) {
|
|
401
|
-
case "session.execution.started":
|
|
402
|
-
case "session.execution.interrupted":
|
|
403
|
-
case "session.execution.succeeded":
|
|
404
|
-
case "session.execution.failed":
|
|
405
|
-
case "session.idle":
|
|
406
|
-
if (!sid || details.data.sessionID === sid) {
|
|
407
|
-
setIsRunning(details.type === "session.execution.started");
|
|
408
|
-
setIsContinuePending(false);
|
|
409
|
-
}
|
|
410
|
-
break;
|
|
411
|
-
case "session.status":
|
|
412
|
-
if (!sid || details.data.sessionID === sid) {
|
|
413
|
-
setIsRunning(details.data.status.type !== "idle");
|
|
414
|
-
setIsContinuePending(false);
|
|
415
|
-
}
|
|
416
|
-
break;
|
|
417
|
-
}
|
|
418
|
-
});
|
|
419
|
-
createEffect(() => {
|
|
420
|
-
const sid = getSessionID();
|
|
421
|
-
if (!sid) {
|
|
422
|
-
setIsRunning(false);
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
try {
|
|
426
|
-
setIsRunning(props.context.data.session.status(sid) === "running");
|
|
427
|
-
} catch {
|
|
428
|
-
setIsRunning(false);
|
|
429
|
-
}
|
|
430
|
-
});
|
|
431
|
-
const startAnimation = () => {
|
|
432
|
-
if (animTimer) return;
|
|
433
|
-
animTimer = setInterval(() => {
|
|
434
|
-
setAnimFrame(prev => (prev + 1) % SPINNER_FRAMES.length);
|
|
435
|
-
}, 100);
|
|
436
|
-
};
|
|
437
|
-
const stopAnimation = () => {
|
|
438
|
-
if (animTimer) {
|
|
439
|
-
clearInterval(animTimer);
|
|
440
|
-
animTimer = undefined;
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
try {
|
|
444
|
-
const baseDir = getTodosDirectory();
|
|
445
|
-
mkdirSync(baseDir, {
|
|
446
|
-
recursive: true
|
|
447
|
-
});
|
|
448
|
-
watcher = watch(baseDir, {
|
|
449
|
-
recursive: true,
|
|
450
|
-
persistent: false
|
|
451
|
-
}, (_, filename) => {
|
|
452
|
-
const sid = getSessionID();
|
|
453
|
-
if (!filename) {
|
|
454
|
-
clearTimeout(timer);
|
|
455
|
-
timer = setTimeout(() => setRefresh(value => value + 1), 50);
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
const nameStr = String(filename);
|
|
459
|
-
let hashedSid = "";
|
|
460
|
-
try {
|
|
461
|
-
if (sid) hashedSid = hashSessionID(sid);
|
|
462
|
-
} catch {}
|
|
463
|
-
if (!sid || hashedSid && nameStr.startsWith(hashedSid) || nameStr.startsWith(sid) || nameStr.startsWith(`todos-${sid}-`)) {
|
|
464
|
-
clearTimeout(timer);
|
|
465
|
-
timer = setTimeout(() => setRefresh(value => value + 1), 50);
|
|
466
18
|
}
|
|
467
19
|
});
|
|
468
|
-
|
|
469
|
-
|
|
20
|
+
return () => {
|
|
21
|
+
releaseSlot();
|
|
22
|
+
};
|
|
470
23
|
}
|
|
471
|
-
onCleanup(() => {
|
|
472
|
-
clearTimeout(timer);
|
|
473
|
-
stopAnimation();
|
|
474
|
-
watcher?.close();
|
|
475
|
-
try {
|
|
476
|
-
unsubscribe();
|
|
477
|
-
} catch {}
|
|
478
|
-
});
|
|
479
|
-
const summary = createMemo(() => {
|
|
480
|
-
refresh();
|
|
481
|
-
const sid = getSessionID();
|
|
482
|
-
if (!sid) return null;
|
|
483
|
-
try {
|
|
484
|
-
return getTodoDisplaySummary(readTodosSync(sid));
|
|
485
|
-
} catch {
|
|
486
|
-
return null;
|
|
487
|
-
}
|
|
488
|
-
});
|
|
489
|
-
createEffect(() => {
|
|
490
|
-
const hasInProgress = summary()?.items.some(i => i.status === "in_progress");
|
|
491
|
-
if (hasInProgress && isRunning()) {
|
|
492
|
-
startAnimation();
|
|
493
|
-
} else {
|
|
494
|
-
stopAnimation();
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
const activeItem = createMemo(() => {
|
|
498
|
-
const d = summary();
|
|
499
|
-
if (!d) return null;
|
|
500
|
-
return d.items.find(i => i.status === "in_progress" && i.depth > 0) ?? d.items.find(i => i.status === "in_progress") ?? null;
|
|
501
|
-
});
|
|
502
|
-
const toggleRowNote = id => {
|
|
503
|
-
setExpandedIds(current => current.includes(id) ? current.filter(entry => entry !== id) : [...current, id]);
|
|
504
|
-
};
|
|
505
|
-
const continueTodos = async () => {
|
|
506
|
-
if (isRunning() || isContinuePending()) return;
|
|
507
|
-
const sid = getSessionID();
|
|
508
|
-
if (!sid) return;
|
|
509
|
-
setIsContinuePending(true);
|
|
510
|
-
try {
|
|
511
|
-
await props.context.client.session.prompt({
|
|
512
|
-
sessionID: sid,
|
|
513
|
-
text: buildContinuePrompt(summary())
|
|
514
|
-
});
|
|
515
|
-
} catch (error) {
|
|
516
|
-
setIsContinuePending(false);
|
|
517
|
-
console.error("[todos] continue failed:", error);
|
|
518
|
-
}
|
|
519
|
-
};
|
|
520
|
-
const getItemIcon = (status, fallbackIcon) => {
|
|
521
|
-
if (status === "in_progress") {
|
|
522
|
-
if (isRunning()) {
|
|
523
|
-
return SPINNER_FRAMES[animFrame()];
|
|
524
|
-
}
|
|
525
|
-
return "-";
|
|
526
|
-
}
|
|
527
|
-
return fallbackIcon;
|
|
528
|
-
};
|
|
529
|
-
const theme = () => props.context.theme;
|
|
530
|
-
const getStatusColor = status => {
|
|
531
|
-
const t = theme();
|
|
532
|
-
switch (status) {
|
|
533
|
-
case "in_progress":
|
|
534
|
-
return isRunning() ? t.text.action?.primary?.base ?? t.text.feedback.info?.base ?? t.text.base : t.text.muted;
|
|
535
|
-
case "completed":
|
|
536
|
-
return t.text.feedback.success.base;
|
|
537
|
-
case "error":
|
|
538
|
-
return t.text.feedback.error.base;
|
|
539
|
-
case "cancelled":
|
|
540
|
-
return t.text.feedback.error.base;
|
|
541
|
-
case "pending":
|
|
542
|
-
default:
|
|
543
|
-
return t.text.muted;
|
|
544
|
-
}
|
|
545
|
-
};
|
|
546
|
-
const getTextColor = status => {
|
|
547
|
-
const t = theme();
|
|
548
|
-
if (status === "completed" || status === "cancelled" || status === "error") {
|
|
549
|
-
return t.text.muted;
|
|
550
|
-
}
|
|
551
|
-
return t.text.base;
|
|
552
|
-
};
|
|
553
|
-
const getBorderColor = () => {
|
|
554
|
-
return theme().border.base;
|
|
555
|
-
};
|
|
556
|
-
const a = "";
|
|
557
|
-
return _$createComponent(Show, {
|
|
558
|
-
get when() {
|
|
559
|
-
return summary();
|
|
560
|
-
},
|
|
561
|
-
children: data => (() => {
|
|
562
|
-
var _el$16 = _$createElement("box"),
|
|
563
|
-
_el$17 = _$createElement("box"),
|
|
564
|
-
_el$18 = _$createElement("box"),
|
|
565
|
-
_el$19 = _$createElement("box"),
|
|
566
|
-
_el$20 = _$createElement("box"),
|
|
567
|
-
_el$21 = _$createElement("text"),
|
|
568
|
-
_el$22 = _$createElement("span"),
|
|
569
|
-
_el$24 = _$createElement("span"),
|
|
570
|
-
_el$28 = _$createElement("box");
|
|
571
|
-
_$insertNode(_el$16, _el$17);
|
|
572
|
-
_$setProp(_el$16, "flexDirection", "column");
|
|
573
|
-
_$setProp(_el$16, "border", true);
|
|
574
|
-
_$setProp(_el$16, "borderStyle", "rounded");
|
|
575
|
-
_$setProp(_el$16, "paddingLeft", 1);
|
|
576
|
-
_$setProp(_el$16, "paddingRight", 1);
|
|
577
|
-
_$insertNode(_el$17, _el$18);
|
|
578
|
-
_$insertNode(_el$17, _el$28);
|
|
579
|
-
_$setProp(_el$17, "flexDirection", "row");
|
|
580
|
-
_$setProp(_el$17, "justifyContent", "space-between");
|
|
581
|
-
_$insertNode(_el$18, _el$19);
|
|
582
|
-
_$insertNode(_el$18, _el$20);
|
|
583
|
-
_$setProp(_el$18, "flexDirection", "row");
|
|
584
|
-
_$setProp(_el$18, "flexGrow", 1);
|
|
585
|
-
_$setProp(_el$18, "onMouseDown", e => {
|
|
586
|
-
if (e.button === 0) {
|
|
587
|
-
e.stopPropagation?.();
|
|
588
|
-
setIsCollapsed(!isCollapsed());
|
|
589
|
-
}
|
|
590
|
-
});
|
|
591
|
-
_$setProp(_el$19, "flexDirection", "row");
|
|
592
|
-
_$setProp(_el$19, "width", 2);
|
|
593
|
-
_$setProp(_el$19, "height", 1);
|
|
594
|
-
_$setProp(_el$19, "marginRight", 1);
|
|
595
|
-
_$setProp(_el$19, "alignItems", "flex-start");
|
|
596
|
-
_$insert(_el$19, (() => {
|
|
597
|
-
var _c$ = _$memo(() => !!isCollapsed());
|
|
598
|
-
return () => _c$() ? _$createComponent(SvgIcon, {
|
|
599
|
-
source: arrowRightSvgIcon,
|
|
600
|
-
get color() {
|
|
601
|
-
return theme().text.muted;
|
|
602
|
-
},
|
|
603
|
-
fallback: "\u25B6",
|
|
604
|
-
width: 2,
|
|
605
|
-
height: 1
|
|
606
|
-
}) : _$createComponent(SvgIcon, {
|
|
607
|
-
source: arrowDownSvgIcon,
|
|
608
|
-
get color() {
|
|
609
|
-
return theme().text.muted;
|
|
610
|
-
},
|
|
611
|
-
fallback: "\u25BC",
|
|
612
|
-
width: 2,
|
|
613
|
-
height: 1
|
|
614
|
-
});
|
|
615
|
-
})());
|
|
616
|
-
_$insertNode(_el$20, _el$21);
|
|
617
|
-
_$setProp(_el$20, "flexDirection", "row");
|
|
618
|
-
_$setProp(_el$20, "flexGrow", 1);
|
|
619
|
-
_$setProp(_el$20, "flexShrink", 1);
|
|
620
|
-
_$setProp(_el$20, "overflow", "hidden");
|
|
621
|
-
_$setProp(_el$20, "marginRight", 1);
|
|
622
|
-
_$insertNode(_el$21, _el$22);
|
|
623
|
-
_$insertNode(_el$21, _el$24);
|
|
624
|
-
_$setProp(_el$21, "wrapMode", "none");
|
|
625
|
-
_$insertNode(_el$22, _$createTextNode(`Todos `));
|
|
626
|
-
_$insert(_el$24, () => `(${data().completed}/${data().total})`);
|
|
627
|
-
_$insert(_el$21, _$createComponent(Show, {
|
|
628
|
-
get when() {
|
|
629
|
-
return _$memo(() => !!isCollapsed())() && activeItem();
|
|
630
|
-
},
|
|
631
|
-
children: active => [(() => {
|
|
632
|
-
var _el$30 = _$createElement("span");
|
|
633
|
-
_$insertNode(_el$30, _$createTextNode(` `));
|
|
634
|
-
_$effect(_$p => _$setProp(_el$30, "style", {
|
|
635
|
-
fg: theme().text.muted
|
|
636
|
-
}, _$p));
|
|
637
|
-
return _el$30;
|
|
638
|
-
})(), (() => {
|
|
639
|
-
var _el$32 = _$createElement("span"),
|
|
640
|
-
_el$33 = _$createTextNode(` `);
|
|
641
|
-
_$insertNode(_el$32, _el$33);
|
|
642
|
-
_$insert(_el$32, () => getItemIcon(active().status, active().icon), _el$33);
|
|
643
|
-
_$effect(_$p => _$setProp(_el$32, "style", {
|
|
644
|
-
fg: getStatusColor(active().status)
|
|
645
|
-
}, _$p));
|
|
646
|
-
return _el$32;
|
|
647
|
-
})(), (() => {
|
|
648
|
-
var _el$34 = _$createElement("span");
|
|
649
|
-
_$insert(_el$34, () => TodoTree.formatIdPrefix(active().id, active().depth), null);
|
|
650
|
-
_$insert(_el$34, () => active().content, null);
|
|
651
|
-
_$effect(_$p => _$setProp(_el$34, "style", {
|
|
652
|
-
fg: getTextColor(active().status)
|
|
653
|
-
}, _$p));
|
|
654
|
-
return _el$34;
|
|
655
|
-
})()]
|
|
656
|
-
}), null);
|
|
657
|
-
_$insert(_el$17, _$createComponent(Show, {
|
|
658
|
-
get when() {
|
|
659
|
-
return _$memo(() => !!(hasOpenTodos(data().items) && !isRunning()))() && !isContinuePending();
|
|
660
|
-
},
|
|
661
|
-
get children() {
|
|
662
|
-
var _el$25 = _$createElement("box"),
|
|
663
|
-
_el$26 = _$createElement("text");
|
|
664
|
-
_$insertNode(_el$25, _el$26);
|
|
665
|
-
_$setProp(_el$25, "flexDirection", "row");
|
|
666
|
-
_$setProp(_el$25, "gap", 1);
|
|
667
|
-
_$setProp(_el$25, "marginRight", 3);
|
|
668
|
-
_$setProp(_el$25, "onMouseOver", () => setIsContinueHovered(true));
|
|
669
|
-
_$setProp(_el$25, "onMouseOut", () => setIsContinueHovered(false));
|
|
670
|
-
_$setProp(_el$25, "onMouseDown", e => {
|
|
671
|
-
if (e.button === 0) {
|
|
672
|
-
e.stopPropagation();
|
|
673
|
-
void continueTodos();
|
|
674
|
-
}
|
|
675
|
-
});
|
|
676
|
-
_$insert(_el$25, _$createComponent(SvgIcon, {
|
|
677
|
-
source: continuePromptSvgIcon,
|
|
678
|
-
get color() {
|
|
679
|
-
return _$memo(() => !!isContinueHovered())() ? theme().text.action.primary.base : theme().text.muted;
|
|
680
|
-
},
|
|
681
|
-
fallback: "\u21BB",
|
|
682
|
-
width: 2,
|
|
683
|
-
height: 1
|
|
684
|
-
}), _el$26);
|
|
685
|
-
_$insertNode(_el$26, _$createTextNode(`Continue`));
|
|
686
|
-
_$setProp(_el$26, "wrapMode", "none");
|
|
687
|
-
_$effect(_$p => _$setProp(_el$26, "fg", isContinueHovered() ? theme().text.action.primary.base : theme().text.muted, _$p));
|
|
688
|
-
return _el$25;
|
|
689
|
-
}
|
|
690
|
-
}), _el$28);
|
|
691
|
-
_$setProp(_el$28, "flexDirection", "row");
|
|
692
|
-
_$setProp(_el$28, "onMouseOver", () => setIsClearHovered(true));
|
|
693
|
-
_$setProp(_el$28, "onMouseOut", () => setIsClearHovered(false));
|
|
694
|
-
_$setProp(_el$28, "onMouseDown", async e => {
|
|
695
|
-
if (e.button === 0) {
|
|
696
|
-
e.stopPropagation();
|
|
697
|
-
const sid = getSessionID();
|
|
698
|
-
if (sid) {
|
|
699
|
-
try {
|
|
700
|
-
await writeTodos(sid, []);
|
|
701
|
-
setRefresh(value => value + 1);
|
|
702
|
-
} catch (error) {
|
|
703
|
-
console.error("[todos] clear failed:", error);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
});
|
|
708
|
-
_$insert(_el$28, _$createComponent(SvgIcon, {
|
|
709
|
-
get color() {
|
|
710
|
-
return _$memo(() => !!isClearHovered())() ? theme().text.feedback.error.base : theme().text.muted;
|
|
711
|
-
},
|
|
712
|
-
source: removeSvgIcon,
|
|
713
|
-
fallback: "\u2716",
|
|
714
|
-
height: 1,
|
|
715
|
-
width: 2
|
|
716
|
-
}));
|
|
717
|
-
_$insert(_el$16, _$createComponent(Show, {
|
|
718
|
-
get when() {
|
|
719
|
-
return !isCollapsed();
|
|
720
|
-
},
|
|
721
|
-
get children() {
|
|
722
|
-
var _el$29 = _$createElement("box");
|
|
723
|
-
_$setProp(_el$29, "flexDirection", "column");
|
|
724
|
-
_$setProp(_el$29, "paddingTop", 1);
|
|
725
|
-
_$setProp(_el$29, "paddingLeft", 0.5);
|
|
726
|
-
_$insert(_el$29, _$createComponent(TodoList, {
|
|
727
|
-
get rows() {
|
|
728
|
-
return data().items.map(item => ({
|
|
729
|
-
id: item.id,
|
|
730
|
-
content: item.content,
|
|
731
|
-
icon: getItemIcon(item.status, item.icon),
|
|
732
|
-
iconColor: getStatusColor(item.status),
|
|
733
|
-
textColor: getTextColor(item.status),
|
|
734
|
-
depth: item.depth,
|
|
735
|
-
status: item.status
|
|
736
|
-
}));
|
|
737
|
-
},
|
|
738
|
-
get expandedIds() {
|
|
739
|
-
return expandedIds();
|
|
740
|
-
},
|
|
741
|
-
onToggleRow: toggleRowNote
|
|
742
|
-
}));
|
|
743
|
-
return _el$29;
|
|
744
|
-
}
|
|
745
|
-
}), null);
|
|
746
|
-
_$effect(_p$ => {
|
|
747
|
-
var _v$10 = getBorderColor(),
|
|
748
|
-
_v$11 = imageSupported() ? 0 : 1,
|
|
749
|
-
_v$12 = {
|
|
750
|
-
fg: theme().text.base
|
|
751
|
-
},
|
|
752
|
-
_v$13 = {
|
|
753
|
-
fg: theme().text.muted
|
|
754
|
-
},
|
|
755
|
-
_v$14 = !imageSupported() ? 1.5 : 0;
|
|
756
|
-
_v$10 !== _p$.e && (_p$.e = _$setProp(_el$16, "borderColor", _v$10, _p$.e));
|
|
757
|
-
_v$11 !== _p$.t && (_p$.t = _$setProp(_el$19, "marginLeft", _v$11, _p$.t));
|
|
758
|
-
_v$12 !== _p$.a && (_p$.a = _$setProp(_el$22, "style", _v$12, _p$.a));
|
|
759
|
-
_v$13 !== _p$.o && (_p$.o = _$setProp(_el$24, "style", _v$13, _p$.o));
|
|
760
|
-
_v$14 !== _p$.i && (_p$.i = _$setProp(_el$28, "marginRight", _v$14, _p$.i));
|
|
761
|
-
return _p$;
|
|
762
|
-
}, {
|
|
763
|
-
e: undefined,
|
|
764
|
-
t: undefined,
|
|
765
|
-
a: undefined,
|
|
766
|
-
o: undefined,
|
|
767
|
-
i: undefined
|
|
768
|
-
});
|
|
769
|
-
return _el$16;
|
|
770
|
-
})()
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
|
-
export default Plugin.define({
|
|
774
|
-
id: "opencode.todolist.tui",
|
|
775
|
-
setup: context => context.ui.slot({
|
|
776
|
-
after: "session.composer.top",
|
|
777
|
-
render: input => _$createComponent(TodoProgress, {
|
|
778
|
-
context: context,
|
|
779
|
-
get sessionID() {
|
|
780
|
-
return input.sessionID;
|
|
781
|
-
}
|
|
782
|
-
})
|
|
783
|
-
})
|
|
784
24
|
});
|