@mulmoclaude/todo-plugin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/Preview.vue.d.ts +8 -0
  2. package/dist/View.vue.d.ts +12 -0
  3. package/dist/composables/index.d.ts +2 -0
  4. package/dist/composables/useTodos.d.ts +26 -0
  5. package/dist/composables.js +101 -0
  6. package/dist/composables.js.map +1 -0
  7. package/dist/definition-mymex4HE.js +55 -0
  8. package/dist/definition-mymex4HE.js.map +1 -0
  9. package/dist/definition.d.ts +43 -0
  10. package/dist/handlers/columns.d.ts +31 -0
  11. package/dist/handlers/items.d.ts +36 -0
  12. package/dist/handlers/llm.d.ts +30 -0
  13. package/dist/handlers/priority-notifier.d.ts +55 -0
  14. package/dist/index.d.ts +71 -0
  15. package/dist/index.js +1223 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/internal/utils.d.ts +4 -0
  18. package/dist/io.d.ts +6 -0
  19. package/dist/labels-C4z7FMoE.js +85 -0
  20. package/dist/labels-C4z7FMoE.js.map +1 -0
  21. package/dist/labels.d.ts +15 -0
  22. package/dist/lang/de.d.ts +25 -0
  23. package/dist/lang/en.d.ts +25 -0
  24. package/dist/lang/es.d.ts +25 -0
  25. package/dist/lang/fr.d.ts +25 -0
  26. package/dist/lang/index.d.ts +28 -0
  27. package/dist/lang/ja.d.ts +25 -0
  28. package/dist/lang/ko.d.ts +25 -0
  29. package/dist/lang/pt-BR.d.ts +25 -0
  30. package/dist/lang/zh.d.ts +25 -0
  31. package/dist/lang-D72AIF9U.js +215 -0
  32. package/dist/lang-D72AIF9U.js.map +1 -0
  33. package/dist/priority.d.ts +10 -0
  34. package/dist/shared.d.ts +13 -0
  35. package/dist/shared.js +89 -0
  36. package/dist/shared.js.map +1 -0
  37. package/dist/types.d.ts +22 -0
  38. package/dist/viewModes.d.ts +11 -0
  39. package/dist/vue.d.ts +59 -0
  40. package/dist/vue.js +422 -0
  41. package/dist/vue.js.map +1 -0
  42. package/package.json +48 -0
package/dist/vue.js ADDED
@@ -0,0 +1,422 @@
1
+ import { t as TOOL_DEFINITION } from "./definition-mymex4HE.js";
2
+ import { a as listLabelsWithCount, n as colorForLabel, r as filterByLabels } from "./labels-C4z7FMoE.js";
3
+ import { n as useT, t as format } from "./lang-D72AIF9U.js";
4
+ import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, defineComponent, normalizeClass, onMounted, onUnmounted, openBlock, ref, renderList, toDisplayString, unref, vModelText, watch, withDirectives, withKeys, withModifiers } from "vue";
5
+ import { useRuntime } from "gui-chat-protocol/vue";
6
+ //#region src/View.vue?vue&type=script&setup=true&lang.ts
7
+ var _hoisted_1$1 = { class: "h-full bg-white flex flex-col" };
8
+ var _hoisted_2$1 = {
9
+ key: 0,
10
+ class: "px-4 py-2 bg-red-50 border-b border-red-200 text-sm text-red-700",
11
+ role: "alert",
12
+ "data-testid": "todo-api-error"
13
+ };
14
+ var _hoisted_3$1 = { class: "flex items-center justify-between px-6 py-4 border-b border-gray-100" };
15
+ var _hoisted_4$1 = { class: "text-lg font-semibold text-gray-800" };
16
+ var _hoisted_5$1 = { class: "text-sm text-gray-500" };
17
+ var _hoisted_6$1 = {
18
+ key: 1,
19
+ class: "flex flex-wrap items-center gap-1.5 px-6 py-2 border-b border-gray-100 bg-gray-50"
20
+ };
21
+ var _hoisted_7$1 = { class: "text-xs text-gray-500 mr-1" };
22
+ var _hoisted_8 = ["onClick"];
23
+ var _hoisted_9 = { class: "opacity-60" };
24
+ var _hoisted_10 = ["title"];
25
+ var _hoisted_11 = {
26
+ key: 2,
27
+ class: "flex-1 flex items-center justify-center text-gray-400"
28
+ };
29
+ var _hoisted_12 = {
30
+ key: 3,
31
+ class: "flex-1 flex items-center justify-center text-gray-400 text-sm"
32
+ };
33
+ var _hoisted_13 = {
34
+ key: 4,
35
+ class: "flex-1 overflow-y-auto p-4 space-y-2"
36
+ };
37
+ var _hoisted_14 = [
38
+ "aria-expanded",
39
+ "aria-label",
40
+ "onClick",
41
+ "onKeydown"
42
+ ];
43
+ var _hoisted_15 = ["checked", "onChange"];
44
+ var _hoisted_16 = { class: "flex-1 min-w-0" };
45
+ var _hoisted_17 = { class: "flex items-center gap-2 flex-wrap" };
46
+ var _hoisted_18 = {
47
+ key: 0,
48
+ class: "text-xs text-gray-400 mt-0.5"
49
+ };
50
+ var _hoisted_19 = ["title", "onClick"];
51
+ var _hoisted_20 = ["title"];
52
+ var _hoisted_21 = {
53
+ key: 0,
54
+ class: "border-t border-blue-100 bg-blue-50 p-4 space-y-3 rounded-b-lg"
55
+ };
56
+ var _hoisted_22 = { class: "flex items-center gap-2" };
57
+ var _hoisted_23 = {
58
+ key: 0,
59
+ class: "text-xs text-red-500"
60
+ };
61
+ //#endregion
62
+ //#region src/View.vue
63
+ var View_default = /* @__PURE__ */ defineComponent({
64
+ __name: "View",
65
+ props: { selectedResult: {} },
66
+ emits: ["updateResult"],
67
+ setup(__props, { emit: __emit }) {
68
+ const messages = useT();
69
+ function t(key, params) {
70
+ const template = messages.value[key];
71
+ return params ? format(template, params) : template;
72
+ }
73
+ const props = __props;
74
+ const emit = __emit;
75
+ const items = ref(props.selectedResult.data?.items ?? []);
76
+ const { dispatch, pubsub } = useRuntime();
77
+ async function refresh() {
78
+ try {
79
+ const result = await dispatch({ kind: "listAll" });
80
+ if (Array.isArray(result.data?.items)) items.value = result.data.items;
81
+ } catch {}
82
+ }
83
+ let unsub;
84
+ onMounted(() => {
85
+ unsub = pubsub.subscribe("changed", () => {
86
+ refresh();
87
+ });
88
+ refresh();
89
+ });
90
+ onUnmounted(() => unsub?.());
91
+ watch(() => props.selectedResult.uuid, () => {
92
+ items.value = props.selectedResult.data?.items ?? [];
93
+ refresh();
94
+ });
95
+ const completedCount = computed(() => items.value.filter((i) => i.completed).length);
96
+ const hasCompleted = computed(() => items.value.some((i) => i.completed));
97
+ const activeFilters = ref(/* @__PURE__ */ new Set());
98
+ const labelInventory = computed(() => listLabelsWithCount(items.value));
99
+ const filteredItems = computed(() => filterByLabels(items.value, [...activeFilters.value]));
100
+ function toggleFilter(label) {
101
+ const key = label.toLowerCase();
102
+ const next = new Set(activeFilters.value);
103
+ if (next.has(key)) next.delete(key);
104
+ else next.add(key);
105
+ activeFilters.value = next;
106
+ }
107
+ function clearFilters() {
108
+ activeFilters.value = /* @__PURE__ */ new Set();
109
+ }
110
+ function yamlStringValue(str) {
111
+ if (str === "" || /[:#[\]{},&*?|<>=!%@`]/.test(str) || /^\s|\s$/.test(str) || /^(true|false|null|~)$/i.test(str) || /^\d/.test(str)) return `"${str.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
112
+ return str;
113
+ }
114
+ function serializeYaml(item) {
115
+ const labels = item.labels ?? [];
116
+ const labelsLine = labels.length > 0 ? `labels: [${labels.map(yamlStringValue).join(", ")}]` : "labels: []";
117
+ return [
118
+ `text: ${yamlStringValue(item.text)}`,
119
+ `note: ${item.note ? yamlStringValue(item.note) : ""}`,
120
+ labelsLine
121
+ ].join("\n");
122
+ }
123
+ function parseFlowSequence(raw) {
124
+ const trimmed = raw.trim();
125
+ if (!trimmed || trimmed === "[]") return [];
126
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return [];
127
+ const inner = trimmed.slice(1, -1);
128
+ const result = [];
129
+ let buffer = "";
130
+ let inQuotes = false;
131
+ for (let i = 0; i < inner.length; i++) {
132
+ const char = inner[i];
133
+ if (char === "\"" && inner[i - 1] !== "\\") {
134
+ inQuotes = !inQuotes;
135
+ buffer += char;
136
+ continue;
137
+ }
138
+ if (char === "," && !inQuotes) {
139
+ const piece = parseYamlValue(buffer.trim());
140
+ if (piece) result.push(piece);
141
+ buffer = "";
142
+ continue;
143
+ }
144
+ buffer += char;
145
+ }
146
+ const last = parseYamlValue(buffer.trim());
147
+ if (last) result.push(last);
148
+ return result;
149
+ }
150
+ function parseYamlValue(raw) {
151
+ if (raw.startsWith("\"") && raw.endsWith("\"")) return raw.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
152
+ if (raw.startsWith("'") && raw.endsWith("'")) return raw.slice(1, -1);
153
+ return raw;
154
+ }
155
+ function parseYaml(text) {
156
+ const result = {};
157
+ for (const line of text.split("\n")) {
158
+ const trimmed = line.trim();
159
+ if (!trimmed || trimmed.startsWith("#")) continue;
160
+ const colonIdx = line.indexOf(": ");
161
+ if (colonIdx === -1) {
162
+ const colonEnd = line.indexOf(":");
163
+ if (colonEnd !== -1) result[line.slice(0, colonEnd).trim()] = "";
164
+ continue;
165
+ }
166
+ const key = line.slice(0, colonIdx).trim();
167
+ const raw = line.slice(colonIdx + 2).trim();
168
+ if (key === "labels") {
169
+ result[key] = raw;
170
+ continue;
171
+ }
172
+ result[key] = parseYamlValue(raw);
173
+ }
174
+ if (typeof result["text"] !== "string" || !result["text"]) return null;
175
+ const labels = parseFlowSequence(result["labels"] ?? "[]");
176
+ return {
177
+ text: result["text"],
178
+ note: result["note"] ?? "",
179
+ labels
180
+ };
181
+ }
182
+ const selectedId = ref(null);
183
+ const yamlText = ref("");
184
+ const yamlError = ref("");
185
+ function selectItem(item) {
186
+ if (selectedId.value === item.id) {
187
+ selectedId.value = null;
188
+ return;
189
+ }
190
+ selectedId.value = item.id;
191
+ yamlText.value = serializeYaml(item);
192
+ yamlError.value = "";
193
+ }
194
+ watch(items, () => {
195
+ if (!selectedId.value) return;
196
+ const item = items.value.find((i) => i.id === selectedId.value);
197
+ if (item) yamlText.value = serializeYaml(item);
198
+ else selectedId.value = null;
199
+ });
200
+ async function applyItemEdit() {
201
+ yamlError.value = "";
202
+ const parsed = parseYaml(yamlText.value);
203
+ if (!parsed) {
204
+ yamlError.value = t("yamlParseError");
205
+ return;
206
+ }
207
+ const id = selectedId.value;
208
+ if (!id) return;
209
+ if (!await callApi({
210
+ kind: "itemPatch",
211
+ id,
212
+ text: parsed.text,
213
+ note: parsed.note,
214
+ labels: parsed.labels
215
+ })) return;
216
+ selectedId.value = null;
217
+ }
218
+ const todoApiError = ref(null);
219
+ async function callApi(body) {
220
+ try {
221
+ const result = await dispatch(body);
222
+ if (result.error) {
223
+ todoApiError.value = result.error;
224
+ return false;
225
+ }
226
+ todoApiError.value = null;
227
+ items.value = result.data?.items ?? [];
228
+ emit("updateResult", {
229
+ ...props.selectedResult,
230
+ ...result,
231
+ uuid: props.selectedResult.uuid
232
+ });
233
+ return true;
234
+ } catch (err) {
235
+ todoApiError.value = err instanceof Error ? err.message : String(err);
236
+ return false;
237
+ }
238
+ }
239
+ function toggle(item) {
240
+ callApi({
241
+ kind: "itemPatch",
242
+ id: item.id,
243
+ completed: !item.completed
244
+ });
245
+ }
246
+ function remove(item) {
247
+ if (selectedId.value === item.id) selectedId.value = null;
248
+ callApi({
249
+ kind: "itemDelete",
250
+ id: item.id
251
+ });
252
+ }
253
+ function clearCompleted() {
254
+ callApi({ action: "clear_completed" });
255
+ }
256
+ return (_ctx, _cache) => {
257
+ return openBlock(), createElementBlock("div", _hoisted_1$1, [
258
+ todoApiError.value ? (openBlock(), createElementBlock("div", _hoisted_2$1, toDisplayString(t("apiError", { error: todoApiError.value })), 1)) : createCommentVNode("", true),
259
+ createElementVNode("div", _hoisted_3$1, [createElementVNode("h2", _hoisted_4$1, toDisplayString(t("heading")), 1), createElementVNode("span", _hoisted_5$1, toDisplayString(t("completedRatio", {
260
+ done: completedCount.value,
261
+ total: items.value.length
262
+ })), 1)]),
263
+ labelInventory.value.length > 0 ? (openBlock(), createElementBlock("div", _hoisted_6$1, [
264
+ createElementVNode("span", _hoisted_7$1, toDisplayString(t("filter")), 1),
265
+ (openBlock(true), createElementBlock(Fragment, null, renderList(labelInventory.value, (entry) => {
266
+ return openBlock(), createElementBlock("button", {
267
+ key: entry.label,
268
+ class: normalizeClass(["px-2 py-0.5 rounded-full text-xs font-medium transition-all", activeFilters.value.has(entry.label.toLowerCase()) ? "ring-2 ring-blue-400 " + unref(colorForLabel)(entry.label) : unref(colorForLabel)(entry.label) + " opacity-70 hover:opacity-100"]),
269
+ onClick: ($event) => toggleFilter(entry.label)
270
+ }, [createTextVNode(toDisplayString(entry.label) + " ", 1), createElementVNode("span", _hoisted_9, toDisplayString(entry.count), 1)], 10, _hoisted_8);
271
+ }), 128)),
272
+ activeFilters.value.size > 0 ? (openBlock(), createElementBlock("button", {
273
+ key: 0,
274
+ class: "ml-auto text-xs text-gray-500 hover:text-gray-700",
275
+ title: t("clearFilters"),
276
+ onClick: clearFilters
277
+ }, toDisplayString(t("clearButton")), 9, _hoisted_10)) : createCommentVNode("", true)
278
+ ])) : createCommentVNode("", true),
279
+ items.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_11, toDisplayString(t("noItems")), 1)) : filteredItems.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_12, toDisplayString(t("noMatchingFilter")), 1)) : (openBlock(), createElementBlock("ul", _hoisted_13, [(openBlock(true), createElementBlock(Fragment, null, renderList(filteredItems.value, (item) => {
280
+ return openBlock(), createElementBlock("li", {
281
+ key: item.id,
282
+ class: normalizeClass(["rounded-lg border", selectedId.value === item.id ? "border-blue-400" : "border-gray-200"])
283
+ }, [createElementVNode("div", {
284
+ class: normalizeClass(["flex items-center gap-3 p-3 cursor-pointer group hover:bg-gray-50 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400", selectedId.value === item.id ? "rounded-b-none" : ""]),
285
+ role: "button",
286
+ tabindex: "0",
287
+ "aria-expanded": selectedId.value === item.id,
288
+ "aria-label": selectedId.value === item.id ? t("collapse") : t("expand"),
289
+ onClick: ($event) => selectItem(item),
290
+ onKeydown: [withKeys(withModifiers(($event) => selectItem(item), ["self", "prevent"]), ["enter"]), withKeys(withModifiers(($event) => selectItem(item), ["self", "prevent"]), ["space"])]
291
+ }, [
292
+ createElementVNode("input", {
293
+ type: "checkbox",
294
+ checked: item.completed,
295
+ class: "cursor-pointer shrink-0",
296
+ onClick: _cache[0] || (_cache[0] = withModifiers(() => {}, ["stop"])),
297
+ onChange: ($event) => toggle(item)
298
+ }, null, 40, _hoisted_15),
299
+ createElementVNode("div", _hoisted_16, [createElementVNode("div", _hoisted_17, [createElementVNode("span", { class: normalizeClass(["text-sm", item.completed ? "line-through text-gray-400" : "text-gray-800"]) }, toDisplayString(item.text), 3), (openBlock(true), createElementBlock(Fragment, null, renderList(item.labels ?? [], (label) => {
300
+ return openBlock(), createElementBlock("span", {
301
+ key: label,
302
+ class: normalizeClass(["px-1.5 py-0.5 rounded-full text-[10px] font-medium shrink-0", unref(colorForLabel)(label)])
303
+ }, toDisplayString(label), 3);
304
+ }), 128))]), item.note ? (openBlock(), createElementBlock("div", _hoisted_18, toDisplayString(item.note), 1)) : createCommentVNode("", true)]),
305
+ createElementVNode("button", {
306
+ class: "opacity-0 group-hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-400 text-gray-400 hover:text-red-500 text-xs px-1 shrink-0",
307
+ title: t("deleteItem"),
308
+ onClick: withModifiers(($event) => remove(item), ["stop"])
309
+ }, toDisplayString(t("deleteSymbol")), 9, _hoisted_19),
310
+ createElementVNode("span", {
311
+ class: "material-icons text-gray-400 text-sm",
312
+ title: selectedId.value === item.id ? t("collapse") : t("expand")
313
+ }, toDisplayString(selectedId.value === item.id ? "expand_less" : "expand_more"), 9, _hoisted_20)
314
+ ], 42, _hoisted_14), selectedId.value === item.id ? (openBlock(), createElementBlock("div", _hoisted_21, [withDirectives(createElementVNode("textarea", {
315
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => yamlText.value = $event),
316
+ class: "w-full h-24 p-3 font-mono text-xs bg-white border border-blue-300 rounded resize-y focus:outline-none focus:border-blue-500",
317
+ spellcheck: "false"
318
+ }, null, 512), [[vModelText, yamlText.value]]), createElementVNode("div", _hoisted_22, [
319
+ createElementVNode("button", {
320
+ class: "px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600",
321
+ onClick: applyItemEdit
322
+ }, toDisplayString(t("update")), 1),
323
+ createElementVNode("button", {
324
+ class: "px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50",
325
+ onClick: _cache[2] || (_cache[2] = ($event) => selectedId.value = null)
326
+ }, toDisplayString(t("cancel")), 1),
327
+ yamlError.value ? (openBlock(), createElementBlock("span", _hoisted_23, toDisplayString(yamlError.value), 1)) : createCommentVNode("", true)
328
+ ])])) : createCommentVNode("", true)], 2);
329
+ }), 128))])),
330
+ hasCompleted.value ? (openBlock(), createElementBlock("button", {
331
+ key: 5,
332
+ class: "mx-6 mb-2 text-sm text-gray-500 hover:text-gray-700 self-start",
333
+ onClick: clearCompleted
334
+ }, toDisplayString(t("clearCompleted")), 1)) : createCommentVNode("", true)
335
+ ]);
336
+ };
337
+ }
338
+ });
339
+ //#endregion
340
+ //#region src/Preview.vue?vue&type=script&setup=true&lang.ts
341
+ var _hoisted_1 = { class: "p-2 text-sm" };
342
+ var _hoisted_2 = { class: "flex items-center gap-1 font-medium text-gray-700 mb-1" };
343
+ var _hoisted_3 = { "aria-hidden": "true" };
344
+ var _hoisted_4 = { class: "shrink-0" };
345
+ var _hoisted_5 = { class: "truncate" };
346
+ var _hoisted_6 = {
347
+ key: 0,
348
+ class: "text-[9px] text-gray-400 shrink-0"
349
+ };
350
+ var _hoisted_7 = {
351
+ key: 0,
352
+ class: "text-xs text-gray-400"
353
+ };
354
+ //#endregion
355
+ //#region src/vue.ts
356
+ var plugin = {
357
+ toolDefinition: TOOL_DEFINITION,
358
+ viewComponent: View_default,
359
+ previewComponent: /* @__PURE__ */ defineComponent({
360
+ __name: "Preview",
361
+ props: { result: {} },
362
+ setup(__props) {
363
+ const messages = useT();
364
+ function t(key, params) {
365
+ const template = messages.value[key];
366
+ return params ? format(template, params) : template;
367
+ }
368
+ const props = __props;
369
+ const items = ref(props.result.data?.items ?? []);
370
+ const { dispatch, pubsub } = useRuntime();
371
+ async function refresh() {
372
+ try {
373
+ const result = await dispatch({ kind: "listAll" });
374
+ if (Array.isArray(result.data?.items)) items.value = result.data.items;
375
+ } catch {}
376
+ }
377
+ let unsub;
378
+ onMounted(() => {
379
+ refresh();
380
+ unsub = pubsub.subscribe("changed", () => {
381
+ refresh();
382
+ });
383
+ });
384
+ onUnmounted(() => unsub?.());
385
+ watch(() => props.result.uuid, () => {
386
+ items.value = props.result.data?.items ?? [];
387
+ refresh();
388
+ });
389
+ const completedCount = computed(() => items.value.filter((i) => i.completed).length);
390
+ const preview = computed(() => items.value.slice(0, 3));
391
+ const more = computed(() => Math.max(0, items.value.length - 3));
392
+ return (_ctx, _cache) => {
393
+ return openBlock(), createElementBlock("div", _hoisted_1, [
394
+ createElementVNode("div", _hoisted_2, [createElementVNode("span", _hoisted_3, toDisplayString(t("previewHeaderIcon")), 1), createElementVNode("span", null, toDisplayString(t("completedRatio", {
395
+ done: completedCount.value,
396
+ total: items.value.length
397
+ })), 1)]),
398
+ (openBlock(true), createElementBlock(Fragment, null, renderList(preview.value, (item) => {
399
+ return openBlock(), createElementBlock("div", {
400
+ key: item.id,
401
+ class: normalizeClass(["text-xs truncate flex items-center gap-1", item.completed ? "line-through text-gray-400" : "text-gray-600"])
402
+ }, [
403
+ createElementVNode("span", _hoisted_4, toDisplayString(item.completed ? t("previewDoneIcon") : t("previewPendingIcon")), 1),
404
+ createElementVNode("span", _hoisted_5, toDisplayString(item.text), 1),
405
+ (item.labels?.length ?? 0) > 0 ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [(openBlock(true), createElementBlock(Fragment, null, renderList((item.labels ?? []).slice(0, 2), (label) => {
406
+ return openBlock(), createElementBlock("span", {
407
+ key: label,
408
+ class: normalizeClass(["px-1 rounded-full text-[9px] font-medium shrink-0", unref(colorForLabel)(label)])
409
+ }, toDisplayString(label), 3);
410
+ }), 128)), (item.labels?.length ?? 0) > 2 ? (openBlock(), createElementBlock("span", _hoisted_6, toDisplayString(t("previewMoreLabels", { count: (item.labels?.length ?? 0) - 2 })), 1)) : createCommentVNode("", true)], 64)) : createCommentVNode("", true)
411
+ ], 2);
412
+ }), 128)),
413
+ more.value > 0 ? (openBlock(), createElementBlock("div", _hoisted_7, toDisplayString(t("previewMoreItems", { count: more.value })), 1)) : createCommentVNode("", true)
414
+ ]);
415
+ };
416
+ }
417
+ })
418
+ };
419
+ //#endregion
420
+ export { plugin };
421
+
422
+ //# sourceMappingURL=vue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vue.js","names":[],"sources":["../src/View.vue","../src/View.vue","../src/Preview.vue","../src/Preview.vue","../src/vue.ts"],"sourcesContent":["<template>\n <div class=\"h-full bg-white flex flex-col\">\n <!-- API error banner — surfaces POST /api/todos failures so a\n silent add/remove/toggle becomes diagnosable. -->\n <div v-if=\"todoApiError\" class=\"px-4 py-2 bg-red-50 border-b border-red-200 text-sm text-red-700\" role=\"alert\" data-testid=\"todo-api-error\">\n {{ t(\"apiError\", { error: todoApiError }) }}\n </div>\n <div class=\"flex items-center justify-between px-6 py-4 border-b border-gray-100\">\n <h2 class=\"text-lg font-semibold text-gray-800\">{{ t(\"heading\") }}</h2>\n <span class=\"text-sm text-gray-500\">{{ t(\"completedRatio\", { done: completedCount, total: items.length }) }}</span>\n </div>\n\n <!-- Filter bar: only shown when at least one label is in use. -->\n <div v-if=\"labelInventory.length > 0\" class=\"flex flex-wrap items-center gap-1.5 px-6 py-2 border-b border-gray-100 bg-gray-50\">\n <span class=\"text-xs text-gray-500 mr-1\">{{ t(\"filter\") }}</span>\n <button\n v-for=\"entry in labelInventory\"\n :key=\"entry.label\"\n class=\"px-2 py-0.5 rounded-full text-xs font-medium transition-all\"\n :class=\"\n activeFilters.has(entry.label.toLowerCase())\n ? 'ring-2 ring-blue-400 ' + colorForLabel(entry.label)\n : colorForLabel(entry.label) + ' opacity-70 hover:opacity-100'\n \"\n @click=\"toggleFilter(entry.label)\"\n >\n {{ entry.label }}\n <span class=\"opacity-60\">{{ entry.count }}</span>\n </button>\n <button v-if=\"activeFilters.size > 0\" class=\"ml-auto text-xs text-gray-500 hover:text-gray-700\" :title=\"t('clearFilters')\" @click=\"clearFilters\">\n {{ t(\"clearButton\") }}\n </button>\n </div>\n\n <div v-if=\"items.length === 0\" class=\"flex-1 flex items-center justify-center text-gray-400\">{{ t(\"noItems\") }}</div>\n\n <div v-else-if=\"filteredItems.length === 0\" class=\"flex-1 flex items-center justify-center text-gray-400 text-sm\">\n {{ t(\"noMatchingFilter\") }}\n </div>\n\n <ul v-else class=\"flex-1 overflow-y-auto p-4 space-y-2\">\n <li v-for=\"item in filteredItems\" :key=\"item.id\" class=\"rounded-lg border\" :class=\"selectedId === item.id ? 'border-blue-400' : 'border-gray-200'\">\n <!-- Item row — `role=\"button\"` + tabindex/keydown rather than\n a real `<button>` because the row hosts nested\n interactives (checkbox, delete) that would be invalid\n children of a button element. The `.self` modifier on\n both keydown handlers stops Enter/Space pressed on\n those nested controls from also toggling the row\n (and from `preventDefault`-ing the child's native\n activation). -->\n <div\n class=\"flex items-center gap-3 p-3 cursor-pointer group hover:bg-gray-50 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400\"\n :class=\"selectedId === item.id ? 'rounded-b-none' : ''\"\n role=\"button\"\n tabindex=\"0\"\n :aria-expanded=\"selectedId === item.id\"\n :aria-label=\"selectedId === item.id ? t('collapse') : t('expand')\"\n @click=\"selectItem(item)\"\n @keydown.self.enter.prevent=\"selectItem(item)\"\n @keydown.self.space.prevent=\"selectItem(item)\"\n >\n <input type=\"checkbox\" :checked=\"item.completed\" class=\"cursor-pointer shrink-0\" @click.stop @change=\"toggle(item)\" />\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2 flex-wrap\">\n <span class=\"text-sm\" :class=\"item.completed ? 'line-through text-gray-400' : 'text-gray-800'\">{{ item.text }}</span>\n <span\n v-for=\"label in item.labels ?? []\"\n :key=\"label\"\n class=\"px-1.5 py-0.5 rounded-full text-[10px] font-medium shrink-0\"\n :class=\"colorForLabel(label)\"\n >{{ label }}</span\n >\n </div>\n <div v-if=\"item.note\" class=\"text-xs text-gray-400 mt-0.5\">\n {{ item.note }}\n </div>\n </div>\n <button\n class=\"opacity-0 group-hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-400 text-gray-400 hover:text-red-500 text-xs px-1 shrink-0\"\n :title=\"t('deleteItem')\"\n @click.stop=\"remove(item)\"\n >\n {{ t(\"deleteSymbol\") }}\n </button>\n <span class=\"material-icons text-gray-400 text-sm\" :title=\"selectedId === item.id ? t('collapse') : t('expand')\">\n {{ selectedId === item.id ? \"expand_less\" : \"expand_more\" }}\n </span>\n </div>\n\n <!-- Inline editor -->\n <div v-if=\"selectedId === item.id\" class=\"border-t border-blue-100 bg-blue-50 p-4 space-y-3 rounded-b-lg\">\n <textarea\n v-model=\"yamlText\"\n class=\"w-full h-24 p-3 font-mono text-xs bg-white border border-blue-300 rounded resize-y focus:outline-none focus:border-blue-500\"\n spellcheck=\"false\"\n />\n <div class=\"flex items-center gap-2\">\n <button class=\"px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600\" @click=\"applyItemEdit\">{{ t(\"update\") }}</button>\n <button class=\"px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50\" @click=\"selectedId = null\">\n {{ t(\"cancel\") }}\n </button>\n <span v-if=\"yamlError\" class=\"text-xs text-red-500\">{{ yamlError }}</span>\n </div>\n </div>\n </li>\n </ul>\n\n <button v-if=\"hasCompleted\" class=\"mx-6 mb-2 text-sm text-gray-500 hover:text-gray-700 self-start\" @click=\"clearCompleted\">\n {{ t(\"clearCompleted\") }}\n </button>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, onMounted, onUnmounted, ref, watch } from \"vue\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport { useRuntime } from \"gui-chat-protocol/vue\";\nimport type { TodoData, TodoItem } from \"./types\";\nimport { colorForLabel, filterByLabels, listLabelsWithCount } from \"./labels\";\nimport { useT, format } from \"./lang\";\n\nconst messages = useT();\n\n// Wrapper that returns either a plain string or a placeholder-substituted\n// one. Mirrors vue-i18n's `t(key, params)` shape so the existing template\n// (e.g. `{{ t(\"completedRatio\", { done, total }) }}`) keeps\n// reading naturally — see calls below.\nfunction t(key: keyof typeof messages.value, params?: Record<string, string | number>): string {\n const template = messages.value[key];\n return params ? format(template, params) : template;\n}\n\nconst props = defineProps<{\n selectedResult: ToolResultComplete<TodoData>;\n}>();\nconst emit = defineEmits<{ updateResult: [result: ToolResultComplete] }>();\n\nconst items = ref<TodoItem[]>(props.selectedResult.data?.items ?? []);\n\nconst { dispatch, pubsub } = useRuntime();\n\ninterface ListResponse {\n data?: { items?: TodoItem[] };\n}\n\nasync function refresh(): Promise<void> {\n try {\n const result = await dispatch<ListResponse>({ kind: \"listAll\" });\n if (Array.isArray(result.data?.items)) items.value = result.data.items;\n } catch {\n // Network or auth issue — leave the existing list alone so the\n // user keeps seeing what they already had. The next pubsub\n // \"changed\" tick will retry.\n }\n}\n\nlet unsub: (() => void) | undefined;\nonMounted(() => {\n unsub = pubsub.subscribe(\"changed\", () => {\n void refresh();\n });\n void refresh();\n});\nonUnmounted(() => unsub?.());\n\n// Re-fetch when the caller swaps in a different tool result.\nwatch(\n () => props.selectedResult.uuid,\n () => {\n items.value = props.selectedResult.data?.items ?? [];\n void refresh();\n },\n);\nconst completedCount = computed(() => items.value.filter((i) => i.completed).length);\nconst hasCompleted = computed(() => items.value.some((i) => i.completed));\n\n// ── Label filter state ──────────────────────────────────────────────────────\n// Filters are local to this View instance — intentional, so that\n// switching sessions or reopening a tool result doesn't drag state\n// across contexts. Active filters are stored lowercased to match\n// `filterByLabels`' case-insensitive semantics.\n\nconst activeFilters = ref<Set<string>>(new Set());\n\nconst labelInventory = computed(() => listLabelsWithCount(items.value));\n\nconst filteredItems = computed(() => filterByLabels(items.value, [...activeFilters.value]));\n\nfunction toggleFilter(label: string): void {\n const key = label.toLowerCase();\n const next = new Set(activeFilters.value);\n if (next.has(key)) {\n next.delete(key);\n } else {\n next.add(key);\n }\n activeFilters.value = next;\n}\n\nfunction clearFilters(): void {\n activeFilters.value = new Set();\n}\n\n// ── YAML helpers ─────────────────────────────────────────────────────────────\n\nfunction yamlStringValue(str: string): string {\n const needsQuotes = str === \"\" || /[:#[\\]{},&*?|<>=!%@`]/.test(str) || /^\\s|\\s$/.test(str) || /^(true|false|null|~)$/i.test(str) || /^\\d/.test(str);\n if (needsQuotes) {\n return `\"${str.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"')}\"`;\n }\n return str;\n}\n\nfunction serializeYaml(item: TodoItem): string {\n const labels = item.labels ?? [];\n const labelsLine = labels.length > 0 ? `labels: [${labels.map(yamlStringValue).join(\", \")}]` : \"labels: []\";\n return [`text: ${yamlStringValue(item.text)}`, `note: ${item.note ? yamlStringValue(item.note) : \"\"}`, labelsLine].join(\"\\n\");\n}\n\n// Parse a YAML flow sequence `[a, \"b\", c]` into an array of strings.\n// Handles quoted and unquoted entries. Whitespace-only input → empty.\nfunction parseFlowSequence(raw: string): string[] {\n const trimmed = raw.trim();\n if (!trimmed || trimmed === \"[]\") return [];\n if (!trimmed.startsWith(\"[\") || !trimmed.endsWith(\"]\")) return [];\n const inner = trimmed.slice(1, -1);\n // Split on commas that are NOT inside double quotes. Cheap scan;\n // fine for our label use case where items don't contain commas\n // (stored labels are normalised strings without commas).\n const result: string[] = [];\n let buffer = \"\";\n let inQuotes = false;\n for (let i = 0; i < inner.length; i++) {\n const char = inner[i];\n if (char === '\"' && inner[i - 1] !== \"\\\\\") {\n inQuotes = !inQuotes;\n buffer += char;\n continue;\n }\n if (char === \",\" && !inQuotes) {\n const piece = parseYamlValue(buffer.trim());\n if (piece) result.push(piece);\n buffer = \"\";\n continue;\n }\n buffer += char;\n }\n const last = parseYamlValue(buffer.trim());\n if (last) result.push(last);\n return result;\n}\n\nfunction parseYamlValue(raw: string): string {\n if (raw.startsWith('\"') && raw.endsWith('\"')) {\n return raw.slice(1, -1).replace(/\\\\\"/g, '\"').replace(/\\\\\\\\/g, \"\\\\\");\n }\n if (raw.startsWith(\"'\") && raw.endsWith(\"'\")) {\n return raw.slice(1, -1);\n }\n return raw;\n}\n\nfunction parseYaml(text: string): { text: string; note: string; labels: string[] } | null {\n const result: Record<string, string> = {};\n for (const line of text.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const colonIdx = line.indexOf(\": \");\n if (colonIdx === -1) {\n // \"key:\" with empty value\n const colonEnd = line.indexOf(\":\");\n if (colonEnd !== -1) result[line.slice(0, colonEnd).trim()] = \"\";\n continue;\n }\n // `labels:` is a flow sequence (`[a, b]`) — parse it as a list\n // instead of running it through `parseYamlValue` which strips\n // brackets as if they were quotes.\n const key = line.slice(0, colonIdx).trim();\n const raw = line.slice(colonIdx + 2).trim();\n if (key === \"labels\") {\n result[key] = raw;\n continue;\n }\n result[key] = parseYamlValue(raw);\n }\n if (typeof result[\"text\"] !== \"string\" || !result[\"text\"]) return null;\n const labels = parseFlowSequence(result[\"labels\"] ?? \"[]\");\n return {\n text: result[\"text\"],\n note: result[\"note\"] ?? \"\",\n labels,\n };\n}\n\n// ── Item selection & YAML edit ────────────────────────────────────────────────\n\nconst selectedId = ref<string | null>(null);\nconst yamlText = ref(\"\");\nconst yamlError = ref(\"\");\n\nfunction selectItem(item: TodoItem) {\n if (selectedId.value === item.id) {\n selectedId.value = null;\n return;\n }\n selectedId.value = item.id;\n yamlText.value = serializeYaml(item);\n yamlError.value = \"\";\n}\n\nwatch(items, () => {\n if (!selectedId.value) return;\n const item = items.value.find((i) => i.id === selectedId.value);\n if (item) yamlText.value = serializeYaml(item);\n else selectedId.value = null;\n});\n\nasync function applyItemEdit() {\n yamlError.value = \"\";\n const parsed = parseYaml(yamlText.value);\n if (!parsed) {\n yamlError.value = t(\"yamlParseError\");\n return;\n }\n // Single id-based UI patch — `handlePatch` accepts text + note +\n // labels in one call and applies them atomically. The earlier\n // multi-call LLM-action flow (`update` + `add_label` + `remove_label`)\n // resolved items by case-insensitive substring match on text, so two\n // todos with similar titles could clobber each other; the UI knows\n // the id, so use it.\n const id = selectedId.value;\n if (!id) return;\n const ok = await callApi({\n kind: \"itemPatch\",\n id,\n text: parsed.text,\n note: parsed.note,\n labels: parsed.labels,\n });\n if (!ok) return;\n selectedId.value = null;\n}\n\n// ── API ───────────────────────────────────────────────────────────────────────\n\n// Last POST /api/todos failure. Cleared on the next successful call so\n// the banner disappears as soon as things recover.\nconst todoApiError = ref<string | null>(null);\n\ninterface CallApiResult {\n data?: { items?: TodoItem[] };\n message?: string;\n jsonData?: unknown;\n instructions?: string;\n error?: string;\n}\n\nasync function callApi(body: Record<string, unknown>): Promise<boolean> {\n try {\n const result = await dispatch<CallApiResult>(body);\n if (result.error) {\n todoApiError.value = result.error;\n return false;\n }\n todoApiError.value = null;\n items.value = result.data?.items ?? [];\n emit(\"updateResult\", {\n ...props.selectedResult,\n ...result,\n uuid: props.selectedResult.uuid,\n } as ToolResultComplete);\n return true;\n } catch (err) {\n todoApiError.value = err instanceof Error ? err.message : String(err);\n return false;\n }\n}\n\nfunction toggle(item: TodoItem) {\n // id-based patch — server's `applyCompletedPatch` flips the\n // completed flag and moves the item between the done column\n // and the default open column the obvious way. The previous\n // text-based `check` / `uncheck` LLM actions resolved by\n // case-insensitive substring and could mutate the wrong row\n // when two todos share a prefix.\n callApi({ kind: \"itemPatch\", id: item.id, completed: !item.completed });\n}\n\nfunction remove(item: TodoItem) {\n if (selectedId.value === item.id) selectedId.value = null;\n callApi({ kind: \"itemDelete\", id: item.id });\n}\n\nfunction clearCompleted() {\n // The LLM `clear_completed` action filters by the boolean flag\n // (no text matching), so it's safe to keep as a single\n // round-trip rather than fanning out into N `itemDelete` calls.\n callApi({ action: \"clear_completed\" });\n}\n</script>\n","<template>\n <div class=\"h-full bg-white flex flex-col\">\n <!-- API error banner — surfaces POST /api/todos failures so a\n silent add/remove/toggle becomes diagnosable. -->\n <div v-if=\"todoApiError\" class=\"px-4 py-2 bg-red-50 border-b border-red-200 text-sm text-red-700\" role=\"alert\" data-testid=\"todo-api-error\">\n {{ t(\"apiError\", { error: todoApiError }) }}\n </div>\n <div class=\"flex items-center justify-between px-6 py-4 border-b border-gray-100\">\n <h2 class=\"text-lg font-semibold text-gray-800\">{{ t(\"heading\") }}</h2>\n <span class=\"text-sm text-gray-500\">{{ t(\"completedRatio\", { done: completedCount, total: items.length }) }}</span>\n </div>\n\n <!-- Filter bar: only shown when at least one label is in use. -->\n <div v-if=\"labelInventory.length > 0\" class=\"flex flex-wrap items-center gap-1.5 px-6 py-2 border-b border-gray-100 bg-gray-50\">\n <span class=\"text-xs text-gray-500 mr-1\">{{ t(\"filter\") }}</span>\n <button\n v-for=\"entry in labelInventory\"\n :key=\"entry.label\"\n class=\"px-2 py-0.5 rounded-full text-xs font-medium transition-all\"\n :class=\"\n activeFilters.has(entry.label.toLowerCase())\n ? 'ring-2 ring-blue-400 ' + colorForLabel(entry.label)\n : colorForLabel(entry.label) + ' opacity-70 hover:opacity-100'\n \"\n @click=\"toggleFilter(entry.label)\"\n >\n {{ entry.label }}\n <span class=\"opacity-60\">{{ entry.count }}</span>\n </button>\n <button v-if=\"activeFilters.size > 0\" class=\"ml-auto text-xs text-gray-500 hover:text-gray-700\" :title=\"t('clearFilters')\" @click=\"clearFilters\">\n {{ t(\"clearButton\") }}\n </button>\n </div>\n\n <div v-if=\"items.length === 0\" class=\"flex-1 flex items-center justify-center text-gray-400\">{{ t(\"noItems\") }}</div>\n\n <div v-else-if=\"filteredItems.length === 0\" class=\"flex-1 flex items-center justify-center text-gray-400 text-sm\">\n {{ t(\"noMatchingFilter\") }}\n </div>\n\n <ul v-else class=\"flex-1 overflow-y-auto p-4 space-y-2\">\n <li v-for=\"item in filteredItems\" :key=\"item.id\" class=\"rounded-lg border\" :class=\"selectedId === item.id ? 'border-blue-400' : 'border-gray-200'\">\n <!-- Item row — `role=\"button\"` + tabindex/keydown rather than\n a real `<button>` because the row hosts nested\n interactives (checkbox, delete) that would be invalid\n children of a button element. The `.self` modifier on\n both keydown handlers stops Enter/Space pressed on\n those nested controls from also toggling the row\n (and from `preventDefault`-ing the child's native\n activation). -->\n <div\n class=\"flex items-center gap-3 p-3 cursor-pointer group hover:bg-gray-50 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400\"\n :class=\"selectedId === item.id ? 'rounded-b-none' : ''\"\n role=\"button\"\n tabindex=\"0\"\n :aria-expanded=\"selectedId === item.id\"\n :aria-label=\"selectedId === item.id ? t('collapse') : t('expand')\"\n @click=\"selectItem(item)\"\n @keydown.self.enter.prevent=\"selectItem(item)\"\n @keydown.self.space.prevent=\"selectItem(item)\"\n >\n <input type=\"checkbox\" :checked=\"item.completed\" class=\"cursor-pointer shrink-0\" @click.stop @change=\"toggle(item)\" />\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2 flex-wrap\">\n <span class=\"text-sm\" :class=\"item.completed ? 'line-through text-gray-400' : 'text-gray-800'\">{{ item.text }}</span>\n <span\n v-for=\"label in item.labels ?? []\"\n :key=\"label\"\n class=\"px-1.5 py-0.5 rounded-full text-[10px] font-medium shrink-0\"\n :class=\"colorForLabel(label)\"\n >{{ label }}</span\n >\n </div>\n <div v-if=\"item.note\" class=\"text-xs text-gray-400 mt-0.5\">\n {{ item.note }}\n </div>\n </div>\n <button\n class=\"opacity-0 group-hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-400 text-gray-400 hover:text-red-500 text-xs px-1 shrink-0\"\n :title=\"t('deleteItem')\"\n @click.stop=\"remove(item)\"\n >\n {{ t(\"deleteSymbol\") }}\n </button>\n <span class=\"material-icons text-gray-400 text-sm\" :title=\"selectedId === item.id ? t('collapse') : t('expand')\">\n {{ selectedId === item.id ? \"expand_less\" : \"expand_more\" }}\n </span>\n </div>\n\n <!-- Inline editor -->\n <div v-if=\"selectedId === item.id\" class=\"border-t border-blue-100 bg-blue-50 p-4 space-y-3 rounded-b-lg\">\n <textarea\n v-model=\"yamlText\"\n class=\"w-full h-24 p-3 font-mono text-xs bg-white border border-blue-300 rounded resize-y focus:outline-none focus:border-blue-500\"\n spellcheck=\"false\"\n />\n <div class=\"flex items-center gap-2\">\n <button class=\"px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600\" @click=\"applyItemEdit\">{{ t(\"update\") }}</button>\n <button class=\"px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50\" @click=\"selectedId = null\">\n {{ t(\"cancel\") }}\n </button>\n <span v-if=\"yamlError\" class=\"text-xs text-red-500\">{{ yamlError }}</span>\n </div>\n </div>\n </li>\n </ul>\n\n <button v-if=\"hasCompleted\" class=\"mx-6 mb-2 text-sm text-gray-500 hover:text-gray-700 self-start\" @click=\"clearCompleted\">\n {{ t(\"clearCompleted\") }}\n </button>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, onMounted, onUnmounted, ref, watch } from \"vue\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport { useRuntime } from \"gui-chat-protocol/vue\";\nimport type { TodoData, TodoItem } from \"./types\";\nimport { colorForLabel, filterByLabels, listLabelsWithCount } from \"./labels\";\nimport { useT, format } from \"./lang\";\n\nconst messages = useT();\n\n// Wrapper that returns either a plain string or a placeholder-substituted\n// one. Mirrors vue-i18n's `t(key, params)` shape so the existing template\n// (e.g. `{{ t(\"completedRatio\", { done, total }) }}`) keeps\n// reading naturally — see calls below.\nfunction t(key: keyof typeof messages.value, params?: Record<string, string | number>): string {\n const template = messages.value[key];\n return params ? format(template, params) : template;\n}\n\nconst props = defineProps<{\n selectedResult: ToolResultComplete<TodoData>;\n}>();\nconst emit = defineEmits<{ updateResult: [result: ToolResultComplete] }>();\n\nconst items = ref<TodoItem[]>(props.selectedResult.data?.items ?? []);\n\nconst { dispatch, pubsub } = useRuntime();\n\ninterface ListResponse {\n data?: { items?: TodoItem[] };\n}\n\nasync function refresh(): Promise<void> {\n try {\n const result = await dispatch<ListResponse>({ kind: \"listAll\" });\n if (Array.isArray(result.data?.items)) items.value = result.data.items;\n } catch {\n // Network or auth issue — leave the existing list alone so the\n // user keeps seeing what they already had. The next pubsub\n // \"changed\" tick will retry.\n }\n}\n\nlet unsub: (() => void) | undefined;\nonMounted(() => {\n unsub = pubsub.subscribe(\"changed\", () => {\n void refresh();\n });\n void refresh();\n});\nonUnmounted(() => unsub?.());\n\n// Re-fetch when the caller swaps in a different tool result.\nwatch(\n () => props.selectedResult.uuid,\n () => {\n items.value = props.selectedResult.data?.items ?? [];\n void refresh();\n },\n);\nconst completedCount = computed(() => items.value.filter((i) => i.completed).length);\nconst hasCompleted = computed(() => items.value.some((i) => i.completed));\n\n// ── Label filter state ──────────────────────────────────────────────────────\n// Filters are local to this View instance — intentional, so that\n// switching sessions or reopening a tool result doesn't drag state\n// across contexts. Active filters are stored lowercased to match\n// `filterByLabels`' case-insensitive semantics.\n\nconst activeFilters = ref<Set<string>>(new Set());\n\nconst labelInventory = computed(() => listLabelsWithCount(items.value));\n\nconst filteredItems = computed(() => filterByLabels(items.value, [...activeFilters.value]));\n\nfunction toggleFilter(label: string): void {\n const key = label.toLowerCase();\n const next = new Set(activeFilters.value);\n if (next.has(key)) {\n next.delete(key);\n } else {\n next.add(key);\n }\n activeFilters.value = next;\n}\n\nfunction clearFilters(): void {\n activeFilters.value = new Set();\n}\n\n// ── YAML helpers ─────────────────────────────────────────────────────────────\n\nfunction yamlStringValue(str: string): string {\n const needsQuotes = str === \"\" || /[:#[\\]{},&*?|<>=!%@`]/.test(str) || /^\\s|\\s$/.test(str) || /^(true|false|null|~)$/i.test(str) || /^\\d/.test(str);\n if (needsQuotes) {\n return `\"${str.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"')}\"`;\n }\n return str;\n}\n\nfunction serializeYaml(item: TodoItem): string {\n const labels = item.labels ?? [];\n const labelsLine = labels.length > 0 ? `labels: [${labels.map(yamlStringValue).join(\", \")}]` : \"labels: []\";\n return [`text: ${yamlStringValue(item.text)}`, `note: ${item.note ? yamlStringValue(item.note) : \"\"}`, labelsLine].join(\"\\n\");\n}\n\n// Parse a YAML flow sequence `[a, \"b\", c]` into an array of strings.\n// Handles quoted and unquoted entries. Whitespace-only input → empty.\nfunction parseFlowSequence(raw: string): string[] {\n const trimmed = raw.trim();\n if (!trimmed || trimmed === \"[]\") return [];\n if (!trimmed.startsWith(\"[\") || !trimmed.endsWith(\"]\")) return [];\n const inner = trimmed.slice(1, -1);\n // Split on commas that are NOT inside double quotes. Cheap scan;\n // fine for our label use case where items don't contain commas\n // (stored labels are normalised strings without commas).\n const result: string[] = [];\n let buffer = \"\";\n let inQuotes = false;\n for (let i = 0; i < inner.length; i++) {\n const char = inner[i];\n if (char === '\"' && inner[i - 1] !== \"\\\\\") {\n inQuotes = !inQuotes;\n buffer += char;\n continue;\n }\n if (char === \",\" && !inQuotes) {\n const piece = parseYamlValue(buffer.trim());\n if (piece) result.push(piece);\n buffer = \"\";\n continue;\n }\n buffer += char;\n }\n const last = parseYamlValue(buffer.trim());\n if (last) result.push(last);\n return result;\n}\n\nfunction parseYamlValue(raw: string): string {\n if (raw.startsWith('\"') && raw.endsWith('\"')) {\n return raw.slice(1, -1).replace(/\\\\\"/g, '\"').replace(/\\\\\\\\/g, \"\\\\\");\n }\n if (raw.startsWith(\"'\") && raw.endsWith(\"'\")) {\n return raw.slice(1, -1);\n }\n return raw;\n}\n\nfunction parseYaml(text: string): { text: string; note: string; labels: string[] } | null {\n const result: Record<string, string> = {};\n for (const line of text.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const colonIdx = line.indexOf(\": \");\n if (colonIdx === -1) {\n // \"key:\" with empty value\n const colonEnd = line.indexOf(\":\");\n if (colonEnd !== -1) result[line.slice(0, colonEnd).trim()] = \"\";\n continue;\n }\n // `labels:` is a flow sequence (`[a, b]`) — parse it as a list\n // instead of running it through `parseYamlValue` which strips\n // brackets as if they were quotes.\n const key = line.slice(0, colonIdx).trim();\n const raw = line.slice(colonIdx + 2).trim();\n if (key === \"labels\") {\n result[key] = raw;\n continue;\n }\n result[key] = parseYamlValue(raw);\n }\n if (typeof result[\"text\"] !== \"string\" || !result[\"text\"]) return null;\n const labels = parseFlowSequence(result[\"labels\"] ?? \"[]\");\n return {\n text: result[\"text\"],\n note: result[\"note\"] ?? \"\",\n labels,\n };\n}\n\n// ── Item selection & YAML edit ────────────────────────────────────────────────\n\nconst selectedId = ref<string | null>(null);\nconst yamlText = ref(\"\");\nconst yamlError = ref(\"\");\n\nfunction selectItem(item: TodoItem) {\n if (selectedId.value === item.id) {\n selectedId.value = null;\n return;\n }\n selectedId.value = item.id;\n yamlText.value = serializeYaml(item);\n yamlError.value = \"\";\n}\n\nwatch(items, () => {\n if (!selectedId.value) return;\n const item = items.value.find((i) => i.id === selectedId.value);\n if (item) yamlText.value = serializeYaml(item);\n else selectedId.value = null;\n});\n\nasync function applyItemEdit() {\n yamlError.value = \"\";\n const parsed = parseYaml(yamlText.value);\n if (!parsed) {\n yamlError.value = t(\"yamlParseError\");\n return;\n }\n // Single id-based UI patch — `handlePatch` accepts text + note +\n // labels in one call and applies them atomically. The earlier\n // multi-call LLM-action flow (`update` + `add_label` + `remove_label`)\n // resolved items by case-insensitive substring match on text, so two\n // todos with similar titles could clobber each other; the UI knows\n // the id, so use it.\n const id = selectedId.value;\n if (!id) return;\n const ok = await callApi({\n kind: \"itemPatch\",\n id,\n text: parsed.text,\n note: parsed.note,\n labels: parsed.labels,\n });\n if (!ok) return;\n selectedId.value = null;\n}\n\n// ── API ───────────────────────────────────────────────────────────────────────\n\n// Last POST /api/todos failure. Cleared on the next successful call so\n// the banner disappears as soon as things recover.\nconst todoApiError = ref<string | null>(null);\n\ninterface CallApiResult {\n data?: { items?: TodoItem[] };\n message?: string;\n jsonData?: unknown;\n instructions?: string;\n error?: string;\n}\n\nasync function callApi(body: Record<string, unknown>): Promise<boolean> {\n try {\n const result = await dispatch<CallApiResult>(body);\n if (result.error) {\n todoApiError.value = result.error;\n return false;\n }\n todoApiError.value = null;\n items.value = result.data?.items ?? [];\n emit(\"updateResult\", {\n ...props.selectedResult,\n ...result,\n uuid: props.selectedResult.uuid,\n } as ToolResultComplete);\n return true;\n } catch (err) {\n todoApiError.value = err instanceof Error ? err.message : String(err);\n return false;\n }\n}\n\nfunction toggle(item: TodoItem) {\n // id-based patch — server's `applyCompletedPatch` flips the\n // completed flag and moves the item between the done column\n // and the default open column the obvious way. The previous\n // text-based `check` / `uncheck` LLM actions resolved by\n // case-insensitive substring and could mutate the wrong row\n // when two todos share a prefix.\n callApi({ kind: \"itemPatch\", id: item.id, completed: !item.completed });\n}\n\nfunction remove(item: TodoItem) {\n if (selectedId.value === item.id) selectedId.value = null;\n callApi({ kind: \"itemDelete\", id: item.id });\n}\n\nfunction clearCompleted() {\n // The LLM `clear_completed` action filters by the boolean flag\n // (no text matching), so it's safe to keep as a single\n // round-trip rather than fanning out into N `itemDelete` calls.\n callApi({ action: \"clear_completed\" });\n}\n</script>\n","<template>\n <div class=\"p-2 text-sm\">\n <div class=\"flex items-center gap-1 font-medium text-gray-700 mb-1\">\n <span aria-hidden=\"true\">{{ t(\"previewHeaderIcon\") }}</span>\n <span>{{ t(\"completedRatio\", { done: completedCount, total: items.length }) }}</span>\n </div>\n <div\n v-for=\"item in preview\"\n :key=\"item.id\"\n class=\"text-xs truncate flex items-center gap-1\"\n :class=\"item.completed ? 'line-through text-gray-400' : 'text-gray-600'\"\n >\n <span class=\"shrink-0\">{{ item.completed ? t(\"previewDoneIcon\") : t(\"previewPendingIcon\") }}</span>\n <span class=\"truncate\">{{ item.text }}</span>\n <template v-if=\"(item.labels?.length ?? 0) > 0\">\n <span\n v-for=\"label in (item.labels ?? []).slice(0, 2)\"\n :key=\"label\"\n class=\"px-1 rounded-full text-[9px] font-medium shrink-0\"\n :class=\"colorForLabel(label)\"\n >{{ label }}</span\n >\n <span v-if=\"(item.labels?.length ?? 0) > 2\" class=\"text-[9px] text-gray-400 shrink-0\">{{\n t(\"previewMoreLabels\", { count: (item.labels?.length ?? 0) - 2 })\n }}</span>\n </template>\n </div>\n <div v-if=\"more > 0\" class=\"text-xs text-gray-400\">{{ t(\"previewMoreItems\", { count: more }) }}</div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, onMounted, onUnmounted, ref, watch } from \"vue\";\nimport { useRuntime } from \"gui-chat-protocol/vue\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport type { TodoData, TodoItem } from \"./types\";\nimport { colorForLabel } from \"./labels\";\nimport { useT, format } from \"./lang\";\n\nconst messages = useT();\n\nfunction t(key: keyof typeof messages.value, params?: Record<string, string | number>): string {\n const template = messages.value[key];\n return params ? format(template, params) : template;\n}\n\nconst props = defineProps<{ result: ToolResultComplete<TodoData> }>();\n\nconst items = ref<TodoItem[]>(props.result.data?.items ?? []);\n\nconst { dispatch, pubsub } = useRuntime();\n\ninterface ListResponse {\n data?: { items?: TodoItem[] };\n}\n\nasync function refresh(): Promise<void> {\n try {\n const result = await dispatch<ListResponse>({ kind: \"listAll\" });\n if (Array.isArray(result.data?.items)) items.value = result.data.items;\n } catch {\n // Preview keeps its prop-initialised state on failure — silent\n // by design (it's a thumbnail, not the canonical view).\n }\n}\n\nlet unsub: (() => void) | undefined;\nonMounted(() => {\n // Initial fetch — `props.result.data.items` is the snapshot the LLM\n // saw at tool-call time, which can already be stale by the time the\n // user opens the preview. Refresh once to match the canonical view,\n // then subscribe for live updates.\n void refresh();\n unsub = pubsub.subscribe(\"changed\", () => {\n void refresh();\n });\n});\nonUnmounted(() => unsub?.());\n\nwatch(\n () => props.result.uuid,\n () => {\n items.value = props.result.data?.items ?? [];\n void refresh();\n },\n);\nconst completedCount = computed(() => items.value.filter((i) => i.completed).length);\nconst preview = computed(() => items.value.slice(0, 3));\nconst more = computed(() => Math.max(0, items.value.length - 3));\n</script>\n","<template>\n <div class=\"p-2 text-sm\">\n <div class=\"flex items-center gap-1 font-medium text-gray-700 mb-1\">\n <span aria-hidden=\"true\">{{ t(\"previewHeaderIcon\") }}</span>\n <span>{{ t(\"completedRatio\", { done: completedCount, total: items.length }) }}</span>\n </div>\n <div\n v-for=\"item in preview\"\n :key=\"item.id\"\n class=\"text-xs truncate flex items-center gap-1\"\n :class=\"item.completed ? 'line-through text-gray-400' : 'text-gray-600'\"\n >\n <span class=\"shrink-0\">{{ item.completed ? t(\"previewDoneIcon\") : t(\"previewPendingIcon\") }}</span>\n <span class=\"truncate\">{{ item.text }}</span>\n <template v-if=\"(item.labels?.length ?? 0) > 0\">\n <span\n v-for=\"label in (item.labels ?? []).slice(0, 2)\"\n :key=\"label\"\n class=\"px-1 rounded-full text-[9px] font-medium shrink-0\"\n :class=\"colorForLabel(label)\"\n >{{ label }}</span\n >\n <span v-if=\"(item.labels?.length ?? 0) > 2\" class=\"text-[9px] text-gray-400 shrink-0\">{{\n t(\"previewMoreLabels\", { count: (item.labels?.length ?? 0) - 2 })\n }}</span>\n </template>\n </div>\n <div v-if=\"more > 0\" class=\"text-xs text-gray-400\">{{ t(\"previewMoreItems\", { count: more }) }}</div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, onMounted, onUnmounted, ref, watch } from \"vue\";\nimport { useRuntime } from \"gui-chat-protocol/vue\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport type { TodoData, TodoItem } from \"./types\";\nimport { colorForLabel } from \"./labels\";\nimport { useT, format } from \"./lang\";\n\nconst messages = useT();\n\nfunction t(key: keyof typeof messages.value, params?: Record<string, string | number>): string {\n const template = messages.value[key];\n return params ? format(template, params) : template;\n}\n\nconst props = defineProps<{ result: ToolResultComplete<TodoData> }>();\n\nconst items = ref<TodoItem[]>(props.result.data?.items ?? []);\n\nconst { dispatch, pubsub } = useRuntime();\n\ninterface ListResponse {\n data?: { items?: TodoItem[] };\n}\n\nasync function refresh(): Promise<void> {\n try {\n const result = await dispatch<ListResponse>({ kind: \"listAll\" });\n if (Array.isArray(result.data?.items)) items.value = result.data.items;\n } catch {\n // Preview keeps its prop-initialised state on failure — silent\n // by design (it's a thumbnail, not the canonical view).\n }\n}\n\nlet unsub: (() => void) | undefined;\nonMounted(() => {\n // Initial fetch — `props.result.data.items` is the snapshot the LLM\n // saw at tool-call time, which can already be stale by the time the\n // user opens the preview. Refresh once to match the canonical view,\n // then subscribe for live updates.\n void refresh();\n unsub = pubsub.subscribe(\"changed\", () => {\n void refresh();\n });\n});\nonUnmounted(() => unsub?.());\n\nwatch(\n () => props.result.uuid,\n () => {\n items.value = props.result.data?.items ?? [];\n void refresh();\n },\n);\nconst completedCount = computed(() => items.value.filter((i) => i.completed).length);\nconst preview = computed(() => items.value.slice(0, 3));\nconst more = computed(() => Math.max(0, items.value.length - 3));\n</script>\n","// Vue entry — exports the canvas + preview components the host\n// runtime plugin loader dynamic-imports as `dist/vue.js`. Same shape\n// as `packages/plugins/bookmarks-plugin/src/vue.ts` so the host's loader\n// (src/tools/runtimeLoader.ts) registers them without special-casing.\n\nimport View from \"./View.vue\";\nimport Preview from \"./Preview.vue\";\nimport { TOOL_DEFINITION } from \"./definition\";\n\nexport const plugin = {\n toolDefinition: TOOL_DEFINITION,\n viewComponent: View,\n previewComponent: Preview,\n};\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@mulmoclaude/todo-plugin",
3
+ "version": "0.1.0",
4
+ "description": "Todo list plugin for MulmoClaude — kanban + list view, multi-tab live sync. Migrating from built-in plugin to runtime-plugin shape (#1145).",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./vue": {
13
+ "types": "./dist/vue.d.ts",
14
+ "import": "./dist/vue.js"
15
+ },
16
+ "./shared": {
17
+ "types": "./dist/shared.d.ts",
18
+ "import": "./dist/shared.js"
19
+ },
20
+ "./composables": {
21
+ "_comment": "Vite bundles `src/composables/index.ts` to `dist/composables.js` (single file), but `vite-plugin-dts` mirrors source structure for declaration emit, so the d.ts lives at `dist/composables/index.d.ts`. The two paths intentionally diverge.",
22
+ "types": "./dist/composables/index.d.ts",
23
+ "import": "./dist/composables.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "scripts": {
30
+ "build": "vite build",
31
+ "typecheck": "tsc --noEmit",
32
+ "test": "echo 'no in-package tests yet; covered by host integration test test/plugins/test_todo_plugin_integration.ts (added in PR2)'"
33
+ },
34
+ "peerDependencies": {
35
+ "gui-chat-protocol": "^0.3.0",
36
+ "vue": "^3.5.0",
37
+ "zod": "^4.3.6"
38
+ },
39
+ "devDependencies": {
40
+ "@vitejs/plugin-vue": "^6.0.7",
41
+ "typescript": "^6.0.3",
42
+ "vite": "^8.0.13",
43
+ "vite-plugin-dts": "^5.0.0",
44
+ "vue": "^3.5.34",
45
+ "zod": "^4.4.3"
46
+ },
47
+ "license": "MIT"
48
+ }