@jetcrab/pi-desk 1.1.16 → 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 +3 -1
- package/next.config.ts +1 -0
- package/package.json +4 -3
- 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.module.css +1 -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 +3 -15
- package/src/server/l4_foundation/process/l4-process-tree.js +31 -5
- 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 +4 -3
- package/temp/build/pi-desk/release/.next/required-server-files.json +4 -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/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-0069319a4fb97211.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/949d0d1a99d5de92.css +0 -1
- /package/temp/build/pi-desk/release/.next/static/{DqamF28k0ShQLbNVzXDIj → RLKcK-RQF5FvEeDmS9uih}/_buildManifest.js +0 -0
- /package/temp/build/pi-desk/release/.next/static/{DqamF28k0ShQLbNVzXDIj → RLKcK-RQF5FvEeDmS9uih}/_ssgManifest.js +0 -0
|
@@ -1,47 +1,31 @@
|
|
|
1
1
|
import 'server-only'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
4
|
import { open, readFile, readdir, realpath, stat } from 'node:fs/promises'
|
|
5
|
-
import {
|
|
6
|
-
import { promisify } from 'node:util'
|
|
5
|
+
import { dirname, isAbsolute, resolve } from 'node:path'
|
|
7
6
|
import sharp from 'sharp'
|
|
8
7
|
import {
|
|
9
8
|
L4_PROJECT_FILE_LIST_MAX_ENTRIES,
|
|
10
|
-
L4_PROJECT_FILE_SEARCH_MAX_RESULTS,
|
|
11
9
|
L4_PROJECT_IMAGE_FILE_MAX_BYTES,
|
|
12
10
|
L4_PROJECT_IMAGE_MAX_PIXELS,
|
|
13
11
|
L4_PROJECT_TEXT_FILE_MAX_BYTES,
|
|
14
12
|
type L4ProjectFileEntry,
|
|
15
13
|
type L4ProjectFileList,
|
|
16
|
-
type L4ProjectFileSearch,
|
|
17
14
|
type L4ProjectFileContent,
|
|
18
15
|
type L4ProjectFileImageMimeType,
|
|
19
16
|
type L4ProjectFileImagePreviewMode
|
|
20
17
|
} from '@common/l4_foundation/file/l4-project-file-contract'
|
|
21
|
-
import {
|
|
18
|
+
import { isL4PathInsideRoot as isInsideRoot } from './l4-file-path'
|
|
22
19
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const SEARCH_CACHE_TTL_MS = 30_000
|
|
29
|
-
const SEARCH_CACHE_MAX_CWDS = 20
|
|
30
|
-
const SEARCH_INDEX_MAX_FILES = 50_000
|
|
20
|
+
IGNORED_NAMES,
|
|
21
|
+
invalidateSearchIndex,
|
|
22
|
+
isSupportedProtocolName
|
|
23
|
+
} from './l4-project-file-index'
|
|
24
|
+
|
|
31
25
|
const IMAGE_COMPRESSION_MIN_BYTES = 512 * 1024
|
|
32
26
|
const IMAGE_COMPRESSION_MAX_WIDTH = 2560
|
|
33
27
|
const IMAGE_COMPRESSION_MIN_SAVING_RATIO = 0.15
|
|
34
28
|
|
|
35
|
-
const IGNORED_NAMES = new Set([
|
|
36
|
-
'.git',
|
|
37
|
-
'node_modules',
|
|
38
|
-
'.next',
|
|
39
|
-
'dist',
|
|
40
|
-
'build',
|
|
41
|
-
'coverage',
|
|
42
|
-
'.turbo'
|
|
43
|
-
])
|
|
44
|
-
|
|
45
29
|
export type L4WorkSessionProjectFileErrorKind =
|
|
46
30
|
| 'not-found'
|
|
47
31
|
| 'not-directory'
|
|
@@ -71,15 +55,8 @@ export function getL4WorkSessionProjectFileErrorKind(
|
|
|
71
55
|
|
|
72
56
|
export type L4WorkSessionProjectFileEntry = L4ProjectFileEntry
|
|
73
57
|
export type L4WorkSessionProjectFileList = L4ProjectFileList
|
|
74
|
-
export type L4WorkSessionProjectFileSearch = L4ProjectFileSearch
|
|
75
58
|
export type L4WorkSessionProjectFileGet = L4ProjectFileContent
|
|
76
59
|
|
|
77
|
-
interface SearchIndexCacheEntry {
|
|
78
|
-
files: string[]
|
|
79
|
-
truncated: boolean
|
|
80
|
-
expiresAt: number
|
|
81
|
-
}
|
|
82
|
-
|
|
83
60
|
interface ProjectFileRevealTarget {
|
|
84
61
|
target: string
|
|
85
62
|
directory: string
|
|
@@ -88,21 +65,6 @@ interface ProjectFileRevealTarget {
|
|
|
88
65
|
|
|
89
66
|
type ProjectFileRevealRunner = (command: string, args: string[]) => Promise<void>
|
|
90
67
|
|
|
91
|
-
const searchIndexCache = new Map<string, SearchIndexCacheEntry>()
|
|
92
|
-
const pendingSearchIndexes = new Map<string, Promise<SearchIndexCacheEntry>>()
|
|
93
|
-
|
|
94
|
-
function joinProtocolPath(parent: string, name: string): string {
|
|
95
|
-
return parent ? `${parent}/${name}` : name
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function isSupportedProtocolName(name: string): boolean {
|
|
99
|
-
return name.length > 0 && !name.includes('/') && !name.includes('\\') && !name.includes('\0')
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function pathContainsIgnoredSegment(path: string): boolean {
|
|
103
|
-
return path.split('/').some((segment) => IGNORED_NAMES.has(segment))
|
|
104
|
-
}
|
|
105
|
-
|
|
106
68
|
async function resolveProjectRoot(cwd: string): Promise<string> {
|
|
107
69
|
try {
|
|
108
70
|
const root = await realpath(cwd)
|
|
@@ -249,12 +211,6 @@ async function runProjectFileRevealCommand(command: string, args: string[]): Pro
|
|
|
249
211
|
})
|
|
250
212
|
}
|
|
251
213
|
|
|
252
|
-
function invalidateSearchIndex(root: string): void {
|
|
253
|
-
const key = fileSystemKey(root)
|
|
254
|
-
searchIndexCache.delete(key)
|
|
255
|
-
pendingSearchIndexes.delete(key)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
214
|
export async function listL4WorkSessionProjectFiles(
|
|
259
215
|
cwd: string,
|
|
260
216
|
protocolPath: string
|
|
@@ -318,286 +274,6 @@ export async function listL4WorkSessionProjectFiles(
|
|
|
318
274
|
}
|
|
319
275
|
}
|
|
320
276
|
|
|
321
|
-
const GIT_SEARCH_OPTIONS = {
|
|
322
|
-
timeout: 10_000,
|
|
323
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
324
|
-
env: { ...process.env, LC_ALL: 'C', GIT_OPTIONAL_LOCKS: '0' }
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async function listFilesWithContainingGit(
|
|
328
|
-
root: string
|
|
329
|
-
): Promise<{ files: string[]; truncated: boolean } | null> {
|
|
330
|
-
try {
|
|
331
|
-
const { stdout: topLevelOutput } = await execFileAsync(
|
|
332
|
-
'git',
|
|
333
|
-
['-C', root, 'rev-parse', '--show-toplevel'],
|
|
334
|
-
GIT_SEARCH_OPTIONS
|
|
335
|
-
)
|
|
336
|
-
const topLevel = await realpath(topLevelOutput.trim())
|
|
337
|
-
const prefix = relative(topLevel, root).split(sep).join('/')
|
|
338
|
-
const { stdout } = await execFileAsync(
|
|
339
|
-
'git',
|
|
340
|
-
[
|
|
341
|
-
'-C',
|
|
342
|
-
topLevel,
|
|
343
|
-
'ls-files',
|
|
344
|
-
'--full-name',
|
|
345
|
-
'--cached',
|
|
346
|
-
'--others',
|
|
347
|
-
'--exclude-standard',
|
|
348
|
-
'-z',
|
|
349
|
-
'--',
|
|
350
|
-
prefix || '.'
|
|
351
|
-
],
|
|
352
|
-
GIT_SEARCH_OPTIONS
|
|
353
|
-
)
|
|
354
|
-
const prefixWithSlash = prefix ? `${prefix}/` : ''
|
|
355
|
-
const all = stdout
|
|
356
|
-
.split('\0')
|
|
357
|
-
.flatMap((path) => {
|
|
358
|
-
if (!path) return []
|
|
359
|
-
const projectPath = prefixWithSlash
|
|
360
|
-
? path.startsWith(prefixWithSlash)
|
|
361
|
-
? path.slice(prefixWithSlash.length)
|
|
362
|
-
: ''
|
|
363
|
-
: path
|
|
364
|
-
return projectPath ? [projectPath] : []
|
|
365
|
-
})
|
|
366
|
-
.filter(
|
|
367
|
-
(path) =>
|
|
368
|
-
isSupportedProtocolName(basename(path)) &&
|
|
369
|
-
!path.includes('\\') &&
|
|
370
|
-
!pathContainsIgnoredSegment(path)
|
|
371
|
-
)
|
|
372
|
-
return {
|
|
373
|
-
files: all.slice(0, SEARCH_INDEX_MAX_FILES),
|
|
374
|
-
truncated: all.length > SEARCH_INDEX_MAX_FILES
|
|
375
|
-
}
|
|
376
|
-
} catch {
|
|
377
|
-
return null
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function isPathInsideRepository(path: string, repositoryRoot: string): boolean {
|
|
382
|
-
return path === repositoryRoot || path.startsWith(`${repositoryRoot}/`)
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
async function listFilesInRepository(
|
|
386
|
-
projectRoot: string,
|
|
387
|
-
repositoryRoot: string,
|
|
388
|
-
nestedRepositoryRoots: readonly string[]
|
|
389
|
-
): Promise<{ files: string[]; truncated: boolean } | null> {
|
|
390
|
-
const absoluteRoot = repositoryRoot
|
|
391
|
-
? resolve(projectRoot, ...repositoryRoot.split('/'))
|
|
392
|
-
: projectRoot
|
|
393
|
-
try {
|
|
394
|
-
const { stdout } = await execFileAsync(
|
|
395
|
-
'git',
|
|
396
|
-
[
|
|
397
|
-
'-C',
|
|
398
|
-
absoluteRoot,
|
|
399
|
-
'ls-files',
|
|
400
|
-
'--full-name',
|
|
401
|
-
'--cached',
|
|
402
|
-
'--others',
|
|
403
|
-
'--exclude-standard',
|
|
404
|
-
'-z'
|
|
405
|
-
],
|
|
406
|
-
GIT_SEARCH_OPTIONS
|
|
407
|
-
)
|
|
408
|
-
const files = stdout.split('\0').flatMap((path) => {
|
|
409
|
-
if (!path || path.includes('\\') || pathContainsIgnoredSegment(path)) return []
|
|
410
|
-
const projectPath = joinProtocolPath(repositoryRoot, path)
|
|
411
|
-
if (
|
|
412
|
-
nestedRepositoryRoots.some((nestedRoot) => isPathInsideRepository(projectPath, nestedRoot))
|
|
413
|
-
) {
|
|
414
|
-
return []
|
|
415
|
-
}
|
|
416
|
-
return isSupportedProtocolName(basename(projectPath)) ? [projectPath] : []
|
|
417
|
-
})
|
|
418
|
-
return {
|
|
419
|
-
files: files.slice(0, SEARCH_INDEX_MAX_FILES),
|
|
420
|
-
truncated: files.length > SEARCH_INDEX_MAX_FILES
|
|
421
|
-
}
|
|
422
|
-
} catch {
|
|
423
|
-
return null
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
async function listFilesWithWalk(
|
|
428
|
-
root: string,
|
|
429
|
-
excludedRepositoryRoots: readonly string[] = []
|
|
430
|
-
): Promise<{ files: string[]; truncated: boolean }> {
|
|
431
|
-
const files: string[] = []
|
|
432
|
-
const queue: Array<{ absolute: string; relative: string }> = [{ absolute: root, relative: '' }]
|
|
433
|
-
let queueIndex = 0
|
|
434
|
-
|
|
435
|
-
while (queueIndex < queue.length) {
|
|
436
|
-
const current = queue[queueIndex]!
|
|
437
|
-
queueIndex += 1
|
|
438
|
-
let dirents
|
|
439
|
-
try {
|
|
440
|
-
dirents = await readdir(current.absolute, { withFileTypes: true })
|
|
441
|
-
} catch {
|
|
442
|
-
continue
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
for (const entry of dirents) {
|
|
446
|
-
if (IGNORED_NAMES.has(entry.name) || !isSupportedProtocolName(entry.name)) continue
|
|
447
|
-
const childRelative = joinProtocolPath(current.relative, entry.name)
|
|
448
|
-
if (entry.isDirectory()) {
|
|
449
|
-
if (excludedRepositoryRoots.includes(childRelative)) continue
|
|
450
|
-
queue.push({ absolute: resolve(current.absolute, entry.name), relative: childRelative })
|
|
451
|
-
continue
|
|
452
|
-
}
|
|
453
|
-
if (!entry.isFile()) continue
|
|
454
|
-
files.push(childRelative)
|
|
455
|
-
if (files.length >= SEARCH_INDEX_MAX_FILES) return { files, truncated: true }
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
return { files, truncated: false }
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
async function listFilesWithRepositoryRoots(
|
|
463
|
-
projectRoot: string,
|
|
464
|
-
repositoryRoots: readonly string[]
|
|
465
|
-
): Promise<{ files: string[]; truncated: boolean }> {
|
|
466
|
-
const files = new Set<string>()
|
|
467
|
-
let truncated = false
|
|
468
|
-
const hasProjectRootRepository = repositoryRoots.includes('')
|
|
469
|
-
|
|
470
|
-
if (!hasProjectRootRepository) {
|
|
471
|
-
const workspaceFiles = await listFilesWithWalk(projectRoot, repositoryRoots.filter(Boolean))
|
|
472
|
-
for (const path of workspaceFiles.files) files.add(path)
|
|
473
|
-
truncated ||= workspaceFiles.truncated
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
for (const repositoryRoot of repositoryRoots) {
|
|
477
|
-
const nestedRoots = repositoryRoots.filter(
|
|
478
|
-
(candidate) =>
|
|
479
|
-
candidate !== repositoryRoot &&
|
|
480
|
-
(repositoryRoot === '' || candidate.startsWith(`${repositoryRoot}/`))
|
|
481
|
-
)
|
|
482
|
-
let listing = await listFilesInRepository(projectRoot, repositoryRoot, nestedRoots)
|
|
483
|
-
if (!listing) {
|
|
484
|
-
const absoluteRoot = repositoryRoot
|
|
485
|
-
? resolve(projectRoot, ...repositoryRoot.split('/'))
|
|
486
|
-
: projectRoot
|
|
487
|
-
const nestedRelativeRoots = nestedRoots.map((root) =>
|
|
488
|
-
repositoryRoot ? root.slice(repositoryRoot.length + 1) : root
|
|
489
|
-
)
|
|
490
|
-
const fallback = await listFilesWithWalk(absoluteRoot, nestedRelativeRoots)
|
|
491
|
-
listing = {
|
|
492
|
-
files: fallback.files.map((path) => joinProtocolPath(repositoryRoot, path)),
|
|
493
|
-
truncated: fallback.truncated
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
for (const path of listing.files) {
|
|
497
|
-
files.add(path)
|
|
498
|
-
if (files.size >= SEARCH_INDEX_MAX_FILES) {
|
|
499
|
-
return { files: [...files], truncated: true }
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
truncated ||= listing.truncated
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
return { files: [...files], truncated }
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
async function getSearchIndex(root: string): Promise<SearchIndexCacheEntry> {
|
|
509
|
-
const key = fileSystemKey(root)
|
|
510
|
-
const now = Date.now()
|
|
511
|
-
const cached = searchIndexCache.get(key)
|
|
512
|
-
if (cached && cached.expiresAt > now) return cached
|
|
513
|
-
|
|
514
|
-
const active = pendingSearchIndexes.get(key)
|
|
515
|
-
if (active) return active
|
|
516
|
-
|
|
517
|
-
const load: Promise<SearchIndexCacheEntry> = Promise.resolve().then(async () => {
|
|
518
|
-
const repositoryRoots = await listL4WorkSessionGitRepositoryRoots(root)
|
|
519
|
-
const listing =
|
|
520
|
-
repositoryRoots.length > 0
|
|
521
|
-
? await listFilesWithRepositoryRoots(root, repositoryRoots)
|
|
522
|
-
: ((await listFilesWithContainingGit(root)) ?? (await listFilesWithWalk(root)))
|
|
523
|
-
const next: SearchIndexCacheEntry = {
|
|
524
|
-
...listing,
|
|
525
|
-
expiresAt: now + SEARCH_CACHE_TTL_MS
|
|
526
|
-
}
|
|
527
|
-
if (pendingSearchIndexes.get(key) === load) {
|
|
528
|
-
for (const [cacheKey, entry] of searchIndexCache) {
|
|
529
|
-
if (entry.expiresAt <= Date.now()) searchIndexCache.delete(cacheKey)
|
|
530
|
-
}
|
|
531
|
-
while (searchIndexCache.size >= SEARCH_CACHE_MAX_CWDS) {
|
|
532
|
-
const oldestKey = searchIndexCache.keys().next().value as string
|
|
533
|
-
searchIndexCache.delete(oldestKey)
|
|
534
|
-
}
|
|
535
|
-
searchIndexCache.set(key, next)
|
|
536
|
-
}
|
|
537
|
-
return next
|
|
538
|
-
})
|
|
539
|
-
pendingSearchIndexes.set(key, load)
|
|
540
|
-
try {
|
|
541
|
-
return await load
|
|
542
|
-
} finally {
|
|
543
|
-
if (pendingSearchIndexes.get(key) === load) pendingSearchIndexes.delete(key)
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function subsequenceGapScore(value: string, query: string): number | null {
|
|
548
|
-
let queryIndex = 0
|
|
549
|
-
let firstIndex = -1
|
|
550
|
-
let previousIndex = -1
|
|
551
|
-
let gaps = 0
|
|
552
|
-
|
|
553
|
-
for (
|
|
554
|
-
let valueIndex = 0;
|
|
555
|
-
valueIndex < value.length && queryIndex < query.length;
|
|
556
|
-
valueIndex += 1
|
|
557
|
-
) {
|
|
558
|
-
if (value[valueIndex] !== query[queryIndex]) continue
|
|
559
|
-
if (firstIndex < 0) firstIndex = valueIndex
|
|
560
|
-
if (previousIndex >= 0) gaps += valueIndex - previousIndex - 1
|
|
561
|
-
previousIndex = valueIndex
|
|
562
|
-
queryIndex += 1
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
return queryIndex === query.length ? firstIndex * 4 + gaps : null
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
function searchScore(path: string, query: string): number | null {
|
|
569
|
-
const normalizedPath = path.toLocaleLowerCase()
|
|
570
|
-
const name = basename(normalizedPath)
|
|
571
|
-
if (name === query) return 0
|
|
572
|
-
if (name.startsWith(query)) return 100 + name.length - query.length
|
|
573
|
-
const nameIndex = name.indexOf(query)
|
|
574
|
-
if (nameIndex >= 0) return 1_000 + nameIndex * 4 + name.length
|
|
575
|
-
const pathIndex = normalizedPath.indexOf(query)
|
|
576
|
-
if (pathIndex >= 0) return 10_000 + pathIndex * 4 + normalizedPath.length
|
|
577
|
-
const fuzzy = subsequenceGapScore(normalizedPath, query)
|
|
578
|
-
return fuzzy === null ? null : 100_000 + fuzzy + normalizedPath.length
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
export async function searchL4WorkSessionProjectFiles(
|
|
582
|
-
cwd: string,
|
|
583
|
-
query: string
|
|
584
|
-
): Promise<L4WorkSessionProjectFileSearch> {
|
|
585
|
-
const root = await resolveProjectRoot(cwd)
|
|
586
|
-
const index = await getSearchIndex(root)
|
|
587
|
-
const normalizedQuery = query.toLocaleLowerCase()
|
|
588
|
-
const matches = index.files
|
|
589
|
-
.flatMap((path) => {
|
|
590
|
-
const score = searchScore(path, normalizedQuery)
|
|
591
|
-
return score === null ? [] : [{ path, score }]
|
|
592
|
-
})
|
|
593
|
-
.sort((left, right) => left.score - right.score || left.path.localeCompare(right.path))
|
|
594
|
-
|
|
595
|
-
return {
|
|
596
|
-
matches: matches.slice(0, L4_PROJECT_FILE_SEARCH_MAX_RESULTS).map((match) => match.path),
|
|
597
|
-
truncated: index.truncated || matches.length > L4_PROJECT_FILE_SEARCH_MAX_RESULTS
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
277
|
export async function revealL4WorkSessionProjectFile(
|
|
602
278
|
cwd: string,
|
|
603
279
|
protocolPath: string,
|
|
@@ -81,12 +81,6 @@ export async function runL4PiPackageMaintenance(input: {
|
|
|
81
81
|
'failed',
|
|
82
82
|
'包维护超过120秒,已停止;结果未确认,请检查配置后手动处理',
|
|
83
83
|
null
|
|
84
|
-
),
|
|
85
|
-
cleanupError: (_output, error) =>
|
|
86
|
-
new L4PiPackageMaintenanceError(
|
|
87
|
-
'failed',
|
|
88
|
-
`维护进程树未确认释放:${error instanceof Error ? error.message : String(error)}`,
|
|
89
|
-
null
|
|
90
84
|
)
|
|
91
85
|
})
|
|
92
86
|
if (code === 0) return
|
|
@@ -138,9 +138,7 @@ export async function runL4PiPluginCapabilityProbe(
|
|
|
138
138
|
env: { PI_DESK_PLUGIN_CAPABILITY_AGENT_DIR: agentDir, PI_DESK_PLUGIN_CAPABILITY_CWD: cwd },
|
|
139
139
|
maxOutputBytes: L4_PI_PLUGIN_CAPABILITY_PROBE_MAX_OUTPUT_BYTES,
|
|
140
140
|
timeoutMs: L4_PI_PLUGIN_CAPABILITY_PROBE_TIMEOUT_MS,
|
|
141
|
-
timeoutError: () => new Error('插件能力盘点超时')
|
|
142
|
-
cleanupError: (output, error) =>
|
|
143
|
-
new Error(`${output}\n能力盘点进程树清理失败`, { cause: error })
|
|
141
|
+
timeoutError: () => new Error('插件能力盘点超时')
|
|
144
142
|
})
|
|
145
143
|
if (code !== 0) throw new Error(stderr.trim() || `插件能力盘点进程退出码 ${code ?? 'null'}`)
|
|
146
144
|
const marker = stdout.lastIndexOf(L4_PI_PLUGIN_CAPABILITY_PROBE_RESULT_PREFIX)
|
|
@@ -61,9 +61,7 @@ export async function runL4PiPluginProbe(
|
|
|
61
61
|
maxOutputBytes: L4_PI_PLUGIN_PROBE_MAX_OUTPUT_BYTES,
|
|
62
62
|
timeoutMs: L4_PI_PLUGIN_PROBE_TIMEOUT_MS,
|
|
63
63
|
signal,
|
|
64
|
-
timeoutError: () => new Error('插件预检超时')
|
|
65
|
-
cleanupError: (output, error) =>
|
|
66
|
-
new Error(`${output}\n插件预检进程树清理失败`, { cause: error })
|
|
64
|
+
timeoutError: () => new Error('插件预检超时')
|
|
67
65
|
})
|
|
68
66
|
if (code !== 0) throw new Error(stderr.trim() || `插件预检进程退出码 ${code ?? 'null'}`)
|
|
69
67
|
const marker = stdout.lastIndexOf(L4_PI_PLUGIN_PROBE_RESULT_PREFIX)
|
|
@@ -19,7 +19,6 @@ export function runL4PiRunnerProcess(input: {
|
|
|
19
19
|
timeoutMs: number
|
|
20
20
|
signal?: AbortSignal
|
|
21
21
|
timeoutError: () => Error
|
|
22
|
-
cleanupError: (stderr: string, error: unknown) => Error
|
|
23
22
|
}): Promise<L4PiRunnerProcessResult> {
|
|
24
23
|
return new Promise<L4PiRunnerProcessResult>((resolveRun, rejectRun) => {
|
|
25
24
|
if (input.signal?.aborted) {
|
|
@@ -54,24 +53,13 @@ export function runL4PiRunnerProcess(input: {
|
|
|
54
53
|
input.signal?.removeEventListener('abort', onAbort)
|
|
55
54
|
void terminateL4PiProcessTree(child).then(callback, (error: unknown) => {
|
|
56
55
|
const failure = error instanceof Error ? error : new Error(String(error))
|
|
57
|
-
const code = 'code' in failure ? String(failure.code) : failure.name
|
|
58
|
-
const killed = 'killed' in failure && failure.killed === true
|
|
59
|
-
const detail =
|
|
60
|
-
'stderr' in failure && typeof failure.stderr === 'string'
|
|
61
|
-
? failure.stderr.trim().slice(-2000)
|
|
62
|
-
: ''
|
|
63
56
|
console.error('[Pi Desk][Runner] 子进程清理未完成', {
|
|
64
57
|
pid: child.pid,
|
|
65
58
|
exitCode: child.exitCode,
|
|
66
|
-
|
|
67
|
-
killed,
|
|
68
|
-
detail
|
|
59
|
+
reason: failure.message
|
|
69
60
|
})
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{ cause: failure }
|
|
73
|
-
)
|
|
74
|
-
rejectRun(input.cleanupError(stderr.trim(), reason))
|
|
61
|
+
// 命令的退出、超时或取消结果已确定;清理诊断不能覆盖业务结果。
|
|
62
|
+
callback()
|
|
75
63
|
})
|
|
76
64
|
}
|
|
77
65
|
const onAbort = (): void => finish(() => rejectRun(new Error('操作已取消')))
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// CLI、服务端与隔离探针共用的原生 Node 实现,不依赖 Next 的 react-server 导出条件。
|
|
4
4
|
const { execFile } = require('node:child_process')
|
|
5
|
+
const { performance } = require('node:perf_hooks')
|
|
6
|
+
const { setTimeout: delay } = require('node:timers/promises')
|
|
5
7
|
const { promisify } = require('node:util')
|
|
6
8
|
const execFileAsync = promisify(execFile)
|
|
7
9
|
|
|
@@ -22,6 +24,8 @@ async function stopWindowsOrphans(pid) {
|
|
|
22
24
|
"$ProgressPreference = 'SilentlyContinue'",
|
|
23
25
|
"$ErrorActionPreference = 'Stop'",
|
|
24
26
|
`$rootPid = ${pid}`,
|
|
27
|
+
'$clock = [System.Diagnostics.Stopwatch]::StartNew()',
|
|
28
|
+
'[Console]::Out.WriteLine("powershell-ready: $($clock.ElapsedMilliseconds)ms")',
|
|
25
29
|
String.raw`Add-Type -TypeDefinition @'
|
|
26
30
|
using System;
|
|
27
31
|
using System.Collections.Generic;
|
|
@@ -90,7 +94,9 @@ public static class PiDeskProcessSnapshot
|
|
|
90
94
|
}
|
|
91
95
|
'@
|
|
92
96
|
`,
|
|
97
|
+
'[Console]::Out.WriteLine("snapshot-compiled: $($clock.ElapsedMilliseconds)ms")',
|
|
93
98
|
'$all = [PiDeskProcessSnapshot]::ReadParents()',
|
|
99
|
+
'[Console]::Out.WriteLine("snapshot-read: $($clock.ElapsedMilliseconds)ms")',
|
|
94
100
|
'$root = Get-Process -Id $rootPid -ErrorAction SilentlyContinue',
|
|
95
101
|
'if ($root -and -not $root.HasExited) { throw "Managed PID is still present; refusing orphan cleanup" }',
|
|
96
102
|
'$ids = [System.Collections.Generic.HashSet[int]]::new()',
|
|
@@ -109,6 +115,7 @@ public static class PiDeskProcessSnapshot
|
|
|
109
115
|
}
|
|
110
116
|
}
|
|
111
117
|
} while ($added)`,
|
|
118
|
+
'[Console]::Out.WriteLine("descendants-selected: $($clock.ElapsedMilliseconds)ms count=$($descendants.Count)")',
|
|
112
119
|
String.raw`for ($i = $descendants.Count - 1; $i -ge 0; $i--) {
|
|
113
120
|
$pidToStop = $descendants[$i]
|
|
114
121
|
if (-not $created.ContainsKey($pidToStop)) {
|
|
@@ -132,6 +139,7 @@ public static class PiDeskProcessSnapshot
|
|
|
132
139
|
}
|
|
133
140
|
}`
|
|
134
141
|
].join('; ')
|
|
142
|
+
const startedAt = performance.now()
|
|
135
143
|
try {
|
|
136
144
|
await execFileAsync(
|
|
137
145
|
'powershell.exe',
|
|
@@ -149,7 +157,7 @@ public static class PiDeskProcessSnapshot
|
|
|
149
157
|
)
|
|
150
158
|
} catch (error) {
|
|
151
159
|
throw new Error(
|
|
152
|
-
`清理 Windows 孤儿进程失败:rootPid=${pid},code=${error.code},signal=${error.signal ?? '无'},超时终止=${error.killed === true};${error.stderr?.trim() ?? ''}`,
|
|
160
|
+
`清理 Windows 孤儿进程失败:rootPid=${pid},code=${error.code},signal=${error.signal ?? '无'},超时终止=${error.killed === true},耗时=${Math.round(performance.now() - startedAt)}ms;${error.stderr?.trim().slice(-2000) ?? ''}\n${error.stdout?.trim().slice(-2000) ?? ''}`,
|
|
153
161
|
{ cause: error }
|
|
154
162
|
)
|
|
155
163
|
}
|
|
@@ -175,10 +183,28 @@ async function terminateManagedTree(child) {
|
|
|
175
183
|
}
|
|
176
184
|
|
|
177
185
|
// 托管服务与维护命令均以自己的进程组启动,不终止 CLI 所在进程组。
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
186
|
+
const deadline = performance.now() + 5_000
|
|
187
|
+
while (true) {
|
|
188
|
+
try {
|
|
189
|
+
process.kill(-child.pid, 'SIGKILL')
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (error.code === 'ESRCH') return
|
|
192
|
+
throw error
|
|
193
|
+
}
|
|
194
|
+
const { stdout } = await execFileAsync('ps', ['-eo', 'pgid=,stat='], {
|
|
195
|
+
timeout: 1_000,
|
|
196
|
+
maxBuffer: 1024 * 1024
|
|
197
|
+
})
|
|
198
|
+
const running = stdout.split('\n').some((line) => {
|
|
199
|
+
const [group, state] = line.trim().split(/\s+/)
|
|
200
|
+
// 僵尸已停止执行并释放文件和端口,只等待父进程回收退出状态。
|
|
201
|
+
return Number(group) === child.pid && state && !state.startsWith('Z')
|
|
202
|
+
})
|
|
203
|
+
if (!running) return
|
|
204
|
+
if (performance.now() >= deadline) {
|
|
205
|
+
throw new Error(`进程组在终止后仍未退出:pgid=${child.pid}`)
|
|
206
|
+
}
|
|
207
|
+
await delay(50)
|
|
182
208
|
}
|
|
183
209
|
}
|
|
184
210
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
RLKcK-RQF5FvEeDmS9uih
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
],
|
|
5
5
|
"devFiles": [],
|
|
6
6
|
"lowPriorityFiles": [
|
|
7
|
-
"static/
|
|
8
|
-
"static/
|
|
7
|
+
"static/RLKcK-RQF5FvEeDmS9uih/_buildManifest.js",
|
|
8
|
+
"static/RLKcK-RQF5FvEeDmS9uih/_ssgManifest.js"
|
|
9
9
|
],
|
|
10
10
|
"rootMainFiles": [
|
|
11
|
-
"static/chunks/webpack-
|
|
11
|
+
"static/chunks/webpack-d7248206b7fc7ba8.js",
|
|
12
12
|
"static/chunks/e388c842-1ca92f1ea0dfe8ee.js",
|
|
13
13
|
"static/chunks/64444-3e955f910569274d.js",
|
|
14
14
|
"static/chunks/main-app-6230945a5a062fce.js"
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"dynamicRoutes": {},
|
|
66
66
|
"notFoundRoutes": [],
|
|
67
67
|
"preview": {
|
|
68
|
-
"previewModeId": "
|
|
69
|
-
"previewModeSigningKey": "
|
|
70
|
-
"previewModeEncryptionKey": "
|
|
68
|
+
"previewModeId": "724e14edd9a9e87d1681837f5b2774b6",
|
|
69
|
+
"previewModeSigningKey": "f8047a56e43fd978b908fab8a310ae5a3a16442a0f73b160f9cc6bb676bbcac2",
|
|
70
|
+
"previewModeEncryptionKey": "7923a8b78086226553ae708e242b307cb0ab5c60303f824f230f133813e9a2f8"
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -2610,7 +2610,7 @@
|
|
|
2610
2610
|
"client/l2_biz/workbench/file-workspace/l2-work-session-file-layout.tsx -> ./l2-file-workspace-view": {
|
|
2611
2611
|
"id": 19991,
|
|
2612
2612
|
"files": [
|
|
2613
|
-
"static/chunks/31087-
|
|
2613
|
+
"static/chunks/31087-d8e5765d541d5c26.js",
|
|
2614
2614
|
"static/chunks/70669.07f3670bfb7d6dd2.js"
|
|
2615
2615
|
]
|
|
2616
2616
|
},
|
|
@@ -2,12 +2,12 @@ self.__SERVER_FILES_MANIFEST={
|
|
|
2
2
|
"version": 1,
|
|
3
3
|
"config": {
|
|
4
4
|
"env": {
|
|
5
|
-
"PI_DESK_APP_VERSION": "1.1.
|
|
6
|
-
"PI_DESK_VERSION_UPDATED_AT": "2026-10-
|
|
5
|
+
"PI_DESK_APP_VERSION": "1.1.20-dev",
|
|
6
|
+
"PI_DESK_VERSION_UPDATED_AT": "2026-10-25T17:12:44+08:00"
|
|
7
7
|
},
|
|
8
8
|
"webpack": null,
|
|
9
9
|
"typescript": {
|
|
10
|
-
"ignoreBuildErrors":
|
|
10
|
+
"ignoreBuildErrors": true
|
|
11
11
|
},
|
|
12
12
|
"typedRoutes": false,
|
|
13
13
|
"distDir": "temp/build/pi-desk/release/.next",
|
|
@@ -328,6 +328,7 @@ self.__SERVER_FILES_MANIFEST={
|
|
|
328
328
|
"@jetcrab/pi-desk-sdk"
|
|
329
329
|
],
|
|
330
330
|
"serverExternalPackages": [
|
|
331
|
+
"@vscode/ripgrep",
|
|
331
332
|
"@earendil-works/pi-coding-agent",
|
|
332
333
|
"@earendil-works/pi-agent-core",
|
|
333
334
|
"@earendil-works/pi-ai",
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"version": 1,
|
|
3
3
|
"config": {
|
|
4
4
|
"env": {
|
|
5
|
-
"PI_DESK_APP_VERSION": "1.1.
|
|
6
|
-
"PI_DESK_VERSION_UPDATED_AT": "2026-10-
|
|
5
|
+
"PI_DESK_APP_VERSION": "1.1.20-dev",
|
|
6
|
+
"PI_DESK_VERSION_UPDATED_AT": "2026-10-25T17:12:44+08:00"
|
|
7
7
|
},
|
|
8
8
|
"webpack": null,
|
|
9
9
|
"typescript": {
|
|
10
|
-
"ignoreBuildErrors":
|
|
10
|
+
"ignoreBuildErrors": true
|
|
11
11
|
},
|
|
12
12
|
"typedRoutes": false,
|
|
13
13
|
"distDir": "temp/build/pi-desk/release/.next",
|
|
@@ -328,6 +328,7 @@
|
|
|
328
328
|
"@jetcrab/pi-desk-sdk"
|
|
329
329
|
],
|
|
330
330
|
"serverExternalPackages": [
|
|
331
|
+
"@vscode/ripgrep",
|
|
331
332
|
"@earendil-works/pi-coding-agent",
|
|
332
333
|
"@earendil-works/pi-agent-core",
|
|
333
334
|
"@earendil-works/pi-ai",
|