agent-canvas 0.2.1 → 0.7.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.
- package/README.md +42 -17
- package/bin/agent-canvas.js +7 -7
- package/bin/agent-canvas.ts +48 -37
- package/bin/lib/commands/daemon.ts +46 -43
- package/bin/lib/commands/exec.ts +100 -100
- package/bin/lib/commands/fetch.ts +15 -15
- package/bin/lib/commands/install.ts +36 -36
- package/bin/lib/commands/instructions.ts +51 -51
- package/bin/lib/commands/push.ts +115 -97
- package/bin/lib/commands/review.ts +190 -0
- package/bin/lib/commands/watch.ts +24 -24
- package/bin/lib/config.ts +12 -12
- package/bin/lib/daemon-lifecycle.ts +47 -51
- package/bin/lib/helpers.ts +82 -76
- package/bin/lib/instructions/app-review.md +88 -0
- package/bin/lib/instructions/component-checklist.md +7 -5
- package/bin/lib/instructions/component-custom.md +20 -14
- package/bin/lib/instructions/component-diff.md +4 -3
- package/bin/lib/instructions/component-item.md +18 -8
- package/bin/lib/instructions/component-markdown.md +4 -2
- package/bin/lib/instructions/component-mermaid.md +12 -6
- package/bin/lib/instructions/component-multichoice.md +6 -2
- package/bin/lib/instructions/component-note.md +1 -1
- package/bin/lib/instructions/component-priority.md +1 -1
- package/bin/lib/instructions/component-rangeinput.md +9 -2
- package/bin/lib/instructions/component-secretinput.md +12 -11
- package/bin/lib/instructions/component-svg.md +62 -16
- package/bin/lib/instructions/component-table.md +6 -6
- package/bin/lib/instructions/component-usefeedback.md +8 -8
- package/bin/lib/instructions/component-userinput.md +6 -2
- package/bin/lib/instructions/flow-feature.md +22 -5
- package/bin/lib/instructions/flow-plan.md +5 -5
- package/bin/lib/instructions/flow-runbook.md +28 -25
- package/bin/lib/instructions/overview.md +27 -18
- package/bin/lib/instructions/theming.md +5 -0
- package/daemon/build.ts +171 -152
- package/daemon/client/App.tsx +1205 -1077
- package/daemon/client/CompareView.tsx +478 -458
- package/daemon/client/FileBrowser.tsx +241 -203
- package/daemon/client/FileViewer.tsx +220 -211
- package/daemon/client/OverviewView.tsx +283 -267
- package/daemon/client/ReviewerIdentityDialog.tsx +72 -71
- package/daemon/client/RevisionSelect.tsx +111 -95
- package/daemon/client/SessionSwitcher.tsx +81 -81
- package/daemon/client/ShareDialog.tsx +211 -212
- package/daemon/client/apiSchemas.ts +78 -0
- package/daemon/client/canvas-runtime.ts +1 -1
- package/daemon/client/clientApi.ts +194 -206
- package/daemon/client/components/SecretInput.tsx +138 -138
- package/daemon/client/components/index.ts +2 -2
- package/daemon/client/domDiff.ts +341 -333
- package/daemon/client/errorMessage.ts +3 -0
- package/daemon/client/exportMarkdown.ts +222 -201
- package/daemon/client/lcs.ts +19 -14
- package/daemon/client/shareCrypto.ts +57 -57
- package/daemon/client/unifiedDiff.ts +660 -649
- package/daemon/dist/annotations-runtime.js +1 -1
- package/daemon/dist/client.css +2 -2
- package/daemon/dist/client.js +77 -80
- package/daemon/dist/components.js +59 -59
- package/daemon/dist/preact-compat.js +1 -1
- package/daemon/dist/review.js +361 -0
- package/daemon/dist/runtime.js +1 -1
- package/daemon/langMap.ts +24 -6
- package/daemon/review-widget/anchor.ts +187 -0
- package/daemon/review-widget/client.ts +95 -0
- package/daemon/review-widget/index.tsx +36 -0
- package/daemon/review-widget/markup.ts +71 -0
- package/daemon/review-widget/screenshot.ts +91 -0
- package/daemon/review-widget/source.ts +103 -0
- package/daemon/review-widget/styles.css +359 -0
- package/daemon/review-widget/text-modules.d.ts +4 -0
- package/daemon/review-widget/tsconfig.json +16 -0
- package/daemon/review-widget/widget.tsx +436 -0
- package/daemon/src/components.ts +3 -3
- package/daemon/src/handlers/api.ts +330 -328
- package/daemon/src/handlers/files.ts +112 -106
- package/daemon/src/handlers/static.ts +47 -46
- package/daemon/src/handlers/uploads.ts +61 -56
- package/daemon/src/paths.ts +13 -10
- package/daemon/src/remote-feedback.ts +192 -192
- package/daemon/src/review/markdown.ts +52 -0
- package/daemon/src/review/model.ts +132 -0
- package/daemon/src/review/proxy.ts +177 -0
- package/daemon/src/review/server.ts +199 -0
- package/daemon/src/review/store.ts +151 -0
- package/daemon/src/server.ts +127 -86
- package/daemon/src/share.ts +163 -169
- package/daemon/src/stubFetch.ts +7 -0
- package/daemon/tsconfig.json +19 -19
- package/package.json +47 -47
- package/skills/canvas/SKILL.md +5 -1
- package/bin/lib/commands/exec.test.ts +0 -27
- package/bin/lib/helpers.test.ts +0 -79
- package/daemon/src/files-api.test.ts +0 -78
- package/daemon/src/remote-feedback.test.ts +0 -165
- package/daemon/src/secrets-api.test.ts +0 -123
- package/daemon/src/share-e2e.test.ts +0 -277
- package/daemon/src/share.test.ts +0 -289
|
@@ -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
|
|
5
|
+
import { LANG_MAP } from '../langMap'
|
|
6
6
|
export function exportCanvasToMarkdown(container: HTMLElement): string {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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([
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
158
|
-
|
|
175
|
+
const text = el.textContent?.trim() || ''
|
|
176
|
+
lines.push(`> *${text}*`, '')
|
|
159
177
|
}
|
|
160
178
|
|
|
161
179
|
function handleTable(el: HTMLElement, lines: string[]) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
293
|
+
return el?.textContent?.trim() || ''
|
|
273
294
|
}
|
package/daemon/client/lcs.ts
CHANGED
|
@@ -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
|
-
|
|
4
|
-
|
|
3
|
+
const m = a.length, n = b.length
|
|
4
|
+
if (m === 0 || n === 0) return []
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
}
|