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

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 (275) hide show
  1. package/dist/authoring/scripts/add-skill.mjs +4 -4
  2. package/dist/authoring/scripts/add-skill.mjs.map +1 -1
  3. package/dist/authoring/scripts/init-plugin.mjs +4 -4
  4. package/dist/authoring/scripts/init-plugin.mjs.map +1 -1
  5. package/dist/authoring/scripts/lib/plugin-version-check.mjs +1 -1
  6. package/dist/authoring/scripts/lib/skill-scaffold.d.mts +77 -90
  7. package/dist/authoring/scripts/lib/skill-scaffold.mjs +99 -120
  8. package/dist/authoring/scripts/lib/skill-scaffold.mjs.map +1 -1
  9. package/dist/authoring/scripts/remove-plugin.mjs +1 -1
  10. package/dist/authoring/scripts/remove-plugin.mjs.map +1 -1
  11. package/dist/authoring/scripts/validate-plugin.mjs +34 -778
  12. package/dist/authoring/scripts/validate-plugin.mjs.map +1 -1
  13. package/dist/scripts/aw-skill-eval-materializer.d.ts +24 -0
  14. package/dist/scripts/{plugin-eval-materializer.js → aw-skill-eval-materializer.js} +34 -145
  15. package/dist/scripts/aw-skill-eval-materializer.js.map +1 -0
  16. package/dist/scripts/bundle-plugins.d.ts +1 -0
  17. package/dist/scripts/bundle-plugins.js +2 -1
  18. package/dist/scripts/bundle-plugins.js.map +1 -1
  19. package/dist/scripts/plugin-staging.d.ts +5 -6
  20. package/dist/scripts/plugin-staging.js +12 -13
  21. package/dist/scripts/plugin-staging.js.map +1 -1
  22. package/dist/src/authoring/workspace-commands.d.ts +2 -1
  23. package/dist/src/authoring/workspace-commands.js.map +1 -1
  24. package/dist/src/commands/aw-capabilities.d.ts +2 -2
  25. package/dist/src/commands/aw-capabilities.js +6 -11
  26. package/dist/src/commands/aw-capabilities.js.map +1 -1
  27. package/dist/src/commands/catalog-dev-source.d.ts +18 -12
  28. package/dist/src/commands/catalog-dev-source.js +241 -305
  29. package/dist/src/commands/catalog-dev-source.js.map +1 -1
  30. package/dist/src/commands/catalog-generation.d.ts +6 -19
  31. package/dist/src/commands/catalog-generation.js +10 -11
  32. package/dist/src/commands/catalog-generation.js.map +1 -1
  33. package/dist/src/commands/catalog-publication-inspection.d.ts +28 -27
  34. package/dist/src/commands/catalog-publication-inspection.js +139 -50
  35. package/dist/src/commands/catalog-publication-inspection.js.map +1 -1
  36. package/dist/src/commands/catalog-publication-journal.d.ts +49 -0
  37. package/dist/src/commands/catalog-publication-journal.js +147 -0
  38. package/dist/src/commands/catalog-publication-journal.js.map +1 -0
  39. package/dist/src/commands/catalog-publication-target.d.ts +2 -1
  40. package/dist/src/commands/catalog-publication-target.js +37 -2
  41. package/dist/src/commands/catalog-publication-target.js.map +1 -1
  42. package/dist/src/commands/catalog-publish.d.ts +51 -15
  43. package/dist/src/commands/catalog-publish.js +576 -219
  44. package/dist/src/commands/catalog-publish.js.map +1 -1
  45. package/dist/src/commands/catalog-source.d.ts +3 -28
  46. package/dist/src/commands/catalog-source.js +29 -140
  47. package/dist/src/commands/catalog-source.js.map +1 -1
  48. package/dist/src/commands/catalog-status.d.ts +6 -26
  49. package/dist/src/commands/catalog-status.js +8 -146
  50. package/dist/src/commands/catalog-status.js.map +1 -1
  51. package/dist/src/commands/catalog-workspace-resolution.d.ts +13 -0
  52. package/dist/src/commands/catalog-workspace-resolution.js +42 -0
  53. package/dist/src/commands/catalog-workspace-resolution.js.map +1 -0
  54. package/dist/src/commands/dependency-operation-plan.d.ts +88 -9
  55. package/dist/src/commands/dependency-operation-plan.js +331 -6
  56. package/dist/src/commands/dependency-operation-plan.js.map +1 -1
  57. package/dist/src/commands/init.d.ts +5 -11
  58. package/dist/src/commands/init.js +30 -51
  59. package/dist/src/commands/init.js.map +1 -1
  60. package/dist/src/commands/offline-cleanup-evidence.d.ts +20 -0
  61. package/dist/src/commands/offline-cleanup-evidence.js +87 -9
  62. package/dist/src/commands/offline-cleanup-evidence.js.map +1 -1
  63. package/dist/src/commands/options.d.ts +6 -1
  64. package/dist/src/commands/options.js +64 -6
  65. package/dist/src/commands/options.js.map +1 -1
  66. package/dist/src/commands/plugin-capabilities.js +4 -0
  67. package/dist/src/commands/plugin-capabilities.js.map +1 -1
  68. package/dist/src/commands/rebuild-command.d.ts +10 -8
  69. package/dist/src/commands/rebuild-command.js +1217 -426
  70. package/dist/src/commands/rebuild-command.js.map +1 -1
  71. package/dist/src/commands/rebuild-integration.d.ts +54 -6
  72. package/dist/src/commands/rebuild-integration.js +691 -364
  73. package/dist/src/commands/rebuild-integration.js.map +1 -1
  74. package/dist/src/commands/rebuild-operations.d.ts +2 -0
  75. package/dist/src/commands/rebuild-operations.js +31 -17
  76. package/dist/src/commands/rebuild-operations.js.map +1 -1
  77. package/dist/src/commands/skill-eval.d.ts +33 -0
  78. package/dist/src/commands/skill-eval.js +363 -0
  79. package/dist/src/commands/skill-eval.js.map +1 -0
  80. package/dist/src/commands/{status-v5.d.ts → status-v6.d.ts} +59 -26
  81. package/dist/src/commands/{status-v5.js → status-v6.js} +882 -79
  82. package/dist/src/commands/status-v6.js.map +1 -0
  83. package/dist/src/commands/sync-command.d.ts +21 -0
  84. package/dist/src/commands/sync-command.js +206 -0
  85. package/dist/src/commands/sync-command.js.map +1 -0
  86. package/dist/src/commands/wizard.d.ts +2 -2
  87. package/dist/src/commands/write-commands.js +1 -43
  88. package/dist/src/commands/write-commands.js.map +1 -1
  89. package/dist/src/constants.d.ts +5 -4
  90. package/dist/src/constants.js +5 -4
  91. package/dist/src/constants.js.map +1 -1
  92. package/dist/src/eval/manifest-v1.d.ts +66 -0
  93. package/dist/src/eval/manifest-v1.js +326 -0
  94. package/dist/src/eval/manifest-v1.js.map +1 -0
  95. package/dist/src/generators/local-plugin.d.ts +3 -0
  96. package/dist/src/generators/local-plugin.js +123 -55
  97. package/dist/src/generators/local-plugin.js.map +1 -1
  98. package/dist/src/index.d.ts +2 -0
  99. package/dist/src/index.js +412 -152
  100. package/dist/src/index.js.map +1 -1
  101. package/dist/src/output/command-receipt.d.ts +58 -8
  102. package/dist/src/output/command-receipt.js +141 -11
  103. package/dist/src/output/command-receipt.js.map +1 -1
  104. package/dist/src/output/model.d.ts +3 -11
  105. package/dist/src/portable/commands.d.ts +2 -2
  106. package/dist/src/portable/commands.js +2 -8
  107. package/dist/src/portable/commands.js.map +1 -1
  108. package/dist/src/portable/materializer.d.ts +1 -4
  109. package/dist/src/portable/materializer.js +0 -2
  110. package/dist/src/portable/materializer.js.map +1 -1
  111. package/dist/src/tui/catalog-generation.d.ts +20 -41
  112. package/dist/src/tui/catalog-generation.js +65 -224
  113. package/dist/src/tui/catalog-generation.js.map +1 -1
  114. package/dist/src/tui/components.js +1 -2
  115. package/dist/src/tui/components.js.map +1 -1
  116. package/dist/src/tui/console-app-v4.js +461 -330
  117. package/dist/src/tui/console-app-v4.js.map +1 -1
  118. package/dist/src/tui/controller-runtime.d.ts +3 -5
  119. package/dist/src/tui/controller-runtime.js +182 -187
  120. package/dist/src/tui/controller-runtime.js.map +1 -1
  121. package/dist/src/tui/controller.d.ts +42 -45
  122. package/dist/src/tui/controller.js +454 -407
  123. package/dist/src/tui/controller.js.map +1 -1
  124. package/dist/src/tui/diagnostics.d.ts +1 -2
  125. package/dist/src/tui/diagnostics.js.map +1 -1
  126. package/dist/src/tui/index.d.ts +1 -1
  127. package/dist/src/tui/index.js +1 -1
  128. package/dist/src/tui/index.js.map +1 -1
  129. package/dist/src/tui/model.d.ts +81 -13
  130. package/dist/src/tui/model.js +378 -95
  131. package/dist/src/tui/model.js.map +1 -1
  132. package/dist/src/tui/router.d.ts +9 -0
  133. package/dist/src/tui/router.js.map +1 -1
  134. package/dist/src/types.d.ts +54 -47
  135. package/dist/src/types.js.map +1 -1
  136. package/dist/src/utils/agent-version.d.ts +4 -1
  137. package/dist/src/utils/agent-version.js +28 -3
  138. package/dist/src/utils/agent-version.js.map +1 -1
  139. package/dist/src/utils/catalog-digests.d.ts +7 -0
  140. package/dist/src/utils/catalog-digests.js +27 -0
  141. package/dist/src/utils/catalog-digests.js.map +1 -1
  142. package/dist/src/utils/catalog-generation-control-snapshot.d.ts +11 -0
  143. package/dist/src/utils/catalog-generation-control-snapshot.js +45 -0
  144. package/dist/src/utils/catalog-generation-control-snapshot.js.map +1 -0
  145. package/dist/src/utils/catalog-generation-error.d.ts +2 -1
  146. package/dist/src/utils/catalog-generation-error.js +12 -0
  147. package/dist/src/utils/catalog-generation-error.js.map +1 -1
  148. package/dist/src/utils/catalog-generation-resolver.d.ts +10 -3
  149. package/dist/src/utils/catalog-generation-resolver.js +90 -76
  150. package/dist/src/utils/catalog-generation-resolver.js.map +1 -1
  151. package/dist/src/utils/catalog-generation-service.d.ts +24 -33
  152. package/dist/src/utils/catalog-generation-service.js +253 -567
  153. package/dist/src/utils/catalog-generation-service.js.map +1 -1
  154. package/dist/src/utils/catalog-generation-state.d.ts +4 -0
  155. package/dist/src/utils/catalog-generation-state.js +73 -106
  156. package/dist/src/utils/catalog-generation-state.js.map +1 -1
  157. package/dist/src/utils/catalog-generation-store.d.ts +16 -26
  158. package/dist/src/utils/catalog-generation-store.js +154 -290
  159. package/dist/src/utils/catalog-generation-store.js.map +1 -1
  160. package/dist/src/utils/catalog-manifest-v6.d.ts +5 -0
  161. package/dist/src/utils/{catalog-manifest-v5.js → catalog-manifest-v6.js} +23 -35
  162. package/dist/src/utils/catalog-manifest-v6.js.map +1 -0
  163. package/dist/src/utils/catalog-operation-context.d.ts +6 -14
  164. package/dist/src/utils/catalog-operation-context.js +68 -169
  165. package/dist/src/utils/catalog-operation-context.js.map +1 -1
  166. package/dist/src/utils/catalog-public-error.js +1 -4
  167. package/dist/src/utils/catalog-public-error.js.map +1 -1
  168. package/dist/src/utils/catalog-runtime-projection.d.ts +3 -0
  169. package/dist/src/utils/catalog-runtime-projection.js +5 -40
  170. package/dist/src/utils/catalog-runtime-projection.js.map +1 -1
  171. package/dist/src/utils/catalog-snapshot.d.ts +2 -2
  172. package/dist/src/utils/catalog-source-references.d.ts +7 -0
  173. package/dist/src/utils/catalog-source-references.js +44 -55
  174. package/dist/src/utils/catalog-source-references.js.map +1 -1
  175. package/dist/src/utils/catalog-sources.js +25 -9
  176. package/dist/src/utils/catalog-sources.js.map +1 -1
  177. package/dist/src/utils/codex-config.d.ts +2 -0
  178. package/dist/src/utils/codex-config.js +22 -1
  179. package/dist/src/utils/codex-config.js.map +1 -1
  180. package/dist/src/utils/codex-manager.d.ts +50 -2
  181. package/dist/src/utils/codex-manager.js +160 -6
  182. package/dist/src/utils/codex-manager.js.map +1 -1
  183. package/dist/src/utils/codex-reconciler.d.ts +131 -0
  184. package/dist/src/utils/codex-reconciler.js +327 -0
  185. package/dist/src/utils/codex-reconciler.js.map +1 -0
  186. package/dist/src/utils/codex-shared-native.d.ts +56 -0
  187. package/dist/src/utils/codex-shared-native.js +87 -0
  188. package/dist/src/utils/codex-shared-native.js.map +1 -0
  189. package/dist/src/utils/codex-surface-analysis.d.ts +44 -0
  190. package/dist/src/utils/codex-surface-analysis.js +247 -0
  191. package/dist/src/utils/codex-surface-analysis.js.map +1 -0
  192. package/dist/src/utils/git-catalog-provider.js +56 -115
  193. package/dist/src/utils/git-catalog-provider.js.map +1 -1
  194. package/dist/src/utils/manifest.d.ts +1 -3
  195. package/dist/src/utils/manifest.js +1 -3
  196. package/dist/src/utils/manifest.js.map +1 -1
  197. package/dist/src/utils/multi-source-catalog.d.ts +1 -10
  198. package/dist/src/utils/multi-source-catalog.js +8 -175
  199. package/dist/src/utils/multi-source-catalog.js.map +1 -1
  200. package/dist/src/utils/native-cli.d.ts +34 -0
  201. package/dist/src/utils/native-cli.js +91 -0
  202. package/dist/src/utils/native-cli.js.map +1 -1
  203. package/dist/src/utils/orphan-marketplace-cleanup.js +1 -0
  204. package/dist/src/utils/orphan-marketplace-cleanup.js.map +1 -1
  205. package/dist/src/utils/ownership.d.ts +0 -10
  206. package/dist/src/utils/ownership.js +3 -88
  207. package/dist/src/utils/ownership.js.map +1 -1
  208. package/dist/src/utils/path-catalog-provider.d.ts +1 -1
  209. package/dist/src/utils/path-catalog-provider.js +45 -62
  210. package/dist/src/utils/path-catalog-provider.js.map +1 -1
  211. package/dist/src/utils/paths.d.ts +0 -1
  212. package/dist/src/utils/paths.js +0 -1
  213. package/dist/src/utils/paths.js.map +1 -1
  214. package/dist/src/utils/plugin-config-write.js +0 -5
  215. package/dist/src/utils/plugin-config-write.js.map +1 -1
  216. package/dist/src/utils/plugin-intent-state.d.ts +39 -0
  217. package/dist/src/utils/plugin-intent-state.js +325 -0
  218. package/dist/src/utils/plugin-intent-state.js.map +1 -0
  219. package/dist/src/utils/target-eligibility.d.ts +3 -2
  220. package/dist/src/utils/target-eligibility.js +4 -3
  221. package/dist/src/utils/target-eligibility.js.map +1 -1
  222. package/package.json +5 -4
  223. package/dist/scripts/aw-skill-eval-engine.d.ts +0 -13
  224. package/dist/scripts/aw-skill-eval-engine.js +0 -31
  225. package/dist/scripts/aw-skill-eval-engine.js.map +0 -1
  226. package/dist/scripts/codex-eval-app-server.d.ts +0 -91
  227. package/dist/scripts/codex-eval-app-server.js +0 -1334
  228. package/dist/scripts/codex-eval-app-server.js.map +0 -1
  229. package/dist/scripts/codex-eval-isolation.d.ts +0 -61
  230. package/dist/scripts/codex-eval-isolation.js +0 -318
  231. package/dist/scripts/codex-eval-isolation.js.map +0 -1
  232. package/dist/scripts/codex-eval-process-management.d.ts +0 -8
  233. package/dist/scripts/codex-eval-process-management.js +0 -69
  234. package/dist/scripts/codex-eval-process-management.js.map +0 -1
  235. package/dist/scripts/codex-eval-process-tree.d.ts +0 -38
  236. package/dist/scripts/codex-eval-process-tree.js +0 -803
  237. package/dist/scripts/codex-eval-process-tree.js.map +0 -1
  238. package/dist/scripts/codex-eval-runtime-config.d.ts +0 -62
  239. package/dist/scripts/codex-eval-runtime-config.js +0 -843
  240. package/dist/scripts/codex-eval-runtime-config.js.map +0 -1
  241. package/dist/scripts/codex-trigger-eval.d.ts +0 -84
  242. package/dist/scripts/codex-trigger-eval.js +0 -968
  243. package/dist/scripts/codex-trigger-eval.js.map +0 -1
  244. package/dist/scripts/eval-plugin-skills-audited.d.ts +0 -6
  245. package/dist/scripts/eval-plugin-skills-audited.js +0 -261
  246. package/dist/scripts/eval-plugin-skills-audited.js.map +0 -1
  247. package/dist/scripts/eval-plugin-skills.d.ts +0 -45
  248. package/dist/scripts/eval-plugin-skills.js +0 -1589
  249. package/dist/scripts/eval-plugin-skills.js.map +0 -1
  250. package/dist/scripts/eval-run-layout.d.ts +0 -17
  251. package/dist/scripts/eval-run-layout.js +0 -26
  252. package/dist/scripts/eval-run-layout.js.map +0 -1
  253. package/dist/scripts/plugin-eval-contract.d.ts +0 -189
  254. package/dist/scripts/plugin-eval-contract.js +0 -745
  255. package/dist/scripts/plugin-eval-contract.js.map +0 -1
  256. package/dist/scripts/plugin-eval-materializer.d.ts +0 -13
  257. package/dist/scripts/plugin-eval-materializer.js.map +0 -1
  258. package/dist/scripts/skill-reporting.d.ts +0 -212
  259. package/dist/scripts/skill-reporting.js +0 -922
  260. package/dist/scripts/skill-reporting.js.map +0 -1
  261. package/dist/src/commands/catalog-context.d.ts +0 -52
  262. package/dist/src/commands/catalog-context.js +0 -156
  263. package/dist/src/commands/catalog-context.js.map +0 -1
  264. package/dist/src/commands/status-v5.js.map +0 -1
  265. package/dist/src/utils/catalog-manifest-v5.d.ts +0 -5
  266. package/dist/src/utils/catalog-manifest-v5.js.map +0 -1
  267. package/dist/src/utils/catalog-source-cache.d.ts +0 -62
  268. package/dist/src/utils/catalog-source-cache.js +0 -321
  269. package/dist/src/utils/catalog-source-cache.js.map +0 -1
  270. package/dist/src/utils/codex-project-surface.d.ts +0 -3
  271. package/dist/src/utils/codex-project-surface.js +0 -49
  272. package/dist/src/utils/codex-project-surface.js.map +0 -1
  273. package/dist/src/utils/plugin-release.d.ts +0 -2
  274. package/dist/src/utils/plugin-release.js +0 -2
  275. package/dist/src/utils/plugin-release.js.map +0 -1
@@ -1,10 +1,10 @@
1
1
  import crypto from 'node:crypto';
2
2
  import { CatalogGenerationError } from '../utils/catalog-generation-error.js';
3
- import { buildTuiViewModel, hasInstalledPlacement, } from './model.js';
3
+ import { requestedIntentAction } from '../output/command-receipt.js';
4
+ import { buildTuiViewModel, dependencyPlacementTargets, hasInstalledPlacement, } from './model.js';
4
5
  import { safeDiagnosticCode } from './diagnostics.js';
5
6
  import { targetHasArtifactCollision } from '../utils/target-eligibility.js';
6
7
  import { TuiRouter, } from './router.js';
7
- import { CatalogRefreshCoordinator, } from './catalog-generation.js';
8
8
  function reviewPreparationStageLabel(stage) {
9
9
  return {
10
10
  validate: '验证当前状态',
@@ -75,11 +75,19 @@ function operationLabels(intent) {
75
75
  destructive: false,
76
76
  };
77
77
  }
78
- if (intent.kind === 'catalogActivate') {
78
+ if (intent.kind === 'pluginWrite' && intent.dependencyPlacementOnly === 'desired') {
79
79
  return {
80
- title: '使用 Catalog 更新',
81
- description: '切换当前 Catalog generation;不会自动升级已安装插件,也不会修改设置。',
82
- primaryLabel: '使用已下载的更新',
80
+ title: '调整依赖位置',
81
+ description: '先更新当前项目的 desired placement;不会删除所有项目中的实际内容。',
82
+ primaryLabel: '更新 desired placement',
83
+ destructive: false,
84
+ };
85
+ }
86
+ if (intent.kind === 'pluginWrite' && intent.dependencyPlacementAdjustment) {
87
+ return {
88
+ title: '调整依赖位置',
89
+ description: '按最终依赖位置更新 desired intent,并应用非破坏性变更。',
90
+ primaryLabel: '应用依赖位置',
83
91
  destructive: false,
84
92
  };
85
93
  }
@@ -114,6 +122,7 @@ function operationLabels(intent) {
114
122
  reinstall: ['重新安装插件', '清理 AW 管理的内容后完整重建。', true],
115
123
  uninstall: ['移除插件', '移除 AW 管理的安装内容。配置保持不变。', true],
116
124
  cleanup: ['清理插件', '只移除可证明属于 AW 的现场内容。配置保持不变。', true],
125
+ sync: ['同步插件', '按已保存的 desired intent 修复当前现场。', false],
117
126
  };
118
127
  const [title, description, destructive] = labels[intent.action];
119
128
  return { title, description, primaryLabel: title, destructive };
@@ -136,8 +145,8 @@ function operationLabels(intent) {
136
145
  }
137
146
  if (intent.kind === 'sourceMutation') {
138
147
  const labels = {
139
- add: ['添加来源', '先保存来源配置,再尽力刷新来源。刷新失败时配置仍保留。', '保存并刷新', false],
140
- update: ['修改来源', '先保存来源配置,再尽力刷新来源。刷新失败时配置仍保留。', '保存并刷新', false],
148
+ add: ['添加来源', '只保存来源配置。保存后请在终端运行 source sync。', '保存来源', false],
149
+ update: ['修改来源', '只保存来源配置。保存后请在终端运行 source sync。', '保存来源', false],
141
150
  setPrimary: ['设为主来源', '只改变默认 marketplace,不迁移安装。', '设为主来源', false],
142
151
  remove: ['移除来源', '移除来源配置;不会删除其它来源。', '移除来源', true],
143
152
  };
@@ -177,27 +186,8 @@ function reviewChanges(intent, evidence, dependencyOverview) {
177
186
  description: review.collisions.map((collision) => collision.name).join('、') }] : []),
178
187
  ];
179
188
  }
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
- }
200
189
  if (intent.kind === 'pluginWrite') {
190
+ const desiredIntentKind = requestedIntentAction(intent.action);
201
191
  const runtimeChanges = evidence?.codexManager
202
192
  ? [{
203
193
  kind: evidence.codexManager.state === 'requires-install' ? 'add' : 'keep',
@@ -212,42 +202,123 @@ function reviewChanges(intent, evidence, dependencyOverview) {
212
202
  : `使用兼容的 system Codex ${evidence.codexManager.version}`,
213
203
  }]
214
204
  : [];
205
+ const codexChanges = (evidence?.codexReconcilePlans ?? []).flatMap((codexPlan) => [
206
+ ...codexPlan.sharedNativeActions.map((action) => ({
207
+ kind: action.action === 'reuse' || action.action === 'claim' || action.action === 'none' ? 'keep'
208
+ : action.action === 'install' ? 'add' : 'change',
209
+ group: 'sharedCodexNative',
210
+ pluginId: action.pluginId,
211
+ title: action.action === 'claim'
212
+ ? `${action.codexPluginId}:复用精确现有 shared native(不建立 ledger)`
213
+ : action.codexPluginId,
214
+ description: action.action === 'claim'
215
+ ? '复用已存在的精确 shared native;不建立 ownership ledger'
216
+ : `${action.action} · shared${action.affectsOtherScopes ? ' · 可能影响其他项目' : ''}`,
217
+ })),
218
+ ...codexPlan.activationActions.user.map((action) => ({
219
+ kind: action.action === 'none' ? 'keep' : action.desired ? 'change' : 'remove',
220
+ group: 'userActivation',
221
+ pluginId: action.pluginId,
222
+ title: action.codexPluginId,
223
+ description: `desired=${String(action.desired)} · ${action.action}`,
224
+ })),
225
+ ...codexPlan.activationActions.project.map((action) => ({
226
+ kind: action.action === 'none' ? 'keep' : action.desired ? 'change' : 'remove',
227
+ group: 'projectActivation',
228
+ pluginId: action.pluginId,
229
+ title: action.codexPluginId,
230
+ description: `desired=${String(action.desired)} · ${action.action}`,
231
+ })),
232
+ ...[...codexPlan.extensionWrites, ...codexPlan.extensionRemovals]
233
+ .map((action) => ({
234
+ kind: action.action === 'write' ? 'change' : 'remove',
235
+ group: 'awExtensions',
236
+ pluginId: action.pluginId,
237
+ title: action.componentKey,
238
+ description: `${action.scope} · ${action.action}`,
239
+ })),
240
+ ]);
215
241
  const plan = evidence?.dependencyPlan;
216
- if (plan && (intent.action === 'install' || intent.action === 'upgrade' || intent.action === 'reinstall')) {
242
+ if (plan && (intent.action === 'install' || intent.action === 'upgrade'
243
+ || intent.action === 'reinstall' || intent.action === 'sync')) {
217
244
  const changes = plan.targets.map((target) => {
218
245
  const relation = dependencyRelation(plan, target.pluginId);
219
- const actionKind = target.action === 'keep'
220
- ? 'keep'
221
- : target.action === 'install'
222
- ? 'add'
223
- : 'change';
224
246
  return {
225
- kind: actionKind,
226
- group: relation === 'requested' ? 'requested' : 'dependencies',
247
+ kind: desiredIntentKind,
248
+ group: target.resolvedScope === 'project' ? 'projectIntent' : 'userIntent',
227
249
  pluginId: target.pluginId,
228
250
  relation,
229
251
  title: pluginNameFromId(target.pluginId),
230
- description: `${target.platform === 'codex' ? 'Codex' : 'Claude Code'} · ${target.resolvedScope === 'user' ? '所有项目' : '当前项目'}${target.action === 'keep' ? ' · 已可用' : ''}`,
252
+ description: `${target.platform === 'codex' ? 'Codex' : 'Claude Code'} · ${target.resolvedScope === 'user' ? '所有项目' : '当前项目'}${target.action === 'keep' ? ' · 已可用' : ''}${target.assignmentSource === 'existingWiderTarget' ? ' · 复用健康的所有项目安装' : target.assignmentSource === 'consumerPromotion' ? ' · 随上游依赖一并调整' : ''}`,
231
253
  };
232
254
  });
233
255
  changes.push({
234
256
  kind: 'keep',
235
257
  group: 'preserved',
236
- title: '插件设置和已有依赖保持不变',
237
- description: '移除根插件时不会自动清理依赖。',
258
+ title: intent.dependencyPlacementAdjustment
259
+ ? '根插件和插件设置保持不变'
260
+ : '插件设置和已有依赖保持不变',
261
+ description: intent.dependencyPlacementAdjustment
262
+ ? '这里只调整所列 dependency placement。'
263
+ : '移除根插件时不会自动清理依赖。',
238
264
  });
239
- return [...changes, ...runtimeChanges];
265
+ if (intent.dependencyPlacementOnly === 'desired') {
266
+ changes.push({
267
+ kind: desiredIntentKind,
268
+ group: 'projectIntent',
269
+ title: '先更新依赖 desired placement',
270
+ description: '这一步不删除所有项目中的实际内容。',
271
+ }, {
272
+ kind: 'warning',
273
+ group: 'preserved',
274
+ title: '后续第 1 步:显式 user cleanup;不自动扫描/清理',
275
+ description: '请在已知的所有项目位置确认 cleanup。',
276
+ }, {
277
+ kind: 'keep',
278
+ group: 'preserved',
279
+ title: '后续第 2 步:同步当前项目',
280
+ description: 'cleanup 完成后运行当前项目 sync。',
281
+ });
282
+ return changes;
283
+ }
284
+ return [...changes, ...codexChanges, ...runtimeChanges];
240
285
  }
241
286
  const platforms = intent.platform === 'both'
242
287
  ? ['codex', 'claude']
243
288
  : [intent.platform];
244
289
  const changes = platforms.map((platform) => ({
290
+ kind: desiredIntentKind,
291
+ group: intent.scope === 'project' ? 'projectIntent' : 'userIntent',
292
+ pluginId: intent.pluginId,
293
+ relation: 'requested',
294
+ title: pluginNameFromId(intent.pluginId),
295
+ description: `${platform === 'codex' ? 'Codex' : 'Claude Code'} · ${intent.scope === 'user' ? '所有项目' : '当前项目'}`,
296
+ }));
297
+ changes.push(...platforms.map((platform) => ({
245
298
  kind: intent.action === 'uninstall' || intent.action === 'cleanup' ? 'remove' : intent.action === 'install' ? 'add' : 'change',
299
+ group: 'requested',
300
+ pluginId: intent.pluginId,
301
+ relation: 'requested',
246
302
  title: `${platform === 'codex' ? 'Codex' : 'Claude Code'} · ${intent.scope === 'user' ? '所有项目' : '当前项目'}`,
247
303
  description: intent.action === 'uninstall' || intent.action === 'cleanup'
248
304
  ? '插件设置不会删除。'
249
305
  : '执行前会再次检查当前安装状态。',
250
- }));
306
+ })));
307
+ if (intent.action === 'cleanup' && evidence?.offlineCleanup) {
308
+ const actionLabels = {
309
+ removeOwnedOutput: '移除 AW-owned 输出',
310
+ removeNativeRegistration: '移除原生插件注册',
311
+ removeLifecycleSnapshot: '移除 lifecycle snapshot',
312
+ };
313
+ changes.push(...evidence.offlineCleanup.plan.actions.map((action) => ({
314
+ kind: 'remove',
315
+ group: 'requested',
316
+ pluginId: evidence.offlineCleanup.pluginId,
317
+ relation: 'requested',
318
+ title: actionLabels[action.action],
319
+ description: action.target,
320
+ })));
321
+ }
251
322
  if (intent.action === 'uninstall' || intent.action === 'cleanup') {
252
323
  changes.push(...(dependencyOverview?.dependencies ?? []).map((dependency) => ({
253
324
  kind: 'keep',
@@ -268,7 +339,15 @@ function reviewChanges(intent, evidence, dependencyOverview) {
268
339
  if (intent.originIssue) {
269
340
  changes.push({ kind: 'keep', title: '插件设置保持不变' }, { kind: 'warning', title: '执行前重新检查完整目标和来源证据' });
270
341
  }
271
- return [...changes, ...runtimeChanges];
342
+ if (intent.recoveryDescription) {
343
+ changes.push({
344
+ kind: 'warning',
345
+ group: 'preserved',
346
+ title: `后续:${intent.scope === 'user' ? '当前项目' : '所有项目'} sync`,
347
+ description: intent.recoveryDescription,
348
+ });
349
+ }
350
+ return [...changes, ...codexChanges, ...runtimeChanges];
272
351
  }
273
352
  if (intent.kind === 'configReset') {
274
353
  const layerLabel = intent.scope === 'user'
@@ -371,13 +450,19 @@ function technicalDetails(evidence) {
371
450
  if (evidence.offlineCleanup) {
372
451
  details.push(`离线清理原因:${evidence.offlineCleanup.reason}`);
373
452
  details.push(`离线清理目标:${short(evidence.offlineCleanup.targetFingerprint)}`);
453
+ details.push(`离线清理计划:${evidence.offlineCleanup.plan.actions.length} 个动作 · ${short(evidence.offlineCleanup.planDigest)}`);
454
+ for (const action of evidence.offlineCleanup.plan.actions) {
455
+ details.push(`离线清理动作:${action.action} · ${action.target}`);
456
+ }
374
457
  }
375
458
  if (evidence.dependencyPlan)
376
459
  details.push(`安装计划:${evidence.dependencyPlan.resolvedPluginIds.length} 个插件`);
460
+ if (evidence.projectIntentDigest)
461
+ details.push(`Project intent:${short(evidence.projectIntentDigest)}`);
377
462
  if (evidence.config)
378
463
  details.push(`设置层:${evidence.config.scope}/${evidence.config.projectLayer} · ${short(evidence.config.storeDigest)}`);
379
464
  if (evidence.sourceWrite) {
380
- details.push(`候选配置:${short(evidence.sourceWrite.candidateDigest)}`);
465
+ details.push(`新配置:${short(evidence.sourceWrite.proposedConfigDigest)}`);
381
466
  details.push(`引用清单:${short(evidence.sourceWrite.inventoryDigest)} · ${evidence.sourceWrite.referenceCount} 项`);
382
467
  }
383
468
  if (evidence.portable) {
@@ -422,18 +507,15 @@ export class TuiController {
422
507
  #router = new TuiRouter();
423
508
  #listeners = new Set();
424
509
  #health = new Map();
425
- #sourceTokens = new Map();
426
- #successfulSourceRefreshes = new Map();
427
- #sourceAborts = new Map();
428
510
  #tickets = new Map();
429
511
  #inflightOperations = new Map();
430
512
  #abort = new AbortController();
431
- #catalogRefreshCoordinator;
432
513
  #progressUnsubscribe;
433
514
  #progressFeed;
434
515
  #phase = 'idle';
435
516
  #status;
436
517
  #catalogGenerations = new Map();
518
+ #sessionGenerationKeys = new Map();
437
519
  #viewModel;
438
520
  #progress = { messages: [] };
439
521
  #progressTargets = [];
@@ -447,32 +529,13 @@ export class TuiController {
447
529
  #portableRequestToken = 0;
448
530
  #portableRequestAbort;
449
531
  #portablePending;
532
+ #installPlacementDraft;
533
+ #installPlacementRequestToken = 0;
534
+ #installPlacementRequestAbort;
450
535
  #generation = 0;
451
- #refreshSequence = 0;
452
- #appliedRefreshSequence = 0;
453
536
  #disposed = false;
454
537
  constructor(dependencies, progressFeed) {
455
538
  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
- }
476
539
  this.#progressFeed = progressFeed;
477
540
  if (progressFeed) {
478
541
  this.#progressUnsubscribe = progressFeed.subscribe((progress) => {
@@ -510,6 +573,7 @@ export class TuiController {
510
573
  sourceHealth: [...this.#health.values()],
511
574
  catalogGenerations: [...this.#catalogGenerations.values()]
512
575
  .sort((left, right) => left.sourceId.localeCompare(right.sourceId, 'en')),
576
+ reloadRequiredSourceIds: [],
513
577
  progress: this.#progress,
514
578
  progressTargets: this.#progressTargets,
515
579
  ...(this.#activeReview ? { activeReview: this.#activeReview } : {}),
@@ -517,6 +581,20 @@ export class TuiController {
517
581
  ...(this.#result ? { result: this.#result } : {}),
518
582
  ...(this.#loadError ? { loadError: this.#loadError } : {}),
519
583
  ...(this.#portableDraft ? { portableDraft: this.#portableDraft } : {}),
584
+ ...(this.#installPlacementDraft
585
+ ? { installPlacementDraft: {
586
+ pluginId: this.#installPlacementDraft.pluginId,
587
+ rootScope: this.#installPlacementDraft.rootScope,
588
+ platforms: this.#installPlacementDraft.platforms,
589
+ dependencyScopeOverrides: this.#installPlacementDraft.dependencyScopeOverrides,
590
+ state: this.#installPlacementDraft.state,
591
+ targets: this.#installPlacementDraft.targets,
592
+ canContinue: this.#installPlacementDraft.canContinue,
593
+ ...(this.#installPlacementDraft.error
594
+ ? { error: this.#installPlacementDraft.error }
595
+ : {}),
596
+ } }
597
+ : {}),
520
598
  });
521
599
  }
522
600
  subscribe(listener) {
@@ -567,6 +645,8 @@ export class TuiController {
567
645
  || route.name === 'pluginDependencies'
568
646
  || route.name === 'installPlacement'
569
647
  || route.name === 'installTargets'
648
+ || route.name === 'dependencyPlacement'
649
+ || route.name === 'dependencyPlacementEditor'
570
650
  || route.name === 'pluginDetail'
571
651
  || route.name === 'platform'
572
652
  || route.name === 'pluginAbout'
@@ -580,6 +660,7 @@ export class TuiController {
580
660
  name: 'pluginList',
581
661
  mode: route.name === 'pluginPreview' || route.name === 'pluginDependencies'
582
662
  || route.name === 'installPlacement' || route.name === 'installTargets'
663
+ || route.name === 'dependencyPlacement' || route.name === 'dependencyPlacementEditor'
583
664
  ? 'browse'
584
665
  : 'installed',
585
666
  };
@@ -595,14 +676,6 @@ export class TuiController {
595
676
  && !this.#viewModel?.sourceTabs.some((source) => source.id === route.sourceId)) {
596
677
  return { name: 'sourceList' };
597
678
  }
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
- }
606
679
  return route;
607
680
  }
608
681
  #normalizeCurrentRoute() {
@@ -619,78 +692,25 @@ export class TuiController {
619
692
  this.#catalogGenerations.set(source.sourceId, structuredClone(source));
620
693
  }
621
694
  }
695
+ this.#sessionGenerationKeys.clear();
696
+ if (status.sessionGenerationKeys) {
697
+ for (const [sourceId, generationKey] of Object.entries(status.sessionGenerationKeys)) {
698
+ this.#sessionGenerationKeys.set(sourceId, generationKey);
699
+ }
700
+ }
622
701
  this.#health.clear();
623
702
  for (const source of status.payload.catalog.sources) {
624
703
  this.#health.set(source.id, {
625
704
  sourceId: source.id,
626
705
  state: source.error ? 'unavailable'
627
- : source.type === 'path' ? 'path'
628
- : source.freshness === 'cache' ? 'cache' : 'ready',
706
+ : 'ready',
629
707
  ...(source.error ? { message: '来源当前没有可用数据。' } : {}),
630
708
  });
631
- const successful = this.#successfulSourceRefreshes.get(source.id);
632
- if (successful?.generation === this.#generation
633
- && successful.sourceIdentity === source.identity
634
- && successful.requestedRef === source.requestedRef
635
- && !source.error) {
636
- this.#health.set(source.id, {
637
- sourceId: source.id,
638
- state: source.type === 'path' || source.freshness === 'path' ? 'path' : 'ready',
639
- });
640
- }
641
- }
642
- const sourceIds = new Set(status.payload.catalog.sources.map((source) => source.id));
643
- for (const sourceId of this.#successfulSourceRefreshes.keys()) {
644
- if (!sourceIds.has(sourceId))
645
- this.#successfulSourceRefreshes.delete(sourceId);
646
709
  }
647
710
  this.#project();
648
711
  this.#normalizeCurrentRoute();
649
712
  }
650
- #applyRefreshStatus(status, sourceId, health, refreshSequence) {
651
- if (refreshSequence < this.#appliedRefreshSequence)
652
- return false;
653
- this.#appliedRefreshSequence = refreshSequence;
654
- this.#applyStatus(status);
655
- if (!status.payload.catalog.sources.some((source) => source.id === sourceId))
656
- return true;
657
- this.#health.set(sourceId, health);
658
- this.#project();
659
- return true;
660
- }
661
- #recordSuccessfulSourceRefresh(sourceId, checked, generation) {
662
- const authoritative = this.#status?.payload.catalog.sources.find((source) => source.id === sourceId);
663
- if (!authoritative || authoritative.type !== 'git' || authoritative.error
664
- || authoritative.identity !== checked.sourceIdentity
665
- || authoritative.requestedRef !== checked.requestedRef)
666
- return;
667
- this.#successfulSourceRefreshes.set(sourceId, {
668
- generation,
669
- sourceIdentity: checked.sourceIdentity,
670
- requestedRef: checked.requestedRef,
671
- });
672
- this.#health.set(sourceId, {
673
- sourceId,
674
- state: 'ready',
675
- });
676
- }
677
- #cancelSourceRefreshes() {
678
- for (const abort of this.#sourceAborts.values())
679
- abort.abort();
680
- this.#sourceAborts.clear();
681
- }
682
- #startGitSourceRefreshes() {
683
- if (this.#catalogRefreshCoordinator) {
684
- this.#catalogRefreshCoordinator.start([...this.#catalogGenerations.values()]);
685
- return;
686
- }
687
- for (const source of this.#status?.payload.catalog.sources ?? []) {
688
- if (source.type === 'git')
689
- void this.refreshSource(source.id).catch(() => undefined);
690
- }
691
- }
692
713
  async load() {
693
- this.#cancelSourceRefreshes();
694
714
  this.dependencies.beginCatalogSession?.();
695
715
  const generation = ++this.#generation;
696
716
  this.#phase = 'loading';
@@ -715,125 +735,11 @@ export class TuiController {
715
735
  this.#phase = 'ready';
716
736
  this.#loadError = undefined;
717
737
  this.#publish();
718
- this.#startGitSourceRefreshes();
719
- }
720
- async refreshSource(sourceId) {
721
- if (this.#disposed || !this.#status)
722
- return;
723
- if (this.#catalogRefreshCoordinator) {
724
- await this.#catalogRefreshCoordinator.refresh(sourceId, this.#catalogGenerations.get(sourceId)?.refreshState ?? 'idle');
725
- return;
726
- }
727
- this.#sourceAborts.get(sourceId)?.abort();
728
- const sourceAbort = new AbortController();
729
- this.#sourceAborts.set(sourceId, sourceAbort);
730
- const finishRefresh = () => {
731
- if (this.#sourceAborts.get(sourceId) === sourceAbort)
732
- this.#sourceAborts.delete(sourceId);
733
- };
734
- const signal = AbortSignal.any([this.#abort.signal, sourceAbort.signal]);
735
- const token = (this.#sourceTokens.get(sourceId) ?? 0) + 1;
736
- this.#sourceTokens.set(sourceId, token);
737
- const generation = this.#generation;
738
- const current = this.#status.payload.catalog.sources.find((source) => source.id === sourceId);
739
- if (!current) {
740
- finishRefresh();
741
- return;
742
- }
743
- const hadCache = current.type === 'git' && current.error === undefined
744
- && (current.freshness === 'cache' || current.freshness === 'remote');
745
- this.#health.set(sourceId, { sourceId, state: 'checking' });
746
- this.#project();
747
- this.#publish();
748
- let checkError;
749
- let checked;
750
- try {
751
- checked = await this.dependencies.checkSource(sourceId, signal);
752
- }
753
- catch (error) {
754
- checkError = error;
755
- }
756
- if (this.#disposed || signal.aborted || generation !== this.#generation
757
- || token !== this.#sourceTokens.get(sourceId)) {
758
- finishRefresh();
759
- return;
760
- }
761
- if (checkError)
762
- this.#successfulSourceRefreshes.delete(sourceId);
763
- const refreshSequence = ++this.#refreshSequence;
764
- let status;
765
- let authoritativeReloadSucceeded = true;
766
- try {
767
- status = await this.dependencies.loadStatus(signal);
768
- }
769
- catch (error) {
770
- authoritativeReloadSucceeded = false;
771
- if (!checkError)
772
- checkError = error;
773
- this.#successfulSourceRefreshes.delete(sourceId);
774
- status = structuredClone(this.#status);
775
- }
776
- if (this.#disposed || signal.aborted || generation !== this.#generation
777
- || token !== this.#sourceTokens.get(sourceId)) {
778
- finishRefresh();
779
- return;
780
- }
781
- if (checkError) {
782
- await this.#logErrorSafe(checkError, 'catalogSourceRefreshFailed', {
783
- privateConfigOperation: true,
784
- });
785
- }
786
- const refreshed = status.payload.catalog.sources.find((source) => source.id === sourceId);
787
- const refreshedMatchesCheck = checked !== undefined && refreshed !== undefined
788
- && refreshed.type === 'git'
789
- && !refreshed.error
790
- && refreshed.identity === checked.sourceIdentity
791
- && refreshed.requestedRef === checked.requestedRef;
792
- const state = checkError
793
- ? authoritativeReloadSucceeded
794
- ? !refreshed || refreshed.error
795
- ? 'unavailable'
796
- : refreshed.type === 'path' || refreshed.freshness === 'path'
797
- ? 'path'
798
- : refreshed.freshness === 'cache' ? 'degraded' : 'unavailable'
799
- : hadCache ? 'degraded' : 'unavailable'
800
- : !refreshed
801
- ? 'unavailable'
802
- : refreshed.error
803
- ? 'unavailable'
804
- : !refreshedMatchesCheck
805
- ? refreshed.type === 'path' || refreshed.freshness === 'path'
806
- ? 'path'
807
- : refreshed.freshness === 'cache' ? 'cache' : 'unavailable'
808
- : refreshed.type === 'path' || refreshed.freshness === 'path'
809
- ? 'path'
810
- : 'ready';
811
- this.#applyRefreshStatus(status, sourceId, {
812
- sourceId,
813
- state,
814
- ...(state === 'degraded' || state === 'unavailable' ? {
815
- issueId: `source:${sourceId}:${state === 'degraded' ? 'cache' : 'unavailable'}`,
816
- message: checkError
817
- ? state === 'degraded'
818
- ? '远端检查失败。当前仍使用已验证的缓存数据。'
819
- : '远端检查失败,并且没有可用缓存数据。'
820
- : state === 'degraded'
821
- ? '远端检查后仍只能使用缓存数据。'
822
- : '来源检查后没有可用数据。',
823
- } : {}),
824
- }, refreshSequence);
825
- if (!checkError && refreshedMatchesCheck && checked) {
826
- this.#recordSuccessfulSourceRefresh(sourceId, checked, generation);
827
- }
828
- finishRefresh();
829
- if (generation !== this.#generation)
830
- return;
831
- this.#project();
832
- this.#publish();
833
738
  }
834
739
  push(route, context) {
835
740
  this.#cancelReviewPreparation();
836
741
  this.#cancelPortableRequest();
742
+ this.#cancelInstallPlacementRequest();
837
743
  this.#discardPortableDraftForRoute(route);
838
744
  this.#router.push(route, context);
839
745
  this.#publish();
@@ -1004,6 +910,7 @@ export class TuiController {
1004
910
  replace(route, context) {
1005
911
  this.#cancelReviewPreparation();
1006
912
  this.#cancelPortableRequest();
913
+ this.#cancelInstallPlacementRequest();
1007
914
  const current = this.#router.snapshot.current.route;
1008
915
  if (current.name === 'portableBlocker' && current.code === 'requiredConfigMissing'
1009
916
  && route.name === 'configLayer' && route.pluginId === current.pluginId) {
@@ -1031,10 +938,9 @@ export class TuiController {
1031
938
  }
1032
939
  back() {
1033
940
  const route = this.#router.snapshot.current.route;
1034
- if (route.name !== 'catalogActivationBlocked' || route.reason !== 'reviewPreparation') {
1035
- this.#cancelReviewPreparation();
1036
- }
941
+ this.#cancelReviewPreparation();
1037
942
  this.#cancelPortableRequest();
943
+ this.#cancelInstallPlacementRequest();
1038
944
  if (route.name === 'review' && this.#activeReview?.operationId === route.operationId) {
1039
945
  this.#tickets.delete(this.#activeReview.reviewToken);
1040
946
  this.#activeReview = undefined;
@@ -1148,6 +1054,213 @@ export class TuiController {
1148
1054
  throw new DOMException('Portable request was cancelled.', 'AbortError');
1149
1055
  }
1150
1056
  }
1057
+ beginInstallFlow(pluginId, rootScope, options = {}) {
1058
+ this.#cancelInstallPlacementRequest();
1059
+ this.#installPlacementDraft = {
1060
+ pluginId,
1061
+ rootScope,
1062
+ platforms: [],
1063
+ dependencyScopeOverrides: [],
1064
+ state: 'selecting',
1065
+ targets: [],
1066
+ canContinue: false,
1067
+ adjustingExisting: options.adjustingExisting === true,
1068
+ revalidateOverrides: false,
1069
+ };
1070
+ this.#publish();
1071
+ }
1072
+ setInstallRootScope(rootScope) {
1073
+ const draft = this.#installPlacementDraft;
1074
+ if (!draft)
1075
+ return;
1076
+ this.#cancelInstallPlacementRequest();
1077
+ draft.rootScope = rootScope;
1078
+ draft.platforms = [];
1079
+ draft.revalidateOverrides = true;
1080
+ draft.plan = undefined;
1081
+ draft.targets = [];
1082
+ draft.state = 'selecting';
1083
+ draft.canContinue = false;
1084
+ draft.error = undefined;
1085
+ this.#publish();
1086
+ }
1087
+ setInstallPlatforms(platforms) {
1088
+ const draft = this.#installPlacementDraft;
1089
+ if (!draft)
1090
+ return;
1091
+ this.#cancelInstallPlacementRequest();
1092
+ const selected = [...new Set(platforms)].sort();
1093
+ draft.platforms = selected;
1094
+ draft.dependencyScopeOverrides = draft.dependencyScopeOverrides.filter((override) => selected.includes(override.platform));
1095
+ draft.revalidateOverrides = true;
1096
+ draft.plan = undefined;
1097
+ draft.targets = [];
1098
+ draft.state = 'selecting';
1099
+ draft.canContinue = selected.length > 0;
1100
+ draft.error = undefined;
1101
+ this.#publish();
1102
+ }
1103
+ #cancelInstallPlacementRequest() {
1104
+ const cancelled = this.#installPlacementRequestAbort !== undefined;
1105
+ this.#installPlacementRequestToken += 1;
1106
+ this.#installPlacementRequestAbort?.abort();
1107
+ this.#installPlacementRequestAbort = undefined;
1108
+ if (cancelled && this.#installPlacementDraft?.state === 'loading') {
1109
+ this.#installPlacementDraft.plan = undefined;
1110
+ this.#installPlacementDraft.targets = [];
1111
+ this.#installPlacementDraft.state = 'selecting';
1112
+ this.#installPlacementDraft.canContinue = false;
1113
+ this.#installPlacementDraft.error = undefined;
1114
+ }
1115
+ }
1116
+ #installPlacementIntent() {
1117
+ const draft = this.#installPlacementDraft;
1118
+ if (!draft || draft.platforms.length === 0) {
1119
+ throw new TuiOperationBlockedError('installPlacementIncomplete', '请先明确选择目标平台。');
1120
+ }
1121
+ return {
1122
+ kind: 'pluginWrite',
1123
+ action: 'install',
1124
+ pluginId: draft.pluginId,
1125
+ platform: draft.platforms.length === 2 ? 'both' : draft.platforms[0],
1126
+ scope: draft.rootScope,
1127
+ ...(draft.dependencyScopeOverrides.length > 0
1128
+ ? { dependencyScopeOverrides: draft.dependencyScopeOverrides }
1129
+ : {}),
1130
+ ...(draft.adjustingExisting ? { dependencyPlacementAdjustment: true } : {}),
1131
+ };
1132
+ }
1133
+ async prepareInstallDependencyPlacement() {
1134
+ const draft = this.#installPlacementDraft;
1135
+ if (!draft)
1136
+ throw new TuiOperationBlockedError('installPlacementMissing', '安装位置草稿不存在。');
1137
+ this.#cancelInstallPlacementRequest();
1138
+ const abort = new AbortController();
1139
+ this.#installPlacementRequestAbort = abort;
1140
+ const token = this.#installPlacementRequestToken;
1141
+ draft.state = 'loading';
1142
+ draft.canContinue = false;
1143
+ draft.error = undefined;
1144
+ this.#publish();
1145
+ try {
1146
+ const signal = AbortSignal.any([this.#abort.signal, abort.signal]);
1147
+ const preview = async (intent) => this.dependencies.previewDependencyPlacement
1148
+ ? this.dependencies.previewDependencyPlacement(structuredClone(intent), signal)
1149
+ : (await this.dependencies.captureEvidence(structuredClone(intent), signal)).dependencyPlan;
1150
+ const throwIfCancelled = () => {
1151
+ if (this.#disposed || token !== this.#installPlacementRequestToken || abort.signal.aborted) {
1152
+ throw new DOMException('Dependency placement preview was cancelled.', 'AbortError');
1153
+ }
1154
+ };
1155
+ const filterOverrides = (plan) => {
1156
+ const before = draft.dependencyScopeOverrides;
1157
+ const filtered = before.filter((override) => {
1158
+ const target = plan?.targets.find((candidate) => candidate.relation === 'dependency'
1159
+ && candidate.pluginId === override.pluginId
1160
+ && candidate.platform === override.platform);
1161
+ return Boolean(target
1162
+ && target.allowedScopes.includes(override.scope)
1163
+ && !(target.minimumScope === 'user' && override.scope === 'project'));
1164
+ });
1165
+ if (filtered.length === before.length)
1166
+ return false;
1167
+ draft.dependencyScopeOverrides = filtered;
1168
+ return true;
1169
+ };
1170
+ let plan;
1171
+ if (draft.revalidateOverrides && draft.dependencyScopeOverrides.length > 0) {
1172
+ const baselineIntent = this.#installPlacementIntent();
1173
+ delete baselineIntent.dependencyScopeOverrides;
1174
+ const baselinePlan = await preview(baselineIntent);
1175
+ throwIfCancelled();
1176
+ filterOverrides(baselinePlan);
1177
+ plan = draft.dependencyScopeOverrides.length > 0
1178
+ ? await preview(this.#installPlacementIntent())
1179
+ : baselinePlan;
1180
+ throwIfCancelled();
1181
+ if (filterOverrides(plan)) {
1182
+ plan = await preview(this.#installPlacementIntent());
1183
+ throwIfCancelled();
1184
+ }
1185
+ }
1186
+ else {
1187
+ plan = await preview(this.#installPlacementIntent());
1188
+ }
1189
+ throwIfCancelled();
1190
+ draft.revalidateOverrides = false;
1191
+ if (!plan) {
1192
+ draft.state = 'ready';
1193
+ draft.plan = undefined;
1194
+ draft.targets = [];
1195
+ draft.canContinue = true;
1196
+ this.#publish();
1197
+ return false;
1198
+ }
1199
+ draft.plan = structuredClone(plan);
1200
+ draft.targets = dependencyPlacementTargets(plan);
1201
+ const blocker = plan.targets.find((target) => target.action === 'block');
1202
+ draft.state = blocker ? 'blocked' : 'ready';
1203
+ draft.canContinue = !blocker;
1204
+ draft.error = blocker
1205
+ ? {
1206
+ code: blocker.errorCode ?? 'dependencyPlacementBlocked',
1207
+ message: '依赖位置不能满足所选根插件和目标平台。',
1208
+ }
1209
+ : undefined;
1210
+ this.#publish();
1211
+ return draft.targets.some((target) => !target.locked);
1212
+ }
1213
+ catch (error) {
1214
+ if (error instanceof Error && error.name === 'AbortError')
1215
+ throw error;
1216
+ const code = issueCode(error);
1217
+ draft.state = 'blocked';
1218
+ draft.canContinue = false;
1219
+ draft.error = { code, message: '无法计算可信的依赖位置。请返回检查选择。' };
1220
+ this.#publish();
1221
+ throw error;
1222
+ }
1223
+ finally {
1224
+ if (token === this.#installPlacementRequestToken) {
1225
+ this.#installPlacementRequestAbort = undefined;
1226
+ }
1227
+ }
1228
+ }
1229
+ async setDependencyScope(pluginId, platform, scope) {
1230
+ const draft = this.#installPlacementDraft;
1231
+ const target = draft?.targets.find((entry) => entry.pluginId === pluginId && entry.platform === platform);
1232
+ if (!draft || !target || target.locked || !target.allowedScopes.includes(scope)
1233
+ || target.minimumScope === 'user' && scope === 'project') {
1234
+ throw new TuiOperationBlockedError('dependencyScopeOverrideInvalid', '这个依赖不支持所选位置。');
1235
+ }
1236
+ draft.dependencyScopeOverrides = [
1237
+ ...draft.dependencyScopeOverrides.filter((override) => override.pluginId !== pluginId || override.platform !== platform),
1238
+ { pluginId, platform, scope },
1239
+ ].sort((left, right) => `${left.pluginId}\0${left.platform}`
1240
+ .localeCompare(`${right.pluginId}\0${right.platform}`, 'en'));
1241
+ draft.revalidateOverrides = false;
1242
+ await this.prepareInstallDependencyPlacement();
1243
+ }
1244
+ async resetDependencyScopes() {
1245
+ const draft = this.#installPlacementDraft;
1246
+ if (!draft)
1247
+ return;
1248
+ draft.dependencyScopeOverrides = [];
1249
+ draft.revalidateOverrides = false;
1250
+ await this.prepareInstallDependencyPlacement();
1251
+ }
1252
+ installPlacementIntent() {
1253
+ const intent = this.#installPlacementIntent();
1254
+ const narrowsUserTarget = this.#installPlacementDraft?.plan?.targets.some((target) => {
1255
+ if (target.relation !== 'dependency' || target.resolvedScope !== 'project')
1256
+ return false;
1257
+ const persistedScopes = new Set(this.#status?.payload.targets.flatMap((candidate) => candidate.pluginId === target.pluginId && candidate.platform === target.platform
1258
+ ? candidate.desired?.scopes ?? []
1259
+ : []) ?? []);
1260
+ return persistedScopes.has('user') && !persistedScopes.has('project');
1261
+ });
1262
+ return narrowsUserTarget ? { ...intent, dependencyPlacementOnly: 'desired' } : intent;
1263
+ }
1151
1264
  listPlugins(options) {
1152
1265
  const query = options.query?.trim().toLocaleLowerCase('en');
1153
1266
  return cloneFrozen((this.#viewModel?.plugins ?? []).filter((plugin) => (!options.sourceId || plugin.sourceId === options.sourceId)
@@ -1192,62 +1305,47 @@ export class TuiController {
1192
1305
  }));
1193
1306
  }
1194
1307
  return plugin.supportedPlatforms.map((platform) => {
1195
- const statusTarget = this.#status?.payload.targets.find((target) => (target.pluginId === pluginId && target.platform === platform && target.scope === scope));
1308
+ const platformTargets = this.#status?.payload.targets.filter((target) => target.pluginId === pluginId && target.platform === platform) ?? [];
1309
+ const rootSummary = this.#status?.pluginSummaries.find((summary) => summary.pluginId === pluginId);
1310
+ const rootSupportedScopes = rootSummary?.supportedTargets?.[platform]
1311
+ ?? platformTargets.filter((target) => !target.errors.some((error) => error.code === 'unsupportedTarget')).map((target) => target.scope);
1312
+ if (!rootSupportedScopes.includes(scope)) {
1313
+ return { platform, compatible: false, disabled: '不支持所选位置' };
1314
+ }
1315
+ const statusTarget = platformTargets.find((target) => target.scope === scope)
1316
+ ?? (platform === 'codex' ? platformTargets[0] : undefined);
1196
1317
  if (statusTarget && targetHasArtifactCollision(statusTarget)) {
1197
1318
  return { platform, compatible: false, disabled: '存在输出冲突;请先移除或迁移占用内容' };
1198
1319
  }
1199
- const placement = plugin.installedPlacements.find((entry) => entry.platform === platform);
1200
- if (!placement)
1201
- return { platform, compatible: false, disabled: '当前没有可信的平台能力信息' };
1202
- if (placement.state === 'scopeConflict') {
1203
- return { platform, compatible: false, disabled: '安装位置冲突;请先处理问题' };
1204
- }
1205
- if (placement.state === 'installed') {
1206
- return {
1207
- platform,
1208
- compatible: false,
1209
- disabled: `已安装到${placement.scope === 'user' ? '所有项目' : '当前项目'};请先移除现有安装`,
1210
- };
1211
- }
1212
- if (!placement.supportedScopes.includes(scope)) {
1213
- return { platform, compatible: false, disabled: '不支持所选位置' };
1214
- }
1215
1320
  const overview = this.#viewModel?.dependencyOverviews.find((entry) => entry.pluginId === pluginId);
1216
1321
  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)) {
1322
+ const dependencySummary = this.#status?.pluginSummaries.find((summary) => summary.pluginId === dependency.pluginId);
1323
+ const allowedScopes = dependencySummary?.supportedTargets?.[platform]
1324
+ ?? (this.#status?.payload.targets.filter((target) => target.pluginId === dependency.pluginId
1325
+ && target.platform === platform
1326
+ && !target.errors.some((error) => error.code === 'unsupportedTarget'))
1327
+ .map((target) => target.scope) ?? []);
1328
+ const viableScopes = allowedScopes.filter((candidateScope) => scope === 'project' || candidateScope === 'user');
1329
+ if (viableScopes.length === 0) {
1221
1330
  return {
1222
1331
  platform,
1223
1332
  compatible: false,
1224
- disabled: `${dependency.displayName} 存在输出冲突`,
1333
+ disabled: `${dependency.displayName} 不支持${platform === 'codex' ? 'Codex' : 'Claude Code'}或所选位置`,
1225
1334
  };
1226
1335
  }
1227
- const dependentPlugin = this.#viewModel?.plugins.find((entry) => entry.pluginId === dependency.pluginId);
1228
- if (!dependentPlugin || !dependentPlugin.supportedPlatforms.includes(platform)) {
1336
+ const collisionAtEveryViableScope = viableScopes.every((candidateScope) => {
1337
+ const target = this.#status?.payload.targets.find((entry) => entry.pluginId === dependency.pluginId
1338
+ && entry.platform === platform
1339
+ && entry.scope === candidateScope);
1340
+ return Boolean(target && targetHasArtifactCollision(target));
1341
+ });
1342
+ if (collisionAtEveryViableScope) {
1229
1343
  return {
1230
1344
  platform,
1231
1345
  compatible: false,
1232
- disabled: `${dependency.displayName} 不支持${platform === 'codex' ? 'Codex' : 'Claude Code'}`,
1346
+ disabled: `${dependency.displayName} 存在输出冲突`,
1233
1347
  };
1234
1348
  }
1235
- const dependencyPlacement = dependentPlugin.installedPlacements.find((entry) => entry.platform === platform);
1236
- if (dependencyPlacement?.state === 'scopeConflict') {
1237
- return { platform, compatible: false, disabled: `${dependency.displayName} 的安装位置冲突` };
1238
- }
1239
- if (dependencyPlacement?.state === 'installed') {
1240
- if (dependencyPlacement.health !== 'ready') {
1241
- return { platform, compatible: false, disabled: `${dependency.displayName} 需要先处理` };
1242
- }
1243
- if (scope === 'user' && dependencyPlacement.scope === 'project') {
1244
- return { platform, compatible: false, disabled: `${dependency.displayName} 只在当前项目可用` };
1245
- }
1246
- }
1247
- if (dependencyPlacement?.state === 'notInstalled'
1248
- && !dependencyPlacement.supportedScopes.includes(scope)) {
1249
- return { platform, compatible: false, disabled: `${dependency.displayName} 不支持所选位置` };
1250
- }
1251
1349
  }
1252
1350
  return { platform, compatible: true };
1253
1351
  });
@@ -1267,11 +1365,6 @@ export class TuiController {
1267
1365
  health: this.#health.get(sourceId),
1268
1366
  });
1269
1367
  }
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
1368
  getCatalogGeneration(sourceId) {
1276
1369
  const source = this.#catalogGenerations.get(sourceId);
1277
1370
  return source ? cloneFrozen(source) : undefined;
@@ -1279,76 +1372,32 @@ export class TuiController {
1279
1372
  getSessionCatalogVersion(sourceId) {
1280
1373
  return this.#status?.payload.catalog.sources.find((source) => source.id === sourceId)?.catalogVersion;
1281
1374
  }
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
- });
1375
+ async reloadCatalog(options) {
1376
+ this.dependencies.beginCatalogSession?.();
1377
+ const generation = ++this.#generation;
1378
+ this.#phase = 'loading';
1379
+ this.#publish();
1380
+ try {
1381
+ const status = await this.dependencies.loadStatus(this.#abort.signal);
1382
+ if (this.#disposed || generation !== this.#generation)
1383
+ return false;
1384
+ this.#applyStatus(status);
1385
+ this.#phase = 'ready';
1386
+ this.#loadError = undefined;
1387
+ this.#router.replace({ name: 'sourceDetail', sourceId: options.sourceId });
1328
1388
  this.#publish();
1329
- return undefined;
1389
+ return true;
1330
1390
  }
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 自动刷新设置不可用。');
1391
+ catch (error) {
1392
+ if (this.#disposed || generation !== this.#generation)
1393
+ return false;
1394
+ const code = issueCode(error);
1395
+ await this.#logErrorSafe(error, code);
1396
+ this.#phase = 'error';
1397
+ this.#loadError = { code, message: '无法安全重新加载 Catalog。请检查诊断后重试。' };
1398
+ this.#publish();
1399
+ return false;
1343
1400
  }
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
1401
  }
1353
1402
  async inspectSourceRemoval(sourceId) {
1354
1403
  if (!this.dependencies.inspectSourceRemoval) {
@@ -1467,8 +1516,7 @@ export class TuiController {
1467
1516
  return Boolean(preparation && preparation.token === token
1468
1517
  && !preparation.abort.signal.aborted
1469
1518
  && !this.#disposed
1470
- && (route.name === 'reviewPreparing' && route.preparationId === preparation.snapshot.id
1471
- || route.name === 'catalogActivationBlocked' && route.reason === 'reviewPreparation'));
1519
+ && route.name === 'reviewPreparing' && route.preparationId === preparation.snapshot.id);
1472
1520
  }
1473
1521
  async #runReviewPreparation(token) {
1474
1522
  const preparation = this.#reviewPreparation;
@@ -1563,6 +1611,7 @@ export class TuiController {
1563
1611
  this.#pruneReviewTickets(now);
1564
1612
  const expiresAt = now
1565
1613
  + (this.dependencies.reviewTtlMs ?? 2 * 60_000);
1614
+ const requiresConfirmation = (evidence.codexReconcilePlans ?? []).some((plan) => plan.review.requiresConfirmation);
1566
1615
  const pluginId = 'pluginId' in intent ? intent.pluginId : intent.sourceId;
1567
1616
  if ('pluginId' in intent)
1568
1617
  sourceIdFromPluginId(intent.pluginId);
@@ -1600,13 +1649,20 @@ export class TuiController {
1600
1649
  : {}),
1601
1650
  changes: reviewChanges(intent, evidence, 'pluginId' in intent ? this.getPluginDependencyOverview(intent.pluginId) : undefined),
1602
1651
  technicalDetails: technicalDetails(evidence),
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' : ''}` }
1652
+ ...(intent.kind === 'pluginWrite' && intent.dependencyPlacementOnly !== 'desired'
1653
+ ? { equivalentCommand: `aw ${intent.action} ${intent.pluginId} --platform ${intent.platform} --scope ${intent.scope}${(intent.dependencyScopeOverrides ?? []).map((override) => ` --dependency-scope ${override.pluginId}#${override.platform}=${override.scope}`).join('')}${intent.action === 'cleanup' && intent.force === true ? ' --force' : ''}${evidence.codexManager?.state === 'requires-install' ? ' --allow-managed-codex-install' : ''}${requiresConfirmation ? ' --yes' : ''}` }
1605
1654
  : {}),
1606
1655
  primaryLabel: intent.kind === 'pluginWrite' && evidence.dependencyPlan
1607
1656
  ? `${labels.primaryLabel} ${evidence.dependencyPlan.resolvedPluginIds.length} 个插件`
1608
1657
  : labels.primaryLabel,
1609
1658
  ...(labels.destructive ? { destructive: true } : {}),
1659
+ requiresConfirmation,
1660
+ sharedImpact: {
1661
+ pluginIds: [...new Set((evidence.codexReconcilePlans ?? []).flatMap((plan) => plan.review.sharedImpact.pluginIds))],
1662
+ ...((evidence.codexReconcilePlans ?? []).find((plan) => plan.review.sharedImpact.warning)?.review.sharedImpact.warning
1663
+ ? { warning: (evidence.codexReconcilePlans ?? []).find((plan) => plan.review.sharedImpact.warning).review.sharedImpact.warning }
1664
+ : {}),
1665
+ },
1610
1666
  };
1611
1667
  this.#tickets.set(reviewToken, {
1612
1668
  intent: structuredClone(intent),
@@ -1683,10 +1739,6 @@ export class TuiController {
1683
1739
  await this.#logOperationError(error, code, ticket.intent);
1684
1740
  if (!this.#operationIsCurrent(generation))
1685
1741
  return this.#detachedBlocked(operationId, ticket.intent);
1686
- if (code === 'operationReviewStale' && ticket.intent.kind === 'catalogActivate') {
1687
- return this.#rebuildStaleReview(ticket, operationId, undefined, generation);
1688
- }
1689
- this.#cancelSourceRefreshes();
1690
1742
  generation = ++this.#generation;
1691
1743
  try {
1692
1744
  const refreshed = await this.dependencies.loadStatus(this.#abort.signal);
@@ -1719,9 +1771,6 @@ export class TuiController {
1719
1771
  await this.#logOperationError(error, code, ticket.intent);
1720
1772
  if (!this.#operationIsCurrent(generation))
1721
1773
  return this.#detachedBlocked(operationId, ticket.intent);
1722
- if (code === 'operationReviewStale' && ticket.intent.kind === 'catalogActivate') {
1723
- return this.#rebuildStaleReview(ticket, operationId, undefined, generation);
1724
- }
1725
1774
  return this.#setBlocked(operationId, code, { intent: ticket.intent, returnRoute: ticket.returnRoute });
1726
1775
  }
1727
1776
  if (!this.#operationIsCurrent(generation))
@@ -1731,7 +1780,6 @@ export class TuiController {
1731
1780
  }
1732
1781
  }
1733
1782
  if (ticket.intent.kind === 'sourceMutation') {
1734
- this.#cancelSourceRefreshes();
1735
1783
  generation = ++this.#generation;
1736
1784
  }
1737
1785
  try {
@@ -1744,16 +1792,13 @@ export class TuiController {
1744
1792
  });
1745
1793
  if (!this.#operationIsCurrent(generation))
1746
1794
  return this.#detachedBlocked(operationId, ticket.intent);
1747
- this.#cancelSourceRefreshes();
1748
1795
  generation = ++this.#generation;
1749
- if (ticket.intent.kind === 'sourceMutation' || ticket.intent.kind === 'catalogActivate') {
1750
- this.dependencies.beginCatalogSession?.(ticket.intent.kind === 'catalogActivate'
1751
- ? { bootstrapMissingActive: false }
1752
- : undefined);
1796
+ if (ticket.intent.kind === 'sourceMutation') {
1797
+ this.dependencies.beginCatalogSession?.({
1798
+ reuseValidatedGenerations: true,
1799
+ });
1753
1800
  try {
1754
- this.#progressFeed?.setCurrent({ target: ticket.intent.kind === 'catalogActivate' ? 'Catalog generation' : 'Catalog 来源配置', phase: '读取最终状态' }, ticket.intent.kind === 'catalogActivate'
1755
- ? 'Catalog 激活:重新加载当前会话'
1756
- : '来源操作:读取最终状态');
1801
+ this.#progressFeed?.setCurrent({ target: 'Catalog 来源配置', phase: '读取最终状态' }, '来源操作:读取最终状态');
1757
1802
  }
1758
1803
  catch {
1759
1804
  // Progress diagnostics must not replace the completed source write.
@@ -1900,8 +1945,6 @@ export class TuiController {
1900
1945
  this.#activeReview = undefined;
1901
1946
  this.#router.replace({ name: 'result', operationId });
1902
1947
  this.#publish();
1903
- if (ticket.intent.kind !== 'catalogActivate')
1904
- this.#startGitSourceRefreshes();
1905
1948
  return cloneFrozen(result);
1906
1949
  }
1907
1950
  catch (error) {
@@ -1914,7 +1957,6 @@ export class TuiController {
1914
1957
  if (code === 'operationReviewStale') {
1915
1958
  return this.#rebuildStaleReview(ticket, operationId, undefined, generation);
1916
1959
  }
1917
- this.#cancelSourceRefreshes();
1918
1960
  generation = ++this.#generation;
1919
1961
  try {
1920
1962
  const refreshed = await this.dependencies.loadStatus(this.#abort.signal);
@@ -1952,8 +1994,7 @@ export class TuiController {
1952
1994
  });
1953
1995
  }
1954
1996
  async #rebuildStaleReview(ticket, operationId, currentEvidence, generation = this.#generation) {
1955
- this.#cancelSourceRefreshes();
1956
- this.dependencies.beginCatalogSession?.({ bootstrapMissingActive: false });
1997
+ this.dependencies.beginCatalogSession?.();
1957
1998
  generation = ++this.#generation;
1958
1999
  try {
1959
2000
  const refreshed = await this.dependencies.loadStatus(this.#abort.signal);
@@ -1971,14 +2012,7 @@ export class TuiController {
1971
2012
  returnRoute: ticket.returnRoute,
1972
2013
  });
1973
2014
  }
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;
2015
+ const intent = ticket.intent;
1982
2016
  try {
1983
2017
  this.#assertIssueOrigin(intent);
1984
2018
  await this.dependencies.validateIntent?.(structuredClone(intent), this.#abort.signal);
@@ -2153,9 +2187,8 @@ export class TuiController {
2153
2187
  this.#disposed = true;
2154
2188
  this.#cancelReviewPreparation();
2155
2189
  this.#cancelPortableRequest();
2190
+ this.#cancelInstallPlacementRequest();
2156
2191
  this.#abort.abort();
2157
- this.#catalogRefreshCoordinator?.dispose();
2158
- this.#cancelSourceRefreshes();
2159
2192
  this.#progressUnsubscribe?.();
2160
2193
  this.#tickets.clear();
2161
2194
  this.#phase = 'disposed';
@@ -2163,11 +2196,16 @@ export class TuiController {
2163
2196
  }
2164
2197
  }
2165
2198
  function operationResultSummary(intent, execution) {
2166
- if (intent.kind === 'catalogActivate') {
2199
+ if (intent.kind === 'pluginWrite' && intent.dependencyPlacementOnly === 'desired') {
2167
2200
  return {
2168
2201
  code: 'operationSucceeded',
2169
- message: 'Catalog 更新已启用;插件列表和依赖关系已重新加载。',
2170
- messages: ['已安装插件没有自动升级。', '插件设置没有改变。'],
2202
+ message: '依赖 desired placement 已更新。',
2203
+ pluginId: intent.pluginId,
2204
+ action: intent.action,
2205
+ messages: [
2206
+ '第 1 步:AW 不会扫描未知项目或自动 cleanup;请在已知的所有项目位置显式确认 cleanup。',
2207
+ '第 2 步:cleanup 完成后同步当前项目。',
2208
+ ],
2171
2209
  };
2172
2210
  }
2173
2211
  if (intent.kind === 'portableInstall') {
@@ -2273,7 +2311,16 @@ function operationResultSummary(intent, execution) {
2273
2311
  action: intent.action,
2274
2312
  } : {}),
2275
2313
  ...(inferredTargets.length > 0 ? { targets: inferredTargets } : {}),
2276
- ...(sourceWarnings.length > 0 ? { messages: sourceWarnings } : {}),
2314
+ ...(sourceWarnings.length > 0 || intent.kind === 'pluginWrite' && intent.recoveryDescription
2315
+ ? { messages: [
2316
+ ...sourceWarnings,
2317
+ ...(intent.kind === 'pluginWrite' && intent.recoveryDescription
2318
+ ? [
2319
+ intent.recoveryDescription,
2320
+ `后续:${intent.scope === 'user' ? '当前项目' : '所有项目'} sync`,
2321
+ ] : []),
2322
+ ] }
2323
+ : {}),
2277
2324
  };
2278
2325
  }
2279
2326
  //# sourceMappingURL=controller.js.map