@lesliechan721/aw 0.1.0-beta.7 → 0.1.0-beta.8

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 (237) hide show
  1. package/dist/authoring/scripts/init-plugin.mjs +4 -33
  2. package/dist/authoring/scripts/init-plugin.mjs.map +1 -1
  3. package/dist/authoring/scripts/lib/plugin-version-check.mjs +1 -1
  4. package/dist/authoring/scripts/plugin-status.mjs +8 -11
  5. package/dist/authoring/scripts/plugin-status.mjs.map +1 -1
  6. package/dist/authoring/scripts/remove-plugin.mjs +1 -0
  7. package/dist/authoring/scripts/remove-plugin.mjs.map +1 -1
  8. package/dist/authoring/scripts/validate-plugin.mjs +104 -54
  9. package/dist/authoring/scripts/validate-plugin.mjs.map +1 -1
  10. package/dist/scripts/codex-eval-app-server.d.ts +2 -0
  11. package/dist/scripts/codex-eval-app-server.js +107 -1
  12. package/dist/scripts/codex-eval-app-server.js.map +1 -1
  13. package/dist/scripts/eval-plugin-skills.js +5 -2
  14. package/dist/scripts/eval-plugin-skills.js.map +1 -1
  15. package/dist/scripts/plugin-eval-contract.d.ts +5 -1
  16. package/dist/scripts/plugin-eval-contract.js +54 -4
  17. package/dist/scripts/plugin-eval-contract.js.map +1 -1
  18. package/dist/scripts/plugin-staging.d.ts +6 -7
  19. package/dist/scripts/plugin-staging.js +11 -23
  20. package/dist/scripts/plugin-staging.js.map +1 -1
  21. package/dist/scripts/test-plugin-scripts.js +2 -33
  22. package/dist/scripts/test-plugin-scripts.js.map +1 -1
  23. package/dist/src/adapters/helpers.js +2 -2
  24. package/dist/src/adapters/helpers.js.map +1 -1
  25. package/dist/src/authoring/workspace-commands.d.ts +17 -2
  26. package/dist/src/authoring/workspace-commands.js +21 -4
  27. package/dist/src/authoring/workspace-commands.js.map +1 -1
  28. package/dist/src/commands/aw-capabilities.d.ts +78 -0
  29. package/dist/src/commands/aw-capabilities.js +259 -0
  30. package/dist/src/commands/aw-capabilities.js.map +1 -0
  31. package/dist/src/commands/catalog-context.d.ts +52 -0
  32. package/dist/src/commands/catalog-context.js +156 -0
  33. package/dist/src/commands/catalog-context.js.map +1 -0
  34. package/dist/src/commands/catalog-dev-source.d.ts +112 -0
  35. package/dist/src/commands/catalog-dev-source.js +481 -0
  36. package/dist/src/commands/catalog-dev-source.js.map +1 -0
  37. package/dist/src/commands/catalog-generation.d.ts +26 -0
  38. package/dist/src/commands/catalog-generation.js +14 -0
  39. package/dist/src/commands/catalog-generation.js.map +1 -0
  40. package/dist/src/commands/catalog-publication-inspection.d.ts +1 -1
  41. package/dist/src/commands/catalog-publication-inspection.js +4 -5
  42. package/dist/src/commands/catalog-publication-inspection.js.map +1 -1
  43. package/dist/src/commands/catalog-publication-target.d.ts +17 -0
  44. package/dist/src/commands/catalog-publication-target.js +60 -0
  45. package/dist/src/commands/catalog-publication-target.js.map +1 -0
  46. package/dist/src/commands/catalog-publish.d.ts +8 -1
  47. package/dist/src/commands/catalog-publish.js +25 -2
  48. package/dist/src/commands/catalog-publish.js.map +1 -1
  49. package/dist/src/commands/catalog-source.d.ts +23 -1
  50. package/dist/src/commands/catalog-source.js +126 -40
  51. package/dist/src/commands/catalog-source.js.map +1 -1
  52. package/dist/src/commands/catalog-status.d.ts +18 -2
  53. package/dist/src/commands/catalog-status.js +122 -18
  54. package/dist/src/commands/catalog-status.js.map +1 -1
  55. package/dist/src/commands/init.js +1 -1
  56. package/dist/src/commands/init.js.map +1 -1
  57. package/dist/src/commands/offline-cleanup-evidence.d.ts +23 -0
  58. package/dist/src/commands/offline-cleanup-evidence.js +38 -0
  59. package/dist/src/commands/offline-cleanup-evidence.js.map +1 -0
  60. package/dist/src/commands/options.d.ts +1 -0
  61. package/dist/src/commands/options.js +1 -0
  62. package/dist/src/commands/options.js.map +1 -1
  63. package/dist/src/commands/plugin-capabilities.js +1 -1
  64. package/dist/src/commands/plugin-capabilities.js.map +1 -1
  65. package/dist/src/commands/rebuild-command.d.ts +19 -7
  66. package/dist/src/commands/rebuild-command.js +415 -205
  67. package/dist/src/commands/rebuild-command.js.map +1 -1
  68. package/dist/src/commands/rebuild-integration.d.ts +10 -3
  69. package/dist/src/commands/rebuild-integration.js +71 -76
  70. package/dist/src/commands/rebuild-integration.js.map +1 -1
  71. package/dist/src/commands/rebuild-operations.d.ts +0 -8
  72. package/dist/src/commands/rebuild-operations.js +17 -115
  73. package/dist/src/commands/rebuild-operations.js.map +1 -1
  74. package/dist/src/commands/runtime.js +21 -34
  75. package/dist/src/commands/runtime.js.map +1 -1
  76. package/dist/src/commands/{status-v4.d.ts → status-v5.d.ts} +67 -21
  77. package/dist/src/commands/{status-v4.js → status-v5.js} +260 -30
  78. package/dist/src/commands/status-v5.js.map +1 -0
  79. package/dist/src/commands/wizard.d.ts +2 -2
  80. package/dist/src/commands/wizard.js +2 -2
  81. package/dist/src/commands/wizard.js.map +1 -1
  82. package/dist/src/commands/write-commands.js +73 -17
  83. package/dist/src/commands/write-commands.js.map +1 -1
  84. package/dist/src/constants.d.ts +6 -1
  85. package/dist/src/constants.js +6 -1
  86. package/dist/src/constants.js.map +1 -1
  87. package/dist/src/generators/local-plugin.js +57 -3
  88. package/dist/src/generators/local-plugin.js.map +1 -1
  89. package/dist/src/index.d.ts +4 -1
  90. package/dist/src/index.js +562 -85
  91. package/dist/src/index.js.map +1 -1
  92. package/dist/src/output/model.d.ts +1 -34
  93. package/dist/src/output/model.js +1 -144
  94. package/dist/src/output/model.js.map +1 -1
  95. package/dist/src/output/rebuild-progress.js +0 -3
  96. package/dist/src/output/rebuild-progress.js.map +1 -1
  97. package/dist/src/portable/commands.d.ts +4 -0
  98. package/dist/src/portable/commands.js +61 -4
  99. package/dist/src/portable/commands.js.map +1 -1
  100. package/dist/src/portable/materializer.d.ts +3 -0
  101. package/dist/src/portable/materializer.js +61 -10
  102. package/dist/src/portable/materializer.js.map +1 -1
  103. package/dist/src/tui/catalog-generation.d.ts +56 -0
  104. package/dist/src/tui/catalog-generation.js +244 -0
  105. package/dist/src/tui/catalog-generation.js.map +1 -0
  106. package/dist/src/tui/console-app-v4.d.ts +1 -1
  107. package/dist/src/tui/console-app-v4.js +446 -126
  108. package/dist/src/tui/console-app-v4.js.map +1 -1
  109. package/dist/src/tui/controller-runtime.d.ts +8 -2
  110. package/dist/src/tui/controller-runtime.js +606 -92
  111. package/dist/src/tui/controller-runtime.js.map +1 -1
  112. package/dist/src/tui/controller.d.ts +71 -6
  113. package/dist/src/tui/controller.js +377 -83
  114. package/dist/src/tui/controller.js.map +1 -1
  115. package/dist/src/tui/index.d.ts +1 -0
  116. package/dist/src/tui/index.js +1 -0
  117. package/dist/src/tui/index.js.map +1 -1
  118. package/dist/src/tui/input-display.d.ts +8 -0
  119. package/dist/src/tui/input-display.js +36 -0
  120. package/dist/src/tui/input-display.js.map +1 -0
  121. package/dist/src/tui/model.d.ts +27 -7
  122. package/dist/src/tui/model.js +273 -65
  123. package/dist/src/tui/model.js.map +1 -1
  124. package/dist/src/tui/router.d.ts +26 -0
  125. package/dist/src/tui/router.js.map +1 -1
  126. package/dist/src/tui/source-ref-presets.d.ts +3 -0
  127. package/dist/src/tui/source-ref-presets.js +11 -0
  128. package/dist/src/tui/source-ref-presets.js.map +1 -0
  129. package/dist/src/tui/views.js +31 -3
  130. package/dist/src/tui/views.js.map +1 -1
  131. package/dist/src/types.d.ts +90 -6
  132. package/dist/src/types.js.map +1 -1
  133. package/dist/src/utils/agent-version.d.ts +3 -0
  134. package/dist/src/utils/agent-version.js +13 -5
  135. package/dist/src/utils/agent-version.js.map +1 -1
  136. package/dist/src/utils/aw-home-mutation-lock.d.ts +1 -0
  137. package/dist/src/utils/aw-home-mutation-lock.js +32 -4
  138. package/dist/src/utils/aw-home-mutation-lock.js.map +1 -1
  139. package/dist/src/utils/bundle-integrity.d.ts +0 -1
  140. package/dist/src/utils/bundle-integrity.js +0 -25
  141. package/dist/src/utils/bundle-integrity.js.map +1 -1
  142. package/dist/src/utils/bundle-layout.js +8 -6
  143. package/dist/src/utils/bundle-layout.js.map +1 -1
  144. package/dist/src/utils/catalog-digests.js +13 -2
  145. package/dist/src/utils/catalog-digests.js.map +1 -1
  146. package/dist/src/utils/catalog-generation-error.d.ts +12 -0
  147. package/dist/src/utils/catalog-generation-error.js +16 -0
  148. package/dist/src/utils/catalog-generation-error.js.map +1 -0
  149. package/dist/src/utils/catalog-generation-resolver.d.ts +13 -0
  150. package/dist/src/utils/catalog-generation-resolver.js +129 -0
  151. package/dist/src/utils/catalog-generation-resolver.js.map +1 -0
  152. package/dist/src/utils/catalog-generation-service.d.ts +52 -0
  153. package/dist/src/utils/catalog-generation-service.js +620 -0
  154. package/dist/src/utils/catalog-generation-service.js.map +1 -0
  155. package/dist/src/utils/catalog-generation-state.d.ts +13 -0
  156. package/dist/src/utils/catalog-generation-state.js +201 -0
  157. package/dist/src/utils/catalog-generation-state.js.map +1 -0
  158. package/dist/src/utils/catalog-generation-store.d.ts +44 -0
  159. package/dist/src/utils/catalog-generation-store.js +387 -0
  160. package/dist/src/utils/catalog-generation-store.js.map +1 -0
  161. package/dist/src/utils/catalog-manifest-v5.d.ts +5 -0
  162. package/dist/src/utils/{catalog-manifest-v4.js → catalog-manifest-v5.js} +39 -35
  163. package/dist/src/utils/{catalog-manifest-v4.js.map → catalog-manifest-v5.js.map} +1 -1
  164. package/dist/src/utils/catalog-operation-context.d.ts +28 -0
  165. package/dist/src/utils/catalog-operation-context.js +242 -0
  166. package/dist/src/utils/catalog-operation-context.js.map +1 -0
  167. package/dist/src/utils/catalog-public-error.d.ts +7 -3
  168. package/dist/src/utils/catalog-public-error.js +20 -4
  169. package/dist/src/utils/catalog-public-error.js.map +1 -1
  170. package/dist/src/utils/catalog-snapshot.d.ts +5 -3
  171. package/dist/src/utils/catalog-snapshot.js +11 -3
  172. package/dist/src/utils/catalog-snapshot.js.map +1 -1
  173. package/dist/src/utils/catalog-source-cache.d.ts +1 -1
  174. package/dist/src/utils/catalog-source-cache.js +37 -12
  175. package/dist/src/utils/catalog-source-cache.js.map +1 -1
  176. package/dist/src/utils/catalog-source-identity.d.ts +1 -0
  177. package/dist/src/utils/catalog-source-identity.js +21 -2
  178. package/dist/src/utils/catalog-source-identity.js.map +1 -1
  179. package/dist/src/utils/catalog-sources.d.ts +1 -0
  180. package/dist/src/utils/catalog-sources.js +30 -11
  181. package/dist/src/utils/catalog-sources.js.map +1 -1
  182. package/dist/src/utils/codex-config.js +4 -4
  183. package/dist/src/utils/codex-config.js.map +1 -1
  184. package/dist/src/utils/codex-manager.d.ts +33 -0
  185. package/dist/src/utils/codex-manager.js +656 -0
  186. package/dist/src/utils/codex-manager.js.map +1 -0
  187. package/dist/src/utils/codex-skill-staging.d.ts +2 -0
  188. package/dist/src/utils/codex-skill-staging.js.map +1 -1
  189. package/dist/src/utils/dynamic-skill-tree.d.ts +4 -0
  190. package/dist/src/utils/dynamic-skill-tree.js +35 -5
  191. package/dist/src/utils/dynamic-skill-tree.js.map +1 -1
  192. package/dist/src/utils/git-catalog-provider.js +10 -25
  193. package/dist/src/utils/git-catalog-provider.js.map +1 -1
  194. package/dist/src/utils/lifecycle-active-store.d.ts +0 -28
  195. package/dist/src/utils/lifecycle-active-store.js +35 -247
  196. package/dist/src/utils/lifecycle-active-store.js.map +1 -1
  197. package/dist/src/utils/multi-source-catalog.d.ts +3 -0
  198. package/dist/src/utils/multi-source-catalog.js +120 -10
  199. package/dist/src/utils/multi-source-catalog.js.map +1 -1
  200. package/dist/src/utils/native-cli.d.ts +15 -6
  201. package/dist/src/utils/native-cli.js +31 -39
  202. package/dist/src/utils/native-cli.js.map +1 -1
  203. package/dist/src/utils/orphan-marketplace-cleanup.js +0 -3
  204. package/dist/src/utils/orphan-marketplace-cleanup.js.map +1 -1
  205. package/dist/src/utils/ownership.d.ts +10 -1
  206. package/dist/src/utils/ownership.js +52 -24
  207. package/dist/src/utils/ownership.js.map +1 -1
  208. package/dist/src/utils/path-catalog-provider.js +12 -8
  209. package/dist/src/utils/path-catalog-provider.js.map +1 -1
  210. package/dist/src/utils/plugin-config-write.js +18 -35
  211. package/dist/src/utils/plugin-config-write.js.map +1 -1
  212. package/dist/src/utils/plugin-config.d.ts +1 -12
  213. package/dist/src/utils/plugin-config.js +107 -226
  214. package/dist/src/utils/plugin-config.js.map +1 -1
  215. package/dist/src/utils/plugin-scripts.d.ts +1 -1
  216. package/dist/src/utils/plugin-scripts.js +1 -1
  217. package/dist/src/utils/plugin-scripts.js.map +1 -1
  218. package/dist/src/utils/state-paths.js +7 -2
  219. package/dist/src/utils/state-paths.js.map +1 -1
  220. package/dist/src/utils/target-eligibility.d.ts +7 -2
  221. package/dist/src/utils/target-eligibility.js +6 -3
  222. package/dist/src/utils/target-eligibility.js.map +1 -1
  223. package/package.json +4 -3
  224. package/dist/authoring/scripts/check-plugin-versions.d.mts +0 -2
  225. package/dist/authoring/scripts/check-plugin-versions.mjs +0 -5
  226. package/dist/authoring/scripts/check-plugin-versions.mjs.map +0 -1
  227. package/dist/src/commands/status-v4.js.map +0 -1
  228. package/dist/src/commands/status.d.ts +0 -93
  229. package/dist/src/commands/status.js +0 -442
  230. package/dist/src/commands/status.js.map +0 -1
  231. package/dist/src/utils/active-marketplace-runtime-lock.d.ts +0 -12
  232. package/dist/src/utils/active-marketplace-runtime-lock.js +0 -133
  233. package/dist/src/utils/active-marketplace-runtime-lock.js.map +0 -1
  234. package/dist/src/utils/catalog-manifest-v3.d.ts +0 -2
  235. package/dist/src/utils/catalog-manifest-v3.js +0 -3
  236. package/dist/src/utils/catalog-manifest-v3.js.map +0 -1
  237. package/dist/src/utils/catalog-manifest-v4.d.ts +0 -5
@@ -1,7 +1,10 @@
1
1
  import crypto from 'node:crypto';
2
+ import { CatalogGenerationError } from '../utils/catalog-generation-error.js';
2
3
  import { buildTuiViewModel, hasInstalledPlacement, } from './model.js';
3
4
  import { safeDiagnosticCode } from './diagnostics.js';
5
+ import { targetHasArtifactCollision } from '../utils/target-eligibility.js';
4
6
  import { TuiRouter, } from './router.js';
7
+ import { CatalogRefreshCoordinator, } from './catalog-generation.js';
5
8
  function reviewPreparationStageLabel(stage) {
6
9
  return {
7
10
  validate: '验证当前状态',
@@ -42,7 +45,10 @@ function cloneFrozen(value) {
42
45
  return deepFreeze(structuredClone(value));
43
46
  }
44
47
  export function operationEvidenceFingerprint(evidence) {
45
- return crypto.createHash('sha256').update(JSON.stringify(stableValue(evidence))).digest('hex');
48
+ const { codexManager: _runtimeSelection, ...staleEvidence } = evidence;
49
+ return crypto.createHash('sha256')
50
+ .update(JSON.stringify(stableValue(staleEvidence)))
51
+ .digest('hex');
46
52
  }
47
53
  function sourceIdFromPluginId(pluginId) {
48
54
  const separator = pluginId.indexOf('/');
@@ -69,6 +75,14 @@ function operationLabels(intent) {
69
75
  destructive: false,
70
76
  };
71
77
  }
78
+ if (intent.kind === 'catalogActivate') {
79
+ return {
80
+ title: '使用 Catalog 更新',
81
+ description: '切换当前 Catalog generation;不会自动升级已安装插件,也不会修改设置。',
82
+ primaryLabel: '使用已下载的更新',
83
+ destructive: false,
84
+ };
85
+ }
72
86
  if (intent.kind === 'pluginWrite') {
73
87
  if (intent.originIssue) {
74
88
  if (intent.action === 'cleanup') {
@@ -163,7 +177,41 @@ function reviewChanges(intent, evidence, dependencyOverview) {
163
177
  description: review.collisions.map((collision) => collision.name).join('、') }] : []),
164
178
  ];
165
179
  }
180
+ if (intent.kind === 'catalogActivate') {
181
+ const candidate = evidence?.catalogCandidate;
182
+ if (!candidate) {
183
+ throw new TuiOperationBlockedError('catalogGenerationCandidateUnavailable', '缺少 Catalog Candidate Review 证据。');
184
+ }
185
+ return [
186
+ {
187
+ kind: 'change',
188
+ title: `Catalog ${candidate.activeVersion} → ${candidate.candidateVersion}`,
189
+ description: `${candidate.channel === 'beta' ? 'Beta' : 'Release'} · ${candidate.requestedRef}`,
190
+ },
191
+ {
192
+ kind: 'change',
193
+ title: `插件变化 +${candidate.pluginChanges.added} ~${candidate.pluginChanges.updated} −${candidate.pluginChanges.removed}`,
194
+ },
195
+ { kind: 'keep', title: '已安装插件不会自动升级' },
196
+ { kind: 'keep', title: '当前设置不会改变' },
197
+ { kind: 'change', title: '激活后重新加载插件列表和依赖关系' },
198
+ ];
199
+ }
166
200
  if (intent.kind === 'pluginWrite') {
201
+ const runtimeChanges = evidence?.codexManager
202
+ ? [{
203
+ kind: evidence.codexManager.state === 'requires-install' ? 'add' : 'keep',
204
+ group: 'runtime',
205
+ title: evidence.codexManager.state === 'requires-install'
206
+ ? '获取 Codex runtime'
207
+ : 'Codex runtime',
208
+ description: evidence.codexManager.state === 'requires-install'
209
+ ? `${evidence.codexManager.packageName}@${evidence.codexManager.version} · AW-managed 缓存`
210
+ : evidence.codexManager.state === 'cached'
211
+ ? `${evidence.codexManager.packageName}@${evidence.codexManager.version} · 复用 AW-managed 缓存`
212
+ : `使用兼容的 system Codex ${evidence.codexManager.version}`,
213
+ }]
214
+ : [];
167
215
  const plan = evidence?.dependencyPlan;
168
216
  if (plan && (intent.action === 'install' || intent.action === 'upgrade' || intent.action === 'reinstall')) {
169
217
  const changes = plan.targets.map((target) => {
@@ -188,7 +236,7 @@ function reviewChanges(intent, evidence, dependencyOverview) {
188
236
  title: '插件设置和已有依赖保持不变',
189
237
  description: '移除根插件时不会自动清理依赖。',
190
238
  });
191
- return changes;
239
+ return [...changes, ...runtimeChanges];
192
240
  }
193
241
  const platforms = intent.platform === 'both'
194
242
  ? ['codex', 'claude']
@@ -220,17 +268,23 @@ function reviewChanges(intent, evidence, dependencyOverview) {
220
268
  if (intent.originIssue) {
221
269
  changes.push({ kind: 'keep', title: '插件设置保持不变' }, { kind: 'warning', title: '执行前重新检查完整目标和来源证据' });
222
270
  }
223
- return changes;
271
+ return [...changes, ...runtimeChanges];
224
272
  }
225
273
  if (intent.kind === 'configReset') {
226
274
  const layerLabel = intent.scope === 'user'
227
275
  ? '个人默认'
228
276
  : intent.projectLayer === 'local' ? '当前项目(仅本机)' : '当前项目';
229
277
  return [
230
- { kind: 'remove', title: `删除${layerLabel}中的已存值`, description: '有效值将恢复为继承值或默认值。' },
231
- { kind: 'change', title: '重新计算有效设置' },
232
- { kind: 'change', title: '重新应用当前可见安装位置' },
233
- { kind: 'keep', title: intent.scope === 'user' ? '当前项目和本机设置层保持不变' : '其他设置层保持不变' },
278
+ ...(intent.changes?.map((change) => ({
279
+ kind: 'remove', group: 'configChanges', title: `${layerLabel} · ${change.title}`,
280
+ description: `${change.before} → ${change.after}`,
281
+ })) ?? [{ kind: 'remove', group: 'configChanges', title: `删除${layerLabel}中的已存值`, description: '有效值将恢复为继承值或默认值。' }]),
282
+ ...((evidence?.config?.affectedTargets.length ?? 0) > 0
283
+ ? evidence.config.affectedTargets.map((target) => ({
284
+ kind: 'change', group: 'configImpact', title: `重新应用 ${target.platform === 'codex' ? 'Codex' : 'Claude Code'} · ${target.scope === 'user' ? '所有项目' : '当前项目'}`,
285
+ }))
286
+ : [{ kind: 'keep', group: 'configImpact', title: '当前没有可见安装;只保存设置' }]),
287
+ { kind: 'keep', group: 'preserved', title: intent.scope === 'user' ? '当前项目和本机设置层保持不变' : '其他设置层保持不变' },
234
288
  ];
235
289
  }
236
290
  if (intent.kind === 'sourceMutation') {
@@ -255,17 +309,24 @@ function reviewChanges(intent, evidence, dependencyOverview) {
255
309
  throw new TuiOperationBlockedError('operationNotImplemented', '这个写操作尚未接入新 Controller。');
256
310
  }
257
311
  const privateKeys = new Set([...(intent.sensitiveKeys ?? []), ...(intent.privateKeys ?? [])]);
312
+ const removeKeys = new Set(intent.removeKeys ?? []);
258
313
  if (intent.changes) {
259
314
  return [
260
315
  ...intent.changes.map((change) => ({
261
- kind: 'change',
262
- title: change.title,
263
- description: privateKeys.has(change.key)
264
- ? change.after === '未设置' ? '已设置 → 未设置' : '已设置 → 将更新'
265
- : `${change.before} → ${change.after}`,
316
+ kind: removeKeys.has(change.key) ? 'remove' : 'change',
317
+ group: 'configChanges',
318
+ title: removeKeys.has(change.key) ? `删除 ${change.title} 中的值` : change.title,
319
+ description: removeKeys.has(change.key)
320
+ ? privateKeys.has(change.key) ? `已设置 → ${change.after}` : `${change.before} → ${change.after}`
321
+ : privateKeys.has(change.key) ? '已设置 → 将更新'
322
+ : `${change.before} → ${change.after}`,
266
323
  })),
267
- { kind: 'change', title: '重新应用插件内容', description: '只处理当前可见安装位置。' },
268
- { kind: 'keep', title: '其他项目不会被自动扫描' },
324
+ ...((evidence?.config?.affectedTargets.length ?? 0) > 0
325
+ ? evidence.config.affectedTargets.map((target) => ({
326
+ kind: 'change', group: 'configImpact', title: `重新应用 ${target.platform === 'codex' ? 'Codex' : 'Claude Code'} · ${target.scope === 'user' ? '所有项目' : '当前项目'}`,
327
+ }))
328
+ : [{ kind: 'keep', group: 'configImpact', title: '当前没有可见安装;只保存设置' }]),
329
+ { kind: 'keep', group: 'preserved', title: '其他项目不会被自动扫描' },
269
330
  ];
270
331
  }
271
332
  const updateKeys = Object.keys(intent.updates ?? {});
@@ -283,33 +344,45 @@ function reviewChanges(intent, evidence, dependencyOverview) {
283
344
  ];
284
345
  }
285
346
  function technicalDetails(evidence) {
347
+ const short = (value) => {
348
+ const normalized = value.startsWith('sha256:') ? value.slice(7) : value;
349
+ return normalized.slice(0, 8);
350
+ };
286
351
  const details = [
287
- `Catalog 配置:${evidence.catalog.configDigest}`,
288
- `Catalog 组合:${evidence.catalog.compositeDigest}`,
352
+ `Catalog 配置:${short(evidence.catalog.configDigest)}`,
353
+ `Catalog 组合:${short(evidence.catalog.compositeDigest)}`,
289
354
  ];
355
+ if (evidence.catalog.generationSetDigest) {
356
+ details.push(`Catalog generation:${short(evidence.catalog.generationSetDigest)}`);
357
+ }
290
358
  if (evidence.source) {
291
359
  details.push(`来源:${evidence.source.sourceId}`);
292
- details.push(`来源身份:${evidence.source.sourceIdentity}`);
360
+ if (evidence.source.sourceIdentity)
361
+ details.push(`来源身份:${short(evidence.source.sourceIdentity)}`);
293
362
  if (evidence.source.resolvedCommit)
294
- details.push(`Snapshot commit:${evidence.source.resolvedCommit}`);
363
+ details.push(`Snapshot commit:${short(evidence.source.resolvedCommit)}`);
295
364
  if (evidence.source.snapshotDigest)
296
- details.push(`Snapshot digest:${evidence.source.snapshotDigest}`);
365
+ details.push(`Snapshot digest:${short(evidence.source.snapshotDigest)}`);
297
366
  if (evidence.source.contentDigest)
298
- details.push(`Content digest:${evidence.source.contentDigest}`);
367
+ details.push(`Content digest:${short(evidence.source.contentDigest)}`);
299
368
  }
300
369
  if (evidence.targetFingerprint)
301
- details.push(`目标状态:${evidence.targetFingerprint}`);
370
+ details.push(`目标状态:${short(evidence.targetFingerprint)}`);
371
+ if (evidence.offlineCleanup) {
372
+ details.push(`离线清理原因:${evidence.offlineCleanup.reason}`);
373
+ details.push(`离线清理目标:${short(evidence.offlineCleanup.targetFingerprint)}`);
374
+ }
302
375
  if (evidence.dependencyPlan)
303
376
  details.push(`安装计划:${evidence.dependencyPlan.resolvedPluginIds.length} 个插件`);
304
377
  if (evidence.config)
305
- details.push(`设置层:${evidence.config.scope}/${evidence.config.projectLayer} · ${evidence.config.storeDigest}`);
378
+ details.push(`设置层:${evidence.config.scope}/${evidence.config.projectLayer} · ${short(evidence.config.storeDigest)}`);
306
379
  if (evidence.sourceWrite) {
307
- details.push(`候选配置:${evidence.sourceWrite.candidateDigest}`);
308
- details.push(`引用清单:${evidence.sourceWrite.inventoryDigest} · ${evidence.sourceWrite.referenceCount} 项`);
380
+ details.push(`候选配置:${short(evidence.sourceWrite.candidateDigest)}`);
381
+ details.push(`引用清单:${short(evidence.sourceWrite.inventoryDigest)} · ${evidence.sourceWrite.referenceCount} 项`);
309
382
  }
310
383
  if (evidence.portable) {
311
384
  details.push(`Portable 候选:${evidence.portable.selected.length} 个`);
312
- details.push(`Materialization:${evidence.portable.materializationDigest}`);
385
+ details.push(`Materialization:${short(evidence.portable.materializationDigest)}`);
313
386
  details.push(`Skills CLI:${evidence.portable.skillsCliVersion}`);
314
387
  }
315
388
  return details;
@@ -317,6 +390,8 @@ function technicalDetails(evidence) {
317
390
  function issueCode(error) {
318
391
  if (error instanceof TuiOperationBlockedError)
319
392
  return error.code;
393
+ if (error instanceof CatalogGenerationError)
394
+ return error.code;
320
395
  if (error instanceof Error && error.name === 'CatalogSourcesConfigConflictError') {
321
396
  return 'operationReviewStale';
322
397
  }
@@ -353,10 +428,12 @@ export class TuiController {
353
428
  #tickets = new Map();
354
429
  #inflightOperations = new Map();
355
430
  #abort = new AbortController();
431
+ #catalogRefreshCoordinator;
356
432
  #progressUnsubscribe;
357
433
  #progressFeed;
358
434
  #phase = 'idle';
359
435
  #status;
436
+ #catalogGenerations = new Map();
360
437
  #viewModel;
361
438
  #progress = { messages: [] };
362
439
  #progressTargets = [];
@@ -376,6 +453,26 @@ export class TuiController {
376
453
  #disposed = false;
377
454
  constructor(dependencies, progressFeed) {
378
455
  this.dependencies = dependencies;
456
+ if (dependencies.refreshCatalogSource) {
457
+ this.#catalogRefreshCoordinator = new CatalogRefreshCoordinator({
458
+ maxConcurrency: 2,
459
+ execute: dependencies.refreshCatalogSource,
460
+ onUpdate: (sourceId, state, errorCode) => {
461
+ const current = this.#catalogGenerations.get(sourceId);
462
+ if (current) {
463
+ this.#catalogGenerations.set(sourceId, {
464
+ ...current,
465
+ refreshState: state,
466
+ ...(errorCode ? { lastCheckCode: errorCode } : {}),
467
+ });
468
+ }
469
+ this.#publish();
470
+ },
471
+ onResult: (source) => {
472
+ this.#catalogGenerations.set(source.sourceId, structuredClone(source));
473
+ },
474
+ });
475
+ }
379
476
  this.#progressFeed = progressFeed;
380
477
  if (progressFeed) {
381
478
  this.#progressUnsubscribe = progressFeed.subscribe((progress) => {
@@ -407,9 +504,12 @@ export class TuiController {
407
504
  get snapshot() {
408
505
  return cloneFrozen({
409
506
  phase: this.#phase,
507
+ ...(this.#status?.cliChannel ? { cliChannel: this.#status.cliChannel } : {}),
410
508
  router: this.#router.snapshot,
411
509
  ...(this.#viewModel ? { viewModel: this.#viewModel } : {}),
412
510
  sourceHealth: [...this.#health.values()],
511
+ catalogGenerations: [...this.#catalogGenerations.values()]
512
+ .sort((left, right) => left.sourceId.localeCompare(right.sourceId, 'en')),
413
513
  progress: this.#progress,
414
514
  progressTargets: this.#progressTargets,
415
515
  ...(this.#activeReview ? { activeReview: this.#activeReview } : {}),
@@ -495,6 +595,14 @@ export class TuiController {
495
595
  && !this.#viewModel?.sourceTabs.some((source) => source.id === route.sourceId)) {
496
596
  return { name: 'sourceList' };
497
597
  }
598
+ if ((route.name === 'catalogCandidate'
599
+ || route.name === 'catalogRefreshPolicy'
600
+ || route.name === 'catalogRefresh'
601
+ || route.name === 'catalogImmutableViolation'
602
+ || route.name === 'catalogActivationBlocked')
603
+ && !this.#catalogGenerations.has(route.sourceId)) {
604
+ return { name: 'sourceList' };
605
+ }
498
606
  return route;
499
607
  }
500
608
  #normalizeCurrentRoute() {
@@ -505,6 +613,12 @@ export class TuiController {
505
613
  }
506
614
  #applyStatus(status) {
507
615
  this.#status = structuredClone(status);
616
+ if (status.catalogGenerations) {
617
+ this.#catalogGenerations.clear();
618
+ for (const source of status.catalogGenerations) {
619
+ this.#catalogGenerations.set(source.sourceId, structuredClone(source));
620
+ }
621
+ }
508
622
  this.#health.clear();
509
623
  for (const source of status.payload.catalog.sources) {
510
624
  this.#health.set(source.id, {
@@ -566,6 +680,10 @@ export class TuiController {
566
680
  this.#sourceAborts.clear();
567
681
  }
568
682
  #startGitSourceRefreshes() {
683
+ if (this.#catalogRefreshCoordinator) {
684
+ this.#catalogRefreshCoordinator.start([...this.#catalogGenerations.values()]);
685
+ return;
686
+ }
569
687
  for (const source of this.#status?.payload.catalog.sources ?? []) {
570
688
  if (source.type === 'git')
571
689
  void this.refreshSource(source.id).catch(() => undefined);
@@ -573,6 +691,7 @@ export class TuiController {
573
691
  }
574
692
  async load() {
575
693
  this.#cancelSourceRefreshes();
694
+ this.dependencies.beginCatalogSession?.();
576
695
  const generation = ++this.#generation;
577
696
  this.#phase = 'loading';
578
697
  this.#publish();
@@ -601,6 +720,10 @@ export class TuiController {
601
720
  async refreshSource(sourceId) {
602
721
  if (this.#disposed || !this.#status)
603
722
  return;
723
+ if (this.#catalogRefreshCoordinator) {
724
+ await this.#catalogRefreshCoordinator.refresh(sourceId, this.#catalogGenerations.get(sourceId)?.refreshState ?? 'idle');
725
+ return;
726
+ }
604
727
  this.#sourceAborts.get(sourceId)?.abort();
605
728
  const sourceAbort = new AbortController();
606
729
  this.#sourceAborts.set(sourceId, sourceAbort);
@@ -907,9 +1030,11 @@ export class TuiController {
907
1030
  this.#publish();
908
1031
  }
909
1032
  back() {
910
- this.#cancelReviewPreparation();
911
- this.#cancelPortableRequest();
912
1033
  const route = this.#router.snapshot.current.route;
1034
+ if (route.name !== 'catalogActivationBlocked' || route.reason !== 'reviewPreparation') {
1035
+ this.#cancelReviewPreparation();
1036
+ }
1037
+ this.#cancelPortableRequest();
913
1038
  if (route.name === 'review' && this.#activeReview?.operationId === route.operationId) {
914
1039
  this.#tickets.delete(this.#activeReview.reviewToken);
915
1040
  this.#activeReview = undefined;
@@ -1067,6 +1192,10 @@ export class TuiController {
1067
1192
  }));
1068
1193
  }
1069
1194
  return plugin.supportedPlatforms.map((platform) => {
1195
+ const statusTarget = this.#status?.payload.targets.find((target) => (target.pluginId === pluginId && target.platform === platform && target.scope === scope));
1196
+ if (statusTarget && targetHasArtifactCollision(statusTarget)) {
1197
+ return { platform, compatible: false, disabled: '存在输出冲突;请先移除或迁移占用内容' };
1198
+ }
1070
1199
  const placement = plugin.installedPlacements.find((entry) => entry.platform === platform);
1071
1200
  if (!placement)
1072
1201
  return { platform, compatible: false, disabled: '当前没有可信的平台能力信息' };
@@ -1085,6 +1214,16 @@ export class TuiController {
1085
1214
  }
1086
1215
  const overview = this.#viewModel?.dependencyOverviews.find((entry) => entry.pluginId === pluginId);
1087
1216
  for (const dependency of overview?.dependencies ?? []) {
1217
+ const dependencyTarget = this.#status?.payload.targets.find((target) => (target.pluginId === dependency.pluginId
1218
+ && target.platform === platform
1219
+ && target.scope === scope));
1220
+ if (dependencyTarget && targetHasArtifactCollision(dependencyTarget)) {
1221
+ return {
1222
+ platform,
1223
+ compatible: false,
1224
+ disabled: `${dependency.displayName} 存在输出冲突`,
1225
+ };
1226
+ }
1088
1227
  const dependentPlugin = this.#viewModel?.plugins.find((entry) => entry.pluginId === dependency.pluginId);
1089
1228
  if (!dependentPlugin || !dependentPlugin.supportedPlatforms.includes(platform)) {
1090
1229
  return {
@@ -1128,6 +1267,89 @@ export class TuiController {
1128
1267
  health: this.#health.get(sourceId),
1129
1268
  });
1130
1269
  }
1270
+ listCatalogCandidates() {
1271
+ return cloneFrozen([...this.#catalogGenerations.values()]
1272
+ .filter((source) => source.candidate)
1273
+ .sort((left, right) => left.sourceId.localeCompare(right.sourceId, 'en')));
1274
+ }
1275
+ getCatalogGeneration(sourceId) {
1276
+ const source = this.#catalogGenerations.get(sourceId);
1277
+ return source ? cloneFrozen(source) : undefined;
1278
+ }
1279
+ getSessionCatalogVersion(sourceId) {
1280
+ return this.#status?.payload.catalog.sources.find((source) => source.id === sourceId)?.catalogVersion;
1281
+ }
1282
+ beginCatalogActivation(options) {
1283
+ const source = this.#catalogGenerations.get(options.sourceId);
1284
+ if (!source?.candidate) {
1285
+ throw new TuiOperationBlockedError('catalogGenerationCandidateUnavailable', 'Catalog Candidate 不再可用。请重新检查来源。');
1286
+ }
1287
+ let reason;
1288
+ let preservedLabel;
1289
+ const refreshingSourceId = this.#catalogRefreshCoordinator?.refreshingSourceIds()[0];
1290
+ if (refreshingSourceId) {
1291
+ reason = 'refreshProgress';
1292
+ preservedLabel = options.dirtyDraft
1293
+ ? `${options.dirtyDraft.label}已保留;${refreshingSourceId} Catalog 来源正在刷新`
1294
+ : `${refreshingSourceId} Catalog 来源刷新`;
1295
+ }
1296
+ else if (options.dirtyDraft) {
1297
+ reason = options.dirtyDraft.kind === 'config' ? 'configDraft' : 'sourceDraft';
1298
+ preservedLabel = options.dirtyDraft.label;
1299
+ }
1300
+ else if (this.#portableDraft) {
1301
+ reason = 'portableDraft';
1302
+ preservedLabel = 'Portable Skills 当前选择';
1303
+ }
1304
+ else if (this.#reviewPreparation)
1305
+ reason = 'reviewPreparation';
1306
+ else if (this.#router.snapshot.current.route.name === 'progress') {
1307
+ reason = this.#activeReview?.operationKind === 'sourceMutation'
1308
+ ? 'sourceMutationProgress' : 'writeProgress';
1309
+ }
1310
+ else if (this.#activeReview)
1311
+ reason = 'activeReview';
1312
+ else {
1313
+ const refreshing = [...this.#catalogGenerations.values()].find((candidate) => candidate.refreshState === 'checking'
1314
+ || candidate.refreshState === 'downloading'
1315
+ || candidate.refreshState === 'validating');
1316
+ if (refreshing) {
1317
+ reason = 'refreshProgress';
1318
+ preservedLabel = `${refreshing.sourceId} Catalog 来源刷新`;
1319
+ }
1320
+ }
1321
+ if (reason) {
1322
+ this.#router.push({
1323
+ name: 'catalogActivationBlocked',
1324
+ sourceId: options.sourceId,
1325
+ reason,
1326
+ ...(preservedLabel ? { preservedLabel } : {}),
1327
+ });
1328
+ this.#publish();
1329
+ return undefined;
1330
+ }
1331
+ return this.startReviewPreparation({
1332
+ kind: 'catalogActivate',
1333
+ sourceId: options.sourceId,
1334
+ candidateGenerationKey: source.candidate.generationKey,
1335
+ }, { name: 'catalogCandidate', sourceId: options.sourceId });
1336
+ }
1337
+ cancelCatalogRefresh(sourceId) {
1338
+ this.#catalogRefreshCoordinator?.cancel(sourceId);
1339
+ }
1340
+ async setCatalogRefreshPolicy(sourceId, refreshPolicy) {
1341
+ if (!this.dependencies.setCatalogRefreshPolicy) {
1342
+ throw new TuiOperationBlockedError('catalogGenerationRefreshPolicyUnavailable', 'Catalog 自动刷新设置不可用。');
1343
+ }
1344
+ const source = await this.dependencies.setCatalogRefreshPolicy(sourceId, refreshPolicy);
1345
+ if (this.#disposed)
1346
+ return;
1347
+ const current = this.#catalogGenerations.get(sourceId);
1348
+ this.#catalogGenerations.set(sourceId, current
1349
+ ? { ...current, refreshPolicy: source.refreshPolicy }
1350
+ : structuredClone(source));
1351
+ this.#publish();
1352
+ }
1131
1353
  async inspectSourceRemoval(sourceId) {
1132
1354
  if (!this.dependencies.inspectSourceRemoval) {
1133
1355
  throw new TuiOperationBlockedError('stateInventoryFailed', '来源引用清单不可用。');
@@ -1167,7 +1389,9 @@ export class TuiController {
1167
1389
  catch (error) {
1168
1390
  const code = issueCode(error);
1169
1391
  await this.#logOperationError(error, code, intent);
1170
- throw new TuiOperationBlockedError(code, '操作未建立。请检查当前设置后重试。');
1392
+ throw new TuiOperationBlockedError(code, code === 'catalogGenerationUnavailable'
1393
+ ? '本次加载时 Catalog 来源不可用。请刷新来源并重新加载后重试。'
1394
+ : '操作未建立。请检查当前设置后重试。');
1171
1395
  }
1172
1396
  }
1173
1397
  startReviewPreparation(intent, returnRoute) {
@@ -1239,11 +1463,12 @@ export class TuiController {
1239
1463
  }
1240
1464
  #reviewPreparationIsCurrent(token) {
1241
1465
  const preparation = this.#reviewPreparation;
1466
+ const route = this.#router.snapshot.current.route;
1242
1467
  return Boolean(preparation && preparation.token === token
1243
1468
  && !preparation.abort.signal.aborted
1244
1469
  && !this.#disposed
1245
- && this.#router.snapshot.current.route.name === 'reviewPreparing'
1246
- && this.#router.snapshot.current.route.preparationId === preparation.snapshot.id);
1470
+ && (route.name === 'reviewPreparing' && route.preparationId === preparation.snapshot.id
1471
+ || route.name === 'catalogActivationBlocked' && route.reason === 'reviewPreparation'));
1247
1472
  }
1248
1473
  async #runReviewPreparation(token) {
1249
1474
  const preparation = this.#reviewPreparation;
@@ -1281,13 +1506,16 @@ export class TuiController {
1281
1506
  catch (error) {
1282
1507
  if (!this.#reviewPreparationIsCurrent(token))
1283
1508
  return;
1509
+ const code = issueCode(error);
1284
1510
  const active = this.#reviewPreparation;
1285
1511
  active.snapshot.state = 'failed';
1286
1512
  active.snapshot.stages = active.snapshot.stages.map((stage) => stage.state === 'running' ? { ...stage, state: 'failed' } : stage);
1287
1513
  active.snapshot.code = 'reviewPreparationFailed';
1288
- active.snapshot.message = '无法准备确认信息。请返回检查选择,或重试。';
1514
+ active.snapshot.message = code === 'catalogGenerationUnavailable'
1515
+ ? '本次加载时 Catalog 来源不可用。请刷新来源并重新加载后重试。'
1516
+ : '无法准备确认信息。请返回检查选择,或重试。';
1289
1517
  this.#publish();
1290
- await this.#logOperationError(error, issueCode(error), intent);
1518
+ await this.#logOperationError(error, code, intent);
1291
1519
  }
1292
1520
  }
1293
1521
  async #prepareReviewEvidence(intent, signal, onStage = () => undefined) {
@@ -1361,10 +1589,19 @@ export class TuiController {
1361
1589
  ? { version: this.#viewModel.plugins.find((plugin) => plugin.pluginId === intent.pluginId).version }
1362
1590
  : {}),
1363
1591
  },
1592
+ ...(evidence.source && 'catalogVersion' in evidence.source
1593
+ && typeof evidence.source.catalogVersion === 'string'
1594
+ && evidence.source.snapshotDigest
1595
+ ? { catalogBinding: {
1596
+ sourceId: evidence.source.sourceId,
1597
+ catalogVersion: evidence.source.catalogVersion,
1598
+ generationDigestShort: evidence.source.snapshotDigest.replace(/^sha256:/u, '').slice(0, 8),
1599
+ } }
1600
+ : {}),
1364
1601
  changes: reviewChanges(intent, evidence, 'pluginId' in intent ? this.getPluginDependencyOverview(intent.pluginId) : undefined),
1365
1602
  technicalDetails: technicalDetails(evidence),
1366
- ...(intent.kind === 'pluginWrite' && intent.action === 'cleanup'
1367
- ? { equivalentCommand: `aw cleanup ${intent.pluginId} --platform ${intent.platform} --scope ${intent.scope}${intent.force === true ? ' --force' : ''}` }
1603
+ ...(intent.kind === 'pluginWrite'
1604
+ ? { equivalentCommand: `aw ${intent.action} ${intent.pluginId} --platform ${intent.platform} --scope ${intent.scope}${intent.action === 'cleanup' && intent.force === true ? ' --force' : ''}${evidence.codexManager?.state === 'requires-install' ? ' --allow-managed-codex-install' : ''}` }
1368
1605
  : {}),
1369
1606
  primaryLabel: intent.kind === 'pluginWrite' && evidence.dependencyPlan
1370
1607
  ? `${labels.primaryLabel} ${evidence.dependencyPlan.resolvedPluginIds.length} 个插件`
@@ -1435,7 +1672,9 @@ export class TuiController {
1435
1672
  this.#publish();
1436
1673
  try {
1437
1674
  this.#assertIssueOrigin(ticket.intent);
1438
- await this.dependencies.validateIntent?.(structuredClone(ticket.intent), this.#abort.signal);
1675
+ if (ticket.intent.kind !== 'pluginWrite') {
1676
+ await this.dependencies.validateIntent?.(structuredClone(ticket.intent), this.#abort.signal);
1677
+ }
1439
1678
  }
1440
1679
  catch (error) {
1441
1680
  if (!this.#operationIsCurrent(generation))
@@ -1444,6 +1683,9 @@ export class TuiController {
1444
1683
  await this.#logOperationError(error, code, ticket.intent);
1445
1684
  if (!this.#operationIsCurrent(generation))
1446
1685
  return this.#detachedBlocked(operationId, ticket.intent);
1686
+ if (code === 'operationReviewStale' && ticket.intent.kind === 'catalogActivate') {
1687
+ return this.#rebuildStaleReview(ticket, operationId, undefined, generation);
1688
+ }
1447
1689
  this.#cancelSourceRefreshes();
1448
1690
  generation = ++this.#generation;
1449
1691
  try {
@@ -1465,23 +1707,28 @@ export class TuiController {
1465
1707
  }
1466
1708
  if (!this.#operationIsCurrent(generation))
1467
1709
  return this.#detachedBlocked(operationId, ticket.intent);
1468
- let current;
1469
- try {
1470
- current = await this.dependencies.captureEvidence(structuredClone(ticket.intent), this.#abort.signal);
1471
- }
1472
- catch (error) {
1473
- if (!this.#operationIsCurrent(generation))
1474
- return this.#detachedBlocked(operationId, ticket.intent);
1475
- const code = issueCode(error);
1476
- await this.#logOperationError(error, code, ticket.intent);
1710
+ if (ticket.intent.kind !== 'pluginWrite') {
1711
+ let current;
1712
+ try {
1713
+ current = await this.dependencies.captureEvidence(structuredClone(ticket.intent), this.#abort.signal);
1714
+ }
1715
+ catch (error) {
1716
+ if (!this.#operationIsCurrent(generation))
1717
+ return this.#detachedBlocked(operationId, ticket.intent);
1718
+ const code = issueCode(error);
1719
+ await this.#logOperationError(error, code, ticket.intent);
1720
+ if (!this.#operationIsCurrent(generation))
1721
+ return this.#detachedBlocked(operationId, ticket.intent);
1722
+ if (code === 'operationReviewStale' && ticket.intent.kind === 'catalogActivate') {
1723
+ return this.#rebuildStaleReview(ticket, operationId, undefined, generation);
1724
+ }
1725
+ return this.#setBlocked(operationId, code, { intent: ticket.intent, returnRoute: ticket.returnRoute });
1726
+ }
1477
1727
  if (!this.#operationIsCurrent(generation))
1478
1728
  return this.#detachedBlocked(operationId, ticket.intent);
1479
- return this.#setBlocked(operationId, code, { intent: ticket.intent, returnRoute: ticket.returnRoute });
1480
- }
1481
- if (!this.#operationIsCurrent(generation))
1482
- return this.#detachedBlocked(operationId, ticket.intent);
1483
- if (operationEvidenceFingerprint(current) !== operationEvidenceFingerprint(ticket.evidence)) {
1484
- return this.#rebuildStaleReview(ticket, operationId, current, generation);
1729
+ if (operationEvidenceFingerprint(current) !== operationEvidenceFingerprint(ticket.evidence)) {
1730
+ return this.#rebuildStaleReview(ticket, operationId, current, generation);
1731
+ }
1485
1732
  }
1486
1733
  if (ticket.intent.kind === 'sourceMutation') {
1487
1734
  this.#cancelSourceRefreshes();
@@ -1499,9 +1746,14 @@ export class TuiController {
1499
1746
  return this.#detachedBlocked(operationId, ticket.intent);
1500
1747
  this.#cancelSourceRefreshes();
1501
1748
  generation = ++this.#generation;
1502
- if (ticket.intent.kind === 'sourceMutation') {
1749
+ if (ticket.intent.kind === 'sourceMutation' || ticket.intent.kind === 'catalogActivate') {
1750
+ this.dependencies.beginCatalogSession?.(ticket.intent.kind === 'catalogActivate'
1751
+ ? { bootstrapMissingActive: false }
1752
+ : undefined);
1503
1753
  try {
1504
- this.#progressFeed?.setCurrent({ target: 'Catalog 来源配置', phase: '读取最终状态' }, '来源操作:读取最终状态');
1754
+ this.#progressFeed?.setCurrent({ target: ticket.intent.kind === 'catalogActivate' ? 'Catalog generation' : 'Catalog 来源配置', phase: '读取最终状态' }, ticket.intent.kind === 'catalogActivate'
1755
+ ? 'Catalog 激活:重新加载当前会话'
1756
+ : '来源操作:读取最终状态');
1505
1757
  }
1506
1758
  catch {
1507
1759
  // Progress diagnostics must not replace the completed source write.
@@ -1545,6 +1797,17 @@ export class TuiController {
1545
1797
  && Boolean(hasFailedDependencyTarget);
1546
1798
  if (pluginSummary?.code === 'operationFailed') {
1547
1799
  const unsuccessfulTargets = pluginSummary.targets?.filter((target) => target.state === 'failed' || target.state === 'skipped') ?? [];
1800
+ const managerFailureCode = unsuccessfulTargets.map((target) => target.errorCode).find((code) => code === 'codexSystemCliUnsupported'
1801
+ || code === 'managedCodexAuthorizationRequired'
1802
+ || code === 'managedCodexUnavailable');
1803
+ if (managerFailureCode) {
1804
+ return this.#setBlocked(operationId, managerFailureCode, {
1805
+ intent: ticket.intent,
1806
+ navigation: 'result',
1807
+ returnRoute: ticket.returnRoute,
1808
+ summary: pluginSummary,
1809
+ });
1810
+ }
1548
1811
  const dependencyStillRequired = unsuccessfulTargets.length > 0
1549
1812
  && unsuccessfulTargets.every((target) => target.errorCode === 'dependencyStillRequired');
1550
1813
  return this.#setBlocked(operationId, dependencyStillRequired ? 'dependencyStillRequired' : 'operationFailed', {
@@ -1637,7 +1900,8 @@ export class TuiController {
1637
1900
  this.#activeReview = undefined;
1638
1901
  this.#router.replace({ name: 'result', operationId });
1639
1902
  this.#publish();
1640
- this.#startGitSourceRefreshes();
1903
+ if (ticket.intent.kind !== 'catalogActivate')
1904
+ this.#startGitSourceRefreshes();
1641
1905
  return cloneFrozen(result);
1642
1906
  }
1643
1907
  catch (error) {
@@ -1689,6 +1953,7 @@ export class TuiController {
1689
1953
  }
1690
1954
  async #rebuildStaleReview(ticket, operationId, currentEvidence, generation = this.#generation) {
1691
1955
  this.#cancelSourceRefreshes();
1956
+ this.dependencies.beginCatalogSession?.({ bootstrapMissingActive: false });
1692
1957
  generation = ++this.#generation;
1693
1958
  try {
1694
1959
  const refreshed = await this.dependencies.loadStatus(this.#abort.signal);
@@ -1706,15 +1971,23 @@ export class TuiController {
1706
1971
  returnRoute: ticket.returnRoute,
1707
1972
  });
1708
1973
  }
1974
+ const intent = ticket.intent.kind === 'catalogActivate'
1975
+ ? (() => {
1976
+ const candidate = this.#catalogGenerations.get(ticket.intent.sourceId)?.candidate;
1977
+ return candidate
1978
+ ? { ...ticket.intent, candidateGenerationKey: candidate.generationKey }
1979
+ : ticket.intent;
1980
+ })()
1981
+ : ticket.intent;
1709
1982
  try {
1710
- this.#assertIssueOrigin(ticket.intent);
1711
- await this.dependencies.validateIntent?.(structuredClone(ticket.intent), this.#abort.signal);
1983
+ this.#assertIssueOrigin(intent);
1984
+ await this.dependencies.validateIntent?.(structuredClone(intent), this.#abort.signal);
1712
1985
  }
1713
1986
  catch (error) {
1714
1987
  if (!this.#operationIsCurrent(generation))
1715
1988
  return this.#detachedBlocked(operationId, ticket.intent);
1716
1989
  const code = issueCode(error);
1717
- await this.#logOperationError(error, code, ticket.intent);
1990
+ await this.#logOperationError(error, code, intent);
1718
1991
  if (!this.#operationIsCurrent(generation))
1719
1992
  return this.#detachedBlocked(operationId, ticket.intent);
1720
1993
  const result = {
@@ -1722,7 +1995,7 @@ export class TuiController {
1722
1995
  operationId,
1723
1996
  code,
1724
1997
  message: '状态已变化,原操作不再安全。请修改选择后重试。',
1725
- ...this.#operationIdentity(ticket.intent),
1998
+ ...this.#operationIdentity(intent),
1726
1999
  returnRoute: structuredClone(this.#normalizeRoute(ticket.returnRoute)),
1727
2000
  };
1728
2001
  this.#result = structuredClone(result);
@@ -1736,50 +2009,56 @@ export class TuiController {
1736
2009
  let evidence = currentEvidence;
1737
2010
  if (!evidence) {
1738
2011
  try {
1739
- evidence = await this.dependencies.captureEvidence(structuredClone(ticket.intent), this.#abort.signal);
2012
+ evidence = await this.dependencies.captureEvidence(structuredClone(intent), this.#abort.signal);
1740
2013
  }
1741
2014
  catch (error) {
1742
2015
  if (!this.#operationIsCurrent(generation))
1743
2016
  return this.#detachedBlocked(operationId, ticket.intent);
1744
2017
  const code = issueCode(error);
1745
- await this.#logOperationError(error, code, ticket.intent);
2018
+ await this.#logOperationError(error, code, intent);
1746
2019
  if (!this.#operationIsCurrent(generation))
1747
2020
  return this.#detachedBlocked(operationId, ticket.intent);
1748
- return this.#setBlocked(operationId, code, { intent: ticket.intent, returnRoute: ticket.returnRoute });
2021
+ return this.#setBlocked(operationId, code, { intent, returnRoute: ticket.returnRoute });
1749
2022
  }
1750
2023
  }
1751
2024
  if (!this.#operationIsCurrent(generation))
1752
2025
  return this.#detachedBlocked(operationId, ticket.intent);
1753
- if (ticket.intent.kind === 'portableInstall') {
2026
+ if (intent.kind === 'portableInstall') {
1754
2027
  try {
1755
- await this.refreshPortableInstallDraft(ticket.intent.pluginId);
2028
+ await this.refreshPortableInstallDraft(intent.pluginId);
1756
2029
  }
1757
2030
  catch (error) {
1758
2031
  if (!this.#operationIsCurrent(generation))
1759
2032
  return this.#detachedBlocked(operationId, ticket.intent);
1760
2033
  const code = issueCode(error);
1761
- await this.#logOperationError(error, code, ticket.intent);
1762
- return this.#setBlocked(operationId, code, { intent: ticket.intent, returnRoute: ticket.returnRoute });
2034
+ await this.#logOperationError(error, code, intent);
2035
+ return this.#setBlocked(operationId, code, { intent, returnRoute: ticket.returnRoute });
1763
2036
  }
1764
2037
  }
1765
2038
  if (!this.#operationIsCurrent(generation))
1766
2039
  return this.#detachedBlocked(operationId, ticket.intent);
1767
- const review = this.#storeOperationReview(ticket.intent, evidence, 'replace', ticket.returnRoute);
1768
- const result = { state: 'stale', operationId, review, ...this.#operationIdentity(ticket.intent) };
2040
+ const review = this.#storeOperationReview(intent, evidence, 'replace', ticket.returnRoute);
2041
+ const result = { state: 'stale', operationId, review, ...this.#operationIdentity(intent) };
1769
2042
  this.#result = result;
1770
2043
  this.#publish();
1771
2044
  return cloneFrozen(result);
1772
2045
  }
1773
2046
  #setBlocked(operationId, code, options) {
1774
- const message = code === 'portableSkillsCliInstallFailed'
1775
- ? 'Skills CLI 安装失败。请修改 Agent 或重试。'
1776
- : options?.writeMayHaveCompleted
1777
- ? '写入可能已完成,当前无法确认。请查看刷新后的平台或问题状态。'
1778
- : code === 'configResetStillPresent'
1779
- ? '重置执行后设置层仍然存在。请重新检查设置问题。'
1780
- : code === 'issueStillPresent'
1781
- ? '写入已完成,但重新检查后原问题仍然存在。'
1782
- : '操作未完成。请重新检查后重试。';
2047
+ const message = code === 'managedCodexAuthorizationRequired'
2048
+ ? '操作在插件写入前停止。请在 Review 中确认获取 Codex runtime,或在命令行传入 --allow-managed-codex-install。'
2049
+ : code === 'codexSystemCliUnsupported'
2050
+ ? '操作在插件写入前停止。系统 Codex CLI 版本不受支持。请升级官方 Codex CLI 后重试。'
2051
+ : code === 'managedCodexUnavailable'
2052
+ ? '操作在插件写入前停止。Codex runtime 当前不可用。请重试、升级 AW,或安装官方 Codex CLI。'
2053
+ : code === 'portableSkillsCliInstallFailed'
2054
+ ? 'Skills CLI 安装失败。请修改 Agent 或重试。'
2055
+ : options?.writeMayHaveCompleted
2056
+ ? '写入可能已完成,当前无法确认。请查看刷新后的平台或问题状态。'
2057
+ : code === 'configResetStillPresent'
2058
+ ? '重置执行后设置层仍然存在。请重新检查设置问题。'
2059
+ : code === 'issueStillPresent'
2060
+ ? '写入已完成,但重新检查后原问题仍然存在。'
2061
+ : '操作未完成。请重新检查后重试。';
1783
2062
  const result = {
1784
2063
  state: 'blocked',
1785
2064
  operationId,
@@ -1843,13 +2122,17 @@ export class TuiController {
1843
2122
  ? origin.actionId === 'cleanup' && action.writeAction === 'cleanup'
1844
2123
  ? { action: 'cleanup', scope: issue.scope }
1845
2124
  : undefined
1846
- : issue.category === 'scopeConflict'
1847
- ? origin.actionId === 'keepUserScope'
1848
- ? { action: 'uninstall', scope: 'project' }
1849
- : origin.actionId === 'keepProjectScope'
1850
- ? { action: 'uninstall', scope: 'user' }
1851
- : undefined
1852
- : undefined;
2125
+ : issue.category === 'sourceMissing'
2126
+ ? origin.actionId === 'cleanup' && action.writeAction === 'cleanup'
2127
+ ? { action: 'cleanup', scope: issue.scope }
2128
+ : undefined
2129
+ : issue.category === 'scopeConflict'
2130
+ ? origin.actionId === 'keepUserScope'
2131
+ ? { action: 'uninstall', scope: 'project' }
2132
+ : origin.actionId === 'keepProjectScope'
2133
+ ? { action: 'uninstall', scope: 'user' }
2134
+ : undefined
2135
+ : undefined;
1853
2136
  const dependencyOriginValid = issue.category !== 'dependencyMissing' && issue.category !== 'dependencyNotReady'
1854
2137
  || (origin.consumerPluginId === issue.pluginId
1855
2138
  && origin.consumerSourceIdentity === issue.sourceIdentity
@@ -1871,6 +2154,7 @@ export class TuiController {
1871
2154
  this.#cancelReviewPreparation();
1872
2155
  this.#cancelPortableRequest();
1873
2156
  this.#abort.abort();
2157
+ this.#catalogRefreshCoordinator?.dispose();
1874
2158
  this.#cancelSourceRefreshes();
1875
2159
  this.#progressUnsubscribe?.();
1876
2160
  this.#tickets.clear();
@@ -1879,6 +2163,13 @@ export class TuiController {
1879
2163
  }
1880
2164
  }
1881
2165
  function operationResultSummary(intent, execution) {
2166
+ if (intent.kind === 'catalogActivate') {
2167
+ return {
2168
+ code: 'operationSucceeded',
2169
+ message: 'Catalog 更新已启用;插件列表和依赖关系已重新加载。',
2170
+ messages: ['已安装插件没有自动升级。', '插件设置没有改变。'],
2171
+ };
2172
+ }
1882
2173
  if (intent.kind === 'portableInstall') {
1883
2174
  const receipt = execution;
1884
2175
  const verificationUnknown = receipt.failures.some((failure) => failure.code === 'portableSkillsCliVerificationFailed'
@@ -1950,7 +2241,10 @@ function operationResultSummary(intent, execution) {
1950
2241
  requiredBy: planned.requiredBy,
1951
2242
  } : {}),
1952
2243
  ...(entry.errorCode === 'dependencyStillRequired'
1953
- ? { errorCode: 'dependencyStillRequired' }
2244
+ || entry.errorCode === 'codexSystemCliUnsupported'
2245
+ || entry.errorCode === 'managedCodexAuthorizationRequired'
2246
+ || entry.errorCode === 'managedCodexUnavailable'
2247
+ ? { errorCode: entry.errorCode }
1954
2248
  : {}),
1955
2249
  }];
1956
2250
  });