@gamflaz/opencode-todolist 1.0.9 → 1.0.10
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/package.json +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/package.json
CHANGED
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
|
-
});
|