@jetcrab/pi-desk 1.1.33 → 1.1.38-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.
Files changed (67) hide show
  1. package/README.md +10 -10
  2. package/README.zh-CN.md +10 -10
  3. package/docs/commands.md +13 -2
  4. package/docs/plugins.md +10 -0
  5. package/package.json +3 -3
  6. package/src/client/l2_biz/pi-settings/l2-pi-settings-locale.ts +3 -3
  7. package/src/client/l2_biz/plugin-host/l2-plugin-host-runtime.ts +5 -6
  8. package/src/client/l2_biz/settings/l2-region-settings.tsx +30 -9
  9. package/src/client/l3_modules/region/l3-region-biz.ts +2 -2
  10. package/src/client/l4_foundation/plugin-host/l4-plugin-host-runtime.ts +20 -2
  11. package/src/common/l2_biz/plugin/l2-plugin-management-messages.ts +29 -17
  12. package/src/common/l2_biz/project-preview/l2-project-history-messages.ts +9 -7
  13. package/src/common/l2_biz/settings/l2-settings-messages.ts +82 -69
  14. package/src/common/l2_biz/terminal/l2-terminal-locale-messages.ts +1 -1
  15. package/src/common/l2_biz/workbench/l2-workbench-locale-messages.ts +48 -38
  16. package/src/common/l2_biz/workbench/l2-workbench-picker-messages.ts +12 -8
  17. package/src/common/l3_modules/conversation/l3-conversation-locale-messages.ts +24 -20
  18. package/src/common/l3_modules/project-files/l3-project-files-locale-messages.ts +29 -17
  19. package/src/common/l4_foundation/locale/l4-host-error-messages.ts +5 -5
  20. package/src/server/l2_biz/plugin-management/l2-plugin-commands.ts +8 -7
  21. package/src/server/l2_biz/plugin-management/l2-plugin-management.ts +51 -2
  22. package/src/server/l4_foundation/pi/l4-pi-global-plugin-runtime.ts +9 -1
  23. package/src/server/l4_foundation/pi/l4-pi-local-extension-maintenance.ts +118 -0
  24. package/src/server/l4_foundation/pi/l4-pi-plugin-owner-runtime.ts +4 -1
  25. package/src/server/l4_foundation/pi/l4-pi-plugin-source-changes.ts +4 -0
  26. package/src/server/l4_foundation/pidesk/l4-pidesk-command.ts +1 -1
  27. package/temp/build/pi-desk/release/.next/BUILD_ID +1 -1
  28. package/temp/build/pi-desk/release/.next/build-manifest.json +2 -2
  29. package/temp/build/pi-desk/release/.next/prerender-manifest.json +3 -3
  30. package/temp/build/pi-desk/release/.next/required-server-files.js +3 -3
  31. package/temp/build/pi-desk/release/.next/required-server-files.json +3 -3
  32. package/temp/build/pi-desk/release/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  33. package/temp/build/pi-desk/release/.next/server/app/_global-error.html +1 -1
  34. package/temp/build/pi-desk/release/.next/server/app/_global-error.rsc +1 -1
  35. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  36. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  37. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  38. package/temp/build/pi-desk/release/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  39. package/temp/build/pi-desk/release/.next/server/app/api/health/route.js +1 -1
  40. package/temp/build/pi-desk/release/.next/server/app/login/page_client-reference-manifest.js +1 -1
  41. package/temp/build/pi-desk/release/.next/server/app/page.js +3 -3
  42. package/temp/build/pi-desk/release/.next/server/app/page_client-reference-manifest.js +1 -1
  43. package/temp/build/pi-desk/release/.next/server/app/project-preview/page_client-reference-manifest.js +1 -1
  44. package/temp/build/pi-desk/release/.next/server/chunks/16438.js +1 -1
  45. package/temp/build/pi-desk/release/.next/server/chunks/25406.js +2 -2
  46. package/temp/build/pi-desk/release/.next/server/chunks/35985.js +2 -2
  47. package/temp/build/pi-desk/release/.next/server/chunks/36361.js +3 -3
  48. package/temp/build/pi-desk/release/.next/server/chunks/47757.js +1 -1
  49. package/temp/build/pi-desk/release/.next/server/chunks/50520.js +2 -2
  50. package/temp/build/pi-desk/release/.next/server/chunks/84231.js +3 -3
  51. package/temp/build/pi-desk/release/.next/server/chunks/{18748.js → 88353.js} +1 -1
  52. package/temp/build/pi-desk/release/.next/server/chunks/92386.js +4 -4
  53. package/temp/build/pi-desk/release/.next/server/instrumentation.js +1 -1
  54. package/temp/build/pi-desk/release/.next/server/middleware-build-manifest.js +1 -1
  55. package/temp/build/pi-desk/release/.next/server/pages/500.html +1 -1
  56. package/temp/build/pi-desk/release/.next/server/server-reference-manifest.json +1 -1
  57. package/temp/build/pi-desk/release/.next/static/chunks/11937-c21edad81915b999.js +1 -0
  58. package/temp/build/pi-desk/release/.next/static/chunks/15302-e9fd56a464b5267b.js +1 -0
  59. package/temp/build/pi-desk/release/.next/static/chunks/app/layout-1ec03fc5766568f6.js +1 -0
  60. package/temp/build/pi-desk/release/.next/static/chunks/app/page-402842ab5d6afb08.js +158 -0
  61. package/temp/build/pi-desk/release/.next/trace-build +1 -1
  62. package/temp/build/pi-desk/release/.next/static/chunks/11937-d5d4d2199356c3ed.js +0 -1
  63. package/temp/build/pi-desk/release/.next/static/chunks/15302-6e070e13fa65c863.js +0 -1
  64. package/temp/build/pi-desk/release/.next/static/chunks/app/layout-fdea504082053156.js +0 -1
  65. package/temp/build/pi-desk/release/.next/static/chunks/app/page-eb5c2229f59dda1a.js +0 -158
  66. /package/temp/build/pi-desk/release/.next/static/{IDVi-aOj0xwp4DoZljk5f → ljXlpZIQpD6BvWyD8Fmc3}/_buildManifest.js +0 -0
  67. /package/temp/build/pi-desk/release/.next/static/{IDVi-aOj0xwp4DoZljk5f → ljXlpZIQpD6BvWyD8Fmc3}/_ssgManifest.js +0 -0
@@ -7,7 +7,7 @@ export const L3_CONVERSATION_LOCALE_MESSAGES = {
7
7
  unknownModel: 'Unknown model',
8
8
  input: 'Input',
9
9
  output: 'Output',
10
- cached: 'Cache',
10
+ cached: 'Cached input',
11
11
  cost: 'Cost',
12
12
  error: 'Failed',
13
13
  aborted: 'Aborted',
@@ -17,7 +17,8 @@ export const L3_CONVERSATION_LOCALE_MESSAGES = {
17
17
  nestedCallsIncomplete: 'Nested calls · Incomplete record',
18
18
  nestedStatus: { ok: 'Completed', error: 'Failed', unfinished: 'Unfinished' },
19
19
  nestedArguments: 'Arguments',
20
- nestedArgumentsBytes: 'Arguments: {{count}} bytes',
20
+ nestedArgumentsBytes_one: 'Arguments: {{count}} byte',
21
+ nestedArgumentsBytes_other: 'Arguments: {{count}} bytes',
21
22
  image: 'Markdown image',
22
23
  imageUnavailable: 'Image unavailable',
23
24
  previewImage: 'Preview image: {{name}}',
@@ -36,11 +37,12 @@ export const L3_CONVERSATION_LOCALE_MESSAGES = {
36
37
  expand: 'Show full message',
37
38
  collapse: 'Collapse',
38
39
  thinking: 'Thinking…',
39
- thinkingLoading: 'Loading thoughts…',
40
- thinkingUnavailable: 'Thoughts unavailable',
41
- thinkingProcess: 'Reasoning',
42
- thinkingTruncated: 'Showing the last {{count}} characters',
43
- copyThinking: 'Copy full reasoning',
40
+ thinkingLoading: 'Loading thinking content…',
41
+ thinkingUnavailable: 'Thinking content unavailable',
42
+ thinkingProcess: 'Thinking',
43
+ thinkingTruncated_one: 'Showing the last {{count}} character',
44
+ thinkingTruncated_other: 'Showing the last {{count}} characters',
45
+ copyThinking: 'Copy thinking content',
44
46
  streamInterrupted: 'The response stream ended before the model completed its answer.',
45
47
  requestTimedOut: 'The model did not complete its response in time.',
46
48
  modelFailed: 'The model request failed without a detailed error.',
@@ -60,7 +62,7 @@ export const L3_CONVERSATION_LOCALE_MESSAGES = {
60
62
  pideskListPlugins: 'View plugins',
61
63
  pideskShowPlugin: 'View plugin',
62
64
  pideskInstallPlugin: 'Install plugin',
63
- pideskRemovePlugin: 'Remove plugin',
65
+ pideskRemovePlugin: 'Uninstall plugin',
64
66
  pidesk: {
65
67
  command: 'Command',
66
68
  output: 'Output',
@@ -69,8 +71,8 @@ export const L3_CONVERSATION_LOCALE_MESSAGES = {
69
71
  empty: 'No output',
70
72
  noPlugins: 'No plugins',
71
73
  unknownVersion: 'Version unknown',
72
- updateChannel: 'Following {{tag}}',
73
- targetVersion: 'Available version {{version}}',
74
+ updateChannel: 'Update channel: {{tag}}',
75
+ targetVersion: 'Target version: {{version}}',
74
76
  restartRequired: 'Pi Desk needs to be restarted',
75
77
  pluginDetail: 'Plugin details',
76
78
  info: {
@@ -90,7 +92,7 @@ export const L3_CONVERSATION_LOCALE_MESSAGES = {
90
92
  disabled: 'Disabled',
91
93
  failed: 'Load failed'
92
94
  },
93
- action: { add: 'Install', update: 'Update', del: 'Remove', apply: 'Load changes' },
95
+ action: { add: 'Install', update: 'Update', del: 'Uninstall', apply: 'Load changes' },
94
96
  phase: {
95
97
  queued: 'Queued',
96
98
  checking: 'Checking',
@@ -100,7 +102,7 @@ export const L3_CONVERSATION_LOCALE_MESSAGES = {
100
102
  },
101
103
  errorPhase: {
102
104
  package: 'Package',
103
- entry: 'Entry',
105
+ entry: 'Entry point',
104
106
  setup: 'Initialization',
105
107
  resources: 'Resources',
106
108
  resolve: 'Resolution',
@@ -121,12 +123,12 @@ export const L3_CONVERSATION_LOCALE_MESSAGES = {
121
123
  writeContent: 'Written file content',
122
124
  writeResult: 'Write tool result',
123
125
  emptyMessage: 'Empty message',
124
- compactionSummary: 'Context summary',
125
- compactionSummaryContent: 'Context summary content',
126
- compactionRunning: 'Summarizing context…',
127
- compactionComplete: 'Context summary complete',
128
- compactionLoading: 'Loading context summary…',
129
- compactionUnavailable: 'Context summary unavailable',
126
+ compactionSummary: 'Compaction summary',
127
+ compactionSummaryContent: 'Compaction summary content',
128
+ compactionRunning: 'Compacting context…',
129
+ compactionComplete: 'Context compacted',
130
+ compactionLoading: 'Loading compaction summary…',
131
+ compactionUnavailable: 'Compaction summary unavailable',
130
132
  processGroup: 'Activity',
131
133
  toolCalls_one: '{{count}} tool call',
132
134
  toolCalls_other: '{{count}} tool calls',
@@ -165,7 +167,8 @@ export const L3_CONVERSATION_LOCALE_MESSAGES = {
165
167
  nestedCallsIncomplete: '嵌套调用 · 记录未完整',
166
168
  nestedStatus: { ok: '完成', error: '失败', unfinished: '未完成' },
167
169
  nestedArguments: '调用参数',
168
- nestedArgumentsBytes: '参数 {{count}} 字节',
170
+ nestedArgumentsBytes_one: '参数 {{count}} 字节',
171
+ nestedArgumentsBytes_other: '参数 {{count}} 字节',
169
172
  image: 'Markdown 图片',
170
173
  imageUnavailable: '图片暂不可用',
171
174
  previewImage: '预览图片:{{name}}',
@@ -187,7 +190,8 @@ export const L3_CONVERSATION_LOCALE_MESSAGES = {
187
190
  thinkingLoading: '正在加载思考内容…',
188
191
  thinkingUnavailable: '思考内容不可用',
189
192
  thinkingProcess: '思考过程',
190
- thinkingTruncated: '仅显示最近 {{count}} 个字符',
193
+ thinkingTruncated_one: '仅显示最近 {{count}} 个字符',
194
+ thinkingTruncated_other: '仅显示最近 {{count}} 个字符',
191
195
  copyThinking: '复制完整思考',
192
196
  streamInterrupted: '模型响应流意外中断:连接在回答完成前被关闭。',
193
197
  requestTimedOut: '模型请求超时:在限定时间内未收到完整响应。',
@@ -33,18 +33,19 @@ export const L3_PROJECT_FILES_LOCALE_MESSAGES = {
33
33
  searchStart: 'Search this project',
34
34
  searchTryOptions: 'Try another query or adjust the search options.',
35
35
  searchFailed: 'Could not search the project. Please try again.',
36
- searchCounts: '{{files}} files · {{lines}} matching lines',
36
+ searchCounts: 'Files: {{files}} · Matching lines: {{lines}}',
37
37
  searchShownCounts: 'Shown: {{counts}}',
38
38
  searchFileHeading: 'Filenames',
39
39
  searchContentHeading: 'Content',
40
- searchContentCounts: '{{lines}} matching lines · {{files}} files',
40
+ searchContentCounts: 'Matching lines: {{lines}} · Files: {{files}}',
41
41
  searchProjectRoot: 'Project root',
42
- searchExpandFiles: 'Show {{count}} more files',
42
+ searchExpandFiles_one: 'Show {{count}} more file',
43
+ searchExpandFiles_other: 'Show {{count}} more files',
43
44
  searchOpenLine: 'Open {{path}}, line {{line}}',
44
45
  searchLineUnavailable: 'The file content has changed and the matching line no longer exists.',
45
46
  searchTextLimit: 'Content search: previewable UTF-8 text up to 5 MiB.',
46
47
  closeSearch: 'Exit search',
47
- layout: 'File organization',
48
+ layout: 'Group files by',
48
49
  directory: 'Folders',
49
50
  repository: 'Repositories',
50
51
  changesOnly: 'Changes only',
@@ -72,14 +73,19 @@ export const L3_PROJECT_FILES_LOCALE_MESSAGES = {
72
73
  copyContentFailed: 'Could not copy the file. Please copy it manually.',
73
74
  fileActions: 'File actions',
74
75
  headToWorking: 'HEAD ↔ working tree',
75
- gitChangesCount: 'Against HEAD · {{count}} changes',
76
- gitChangesCompact: '{{count}} changes',
76
+ gitChangesCount_one: 'Against HEAD · {{count}} change',
77
+ gitChangesCount_other: 'Against HEAD · {{count}} changes',
78
+ gitChangesCompact_one: '{{count}} change',
79
+ gitChangesCompact_other: '{{count}} changes',
77
80
  gitMarkersLoading: 'Comparing with HEAD…',
78
81
  gitMarkersFailed: 'Could not read changes · Reload to retry',
79
82
  gitMarkersUnavailable: 'Line change markers unavailable',
80
- gitAddedLines: '{{count}} lines added',
81
- gitModifiedLines: '{{count}} lines modified',
82
- gitDeletedLines: '{{count}} lines deleted',
83
+ gitAddedLines_one: '{{count}} line added',
84
+ gitAddedLines_other: '{{count}} lines added',
85
+ gitModifiedLines_one: '{{count}} line modified',
86
+ gitModifiedLines_other: '{{count}} lines modified',
87
+ gitDeletedLines_one: '{{count}} line deleted',
88
+ gitDeletedLines_other: '{{count}} lines deleted',
83
89
  noDiffChanges: ' · No content differences',
84
90
  previousChange: 'Previous change',
85
91
  nextChange: 'Next change',
@@ -99,8 +105,8 @@ export const L3_PROJECT_FILES_LOCALE_MESSAGES = {
99
105
  goToLine: 'Go to line',
100
106
  copySelectionOrAll: 'Copy selected or all content',
101
107
  reloadCurrentFile: 'Reload this file',
102
- binaryDiffUnavailable: 'Binary files cannot be diffed online.',
103
- largeDiffUnavailable: 'Files over 5 MB cannot be diffed online.',
108
+ binaryDiffUnavailable: 'Diff previews are not available for binary files.',
109
+ largeDiffUnavailable: 'Diff previews are not available for files over 5 MB.',
104
110
  readAgain: 'Reload',
105
111
  noOpenFile: 'No file open',
106
112
  imageViewer: '{{name}} image viewer',
@@ -162,7 +168,8 @@ export const L3_PROJECT_FILES_LOCALE_MESSAGES = {
162
168
  searchContentHeading: '内容',
163
169
  searchContentCounts: '{{lines}} 个命中行 · {{files}} 个文件',
164
170
  searchProjectRoot: '项目根目录',
165
- searchExpandFiles: '展开其余 {{count}} 个文件',
171
+ searchExpandFiles_one: '展开其余 {{count}} 个文件',
172
+ searchExpandFiles_other: '展开其余 {{count}} 个文件',
166
173
  searchOpenLine: '打开 {{path}} 第 {{line}} 行',
167
174
  searchLineUnavailable: '文件内容已变化,搜索命中行已不存在',
168
175
  searchTextLimit: '正文搜索:可预览的 UTF-8 文本,不超过 5 MiB。',
@@ -195,14 +202,19 @@ export const L3_PROJECT_FILES_LOCALE_MESSAGES = {
195
202
  copyContentFailed: '复制文件内容失败,请手动复制。',
196
203
  fileActions: '文件操作',
197
204
  headToWorking: 'HEAD ↔ 工作区',
198
- gitChangesCount: '相对 HEAD · {{count}} 处变更',
199
- gitChangesCompact: '{{count}} 处变更',
205
+ gitChangesCount_one: '相对 HEAD · {{count}} 处变更',
206
+ gitChangesCount_other: '相对 HEAD · {{count}} 处变更',
207
+ gitChangesCompact_one: '{{count}} 处变更',
208
+ gitChangesCompact_other: '{{count}} 处变更',
200
209
  gitMarkersLoading: '正在与 HEAD 比较…',
201
210
  gitMarkersFailed: '变更读取失败 · 刷新重试',
202
211
  gitMarkersUnavailable: '此文件无法显示行变更',
203
- gitAddedLines: '新增 {{count}} 行',
204
- gitModifiedLines: '修改 {{count}} 行',
205
- gitDeletedLines: '删除 {{count}} 行',
212
+ gitAddedLines_one: '新增 {{count}} 行',
213
+ gitAddedLines_other: '新增 {{count}} 行',
214
+ gitModifiedLines_one: '修改 {{count}} 行',
215
+ gitModifiedLines_other: '修改 {{count}} 行',
216
+ gitDeletedLines_one: '删除 {{count}} 行',
217
+ gitDeletedLines_other: '删除 {{count}} 行',
206
218
  noDiffChanges: ' · 当前比较无内容差异',
207
219
  previousChange: '上一个变更',
208
220
  nextChange: '下一个变更',
@@ -69,9 +69,9 @@ export const L4_HOST_ERROR_MESSAGES = {
69
69
  modelSettingsGetInvalid: 'Invalid model settings request',
70
70
  modelSettingsGetFailed: 'Could not read model settings',
71
71
  projectDefaultInputInvalid: 'Invalid project directory',
72
- projectDefaultGetFailed: 'Could not read project model default',
73
- projectDefaultSaveInvalid: 'Invalid project model default',
74
- projectDefaultSaveFailed: 'Could not save project model default',
72
+ projectDefaultGetFailed: 'Could not read the project default model',
73
+ projectDefaultSaveInvalid: 'Invalid project default model',
74
+ projectDefaultSaveFailed: 'Could not save the project default model',
75
75
  modelsListFailed: 'Could not list models',
76
76
  directoryBrowseInvalid: 'Invalid directory browser request',
77
77
  directoryMissing: 'Directory not found or is not a folder',
@@ -101,7 +101,7 @@ export const L4_HOST_ERROR_MESSAGES = {
101
101
  sessionTreeEntryFailed: 'Could not read session tree entry',
102
102
  branchInputInvalid: 'Invalid branch operation request',
103
103
  branchEntryMissing: 'Branch entry not found or no longer available',
104
- branchForkUserOnly: 'Only user messages can start a new session',
104
+ branchForkUserOnly: 'Sessions can only be forked from user messages',
105
105
  branchResendUserOnly: 'Only user messages can be resent',
106
106
  branchCloneEmpty: 'There are no messages to clone yet',
107
107
  branchFailed: 'Could not perform branch operation',
@@ -137,7 +137,7 @@ export const L4_HOST_ERROR_MESSAGES = {
137
137
  pluginResourceReadFailed: 'Could not read plugin browser resource',
138
138
  hostResourceMissing: 'Host plugin resource not found',
139
139
  hostRuntimeUnavailable: 'Host plugin runtime is not built',
140
- hostResourceReadFailed: 'Could not read Host plugin resource',
140
+ hostResourceReadFailed: 'Could not read host plugin resource',
141
141
  gitAddJsonInvalid: 'Invalid JSON for new Git branch',
142
142
  gitAddInputInvalid: 'Invalid new Git branch request',
143
143
  gitAddFailed: 'Could not create Git branch',
@@ -2,6 +2,7 @@ import 'server-only'
2
2
 
3
3
  import { getAgentDir } from '@earendil-works/pi-coding-agent'
4
4
  import { selectL4LocalizedText } from '@common/l4_foundation/locale/l4-localized-text'
5
+ import { l4PluginSourceIdentity } from '@common/l4_foundation/plugin/l4-plugin-package'
5
6
  import type {
6
7
  L2PluginManagementItem,
7
8
  L2PluginManagementSnapshot
@@ -73,9 +74,6 @@ async function resolveTargets(
73
74
  }
74
75
  if (command.action === 'remove') {
75
76
  if (!existing) throw new Error(`插件不存在:${name}`)
76
- if (existing.kind !== 'package') {
77
- throw new Error(`当前插件管理只支持卸载包来源,不支持卸载裸扩展:${name}`)
78
- }
79
77
  targets.set(existing.source, { name, source: existing.source, local: false })
80
78
  continue
81
79
  }
@@ -173,15 +171,18 @@ function completedResult(
173
171
  snapshot: L2PluginManagementSnapshot,
174
172
  agentDir: string
175
173
  ): string {
174
+ if (command.action === 'remove') {
175
+ const identity = l4PluginSourceIdentity(target.source)
176
+ if (snapshot.plugins.some((item) => l4PluginSourceIdentity(item.source) === identity)) {
177
+ throw new Error('卸载后仍发现该来源,无法确认卸载完成')
178
+ }
179
+ return '成功:已卸载并完成宿主插件应用'
180
+ }
176
181
  const item = findL2PluginCommandTarget(
177
182
  snapshot.plugins,
178
183
  readL2PluginNpmName(target.source) ?? target.name,
179
184
  agentDir
180
185
  )
181
- if (command.action === 'remove') {
182
- if (item) throw new Error('卸载后仍发现该包来源,无法确认卸载完成')
183
- return '成功:已卸载并完成宿主插件应用'
184
- }
185
186
  if (!item) throw new Error('未发现安装后的插件,无法确认生效')
186
187
  if (item.error) throw new Error(selectL4LocalizedText(item.error.message, 'zh-CN'))
187
188
  if (item.status === 'disabled') throw new Error('该插件仍被现有配置禁用,尚未生效')
@@ -40,9 +40,11 @@ import { readL4PluginReadme } from '@server/l4_foundation/pi/l4-pi-plugin-regist
40
40
  import {
41
41
  readL4PiPluginPreferences,
42
42
  readL4PiPluginUpdateTag,
43
+ removeL4PiPluginPreference,
43
44
  setL4PiPluginUpdateTag,
44
45
  setL4PiPluginEnabled
45
46
  } from '@server/l4_foundation/pi/l4-pi-plugin-preferences'
47
+ import { removeL4PiLocalExtension } from '@server/l4_foundation/pi/l4-pi-local-extension-maintenance'
46
48
  import { getL4PiGlobalPluginRuntime } from '@server/l4_foundation/pi/l4-pi-global-plugin-runtime'
47
49
  import {
48
50
  L4PiPackageMaintenanceError,
@@ -109,6 +111,7 @@ interface L2PluginPreparedMutation {
109
111
  request: L4PiDeskPackageMaintenanceRequest | null
110
112
  updateTag: string | null
111
113
  pluginName: string | null
114
+ localExtension?: boolean
112
115
  }
113
116
 
114
117
  export interface L2PluginManagementWorkSessions {
@@ -739,7 +742,12 @@ export class L2PluginManagement {
739
742
  )
740
743
  if (mutation !== 'add') {
741
744
  if (!existing) throw new L2PluginManagementNotFoundError(source)
742
- if (existing.kind !== 'package' && mutation !== 'enable' && mutation !== 'disable') {
745
+ if (
746
+ existing.kind !== 'package' &&
747
+ mutation !== 'del' &&
748
+ mutation !== 'enable' &&
749
+ mutation !== 'disable'
750
+ ) {
743
751
  throw new L2PluginManagementInvalidPackageError(
744
752
  '裸扩展不是已安装包,请编辑源码后加载变更;此操作不会删除源码'
745
753
  )
@@ -834,6 +842,18 @@ export class L2PluginManagement {
834
842
  .listConfiguredPackages()
835
843
  .filter((item) => item.scope === 'user')
836
844
  const target = configured.find((item) => item.source === operationSource)
845
+ if (mutation === 'del' && !target) {
846
+ const existing = (await this.readSources()).sources.find(
847
+ (item) => item.source === operationSource
848
+ )
849
+ if (!existing) throw new L2PluginManagementNotFoundError(source)
850
+ return {
851
+ request: null,
852
+ updateTag: null,
853
+ pluginName: null,
854
+ ...(existing.kind === 'extension' ? { localExtension: true } : {})
855
+ }
856
+ }
837
857
  let request: L4PiDeskPackageMaintenanceRequest | null = null
838
858
  let updateTag: string | null = null
839
859
  let pluginName: string | null = null
@@ -876,7 +896,7 @@ export class L2PluginManagement {
876
896
  prepared: L2PluginPreparedMutation
877
897
  ): Promise<void> {
878
898
  const { mutation, source, operationSource } = input
879
- const { request, updateTag, pluginName } = prepared
899
+ const { request, updateTag, pluginName, localExtension } = prepared
880
900
  const runtime = getL4PiGlobalPluginRuntime()
881
901
  const waiting = (reason: string): void => this.setOperation(operationSource, 'waiting', reason)
882
902
  if (request) {
@@ -919,6 +939,26 @@ export class L2PluginManagement {
919
939
  this.sourceCache = null
920
940
  throw error
921
941
  }
942
+ } else if (mutation === 'del' && !request) {
943
+ await runtime.replaceSource(operationSource, null)
944
+ if (localExtension) {
945
+ await removeL4PiLocalExtension({
946
+ source: operationSource,
947
+ agentDir: this.agentDir,
948
+ cwd: this.packageManagerCwd
949
+ })
950
+ } else {
951
+ removeL4PiPluginPreference(operationSource, this.agentDir)
952
+ }
953
+ for (const change of await runtime.scanSourceChanges([operationSource])) {
954
+ if (change.current) {
955
+ throw new L2PluginManagementInvalidPackageError(
956
+ '该来源仍被全局资源规则引用,请先移除对应规则后重试卸载'
957
+ )
958
+ }
959
+ await runtime.applySourceChange(change)
960
+ if (change.previous?.path) clearL4PiPluginNativeDiagnostics(change.previous.path)
961
+ }
922
962
  } else if (request) {
923
963
  try {
924
964
  await runtime.replaceSource(operationSource, null)
@@ -1065,6 +1105,15 @@ export class L2PluginManagement {
1065
1105
  DefaultPackageManager,
1066
1106
  SettingsManager
1067
1107
  })
1108
+ const current = new Set(snapshot.sources.map((item) => l2PluginSourceIdentity(item.source)))
1109
+ for (const previous of getL4PiGlobalPluginRuntime().readAppliedSources()) {
1110
+ if (!current.has(l2PluginSourceIdentity(previous.source))) {
1111
+ snapshot.sources.push({
1112
+ ...previous,
1113
+ error: '插件来源已移除,运行资源尚未卸载'
1114
+ })
1115
+ }
1116
+ }
1068
1117
  if (!writing() || !this.sourceCache) this.sourceCache = snapshot
1069
1118
  return writing() && this.sourceCache ? this.sourceCache : snapshot
1070
1119
  }
@@ -9,7 +9,11 @@ import {
9
9
  } from '@earendil-works/pi-coding-agent'
10
10
  import { L4PiPluginOwnerRuntime, type L4PiPluginPackageSource } from './l4-pi-plugin-owner-runtime'
11
11
  import { isL4PiDeskSafeMode } from './l4-pi-desk-mode'
12
- import { discoverL4PiPluginSources, type L4PiPluginSourceSnapshot } from './l4-pi-plugin-sources'
12
+ import {
13
+ discoverL4PiPluginSources,
14
+ type L4PiPluginSource,
15
+ type L4PiPluginSourceSnapshot
16
+ } from './l4-pi-plugin-sources'
13
17
  import { L4PiPluginSourceChanges, type L4PiPluginSourceChange } from './l4-pi-plugin-source-changes'
14
18
  import { clearL4PiPluginCodeCache } from './l4-pi-plugin-module-loader'
15
19
 
@@ -44,6 +48,10 @@ export class L4PiGlobalPluginRuntime extends L4PiPluginOwnerRuntime {
44
48
  this.discover = discover
45
49
  }
46
50
 
51
+ readAppliedSources(): L4PiPluginSource[] {
52
+ return this.sourceChanges.readAppliedSources()
53
+ }
54
+
47
55
  async scanSourceChanges(sources?: readonly string[]): Promise<L4PiPluginSourceChange[]> {
48
56
  await this.initialize()
49
57
  return this.sourceChanges.scan(await this.discover(), sources)
@@ -0,0 +1,118 @@
1
+ import 'server-only'
2
+
3
+ import { readdir, realpath, rm, stat } from 'node:fs/promises'
4
+ import { homedir } from 'node:os'
5
+ import { join, resolve } from 'node:path'
6
+ import { SettingsManager } from '@earendil-works/pi-coding-agent'
7
+ import { l4PluginSourceIdentity } from '@common/l4_foundation/plugin/l4-plugin-package'
8
+ import { getL4FileSystemKey, isL4PathInsideRoot } from '@server/l4_foundation/file/l4-file-path'
9
+ import { readL4PiPluginPreferences, removeL4PiPluginPreference } from './l4-pi-plugin-preferences'
10
+
11
+ function isMissing(error: unknown): boolean {
12
+ return (
13
+ error !== null &&
14
+ typeof error === 'object' &&
15
+ 'code' in error &&
16
+ (error.code === 'ENOENT' || error.code === 'ENOTDIR')
17
+ )
18
+ }
19
+
20
+ async function canonical(path: string): Promise<string> {
21
+ try {
22
+ return getL4FileSystemKey(await realpath(path))
23
+ } catch (error) {
24
+ if (!isMissing(error)) throw error
25
+ return getL4FileSystemKey(resolve(path))
26
+ }
27
+ }
28
+
29
+ export async function removeL4PiLocalExtension(input: {
30
+ source: string
31
+ agentDir: string
32
+ cwd: string
33
+ }): Promise<void> {
34
+ const { source, agentDir, cwd } = input
35
+ try {
36
+ const settings = SettingsManager.create(cwd, agentDir, { projectTrusted: false })
37
+ const initialErrors = settings.drainErrors()
38
+ if (initialErrors.length) {
39
+ throw new Error(
40
+ `读取扩展配置失败:${initialErrors.map(({ error }) => error.message).join(';')}`
41
+ )
42
+ }
43
+
44
+ const target = await canonical(source)
45
+ const metadata = await stat(source).catch((error: unknown) => {
46
+ if (!isMissing(error)) throw error
47
+ return null
48
+ })
49
+ const configured = settings.getGlobalSettings().extensions ?? []
50
+ const retained: string[] = []
51
+ for (const reference of configured) {
52
+ // 规则可能同时加载其他来源,留给卸载后的真实发现校验处理。
53
+ if (/^[!+-]|[*?{}[\]()]/.test(reference)) {
54
+ retained.push(reference)
55
+ continue
56
+ }
57
+ const path = await canonical(
58
+ reference.startsWith('~/')
59
+ ? join(homedir(), reference.slice(2))
60
+ : resolve(agentDir, reference)
61
+ )
62
+ if (
63
+ path !== target &&
64
+ !((metadata === null || metadata.isDirectory()) && isL4PathInsideRoot(target, path))
65
+ ) {
66
+ retained.push(reference)
67
+ }
68
+ }
69
+
70
+ const extensionDir = join(agentDir, 'extensions')
71
+ const entries = await readdir(extensionDir, { withFileTypes: true }).catch((error: unknown) => {
72
+ if (!isMissing(error)) throw error
73
+ return []
74
+ })
75
+ const removals: Array<{ path: string; recursive: boolean }> = []
76
+ for (const entry of entries) {
77
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue
78
+ const path = join(extensionDir, entry.name)
79
+ if ((await canonical(path)) === target) {
80
+ removals.push({ path, recursive: !entry.isSymbolicLink() && entry.isDirectory() })
81
+ }
82
+ }
83
+ for (const entry of removals) {
84
+ // 使用原始入口路径;链接只删入口,不能用规范目标执行递归删除。
85
+ await rm(entry.path, { recursive: entry.recursive, force: true })
86
+ }
87
+
88
+ const removedReferences = configured.length - retained.length
89
+ if (removedReferences) {
90
+ settings.setExtensionPaths(retained)
91
+ await settings.flush()
92
+ const errors = settings.drainErrors()
93
+ if (errors.length) {
94
+ throw new Error(`保存扩展配置失败:${errors.map(({ error }) => error.message).join(';')}`)
95
+ }
96
+ }
97
+
98
+ const preferences = readL4PiPluginPreferences(agentDir)
99
+ const identity = l4PluginSourceIdentity(source)
100
+ const hasPreference =
101
+ preferences.disabled.includes(identity) ||
102
+ identity in preferences.registries ||
103
+ identity in preferences.updateTags
104
+ if (hasPreference) removeL4PiPluginPreference(source, agentDir)
105
+
106
+ if (removals.length || removedReferences || hasPreference) {
107
+ console.info('[Pi Desk][LocalExtension] 已移除本地扩展入口与加载声明', {
108
+ source,
109
+ entries: removals.length,
110
+ references: removedReferences
111
+ })
112
+ }
113
+ } catch (error) {
114
+ throw new Error(`移除本地扩展失败:${error instanceof Error ? error.message : String(error)}`, {
115
+ cause: error
116
+ })
117
+ }
118
+ }
@@ -437,7 +437,10 @@ export class L4PiPluginOwnerRuntime {
437
437
 
438
438
  this.changingSources.add(source)
439
439
  try {
440
- if (owner) void this.releaseOwner(owner)
440
+ if (owner) {
441
+ const released = this.releaseOwner(owner)
442
+ if (installedPath === null) await released
443
+ }
441
444
  if (this.disposed) throw new Error('Global plugin runtime has been disposed')
442
445
  clearL4PiPluginCodeCache()
443
446
  this.packageDiagnostics.delete(source)
@@ -109,6 +109,10 @@ export class L4PiPluginSourceChanges {
109
109
  for (const source of snapshot.sources) this.applied.set(source.source, await describe(source))
110
110
  }
111
111
 
112
+ readAppliedSources(): L4PiPluginSource[] {
113
+ return structuredClone([...this.applied.values()].map((item) => item.source))
114
+ }
115
+
112
116
  async scan(
113
117
  snapshot: L4PiPluginSourceSnapshot,
114
118
  requested?: readonly string[]
@@ -63,7 +63,7 @@ const definitions: readonly CommandDefinition[] = [
63
63
  },
64
64
  {
65
65
  path: ['plugins', 'remove'],
66
- description: '卸载一个或多个包来源;完成后向本会话汇总结果',
66
+ description: '卸载包、本地扩展或残留来源并释放实例;完成后向本会话汇总结果',
67
67
  name: 'multiple',
68
68
  scope: 'required'
69
69
  }
@@ -1 +1 @@
1
- IDVi-aOj0xwp4DoZljk5f
1
+ ljXlpZIQpD6BvWyD8Fmc3
@@ -4,8 +4,8 @@
4
4
  ],
5
5
  "devFiles": [],
6
6
  "lowPriorityFiles": [
7
- "static/IDVi-aOj0xwp4DoZljk5f/_buildManifest.js",
8
- "static/IDVi-aOj0xwp4DoZljk5f/_ssgManifest.js"
7
+ "static/ljXlpZIQpD6BvWyD8Fmc3/_buildManifest.js",
8
+ "static/ljXlpZIQpD6BvWyD8Fmc3/_ssgManifest.js"
9
9
  ],
10
10
  "rootMainFiles": [
11
11
  "static/chunks/webpack-a84310d111797bef.js",
@@ -65,8 +65,8 @@
65
65
  "dynamicRoutes": {},
66
66
  "notFoundRoutes": [],
67
67
  "preview": {
68
- "previewModeId": "bb1593dc8a6f1734dec16a48d108ce65",
69
- "previewModeSigningKey": "49501a4cceb280cd75dbc0d8c303f549628d17dea745740b14617bae0620eeb6",
70
- "previewModeEncryptionKey": "465972427951c5a25a36e85596b8a42f89a1fc9588a42ff6b2e451f778cea1d3"
68
+ "previewModeId": "724e14edd9a9e87d1681837f5b2774b6",
69
+ "previewModeSigningKey": "f8047a56e43fd978b908fab8a310ae5a3a16442a0f73b160f9cc6bb676bbcac2",
70
+ "previewModeEncryptionKey": "7923a8b78086226553ae708e242b307cb0ab5c60303f824f230f133813e9a2f8"
71
71
  }
72
72
  }
@@ -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.33",
6
- "PI_DESK_VERSION_UPDATED_AT": "2026-11-01T12:38:35+08:00"
5
+ "PI_DESK_APP_VERSION": "1.1.38-dev",
6
+ "PI_DESK_VERSION_UPDATED_AT": "2026-11-03T13:31:38+08:00"
7
7
  },
8
8
  "webpack": null,
9
9
  "typescript": {
10
- "ignoreBuildErrors": false
10
+ "ignoreBuildErrors": true
11
11
  },
12
12
  "typedRoutes": false,
13
13
  "distDir": "temp/build/pi-desk/release/.next",
@@ -2,12 +2,12 @@
2
2
  "version": 1,
3
3
  "config": {
4
4
  "env": {
5
- "PI_DESK_APP_VERSION": "1.1.33",
6
- "PI_DESK_VERSION_UPDATED_AT": "2026-11-01T12:38:35+08:00"
5
+ "PI_DESK_APP_VERSION": "1.1.38-dev",
6
+ "PI_DESK_VERSION_UPDATED_AT": "2026-11-03T13:31:38+08:00"
7
7
  },
8
8
  "webpack": null,
9
9
  "typescript": {
10
- "ignoreBuildErrors": false
10
+ "ignoreBuildErrors": true
11
11
  },
12
12
  "typedRoutes": false,
13
13
  "distDir": "temp/build/pi-desk/release/.next",