@jetcrab/pi-desk 1.1.23-dev → 1.1.25
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/docs/commands.md +4 -2
- package/docs/plugins.md +2 -0
- package/package.json +2 -2
- package/src/app/project-preview/page.tsx +13 -0
- package/src/client/l1_entry/pages/l1-project-preview-page.tsx +1 -5
- package/src/client/l2_biz/project-preview/hooks/l2-use-project-preview.ts +25 -0
- package/src/client/l2_biz/project-preview/l2-project-preview-storage.ts +7 -7
- package/src/client/l2_biz/project-preview/l2-project-preview.tsx +8 -5
- package/src/client/l2_biz/project-preview/runtime/l2-project-history-runtime.ts +14 -11
- package/src/client/l2_biz/project-preview/views/l2-project-branch-select.tsx +347 -0
- package/src/client/l2_biz/project-preview/views/l2-project-change-tree.tsx +28 -11
- package/src/client/l2_biz/project-preview/views/l2-project-history-list.tsx +35 -8
- package/src/client/l2_biz/project-preview/views/l2-project-history-view.tsx +46 -48
- package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-runtime.ts +2 -1
- package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-view.tsx +6 -2
- package/src/client/l3_modules/conversation/l3-conversation-viewport.tsx +18 -8
- package/src/client/l3_modules/project-files/l3-project-document-view.tsx +7 -4
- package/src/client/l3_modules/project-files/l3-project-file-runtime.ts +181 -89
- package/src/client/l3_modules/project-files/l3-project-file-tabs.tsx +80 -28
- package/src/common/l2_biz/project-preview/l2-project-history-messages.ts +4 -2
- package/src/common/l2_biz/project-preview/l2-project-preview-navigation.ts +14 -0
- package/src/server/l1_entry/api/l1-plugin-management-route.ts +0 -4
- package/src/server/l2_biz/plugin-management/l2-plugin-management.ts +4 -37
- package/src/server/l2_biz/plugin-management/l2-plugin-updates.ts +1 -4
- package/src/server/l2_biz/work-session/l2-work-session-chat-runtime.ts +4 -2
- package/src/server/l4_foundation/pi/l4-pi-chat-worker.ts +16 -1
- package/src/server/l4_foundation/pi/l4-pi-package-root-consistency.ts +3 -6
- package/src/server/l4_foundation/pi/l4-pi-package-root-gate.ts +1 -1
- package/src/server/l4_foundation/pi/l4-pi-plugin-owner-runtime.ts +4 -0
- package/src/server/l4_foundation/pi/l4-pi-plugin-sources.ts +5 -1
- package/src/server/l4_foundation/pi/l4-pi-session-initialization.ts +30 -34
- package/src/server/l4_foundation/pi/l4-pi-session-resources.ts +15 -14
- package/src/server/l4_foundation/pi/l4-pi-work-session-runtime.ts +1 -1
- package/src/server/l4_foundation/skills/runtime/l4-skill-catalog.ts +159 -166
- 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 +1 -1
- 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_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/api/plugins/add/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/plugins/apply/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/plugins/batch/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/plugins/del/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/plugins/enabled-set/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/plugins/get/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/plugins/list/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/plugins/reload/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/plugins/update/route.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 +2 -2
- package/temp/build/pi-desk/release/.next/server/app/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/project-preview/page.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/project-preview/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/16438.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/18748.js +1 -0
- package/temp/build/pi-desk/release/.next/server/chunks/24557.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/25406.js +13 -14
- package/temp/build/pi-desk/release/.next/server/chunks/27344.js +15 -0
- package/temp/build/pi-desk/release/.next/server/chunks/35985.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/36361.js +2 -2
- package/temp/build/pi-desk/release/.next/server/chunks/50520.js +2 -2
- package/temp/build/pi-desk/release/.next/server/chunks/6917.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/80290.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/92386.js +15 -16
- package/temp/build/pi-desk/release/.next/server/instrumentation.js +1 -1
- 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/static/chunks/15302-6e070e13fa65c863.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/19991.bcca96d396b83030.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/65232-ef4e5a4174f62faf.js +15 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/{page-54040b9fd54ee8b3.js → page-18d63fd3920dfae0.js} +3 -3
- package/temp/build/pi-desk/release/.next/static/chunks/app/project-preview/page-038b6c14e03ed878.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/{webpack-48eecbb66781c006.js → webpack-c0b805df46fcf8b3.js} +1 -1
- package/temp/build/pi-desk/release/.next/static/css/c43b04c244a64fb4.css +1 -0
- package/temp/build/pi-desk/release/.next/trace-build +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/20460.js +0 -15
- package/temp/build/pi-desk/release/.next/server/chunks/93049.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/15302-9c807afc6c60de0c.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/19991.8aa2e4b6df15fad6.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/9038-05fd2688407b39c1.js +0 -15
- package/temp/build/pi-desk/release/.next/static/chunks/app/project-preview/page-bc18765121950e65.js +0 -1
- package/temp/build/pi-desk/release/.next/static/css/10e063cfb3cda7bd.css +0 -1
- /package/temp/build/pi-desk/release/.next/static/{gAYTx4KJJR3vTtGYexj4J → 6a4eIEjmpW5l8Qlgtzzzg}/_buildManifest.js +0 -0
- /package/temp/build/pi-desk/release/.next/static/{gAYTx4KJJR3vTtGYexj4J → 6a4eIEjmpW5l8Qlgtzzzg}/_ssgManifest.js +0 -0
|
@@ -72,16 +72,29 @@ function emptySearch(input: L4ProjectSearchInput): L3FileSearch {
|
|
|
72
72
|
error: null
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
interface
|
|
75
|
+
export interface L3FileDiffTarget {
|
|
76
76
|
repositoryRoot: string
|
|
77
77
|
path: string
|
|
78
78
|
comparison?: L4GitHistoricalDiff
|
|
79
|
+
}
|
|
80
|
+
export type L3FileTabTarget = string | L3FileDiffTarget
|
|
81
|
+
export function getL3FileDiffKey(target: L3FileDiffTarget): string {
|
|
82
|
+
return JSON.stringify([
|
|
83
|
+
target.repositoryRoot,
|
|
84
|
+
target.path,
|
|
85
|
+
target.comparison?.baseCommit ?? null,
|
|
86
|
+
target.comparison?.targetCommit ?? null,
|
|
87
|
+
target.comparison?.oldPath ?? null
|
|
88
|
+
])
|
|
89
|
+
}
|
|
90
|
+
interface DiffOptions extends L3FileDiffTarget {
|
|
79
91
|
editorState?: L4CodeDiffState | null
|
|
80
92
|
label?: string
|
|
81
93
|
initialLine?: number
|
|
82
94
|
}
|
|
83
95
|
export type L3FileDiff = DiffOptions &
|
|
84
96
|
(
|
|
97
|
+
| { status: 'unloaded' }
|
|
85
98
|
| { status: 'loading' }
|
|
86
99
|
| {
|
|
87
100
|
status: 'text'
|
|
@@ -101,6 +114,7 @@ export interface L3ProjectFileState {
|
|
|
101
114
|
search: L3FileSearch
|
|
102
115
|
searchOpen: boolean
|
|
103
116
|
documents: Map<string, L3FileDocument>
|
|
117
|
+
diffs: L3FileDiff[]
|
|
104
118
|
activeDiff: L3FileDiff | null
|
|
105
119
|
}
|
|
106
120
|
interface RuntimeOptions {
|
|
@@ -118,7 +132,8 @@ export interface L3ProjectFileBrowsingState {
|
|
|
118
132
|
expandedPaths: string[]
|
|
119
133
|
treeScrollTop: number
|
|
120
134
|
searchQuery: string
|
|
121
|
-
|
|
135
|
+
diffs?: L3FileDiffTarget[]
|
|
136
|
+
activeDiff: L3FileDiffTarget | null
|
|
122
137
|
}
|
|
123
138
|
function projectText(
|
|
124
139
|
key: keyof (typeof L3_PROJECT_FILES_LOCALE_MESSAGES)['zh-CN'],
|
|
@@ -153,6 +168,7 @@ export class L3ProjectFileRuntime {
|
|
|
153
168
|
search: emptySearch({ query: '', ...DEFAULT_SEARCH_OPTIONS }),
|
|
154
169
|
searchOpen: false,
|
|
155
170
|
documents: new Map(),
|
|
171
|
+
diffs: [],
|
|
156
172
|
activeDiff: null
|
|
157
173
|
}
|
|
158
174
|
private readonly listeners = new Set<() => void>()
|
|
@@ -169,7 +185,7 @@ export class L3ProjectFileRuntime {
|
|
|
169
185
|
private readonly restoredPreviewScrollTops = new Map<string, number>()
|
|
170
186
|
private searchRequest: AbortController | null = null
|
|
171
187
|
private navigation: L4CodeLineNavigation | null = null
|
|
172
|
-
private
|
|
188
|
+
private readonly diffRequests = new Map<string, AbortController>()
|
|
173
189
|
private revision = 0
|
|
174
190
|
private disposed = false
|
|
175
191
|
|
|
@@ -225,8 +241,9 @@ export class L3ProjectFileRuntime {
|
|
|
225
241
|
return { editorStates, fileViewModes }
|
|
226
242
|
}
|
|
227
243
|
restoreBrowsing(input: L3ProjectFileBrowsingState): void {
|
|
228
|
-
this.
|
|
229
|
-
|
|
244
|
+
if (this.disposed) return
|
|
245
|
+
this.state.tabs = input.tabs.slice(0, this.options.maxTabs)
|
|
246
|
+
for (const path of this.state.tabs) {
|
|
230
247
|
this.restoredTabs.add(path)
|
|
231
248
|
const editorState = input.editorStates[path]
|
|
232
249
|
if (editorState) this.restoredEditorStates.set(path, editorState)
|
|
@@ -234,18 +251,30 @@ export class L3ProjectFileRuntime {
|
|
|
234
251
|
if (viewMode) this.restoredFileViewModes.set(path, viewMode)
|
|
235
252
|
}
|
|
236
253
|
this.state.activePath =
|
|
237
|
-
input.activePath &&
|
|
254
|
+
input.activePath && this.state.tabs.includes(input.activePath) ? input.activePath : null
|
|
238
255
|
this.state.expandedPaths = new Set(input.expandedPaths)
|
|
239
256
|
this.state.treeScrollTop = input.treeScrollTop
|
|
240
257
|
this.state.search = emptySearch({ query: input.searchQuery, ...DEFAULT_SEARCH_OPTIONS })
|
|
241
|
-
for (const path of
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
this.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
258
|
+
for (const path of this.state.tabs) this.state.documents.set(path, { status: 'unloaded' })
|
|
259
|
+
const targets = [...(input.diffs ?? []), ...(input.activeDiff ? [input.activeDiff] : [])]
|
|
260
|
+
for (const target of targets) {
|
|
261
|
+
if (this.state.diffs.some((diff) => getL3FileDiffKey(diff) === getL3FileDiffKey(target)))
|
|
262
|
+
continue
|
|
263
|
+
if (
|
|
264
|
+
this.options.maxTabs &&
|
|
265
|
+
this.state.tabs.length + this.state.diffs.length >= this.options.maxTabs
|
|
248
266
|
)
|
|
267
|
+
break
|
|
268
|
+
this.state.diffs.push({ ...target, status: 'unloaded' })
|
|
269
|
+
}
|
|
270
|
+
for (const path of input.expandedPaths) void this.ensureDirectory(path)
|
|
271
|
+
this.publish()
|
|
272
|
+
const activeTarget = input.activeDiff
|
|
273
|
+
const activeDiff = activeTarget
|
|
274
|
+
? this.state.diffs.find((diff) => getL3FileDiffKey(diff) === getL3FileDiffKey(activeTarget))
|
|
275
|
+
: null
|
|
276
|
+
if (activeDiff) {
|
|
277
|
+
this.openDiff(activeDiff.repositoryRoot, activeDiff.path, activeDiff.comparison)
|
|
249
278
|
} else if (this.state.activePath) {
|
|
250
279
|
void this.ensureDocument(this.state.activePath)
|
|
251
280
|
}
|
|
@@ -421,13 +450,12 @@ export class L3ProjectFileRuntime {
|
|
|
421
450
|
normalized)
|
|
422
451
|
: normalized
|
|
423
452
|
if (!this.state.tabs.includes(path)) {
|
|
424
|
-
if (this.
|
|
425
|
-
this.options.onNotice?.(projectText('maxOpenFiles', { count: this.options.maxTabs }))
|
|
453
|
+
if (!this.canOpenTab()) {
|
|
426
454
|
return
|
|
427
455
|
}
|
|
428
456
|
this.state.tabs.push(path)
|
|
429
457
|
}
|
|
430
|
-
this.
|
|
458
|
+
this.state.activeDiff = null
|
|
431
459
|
if (!this.state.documents.has(path)) this.state.documents.set(path, { status: 'unloaded' })
|
|
432
460
|
this.state.activePath = path
|
|
433
461
|
this.navigation = line === undefined ? null : { line }
|
|
@@ -436,55 +464,76 @@ export class L3ProjectFileRuntime {
|
|
|
436
464
|
void this.ensureDocument(path, line !== undefined)
|
|
437
465
|
this.publish()
|
|
438
466
|
}
|
|
439
|
-
closeTabs(target:
|
|
467
|
+
closeTabs(target: L3FileTabTarget | null, scope: L3FileTabCloseScope): void {
|
|
468
|
+
if (this.disposed) return
|
|
440
469
|
const state = this.state
|
|
441
|
-
const tabs: (string |
|
|
442
|
-
const
|
|
470
|
+
const tabs: (string | L3FileDiff)[] = [...state.tabs, ...state.diffs]
|
|
471
|
+
const resolved =
|
|
472
|
+
target === null
|
|
473
|
+
? state.activeDiff
|
|
474
|
+
: typeof target === 'string'
|
|
475
|
+
? target
|
|
476
|
+
: state.diffs.find((diff) => getL3FileDiffKey(diff) === getL3FileDiffKey(target))
|
|
477
|
+
const index = resolved === null || resolved === undefined ? -1 : tabs.indexOf(resolved)
|
|
443
478
|
if (index < 0 && scope !== 'all') return
|
|
444
479
|
const removed = new Set(
|
|
445
480
|
tabs.filter(
|
|
446
481
|
(tab, position) =>
|
|
447
482
|
scope === 'all' ||
|
|
448
483
|
(scope === 'current'
|
|
449
|
-
? tab ===
|
|
484
|
+
? tab === resolved
|
|
450
485
|
: scope === 'others'
|
|
451
|
-
? tab !==
|
|
486
|
+
? tab !== resolved
|
|
452
487
|
: scope === 'left'
|
|
453
488
|
? position < index
|
|
454
489
|
: position > index)
|
|
455
490
|
)
|
|
456
491
|
)
|
|
457
492
|
if (!removed.size) return
|
|
458
|
-
const previousActive = state.activeDiff
|
|
459
|
-
for (const
|
|
460
|
-
if (
|
|
461
|
-
this.
|
|
493
|
+
const previousActive = state.activeDiff ?? state.activePath
|
|
494
|
+
for (const tab of removed) {
|
|
495
|
+
if (typeof tab !== 'string') {
|
|
496
|
+
this.invalidateDiff(tab)
|
|
462
497
|
continue
|
|
463
498
|
}
|
|
464
|
-
this.documentRequests.get(
|
|
465
|
-
this.documentRequests.delete(
|
|
466
|
-
this.releaseDocument(state.documents.get(
|
|
467
|
-
state.documents.delete(
|
|
468
|
-
this.restoredEditorStates.delete(
|
|
469
|
-
this.restoredFileViewModes.delete(
|
|
470
|
-
this.restoredPreviewScrollTops.delete(
|
|
471
|
-
this.closeMissing.delete(
|
|
472
|
-
this.restoredTabs.delete(
|
|
499
|
+
this.documentRequests.get(tab)?.abort()
|
|
500
|
+
this.documentRequests.delete(tab)
|
|
501
|
+
this.releaseDocument(state.documents.get(tab))
|
|
502
|
+
state.documents.delete(tab)
|
|
503
|
+
this.restoredEditorStates.delete(tab)
|
|
504
|
+
this.restoredFileViewModes.delete(tab)
|
|
505
|
+
this.restoredPreviewScrollTops.delete(tab)
|
|
506
|
+
this.closeMissing.delete(tab)
|
|
507
|
+
this.restoredTabs.delete(tab)
|
|
473
508
|
}
|
|
474
509
|
state.tabs = state.tabs.filter((path) => !removed.has(path))
|
|
510
|
+
state.diffs = state.diffs.filter((diff) => !removed.has(diff))
|
|
475
511
|
if (state.activePath && removed.has(state.activePath)) {
|
|
476
|
-
|
|
477
|
-
const activeIndex =
|
|
512
|
+
const fileTabs = tabs.filter((tab): tab is string => typeof tab === 'string')
|
|
513
|
+
const activeIndex = fileTabs.indexOf(state.activePath)
|
|
478
514
|
state.activePath =
|
|
479
|
-
|
|
480
|
-
|
|
515
|
+
fileTabs.slice(activeIndex + 1).find((path) => !removed.has(path)) ??
|
|
516
|
+
fileTabs
|
|
481
517
|
.slice(0, activeIndex)
|
|
482
518
|
.reverse()
|
|
483
|
-
.find((path) =>
|
|
519
|
+
.find((path) => !removed.has(path)) ??
|
|
484
520
|
null
|
|
485
521
|
}
|
|
522
|
+
if (previousActive !== null && removed.has(previousActive)) {
|
|
523
|
+
this.navigation = null
|
|
524
|
+
const activeIndex = tabs.indexOf(previousActive)
|
|
525
|
+
const next =
|
|
526
|
+
tabs.slice(activeIndex + 1).find((tab) => !removed.has(tab)) ??
|
|
527
|
+
tabs
|
|
528
|
+
.slice(0, activeIndex)
|
|
529
|
+
.reverse()
|
|
530
|
+
.find((tab) => !removed.has(tab))
|
|
531
|
+
state.activeDiff = typeof next === 'object' ? next : null
|
|
532
|
+
if (typeof next === 'string') state.activePath = next
|
|
533
|
+
}
|
|
486
534
|
this.publish()
|
|
487
|
-
if (
|
|
535
|
+
if (state.activeDiff) void this.ensureDiff(state.activeDiff)
|
|
536
|
+
else if (state.activePath && previousActive !== state.activePath)
|
|
488
537
|
void this.ensureDocument(state.activePath)
|
|
489
538
|
}
|
|
490
539
|
reloadFile(path: string): void {
|
|
@@ -634,16 +683,32 @@ export class L3ProjectFileRuntime {
|
|
|
634
683
|
if (this.disposed || document?.status !== 'text' || document.comparison?.status !== 'ready')
|
|
635
684
|
return
|
|
636
685
|
const comparison = document.comparison
|
|
637
|
-
this.
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
686
|
+
let diff = this.state.diffs.find(
|
|
687
|
+
(item) =>
|
|
688
|
+
!item.comparison &&
|
|
689
|
+
item.repositoryRoot === comparison.repositoryRoot &&
|
|
690
|
+
item.path === comparison.path
|
|
691
|
+
)
|
|
692
|
+
if (!diff) {
|
|
693
|
+
if (!this.canOpenTab()) return
|
|
694
|
+
diff = {
|
|
695
|
+
status: 'text',
|
|
696
|
+
repositoryRoot: comparison.repositoryRoot,
|
|
697
|
+
path: comparison.path,
|
|
698
|
+
original: comparison.original,
|
|
699
|
+
modified: { path: comparison.path, content: document.content }
|
|
700
|
+
}
|
|
701
|
+
this.state.diffs.push(diff)
|
|
702
|
+
} else {
|
|
703
|
+
this.invalidateDiff(diff)
|
|
704
|
+
Object.assign(diff, {
|
|
705
|
+
status: 'text',
|
|
706
|
+
original: comparison.original,
|
|
707
|
+
modified: { path: comparison.path, content: document.content }
|
|
708
|
+
})
|
|
645
709
|
}
|
|
646
|
-
|
|
710
|
+
if (initialLine !== undefined) diff.initialLine = initialLine
|
|
711
|
+
this.openDiff(diff.repositoryRoot, diff.path)
|
|
647
712
|
}
|
|
648
713
|
openDiff(
|
|
649
714
|
repositoryRoot: string,
|
|
@@ -652,30 +717,35 @@ export class L3ProjectFileRuntime {
|
|
|
652
717
|
label?: string
|
|
653
718
|
): void {
|
|
654
719
|
if (this.disposed) return
|
|
655
|
-
|
|
656
|
-
this.state.
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
720
|
+
const target = { repositoryRoot, path, comparison }
|
|
721
|
+
let diff = this.state.diffs.find((item) => getL3FileDiffKey(item) === getL3FileDiffKey(target))
|
|
722
|
+
if (!diff) {
|
|
723
|
+
if (!this.canOpenTab()) return
|
|
724
|
+
diff = { ...target, status: 'unloaded', label }
|
|
725
|
+
this.state.diffs.push(diff)
|
|
726
|
+
} else if (label !== undefined) {
|
|
727
|
+
diff.label = label
|
|
662
728
|
}
|
|
729
|
+
this.navigation = null
|
|
730
|
+
this.state.activeDiff = diff
|
|
663
731
|
this.publish()
|
|
664
|
-
void this.ensureDiff()
|
|
732
|
+
void this.ensureDiff(diff)
|
|
665
733
|
}
|
|
666
734
|
closeDiff(): void {
|
|
667
735
|
this.closeTabs(null, 'current')
|
|
668
736
|
}
|
|
669
737
|
reloadDiff(): void {
|
|
670
|
-
const
|
|
671
|
-
if (!
|
|
672
|
-
this.
|
|
673
|
-
this.state.activeDiff = { ...previous, status: 'loading' }
|
|
738
|
+
const diff = this.state.activeDiff
|
|
739
|
+
if (!diff || this.disposed) return
|
|
740
|
+
this.invalidateDiff(diff)
|
|
674
741
|
this.publish()
|
|
675
|
-
void this.ensureDiff()
|
|
742
|
+
void this.ensureDiff(diff)
|
|
676
743
|
}
|
|
677
744
|
saveDiffState(expected: L3FileDiff, state: L4CodeDiffState | null): void {
|
|
678
|
-
|
|
745
|
+
// 卸载回调仍属于原标签;尚未创建编辑器时的空状态不能抹掉已有位置。
|
|
746
|
+
if (this.disposed || !this.state.diffs.includes(expected) || !state) return
|
|
747
|
+
expected.editorState = state
|
|
748
|
+
delete expected.initialLine
|
|
679
749
|
}
|
|
680
750
|
setDiffViewMode(diffViewMode: 'side-by-side' | 'inline'): void {
|
|
681
751
|
if (this.state.activeDiff) saveL4CodePreferences({ diffViewMode })
|
|
@@ -694,10 +764,12 @@ export class L3ProjectFileRuntime {
|
|
|
694
764
|
this.invalidateDocument(path)
|
|
695
765
|
this.closeMissing.add(path)
|
|
696
766
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
767
|
+
for (const diff of this.state.diffs) {
|
|
768
|
+
if (diff.repositoryRoot === repositoryRoot && !diff.comparison) this.invalidateDiff(diff)
|
|
769
|
+
}
|
|
770
|
+
this.publish()
|
|
771
|
+
if (this.state.activeDiff) void this.ensureDiff(this.state.activeDiff)
|
|
772
|
+
else if (this.state.activePath) void this.ensureDocument(this.state.activePath)
|
|
701
773
|
}
|
|
702
774
|
saveEditorState(path: string, state: L4CodePreviewState | null): void {
|
|
703
775
|
const document = this.state.documents.get(path)
|
|
@@ -725,49 +797,69 @@ export class L3ProjectFileRuntime {
|
|
|
725
797
|
for (const request of this.documentRequests.values()) request.abort()
|
|
726
798
|
this.documentRequests.clear()
|
|
727
799
|
this.cancelSearch()
|
|
728
|
-
this.
|
|
800
|
+
for (const diff of this.state.diffs) this.invalidateDiff(diff)
|
|
801
|
+
this.state.diffs = []
|
|
802
|
+
this.state.activeDiff = null
|
|
729
803
|
for (const document of this.state.documents.values()) this.releaseDocument(document)
|
|
730
804
|
this.state.documents.clear()
|
|
731
805
|
this.restoredPreviewScrollTops.clear()
|
|
732
806
|
this.listeners.clear()
|
|
733
807
|
}
|
|
734
|
-
private async ensureDiff(): Promise<void> {
|
|
735
|
-
const
|
|
736
|
-
if (this.disposed ||
|
|
808
|
+
private async ensureDiff(diff: L3FileDiff): Promise<void> {
|
|
809
|
+
const key = getL3FileDiffKey(diff)
|
|
810
|
+
if (this.disposed || diff.status !== 'unloaded' || this.diffRequests.has(key)) return
|
|
737
811
|
const request = new AbortController()
|
|
738
|
-
this.
|
|
812
|
+
this.diffRequests.set(key, request)
|
|
813
|
+
Object.assign(diff, { status: 'loading' })
|
|
814
|
+
this.publish()
|
|
739
815
|
try {
|
|
740
816
|
const result = await this.reader.diff(
|
|
741
|
-
|
|
742
|
-
|
|
817
|
+
diff.repositoryRoot,
|
|
818
|
+
diff.path,
|
|
743
819
|
request.signal,
|
|
744
|
-
|
|
820
|
+
diff.comparison
|
|
745
821
|
)
|
|
746
|
-
if (this.
|
|
747
|
-
|
|
822
|
+
if (this.diffRequests.get(key) !== request) return
|
|
823
|
+
// 标签对象贯穿整个生命周期,异步完成不能覆盖卸载回调保存的编辑位置。
|
|
824
|
+
Object.assign(
|
|
825
|
+
diff,
|
|
748
826
|
result.kind === 'text'
|
|
749
|
-
? {
|
|
827
|
+
? { status: 'text', original: result.original, modified: result.modified }
|
|
750
828
|
: result.kind === 'metadata'
|
|
751
829
|
? {
|
|
752
|
-
...current,
|
|
753
830
|
status: 'metadata',
|
|
754
831
|
originalMetadata: result.original,
|
|
755
832
|
modifiedMetadata: result.modified
|
|
756
833
|
}
|
|
757
|
-
: {
|
|
834
|
+
: { status: result.kind, error: null }
|
|
835
|
+
)
|
|
758
836
|
} catch (cause) {
|
|
759
|
-
if (this.
|
|
760
|
-
|
|
837
|
+
if (this.diffRequests.get(key) !== request) return
|
|
838
|
+
console.warn('[Pi Desk][ProjectFile] 读取 Diff 失败', {
|
|
839
|
+
cwd: this.cwd,
|
|
840
|
+
repositoryRoot: diff.repositoryRoot,
|
|
841
|
+
path: diff.path,
|
|
842
|
+
error: message(cause)
|
|
843
|
+
})
|
|
844
|
+
Object.assign(diff, { status: 'error', error: message(cause) })
|
|
761
845
|
}
|
|
762
|
-
if (this.
|
|
763
|
-
this.
|
|
846
|
+
if (this.diffRequests.get(key) !== request) return
|
|
847
|
+
this.diffRequests.delete(key)
|
|
764
848
|
this.publish()
|
|
765
849
|
}
|
|
766
|
-
private
|
|
767
|
-
|
|
768
|
-
this.
|
|
769
|
-
this.
|
|
770
|
-
|
|
850
|
+
private invalidateDiff(diff: L3FileDiff): void {
|
|
851
|
+
const key = getL3FileDiffKey(diff)
|
|
852
|
+
this.diffRequests.get(key)?.abort()
|
|
853
|
+
this.diffRequests.delete(key)
|
|
854
|
+
Object.assign(diff, { status: 'unloaded' })
|
|
855
|
+
for (const field of ['original', 'modified', 'originalMetadata', 'modifiedMetadata', 'error'])
|
|
856
|
+
Reflect.deleteProperty(diff, field)
|
|
857
|
+
}
|
|
858
|
+
private canOpenTab(): boolean {
|
|
859
|
+
const maxTabs = this.options.maxTabs
|
|
860
|
+
if (!maxTabs || this.state.tabs.length + this.state.diffs.length < maxTabs) return true
|
|
861
|
+
this.options.onNotice?.(projectText('maxOpenFiles', { count: maxTabs }))
|
|
862
|
+
return false
|
|
771
863
|
}
|
|
772
864
|
private cancelSearch(): void {
|
|
773
865
|
this.searchRequest?.abort()
|
|
@@ -11,7 +11,12 @@ import {
|
|
|
11
11
|
ContextMenuSeparator,
|
|
12
12
|
ContextMenuTrigger
|
|
13
13
|
} from '@client/l4_foundation/ui/shadcn/context-menu'
|
|
14
|
-
import
|
|
14
|
+
import {
|
|
15
|
+
getL3FileDiffKey,
|
|
16
|
+
type L3FileDiffTarget,
|
|
17
|
+
type L3FileTabTarget,
|
|
18
|
+
type L3FileTabCloseScope
|
|
19
|
+
} from './l3-project-file-runtime'
|
|
15
20
|
import { L3FileContextMenuContent } from './l3-file-context-menu-content'
|
|
16
21
|
import type { L3ProjectGitBrowser } from './l3-project-git-runtime'
|
|
17
22
|
import { findL3GitFile, getL3GitChangeBadge, getL3GitChangeClass } from './l3-project-git-model'
|
|
@@ -21,11 +26,12 @@ interface L3ProjectFileTabsProps {
|
|
|
21
26
|
gitRuntime: L3ProjectGitBrowser
|
|
22
27
|
paths: readonly string[]
|
|
23
28
|
activePath: string | null
|
|
24
|
-
|
|
29
|
+
diffs: readonly L3FileDiffTarget[]
|
|
30
|
+
activeDiff: L3FileDiffTarget | null
|
|
25
31
|
mobile: boolean
|
|
26
32
|
actions?: ReactNode
|
|
27
|
-
onActivate: (
|
|
28
|
-
onClose: (
|
|
33
|
+
onActivate: (target: L3FileTabTarget) => void
|
|
34
|
+
onClose: (target: L3FileTabTarget | null, scope: L3FileTabCloseScope) => void
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
const CLOSE_ACTIONS: readonly { scope: L3FileTabCloseScope; label: string }[] = [
|
|
@@ -48,7 +54,8 @@ export function L3ProjectFileTabs({
|
|
|
48
54
|
gitRuntime,
|
|
49
55
|
paths,
|
|
50
56
|
activePath,
|
|
51
|
-
|
|
57
|
+
diffs,
|
|
58
|
+
activeDiff,
|
|
52
59
|
mobile,
|
|
53
60
|
actions,
|
|
54
61
|
onActivate,
|
|
@@ -65,14 +72,50 @@ export function L3ProjectFileTabs({
|
|
|
65
72
|
const merged = actions !== undefined
|
|
66
73
|
const rowHeight = merged && !mobile ? 32 : 36
|
|
67
74
|
const maxHeight = (mobile ? 2 : 3) * rowHeight
|
|
68
|
-
const
|
|
75
|
+
const allPaths = [
|
|
76
|
+
...paths,
|
|
77
|
+
...diffs.map((diff) => [diff.repositoryRoot, diff.path].filter(Boolean).join('/'))
|
|
78
|
+
]
|
|
79
|
+
const tabs: {
|
|
80
|
+
target: L3FileTabTarget
|
|
81
|
+
key: string
|
|
82
|
+
path: string
|
|
83
|
+
label: string
|
|
84
|
+
revisions?: string
|
|
85
|
+
title: string
|
|
86
|
+
testId: string
|
|
87
|
+
}[] = paths.map((path) => ({
|
|
88
|
+
target: path,
|
|
89
|
+
key: `file:${path}`,
|
|
69
90
|
path,
|
|
70
|
-
label: fileLabel(path,
|
|
71
|
-
title: path
|
|
91
|
+
label: fileLabel(path, allPaths),
|
|
92
|
+
title: path,
|
|
93
|
+
testId: `file-tab-${path}`
|
|
72
94
|
}))
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
95
|
+
for (const diff of diffs) {
|
|
96
|
+
const path = [diff.repositoryRoot, diff.path].filter(Boolean).join('/')
|
|
97
|
+
const comparison = diff.comparison
|
|
98
|
+
const revisions = comparison
|
|
99
|
+
? `${comparison.baseCommit?.slice(0, 7) ?? '∅'} → ${comparison.targetCommit.slice(0, 7)}`
|
|
100
|
+
: ''
|
|
101
|
+
const key = getL3FileDiffKey(diff)
|
|
102
|
+
tabs.push({
|
|
103
|
+
target: diff,
|
|
104
|
+
key: `diff:${key}`,
|
|
105
|
+
path,
|
|
106
|
+
label: `${fileLabel(path, allPaths)} · Diff`,
|
|
107
|
+
revisions,
|
|
108
|
+
title: [
|
|
109
|
+
path,
|
|
110
|
+
comparison?.oldPath,
|
|
111
|
+
comparison ? `${comparison.baseCommit ?? '∅'} → ${comparison.targetCommit}` : null
|
|
112
|
+
]
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.join('\n'),
|
|
115
|
+
testId: `file-diff-tab-${path}${comparison ? `-${key}` : ''}`
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
const active = activeDiff ? `diff:${getL3FileDiffKey(activeDiff)}` : `file:${activePath}`
|
|
76
119
|
|
|
77
120
|
useLayoutEffect(() => {
|
|
78
121
|
const rows = rowsRef.current
|
|
@@ -95,7 +138,7 @@ export function L3ProjectFileTabs({
|
|
|
95
138
|
const observer = new ResizeObserver(inspect)
|
|
96
139
|
observer.observe(rows)
|
|
97
140
|
return () => observer.disconnect()
|
|
98
|
-
}, [active,
|
|
141
|
+
}, [active, diffs, maxHeight, paths])
|
|
99
142
|
|
|
100
143
|
if (tabs.length === 0) return null
|
|
101
144
|
return (
|
|
@@ -115,18 +158,20 @@ export function L3ProjectFileTabs({
|
|
|
115
158
|
>
|
|
116
159
|
{tabs.map((tab, index) => {
|
|
117
160
|
const clipped = !expanded && clippedIndexes.includes(index)
|
|
118
|
-
const selected = tab.
|
|
161
|
+
const selected = tab.key === active
|
|
162
|
+
const isDiff = typeof tab.target !== 'string'
|
|
119
163
|
const change =
|
|
120
|
-
|
|
164
|
+
!isDiff && snapshot?.status === 'ready'
|
|
121
165
|
? findL3GitFile(cwd, tab.path, snapshot.repositories)?.change
|
|
122
166
|
: undefined
|
|
123
|
-
const closeLabel =
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
167
|
+
const closeLabel = isDiff
|
|
168
|
+
? t('closeDiff', {
|
|
169
|
+
name: `${tab.path.split('/').at(-1)}${tab.revisions ? ` · ${tab.revisions}` : ''}`
|
|
170
|
+
})
|
|
171
|
+
: t('closeFile', { name: tab.path.split('/').at(-1) })
|
|
127
172
|
return (
|
|
128
173
|
<ContextMenu
|
|
129
|
-
key={tab.
|
|
174
|
+
key={tab.key}
|
|
130
175
|
onOpenChange={(open, details) => {
|
|
131
176
|
if (
|
|
132
177
|
open &&
|
|
@@ -140,16 +185,20 @@ export function L3ProjectFileTabs({
|
|
|
140
185
|
render={
|
|
141
186
|
<div
|
|
142
187
|
data-file-tab
|
|
143
|
-
data-testid={
|
|
144
|
-
tab.path === null ? `file-diff-tab-${tab.title}` : `file-tab-${tab.path}`
|
|
145
|
-
}
|
|
188
|
+
data-testid={tab.testId}
|
|
146
189
|
aria-hidden={clipped || undefined}
|
|
147
190
|
inert={clipped || undefined}
|
|
148
191
|
title={tab.title}
|
|
149
192
|
className={cn(
|
|
150
193
|
'flex max-w-full items-center text-sm',
|
|
151
194
|
merged ? (mobile ? 'h-9' : 'h-8') : 'h-9 border-r border-b',
|
|
152
|
-
mobile
|
|
195
|
+
mobile
|
|
196
|
+
? tab.revisions
|
|
197
|
+
? 'w-full'
|
|
198
|
+
: 'w-[min(50%,12rem)]'
|
|
199
|
+
: tab.revisions
|
|
200
|
+
? 'max-w-80'
|
|
201
|
+
: 'max-w-60',
|
|
153
202
|
clipped && 'invisible',
|
|
154
203
|
selected
|
|
155
204
|
? 'bg-background text-foreground shadow-[inset_0_-2px_0_var(--primary)]'
|
|
@@ -171,7 +220,7 @@ export function L3ProjectFileTabs({
|
|
|
171
220
|
suppressTouchClickRef.current = false
|
|
172
221
|
return
|
|
173
222
|
}
|
|
174
|
-
|
|
223
|
+
onActivate(tab.target)
|
|
175
224
|
}}
|
|
176
225
|
onKeyDown={(event) => {
|
|
177
226
|
if (event.key === 'F10' && event.shiftKey) {
|
|
@@ -212,6 +261,9 @@ export function L3ProjectFileTabs({
|
|
|
212
261
|
<span className={cn('truncate', change && getL3GitChangeClass(change))}>
|
|
213
262
|
{tab.label}
|
|
214
263
|
</span>
|
|
264
|
+
{tab.revisions ? (
|
|
265
|
+
<span className="shrink-0 font-mono text-xs">{tab.revisions}</span>
|
|
266
|
+
) : null}
|
|
215
267
|
{change ? (
|
|
216
268
|
<span
|
|
217
269
|
className={cn('shrink-0 text-xs', getL3GitChangeClass(change))}
|
|
@@ -225,8 +277,8 @@ export function L3ProjectFileTabs({
|
|
|
225
277
|
variant="ghost"
|
|
226
278
|
size="icon"
|
|
227
279
|
data-testid={
|
|
228
|
-
|
|
229
|
-
?
|
|
280
|
+
isDiff
|
|
281
|
+
? tab.testId.replace('file-diff-tab-', 'file-diff-tab-close-')
|
|
230
282
|
: `file-tab-close-${tab.path}`
|
|
231
283
|
}
|
|
232
284
|
aria-label={closeLabel}
|
|
@@ -236,7 +288,7 @@ export function L3ProjectFileTabs({
|
|
|
236
288
|
suppressTouchClickRef.current = false
|
|
237
289
|
return
|
|
238
290
|
}
|
|
239
|
-
onClose(tab.
|
|
291
|
+
onClose(tab.target, 'current')
|
|
240
292
|
}}
|
|
241
293
|
>
|
|
242
294
|
<XIcon className="size-3.5" />
|
|
@@ -258,7 +310,7 @@ export function L3ProjectFileTabs({
|
|
|
258
310
|
}
|
|
259
311
|
onClick={() => {
|
|
260
312
|
suppressTouchClickRef.current = false
|
|
261
|
-
onClose(tab.
|
|
313
|
+
onClose(tab.target, scope)
|
|
262
314
|
}}
|
|
263
315
|
>
|
|
264
316
|
{t(label)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const L2_PROJECT_HISTORY_MESSAGES = {
|
|
2
2
|
en: {
|
|
3
|
+
previewTitle: 'Project preview',
|
|
3
4
|
chooseProject: 'Open a project',
|
|
4
5
|
switchProject: 'Switch project',
|
|
5
6
|
directoryPath: 'Directory path',
|
|
@@ -26,7 +27,7 @@ export const L2_PROJECT_HISTORY_MESSAGES = {
|
|
|
26
27
|
noCommits: 'No commits',
|
|
27
28
|
scanLimit: 'History scan limit reached. Choose a more specific branch or commit.',
|
|
28
29
|
loadedLimit: '2,000 commits loaded. Narrow your search.',
|
|
29
|
-
|
|
30
|
+
noMoreCommits: 'All commits loaded',
|
|
30
31
|
backCommits: 'Back to commits',
|
|
31
32
|
commitChanges: 'Commit changes',
|
|
32
33
|
cumulativeChanges: 'Cumulative changes',
|
|
@@ -90,6 +91,7 @@ export const L2_PROJECT_HISTORY_MESSAGES = {
|
|
|
90
91
|
historyReadFailed: 'Could not read Git history'
|
|
91
92
|
},
|
|
92
93
|
'zh-CN': {
|
|
94
|
+
previewTitle: '项目预览',
|
|
93
95
|
chooseProject: '打开项目',
|
|
94
96
|
switchProject: '切换项目',
|
|
95
97
|
directoryPath: '目录路径',
|
|
@@ -116,7 +118,7 @@ export const L2_PROJECT_HISTORY_MESSAGES = {
|
|
|
116
118
|
noCommits: '没有提交记录',
|
|
117
119
|
scanLimit: '历史扫描达到上限,请选择更具体的分支或提交。',
|
|
118
120
|
loadedLimit: '已加载 2000 条,请缩小查询范围。',
|
|
119
|
-
|
|
121
|
+
noMoreCommits: '已加载全部提交',
|
|
120
122
|
backCommits: '返回提交列表',
|
|
121
123
|
commitChanges: '提交变更',
|
|
122
124
|
cumulativeChanges: '累计变更',
|