@jetcrab/pi-desk 1.1.18-dev → 1.1.20-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/docs/plugins.md +1 -1
- package/next.config.ts +1 -0
- package/package.json +3 -2
- package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-runtime.ts +1 -1
- package/src/client/l2_biz/workbench/hooks/l2-workbench-chat-reload.ts +42 -0
- package/src/client/l2_biz/workbench/l2-work-session-sidebar.tsx +19 -1
- package/src/client/l2_biz/workbench/l2-workbench-chat.ts +45 -0
- package/src/client/l2_biz/workbench/l2-workbench.tsx +4 -0
- package/src/client/l3_modules/conversation/l3-conversation-markdown.tsx +10 -1
- package/src/client/l3_modules/project-files/l3-markdown-preview.tsx +10 -1
- package/src/client/l3_modules/project-files/l3-project-document-view.tsx +2 -0
- package/src/client/l3_modules/project-files/l3-project-file-browser.tsx +171 -229
- package/src/client/l3_modules/project-files/l3-project-file-runtime.ts +133 -70
- package/src/client/l3_modules/project-files/l3-project-reader.ts +4 -3
- package/src/client/l3_modules/project-files/views/l3-project-search-controls.tsx +201 -0
- package/src/client/l3_modules/project-files/views/l3-project-search-results.tsx +333 -0
- package/src/client/l4_foundation/ui/code/l4-code-preview.tsx +47 -3
- package/src/client/l4_foundation/ui/markdown/l4-markdown-remark.ts +89 -0
- package/src/client/l4_foundation/ui/markdown/l4-markdown.module.css +58 -0
- package/src/client/l4_foundation/ui/markdown/l4-markdown.tsx +108 -0
- package/src/common/l2_biz/work-session/l2-work-session-file-contract.ts +4 -3
- package/src/common/l2_biz/workbench/l2-workbench-locale-messages.ts +6 -0
- package/src/common/l3_modules/project-files/l3-project-files-contract.ts +2 -3
- package/src/common/l3_modules/project-files/l3-project-files-locale-messages.ts +56 -10
- package/src/common/l4_foundation/file/l4-project-file-contract.ts +51 -1
- package/src/common/l4_foundation/locale/l4-host-error-messages.ts +11 -0
- package/src/common/l4_foundation/locale/l4-locale-messages.ts +16 -0
- package/src/instrumentation.ts +0 -2
- package/src/server/l1_entry/api/l1-project-read-routes.ts +6 -4
- package/src/server/l1_entry/api/l1-project-route.ts +9 -8
- package/src/server/l1_entry/api/l1-work-session-file-error.ts +23 -0
- package/src/server/l2_biz/plugin-management/l2-plugin-management.ts +0 -3
- package/src/server/l2_biz/work-session/l2-work-session-chat-runtime.ts +30 -58
- package/src/server/l2_biz/work-session/l2-work-session-manage.ts +0 -4
- package/src/server/l4_foundation/file/l4-project-file-index.ts +276 -0
- package/src/server/l4_foundation/file/l4-project-search.ts +399 -0
- package/src/server/l4_foundation/file/l4-ripgrep-search.ts +192 -0
- package/src/server/l4_foundation/file/l4-work-session-project-file.ts +8 -332
- package/src/server/l4_foundation/pi/l4-pi-package-maintenance.ts +0 -6
- package/src/server/l4_foundation/pi/l4-pi-plugin-capability-probe.ts +1 -3
- package/src/server/l4_foundation/pi/l4-pi-plugin-probe.ts +1 -3
- package/src/server/l4_foundation/pi/l4-pi-runner-process.ts +2 -2
- package/src/server/l4_foundation/process/l4-process-tree.js +23 -4
- 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 -2
- package/temp/build/pi-desk/release/.next/required-server-files.json +3 -2
- 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/api/project-files/get/route.js +2 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-files/list/route.js +2 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-files/reveal/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-files/search/route.js +2 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-git/get/route.js +2 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-git-branches/list/route.js +2 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-git-changes/list/route.js +2 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-git-diff/get/route.js +2 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-git-heads/get/route.js +2 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-git-log/list/route.js +2 -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 +77 -77
- 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/{33658.js → 10881.js} +8 -8
- package/temp/build/pi-desk/release/.next/server/chunks/23054.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/25406.js +3 -3
- package/temp/build/pi-desk/release/.next/server/chunks/35985.js +5 -5
- package/temp/build/pi-desk/release/.next/server/chunks/50520.js +2 -4
- package/temp/build/pi-desk/release/.next/server/chunks/6917.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/70780.js +1 -0
- package/temp/build/pi-desk/release/.next/server/chunks/74528.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/92386.js +1 -1
- 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/27611-0890482fe76bd9d6.js +238 -0
- package/temp/build/pi-desk/release/.next/static/chunks/{31087-ba426d09d6f68bfd.js → 31087-d8e5765d541d5c26.js} +1 -1
- package/temp/build/pi-desk/release/.next/static/chunks/{49516-cc4dcd84d64ab4fe.js → 51474-8dc714cb76b2e60a.js} +2 -2
- package/temp/build/pi-desk/release/.next/static/chunks/77592-5bef6acb8e7c4b96.js +13 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/layout-a50c456648537afb.js +3 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/{page-a984139e99586041.js → page-0789c43551b29dd2.js} +4 -4
- package/temp/build/pi-desk/release/.next/static/chunks/{webpack-40a782fc8a3535b9.js → webpack-d7248206b7fc7ba8.js} +1 -1
- package/temp/build/pi-desk/release/.next/static/css/250a6b93cc2222d1.css +1 -0
- package/temp/build/pi-desk/release/.next/static/css/6dbae2922f88ca31.css +1 -0
- package/temp/build/pi-desk/release/.next/static/css/a4cf28fd76c70165.css +1 -0
- package/temp/build/pi-desk/release/.next/trace-build +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/8584.js +0 -2
- package/temp/build/pi-desk/release/.next/static/chunks/39654-b5fedd762ea0312e.js +0 -238
- package/temp/build/pi-desk/release/.next/static/chunks/75130-d2ca74894aff8085.js +0 -13
- package/temp/build/pi-desk/release/.next/static/chunks/app/layout-c0e1c5cef8620cf8.js +0 -3
- package/temp/build/pi-desk/release/.next/static/css/6427d02b6edf406f.css +0 -1
- package/temp/build/pi-desk/release/.next/static/css/79e06f05df9edff0.css +0 -1
- /package/temp/build/pi-desk/release/.next/static/{n2LrXQgq8UkbH_AzhR3fw → RLKcK-RQF5FvEeDmS9uih}/_buildManifest.js +0 -0
- /package/temp/build/pi-desk/release/.next/static/{n2LrXQgq8UkbH_AzhR3fw → RLKcK-RQF5FvEeDmS9uih}/_ssgManifest.js +0 -0
|
@@ -108,7 +108,6 @@ interface L2WorkSessionChatRecord {
|
|
|
108
108
|
source: L2ChatSource
|
|
109
109
|
state: L2ChatSourceState
|
|
110
110
|
sourceEventVersion: number
|
|
111
|
-
forceFullSync: boolean
|
|
112
111
|
rawTemporaryMessages: Map<string, L4PiChatMessage>
|
|
113
112
|
metadata: L2WorkSessionChatMetadata
|
|
114
113
|
mainRunning: boolean
|
|
@@ -302,20 +301,6 @@ export class L2WorkSessionChatRuntime {
|
|
|
302
301
|
return isCurrent() ? { cwd, isCurrent } : null
|
|
303
302
|
}
|
|
304
303
|
|
|
305
|
-
refreshPluginMessages(): void {
|
|
306
|
-
for (const record of this.recordsByWorkId.values()) {
|
|
307
|
-
if (record.state.runtime.presentationMode === 'basic') continue
|
|
308
|
-
try {
|
|
309
|
-
this.reprojectRecord(record, record.state.runtime.presentationMode)
|
|
310
|
-
} catch (error) {
|
|
311
|
-
console.error('[Pi Desk][WorkSessionChatRuntime] 插件消息刷新失败,保留已有聊天', {
|
|
312
|
-
workId: record.source.workId,
|
|
313
|
-
message: error instanceof Error ? error.message : String(error)
|
|
314
|
-
})
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
304
|
setPresentation(source: L2ChatSource, mode: L2ChatMode): Promise<void> {
|
|
320
305
|
const record = this.currentRecord(source)
|
|
321
306
|
return this.enqueueCommand(record, async () => {
|
|
@@ -363,7 +348,6 @@ export class L2WorkSessionChatRuntime {
|
|
|
363
348
|
temporaryMessages,
|
|
364
349
|
runtime: { ...record.state.runtime, presentationMode: mode }
|
|
365
350
|
}
|
|
366
|
-
record.forceFullSync = true
|
|
367
351
|
record.sourceEventVersion += 1
|
|
368
352
|
this.publish({
|
|
369
353
|
type: 'source_event',
|
|
@@ -380,16 +364,14 @@ export class L2WorkSessionChatRuntime {
|
|
|
380
364
|
|
|
381
365
|
private async syncRecordRuntime(record: L2WorkSessionChatRecord): Promise<void> {
|
|
382
366
|
const source = record.source
|
|
383
|
-
const runtime =
|
|
384
|
-
|
|
367
|
+
const runtime = await getL4PiWorkSessionRuntime(source.sessionId).readChatRuntime()
|
|
368
|
+
const nextRuntime = L2ChatRuntimeSchema.parse({
|
|
369
|
+
...runtime,
|
|
385
370
|
presentationMode: record.state.runtime.presentationMode
|
|
386
371
|
})
|
|
387
372
|
if (!this.isCurrentSource(source)) return
|
|
388
|
-
if (!sameRuntime(record.state.runtime,
|
|
389
|
-
this.applySourceEvent(source, { type: 'runtime_update', runtime })
|
|
390
|
-
if (runtime.extensionMode === 'basic' && record.state.runtime.presentationMode !== 'basic') {
|
|
391
|
-
this.reprojectRecord(record, 'basic')
|
|
392
|
-
}
|
|
373
|
+
if (!sameRuntime(record.state.runtime, nextRuntime))
|
|
374
|
+
this.applySourceEvent(source, { type: 'runtime_update', runtime: nextRuntime })
|
|
393
375
|
}
|
|
394
376
|
|
|
395
377
|
createSync(sourceInput: L2ChatSource, cursor: L2ChatCursor): L2PreparedChatSourceSync {
|
|
@@ -413,24 +395,23 @@ export class L2WorkSessionChatRuntime {
|
|
|
413
395
|
})
|
|
414
396
|
}
|
|
415
397
|
|
|
416
|
-
const event: L2ChatSessionSyncEvent =
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
398
|
+
const event: L2ChatSessionSyncEvent = cursorMatches
|
|
399
|
+
? {
|
|
400
|
+
type: 'session_sync',
|
|
401
|
+
mode: 'incremental',
|
|
402
|
+
baseCursor: cursor,
|
|
403
|
+
messages: messages.slice(cursor.index + 1).map(withoutL3ConversationMessageDetail),
|
|
404
|
+
temporaryMessages,
|
|
405
|
+
runtime
|
|
406
|
+
}
|
|
407
|
+
: {
|
|
408
|
+
type: 'session_sync',
|
|
409
|
+
mode: 'full',
|
|
410
|
+
baseCursor: null,
|
|
411
|
+
messages: messages.map(withoutL3ConversationMessageDetail),
|
|
412
|
+
temporaryMessages,
|
|
413
|
+
runtime
|
|
414
|
+
}
|
|
434
415
|
|
|
435
416
|
return { event, watermark: record.sourceEventVersion }
|
|
436
417
|
}
|
|
@@ -548,12 +529,6 @@ export class L2WorkSessionChatRuntime {
|
|
|
548
529
|
} finally {
|
|
549
530
|
if (this.recordsByWorkId.get(source.workId) === record) {
|
|
550
531
|
await this.syncRecordRuntime(record)
|
|
551
|
-
if (
|
|
552
|
-
record.state.runtime.extensionMode === 'basic' &&
|
|
553
|
-
record.state.runtime.presentationMode !== 'basic'
|
|
554
|
-
) {
|
|
555
|
-
this.reprojectRecord(record, 'basic')
|
|
556
|
-
}
|
|
557
532
|
}
|
|
558
533
|
}
|
|
559
534
|
})
|
|
@@ -685,7 +660,6 @@ export class L2WorkSessionChatRuntime {
|
|
|
685
660
|
source,
|
|
686
661
|
state,
|
|
687
662
|
sourceEventVersion: 0,
|
|
688
|
-
forceFullSync: isL4PiDeskSafeMode(),
|
|
689
663
|
rawTemporaryMessages: new Map(),
|
|
690
664
|
metadata: {
|
|
691
665
|
sessionTitle: snapshot.sessionName ?? firstUserTitle,
|
|
@@ -714,6 +688,9 @@ export class L2WorkSessionChatRuntime {
|
|
|
714
688
|
if (this.recordsByWorkId.get(record.source.workId) !== record) return
|
|
715
689
|
const previousMetadata = record.metadata
|
|
716
690
|
const previousStatus = this.statusForRecord(record)
|
|
691
|
+
const basic =
|
|
692
|
+
record.state.runtime.extensionMode === 'basic' ||
|
|
693
|
+
record.state.runtime.presentationMode === 'basic'
|
|
717
694
|
|
|
718
695
|
switch (event.type) {
|
|
719
696
|
case 'message_start':
|
|
@@ -726,7 +703,7 @@ export class L2WorkSessionChatRuntime {
|
|
|
726
703
|
record.source,
|
|
727
704
|
false,
|
|
728
705
|
'temporary',
|
|
729
|
-
|
|
706
|
+
basic
|
|
730
707
|
)
|
|
731
708
|
})
|
|
732
709
|
return
|
|
@@ -744,7 +721,7 @@ export class L2WorkSessionChatRuntime {
|
|
|
744
721
|
record.source,
|
|
745
722
|
false,
|
|
746
723
|
'update',
|
|
747
|
-
|
|
724
|
+
basic
|
|
748
725
|
)
|
|
749
726
|
)
|
|
750
727
|
if (update) this.applySourceEvent(record.source, update)
|
|
@@ -761,7 +738,7 @@ export class L2WorkSessionChatRuntime {
|
|
|
761
738
|
record.source,
|
|
762
739
|
false,
|
|
763
740
|
'final',
|
|
764
|
-
|
|
741
|
+
basic
|
|
765
742
|
)
|
|
766
743
|
const update = buildL3ConversationMessageUpdate(current, publicFinal)
|
|
767
744
|
if (update) this.applySourceEvent(record.source, update)
|
|
@@ -801,12 +778,6 @@ export class L2WorkSessionChatRuntime {
|
|
|
801
778
|
if (!sameRuntime(record.state.runtime, runtime)) {
|
|
802
779
|
this.applySourceEvent(record.source, { type: 'runtime_update', runtime })
|
|
803
780
|
}
|
|
804
|
-
if (
|
|
805
|
-
runtime.extensionMode === 'basic' &&
|
|
806
|
-
record.state.runtime.presentationMode !== 'basic'
|
|
807
|
-
) {
|
|
808
|
-
this.reprojectRecord(record, 'basic')
|
|
809
|
-
}
|
|
810
781
|
return
|
|
811
782
|
}
|
|
812
783
|
case 'agent_started':
|
|
@@ -861,7 +832,8 @@ export class L2WorkSessionChatRuntime {
|
|
|
861
832
|
record.source,
|
|
862
833
|
true,
|
|
863
834
|
'final',
|
|
864
|
-
record.state.runtime.
|
|
835
|
+
record.state.runtime.extensionMode === 'basic' ||
|
|
836
|
+
record.state.runtime.presentationMode === 'basic'
|
|
865
837
|
)
|
|
866
838
|
record.state = { ...record.state, temporaryMessages }
|
|
867
839
|
}
|
|
@@ -250,10 +250,6 @@ export class WorkSessionManage {
|
|
|
250
250
|
})
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
refreshPluginMessages(): void {
|
|
254
|
-
this.chat.refreshPluginMessages()
|
|
255
|
-
}
|
|
256
|
-
|
|
257
253
|
runPluginRestart<T>(operation: () => Promise<T>): Promise<T> {
|
|
258
254
|
return this.runExclusive(async () => {
|
|
259
255
|
await this.ensureHydrated()
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import { execFile } from 'node:child_process'
|
|
4
|
+
import { readdir, realpath } from 'node:fs/promises'
|
|
5
|
+
import { basename, relative, resolve, sep } from 'node:path'
|
|
6
|
+
import { promisify } from 'node:util'
|
|
7
|
+
import { listL4WorkSessionGitRepositoryRoots } from '@server/l4_foundation/git/l4-work-session-git'
|
|
8
|
+
import { getL4FileSystemKey as fileSystemKey } from './l4-file-path'
|
|
9
|
+
|
|
10
|
+
const execFileAsync = promisify(execFile)
|
|
11
|
+
const SEARCH_CACHE_TTL_MS = 30_000
|
|
12
|
+
const SEARCH_CACHE_MAX_CWDS = 20
|
|
13
|
+
const SEARCH_INDEX_MAX_FILES = 50_000
|
|
14
|
+
|
|
15
|
+
export const IGNORED_NAMES = new Set([
|
|
16
|
+
'.git',
|
|
17
|
+
'node_modules',
|
|
18
|
+
'.next',
|
|
19
|
+
'dist',
|
|
20
|
+
'build',
|
|
21
|
+
'coverage',
|
|
22
|
+
'.turbo'
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
interface SearchIndexCacheEntry {
|
|
26
|
+
files: string[]
|
|
27
|
+
truncated: boolean
|
|
28
|
+
expiresAt: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const searchIndexCache = new Map<string, SearchIndexCacheEntry>()
|
|
32
|
+
const pendingSearchIndexes = new Map<string, Promise<SearchIndexCacheEntry>>()
|
|
33
|
+
|
|
34
|
+
function joinProtocolPath(parent: string, name: string): string {
|
|
35
|
+
return parent ? `${parent}/${name}` : name
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isSupportedProtocolName(name: string): boolean {
|
|
39
|
+
return name.length > 0 && !name.includes('/') && !name.includes('\\') && !name.includes('\0')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function pathContainsIgnoredSegment(path: string): boolean {
|
|
43
|
+
return path.split('/').some((segment) => IGNORED_NAMES.has(segment))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function invalidateSearchIndex(root: string): void {
|
|
47
|
+
const key = fileSystemKey(root)
|
|
48
|
+
searchIndexCache.delete(key)
|
|
49
|
+
pendingSearchIndexes.delete(key)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const GIT_SEARCH_OPTIONS = {
|
|
53
|
+
timeout: 10_000,
|
|
54
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
55
|
+
env: { ...process.env, LC_ALL: 'C', GIT_OPTIONAL_LOCKS: '0' }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function listFilesWithContainingGit(
|
|
59
|
+
root: string
|
|
60
|
+
): Promise<{ files: string[]; truncated: boolean } | null> {
|
|
61
|
+
try {
|
|
62
|
+
const { stdout: topLevelOutput } = await execFileAsync(
|
|
63
|
+
'git',
|
|
64
|
+
['-C', root, 'rev-parse', '--show-toplevel'],
|
|
65
|
+
GIT_SEARCH_OPTIONS
|
|
66
|
+
)
|
|
67
|
+
const topLevel = await realpath(topLevelOutput.trim())
|
|
68
|
+
const prefix = relative(topLevel, root).split(sep).join('/')
|
|
69
|
+
const { stdout } = await execFileAsync(
|
|
70
|
+
'git',
|
|
71
|
+
[
|
|
72
|
+
'-C',
|
|
73
|
+
topLevel,
|
|
74
|
+
'ls-files',
|
|
75
|
+
'--full-name',
|
|
76
|
+
'--cached',
|
|
77
|
+
'--others',
|
|
78
|
+
'--exclude-standard',
|
|
79
|
+
'-z',
|
|
80
|
+
'--',
|
|
81
|
+
prefix || '.'
|
|
82
|
+
],
|
|
83
|
+
GIT_SEARCH_OPTIONS
|
|
84
|
+
)
|
|
85
|
+
const prefixWithSlash = prefix ? `${prefix}/` : ''
|
|
86
|
+
const all = stdout
|
|
87
|
+
.split('\0')
|
|
88
|
+
.flatMap((path) => {
|
|
89
|
+
if (!path) return []
|
|
90
|
+
const projectPath = prefixWithSlash
|
|
91
|
+
? path.startsWith(prefixWithSlash)
|
|
92
|
+
? path.slice(prefixWithSlash.length)
|
|
93
|
+
: ''
|
|
94
|
+
: path
|
|
95
|
+
return projectPath ? [projectPath] : []
|
|
96
|
+
})
|
|
97
|
+
.filter(
|
|
98
|
+
(path) =>
|
|
99
|
+
isSupportedProtocolName(basename(path)) &&
|
|
100
|
+
!path.includes('\\') &&
|
|
101
|
+
!pathContainsIgnoredSegment(path)
|
|
102
|
+
)
|
|
103
|
+
return {
|
|
104
|
+
files: all.slice(0, SEARCH_INDEX_MAX_FILES),
|
|
105
|
+
truncated: all.length > SEARCH_INDEX_MAX_FILES
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isPathInsideRepository(path: string, repositoryRoot: string): boolean {
|
|
113
|
+
return path === repositoryRoot || path.startsWith(`${repositoryRoot}/`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function listFilesInRepository(
|
|
117
|
+
projectRoot: string,
|
|
118
|
+
repositoryRoot: string,
|
|
119
|
+
nestedRepositoryRoots: readonly string[]
|
|
120
|
+
): Promise<{ files: string[]; truncated: boolean } | null> {
|
|
121
|
+
const absoluteRoot = repositoryRoot
|
|
122
|
+
? resolve(projectRoot, ...repositoryRoot.split('/'))
|
|
123
|
+
: projectRoot
|
|
124
|
+
try {
|
|
125
|
+
const { stdout } = await execFileAsync(
|
|
126
|
+
'git',
|
|
127
|
+
[
|
|
128
|
+
'-C',
|
|
129
|
+
absoluteRoot,
|
|
130
|
+
'ls-files',
|
|
131
|
+
'--full-name',
|
|
132
|
+
'--cached',
|
|
133
|
+
'--others',
|
|
134
|
+
'--exclude-standard',
|
|
135
|
+
'-z'
|
|
136
|
+
],
|
|
137
|
+
GIT_SEARCH_OPTIONS
|
|
138
|
+
)
|
|
139
|
+
const files = stdout.split('\0').flatMap((path) => {
|
|
140
|
+
if (!path || path.includes('\\') || pathContainsIgnoredSegment(path)) return []
|
|
141
|
+
const projectPath = joinProtocolPath(repositoryRoot, path)
|
|
142
|
+
if (
|
|
143
|
+
nestedRepositoryRoots.some((nestedRoot) => isPathInsideRepository(projectPath, nestedRoot))
|
|
144
|
+
) {
|
|
145
|
+
return []
|
|
146
|
+
}
|
|
147
|
+
return isSupportedProtocolName(basename(projectPath)) ? [projectPath] : []
|
|
148
|
+
})
|
|
149
|
+
return {
|
|
150
|
+
files: files.slice(0, SEARCH_INDEX_MAX_FILES),
|
|
151
|
+
truncated: files.length > SEARCH_INDEX_MAX_FILES
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function listFilesWithWalk(
|
|
159
|
+
root: string,
|
|
160
|
+
excludedRepositoryRoots: readonly string[] = []
|
|
161
|
+
): Promise<{ files: string[]; truncated: boolean }> {
|
|
162
|
+
const files: string[] = []
|
|
163
|
+
const queue: Array<{ absolute: string; relative: string }> = [{ absolute: root, relative: '' }]
|
|
164
|
+
let queueIndex = 0
|
|
165
|
+
|
|
166
|
+
while (queueIndex < queue.length) {
|
|
167
|
+
const current = queue[queueIndex]!
|
|
168
|
+
queueIndex += 1
|
|
169
|
+
let dirents
|
|
170
|
+
try {
|
|
171
|
+
dirents = await readdir(current.absolute, { withFileTypes: true })
|
|
172
|
+
} catch {
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const entry of dirents) {
|
|
177
|
+
if (IGNORED_NAMES.has(entry.name) || !isSupportedProtocolName(entry.name)) continue
|
|
178
|
+
const childRelative = joinProtocolPath(current.relative, entry.name)
|
|
179
|
+
if (entry.isDirectory()) {
|
|
180
|
+
if (excludedRepositoryRoots.includes(childRelative)) continue
|
|
181
|
+
queue.push({ absolute: resolve(current.absolute, entry.name), relative: childRelative })
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
if (!entry.isFile()) continue
|
|
185
|
+
files.push(childRelative)
|
|
186
|
+
if (files.length >= SEARCH_INDEX_MAX_FILES) return { files, truncated: true }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { files, truncated: false }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function listFilesWithRepositoryRoots(
|
|
194
|
+
projectRoot: string,
|
|
195
|
+
repositoryRoots: readonly string[]
|
|
196
|
+
): Promise<{ files: string[]; truncated: boolean }> {
|
|
197
|
+
const files = new Set<string>()
|
|
198
|
+
let truncated = false
|
|
199
|
+
const hasProjectRootRepository = repositoryRoots.includes('')
|
|
200
|
+
|
|
201
|
+
if (!hasProjectRootRepository) {
|
|
202
|
+
const workspaceFiles = await listFilesWithWalk(projectRoot, repositoryRoots.filter(Boolean))
|
|
203
|
+
for (const path of workspaceFiles.files) files.add(path)
|
|
204
|
+
truncated ||= workspaceFiles.truncated
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (const repositoryRoot of repositoryRoots) {
|
|
208
|
+
const nestedRoots = repositoryRoots.filter(
|
|
209
|
+
(candidate) =>
|
|
210
|
+
candidate !== repositoryRoot &&
|
|
211
|
+
(repositoryRoot === '' || candidate.startsWith(`${repositoryRoot}/`))
|
|
212
|
+
)
|
|
213
|
+
let listing = await listFilesInRepository(projectRoot, repositoryRoot, nestedRoots)
|
|
214
|
+
if (!listing) {
|
|
215
|
+
const absoluteRoot = repositoryRoot
|
|
216
|
+
? resolve(projectRoot, ...repositoryRoot.split('/'))
|
|
217
|
+
: projectRoot
|
|
218
|
+
const nestedRelativeRoots = nestedRoots.map((root) =>
|
|
219
|
+
repositoryRoot ? root.slice(repositoryRoot.length + 1) : root
|
|
220
|
+
)
|
|
221
|
+
const fallback = await listFilesWithWalk(absoluteRoot, nestedRelativeRoots)
|
|
222
|
+
listing = {
|
|
223
|
+
files: fallback.files.map((path) => joinProtocolPath(repositoryRoot, path)),
|
|
224
|
+
truncated: fallback.truncated
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (const path of listing.files) {
|
|
228
|
+
files.add(path)
|
|
229
|
+
if (files.size >= SEARCH_INDEX_MAX_FILES) {
|
|
230
|
+
return { files: [...files], truncated: true }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
truncated ||= listing.truncated
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { files: [...files], truncated }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function getSearchIndex(root: string): Promise<SearchIndexCacheEntry> {
|
|
240
|
+
const key = fileSystemKey(root)
|
|
241
|
+
const now = Date.now()
|
|
242
|
+
const cached = searchIndexCache.get(key)
|
|
243
|
+
if (cached && cached.expiresAt > now) return cached
|
|
244
|
+
|
|
245
|
+
const active = pendingSearchIndexes.get(key)
|
|
246
|
+
if (active) return active
|
|
247
|
+
|
|
248
|
+
const load: Promise<SearchIndexCacheEntry> = Promise.resolve().then(async () => {
|
|
249
|
+
const repositoryRoots = await listL4WorkSessionGitRepositoryRoots(root)
|
|
250
|
+
const listing =
|
|
251
|
+
repositoryRoots.length > 0
|
|
252
|
+
? await listFilesWithRepositoryRoots(root, repositoryRoots)
|
|
253
|
+
: ((await listFilesWithContainingGit(root)) ?? (await listFilesWithWalk(root)))
|
|
254
|
+
const next: SearchIndexCacheEntry = {
|
|
255
|
+
...listing,
|
|
256
|
+
expiresAt: now + SEARCH_CACHE_TTL_MS
|
|
257
|
+
}
|
|
258
|
+
if (pendingSearchIndexes.get(key) === load) {
|
|
259
|
+
for (const [cacheKey, entry] of searchIndexCache) {
|
|
260
|
+
if (entry.expiresAt <= Date.now()) searchIndexCache.delete(cacheKey)
|
|
261
|
+
}
|
|
262
|
+
while (searchIndexCache.size >= SEARCH_CACHE_MAX_CWDS) {
|
|
263
|
+
const oldestKey = searchIndexCache.keys().next().value as string
|
|
264
|
+
searchIndexCache.delete(oldestKey)
|
|
265
|
+
}
|
|
266
|
+
searchIndexCache.set(key, next)
|
|
267
|
+
}
|
|
268
|
+
return next
|
|
269
|
+
})
|
|
270
|
+
pendingSearchIndexes.set(key, load)
|
|
271
|
+
try {
|
|
272
|
+
return await load
|
|
273
|
+
} finally {
|
|
274
|
+
if (pendingSearchIndexes.get(key) === load) pendingSearchIndexes.delete(key)
|
|
275
|
+
}
|
|
276
|
+
}
|