@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.
Files changed (95) hide show
  1. package/docs/commands.md +4 -2
  2. package/docs/plugins.md +2 -0
  3. package/package.json +2 -2
  4. package/src/app/project-preview/page.tsx +13 -0
  5. package/src/client/l1_entry/pages/l1-project-preview-page.tsx +1 -5
  6. package/src/client/l2_biz/project-preview/hooks/l2-use-project-preview.ts +25 -0
  7. package/src/client/l2_biz/project-preview/l2-project-preview-storage.ts +7 -7
  8. package/src/client/l2_biz/project-preview/l2-project-preview.tsx +8 -5
  9. package/src/client/l2_biz/project-preview/runtime/l2-project-history-runtime.ts +14 -11
  10. package/src/client/l2_biz/project-preview/views/l2-project-branch-select.tsx +347 -0
  11. package/src/client/l2_biz/project-preview/views/l2-project-change-tree.tsx +28 -11
  12. package/src/client/l2_biz/project-preview/views/l2-project-history-list.tsx +35 -8
  13. package/src/client/l2_biz/project-preview/views/l2-project-history-view.tsx +46 -48
  14. package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-runtime.ts +2 -1
  15. package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-view.tsx +6 -2
  16. package/src/client/l3_modules/conversation/l3-conversation-viewport.tsx +18 -8
  17. package/src/client/l3_modules/project-files/l3-project-document-view.tsx +7 -4
  18. package/src/client/l3_modules/project-files/l3-project-file-runtime.ts +181 -89
  19. package/src/client/l3_modules/project-files/l3-project-file-tabs.tsx +80 -28
  20. package/src/common/l2_biz/project-preview/l2-project-history-messages.ts +4 -2
  21. package/src/common/l2_biz/project-preview/l2-project-preview-navigation.ts +14 -0
  22. package/src/server/l1_entry/api/l1-plugin-management-route.ts +0 -4
  23. package/src/server/l2_biz/plugin-management/l2-plugin-management.ts +4 -37
  24. package/src/server/l2_biz/plugin-management/l2-plugin-updates.ts +1 -4
  25. package/src/server/l2_biz/work-session/l2-work-session-chat-runtime.ts +4 -2
  26. package/src/server/l4_foundation/pi/l4-pi-chat-worker.ts +16 -1
  27. package/src/server/l4_foundation/pi/l4-pi-package-root-consistency.ts +3 -6
  28. package/src/server/l4_foundation/pi/l4-pi-package-root-gate.ts +1 -1
  29. package/src/server/l4_foundation/pi/l4-pi-plugin-owner-runtime.ts +4 -0
  30. package/src/server/l4_foundation/pi/l4-pi-plugin-sources.ts +5 -1
  31. package/src/server/l4_foundation/pi/l4-pi-session-initialization.ts +30 -34
  32. package/src/server/l4_foundation/pi/l4-pi-session-resources.ts +15 -14
  33. package/src/server/l4_foundation/pi/l4-pi-work-session-runtime.ts +1 -1
  34. package/src/server/l4_foundation/skills/runtime/l4-skill-catalog.ts +159 -166
  35. package/temp/build/pi-desk/release/.next/BUILD_ID +1 -1
  36. package/temp/build/pi-desk/release/.next/build-manifest.json +3 -3
  37. package/temp/build/pi-desk/release/.next/prerender-manifest.json +3 -3
  38. package/temp/build/pi-desk/release/.next/react-loadable-manifest.json +1 -1
  39. package/temp/build/pi-desk/release/.next/required-server-files.js +3 -3
  40. package/temp/build/pi-desk/release/.next/required-server-files.json +3 -3
  41. package/temp/build/pi-desk/release/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  42. package/temp/build/pi-desk/release/.next/server/app/_global-error.html +1 -1
  43. package/temp/build/pi-desk/release/.next/server/app/_global-error.rsc +1 -1
  44. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  45. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  46. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  47. package/temp/build/pi-desk/release/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  48. package/temp/build/pi-desk/release/.next/server/app/api/health/route.js +1 -1
  49. package/temp/build/pi-desk/release/.next/server/app/api/plugins/add/route.js +1 -1
  50. package/temp/build/pi-desk/release/.next/server/app/api/plugins/apply/route.js +1 -1
  51. package/temp/build/pi-desk/release/.next/server/app/api/plugins/batch/route.js +1 -1
  52. package/temp/build/pi-desk/release/.next/server/app/api/plugins/del/route.js +1 -1
  53. package/temp/build/pi-desk/release/.next/server/app/api/plugins/enabled-set/route.js +1 -1
  54. package/temp/build/pi-desk/release/.next/server/app/api/plugins/get/route.js +1 -1
  55. package/temp/build/pi-desk/release/.next/server/app/api/plugins/list/route.js +1 -1
  56. package/temp/build/pi-desk/release/.next/server/app/api/plugins/reload/route.js +1 -1
  57. package/temp/build/pi-desk/release/.next/server/app/api/plugins/update/route.js +1 -1
  58. package/temp/build/pi-desk/release/.next/server/app/login/page_client-reference-manifest.js +1 -1
  59. package/temp/build/pi-desk/release/.next/server/app/page.js +2 -2
  60. package/temp/build/pi-desk/release/.next/server/app/page_client-reference-manifest.js +1 -1
  61. package/temp/build/pi-desk/release/.next/server/app/project-preview/page.js +1 -1
  62. package/temp/build/pi-desk/release/.next/server/app/project-preview/page_client-reference-manifest.js +1 -1
  63. package/temp/build/pi-desk/release/.next/server/chunks/16438.js +1 -1
  64. package/temp/build/pi-desk/release/.next/server/chunks/18748.js +1 -0
  65. package/temp/build/pi-desk/release/.next/server/chunks/24557.js +1 -1
  66. package/temp/build/pi-desk/release/.next/server/chunks/25406.js +13 -14
  67. package/temp/build/pi-desk/release/.next/server/chunks/27344.js +15 -0
  68. package/temp/build/pi-desk/release/.next/server/chunks/35985.js +1 -1
  69. package/temp/build/pi-desk/release/.next/server/chunks/36361.js +2 -2
  70. package/temp/build/pi-desk/release/.next/server/chunks/50520.js +2 -2
  71. package/temp/build/pi-desk/release/.next/server/chunks/6917.js +1 -1
  72. package/temp/build/pi-desk/release/.next/server/chunks/80290.js +1 -1
  73. package/temp/build/pi-desk/release/.next/server/chunks/92386.js +15 -16
  74. package/temp/build/pi-desk/release/.next/server/instrumentation.js +1 -1
  75. package/temp/build/pi-desk/release/.next/server/middleware-build-manifest.js +1 -1
  76. package/temp/build/pi-desk/release/.next/server/middleware-react-loadable-manifest.js +1 -1
  77. package/temp/build/pi-desk/release/.next/server/pages/500.html +1 -1
  78. package/temp/build/pi-desk/release/.next/server/server-reference-manifest.json +1 -1
  79. package/temp/build/pi-desk/release/.next/static/chunks/15302-6e070e13fa65c863.js +1 -0
  80. package/temp/build/pi-desk/release/.next/static/chunks/19991.bcca96d396b83030.js +1 -0
  81. package/temp/build/pi-desk/release/.next/static/chunks/65232-ef4e5a4174f62faf.js +15 -0
  82. package/temp/build/pi-desk/release/.next/static/chunks/app/{page-54040b9fd54ee8b3.js → page-18d63fd3920dfae0.js} +3 -3
  83. package/temp/build/pi-desk/release/.next/static/chunks/app/project-preview/page-038b6c14e03ed878.js +1 -0
  84. package/temp/build/pi-desk/release/.next/static/chunks/{webpack-48eecbb66781c006.js → webpack-c0b805df46fcf8b3.js} +1 -1
  85. package/temp/build/pi-desk/release/.next/static/css/c43b04c244a64fb4.css +1 -0
  86. package/temp/build/pi-desk/release/.next/trace-build +1 -1
  87. package/temp/build/pi-desk/release/.next/server/chunks/20460.js +0 -15
  88. package/temp/build/pi-desk/release/.next/server/chunks/93049.js +0 -1
  89. package/temp/build/pi-desk/release/.next/static/chunks/15302-9c807afc6c60de0c.js +0 -1
  90. package/temp/build/pi-desk/release/.next/static/chunks/19991.8aa2e4b6df15fad6.js +0 -1
  91. package/temp/build/pi-desk/release/.next/static/chunks/9038-05fd2688407b39c1.js +0 -15
  92. package/temp/build/pi-desk/release/.next/static/chunks/app/project-preview/page-bc18765121950e65.js +0 -1
  93. package/temp/build/pi-desk/release/.next/static/css/10e063cfb3cda7bd.css +0 -1
  94. /package/temp/build/pi-desk/release/.next/static/{gAYTx4KJJR3vTtGYexj4J → 6a4eIEjmpW5l8Qlgtzzzg}/_buildManifest.js +0 -0
  95. /package/temp/build/pi-desk/release/.next/static/{gAYTx4KJJR3vTtGYexj4J → 6a4eIEjmpW5l8Qlgtzzzg}/_ssgManifest.js +0 -0
@@ -1,5 +1,19 @@
1
+ import type { L4Locale } from '@common/l4_foundation/locale/l4-locale'
2
+ import { L2_PROJECT_HISTORY_MESSAGES } from './l2-project-history-messages'
3
+
1
4
  export const L2_PROJECT_PREVIEW_PATH = '/project-preview'
2
5
 
3
6
  export function buildL2ProjectPreviewHref(cwd?: string | null): string {
4
7
  return cwd ? `${L2_PROJECT_PREVIEW_PATH}?cwd=${encodeURIComponent(cwd)}` : L2_PROJECT_PREVIEW_PATH
5
8
  }
9
+
10
+ export function buildL2ProjectPreviewTitle(
11
+ cwd: string | null | undefined,
12
+ locale: L4Locale
13
+ ): string {
14
+ const directory = cwd?.trim() || null
15
+ const name = directory?.replaceAll('\\', '/').split('/').filter(Boolean).at(-1) ?? directory
16
+ return [name, L2_PROJECT_HISTORY_MESSAGES[locale].previewTitle, 'Pi Desk']
17
+ .filter(Boolean)
18
+ .join(' · ')
19
+ }
@@ -15,7 +15,6 @@ import {
15
15
  L2PluginManagementInvalidPackageError,
16
16
  L2PluginManagementNotFoundError,
17
17
  L2PluginManagementOperationConflictError,
18
- L2PluginManagementPackageRootError,
19
18
  L2PluginManagementRestartPendingError,
20
19
  L2PluginManagementRestartUnavailableError
21
20
  } from '@server/l2_biz/plugin-management/l2-plugin-management'
@@ -43,9 +42,6 @@ function pluginOperationError(error: unknown, action: string): Response {
43
42
  ) {
44
43
  return createL4ApiErrorResponse(409, error.message)
45
44
  }
46
- if (error instanceof L2PluginManagementPackageRootError) {
47
- return createL4ApiErrorResponse(409, error.message, error.rootError.i18n)
48
- }
49
45
  if (error instanceof L2PluginReloadBlockedError) {
50
46
  return createL4ApiErrorResponse(409, error.message, {
51
47
  key: 'errors:pluginReloadBlocked',
@@ -136,13 +136,6 @@ export class L2PluginManagementInvalidPackageError extends Error {
136
136
  }
137
137
  }
138
138
 
139
- export class L2PluginManagementPackageRootError extends Error {
140
- constructor(readonly rootError: L4PiPackageRootConsistencyError) {
141
- super(rootError.message)
142
- this.name = 'L2PluginManagementPackageRootError'
143
- }
144
- }
145
-
146
139
  export class L2PluginManagementRestartPendingError extends Error {
147
140
  constructor() {
148
141
  super('已有插件操作需要重启完成')
@@ -845,12 +838,6 @@ export class L2PluginManagement {
845
838
  let updateTag: string | null = null
846
839
  let pluginName: string | null = null
847
840
  if (mutation !== 'enable' && mutation !== 'disable') {
848
- const packageRootError = await readL4PiPackageRootConsistencyError({
849
- agentDir: this.agentDir,
850
- configured,
851
- repairSource: mutation === 'add' && reinstall ? operationSource : undefined
852
- })
853
- if (packageRootError) throw new L2PluginManagementPackageRootError(packageRootError)
854
841
  if (mutation !== 'add' && !target) throw new L2PluginManagementNotFoundError(source)
855
842
  this.setOperation(operationSource, 'checking', '正在确认插件版本和下载源')
856
843
  const candidate = this.candidateSource(source, target?.installedPath)
@@ -899,29 +886,11 @@ export class L2PluginManagement {
899
886
  setL4PiPluginUpdateTag(operationSource, updateTag, this.agentDir)
900
887
  this.updates.clear(operationSource)
901
888
  }
902
- if (this.deferredMaintenance.size > 0) {
903
- this.deferredMaintenance.set(operationSource, request)
904
- waiting('等待重启后完成安装维护')
905
- return
906
- }
907
889
  }
908
890
  this.assertOpen()
909
891
  await runtime.withSourceIdle(
910
892
  operationSource,
911
893
  async () => {
912
- if (request) {
913
- const current = this.createPackageManager()
914
- this.throwSettingsErrors(current.settingsManager)
915
- const configured = current.packageManager
916
- .listConfiguredPackages()
917
- .filter((item) => item.scope === 'user')
918
- const rootError = await readL4PiPackageRootConsistencyError({
919
- agentDir: this.agentDir,
920
- configured,
921
- repairSource: mutation === 'add' && input.reinstall ? operationSource : undefined
922
- })
923
- if (rootError) throw new L2PluginManagementPackageRootError(rootError)
924
- }
925
894
  this.assertOpen()
926
895
  this.setOperation(
927
896
  operationSource,
@@ -957,7 +926,8 @@ export class L2PluginManagement {
957
926
  await runL4PiPackageMaintenance({
958
927
  maintenance: request!,
959
928
  agentDir: this.agentDir,
960
- cwd: this.packageManagerCwd
929
+ cwd: this.packageManagerCwd,
930
+ signal: this.lifetime.signal
961
931
  })
962
932
  if (mutation !== 'del') await this.verifyInstalledPackage(request!.source)
963
933
  })
@@ -1372,11 +1342,8 @@ export class L2PluginManagement {
1372
1342
  if (!force && this.capabilityCache) return this.capabilityCache
1373
1343
 
1374
1344
  try {
1375
- this.capabilityCache = await runL4PiPackageRootExclusive(() => {
1376
- this.assertOpen()
1377
- if (!force && this.capabilityCache) return Promise.resolve(this.capabilityCache)
1378
- return runL4PiPluginCapabilityProbe(this.cwd, this.agentDir)
1379
- })
1345
+ this.assertOpen()
1346
+ this.capabilityCache = await runL4PiPluginCapabilityProbe(this.cwd, this.agentDir)
1380
1347
  this.publish()
1381
1348
  console.info('[Pi Desk][PluginManagement] 插件能力盘点完成', {
1382
1349
  packageCount: this.capabilityCache.packages.length,
@@ -20,7 +20,6 @@ import {
20
20
  resolveL4PluginInstall
21
21
  } from '@server/l4_foundation/pi/l4-pi-plugin-registry'
22
22
  import type { L4PiPluginSource } from '@server/l4_foundation/pi/l4-pi-plugin-sources'
23
- import { runL4PiPackageRootExclusive } from '@server/l4_foundation/pi/l4-pi-package-root-gate'
24
23
 
25
24
  const UPDATE_CACHE_TTL_MS = 5 * 60 * 1000
26
25
  const UPDATE_CACHE_MAX_ENTRIES = 128
@@ -163,9 +162,7 @@ export class L2PluginUpdates {
163
162
  cwd: this.cwd,
164
163
  settingsManager
165
164
  })
166
- const updates = await runL4PiPackageRootExclusive(() =>
167
- manager.checkForAvailableUpdates()
168
- )
165
+ const updates = await manager.checkForAvailableUpdates()
169
166
  const value: L2PluginUpdateStatus = {
170
167
  ...empty,
171
168
  updateAvailable: updates.some((item) => item.scope === 'user')
@@ -359,17 +359,19 @@ export class L2WorkSessionChatRuntime {
359
359
 
360
360
  prepareSource(sourceInput: L2ChatSource): Promise<void> {
361
361
  const record = this.currentRecord(sourceInput)
362
- return this.enqueueCommand(record, () => this.syncRecordRuntime(record))
362
+ // 快照读取不能排在重载、原生命令或插件初始化等修改操作后面。
363
+ return this.syncRecordRuntime(record)
363
364
  }
364
365
 
365
366
  private async syncRecordRuntime(record: L2WorkSessionChatRecord): Promise<void> {
366
367
  const source = record.source
368
+ const version = record.sourceEventVersion
367
369
  const runtime = await getL4PiWorkSessionRuntime(source.sessionId).readChatRuntime()
368
370
  const nextRuntime = L2ChatRuntimeSchema.parse({
369
371
  ...runtime,
370
372
  presentationMode: record.state.runtime.presentationMode
371
373
  })
372
- if (!this.isCurrentSource(source)) return
374
+ if (!this.isCurrentSource(source) || record.sourceEventVersion !== version) return
373
375
  if (!sameRuntime(record.state.runtime, nextRuntime))
374
376
  this.applySourceEvent(source, { type: 'runtime_update', runtime: nextRuntime })
375
377
  }
@@ -769,7 +769,22 @@ export class L4PiChatWorker {
769
769
  return () => this.listeners.delete(listener)
770
770
  }
771
771
 
772
- async getRuntime(): Promise<L4PiChatRuntimeSnapshot> {
772
+ async getRuntime(waitForInitialization = true): Promise<L4PiChatRuntimeSnapshot> {
773
+ if (!waitForInitialization) {
774
+ if (this.disposed) throw new Error('Pi chat worker has been disposed')
775
+ // 订阅/重连先交付当前快照;初始化结果沿已有 runtime_changed 独立推送。
776
+ if (!this.session && !this.sessionLifecycle) {
777
+ void this.ensureSession().catch((cause: unknown) => {
778
+ if (this.disposed || this.session) return
779
+ this.initializationError = (cause instanceof Error ? cause.message : String(cause)).slice(
780
+ 0,
781
+ 32_000
782
+ )
783
+ this.emit({ type: 'runtime_changed', runtime: this.runtimeSnapshot() })
784
+ })
785
+ }
786
+ return this.runtimeSnapshot()
787
+ }
773
788
  const initialization = this.ensureSession()
774
789
  let release = (): void => undefined
775
790
  const interaction = new Promise<void>((resolve) => {
@@ -37,7 +37,6 @@ async function directoryExists(path: string): Promise<boolean> {
37
37
  export async function readL4PiPackageRootConsistencyError(input: {
38
38
  agentDir: string
39
39
  configured: readonly L4PiConfiguredPackageLocation[]
40
- repairSource?: string
41
40
  }): Promise<L4PiPackageRootConsistencyError | null> {
42
41
  const npmPackages = input.configured.flatMap((item) => {
43
42
  const name = npmPackageName(item.source)
@@ -63,14 +62,12 @@ export async function readL4PiPackageRootConsistencyError(input: {
63
62
  }
64
63
  }
65
64
 
66
- // 重装失败仍保留 Pi 声明;仅显式重装的目标可以缺失,安装根本身仍须可读。
67
- const repairName = input.repairSource ? npmPackageName(input.repairSource) : null
68
- const requiredPackages = npmPackages.filter((item) => item.name !== repairName)
69
- const expected = new Set(requiredPackages.map((item) => item.name))
65
+ // 只用于管理页诊断,不作为其他插件操作或聊天的前置条件。
66
+ const expected = new Set(npmPackages.map((item) => item.name))
70
67
  const actual = new Set(Object.keys(dependencies))
71
68
  const missing = [...expected].filter((name) => !actual.has(name))
72
69
  const missingDirectories: string[] = []
73
- for (const { name, item } of requiredPackages) {
70
+ for (const { name, item } of npmPackages) {
74
71
  if (!item.installedPath || !(await directoryExists(item.installedPath))) {
75
72
  missingDirectories.push(name)
76
73
  }
@@ -1,6 +1,6 @@
1
1
  import 'server-only'
2
2
 
3
- /** 只保护安装目录的读写;扩展工厂和生命周期不能持有此锁。 */
3
+ /** 只串行安装目录写入;只读发现、聊天与扩展生命周期不得等待此锁。 */
4
4
  export function runL4PiPackageRootExclusive<T>(operation: () => Promise<T>): Promise<T> {
5
5
  const previous = globalThis.__piDeskPackageRootTail ?? Promise.resolve()
6
6
  const result = previous.then(operation, operation)
@@ -411,8 +411,12 @@ export class L4PiPluginOwnerRuntime {
411
411
  }
412
412
  this.cancelRecovery(source)
413
413
  this.maintainingSources.add(source)
414
+ const deadline = performance.now() + 15_000
414
415
  try {
415
416
  while (this.owners.some((owner) => owner.source === source && owner.activeCalls.size > 0)) {
417
+ if (performance.now() >= deadline) {
418
+ throw new Error(`插件 ${source} 的在途调用未在15秒内结束,本次维护失败,可稍后手动重试`)
419
+ }
416
420
  onWaiting(`插件 ${source} 仍有调用正在执行`)
417
421
  await delay(250, undefined, { signal: lifetime })
418
422
  }
@@ -160,7 +160,11 @@ export async function discoverL4PiPluginSources(
160
160
 
161
161
  try {
162
162
  // 显式 skip,列清单不能触发缺失包安装、扩展导入或 AgentSession。
163
- const resolved = await manager.resolve(async () => 'skip')
163
+ const resolved = await manager.resolve(async (source) => {
164
+ const item = sources.get(source)
165
+ if (item) item.error ??= '插件包未安装或版本不匹配'
166
+ return 'skip'
167
+ })
164
168
  for (const item of sources.values()) item.hasPiResources = false
165
169
  for (const resource of [
166
170
  ...resolved.extensions,
@@ -32,7 +32,7 @@ import { L4PiUiRuntime } from './l4-pi-ui-runtime'
32
32
  const INITIALIZATION_TIMEOUT_MS = 60_000
33
33
  const CLEANUP_TIMEOUT_MS = 1_500
34
34
  const MAX_RETIRED_INITIALIZATIONS = 4
35
- const retiredInitializations = new Set<Promise<unknown>>()
35
+ const retiredInitializations = new WeakMap<L4PiSessionResources, Set<Promise<unknown>>>()
36
36
  const STALE_MESSAGE = 'Pi 会话实例已失效,不能继续访问当前会话'
37
37
 
38
38
  interface InitializationOptions {
@@ -57,6 +57,7 @@ export class L4PiSessionInitialization {
57
57
  readonly ui: L4PiUiRuntime
58
58
  private readonly manager: SessionManager
59
59
  private readonly sessionId: string
60
+ private readonly retired: Set<Promise<unknown>>
60
61
  private active = true
61
62
  private accessRevoked = false
62
63
  private session: AgentSession | null = null
@@ -68,6 +69,9 @@ export class L4PiSessionInitialization {
68
69
 
69
70
  constructor(private readonly options: InitializationOptions) {
70
71
  this.sessionId = options.sessionManager.getSessionId()
72
+ // 只限制同一工作会话的重复尝试,不能因其他会话的插件清理挂起而拒绝加载。
73
+ this.retired = retiredInitializations.get(options.resources) ?? new Set()
74
+ retiredInitializations.set(options.resources, this.retired)
71
75
  this.ui = options.ui ?? new L4PiUiRuntime()
72
76
  this.capabilities = new L4PiCapabilityRuntime(options.readCapabilities)
73
77
  this.plugins = new L4PiSessionPluginRuntime({
@@ -116,29 +120,20 @@ export class L4PiSessionInitialization {
116
120
  async reload(): Promise<void> {
117
121
  this.assertActive()
118
122
  const session = await this.start()
119
- let failure: Error | null = null
120
- this.onLifecycleError = (error) => {
121
- failure ??= error
122
- }
123
123
  this.setStage('重载 Pi 配置')
124
- try {
125
- await session.reload({
126
- beforeSessionStart: async () => {
127
- this.assertActive()
128
- this.validateResources(
129
- session.resourceLoader,
130
- session.settingsManager.drainErrors().map(({ error }) => ({
131
- type: 'error',
132
- message: error.message
133
- }))
134
- )
135
- }
136
- })
137
- this.assertActive()
138
- if (failure) throw failure
139
- } finally {
140
- this.onLifecycleError = null
141
- }
124
+ await session.reload({
125
+ beforeSessionStart: async () => {
126
+ this.assertActive()
127
+ this.validateResources(
128
+ session.resourceLoader,
129
+ session.settingsManager.drainErrors().map(({ error }) => ({
130
+ type: 'error',
131
+ message: error.message
132
+ }))
133
+ )
134
+ }
135
+ })
136
+ this.assertActive()
142
137
  }
143
138
 
144
139
  setStage(stage: string): void {
@@ -154,11 +149,8 @@ export class L4PiSessionInitialization {
154
149
  private async initialize(refreshResources: boolean): Promise<AgentSession> {
155
150
  const startedAt = performance.now()
156
151
  this.assertActive()
157
- if (
158
- this.options.mode === 'normal' &&
159
- retiredInitializations.size >= MAX_RETIRED_INITIALIZATIONS
160
- ) {
161
- throw new Error('尚未结束的插件初始化或关闭任务过多,请重启服务后再加载插件')
152
+ if (this.options.mode === 'normal' && this.retired.size >= MAX_RETIRED_INITIALIZATIONS) {
153
+ throw new Error('当前会话尚未结束的插件初始化或关闭任务过多,可先使用其他会话')
162
154
  }
163
155
  const failure = new Promise<never>((_, reject) => {
164
156
  this.onLifecycleError = reject
@@ -244,7 +236,7 @@ export class L4PiSessionInitialization {
244
236
  )
245
237
  : Promise.resolve()
246
238
  const cleanup = Promise.allSettled([this.execution, shutdown])
247
- retiredInitializations.add(cleanup)
239
+ this.retired.add(cleanup)
248
240
  const timer = setTimeout(() => {
249
241
  console.warn('[Pi Desk][PiSession] 旧实例仍在后台清理,不阻塞基础聊天', {
250
242
  sessionId: this.sessionId,
@@ -255,7 +247,7 @@ export class L4PiSessionInitialization {
255
247
  timer.unref()
256
248
  void cleanup.then(() => {
257
249
  clearTimeout(timer)
258
- retiredInitializations.delete(cleanup)
250
+ this.retired.delete(cleanup)
259
251
  })
260
252
  }
261
253
 
@@ -271,9 +263,13 @@ export class L4PiSessionInitialization {
271
263
  resourceLoader: ResourceLoader,
272
264
  diagnostics: AgentSessionServices['diagnostics']
273
265
  ): void {
274
- const failures = resourceLoader
275
- .getExtensions()
276
- .errors.map(({ path, error }) => `${error}\n插件入口:${path}`)
266
+ // Pi 已将失败入口排除;保留局部诊断,不把其余可用扩展一起作废。
267
+ for (const { path, error } of resourceLoader.getExtensions().errors) {
268
+ const message = `${error}\n插件入口:${path}`
269
+ reportL4PiPluginNativeDiagnostic(this.options.cwd, message)
270
+ console.warn('[Pi Desk][PiSession] 已跳过失败扩展', { cwd: this.options.cwd, message })
271
+ }
272
+ const failures: string[] = []
277
273
  for (const diagnostic of diagnostics) {
278
274
  if (diagnostic.type === 'error') failures.push(diagnostic.message)
279
275
  else console.info('[Pi Desk][PiSession] 初始化诊断', { cwd: this.options.cwd, ...diagnostic })
@@ -369,7 +365,7 @@ export class L4PiSessionInitialization {
369
365
  sessionId: this.sessionId,
370
366
  message: diagnostic
371
367
  })
372
- this.onLifecycleError?.(new Error(diagnostic))
368
+ reportL4PiPluginNativeDiagnostic(this.options.cwd, diagnostic)
373
369
  if (error.event === 'session_tree' || error.event === 'session_before_tree') {
374
370
  this.session?.extensionRunner
375
371
  .getUIContext()
@@ -16,7 +16,7 @@ import {
16
16
  type SourceInfo
17
17
  } from '@earendil-works/pi-coding-agent'
18
18
  import { clearL4PiPluginCodeCache, loadL4PiDeskPluginEntry } from './l4-pi-plugin-module-loader'
19
- import { runL4PiPackageRootExclusive } from './l4-pi-package-root-gate'
19
+ import { reportL4PiPluginNativeDiagnostic } from './l4-pi-plugin-native-diagnostic'
20
20
  import { createL4PiPluginDisabledFilter } from './l4-pi-plugin-disabled'
21
21
 
22
22
  type LoaderOptions = ConstructorParameters<typeof DefaultResourceLoader>[0]
@@ -93,15 +93,8 @@ export class L4PiSessionResources {
93
93
  onProgress?.('等待已选择的插件资源准备完成')
94
94
  return this.selection
95
95
  }
96
- onProgress?.(`等待插件资源队列\n工作目录:${cwd}`)
97
- const queuedAt = performance.now()
98
- this.selection = runL4PiPackageRootExclusive(() => {
99
- const durationMs = Math.round(performance.now() - queuedAt)
100
- if (durationMs >= 1000) {
101
- console.info('[Pi Desk][PluginResources] 插件资源队列等待结束', { cwd, durationMs })
102
- }
103
- return this.discover(cwd, agentDir, settings, onProgress)
104
- })
96
+ // 维护允许暂时缺包;只读发现不能排在 npm 网络操作后面。
97
+ this.selection = this.discover(cwd, agentDir, settings, onProgress)
105
98
  return this.selection
106
99
  }
107
100
 
@@ -122,7 +115,10 @@ export class L4PiSessionResources {
122
115
  })
123
116
  const paths = await runLoadStage('发现已配置的插件与资源', onProgress, () =>
124
117
  manager.resolve(async (source) => {
125
- throw new Error(`插件包未安装:${source}`)
118
+ const message = `插件包未安装或版本不匹配,已跳过:${source}`
119
+ reportL4PiPluginNativeDiagnostic(cwd, message)
120
+ console.warn('[Pi Desk][PluginResources]', { cwd, message })
121
+ return 'skip'
126
122
  })
127
123
  )
128
124
  const disabled = await createL4PiPluginDisabledFilter(agentDir, manager, settings)
@@ -307,25 +303,30 @@ export class L4PiSessionResources {
307
303
  }
308
304
  })
309
305
  const runtime = resourceLoader.getExtensions().runtime
306
+ const reportProviderError = (extensionPath: string, cause: unknown): void => {
307
+ const message = `${extensionPath}: ${String(cause)}`
308
+ reportL4PiPluginNativeDiagnostic(cwd, message)
309
+ diagnostics.push({ type: 'warning', message })
310
+ }
310
311
  for (const { name, config, extensionPath } of runtime.pendingProviderRegistrations) {
311
312
  try {
312
313
  modelRuntime.registerProvider(name, config)
313
314
  } catch (cause) {
314
- diagnostics.push({ type: 'error', message: `${extensionPath}: ${String(cause)}` })
315
+ reportProviderError(extensionPath, cause)
315
316
  }
316
317
  }
317
318
  for (const { provider, extensionPath } of runtime.pendingNativeProviderRegistrations) {
318
319
  try {
319
320
  modelRuntime.registerNativeProvider(provider)
320
321
  } catch (cause) {
321
- diagnostics.push({ type: 'error', message: `${extensionPath}: ${String(cause)}` })
322
+ reportProviderError(extensionPath, cause)
322
323
  }
323
324
  }
324
325
  for (const { definition, extensionPath } of runtime.pendingVirtualModelRegistrations) {
325
326
  try {
326
327
  modelRuntime.registerVirtualModel(definition)
327
328
  } catch (cause) {
328
- diagnostics.push({ type: 'error', message: `${extensionPath}: ${String(cause)}` })
329
+ reportProviderError(extensionPath, cause)
329
330
  }
330
331
  }
331
332
  runtime.pendingProviderRegistrations = []
@@ -215,7 +215,7 @@ export class L4PiWorkSessionRuntime {
215
215
 
216
216
  async readChatRuntime(): Promise<L4PiChatRuntimeSnapshot> {
217
217
  this.assertActive()
218
- return this.chatWorker.getRuntime()
218
+ return this.chatWorker.getRuntime(false)
219
219
  }
220
220
 
221
221
  async readModelContext(): Promise<L4PiModelContext> {