agent-canvas 0.2.0 → 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.
- 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 -105
- 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 +9 -1
- package/bin/lib/commands/exec.test.ts +0 -27
- package/bin/lib/helpers.test.ts +0 -79
- 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
|
@@ -1,222 +1,231 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { FS_AVAILABLE } from "./clientApi";
|
|
1
|
+
import { SessionContext } from '#canvas/runtime'
|
|
2
|
+
import type { AnnotationContext } from '#canvas/runtime'
|
|
3
|
+
import { wrapRangeWithMark } from '@fabrika/annotations'
|
|
4
|
+
import { useTextAnnotation } from '@fabrika/annotations'
|
|
5
|
+
import { filePreviewResponseSchema, useAnnotations } from '@fabrika/canvas-kernel/client'
|
|
6
|
+
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
|
7
|
+
import { LANG_MAP } from '../langMap'
|
|
8
|
+
import { FS_AVAILABLE } from './clientApi'
|
|
10
9
|
|
|
11
10
|
/** Walk up from a node to find the parent line div with data-line-num */
|
|
12
11
|
function findLineDiv(node: Node): HTMLElement | null {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
let el: HTMLElement | null = node instanceof HTMLElement ? node : node.parentElement
|
|
13
|
+
while (el) {
|
|
14
|
+
if (el.hasAttribute('data-line-num')) return el
|
|
15
|
+
el = el.parentElement
|
|
16
|
+
}
|
|
17
|
+
return null
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
/** Build AnnotationContext from a text selection Range inside file content */
|
|
22
21
|
function buildFileContext(range: Range): AnnotationContext {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
22
|
+
const snippet = range.toString().trim()
|
|
23
|
+
const startLineDiv = findLineDiv(range.startContainer)
|
|
24
|
+
const endLineDiv = findLineDiv(range.endContainer)
|
|
25
|
+
const lineStart = startLineDiv ? parseInt(startLineDiv.getAttribute('data-line-num')!, 10) : undefined
|
|
26
|
+
const lineEnd = endLineDiv ? parseInt(endLineDiv.getAttribute('data-line-num')!, 10) : undefined
|
|
27
|
+
|
|
28
|
+
let before = ''
|
|
29
|
+
let after = ''
|
|
30
|
+
if (snippet.length < 30 && startLineDiv && startLineDiv === endLineDiv) {
|
|
31
|
+
const code = startLineDiv.querySelector('code')
|
|
32
|
+
if (code) {
|
|
33
|
+
const lineText = code.textContent || ''
|
|
34
|
+
const idx = lineText.indexOf(snippet)
|
|
35
|
+
if (idx >= 0) {
|
|
36
|
+
before = lineText.slice(0, idx).trimStart()
|
|
37
|
+
after = lineText.slice(idx + snippet.length).trimEnd()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { before, after, hierarchy: [], lineStart, lineEnd }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function HighlightedLine({ html }: { html: string }) {
|
|
46
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: highlight.js escapes the file text and adds only its own span markup.
|
|
47
|
+
return <code className="py-px" dangerouslySetInnerHTML={{ __html: html }} />
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
interface FileViewerProps {
|
|
47
|
-
|
|
51
|
+
path: string
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
export function FileViewer({ path }: FileViewerProps) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
55
|
+
if (!FS_AVAILABLE) {
|
|
56
|
+
return (
|
|
57
|
+
<div className="max-w-[720px] mx-auto px-6 pt-12 pb-32">
|
|
58
|
+
<div className="p-4 rounded-lg border border-border-subtle bg-bg-surface">
|
|
59
|
+
<div className="text-[11px] uppercase tracking-widest text-text-tertiary font-body mb-2">
|
|
60
|
+
File not available
|
|
61
|
+
</div>
|
|
62
|
+
<p className="text-[13px] text-text-secondary font-body leading-relaxed">
|
|
63
|
+
Opening <code className="px-1 py-0.5 rounded bg-bg-elevated text-text-primary text-[11px]">{path}</code>{' '}
|
|
64
|
+
requires filesystem access, which isn't available in shared view. If the canvas author wanted this file visible, they should embed it as a{' '}
|
|
65
|
+
<code className="px-1 py-0.5 rounded bg-bg-elevated text-text-primary text-[11px]"><FilePreview></code> in the canvas.
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
return <LocalFileViewer path={path} />
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function LocalFileViewer({ path }: FileViewerProps) {
|
|
75
|
+
const sessionId = useContext(SessionContext)
|
|
76
|
+
const { annotations } = useAnnotations()
|
|
77
|
+
const [content, setContent] = useState<string | null>(null)
|
|
78
|
+
const [loading, setLoading] = useState(true)
|
|
79
|
+
const [error, setError] = useState<string | null>(null)
|
|
80
|
+
const contentRef = useRef<HTMLPreElement>(null)
|
|
81
|
+
|
|
82
|
+
// Line gutter selection state
|
|
83
|
+
const [lineSelectStart, setLineSelectStart] = useState<number | null>(null)
|
|
84
|
+
const [lineSelectEnd, setLineSelectEnd] = useState<number | null>(null)
|
|
85
|
+
const lineSelectStartRef = useRef<number | null>(null)
|
|
86
|
+
const lineSelectEndRef = useRef<number | null>(null)
|
|
87
|
+
const linesRef = useRef<string[]>([])
|
|
88
|
+
|
|
89
|
+
const fileAnns = annotations.filter((a) => a.filePath === path)
|
|
90
|
+
|
|
91
|
+
const scrollContainer = contentRef.current?.closest('#plan-scroll-container') as HTMLElement | null
|
|
92
|
+
|
|
93
|
+
const { popovers, openCreatePopover } = useTextAnnotation({
|
|
94
|
+
containerRef: contentRef,
|
|
95
|
+
restoreKey: `${content}:${path}`,
|
|
96
|
+
restoreAnnotations: fileAnns,
|
|
97
|
+
extractContext: buildFileContext,
|
|
98
|
+
filePath: path,
|
|
99
|
+
scrollContainer,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
setLoading(true)
|
|
104
|
+
setError(null)
|
|
105
|
+
fetch(`/api/file?session=${sessionId}&path=${encodeURIComponent(path)}`)
|
|
106
|
+
.then((r) => r.json())
|
|
107
|
+
.then((raw) => {
|
|
108
|
+
const data = filePreviewResponseSchema.parse(raw)
|
|
109
|
+
if ('error' in data) {
|
|
110
|
+
setError(data.error)
|
|
111
|
+
setLoading(false)
|
|
112
|
+
} else {
|
|
113
|
+
setContent(data.content)
|
|
114
|
+
setLoading(false)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
.catch(() => {
|
|
118
|
+
setError('Failed to fetch file')
|
|
119
|
+
setLoading(false)
|
|
120
|
+
})
|
|
121
|
+
}, [sessionId, path])
|
|
122
|
+
|
|
123
|
+
// Line gutter: mousedown starts line selection
|
|
124
|
+
const handleGutterMouseDown = useCallback((e: React.MouseEvent, lineNum: number) => {
|
|
125
|
+
e.preventDefault() // Prevent text selection
|
|
126
|
+
lineSelectStartRef.current = lineNum
|
|
127
|
+
lineSelectEndRef.current = lineNum
|
|
128
|
+
setLineSelectStart(lineNum)
|
|
129
|
+
setLineSelectEnd(lineNum)
|
|
130
|
+
}, [])
|
|
131
|
+
|
|
132
|
+
// Line gutter: mousemove + mouseup for drag selection
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
const handleMouseMoveForLines = (e: MouseEvent) => {
|
|
135
|
+
if (lineSelectStartRef.current === null) return
|
|
136
|
+
const lineDiv = (e.target as HTMLElement).closest('[data-line-num]') as HTMLElement
|
|
137
|
+
if (lineDiv) {
|
|
138
|
+
const num = parseInt(lineDiv.getAttribute('data-line-num')!, 10)
|
|
139
|
+
lineSelectEndRef.current = num
|
|
140
|
+
setLineSelectEnd(num)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const handleMouseUpForLines = () => {
|
|
145
|
+
const start = lineSelectStartRef.current
|
|
146
|
+
if (start === null) return
|
|
147
|
+
lineSelectStartRef.current = null
|
|
148
|
+
const end = lineSelectEndRef.current ?? start
|
|
149
|
+
const startLine = Math.min(start, end)
|
|
150
|
+
const endLine = Math.max(start, end)
|
|
151
|
+
const currentLines = linesRef.current
|
|
152
|
+
const snippet = currentLines.slice(startLine - 1, endLine).join('\n')
|
|
153
|
+
if (!snippet.trim()) {
|
|
154
|
+
setLineSelectStart(null)
|
|
155
|
+
setLineSelectEnd(null)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const tempId = `__pending_${Date.now()}`
|
|
160
|
+
const ctx: AnnotationContext = { before: '', after: '', hierarchy: [], lineStart: startLine, lineEnd: endLine }
|
|
161
|
+
|
|
162
|
+
// Wrap code content of selected lines with marks
|
|
163
|
+
if (contentRef.current) {
|
|
164
|
+
const lineDivs = contentRef.current.querySelectorAll('[data-line-num]')
|
|
165
|
+
const firstDiv = lineDivs[startLine - 1] as HTMLElement
|
|
166
|
+
const lastDiv = lineDivs[endLine - 1] as HTMLElement
|
|
167
|
+
if (firstDiv && lastDiv) {
|
|
168
|
+
const firstCode = firstDiv.querySelector('code')
|
|
169
|
+
const lastCode = lastDiv.querySelector('code')
|
|
170
|
+
if (firstCode && lastCode) {
|
|
171
|
+
try {
|
|
172
|
+
const range = document.createRange()
|
|
173
|
+
range.setStartBefore(firstCode.firstChild || firstCode)
|
|
174
|
+
range.setEndAfter(lastCode.lastChild || lastCode)
|
|
175
|
+
wrapRangeWithMark(range, tempId)
|
|
176
|
+
} catch {}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const anchorEl = lineDivs[endLine - 1] as HTMLElement
|
|
180
|
+
if (anchorEl) {
|
|
181
|
+
openCreatePopover(anchorEl, tempId, snippet, ctx)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
setLineSelectStart(null)
|
|
185
|
+
setLineSelectEnd(null)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
document.addEventListener('mousemove', handleMouseMoveForLines)
|
|
189
|
+
document.addEventListener('mouseup', handleMouseUpForLines)
|
|
190
|
+
return () => {
|
|
191
|
+
document.removeEventListener('mousemove', handleMouseMoveForLines)
|
|
192
|
+
document.removeEventListener('mouseup', handleMouseUpForLines)
|
|
193
|
+
}
|
|
194
|
+
}, [])
|
|
195
|
+
|
|
196
|
+
// Highlight content
|
|
197
|
+
const highlightedLines = useMemo(() => {
|
|
198
|
+
if (!content) return null
|
|
199
|
+
const hljs = window.hljs
|
|
200
|
+
if (!hljs) return null
|
|
201
|
+
const ext = path.split('.').pop() || ''
|
|
202
|
+
const lang = LANG_MAP[ext]
|
|
203
|
+
try {
|
|
204
|
+
const result = lang ? hljs.highlight(content, { language: lang }) : hljs.highlightAuto(content)
|
|
205
|
+
return result.value.split('\n')
|
|
206
|
+
} catch {
|
|
207
|
+
return null
|
|
208
|
+
}
|
|
209
|
+
}, [content, path])
|
|
210
|
+
|
|
211
|
+
// Computed selected line range for highlighting during drag
|
|
212
|
+
const selectedLineRange = useMemo(() => {
|
|
213
|
+
if (lineSelectStart === null) return null
|
|
214
|
+
const end = lineSelectEnd ?? lineSelectStart
|
|
215
|
+
return { start: Math.min(lineSelectStart, end), end: Math.max(lineSelectStart, end) }
|
|
216
|
+
}, [lineSelectStart, lineSelectEnd])
|
|
217
|
+
|
|
218
|
+
if (loading) return <div className="flex items-center justify-center h-64 text-text-tertiary font-body text-[13px]">Loading {path}...</div>
|
|
219
|
+
if (error) return <div className="p-8 text-accent-red font-body text-[13px]">{error}</div>
|
|
220
|
+
if (!content) return null
|
|
221
|
+
|
|
222
|
+
const lines = content.split('\n')
|
|
223
|
+
linesRef.current = lines
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div>
|
|
227
|
+
{/* Code */}
|
|
228
|
+
<pre ref={contentRef} className="text-code font-mono text-text-code bg-bg-base select-text cursor-text py-3 hljs">
|
|
220
229
|
{lines.map((line, i) => {
|
|
221
230
|
const lineNum = i + 1;
|
|
222
231
|
const isSelected = selectedLineRange && lineNum >= selectedLineRange.start && lineNum <= selectedLineRange.end;
|
|
@@ -227,16 +236,16 @@ export function FileViewer({ path }: FileViewerProps) {
|
|
|
227
236
|
onMouseDown={(e) => handleGutterMouseDown(e, lineNum)}
|
|
228
237
|
>{lineNum}</span>
|
|
229
238
|
{highlightedLines ? (
|
|
230
|
-
<
|
|
239
|
+
<HighlightedLine html={highlightedLines[i] || " "} />
|
|
231
240
|
) : (
|
|
232
241
|
<code className="py-px">{line || " "}</code>
|
|
233
242
|
)}
|
|
234
243
|
</div>
|
|
235
244
|
);
|
|
236
245
|
})}
|
|
237
|
-
|
|
246
|
+
</pre>
|
|
238
247
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
248
|
+
{popovers}
|
|
249
|
+
</div>
|
|
250
|
+
)
|
|
242
251
|
}
|