agent-canvas 0.2.1 → 0.6.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 (99) hide show
  1. package/README.md +42 -17
  2. package/bin/agent-canvas.js +7 -7
  3. package/bin/agent-canvas.ts +48 -37
  4. package/bin/lib/commands/daemon.ts +46 -43
  5. package/bin/lib/commands/exec.ts +100 -100
  6. package/bin/lib/commands/fetch.ts +15 -15
  7. package/bin/lib/commands/install.ts +36 -36
  8. package/bin/lib/commands/instructions.ts +51 -51
  9. package/bin/lib/commands/push.ts +115 -97
  10. package/bin/lib/commands/review.ts +190 -0
  11. package/bin/lib/commands/watch.ts +24 -24
  12. package/bin/lib/config.ts +12 -12
  13. package/bin/lib/daemon-lifecycle.ts +47 -51
  14. package/bin/lib/helpers.ts +82 -76
  15. package/bin/lib/instructions/app-review.md +88 -0
  16. package/bin/lib/instructions/component-checklist.md +7 -5
  17. package/bin/lib/instructions/component-custom.md +20 -14
  18. package/bin/lib/instructions/component-diff.md +4 -3
  19. package/bin/lib/instructions/component-item.md +18 -8
  20. package/bin/lib/instructions/component-markdown.md +4 -2
  21. package/bin/lib/instructions/component-mermaid.md +12 -6
  22. package/bin/lib/instructions/component-multichoice.md +6 -2
  23. package/bin/lib/instructions/component-note.md +1 -1
  24. package/bin/lib/instructions/component-priority.md +1 -1
  25. package/bin/lib/instructions/component-rangeinput.md +9 -2
  26. package/bin/lib/instructions/component-secretinput.md +12 -11
  27. package/bin/lib/instructions/component-svg.md +62 -16
  28. package/bin/lib/instructions/component-table.md +6 -6
  29. package/bin/lib/instructions/component-usefeedback.md +8 -8
  30. package/bin/lib/instructions/component-userinput.md +6 -2
  31. package/bin/lib/instructions/flow-feature.md +22 -5
  32. package/bin/lib/instructions/flow-plan.md +5 -5
  33. package/bin/lib/instructions/flow-runbook.md +28 -25
  34. package/bin/lib/instructions/overview.md +27 -18
  35. package/bin/lib/instructions/theming.md +5 -0
  36. package/daemon/build.ts +171 -152
  37. package/daemon/client/App.tsx +1205 -1077
  38. package/daemon/client/CompareView.tsx +478 -458
  39. package/daemon/client/FileBrowser.tsx +241 -203
  40. package/daemon/client/FileViewer.tsx +220 -211
  41. package/daemon/client/OverviewView.tsx +283 -267
  42. package/daemon/client/ReviewerIdentityDialog.tsx +72 -71
  43. package/daemon/client/RevisionSelect.tsx +111 -95
  44. package/daemon/client/SessionSwitcher.tsx +81 -81
  45. package/daemon/client/ShareDialog.tsx +211 -212
  46. package/daemon/client/apiSchemas.ts +78 -0
  47. package/daemon/client/canvas-runtime.ts +1 -1
  48. package/daemon/client/clientApi.ts +194 -206
  49. package/daemon/client/components/SecretInput.tsx +138 -138
  50. package/daemon/client/components/index.ts +2 -2
  51. package/daemon/client/domDiff.ts +341 -333
  52. package/daemon/client/errorMessage.ts +3 -0
  53. package/daemon/client/exportMarkdown.ts +222 -201
  54. package/daemon/client/lcs.ts +19 -14
  55. package/daemon/client/shareCrypto.ts +57 -57
  56. package/daemon/client/unifiedDiff.ts +660 -649
  57. package/daemon/dist/annotations-runtime.js +1 -1
  58. package/daemon/dist/client.css +2 -2
  59. package/daemon/dist/client.js +77 -80
  60. package/daemon/dist/components.js +59 -59
  61. package/daemon/dist/preact-compat.js +1 -1
  62. package/daemon/dist/review.js +361 -0
  63. package/daemon/dist/runtime.js +1 -1
  64. package/daemon/langMap.ts +24 -6
  65. package/daemon/review-widget/anchor.ts +187 -0
  66. package/daemon/review-widget/client.ts +95 -0
  67. package/daemon/review-widget/index.tsx +36 -0
  68. package/daemon/review-widget/markup.ts +71 -0
  69. package/daemon/review-widget/screenshot.ts +91 -0
  70. package/daemon/review-widget/source.ts +103 -0
  71. package/daemon/review-widget/styles.css +359 -0
  72. package/daemon/review-widget/text-modules.d.ts +4 -0
  73. package/daemon/review-widget/tsconfig.json +16 -0
  74. package/daemon/review-widget/widget.tsx +436 -0
  75. package/daemon/src/components.ts +3 -3
  76. package/daemon/src/handlers/api.ts +330 -328
  77. package/daemon/src/handlers/files.ts +112 -106
  78. package/daemon/src/handlers/static.ts +47 -46
  79. package/daemon/src/handlers/uploads.ts +61 -56
  80. package/daemon/src/paths.ts +13 -10
  81. package/daemon/src/remote-feedback.ts +192 -192
  82. package/daemon/src/review/markdown.ts +52 -0
  83. package/daemon/src/review/model.ts +132 -0
  84. package/daemon/src/review/proxy.ts +177 -0
  85. package/daemon/src/review/server.ts +199 -0
  86. package/daemon/src/review/store.ts +151 -0
  87. package/daemon/src/server.ts +127 -86
  88. package/daemon/src/share.ts +163 -169
  89. package/daemon/src/stubFetch.ts +7 -0
  90. package/daemon/tsconfig.json +19 -19
  91. package/package.json +47 -47
  92. package/skills/canvas/SKILL.md +5 -1
  93. package/bin/lib/commands/exec.test.ts +0 -27
  94. package/bin/lib/helpers.test.ts +0 -79
  95. package/daemon/src/files-api.test.ts +0 -78
  96. package/daemon/src/remote-feedback.test.ts +0 -165
  97. package/daemon/src/secrets-api.test.ts +0 -123
  98. package/daemon/src/share-e2e.test.ts +0 -277
  99. package/daemon/src/share.test.ts +0 -289
@@ -0,0 +1,3 @@
1
+ export function errorMessage(error: unknown): string {
2
+ return error instanceof Error && error.message ? error.message : String(error)
3
+ }
@@ -2,272 +2,293 @@
2
2
  * Export the rendered plan canvas to markdown.
3
3
  * Reads data-md attributes from known components, falls back to text extraction for unknown elements.
4
4
  */
5
- import { LANG_MAP } from "../langMap";
5
+ import { LANG_MAP } from '../langMap'
6
6
  export function exportCanvasToMarkdown(container: HTMLElement): string {
7
- const lines: string[] = [];
8
- walkChildren(container, lines, 0);
9
- return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
7
+ const lines: string[] = []
8
+ walkChildren(container, lines, 0)
9
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n'
10
10
  }
11
11
 
12
- const INLINE_TAGS = new Set(["CODE", "SPAN", "STRONG", "EM", "A", "B", "I", "U", "SMALL", "SUB", "SUP", "MARK", "ABBR"]);
12
+ const INLINE_TAGS = new Set(['CODE', 'SPAN', 'STRONG', 'EM', 'A', 'B', 'I', 'U', 'SMALL', 'SUB', 'SUP', 'MARK', 'ABBR'])
13
13
 
14
14
  function walkChildren(el: HTMLElement, lines: string[], depth: number) {
15
- // Detect if this element contains only inline content (text nodes + inline elements).
16
- // If so, concatenate into a single paragraph instead of splitting across lines.
17
- if (isInlineContainer(el)) {
18
- const text = extractInlineText(el);
19
- if (text) lines.push(text, "");
20
- return;
21
- }
22
-
23
- for (const child of el.childNodes) {
24
- if (child.nodeType === Node.ELEMENT_NODE) {
25
- walkNode(child as HTMLElement, lines, depth);
26
- } else if (child.nodeType === Node.TEXT_NODE) {
27
- const text = child.textContent?.trim();
28
- if (text) lines.push(text, "");
29
- }
30
- }
15
+ // Detect if this element contains only inline content (text nodes + inline elements).
16
+ // If so, concatenate into a single paragraph instead of splitting across lines.
17
+ if (isInlineContainer(el)) {
18
+ const text = extractInlineText(el)
19
+ if (text) lines.push(text, '')
20
+ return
21
+ }
22
+
23
+ for (const child of el.childNodes) {
24
+ if (child.nodeType === Node.ELEMENT_NODE) {
25
+ walkNode(child as HTMLElement, lines, depth)
26
+ } else if (child.nodeType === Node.TEXT_NODE) {
27
+ const text = child.textContent?.trim()
28
+ if (text) lines.push(text, '')
29
+ }
30
+ }
31
31
  }
32
32
 
33
33
  /** Check if an element contains only text nodes and inline elements (no block-level children) */
34
34
  function isInlineContainer(el: HTMLElement): boolean {
35
- for (const child of el.childNodes) {
36
- if (child.nodeType === Node.TEXT_NODE) continue;
37
- if (child.nodeType === Node.ELEMENT_NODE) {
38
- const childEl = child as HTMLElement;
39
- if (!INLINE_TAGS.has(childEl.tagName) || childEl.hasAttribute("data-md")) return false;
40
- }
41
- }
42
- return el.childNodes.length > 0;
35
+ for (const child of el.childNodes) {
36
+ if (child.nodeType === Node.TEXT_NODE) continue
37
+ if (child.nodeType === Node.ELEMENT_NODE) {
38
+ const childEl = child as HTMLElement
39
+ if (!INLINE_TAGS.has(childEl.tagName) || childEl.hasAttribute('data-md')) return false
40
+ }
41
+ }
42
+ return el.childNodes.length > 0
43
43
  }
44
44
 
45
45
  /** Extract text from an inline container, wrapping <code> in backticks */
46
46
  function extractInlineText(el: HTMLElement): string {
47
- let result = "";
48
- for (const child of el.childNodes) {
49
- if (child.nodeType === Node.TEXT_NODE) {
50
- result += child.textContent || "";
51
- } else if (child.nodeType === Node.ELEMENT_NODE) {
52
- const childEl = child as HTMLElement;
53
- if (childEl.tagName === "CODE") {
54
- result += "`" + (childEl.textContent || "") + "`";
55
- } else {
56
- result += childEl.textContent || "";
57
- }
58
- }
59
- }
60
- return result.trim();
47
+ let result = ''
48
+ for (const child of el.childNodes) {
49
+ if (child.nodeType === Node.TEXT_NODE) {
50
+ result += child.textContent || ''
51
+ } else if (child.nodeType === Node.ELEMENT_NODE) {
52
+ const childEl = child as HTMLElement
53
+ if (childEl.tagName === 'CODE') {
54
+ result += '`' + (childEl.textContent || '') + '`'
55
+ } else {
56
+ result += childEl.textContent || ''
57
+ }
58
+ }
59
+ }
60
+ return result.trim()
61
61
  }
62
62
 
63
63
  function walkNode(el: HTMLElement, lines: string[], depth: number) {
64
- const md = el.getAttribute("data-md");
65
-
66
- if (md) {
67
- switch (md) {
68
- case "section": return handleSection(el, lines, depth);
69
- case "item": return handleItem(el, lines);
70
- case "codeblock": return handleCodeBlock(el, lines);
71
- case "callout": return handleCallout(el, lines);
72
- case "note": return handleNote(el, lines);
73
- case "markdown": return handleMarkdown(el, lines);
74
- case "table": return handleTable(el, lines);
75
- case "checklist": return handleChecklist(el, lines);
76
- case "diff": return handleDiff(el, lines);
77
- case "mermaid": return handleMermaid(el, lines);
78
- case "choice": return handleChoice(el, lines);
79
- case "multichoice": return handleMultiChoice(el, lines);
80
- case "choice-option":
81
- case "multichoice-option": return; // handled by parent
82
- case "userinput": return handleUserInput(el, lines);
83
- case "rangeinput": return handleRangeInput(el, lines);
84
- case "checklist-item": return; // handled by parent (checklist)
85
- case "filepreview": return handleFilePreview(el, lines);
86
- case "priority": return handlePriority(el, lines);
87
- }
88
- }
89
-
90
- // Skip interactive widgets (custom components with buttons/inputs/selects)
91
- if (el.querySelector("button, input, select, textarea")) return;
92
-
93
- // Unknown element — if it has text, extract it; if it has children, recurse
94
- if (el.children.length > 0) {
95
- walkChildren(el, lines, depth);
96
- } else {
97
- const text = el.textContent?.trim();
98
- if (text) lines.push(text, "");
99
- }
64
+ const md = el.getAttribute('data-md')
65
+
66
+ if (md) {
67
+ switch (md) {
68
+ case 'section':
69
+ return handleSection(el, lines, depth)
70
+ case 'item':
71
+ return handleItem(el, lines)
72
+ case 'codeblock':
73
+ return handleCodeBlock(el, lines)
74
+ case 'callout':
75
+ return handleCallout(el, lines)
76
+ case 'note':
77
+ return handleNote(el, lines)
78
+ case 'markdown':
79
+ return handleMarkdown(el, lines)
80
+ case 'table':
81
+ return handleTable(el, lines)
82
+ case 'checklist':
83
+ return handleChecklist(el, lines)
84
+ case 'diff':
85
+ return handleDiff(el, lines)
86
+ case 'mermaid':
87
+ return handleMermaid(el, lines)
88
+ case 'choice':
89
+ return handleChoice(el, lines)
90
+ case 'multichoice':
91
+ return handleMultiChoice(el, lines)
92
+ case 'choice-option':
93
+ case 'multichoice-option':
94
+ return // handled by parent
95
+ case 'userinput':
96
+ return handleUserInput(el, lines)
97
+ case 'rangeinput':
98
+ return handleRangeInput(el, lines)
99
+ case 'checklist-item':
100
+ return // handled by parent (checklist)
101
+ case 'filepreview':
102
+ return handleFilePreview(el, lines)
103
+ case 'priority':
104
+ return handlePriority(el, lines)
105
+ }
106
+ }
107
+
108
+ // Skip interactive widgets (custom components with buttons/inputs/selects)
109
+ if (el.querySelector('button, input, select, textarea')) return
110
+
111
+ // Unknown element — if it has text, extract it; if it has children, recurse
112
+ if (el.children.length > 0) {
113
+ walkChildren(el, lines, depth)
114
+ } else {
115
+ const text = el.textContent?.trim()
116
+ if (text) lines.push(text, '')
117
+ }
100
118
  }
101
119
 
102
120
  function handleSection(el: HTMLElement, lines: string[], depth: number) {
103
- const title = el.getAttribute("data-md-title") || "Untitled";
104
- const prefix = depth === 0 ? "##" : "###";
105
- lines.push(`${prefix} ${title}`, "");
106
-
107
- // Process section content (skip the heading div, process the content div)
108
- // Use :last-of-type instead of :last-child — portaled <button> elements from
109
- // BlockCommentButtons can be appended as the last child, breaking :last-child
110
- const contentDiv = el.querySelector(":scope > div:last-of-type");
111
- if (contentDiv) {
112
- walkChildren(contentDiv as HTMLElement, lines, depth + 1);
113
- }
121
+ const title = el.getAttribute('data-md-title') || 'Untitled'
122
+ const prefix = depth === 0 ? '##' : '###'
123
+ lines.push(`${prefix} ${title}`, '')
124
+
125
+ // Process section content (skip the heading div, process the content div)
126
+ // Use :last-of-type instead of :last-child — portaled <button> elements from
127
+ // BlockCommentButtons can be appended as the last child, breaking :last-child
128
+ const contentDiv = el.querySelector(':scope > div:last-of-type')
129
+ if (contentDiv) {
130
+ walkChildren(contentDiv as HTMLElement, lines, depth + 1)
131
+ }
114
132
  }
115
133
 
116
134
  function handleItem(el: HTMLElement, lines: string[]) {
117
- const label = el.getAttribute("data-md-label") || "";
118
- const status = el.getAttribute("data-md-status");
119
- const badge = el.getAttribute("data-md-badge");
120
-
121
- let prefix = "-";
122
- if (status === "done") prefix = "- [x]";
123
- else if (status) prefix = "- [ ]";
124
-
125
- let line = `${prefix} **${label}**`;
126
- if (badge) line += ` \`${badge}\``;
127
- lines.push(line);
128
-
129
- // Walk children content (description, nested CodeBlock, Callout, Priority, etc.)
130
- const childrenDiv = el.querySelector(".text-text-secondary");
131
- if (childrenDiv) {
132
- const childLines: string[] = [];
133
- walkChildren(childrenDiv as HTMLElement, childLines, 0);
134
- // Indent child content under the list item
135
- for (const cl of childLines) {
136
- lines.push(cl ? ` ${cl}` : "");
137
- }
138
- }
139
- lines.push("");
135
+ const label = el.getAttribute('data-md-label') || ''
136
+ const status = el.getAttribute('data-md-status')
137
+ const badge = el.getAttribute('data-md-badge')
138
+
139
+ let prefix = '-'
140
+ if (status === 'done') prefix = '- [x]'
141
+ else if (status) prefix = '- [ ]'
142
+
143
+ let line = `${prefix} **${label}**`
144
+ if (badge) line += ` \`${badge}\``
145
+ lines.push(line)
146
+
147
+ // Walk children content (description, nested CodeBlock, Callout, Priority, etc.)
148
+ const childrenDiv = el.querySelector('.text-text-secondary')
149
+ if (childrenDiv) {
150
+ const childLines: string[] = []
151
+ walkChildren(childrenDiv as HTMLElement, childLines, 0)
152
+ // Indent child content under the list item
153
+ for (const cl of childLines) {
154
+ lines.push(cl ? ` ${cl}` : '')
155
+ }
156
+ }
157
+ lines.push('')
140
158
  }
141
159
 
142
160
  function handleCodeBlock(el: HTMLElement, lines: string[]) {
143
- const language = el.getAttribute("data-md-language") || "";
144
- const code = el.querySelector("code");
145
- const text = code?.textContent || "";
146
- lines.push(`\`\`\`${language}`, text, "```", "");
161
+ const language = el.getAttribute('data-md-language') || ''
162
+ const code = el.querySelector('code')
163
+ const text = code?.textContent || ''
164
+ lines.push(`\`\`\`${language}`, text, '```', '')
147
165
  }
148
166
 
149
167
  function handleCallout(el: HTMLElement, lines: string[]) {
150
- const type = el.getAttribute("data-md-type") || "info";
151
- const labels: Record<string, string> = { info: "Info", warning: "Warning", danger: "Danger", tip: "Tip" };
152
- const content = extractText(el.querySelector(".text-text-secondary") as HTMLElement || el);
153
- lines.push(`> **${labels[type] || type}:** ${content}`, "");
168
+ const type = el.getAttribute('data-md-type') || 'info'
169
+ const labels: Record<string, string> = { info: 'Info', warning: 'Warning', danger: 'Danger', tip: 'Tip' }
170
+ const content = extractText(el.querySelector('.text-text-secondary') as HTMLElement || el)
171
+ lines.push(`> **${labels[type] || type}:** ${content}`, '')
154
172
  }
155
173
 
156
174
  function handleNote(el: HTMLElement, lines: string[]) {
157
- const text = el.textContent?.trim() || "";
158
- lines.push(`> *${text}*`, "");
175
+ const text = el.textContent?.trim() || ''
176
+ lines.push(`> *${text}*`, '')
159
177
  }
160
178
 
161
179
  function handleTable(el: HTMLElement, lines: string[]) {
162
- const table = el.querySelector("table");
163
- if (!table) return;
164
-
165
- const headers: string[] = [];
166
- for (const th of table.querySelectorAll("thead th")) {
167
- headers.push(th.textContent?.trim() || "");
168
- }
169
- if (headers.length === 0) return;
170
-
171
- lines.push(`| ${headers.join(" | ")} |`);
172
- lines.push(`| ${headers.map(() => "---").join(" | ")} |`);
173
-
174
- for (const tr of table.querySelectorAll("tbody tr")) {
175
- const cells: string[] = [];
176
- for (const td of tr.querySelectorAll("td")) {
177
- cells.push(td.textContent?.trim() || "");
178
- }
179
- lines.push(`| ${cells.join(" | ")} |`);
180
- }
181
- lines.push("");
180
+ const table = el.querySelector('table')
181
+ if (!table) return
182
+
183
+ const headers: string[] = []
184
+ for (const th of table.querySelectorAll('thead th')) {
185
+ headers.push(th.textContent?.trim() || '')
186
+ }
187
+ if (headers.length === 0) return
188
+
189
+ lines.push(`| ${headers.join(' | ')} |`)
190
+ lines.push(`| ${headers.map(() => '---').join(' | ')} |`)
191
+
192
+ for (const tr of table.querySelectorAll('tbody tr')) {
193
+ const cells: string[] = []
194
+ for (const td of tr.querySelectorAll('td')) {
195
+ cells.push(td.textContent?.trim() || '')
196
+ }
197
+ lines.push(`| ${cells.join(' | ')} |`)
198
+ }
199
+ lines.push('')
182
200
  }
183
201
 
184
202
  function handleChecklist(el: HTMLElement, lines: string[]) {
185
- for (const li of el.querySelectorAll("li")) {
186
- const hasCheck = li.querySelector(".bg-accent-green") !== null;
187
- const label = li.textContent?.trim() || "";
188
- lines.push(`- [${hasCheck ? "x" : " "}] ${label}`);
189
- }
190
- lines.push("");
203
+ for (const li of el.querySelectorAll('li')) {
204
+ const hasCheck = li.querySelector('.bg-accent-green') !== null
205
+ const label = li.textContent?.trim() || ''
206
+ lines.push(`- [${hasCheck ? 'x' : ' '}] ${label}`)
207
+ }
208
+ lines.push('')
191
209
  }
192
210
 
193
211
  function handleDiff(el: HTMLElement, lines: string[]) {
194
- const language = el.getAttribute("data-md-language") || "";
195
- lines.push(`\`\`\`diff${language ? ` (${language})` : ""}`);
196
- for (const row of el.querySelectorAll("[class*='flex']")) {
197
- const prefix = row.querySelector(".text-accent-red") ? "-" :
198
- row.querySelector(".text-accent-green") ? "+" : " ";
199
- const code = row.querySelector("code");
200
- if (code) lines.push(`${prefix} ${code.textContent}`);
201
- }
202
- lines.push("```", "");
212
+ const language = el.getAttribute('data-md-language') || ''
213
+ lines.push(`\`\`\`diff${language ? ` (${language})` : ''}`)
214
+ for (const row of el.querySelectorAll("[class*='flex']")) {
215
+ const prefix = row.querySelector('.text-accent-red')
216
+ ? '-'
217
+ : row.querySelector('.text-accent-green')
218
+ ? '+'
219
+ : ' '
220
+ const code = row.querySelector('code')
221
+ if (code) lines.push(`${prefix} ${code.textContent}`)
222
+ }
223
+ lines.push('```', '')
203
224
  }
204
225
 
205
226
  function handleMermaid(el: HTMLElement, lines: string[]) {
206
- const source = el.getAttribute("data-md-source") || "";
207
- if (source) {
208
- lines.push("```mermaid", source, "```", "");
209
- }
227
+ const source = el.getAttribute('data-md-source') || ''
228
+ if (source) {
229
+ lines.push('```mermaid', source, '```', '')
230
+ }
210
231
  }
211
232
 
212
233
  function handleMarkdown(el: HTMLElement, lines: string[]) {
213
- const source = el.getAttribute("data-md-source") || "";
214
- if (source) lines.push(source, "");
234
+ const source = el.getAttribute('data-md-source') || ''
235
+ if (source) lines.push(source, '')
215
236
  }
216
237
 
217
238
  function handleChoice(el: HTMLElement, lines: string[]) {
218
- const label = el.getAttribute("data-md-label") || "";
219
- lines.push(`**${label}:**`);
220
- for (const opt of el.querySelectorAll("[data-md='choice-option']")) {
221
- const optLabel = opt.getAttribute("data-md-label") || opt.textContent?.trim() || "";
222
- const selected = opt.querySelector(".bg-accent-amber") !== null;
223
- lines.push(`- ${selected ? "(x)" : "( )"} ${optLabel}`);
224
- }
225
- lines.push("");
239
+ const label = el.getAttribute('data-md-label') || ''
240
+ lines.push(`**${label}:**`)
241
+ for (const opt of el.querySelectorAll("[data-md='choice-option']")) {
242
+ const optLabel = opt.getAttribute('data-md-label') || opt.textContent?.trim() || ''
243
+ const selected = opt.querySelector('.bg-accent-amber') !== null
244
+ lines.push(`- ${selected ? '(x)' : '( )'} ${optLabel}`)
245
+ }
246
+ lines.push('')
226
247
  }
227
248
 
228
249
  function handleMultiChoice(el: HTMLElement, lines: string[]) {
229
- const label = el.getAttribute("data-md-label") || "";
230
- lines.push(`**${label}:**`);
231
- for (const opt of el.querySelectorAll("[data-md='multichoice-option']")) {
232
- const optLabel = opt.getAttribute("data-md-label") || opt.textContent?.trim() || "";
233
- const checked = opt.querySelector(".bg-accent-amber") !== null;
234
- lines.push(`- [${checked ? "x" : " "}] ${optLabel}`);
235
- }
236
- lines.push("");
250
+ const label = el.getAttribute('data-md-label') || ''
251
+ lines.push(`**${label}:**`)
252
+ for (const opt of el.querySelectorAll("[data-md='multichoice-option']")) {
253
+ const optLabel = opt.getAttribute('data-md-label') || opt.textContent?.trim() || ''
254
+ const checked = opt.querySelector('.bg-accent-amber') !== null
255
+ lines.push(`- [${checked ? 'x' : ' '}] ${optLabel}`)
256
+ }
257
+ lines.push('')
237
258
  }
238
259
 
239
260
  function handleUserInput(el: HTMLElement, lines: string[]) {
240
- const label = el.getAttribute("data-md-label") || "";
241
- const textarea = el.querySelector("textarea") as HTMLTextAreaElement | null;
242
- const value = textarea?.value?.trim() || "";
243
- lines.push(`**${label}:** ${value || "_empty_"}`, "");
261
+ const label = el.getAttribute('data-md-label') || ''
262
+ const textarea = el.querySelector('textarea') as HTMLTextAreaElement | null
263
+ const value = textarea?.value?.trim() || ''
264
+ lines.push(`**${label}:** ${value || '_empty_'}`, '')
244
265
  }
245
266
 
246
267
  function handleRangeInput(el: HTMLElement, lines: string[]) {
247
- const label = el.getAttribute("data-md-label") || "";
248
- const input = el.querySelector("input[type='range']") as HTMLInputElement | null;
249
- const display = el.querySelector(".font-mono");
250
- const value = display?.textContent?.trim() || input?.value || "—";
251
- lines.push(`**${label}:** ${value}`, "");
268
+ const label = el.getAttribute('data-md-label') || ''
269
+ const input = el.querySelector("input[type='range']") as HTMLInputElement | null
270
+ const display = el.querySelector('.font-mono')
271
+ const value = display?.textContent?.trim() || input?.value || '—'
272
+ lines.push(`**${label}:** ${value}`, '')
252
273
  }
253
274
 
254
275
  function handleFilePreview(el: HTMLElement, lines: string[]) {
255
- const path = el.getAttribute("data-md-path") || "";
256
- const linesAttr = el.getAttribute("data-md-lines") || "";
257
- const code = el.querySelector("code");
258
- const text = code?.textContent || "";
259
- const ext = path.split(".").pop() || "";
260
- const lang = LANG_MAP[ext] || "";
261
- const header = linesAttr ? `${path} (L${linesAttr})` : path;
262
- lines.push(header, "", `\`\`\`${lang}`, text, "```", "");
276
+ const path = el.getAttribute('data-md-path') || ''
277
+ const linesAttr = el.getAttribute('data-md-lines') || ''
278
+ const code = el.querySelector('code')
279
+ const text = code?.textContent || ''
280
+ const ext = path.split('.').pop() || ''
281
+ const lang = LANG_MAP[ext] || ''
282
+ const header = linesAttr ? `${path} (L${linesAttr})` : path
283
+ lines.push(header, '', `\`\`\`${lang}`, text, '```', '')
263
284
  }
264
285
 
265
286
  function handlePriority(_el: HTMLElement, _lines: string[]) {
266
- // Priority is rendered inline within Task items — the Task handler already
267
- // captures it as part of child content. Skip standalone rendering to avoid
268
- // duplicating the bare level text (e.g. "high").
287
+ // Priority is rendered inline within Task items — the Task handler already
288
+ // captures it as part of child content. Skip standalone rendering to avoid
289
+ // duplicating the bare level text (e.g. "high").
269
290
  }
270
291
 
271
292
  function extractText(el: HTMLElement): string {
272
- return el?.textContent?.trim() || "";
293
+ return el?.textContent?.trim() || ''
273
294
  }
@@ -1,19 +1,24 @@
1
1
  /** Simple O(mn) space LCS for small inputs. */
2
2
  export function simpleLCS(a: string[], b: string[]): string[] {
3
- const m = a.length, n = b.length;
4
- if (m === 0 || n === 0) return [];
3
+ const m = a.length, n = b.length
4
+ if (m === 0 || n === 0) return []
5
5
 
6
- const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
7
- for (let i = 1; i <= m; i++)
8
- for (let j = 1; j <= n; j++)
9
- dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
6
+ const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))
7
+ for (let i = 1; i <= m; i++) {
8
+ for (let j = 1; j <= n; j++) {
9
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1])
10
+ }
11
+ }
10
12
 
11
- const result: string[] = [];
12
- let i = m, j = n;
13
- while (i > 0 && j > 0) {
14
- if (a[i - 1] === b[j - 1]) { result.unshift(a[i - 1]); i--; j--; }
15
- else if (dp[i - 1][j] > dp[i][j - 1]) i--;
16
- else j--;
17
- }
18
- return result;
13
+ const result: string[] = []
14
+ let i = m, j = n
15
+ while (i > 0 && j > 0) {
16
+ if (a[i - 1] === b[j - 1]) {
17
+ result.unshift(a[i - 1])
18
+ i--
19
+ j--
20
+ } else if (dp[i - 1][j] > dp[i][j - 1]) i--
21
+ else j--
22
+ }
23
+ return result
19
24
  }