@jetcrab/pi-desk 1.1.5 → 1.1.8-dev
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 +4 -4
- package/README.zh-CN.md +4 -4
- package/docs/README.md +8 -0
- package/docs/doc_asset/client-overview.webp +0 -0
- package/docs/doc_asset/multi-window.webp +0 -0
- package/docs/{pi-desk/plugins.md → plugins.md} +108 -1
- package/package.json +2 -2
- package/src/client/l2_biz/project-preview/l2-project-preview-dialog.tsx +3 -0
- package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-view.tsx +8 -0
- package/src/client/l2_biz/workbench/hooks/l2-workbench-chat-viewport.ts +81 -7
- package/src/client/l2_biz/workbench/l2-workbench-chat-view.tsx +2 -1
- package/src/client/l3_modules/project-files/l3-project-document-git.ts +82 -0
- package/src/client/l3_modules/project-files/l3-project-document-view.tsx +102 -5
- package/src/client/l3_modules/project-files/l3-project-file-browser.tsx +2 -6
- package/src/client/l3_modules/project-files/l3-project-file-runtime.ts +83 -1
- package/src/client/l3_modules/project-files/l3-project-file-tabs.tsx +24 -2
- package/src/client/l3_modules/project-files/l3-project-git-model.ts +32 -0
- package/src/client/l4_foundation/ui/code/l4-code-diff.tsx +25 -5
- package/src/client/l4_foundation/ui/code/l4-code-line-changes.ts +49 -0
- package/src/client/l4_foundation/ui/code/l4-code-line-changes.worker.ts +10 -0
- package/src/client/l4_foundation/ui/code/l4-code-line-markers.module.css +60 -0
- package/src/client/l4_foundation/ui/code/l4-code-line-markers.ts +156 -0
- package/src/client/l4_foundation/ui/code/l4-code-preview.tsx +61 -5
- package/src/client/l4_foundation/ui/code/l4-monaco-lines-diff.d.ts +21 -0
- package/src/client/l4_foundation/ui/shadcn/hover-card.tsx +28 -2
- package/src/common/l3_modules/project-files/l3-project-files-locale-messages.ts +16 -0
- package/src/server/l4_foundation/pidesk/l4-pidesk-runtime.ts +1 -1
- package/temp/build/pi-desk/release/.next/BUILD_ID +1 -1
- package/temp/build/pi-desk/release/.next/build-manifest.json +3 -3
- package/temp/build/pi-desk/release/.next/prerender-manifest.json +3 -3
- package/temp/build/pi-desk/release/.next/react-loadable-manifest.json +5 -4
- package/temp/build/pi-desk/release/.next/required-server-files.js +3 -3
- package/temp/build/pi-desk/release/.next/required-server-files.json +3 -3
- package/temp/build/pi-desk/release/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.html +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.rsc +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_not-found/page.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/health/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/login/page.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/page.js +6 -6
- package/temp/build/pi-desk/release/.next/server/app/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/20842.js +1 -0
- package/temp/build/pi-desk/release/.next/server/chunks/25406.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/{9248.js → 354.js} +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/35985.js +6 -6
- package/temp/build/pi-desk/release/.next/server/chunks/45671.js +1 -0
- package/temp/build/pi-desk/release/.next/server/chunks/47757.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/56284.js +62 -0
- package/temp/build/pi-desk/release/.next/server/chunks/73691.js +1 -0
- package/temp/build/pi-desk/release/.next/server/chunks/82499.js +2 -2
- package/temp/build/pi-desk/release/.next/server/chunks/94296.js +2 -2
- package/temp/build/pi-desk/release/.next/server/chunks/pi-desk-line-changes.js +1 -0
- package/temp/build/pi-desk/release/.next/server/middleware-build-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/pages/500.html +1 -1
- package/temp/build/pi-desk/release/.next/server/server-reference-manifest.json +1 -1
- package/temp/build/pi-desk/release/.next/server/webpack-runtime.js +1 -1
- package/temp/build/pi-desk/release/.next/static/chunks/28739.540a4f24a8eaa3fd.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/31087-333cae396a52f232.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/3814-35cfab0ce3aa7c41.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/38459.9a9b6f4f3ec3ff50.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/45992-a39a014497ec4dd1.js +13 -0
- package/temp/build/pi-desk/release/.next/static/chunks/53291.ab6ef49016107124.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/{54206.82a63ab971911eda.js → 54206.ffb8c581c6661ae2.js} +2 -2
- package/temp/build/pi-desk/release/.next/static/chunks/5598.f34dce2a9de0d0e3.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/70669.8b4231a8675499e0.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/95287.34b860abf26f64a5.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/layout-586f6f88483051a7.js +3 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/{page-c6c68b33156be10a.js → page-0f2cf68a4666420b.js} +4 -4
- package/temp/build/pi-desk/release/.next/static/chunks/pi-desk-line-changes.49f8d59c256621d6.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/webpack-26261285e181235a.js +1 -0
- package/temp/build/pi-desk/release/.next/static/css/{f6a76a493aae9ae3.css → adb1d4e48dd43c0a.css} +1 -1
- package/temp/build/pi-desk/release/.next/trace-build +1 -1
- package/docs/pi-desk/README.md +0 -47
- package/temp/build/pi-desk/release/.next/server/chunks/6656.js +0 -1
- package/temp/build/pi-desk/release/.next/server/chunks/89236.js +0 -62
- package/temp/build/pi-desk/release/.next/static/chunks/19991.02fcb7c8ca3b1fe4.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/26285.54b8bcbf7a198c84.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/52354-6461358c522130be.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/74132-876f256c7a6101af.js +0 -13
- package/temp/build/pi-desk/release/.next/static/chunks/79615.7d68b269d93fcb93.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/app/layout-4faf3a52d219296d.js +0 -3
- package/temp/build/pi-desk/release/.next/static/chunks/webpack-7bfbf627379fb4c1.js +0 -1
- /package/docs/{pi-desk/commands.md → commands.md} +0 -0
- /package/docs/{pi-desk/examples → examples}/local-application/browser/application.js +0 -0
- /package/docs/{pi-desk/examples → examples}/local-application/browser/entry.js +0 -0
- /package/docs/{pi-desk/examples → examples}/local-application/package.json +0 -0
- /package/docs/{pi-desk/examples → examples}/local-application/pi-desk.ts +0 -0
- /package/temp/build/pi-desk/release/.next/static/{1LdtGYUckBhYZy0R-6yG0 → -pemk30NXpiExEU5tjb1t}/_buildManifest.js +0 -0
- /package/temp/build/pi-desk/release/.next/static/{1LdtGYUckBhYZy0R-6yG0 → -pemk30NXpiExEU5tjb1t}/_ssgManifest.js +0 -0
|
@@ -6,13 +6,21 @@ import {
|
|
|
6
6
|
CopyIcon,
|
|
7
7
|
FileCode2Icon,
|
|
8
8
|
FoldVerticalIcon,
|
|
9
|
+
GitCompareArrowsIcon,
|
|
9
10
|
ListOrderedIcon,
|
|
10
11
|
LoaderCircleIcon,
|
|
11
12
|
RefreshCwIcon,
|
|
12
13
|
SearchIcon,
|
|
13
14
|
WrapTextIcon
|
|
14
15
|
} from 'lucide-react'
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
useEffect,
|
|
18
|
+
useImperativeHandle,
|
|
19
|
+
useRef,
|
|
20
|
+
useState,
|
|
21
|
+
useSyncExternalStore,
|
|
22
|
+
type Ref
|
|
23
|
+
} from 'react'
|
|
16
24
|
import { useTranslation } from 'react-i18next'
|
|
17
25
|
import { copyL4BrowserText } from '@client/l4_foundation/lib/l4-browser-clipboard'
|
|
18
26
|
import {
|
|
@@ -25,6 +33,8 @@ import { L4ImageViewer } from '@client/l4_foundation/ui/l4-image-viewer'
|
|
|
25
33
|
import { useL4AppToast } from '@client/l4_foundation/ui/l4-app-toast'
|
|
26
34
|
import { Button } from '@client/l4_foundation/ui/shadcn/button'
|
|
27
35
|
import type { L3ProjectFileRuntime } from './l3-project-file-runtime'
|
|
36
|
+
import type { L3ProjectGitBrowser } from './l3-project-git-runtime'
|
|
37
|
+
import { useL3ProjectDocumentGit } from './l3-project-document-git'
|
|
28
38
|
import { L3CodeSurfaceToolbarButton } from './l3-code-surface-toolbar-button'
|
|
29
39
|
import { buildL3FilePreviewHtml } from './l3-file-preview-html'
|
|
30
40
|
|
|
@@ -38,13 +48,17 @@ export interface L3ProjectDocumentHandle {
|
|
|
38
48
|
export function L3ProjectDocumentView({
|
|
39
49
|
ref: handleRef,
|
|
40
50
|
runtime,
|
|
51
|
+
gitRuntime,
|
|
41
52
|
compact,
|
|
53
|
+
visible = true,
|
|
42
54
|
onEscape,
|
|
43
55
|
onSwitchPane
|
|
44
56
|
}: {
|
|
45
57
|
ref?: Ref<L3ProjectDocumentHandle>
|
|
46
58
|
runtime: L3ProjectFileRuntime
|
|
59
|
+
gitRuntime: L3ProjectGitBrowser
|
|
47
60
|
compact: boolean
|
|
61
|
+
visible?: boolean
|
|
48
62
|
onEscape: () => void
|
|
49
63
|
onSwitchPane?: () => void
|
|
50
64
|
}): React.JSX.Element {
|
|
@@ -55,6 +69,7 @@ export function L3ProjectDocumentView({
|
|
|
55
69
|
const document = !activeDiff && activePath ? runtime.state.documents.get(activePath) : null
|
|
56
70
|
const isHtml = Boolean(activePath && /\.html?$/i.test(activePath))
|
|
57
71
|
const showSource = document?.status === 'text' && (!isHtml || document.viewMode === 'source')
|
|
72
|
+
const hostRef = useRef<HTMLDivElement>(null)
|
|
58
73
|
const previewRef = useRef<L4CodePreviewHandle>(null)
|
|
59
74
|
const diffRef = useRef<L4CodeDiffHandle>(null)
|
|
60
75
|
const iframeRef = useRef<HTMLIFrameElement>(null)
|
|
@@ -83,6 +98,42 @@ export function L3ProjectDocumentView({
|
|
|
83
98
|
if (activePath && previewRef.current)
|
|
84
99
|
runtime.saveEditorState(activePath, previewRef.current.getViewState())
|
|
85
100
|
}
|
|
101
|
+
const { refresh, unavailable } = useL3ProjectDocumentGit(
|
|
102
|
+
runtime,
|
|
103
|
+
gitRuntime,
|
|
104
|
+
hostRef,
|
|
105
|
+
savePosition,
|
|
106
|
+
visible
|
|
107
|
+
)
|
|
108
|
+
const comparison = document?.status === 'text' ? document.comparison : undefined
|
|
109
|
+
const [computed, setComputed] = useState<{
|
|
110
|
+
original: string
|
|
111
|
+
modified: string
|
|
112
|
+
count: number | null
|
|
113
|
+
} | null>(null)
|
|
114
|
+
const count =
|
|
115
|
+
comparison?.status === 'ready' &&
|
|
116
|
+
document?.status === 'text' &&
|
|
117
|
+
computed?.original === comparison.original.content &&
|
|
118
|
+
computed.modified === document.content
|
|
119
|
+
? computed.count
|
|
120
|
+
: undefined
|
|
121
|
+
const openDiff = (line?: number): void => {
|
|
122
|
+
if (!activePath) return
|
|
123
|
+
savePosition()
|
|
124
|
+
runtime.openFileDiff(activePath, line)
|
|
125
|
+
}
|
|
126
|
+
const gitStatus =
|
|
127
|
+
unavailable || comparison?.status === 'error'
|
|
128
|
+
? t('gitMarkersFailed')
|
|
129
|
+
: comparison?.status === 'unavailable' || (comparison?.status === 'ready' && count === null)
|
|
130
|
+
? t('gitMarkersUnavailable')
|
|
131
|
+
: comparison?.status === 'loading' ||
|
|
132
|
+
(comparison?.status === 'ready' && count === undefined)
|
|
133
|
+
? t('gitMarkersLoading')
|
|
134
|
+
: comparison?.status === 'ready'
|
|
135
|
+
? t('gitChangesCount', { count })
|
|
136
|
+
: null
|
|
86
137
|
const copyText = (): void => {
|
|
87
138
|
if (document?.status !== 'text') return
|
|
88
139
|
const selection = previewRef.current?.getSelectionText() ?? ''
|
|
@@ -93,6 +144,7 @@ export function L3ProjectDocumentView({
|
|
|
93
144
|
}
|
|
94
145
|
return (
|
|
95
146
|
<div
|
|
147
|
+
ref={hostRef}
|
|
96
148
|
className="flex size-full min-h-0 min-w-0 flex-col outline-none"
|
|
97
149
|
tabIndex={-1}
|
|
98
150
|
onFocus={(event) => {
|
|
@@ -181,6 +233,41 @@ export function L3ProjectDocumentView({
|
|
|
181
233
|
))}
|
|
182
234
|
</div>
|
|
183
235
|
) : null}
|
|
236
|
+
{showSource && gitStatus ? (
|
|
237
|
+
<span
|
|
238
|
+
data-testid="file-git-status"
|
|
239
|
+
role="status"
|
|
240
|
+
className="mr-auto min-w-0 truncate text-xs text-muted-foreground"
|
|
241
|
+
title={comparison?.status === 'error' ? comparison.error : gitStatus}
|
|
242
|
+
>
|
|
243
|
+
{compact && comparison?.status === 'ready' && typeof count === 'number'
|
|
244
|
+
? t('gitChangesCompact', { count })
|
|
245
|
+
: gitStatus}
|
|
246
|
+
</span>
|
|
247
|
+
) : null}
|
|
248
|
+
{showSource && comparison?.status === 'ready' ? (
|
|
249
|
+
<>
|
|
250
|
+
{typeof count === 'number' && count > 0 ? (
|
|
251
|
+
<>
|
|
252
|
+
<L3CodeSurfaceToolbarButton
|
|
253
|
+
label={t('previousChange')}
|
|
254
|
+
onClick={() => previewRef.current?.previousChange()}
|
|
255
|
+
>
|
|
256
|
+
<ArrowUpIcon className="size-4" />
|
|
257
|
+
</L3CodeSurfaceToolbarButton>
|
|
258
|
+
<L3CodeSurfaceToolbarButton
|
|
259
|
+
label={t('nextChange')}
|
|
260
|
+
onClick={() => previewRef.current?.nextChange()}
|
|
261
|
+
>
|
|
262
|
+
<ArrowDownIcon className="size-4" />
|
|
263
|
+
</L3CodeSurfaceToolbarButton>
|
|
264
|
+
</>
|
|
265
|
+
) : null}
|
|
266
|
+
<L3CodeSurfaceToolbarButton label={t('viewDiff')} onClick={() => openDiff()}>
|
|
267
|
+
<GitCompareArrowsIcon className="size-4" />
|
|
268
|
+
</L3CodeSurfaceToolbarButton>
|
|
269
|
+
</>
|
|
270
|
+
) : null}
|
|
184
271
|
{showSource && activePath ? (
|
|
185
272
|
<>
|
|
186
273
|
<L3CodeSurfaceToolbarButton
|
|
@@ -210,10 +297,7 @@ export function L3ProjectDocumentView({
|
|
|
210
297
|
</>
|
|
211
298
|
) : null}
|
|
212
299
|
{activePath ? (
|
|
213
|
-
<L3CodeSurfaceToolbarButton
|
|
214
|
-
label={t('reloadCurrentFile')}
|
|
215
|
-
onClick={() => runtime.reloadFile(activePath)}
|
|
216
|
-
>
|
|
300
|
+
<L3CodeSurfaceToolbarButton label={t('reloadCurrentFile')} onClick={refresh}>
|
|
217
301
|
<RefreshCwIcon className="size-4" />
|
|
218
302
|
</L3CodeSurfaceToolbarButton>
|
|
219
303
|
) : null}
|
|
@@ -231,6 +315,7 @@ export function L3ProjectDocumentView({
|
|
|
231
315
|
modified={activeDiff.modified}
|
|
232
316
|
viewMode={compact ? 'inline' : diffViewMode}
|
|
233
317
|
savedState={activeDiff.editorState}
|
|
318
|
+
initialLine={activeDiff.initialLine}
|
|
234
319
|
onSaveState={(position) => runtime.saveDiffState(activeDiff, position)}
|
|
235
320
|
onEscape={escape}
|
|
236
321
|
/>
|
|
@@ -302,6 +387,18 @@ export function L3ProjectDocumentView({
|
|
|
302
387
|
path={activePath}
|
|
303
388
|
content={document.content}
|
|
304
389
|
size={document.size}
|
|
390
|
+
originalContent={
|
|
391
|
+
comparison?.status === 'ready' ? comparison.original.content : undefined
|
|
392
|
+
}
|
|
393
|
+
onChanges={(count) => {
|
|
394
|
+
if (comparison?.status === 'ready')
|
|
395
|
+
setComputed({
|
|
396
|
+
original: comparison.original.content,
|
|
397
|
+
modified: document.content,
|
|
398
|
+
count
|
|
399
|
+
})
|
|
400
|
+
}}
|
|
401
|
+
onOpenDiff={openDiff}
|
|
305
402
|
savedState={document.editorState}
|
|
306
403
|
onSaveState={(path, state) => runtime.saveEditorState(path, state)}
|
|
307
404
|
onEscape={escape}
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
import type { L3ProjectGitBrowser } from './l3-project-git-runtime'
|
|
40
40
|
import {
|
|
41
41
|
getL3GitChangeBadge,
|
|
42
|
+
getL3GitChangeClass,
|
|
42
43
|
getL3GitHeadLabel,
|
|
43
44
|
getL3GitRepositoryName,
|
|
44
45
|
joinL3GitProjectPath,
|
|
@@ -118,12 +119,7 @@ function fileIcon(path: string): React.JSX.Element {
|
|
|
118
119
|
|
|
119
120
|
function changedFileNameClass(change: L3GitProjectChange | null): string | undefined {
|
|
120
121
|
if (!change) return undefined
|
|
121
|
-
|
|
122
|
-
if (badge === 'D') return 'text-muted-foreground'
|
|
123
|
-
if (badge === '?') return 'text-red-600 dark:text-red-400'
|
|
124
|
-
if (badge === 'A') return 'text-emerald-600 dark:text-emerald-400'
|
|
125
|
-
if (badge === '!') return 'text-violet-600 dark:text-violet-400'
|
|
126
|
-
return 'text-blue-600 dark:text-blue-400'
|
|
122
|
+
return getL3GitChangeClass(change.change)
|
|
127
123
|
}
|
|
128
124
|
|
|
129
125
|
function DirectoryFeedback({
|
|
@@ -22,6 +22,16 @@ export type L3DirectoryState =
|
|
|
22
22
|
| { status: 'idle' | 'loading' }
|
|
23
23
|
| { status: 'ready'; entries: L4ProjectFileEntry[]; truncated: boolean }
|
|
24
24
|
| { status: 'error'; error: string }
|
|
25
|
+
export type L3FileComparison =
|
|
26
|
+
| { status: 'loading' }
|
|
27
|
+
| {
|
|
28
|
+
status: 'ready'
|
|
29
|
+
repositoryRoot: string
|
|
30
|
+
path: string
|
|
31
|
+
original: { path: string; content: string }
|
|
32
|
+
}
|
|
33
|
+
| { status: 'unavailable' }
|
|
34
|
+
| { status: 'error'; error: string }
|
|
25
35
|
export type L3FileDocument =
|
|
26
36
|
| { status: 'unloaded' }
|
|
27
37
|
| { status: 'loading' }
|
|
@@ -31,6 +41,7 @@ export type L3FileDocument =
|
|
|
31
41
|
size: number
|
|
32
42
|
editorState: L4CodePreviewState | null
|
|
33
43
|
viewMode: 'source' | 'preview'
|
|
44
|
+
comparison?: L3FileComparison
|
|
34
45
|
}
|
|
35
46
|
| { status: 'image'; mimeType: L4ProjectFileImageMimeType; size: number; objectUrl: string }
|
|
36
47
|
| { status: 'error'; error: string }
|
|
@@ -47,6 +58,7 @@ interface DiffOptions {
|
|
|
47
58
|
comparison?: L4GitHistoricalDiff
|
|
48
59
|
editorState?: L4CodeDiffState | null
|
|
49
60
|
label?: string
|
|
61
|
+
initialLine?: number
|
|
50
62
|
}
|
|
51
63
|
export type L3FileDiff = DiffOptions &
|
|
52
64
|
(
|
|
@@ -492,6 +504,71 @@ export class L3ProjectFileRuntime {
|
|
|
492
504
|
}
|
|
493
505
|
this.publish()
|
|
494
506
|
}
|
|
507
|
+
async compareFile(
|
|
508
|
+
path: string,
|
|
509
|
+
target: { repositoryRoot: string; path: string } | null,
|
|
510
|
+
signal: AbortSignal
|
|
511
|
+
): Promise<void> {
|
|
512
|
+
const document = this.state.documents.get(path)
|
|
513
|
+
if (this.disposed || signal.aborted || document?.status !== 'text') return
|
|
514
|
+
if (!target) {
|
|
515
|
+
if (document.comparison) {
|
|
516
|
+
delete document.comparison
|
|
517
|
+
this.publish()
|
|
518
|
+
}
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
const loading: L3FileComparison = { status: 'loading' }
|
|
522
|
+
document.comparison = loading
|
|
523
|
+
this.publish()
|
|
524
|
+
const current = (): boolean =>
|
|
525
|
+
!this.disposed &&
|
|
526
|
+
!signal.aborted &&
|
|
527
|
+
this.state.documents.get(path) === document &&
|
|
528
|
+
document.comparison === loading
|
|
529
|
+
try {
|
|
530
|
+
const result = await this.reader.diff(target.repositoryRoot, target.path, signal)
|
|
531
|
+
if (!current()) return
|
|
532
|
+
if (result.kind === 'text') {
|
|
533
|
+
// 比较结果的工作区正文与基线一起替换,不能把新行号画在缓存正文上。
|
|
534
|
+
document.content = result.modified.content
|
|
535
|
+
document.size = new TextEncoder().encode(result.modified.content).length
|
|
536
|
+
document.comparison = {
|
|
537
|
+
status: 'ready',
|
|
538
|
+
repositoryRoot: target.repositoryRoot,
|
|
539
|
+
path: target.path,
|
|
540
|
+
original: result.original
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
document.comparison = { status: 'unavailable' }
|
|
544
|
+
}
|
|
545
|
+
} catch (cause) {
|
|
546
|
+
if (!current()) return
|
|
547
|
+
console.warn('[Pi Desk][ProjectFile] 读取行变更失败', {
|
|
548
|
+
cwd: this.cwd,
|
|
549
|
+
path,
|
|
550
|
+
error: message(cause)
|
|
551
|
+
})
|
|
552
|
+
document.comparison = { status: 'error', error: message(cause) }
|
|
553
|
+
}
|
|
554
|
+
this.publish()
|
|
555
|
+
}
|
|
556
|
+
openFileDiff(path: string, initialLine?: number): void {
|
|
557
|
+
const document = this.state.documents.get(path)
|
|
558
|
+
if (this.disposed || document?.status !== 'text' || document.comparison?.status !== 'ready')
|
|
559
|
+
return
|
|
560
|
+
const comparison = document.comparison
|
|
561
|
+
this.clearDiff()
|
|
562
|
+
this.state.activeDiff = {
|
|
563
|
+
status: 'text',
|
|
564
|
+
repositoryRoot: comparison.repositoryRoot,
|
|
565
|
+
path: comparison.path,
|
|
566
|
+
original: comparison.original,
|
|
567
|
+
modified: { path: comparison.path, content: document.content },
|
|
568
|
+
initialLine
|
|
569
|
+
}
|
|
570
|
+
this.publish()
|
|
571
|
+
}
|
|
495
572
|
openDiff(
|
|
496
573
|
repositoryRoot: string,
|
|
497
574
|
path: string,
|
|
@@ -616,7 +693,12 @@ export class L3ProjectFileRuntime {
|
|
|
616
693
|
private invalidateDocument(path: string): void {
|
|
617
694
|
this.documentRequests.get(path)?.abort()
|
|
618
695
|
this.documentRequests.delete(path)
|
|
619
|
-
this.
|
|
696
|
+
const document = this.state.documents.get(path)
|
|
697
|
+
if (document?.status === 'text') {
|
|
698
|
+
if (document.editorState) this.restoredEditorStates.set(path, document.editorState)
|
|
699
|
+
this.restoredFileViewModes.set(path, document.viewMode)
|
|
700
|
+
}
|
|
701
|
+
this.releaseDocument(document)
|
|
620
702
|
this.state.documents.set(path, { status: 'unloaded' })
|
|
621
703
|
}
|
|
622
704
|
private releaseDocument(document: L3FileDocument | undefined): void {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { ChevronDownIcon, ChevronUpIcon, FileCode2Icon, XIcon } from 'lucide-react'
|
|
4
|
-
import { useLayoutEffect, useRef, useState } from 'react'
|
|
4
|
+
import { useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'
|
|
5
5
|
import { useTranslation } from 'react-i18next'
|
|
6
6
|
import { cn } from '@client/l4_foundation/lib/l4-utils'
|
|
7
7
|
import { Button } from '@client/l4_foundation/ui/shadcn/button'
|
|
@@ -13,8 +13,12 @@ import {
|
|
|
13
13
|
} from '@client/l4_foundation/ui/shadcn/context-menu'
|
|
14
14
|
import type { L3FileTabCloseScope } from './l3-project-file-runtime'
|
|
15
15
|
import { L3FileContextMenuContent } from './l3-file-context-menu-content'
|
|
16
|
+
import type { L3ProjectGitBrowser } from './l3-project-git-runtime'
|
|
17
|
+
import { findL3GitFile, getL3GitChangeBadge, getL3GitChangeClass } from './l3-project-git-model'
|
|
16
18
|
|
|
17
19
|
interface L3ProjectFileTabsProps {
|
|
20
|
+
cwd: string
|
|
21
|
+
gitRuntime: L3ProjectGitBrowser
|
|
18
22
|
paths: readonly string[]
|
|
19
23
|
activePath: string | null
|
|
20
24
|
diffPath: string | null
|
|
@@ -39,6 +43,8 @@ function fileLabel(path: string, paths: readonly string[]): string {
|
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export function L3ProjectFileTabs({
|
|
46
|
+
cwd,
|
|
47
|
+
gitRuntime,
|
|
42
48
|
paths,
|
|
43
49
|
activePath,
|
|
44
50
|
diffPath,
|
|
@@ -47,6 +53,8 @@ export function L3ProjectFileTabs({
|
|
|
47
53
|
onClose
|
|
48
54
|
}: L3ProjectFileTabsProps): React.JSX.Element | null {
|
|
49
55
|
const { t } = useTranslation('projectFiles')
|
|
56
|
+
useSyncExternalStore(gitRuntime.subscribe, gitRuntime.getRevision, gitRuntime.getRevision)
|
|
57
|
+
const snapshot = gitRuntime.getWorkspace(cwd)?.snapshot
|
|
50
58
|
const rowsRef = useRef<HTMLDivElement>(null)
|
|
51
59
|
const suppressTouchClickRef = useRef(false)
|
|
52
60
|
const [expanded, setExpanded] = useState(false)
|
|
@@ -98,6 +106,10 @@ export function L3ProjectFileTabs({
|
|
|
98
106
|
{tabs.map((tab, index) => {
|
|
99
107
|
const clipped = !expanded && clippedIndexes.includes(index)
|
|
100
108
|
const selected = tab.path === active
|
|
109
|
+
const change =
|
|
110
|
+
tab.path && snapshot?.status === 'ready'
|
|
111
|
+
? findL3GitFile(cwd, tab.path, snapshot.repositories)?.change
|
|
112
|
+
: undefined
|
|
101
113
|
const closeLabel =
|
|
102
114
|
tab.path === null
|
|
103
115
|
? t('closeDiff', { name: tab.title.split('/').at(-1) })
|
|
@@ -186,7 +198,17 @@ export function L3ProjectFileTabs({
|
|
|
186
198
|
}}
|
|
187
199
|
>
|
|
188
200
|
<FileCode2Icon className="size-3.5 shrink-0" />
|
|
189
|
-
<span className=
|
|
201
|
+
<span className={cn('truncate', change && getL3GitChangeClass(change))}>
|
|
202
|
+
{tab.label}
|
|
203
|
+
</span>
|
|
204
|
+
{change ? (
|
|
205
|
+
<span
|
|
206
|
+
className={cn('shrink-0 text-xs', getL3GitChangeClass(change))}
|
|
207
|
+
aria-label={t('gitStatus', { status: getL3GitChangeBadge(change) })}
|
|
208
|
+
>
|
|
209
|
+
{getL3GitChangeBadge(change)}
|
|
210
|
+
</span>
|
|
211
|
+
) : null}
|
|
190
212
|
</button>
|
|
191
213
|
<Button
|
|
192
214
|
variant="ghost"
|
|
@@ -131,6 +131,38 @@ export function getL3GitChangeBadge(change: L4GitFileChange): string {
|
|
|
131
131
|
return '?'
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
export function getL3GitChangeClass(change: L4GitFileChange): string {
|
|
135
|
+
const badge = getL3GitChangeBadge(change)
|
|
136
|
+
if (badge === 'D') return 'text-muted-foreground'
|
|
137
|
+
if (badge === '?') return 'text-red-600 dark:text-red-400'
|
|
138
|
+
if (badge === 'A') return 'text-emerald-600 dark:text-emerald-400'
|
|
139
|
+
if (badge === '!') return 'text-violet-600 dark:text-violet-400'
|
|
140
|
+
return 'text-blue-600 dark:text-blue-400'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function findL3GitFile(
|
|
144
|
+
cwd: string,
|
|
145
|
+
path: string,
|
|
146
|
+
repositories: readonly L4GitRepository[]
|
|
147
|
+
): { repository: L4GitRepository; path: string; change: L4GitFileChange | undefined } | null {
|
|
148
|
+
if (/^(?:[a-z]:|\/|\\\\)/i.test(path)) return null
|
|
149
|
+
const windows = /^[a-z]:[\\/]/i.test(cwd) || cwd.startsWith('\\\\')
|
|
150
|
+
const key = (value: string): string => (windows ? value.toLowerCase() : value)
|
|
151
|
+
const repository = [...repositories]
|
|
152
|
+
.sort((left, right) => right.root.length - left.root.length)
|
|
153
|
+
.find((item) => !item.root || key(path).startsWith(`${key(item.root)}/`))
|
|
154
|
+
if (!repository) return null
|
|
155
|
+
const relative = repository.root ? path.slice(repository.root.length + 1) : path
|
|
156
|
+
return {
|
|
157
|
+
repository,
|
|
158
|
+
path: relative,
|
|
159
|
+
change:
|
|
160
|
+
repository.state === 'ready'
|
|
161
|
+
? repository.changes.find((item) => key(item.path) === key(relative))
|
|
162
|
+
: undefined
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
134
166
|
export function listL3GitProjectChanges(
|
|
135
167
|
repositories: readonly L4GitRepository[]
|
|
136
168
|
): L3GitProjectChange[] {
|
|
@@ -44,10 +44,11 @@ interface L4CodeDiffProps {
|
|
|
44
44
|
onEscape?: () => void
|
|
45
45
|
savedState?: L4CodeDiffState | null
|
|
46
46
|
onSaveState?: (state: L4CodeDiffState | null) => void
|
|
47
|
+
initialLine?: number
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
function L4CodeDiffComponent(
|
|
50
|
-
{ original, modified, viewMode, onEscape, savedState, onSaveState }: L4CodeDiffProps,
|
|
51
|
+
{ original, modified, viewMode, onEscape, savedState, onSaveState, initialLine }: L4CodeDiffProps,
|
|
51
52
|
ref: ForwardedRef<L4CodeDiffHandle>
|
|
52
53
|
): React.JSX.Element {
|
|
53
54
|
const { t } = useTranslation('common')
|
|
@@ -58,12 +59,14 @@ function L4CodeDiffComponent(
|
|
|
58
59
|
const escapeRef = useRef(onEscape)
|
|
59
60
|
const savedStateRef = useRef(savedState)
|
|
60
61
|
const saveStateRef = useRef(onSaveState)
|
|
62
|
+
const initialLineRef = useRef(initialLine)
|
|
61
63
|
|
|
62
64
|
useEffect(() => {
|
|
63
65
|
escapeRef.current = onEscape
|
|
64
66
|
savedStateRef.current = savedState
|
|
65
67
|
saveStateRef.current = onSaveState
|
|
66
|
-
|
|
68
|
+
initialLineRef.current = initialLine
|
|
69
|
+
}, [initialLine, onEscape, onSaveState, savedState])
|
|
67
70
|
const [loadState, setLoadState] = useState<'loading' | 'ready' | 'error'>('loading')
|
|
68
71
|
const [changeCount, setChangeCount] = useState<number | null>(null)
|
|
69
72
|
|
|
@@ -130,6 +133,7 @@ function L4CodeDiffComponent(
|
|
|
130
133
|
useInlineViewWhenSpaceIsLimited: false,
|
|
131
134
|
enableSplitViewResizing: true,
|
|
132
135
|
diffAlgorithm: 'advanced',
|
|
136
|
+
ignoreTrimWhitespace: false,
|
|
133
137
|
maxComputationTime: 5_000,
|
|
134
138
|
hideUnchangedRegions: initialSettings.collapseUnchanged
|
|
135
139
|
? {
|
|
@@ -156,13 +160,29 @@ function L4CodeDiffComponent(
|
|
|
156
160
|
contextmenu: true,
|
|
157
161
|
ariaLabel: t('gitDiffReadOnly')
|
|
158
162
|
})
|
|
159
|
-
|
|
163
|
+
const requestedLine = initialLineRef.current
|
|
164
|
+
let initialPositionApplied = false
|
|
160
165
|
scrollListeners.push(
|
|
161
166
|
diffEditor.onDidUpdateDiff(() => {
|
|
162
|
-
if (active
|
|
167
|
+
if (!active || !diffEditor) return
|
|
168
|
+
const changes = diffEditor.getLineChanges()
|
|
169
|
+
setChangeCount(changes?.length ?? null)
|
|
170
|
+
if (!initialPositionApplied && changes !== null && requestedLine !== undefined) {
|
|
171
|
+
initialPositionApplied = true
|
|
172
|
+
const lineNumber = Math.max(
|
|
173
|
+
1,
|
|
174
|
+
Math.min(requestedLine, modifiedModel?.getLineCount() ?? 1)
|
|
175
|
+
)
|
|
176
|
+
const modifiedEditor = diffEditor.getModifiedEditor()
|
|
177
|
+
modifiedEditor.setPosition({ lineNumber, column: 1 })
|
|
178
|
+
modifiedEditor.revealLineInCenter(lineNumber)
|
|
179
|
+
}
|
|
163
180
|
})
|
|
164
181
|
)
|
|
165
|
-
|
|
182
|
+
diffEditor.setModel({ original: originalModel, modified: modifiedModel })
|
|
183
|
+
if (requestedLine === undefined && savedStateRef.current) {
|
|
184
|
+
diffEditor.restoreViewState(savedStateRef.current)
|
|
185
|
+
}
|
|
166
186
|
const save = (): void => saveStateRef.current?.(diffEditor?.saveViewState() ?? null)
|
|
167
187
|
scrollListeners.push(
|
|
168
188
|
diffEditor.getOriginalEditor().onDidScrollChange(save),
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { DefaultLinesDiffComputer } from 'monaco-editor/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer'
|
|
2
|
+
|
|
3
|
+
export interface L4LineChange {
|
|
4
|
+
kind: 'added' | 'modified' | 'deleted'
|
|
5
|
+
originalStartLineNumber: number
|
|
6
|
+
originalEndLineNumberExclusive: number
|
|
7
|
+
modifiedStartLineNumber: number
|
|
8
|
+
modifiedEndLineNumberExclusive: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** 行号从 1 开始,结束行不包含在范围内;纯删除的 modified 范围为空。 */
|
|
12
|
+
export function computeL4LineChanges(original: string, modified: string): L4LineChange[] | null {
|
|
13
|
+
if (original.length + modified.length > 2 * 1024 * 1024) return null
|
|
14
|
+
const originalLines = original.split(/\r\n|\r|\n/)
|
|
15
|
+
const modifiedLines = modified.split(/\r\n|\r|\n/)
|
|
16
|
+
if (originalLines.length + modifiedLines.length > 50_000) return null
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const startedAt = Date.now()
|
|
20
|
+
const result = new DefaultLinesDiffComputer().computeDiff(originalLines, modifiedLines, {
|
|
21
|
+
ignoreTrimWhitespace: false,
|
|
22
|
+
maxComputationTimeMs: 500,
|
|
23
|
+
computeMoves: false
|
|
24
|
+
})
|
|
25
|
+
if (result.hitTimeout || Date.now() - startedAt > 500) return null
|
|
26
|
+
return result.changes.map(({ original: before, modified: after }) => {
|
|
27
|
+
// Monaco 的空模型占一行,但空文件不应被归类为替换了这一行。
|
|
28
|
+
const originalEnd = original === '' ? 1 : before.endLineNumberExclusive
|
|
29
|
+
const modifiedEnd = modified === '' ? 1 : after.endLineNumberExclusive
|
|
30
|
+
return {
|
|
31
|
+
kind:
|
|
32
|
+
before.startLineNumber === originalEnd
|
|
33
|
+
? 'added'
|
|
34
|
+
: after.startLineNumber === modifiedEnd
|
|
35
|
+
? 'deleted'
|
|
36
|
+
: 'modified',
|
|
37
|
+
originalStartLineNumber: before.startLineNumber,
|
|
38
|
+
originalEndLineNumberExclusive: originalEnd,
|
|
39
|
+
modifiedStartLineNumber: after.startLineNumber,
|
|
40
|
+
modifiedEndLineNumberExclusive: modifiedEnd
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
} catch (error: unknown) {
|
|
44
|
+
console.warn('[Pi Desk][CodePreview] 行差异计算失败', {
|
|
45
|
+
errorName: error instanceof Error ? error.name : 'UnknownError'
|
|
46
|
+
})
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { computeL4LineChanges, type L4LineChange } from './l4-code-line-changes'
|
|
2
|
+
|
|
3
|
+
const scope = self as unknown as {
|
|
4
|
+
onmessage: ((event: MessageEvent<{ original: string; modified: string }>) => void) | null
|
|
5
|
+
postMessage: (changes: L4LineChange[] | null) => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
scope.onmessage = (event): void => {
|
|
9
|
+
scope.postMessage(computeL4LineChanges(event.data.original, event.data.modified))
|
|
10
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
.host {
|
|
2
|
+
--git-added: #16803c;
|
|
3
|
+
--git-modified: #216bcc;
|
|
4
|
+
--git-deleted: #d12c38;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
:global(.dark) .host {
|
|
8
|
+
--git-added: #59c77b;
|
|
9
|
+
--git-modified: #6aabff;
|
|
10
|
+
--git-deleted: #ff757f;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.added,
|
|
14
|
+
.modified,
|
|
15
|
+
.deletedBefore,
|
|
16
|
+
.deletedAfter {
|
|
17
|
+
/* 实际移动命中区,保持点击相对元素左缘小于 4px,避免触发 Monaco 折叠。 */
|
|
18
|
+
margin-left: 8px;
|
|
19
|
+
width: 4px !important;
|
|
20
|
+
z-index: 1;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.added::before,
|
|
25
|
+
.modified::before {
|
|
26
|
+
position: absolute;
|
|
27
|
+
top: 0;
|
|
28
|
+
bottom: 0;
|
|
29
|
+
left: 0;
|
|
30
|
+
width: 3px;
|
|
31
|
+
content: '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.added::before {
|
|
35
|
+
background: var(--git-added);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.modified::before {
|
|
39
|
+
background: var(--git-modified);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.deletedBefore::before,
|
|
43
|
+
.deletedAfter::before {
|
|
44
|
+
position: absolute;
|
|
45
|
+
left: 0;
|
|
46
|
+
width: 0;
|
|
47
|
+
height: 0;
|
|
48
|
+
border-top: 4px solid transparent;
|
|
49
|
+
border-bottom: 4px solid transparent;
|
|
50
|
+
border-left: 4px solid var(--git-deleted);
|
|
51
|
+
content: '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.deletedBefore::before {
|
|
55
|
+
top: -3px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.deletedAfter::before {
|
|
59
|
+
bottom: -3px;
|
|
60
|
+
}
|