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.
- 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
|
@@ -1,326 +1,342 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import { SessionContext } from '#canvas/runtime'
|
|
2
|
+
import { PlanRenderer } from '@fabrika/canvas-kernel/client'
|
|
3
|
+
import { ActiveViewContext, RevisionContext, type RevisionInfo } from '@fabrika/canvas-kernel/client'
|
|
4
|
+
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
|
5
|
+
import { ComparePanelRenderer } from './CompareView'
|
|
6
|
+
import { runDomDiff } from './domDiff'
|
|
7
|
+
import { buildUnifiedDom, extractBlockTree, matchBlocks } from './unifiedDiff'
|
|
8
8
|
|
|
9
9
|
interface OverviewViewProps {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
revision: number
|
|
11
|
+
responseBanner?: React.ReactNode
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
interface FileChange {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
filename: string
|
|
16
|
+
status: 'new' | 'changed' | 'unchanged'
|
|
17
|
+
diffStats?: { added: number; removed: number }
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function categorizeChanges(
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
currentRev: RevisionInfo | undefined,
|
|
22
|
+
previousRev: RevisionInfo | undefined,
|
|
23
23
|
): FileChange[] {
|
|
24
|
-
|
|
24
|
+
if (!currentRev) return []
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
return currentRev.canvasFiles.map(cf => {
|
|
27
|
+
if (!cf.diffStats) {
|
|
28
|
+
return { filename: cf.filename, status: 'new' as const }
|
|
29
|
+
}
|
|
30
|
+
if (cf.diffStats.added === 0 && cf.diffStats.removed === 0) {
|
|
31
|
+
return { filename: cf.filename, status: 'unchanged' as const, diffStats: cf.diffStats }
|
|
32
|
+
}
|
|
33
|
+
return { filename: cf.filename, status: 'changed' as const, diffStats: cf.diffStats }
|
|
34
|
+
})
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export function getAffectedFiles(changes: FileChange[]): string[] {
|
|
38
|
-
|
|
38
|
+
return changes.filter(c => c.status !== 'unchanged').map(c => c.filename)
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function OverviewView({ revision, responseBanner }: OverviewViewProps) {
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
const { setActiveView } = useContext(ActiveViewContext)
|
|
43
|
+
const { revisions } = useContext(RevisionContext)
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const currentRev = revisions.find(r => r.revision === revision)
|
|
46
|
+
const previousRev = revisions.find(r => r.revision === revision - 1)
|
|
47
|
+
const changes = useMemo(() => categorizeChanges(currentRev, previousRev), [currentRev, previousRev])
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
const affected = changes.filter(c => c.status !== 'unchanged')
|
|
50
|
+
const unchanged = changes.filter(c => c.status === 'unchanged')
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
return (
|
|
53
|
+
<div className="max-w-[720px] mx-auto px-6 pt-12 pb-32">
|
|
54
|
+
{responseBanner}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
{affected.map(change => (
|
|
57
|
+
<OverviewFileSection
|
|
58
|
+
key={change.filename}
|
|
59
|
+
change={change}
|
|
60
|
+
revision={revision}
|
|
61
|
+
previousRevision={previousRev?.revision}
|
|
62
|
+
onOpenTab={() => setActiveView({ type: 'canvas', filename: change.filename })}
|
|
63
|
+
/>
|
|
64
|
+
))}
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
66
|
+
{unchanged.length > 0 && (
|
|
67
|
+
<div className="mt-8 border-t border-border-subtle pt-4">
|
|
68
|
+
<div className="text-[11px] font-body font-medium uppercase tracking-widest text-text-tertiary mb-3">
|
|
69
|
+
Unchanged
|
|
70
|
+
</div>
|
|
71
|
+
{unchanged.map(change => (
|
|
72
|
+
<UnchangedFileRow
|
|
73
|
+
key={change.filename}
|
|
74
|
+
change={change}
|
|
75
|
+
revision={revision}
|
|
76
|
+
onOpenTab={() => setActiveView({ type: 'canvas', filename: change.filename })}
|
|
77
|
+
/>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
type ViewMode =
|
|
85
|
+
type ViewMode = 'rendered' | 'unified' | 'side-by-side'
|
|
86
86
|
|
|
87
87
|
function ViewModeToggle({ mode, onChange, showDiff }: { mode: ViewMode; onChange: (m: ViewMode) => void; showDiff: boolean }) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
88
|
+
if (!showDiff) return null
|
|
89
|
+
return (
|
|
90
|
+
<div className="flex items-center bg-bg-elevated rounded-md p-0.5">
|
|
91
|
+
{(['rendered', 'unified', 'side-by-side'] as ViewMode[]).map(m => (
|
|
92
|
+
<button
|
|
93
|
+
key={m}
|
|
94
|
+
onClick={() => onChange(m)}
|
|
95
|
+
className={`text-[10px] font-body px-2 py-0.5 rounded transition-colors ${
|
|
96
|
+
mode === m
|
|
97
|
+
? 'bg-bg-surface text-text-primary font-medium shadow-sm'
|
|
98
|
+
: 'text-text-tertiary hover:text-text-secondary'
|
|
99
|
+
}`}
|
|
100
|
+
>
|
|
101
|
+
{m === 'rendered' ? 'Full' : m === 'unified' ? 'Unified' : 'Split'}
|
|
102
|
+
</button>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
function OverviewFileSection({
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
change,
|
|
110
|
+
revision,
|
|
111
|
+
previousRevision,
|
|
112
|
+
onOpenTab,
|
|
113
113
|
}: {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
change: FileChange
|
|
115
|
+
revision: number
|
|
116
|
+
previousRevision?: number
|
|
117
|
+
onOpenTab: () => void
|
|
118
118
|
}) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
const label = change.filename.replace(/\.jsx$/, '')
|
|
120
|
+
const isNew = change.status === 'new'
|
|
121
|
+
const diffStats = change.diffStats
|
|
122
|
+
const canDiff = change.status === 'changed' && previousRevision != null
|
|
123
|
+
const [viewMode, setViewMode] = useState<ViewMode>('rendered')
|
|
124
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
|
-
|
|
125
|
+
return (
|
|
126
|
+
<div className="mb-8">
|
|
127
|
+
{/* File header */}
|
|
128
|
+
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-border-subtle">
|
|
129
|
+
<span
|
|
130
|
+
className={`text-[11px] font-mono px-1.5 py-0.5 rounded ${
|
|
131
|
+
isNew
|
|
132
|
+
? 'bg-accent-green/10 text-accent-green border border-accent-green/20'
|
|
133
|
+
: 'bg-accent-amber/10 text-accent-amber border border-accent-amber/20'
|
|
134
|
+
}`}
|
|
135
|
+
>
|
|
136
|
+
{isNew ? 'new' : 'modified'}
|
|
137
|
+
</span>
|
|
138
|
+
<span className="text-[14px] font-body font-medium text-text-primary">{label}</span>
|
|
139
|
+
{diffStats && (diffStats.added > 0 || diffStats.removed > 0) && (
|
|
140
|
+
<span className="text-[11px] font-mono text-text-tertiary">
|
|
141
|
+
<span className="text-accent-green">+{diffStats.added}</span> <span className="text-accent-red">-{diffStats.removed}</span>
|
|
142
|
+
</span>
|
|
143
|
+
)}
|
|
144
|
+
<ViewModeToggle mode={viewMode} onChange={setViewMode} showDiff={canDiff} />
|
|
145
|
+
<button
|
|
146
|
+
onClick={onOpenTab}
|
|
147
|
+
className={`${canDiff ? '' : 'ml-auto'} text-[11px] font-body font-medium text-accent-blue hover:text-accent-blue/80 transition-colors`}
|
|
148
|
+
>
|
|
149
|
+
Open in tab
|
|
150
|
+
</button>
|
|
151
|
+
</div>
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
153
|
+
{/* Content based on view mode */}
|
|
154
|
+
{viewMode === 'rendered'
|
|
155
|
+
? (
|
|
156
|
+
<div data-canvas-file={change.filename}>
|
|
157
|
+
<PlanRenderer revision={revision} filename={change.filename} />
|
|
158
|
+
</div>
|
|
159
|
+
)
|
|
160
|
+
: canDiff && previousRevision != null
|
|
161
|
+
? (
|
|
162
|
+
<InlineDiff
|
|
163
|
+
key={`${viewMode}-${revision}-${previousRevision}`}
|
|
164
|
+
filename={change.filename}
|
|
165
|
+
leftRev={previousRevision}
|
|
166
|
+
rightRev={revision}
|
|
167
|
+
mode={viewMode}
|
|
168
|
+
/>
|
|
169
|
+
)
|
|
170
|
+
: null}
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
169
173
|
}
|
|
170
174
|
|
|
171
175
|
/* ── Inline diff for overview ── */
|
|
172
176
|
|
|
173
177
|
function InlineDiff({
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
+
filename,
|
|
179
|
+
leftRev,
|
|
180
|
+
rightRev,
|
|
181
|
+
mode,
|
|
178
182
|
}: {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
+
filename: string
|
|
184
|
+
leftRev: number
|
|
185
|
+
rightRev: number
|
|
186
|
+
mode: 'unified' | 'side-by-side'
|
|
183
187
|
}) {
|
|
184
|
-
|
|
188
|
+
const sessionId = useContext(SessionContext)
|
|
185
189
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
+
if (mode === 'unified') {
|
|
191
|
+
return <InlineUnifiedDiff sessionId={sessionId} filename={filename} leftRev={leftRev} rightRev={rightRev} />
|
|
192
|
+
}
|
|
193
|
+
return <InlineSideBySideDiff sessionId={sessionId} filename={filename} leftRev={leftRev} rightRev={rightRev} />
|
|
190
194
|
}
|
|
191
195
|
|
|
192
196
|
function InlineUnifiedDiff({ sessionId, filename, leftRev, rightRev }: { sessionId: string; filename: string; leftRev: number; rightRev: number }) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
const oldRef = useRef<HTMLDivElement>(null)
|
|
198
|
+
const newRef = useRef<HTMLDivElement>(null)
|
|
199
|
+
const unifiedRef = useRef<HTMLDivElement>(null)
|
|
200
|
+
const [leftReady, setLeftReady] = useState(false)
|
|
201
|
+
const [rightReady, setRightReady] = useState(false)
|
|
202
|
+
const [done, setDone] = useState(false)
|
|
203
|
+
const [error, setError] = useState<string | null>(null)
|
|
200
204
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (!leftReady || !rightReady || done) return
|
|
207
|
+
if (!oldRef.current || !newRef.current) return
|
|
204
208
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
209
|
+
requestAnimationFrame(() => {
|
|
210
|
+
requestAnimationFrame(() => {
|
|
211
|
+
if (!oldRef.current || !newRef.current || !unifiedRef.current) return
|
|
212
|
+
try {
|
|
213
|
+
const oldTree = extractBlockTree(oldRef.current)
|
|
214
|
+
const newTree = extractBlockTree(newRef.current)
|
|
215
|
+
const matches = matchBlocks(oldTree, newTree)
|
|
216
|
+
const { fragment } = buildUnifiedDom(matches)
|
|
217
|
+
unifiedRef.current.innerHTML = ''
|
|
218
|
+
unifiedRef.current.appendChild(fragment)
|
|
219
|
+
setDone(true)
|
|
220
|
+
} catch (e) {
|
|
221
|
+
setError(e instanceof Error ? e.message : String(e))
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
}, [leftReady, rightReady, done])
|
|
222
226
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
227
|
+
return (
|
|
228
|
+
<div>
|
|
229
|
+
{/* Offscreen panels for diff computation */}
|
|
230
|
+
{!done && !error && (
|
|
231
|
+
<div style={{ position: 'absolute', left: -9999, visibility: 'hidden' as const, pointerEvents: 'none' as const, width: 720 }}>
|
|
232
|
+
<ComparePanelRenderer
|
|
233
|
+
ref={oldRef}
|
|
234
|
+
sessionId={sessionId}
|
|
235
|
+
revision={leftRev}
|
|
236
|
+
filename={filename}
|
|
237
|
+
onReady={() => setLeftReady(true)}
|
|
238
|
+
onError={(msg) => setError(msg)}
|
|
239
|
+
/>
|
|
240
|
+
<ComparePanelRenderer
|
|
241
|
+
ref={newRef}
|
|
242
|
+
sessionId={sessionId}
|
|
243
|
+
revision={rightRev}
|
|
244
|
+
filename={filename}
|
|
245
|
+
onReady={() => setRightReady(true)}
|
|
246
|
+
onError={(msg) => setError(msg)}
|
|
247
|
+
/>
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
{!done && !error && <div className="flex items-center justify-center h-24 text-text-tertiary font-body text-[13px]">Computing diff...</div>}
|
|
251
|
+
{error && <div className="text-accent-red text-[13px] font-body p-3 bg-accent-red-muted rounded-lg">{error}</div>}
|
|
252
|
+
<div ref={unifiedRef} />
|
|
253
|
+
</div>
|
|
254
|
+
)
|
|
241
255
|
}
|
|
242
256
|
|
|
243
|
-
function InlineSideBySideDiff(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
257
|
+
function InlineSideBySideDiff(
|
|
258
|
+
{ sessionId, filename, leftRev, rightRev }: { sessionId: string; filename: string; leftRev: number; rightRev: number },
|
|
259
|
+
) {
|
|
260
|
+
const oldRef = useRef<HTMLDivElement>(null)
|
|
261
|
+
const newRef = useRef<HTMLDivElement>(null)
|
|
262
|
+
const [leftReady, setLeftReady] = useState(false)
|
|
263
|
+
const [rightReady, setRightReady] = useState(false)
|
|
264
|
+
const [done, setDone] = useState(false)
|
|
249
265
|
|
|
250
|
-
|
|
251
|
-
|
|
266
|
+
const handleLeftReady = useCallback(() => setLeftReady(true), [])
|
|
267
|
+
const handleRightReady = useCallback(() => setRightReady(true), [])
|
|
252
268
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
if (!leftReady || !rightReady || done) return
|
|
271
|
+
if (!oldRef.current || !newRef.current) return
|
|
256
272
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
273
|
+
requestAnimationFrame(() => {
|
|
274
|
+
requestAnimationFrame(() => {
|
|
275
|
+
if (!oldRef.current || !newRef.current) return
|
|
276
|
+
runDomDiff(oldRef.current, newRef.current)
|
|
277
|
+
setDone(true)
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
}, [leftReady, rightReady, done])
|
|
265
281
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
282
|
+
return (
|
|
283
|
+
<div className="flex gap-0 border-y border-border-subtle overflow-hidden w-screen relative left-1/2 -translate-x-1/2">
|
|
284
|
+
<div className="w-1/2 border-r border-border-subtle">
|
|
285
|
+
<div className="px-2 py-1 text-[10px] font-body font-medium text-text-tertiary bg-bg-elevated border-b border-border-subtle">
|
|
286
|
+
Rev {leftRev}
|
|
287
|
+
</div>
|
|
288
|
+
<div className="px-6 py-4 max-w-[600px] mx-auto">
|
|
289
|
+
<ComparePanelRenderer ref={oldRef} sessionId={sessionId} revision={leftRev} filename={filename} onReady={handleLeftReady} />
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
<div className="w-1/2">
|
|
293
|
+
<div className="px-2 py-1 text-[10px] font-body font-medium text-text-tertiary bg-bg-elevated border-b border-border-subtle">
|
|
294
|
+
Rev {rightRev}
|
|
295
|
+
</div>
|
|
296
|
+
<div className="px-6 py-4 max-w-[600px] mx-auto">
|
|
297
|
+
<ComparePanelRenderer ref={newRef} sessionId={sessionId} revision={rightRev} filename={filename} onReady={handleRightReady} />
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
)
|
|
286
302
|
}
|
|
287
303
|
|
|
288
304
|
function UnchangedFileRow({
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
305
|
+
change,
|
|
306
|
+
revision,
|
|
307
|
+
onOpenTab,
|
|
292
308
|
}: {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
309
|
+
change: FileChange
|
|
310
|
+
revision: number
|
|
311
|
+
onOpenTab: () => void
|
|
296
312
|
}) {
|
|
297
|
-
|
|
298
|
-
|
|
313
|
+
const [expanded, setExpanded] = useState(false)
|
|
314
|
+
const label = change.filename.replace(/\.jsx$/, '')
|
|
299
315
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
316
|
+
return (
|
|
317
|
+
<div className="mb-2">
|
|
318
|
+
<div className="flex items-center gap-2 py-1.5">
|
|
319
|
+
<button
|
|
320
|
+
onClick={() => setExpanded(!expanded)}
|
|
321
|
+
className="text-text-tertiary hover:text-text-secondary transition-colors"
|
|
322
|
+
>
|
|
323
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" className={`transition-transform duration-150 ${expanded ? 'rotate-90' : ''}`}>
|
|
324
|
+
<path d="M3.5 2L6.5 5L3.5 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
325
|
+
</svg>
|
|
326
|
+
</button>
|
|
327
|
+
<span className="text-[13px] font-body text-text-secondary">{label}</span>
|
|
328
|
+
<button
|
|
329
|
+
onClick={onOpenTab}
|
|
330
|
+
className="ml-auto text-[11px] font-body text-text-tertiary hover:text-accent-blue transition-colors"
|
|
331
|
+
>
|
|
332
|
+
Open in tab
|
|
333
|
+
</button>
|
|
334
|
+
</div>
|
|
335
|
+
{expanded && (
|
|
336
|
+
<div className="ml-5 mt-1 mb-3" data-canvas-file={change.filename}>
|
|
337
|
+
<PlanRenderer revision={revision} filename={change.filename} />
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
)
|
|
326
342
|
}
|