@lesliechan721/aw 0.1.0-beta.13 → 0.1.0-beta.14

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 (62) hide show
  1. package/dist/src/commands/catalog-dev-source.d.ts +31 -0
  2. package/dist/src/commands/catalog-dev-source.js +165 -5
  3. package/dist/src/commands/catalog-dev-source.js.map +1 -1
  4. package/dist/src/commands/catalog-generation.d.ts +5 -0
  5. package/dist/src/commands/catalog-generation.js.map +1 -1
  6. package/dist/src/commands/catalog-publication-journal.d.ts +12 -0
  7. package/dist/src/commands/catalog-publication-journal.js +77 -0
  8. package/dist/src/commands/catalog-publication-journal.js.map +1 -1
  9. package/dist/src/commands/catalog-store.d.ts +5 -0
  10. package/dist/src/commands/catalog-store.js +19 -1
  11. package/dist/src/commands/catalog-store.js.map +1 -1
  12. package/dist/src/commands/dependency-operation-plan.d.ts +7 -0
  13. package/dist/src/commands/dependency-operation-plan.js +63 -27
  14. package/dist/src/commands/dependency-operation-plan.js.map +1 -1
  15. package/dist/src/commands/rebuild-command.d.ts +22 -8
  16. package/dist/src/commands/rebuild-command.js +562 -250
  17. package/dist/src/commands/rebuild-command.js.map +1 -1
  18. package/dist/src/commands/reconcile-operation-plan.d.ts +11 -9
  19. package/dist/src/commands/reconcile-operation-plan.js +25 -6
  20. package/dist/src/commands/reconcile-operation-plan.js.map +1 -1
  21. package/dist/src/commands/write-commands.d.ts +2 -1
  22. package/dist/src/commands/write-commands.js +24 -1
  23. package/dist/src/commands/write-commands.js.map +1 -1
  24. package/dist/src/tui/catalog-management-screens.d.ts +45 -0
  25. package/dist/src/tui/catalog-management-screens.js +25 -0
  26. package/dist/src/tui/catalog-management-screens.js.map +1 -0
  27. package/dist/src/tui/catalog-management.d.ts +240 -0
  28. package/dist/src/tui/catalog-management.js +856 -0
  29. package/dist/src/tui/catalog-management.js.map +1 -0
  30. package/dist/src/tui/catalog-sources/session.d.ts +5 -0
  31. package/dist/src/tui/catalog-sources/session.js +49 -1
  32. package/dist/src/tui/catalog-sources/session.js.map +1 -1
  33. package/dist/src/tui/catalog-sources/types.d.ts +11 -0
  34. package/dist/src/tui/console-app-v4.d.ts +1 -1
  35. package/dist/src/tui/console-app-v4.js +2184 -564
  36. package/dist/src/tui/console-app-v4.js.map +1 -1
  37. package/dist/src/tui/controller-runtime.js +609 -13
  38. package/dist/src/tui/controller-runtime.js.map +1 -1
  39. package/dist/src/tui/controller.d.ts +121 -6
  40. package/dist/src/tui/controller.js +667 -17
  41. package/dist/src/tui/controller.js.map +1 -1
  42. package/dist/src/tui/dependency-graph.d.ts +67 -0
  43. package/dist/src/tui/dependency-graph.js +399 -0
  44. package/dist/src/tui/dependency-graph.js.map +1 -0
  45. package/dist/src/tui/lifecycle-action-view.js +4 -2
  46. package/dist/src/tui/lifecycle-action-view.js.map +1 -1
  47. package/dist/src/tui/model.d.ts +37 -0
  48. package/dist/src/tui/model.js +164 -7
  49. package/dist/src/tui/model.js.map +1 -1
  50. package/dist/src/tui/operation-review-planner.js +244 -8
  51. package/dist/src/tui/operation-review-planner.js.map +1 -1
  52. package/dist/src/tui/plugin-write-task.d.ts +66 -0
  53. package/dist/src/tui/plugin-write-task.js +265 -0
  54. package/dist/src/tui/plugin-write-task.js.map +1 -0
  55. package/dist/src/tui/router.d.ts +38 -2
  56. package/dist/src/tui/router.js.map +1 -1
  57. package/dist/src/tui/screen-registry.d.ts +105 -6
  58. package/dist/src/tui/screen-registry.js +31 -4
  59. package/dist/src/tui/screen-registry.js.map +1 -1
  60. package/dist/src/utils/catalog-generation-service.d.ts +4 -0
  61. package/dist/src/utils/catalog-generation-service.js.map +1 -1
  62. package/package.json +3 -3
@@ -1,4 +1,5 @@
1
1
  import crypto from 'node:crypto';
2
+ import { CatalogManagementError, } from './catalog-management.js';
2
3
  import { CatalogGenerationError } from '../utils/catalog-generation-error.js';
3
4
  import { readSafeOperationError } from '../utils/safe-operation-diagnostic.js';
4
5
  import { buildRebuildReceiptJson, requestedIntentAction } from '../output/command-receipt.js';
@@ -13,6 +14,7 @@ import { TuiRouter, } from './router.js';
13
14
  import { normalizeTuiRoute } from './screen-registry.js';
14
15
  import { CatalogSourcesSession } from './catalog-sources/session.js';
15
16
  import { catalogSourceAutoCheckOnlyValue, projectCatalogSourceMutationWarnings, } from './catalog-sources/review.js';
17
+ import { buildPluginWriteTaskCandidates, pluginWriteTaskTargetKey, } from './plugin-write-task.js';
16
18
  export const PLUGIN_WRITE_BATCH_ACTIONS = [
17
19
  'install',
18
20
  'sync',
@@ -70,10 +72,25 @@ function canonicalBatchTargets(targets) {
70
72
  return [...byKey.values()].sort((left, right) => [left.pluginId, left.platform, left.scope].join('\0')
71
73
  .localeCompare([right.pluginId, right.platform, right.scope].join('\0'), 'en'));
72
74
  }
75
+ function canonicalPluginWriteTaskTargets(targets) {
76
+ return [...new Map(targets.map((target) => [pluginWriteTaskTargetKey(target), target])).values()]
77
+ .sort((left, right) => pluginWriteTaskTargetKey(left)
78
+ .localeCompare(pluginWriteTaskTargetKey(right), 'en'));
79
+ }
73
80
  function normalizeOperationIntent(intent) {
74
81
  if (intent.kind === 'configSave' && intent.retryApply && !intent.apply) {
75
82
  return { ...intent, apply: true };
76
83
  }
84
+ if (intent.kind === 'pluginWriteTask') {
85
+ return { ...intent, targets: canonicalPluginWriteTaskTargets(intent.targets) };
86
+ }
87
+ if (intent.kind === 'catalogManagement' && intent.action === 'storePrune') {
88
+ const items = [...new Map(intent.items.map((item) => [
89
+ `${item.category}\0${item.storeKey}`,
90
+ item,
91
+ ])).values()].sort((left, right) => `${left.category}\0${left.storeKey}`.localeCompare(`${right.category}\0${right.storeKey}`, 'en'));
92
+ return { ...intent, items };
93
+ }
77
94
  if (intent.kind !== 'pluginWriteBatch')
78
95
  return intent;
79
96
  if (intent.selection.kind === 'targets') {
@@ -92,6 +109,49 @@ function normalizeOperationIntent(intent) {
92
109
  },
93
110
  };
94
111
  }
112
+ function assertPluginWriteTaskEvidence(intent, evidence) {
113
+ const candidate = evidence;
114
+ const plan = candidate.taskPlan;
115
+ if (!plan || plan.goal !== intent.goal || !Array.isArray(plan.requestedTargets)
116
+ || !Array.isArray(plan.executableTargets) || !Array.isArray(plan.blockedTargets)
117
+ || !Array.isArray(plan.causes) || !Array.isArray(plan.sources)
118
+ || typeof plan.targetFingerprint !== 'string' || typeof plan.planFingerprint !== 'string'
119
+ || JSON.stringify(plan.requestedTargets) !== JSON.stringify(intent.targets)
120
+ || plan.executableTargets.length === 0) {
121
+ throw new TuiOperationBlockedError('taskEvidenceInvalid', '任务确认信息不完整。');
122
+ }
123
+ if (plan.planFingerprint !== operationEvidenceFingerprint(evidence)) {
124
+ throw new TuiOperationBlockedError('taskEvidenceInvalid', '任务确认指纹无效。');
125
+ }
126
+ if (intent.executionPolicy === 'all' && plan.blockedTargets.length > 0) {
127
+ throw new TuiOperationBlockedError('taskHasBlockedTargets', '部分目标需要先处理,不能作为完整任务执行。');
128
+ }
129
+ const requested = new Set(plan.requestedTargets.map(pluginWriteTaskTargetKey));
130
+ const executable = new Set(plan.executableTargets.map(pluginWriteTaskTargetKey));
131
+ const blocked = new Set(plan.blockedTargets.map((target) => pluginWriteTaskTargetKey(target.target)));
132
+ if (executable.size !== plan.executableTargets.length || blocked.size !== plan.blockedTargets.length
133
+ || [...executable].some((key) => !requested.has(key) || blocked.has(key))
134
+ || [...blocked].some((key) => !requested.has(key))
135
+ || [...requested].some((key) => !executable.has(key) && !blocked.has(key))) {
136
+ throw new TuiOperationBlockedError('taskEvidenceInvalid', '任务目标分区无效。');
137
+ }
138
+ const grouped = new Set();
139
+ for (const source of plan.sources) {
140
+ if (!source.sourceId || !source.sourceIdentity || source.requestedTargets.length === 0
141
+ || source.executable !== Boolean(source.preview.operationPlan)
142
+ || source.requestedTargets.some((target) => !executable.has(pluginWriteTaskTargetKey(target))
143
+ || sourceIdFromPluginId(target.pluginId) !== source.sourceId)
144
+ || source.causes.some((cause) => cause.sourceId !== source.sourceId
145
+ || cause.sourceIdentity !== source.sourceIdentity
146
+ || cause.consumers.some((consumer) => !executable.has(pluginWriteTaskTargetKey(consumer))))) {
147
+ throw new TuiOperationBlockedError('taskEvidenceInvalid', '任务来源计划无效。');
148
+ }
149
+ source.requestedTargets.forEach((target) => grouped.add(pluginWriteTaskTargetKey(target)));
150
+ }
151
+ if ([...executable].some((key) => !grouped.has(key))) {
152
+ throw new TuiOperationBlockedError('taskEvidenceInvalid', '任务来源计划缺少可执行目标。');
153
+ }
154
+ }
95
155
  function assertPluginWriteBatchEvidence(evidence) {
96
156
  const candidate = evidence;
97
157
  if (!Array.isArray(candidate.requestedPluginIds) || !Array.isArray(candidate.groups)) {
@@ -195,7 +255,62 @@ export function pluginWriteBatchFailedRequestedPluginIds(execution) {
195
255
  return canonicalPluginIds(pluginWriteBatchFailedRequestedTargets(execution)
196
256
  .map((target) => target.pluginId));
197
257
  }
258
+ export function pluginWriteTaskFailedRequestedTargets(execution) {
259
+ const requested = new Map(execution.taskPlan.requestedTargets.map((target) => [
260
+ pluginWriteTaskTargetKey(target), target,
261
+ ]));
262
+ const failed = new Set();
263
+ const succeeded = new Set();
264
+ for (const source of execution.sourceResults) {
265
+ const sourceRequested = new Set(source.requestedTargets.map(pluginWriteTaskTargetKey));
266
+ if (source.result.targetResults.length === 0 && source.result.overallState !== 'succeeded') {
267
+ sourceRequested.forEach((key) => failed.add(key));
268
+ continue;
269
+ }
270
+ for (const result of source.result.targetResults) {
271
+ const pluginId = result.target.pluginId;
272
+ if (!pluginId)
273
+ continue;
274
+ const exactKey = pluginWriteTaskTargetKey({
275
+ pluginId,
276
+ platform: result.target.platform,
277
+ scope: result.target.scope,
278
+ });
279
+ if (sourceRequested.has(exactKey)) {
280
+ if (result.state === 'failed' || result.action === 'skipped')
281
+ failed.add(exactKey);
282
+ else
283
+ succeeded.add(exactKey);
284
+ }
285
+ if (result.state !== 'failed' && result.action !== 'skipped')
286
+ continue;
287
+ for (const cause of source.requestedTargets.length > 0
288
+ ? execution.taskPlan.causes.filter((cause) => cause.sourceId === source.sourceId) : []) {
289
+ if (pluginWriteTaskTargetKey(cause.repairTarget) !== exactKey)
290
+ continue;
291
+ cause.consumers.forEach((consumer) => failed.add(pluginWriteTaskTargetKey(consumer)));
292
+ }
293
+ const placement = source.result.dependencyPlan?.targets.find((target) => target.pluginId === pluginId && target.platform === result.target.platform
294
+ && (target.requestedScope ?? target.resolvedScope) === result.target.scope);
295
+ for (const consumer of placement?.requiredByTargets ?? []) {
296
+ failed.add(pluginWriteTaskTargetKey(consumer));
297
+ }
298
+ }
299
+ }
300
+ for (const key of succeeded)
301
+ failed.delete(key);
302
+ return canonicalPluginWriteTaskTargets([...failed].flatMap((key) => {
303
+ const target = requested.get(key);
304
+ return target ? [target] : [];
305
+ }));
306
+ }
198
307
  function collectOperationSensitiveValues(intent) {
308
+ if (intent.kind === 'catalogManagement') {
309
+ return [
310
+ ...('ownerUrl' in intent && intent.ownerUrl ? [intent.ownerUrl] : []),
311
+ ...('remoteUrl' in intent && intent.remoteUrl ? [intent.remoteUrl] : []),
312
+ ];
313
+ }
199
314
  if (intent.kind !== 'configSave')
200
315
  return [];
201
316
  const sensitiveKeys = new Set([...(intent.sensitiveKeys ?? []), ...(intent.privateKeys ?? [])]);
@@ -243,6 +358,53 @@ function cloneFrozen(value) {
243
358
  return deepFreeze(structuredClone(value));
244
359
  }
245
360
  export function operationEvidenceFingerprint(evidence) {
361
+ if (evidence.catalogManagement) {
362
+ const management = evidence.catalogManagement;
363
+ return crypto.createHash('sha256')
364
+ .update(JSON.stringify(stableValue(management.action === 'storePrune'
365
+ ? {
366
+ action: management.action,
367
+ workspaceBinding: management.workspaceBinding,
368
+ authorizedStoreItems: management.authorizedStoreItems,
369
+ }
370
+ : {
371
+ action: management.action,
372
+ workspaceBinding: management.workspaceBinding,
373
+ evidenceDigest: management.evidenceDigest,
374
+ })))
375
+ .digest('hex');
376
+ }
377
+ if ('taskPlan' in evidence) {
378
+ const task = evidence.taskPlan;
379
+ return crypto.createHash('sha256')
380
+ .update(JSON.stringify(stableValue({
381
+ catalog: evidence.catalog,
382
+ goal: task.goal,
383
+ requestedTargets: task.requestedTargets,
384
+ executableTargets: task.executableTargets,
385
+ causes: task.causes,
386
+ blockedTargets: task.blockedTargets,
387
+ targetFingerprint: task.targetFingerprint,
388
+ sources: task.sources.map((source) => ({
389
+ sourceId: source.sourceId,
390
+ sourceIdentity: source.sourceIdentity,
391
+ requestedTargets: source.requestedTargets,
392
+ causes: source.causes,
393
+ executable: source.executable,
394
+ planFingerprint: source.preview.operationPlan?.planFingerprint,
395
+ targetResults: source.preview.targetResults.map((target) => ({
396
+ pluginId: target.target.pluginId,
397
+ platform: target.target.platform,
398
+ scope: target.target.scope,
399
+ state: target.state,
400
+ action: target.action,
401
+ errorCode: target.errorCode,
402
+ requiredBy: target.requiredBy,
403
+ })),
404
+ })),
405
+ })))
406
+ .digest('hex');
407
+ }
246
408
  if ('groups' in evidence && 'requestedPluginIds' in evidence) {
247
409
  const batch = evidence;
248
410
  return crypto.createHash('sha256')
@@ -337,6 +499,8 @@ function issueCode(error) {
337
499
  return safeDiagnosticCode(safeOperationError.code, 'operationBlocked');
338
500
  if (error instanceof TuiOperationBlockedError)
339
501
  return error.code;
502
+ if (error instanceof CatalogManagementError)
503
+ return error.code;
340
504
  if (error instanceof CatalogGenerationError)
341
505
  return error.code;
342
506
  if (error instanceof Error && error.name === 'CatalogSourcesConfigConflictError') {
@@ -408,6 +572,7 @@ export class TuiController {
408
572
  #retiring = false;
409
573
  #externalDrafts = [];
410
574
  #batchRetryIntent;
575
+ #taskRetryIntent;
411
576
  #operationHistory = new OperationHistoryStore();
412
577
  #sessionEpoch = 0;
413
578
  #lastRecordedResultId;
@@ -431,6 +596,7 @@ export class TuiController {
431
596
  onError: (error, code) => {
432
597
  void this.#logErrorSafe(error, code);
433
598
  },
599
+ now: this.dependencies.now,
434
600
  });
435
601
  if (progressFeed) {
436
602
  this.#progressUnsubscribe = progressFeed.subscribe((progress) => {
@@ -622,6 +788,7 @@ export class TuiController {
622
788
  this.#installPlacementDraft = undefined;
623
789
  this.#recoverableInstallPlacementDraft = undefined;
624
790
  this.#batchRetryIntent = undefined;
791
+ this.#taskRetryIntent = undefined;
625
792
  this.#externalDrafts = [];
626
793
  this.#result = undefined;
627
794
  this.#resultAnchor = undefined;
@@ -774,6 +941,18 @@ export class TuiController {
774
941
  }
775
942
  #ensureOperationFlow(intent) {
776
943
  const active = this.#router.activeFlow();
944
+ if (intent.kind === 'catalogManagement') {
945
+ if (active?.kind === 'catalogManagement')
946
+ return;
947
+ this.#router.beginFlow('catalogManagement', this.#router.snapshot.current.route);
948
+ return;
949
+ }
950
+ if (intent.kind === 'pluginWriteTask') {
951
+ if (active?.kind === 'pluginWriteTask')
952
+ return;
953
+ this.#router.beginFlow('pluginWriteTask', this.#router.snapshot.current.route);
954
+ return;
955
+ }
777
956
  if (intent.kind === 'pluginWriteBatch') {
778
957
  if (active?.kind === 'batchWrite')
779
958
  return;
@@ -818,7 +997,9 @@ export class TuiController {
818
997
  : intent.kind === 'sourceMutation' && intent.action !== 'remove'
819
998
  ? { name: 'sourceDetail', sourceId: intent.sourceId }
820
999
  : intent.kind === 'catalogGeneration'
821
- ? { name: 'sourceDetail', sourceId: intent.sourceId }
1000
+ ? this.#router.snapshot.current.route.name === 'pluginWriteTask'
1001
+ ? this.#router.snapshot.current.route
1002
+ : { name: 'sourceDetail', sourceId: intent.sourceId }
822
1003
  : { name: 'sourceList' });
823
1004
  }
824
1005
  #stableResultRoute(intent, result) {
@@ -836,6 +1017,8 @@ export class TuiController {
836
1017
  }
837
1018
  if (intent.kind === 'pluginWriteBatch')
838
1019
  return { name: 'pluginList', mode: 'installed' };
1020
+ if (intent.kind === 'pluginWriteTask')
1021
+ return { name: 'pluginList', mode: 'installed' };
839
1022
  if (intent.kind === 'portableInstall' || intent.kind === 'configSave' || intent.kind === 'configReset') {
840
1023
  return this.#normalizeRoute({ name: 'pluginDetail', pluginId: intent.pluginId });
841
1024
  }
@@ -843,8 +1026,22 @@ export class TuiController {
843
1026
  return this.#normalizeRoute({ name: 'sourceDetail', sourceId: intent.sourceId });
844
1027
  }
845
1028
  if (intent.kind === 'catalogGeneration') {
1029
+ if ('returnRoute' in result && result.returnRoute?.name === 'pluginWriteTask') {
1030
+ return this.#normalizeRoute(result.returnRoute);
1031
+ }
846
1032
  return this.#normalizeRoute({ name: 'sourceDetail', sourceId: intent.sourceId });
847
1033
  }
1034
+ if (intent.kind === 'catalogManagement') {
1035
+ if (intent.action === 'devActivate' || intent.action === 'devDeactivate') {
1036
+ return { name: 'catalogDev', step: 'overview' };
1037
+ }
1038
+ if (intent.action === 'storePrune')
1039
+ return { name: 'catalogStore' };
1040
+ if (intent.action === 'publishPlan' || intent.action === 'publishApply') {
1041
+ return { name: 'catalogPublish', step: 'overview' };
1042
+ }
1043
+ return { name: 'catalogWorkspace', view: 'overview' };
1044
+ }
848
1045
  return { name: 'sourceList' };
849
1046
  }
850
1047
  #showResult(result, intent) {
@@ -883,6 +1080,19 @@ export class TuiController {
883
1080
  },
884
1081
  }
885
1082
  : undefined;
1083
+ this.#taskRetryIntent = intent.kind === 'pluginWriteTask'
1084
+ && 'summary' in result
1085
+ && result.summary?.task
1086
+ && (result.summary.task.retryTargets?.length ?? 0) > 0
1087
+ ? {
1088
+ ...structuredClone(intent),
1089
+ targets: result.summary.task.retryTargets.map((target) => {
1090
+ const current = this.listPluginWriteTaskCandidates(intent.goal).find((candidate) => pluginWriteTaskTargetKey(candidate.target) === pluginWriteTaskTargetKey(target));
1091
+ return structuredClone(current?.eligible ? current.target : target);
1092
+ }),
1093
+ executionPolicy: 'all',
1094
+ }
1095
+ : undefined;
886
1096
  if (!this.#router.finishFlow(target, terminal))
887
1097
  this.#router.replace(terminal);
888
1098
  }
@@ -923,14 +1133,18 @@ export class TuiController {
923
1133
  this.#catalogSourcesSession.configure(status.payload.catalog.sources.map((source) => {
924
1134
  const generation = this.#catalogGenerations.get(source.id);
925
1135
  const detail = details.get(source.id);
1136
+ const probeFingerprint = JSON.stringify({
1137
+ sourceIdentity: source.identity,
1138
+ requestedRef: source.requestedRef,
1139
+ });
926
1140
  return {
927
1141
  sourceId: source.id,
928
1142
  primary: source.primary,
929
1143
  autoCheck: this.dependencies.probeCatalogFreshness !== undefined
930
1144
  && (detail?.autoCheck ?? true),
1145
+ probeFingerprint,
931
1146
  fingerprint: JSON.stringify({
932
- sourceIdentity: source.identity,
933
- requestedRef: source.requestedRef,
1147
+ probeFingerprint,
934
1148
  activeStoreKey: generation?.active?.storeKey,
935
1149
  generationState: generation?.state,
936
1150
  generationErrorCode: generation?.errorCode,
@@ -1621,6 +1835,7 @@ export class TuiController {
1621
1835
  return cloneFrozen((this.#viewModel?.plugins ?? []).filter((plugin) => (!options.sourceId || plugin.sourceId === options.sourceId)
1622
1836
  && (options.mode === 'browse' || hasInstalledPlacement(plugin) || plugin.savedConfigScopes.length > 0)
1623
1837
  && (options.installFilter !== 'uninstalled' || !hasInstalledPlacement(plugin))
1838
+ && (options.installFilter !== 'updateAvailable' || plugin.update?.state === 'available')
1624
1839
  && (!query || `${plugin.displayName}\n${plugin.description}\n${plugin.sourceId}`.toLocaleLowerCase('en').includes(query))));
1625
1840
  }
1626
1841
  getPluginOverview(pluginId) {
@@ -1631,6 +1846,9 @@ export class TuiController {
1631
1846
  const overview = this.#viewModel?.dependencyOverviews.find((entry) => entry.pluginId === pluginId);
1632
1847
  return overview ? cloneFrozen(overview) : undefined;
1633
1848
  }
1849
+ listPluginDependencyOverviews() {
1850
+ return cloneFrozen(this.#viewModel?.dependencyOverviews ?? []);
1851
+ }
1634
1852
  getBlockingDependents(pluginId, platform, scope) {
1635
1853
  const dependents = this.#viewModel?.dependencyOverviews.find((entry) => entry.pluginId === pluginId)?.dependents ?? [];
1636
1854
  return cloneFrozen(dependents.flatMap((dependent) => {
@@ -1756,7 +1974,7 @@ export class TuiController {
1756
1974
  return this.#catalogSourcesSession.fullValidate(sourceId);
1757
1975
  }
1758
1976
  setExternalDraftState(states = []) {
1759
- const priority = { sourceDraft: 0, batchDraft: 1, configDraft: 2 };
1977
+ const priority = { sourceDraft: 0, batchDraft: 1, catalogDraft: 2, configDraft: 3 };
1760
1978
  this.#externalDrafts = structuredClone(states).sort((left, right) => priority[left.kind] - priority[right.kind] || left.owner.localeCompare(right.owner, 'en'));
1761
1979
  this.#publish();
1762
1980
  }
@@ -1769,6 +1987,9 @@ export class TuiController {
1769
1987
  startPluginWriteBatchReview(intent) {
1770
1988
  return this.startReviewPreparation(intent, { name: 'batchPlacement' });
1771
1989
  }
1990
+ startPluginWriteTaskReview(intent, returnRoute) {
1991
+ return this.startReviewPreparation(intent, returnRoute);
1992
+ }
1772
1993
  retryFailedPluginWriteBatch() {
1773
1994
  if (!this.#batchRetryIntent)
1774
1995
  return undefined;
@@ -1776,6 +1997,13 @@ export class TuiController {
1776
1997
  this.#batchRetryIntent = undefined;
1777
1998
  return this.startReviewPreparation(intent, { name: 'pluginList', mode: 'installed' });
1778
1999
  }
2000
+ retryFailedPluginWriteTask() {
2001
+ if (!this.#taskRetryIntent)
2002
+ return undefined;
2003
+ const intent = structuredClone(this.#taskRetryIntent);
2004
+ this.#taskRetryIntent = undefined;
2005
+ return this.startReviewPreparation(intent, { name: 'pluginList', mode: 'installed' });
2006
+ }
1779
2007
  catalogActivationBlockReason() {
1780
2008
  if (this.#externalDrafts[0])
1781
2009
  return this.#externalDrafts[0].kind;
@@ -1790,11 +2018,25 @@ export class TuiController {
1790
2018
  return 'writeProgress';
1791
2019
  return undefined;
1792
2020
  }
1793
- catalogSourceRecoveryBlockReason() {
2021
+ catalogSourceRecoveryBlockReason(intent) {
1794
2022
  const activeFlow = this.#router.activeFlow();
2023
+ const route = this.#router.snapshot.current.route;
2024
+ const ownedFrozenBatchTask = intent !== undefined
2025
+ && route.name === 'pluginWriteTask'
2026
+ && route.targetKeys !== undefined
2027
+ && route.targetKeys.length > 0
2028
+ && route.batchOrigin !== undefined
2029
+ && activeFlow?.id === route.batchOrigin.flowId
2030
+ && this.listPluginWriteTaskCandidates(route.goal).some((candidate) => candidate.sourceId === intent.sourceId
2031
+ && candidate.blockReason === 'catalogActivationRequired'
2032
+ && route.targetKeys.includes(pluginWriteTaskTargetKey(candidate.target)));
1795
2033
  const blockingExternalDraft = this.#externalDrafts.find((draft) => !(draft.kind === 'configDraft'
1796
2034
  && activeFlow?.kind === 'sourceRecovery'
1797
- && activeFlow.parentFlowId !== undefined));
2035
+ && activeFlow.parentFlowId !== undefined)
2036
+ && !(ownedFrozenBatchTask
2037
+ && draft.kind === 'batchDraft'
2038
+ && draft.owner === route.batchOrigin?.owner
2039
+ && draft.flowId === route.batchOrigin.flowId));
1798
2040
  if (blockingExternalDraft)
1799
2041
  return blockingExternalDraft.kind;
1800
2042
  if (this.#portableDraft)
@@ -1865,6 +2107,61 @@ export class TuiController {
1865
2107
  : this.#abort.signal;
1866
2108
  return cloneFrozen(await this.#trackRead(this.dependencies.getConfigPage(pluginId, structuredClone(layer), combined)));
1867
2109
  }
2110
+ async selectCatalogWorkspace(workspaceRoot) {
2111
+ this.#assertAcceptingActions();
2112
+ if (!this.dependencies.catalogManagement) {
2113
+ throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
2114
+ }
2115
+ return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.selectWorkspace(workspaceRoot)));
2116
+ }
2117
+ async getCatalogWorkspaceManagement() {
2118
+ this.#assertAcceptingActions();
2119
+ if (!this.dependencies.catalogManagement) {
2120
+ throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
2121
+ }
2122
+ return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.getWorkspace()));
2123
+ }
2124
+ async validateCatalogWorkspace() {
2125
+ this.#assertAcceptingActions();
2126
+ if (!this.dependencies.catalogManagement) {
2127
+ throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
2128
+ }
2129
+ return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.validateWorkspace()));
2130
+ }
2131
+ async getCatalogDevManagement() {
2132
+ this.#assertAcceptingActions();
2133
+ if (!this.dependencies.catalogManagement) {
2134
+ throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
2135
+ }
2136
+ return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.getDev()));
2137
+ }
2138
+ async getCatalogStoreManagement() {
2139
+ this.#assertAcceptingActions();
2140
+ if (!this.dependencies.catalogManagement) {
2141
+ throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
2142
+ }
2143
+ return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.getStore()));
2144
+ }
2145
+ async getCatalogPublicationManagement() {
2146
+ this.#assertAcceptingActions();
2147
+ if (!this.dependencies.catalogManagement) {
2148
+ throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
2149
+ }
2150
+ return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.getPublication()));
2151
+ }
2152
+ async previewConfigSaveImpact(intent, signal) {
2153
+ this.#assertAcceptingActions();
2154
+ const normalized = normalizeOperationIntent(intent);
2155
+ this.#validateReviewIntent(normalized);
2156
+ const combined = signal
2157
+ ? AbortSignal.any([this.#abort.signal, signal])
2158
+ : this.#abort.signal;
2159
+ const evidence = await this.#trackRead(this.#prepareReviewEvidence(normalized, combined));
2160
+ if (!evidence.config) {
2161
+ throw new TuiOperationBlockedError('configImpactUnavailable', '无法确定设置修改的应用影响。');
2162
+ }
2163
+ return cloneFrozen(evidence.config);
2164
+ }
1868
2165
  async getConfigResetAvailability(pluginId, layer) {
1869
2166
  this.#assertAcceptingActions();
1870
2167
  if (!this.dependencies.getConfigResetAvailability) {
@@ -1875,6 +2172,11 @@ export class TuiController {
1875
2172
  listUserIssues() {
1876
2173
  return cloneFrozen(this.#viewModel?.issues ?? []);
1877
2174
  }
2175
+ listPluginWriteTaskCandidates(goal) {
2176
+ return cloneFrozen(this.#viewModel
2177
+ ? buildPluginWriteTaskCandidates(this.#viewModel, goal)
2178
+ : []);
2179
+ }
1878
2180
  async buildOperationReview(intent, returnRoute) {
1879
2181
  this.#assertAcceptingActions();
1880
2182
  intent = normalizeOperationIntent(intent);
@@ -1895,7 +2197,7 @@ export class TuiController {
1895
2197
  this.#assertAcceptingActions();
1896
2198
  intent = normalizeOperationIntent(intent);
1897
2199
  if (intent.kind === 'catalogGeneration' && !intent.sourceMutation) {
1898
- const blockReason = this.catalogSourceRecoveryBlockReason();
2200
+ const blockReason = this.catalogSourceRecoveryBlockReason(intent);
1899
2201
  if (blockReason) {
1900
2202
  throw new TuiOperationBlockedError('catalogActivationBlocked', `Catalog Active 切换已阻止:${blockReason}。请先完成或放弃当前草稿或操作。`);
1901
2203
  }
@@ -1958,6 +2260,37 @@ export class TuiController {
1958
2260
  return this.startReviewPreparation(structuredClone(intent), preparation.returnRoute);
1959
2261
  }
1960
2262
  #validateReviewIntent(intent) {
2263
+ if (intent.kind === 'catalogManagement') {
2264
+ if (intent.action === 'storePrune' && intent.items.length === 0) {
2265
+ throw new TuiOperationBlockedError('catalogStoreSelectionInvalid', 'Select at least one reclaimable Catalog store item.');
2266
+ }
2267
+ if (intent.action === 'workspaceVersion'
2268
+ && (intent.version === undefined) === (intent.bump === undefined)) {
2269
+ throw new TuiOperationBlockedError('catalogVersionSelectionInvalid', 'Select exactly one Catalog version or bump level.');
2270
+ }
2271
+ if ((intent.action === 'publishPlan' || intent.action === 'publishApply')
2272
+ && intent.remote !== undefined && intent.remoteUrl !== undefined) {
2273
+ throw new TuiOperationBlockedError('catalogPublicationTargetInvalid', 'Select a Git remote name or one direct remote URL.');
2274
+ }
2275
+ if (intent.action === 'publishApply'
2276
+ && !/^sha256:[0-9a-f]{64}$/u.test(intent.expectedPlanDigest)) {
2277
+ throw new TuiOperationBlockedError('catalogPublicationPlanInvalid', 'Select an exact prepared Catalog publication plan.');
2278
+ }
2279
+ }
2280
+ if (intent.kind === 'pluginWriteTask') {
2281
+ if (intent.targets.length === 0
2282
+ || JSON.stringify(intent.targets) !== JSON.stringify(canonicalPluginWriteTaskTargets(intent.targets))) {
2283
+ throw new TuiOperationBlockedError('taskSelectionInvalid', '任务目标必须是非空精确目标集合。');
2284
+ }
2285
+ for (const target of intent.targets) {
2286
+ sourceIdFromPluginId(target.pluginId);
2287
+ if ((target.platform !== 'codex' && target.platform !== 'claude')
2288
+ || (target.scope !== 'user' && target.scope !== 'project')
2289
+ || (intent.goal === 'update') !== Boolean(target.expectedUpdate)) {
2290
+ throw new TuiOperationBlockedError('taskSelectionInvalid', '任务目标或版本证据无效。');
2291
+ }
2292
+ }
2293
+ }
1961
2294
  if (intent.kind === 'pluginWriteBatch') {
1962
2295
  if (intent.selection.kind === 'currentProjectIntentRoots'
1963
2296
  && (intent.action !== 'sync' || intent.scope !== 'project')) {
@@ -1978,8 +2311,12 @@ export class TuiController {
1978
2311
  }
1979
2312
  }
1980
2313
  #reviewPreparationLabel(intent) {
2314
+ if (intent.kind === 'catalogManagement')
2315
+ return 'Catalog 管理';
1981
2316
  if (intent.kind === 'catalogSourcesRecovery')
1982
2317
  return 'Catalog 来源配置';
2318
+ if (intent.kind === 'pluginWriteTask')
2319
+ return `${intent.targets.length} 个安装位置`;
1983
2320
  if (intent.kind === 'pluginWriteBatch') {
1984
2321
  return intent.selection.kind === 'selected'
1985
2322
  ? `${intent.selection.pluginIds.length} 个插件`
@@ -2033,9 +2370,10 @@ export class TuiController {
2033
2370
  preparation.snapshot.stages = preparation.snapshot.stages.map((stage) => stage.state === 'running' ? { ...stage, state: 'completed' } : stage);
2034
2371
  this.#publish();
2035
2372
  const returnRoute = structuredClone(preparation.returnRoute);
2373
+ this.#storeOperationReview(intent, evidence, 'replace', returnRoute);
2036
2374
  this.#reviewPreparationIntents.delete(preparation.snapshot.id);
2037
2375
  this.#reviewPreparation = undefined;
2038
- this.#storeOperationReview(intent, evidence, 'replace', returnRoute);
2376
+ this.#publish();
2039
2377
  }
2040
2378
  catch (error) {
2041
2379
  if (!this.#reviewPreparationIsCurrent(token))
@@ -2148,6 +2486,8 @@ export class TuiController {
2148
2486
  this.#ensureOperationFlow(intent);
2149
2487
  if (intent.kind === 'pluginWriteBatch')
2150
2488
  assertPluginWriteBatchEvidence(evidence);
2489
+ if (intent.kind === 'pluginWriteTask')
2490
+ assertPluginWriteTaskEvidence(intent, evidence);
2151
2491
  if (this.#router.snapshot.current.route.name === 'result')
2152
2492
  navigation = 'replace';
2153
2493
  const projection = buildOperationReviewProjection({
@@ -2164,10 +2504,12 @@ export class TuiController {
2164
2504
  const expiresAt = now
2165
2505
  + (this.dependencies.reviewTtlMs ?? 2 * 60_000);
2166
2506
  const pluginId = 'pluginId' in intent ? intent.pluginId
2167
- : 'sourceId' in intent ? intent.sourceId
2507
+ : 'sourceId' in intent ? intent.sourceId ?? 'Catalog 管理'
2168
2508
  : intent.kind === 'pluginWriteBatch'
2169
2509
  ? `${evidence.requestedPluginIds.length} 个插件`
2170
- : 'Catalog 来源配置';
2510
+ : intent.kind === 'pluginWriteTask'
2511
+ ? `${evidence.taskPlan.requestedTargets.length} 个安装位置`
2512
+ : 'Catalog 来源配置';
2171
2513
  if ('pluginId' in intent)
2172
2514
  sourceIdFromPluginId(intent.pluginId);
2173
2515
  const pluginVersion = 'pluginId' in intent
@@ -2184,6 +2526,7 @@ export class TuiController {
2184
2526
  ? { sourceAction: intent.sourceMutation.action } : {}),
2185
2527
  ...(intent.kind === 'pluginWrite' ? { pluginAction: intent.action } : {}),
2186
2528
  ...(intent.kind === 'pluginWriteBatch' ? { pluginAction: intent.action } : {}),
2529
+ ...(intent.kind === 'pluginWriteTask' ? { taskGoal: intent.goal } : {}),
2187
2530
  ...(intent.kind === 'pluginWrite' && intent.originIssue
2188
2531
  ? { originIssue: intent.originIssue }
2189
2532
  : {}),
@@ -2204,6 +2547,15 @@ export class TuiController {
2204
2547
  .filter((group) => group.executable).length,
2205
2548
  },
2206
2549
  } : {}),
2550
+ ...(intent.kind === 'pluginWriteTask' ? {
2551
+ task: {
2552
+ goal: intent.goal,
2553
+ requestedTargetCount: evidence.taskPlan.requestedTargets.length,
2554
+ executableTargetCount: evidence.taskPlan.executableTargets.length,
2555
+ blockedTargetCount: evidence.taskPlan.blockedTargets.length,
2556
+ causeCount: evidence.taskPlan.causes.length,
2557
+ },
2558
+ } : {}),
2207
2559
  ...(evidence.source && 'catalogVersion' in evidence.source
2208
2560
  && typeof evidence.source.catalogVersion === 'string'
2209
2561
  && evidence.source.snapshotDigest
@@ -2267,7 +2619,9 @@ export class TuiController {
2267
2619
  this.#progressFeed?.clearCurrent?.();
2268
2620
  const dependencyTargets = ticket.intent.kind === 'pluginWriteBatch'
2269
2621
  ? ticket.evidence.groups.flatMap((group) => group.preview.dependencyPlan?.targets ?? [])
2270
- : ticket.evidence.dependencyPlan?.targets ?? [];
2622
+ : ticket.intent.kind === 'pluginWriteTask'
2623
+ ? ticket.evidence.taskPlan.sources.flatMap((source) => source.preview.dependencyPlan?.targets ?? [])
2624
+ : ticket.evidence.dependencyPlan?.targets ?? [];
2271
2625
  this.#progressTargets = dependencyTargets.map((target) => ({
2272
2626
  pluginId: target.pluginId,
2273
2627
  platform: target.platform,
@@ -2285,15 +2639,18 @@ export class TuiController {
2285
2639
  current: {
2286
2640
  target: ticket.intent.kind === 'pluginWriteBatch'
2287
2641
  ? `${ticket.evidence.requestedPluginIds.length} 个插件`
2288
- : 'pluginId' in ticket.intent ? ticket.intent.pluginId
2289
- : 'sourceId' in ticket.intent ? ticket.intent.sourceId : 'Catalog 来源配置',
2642
+ : ticket.intent.kind === 'pluginWriteTask'
2643
+ ? `${ticket.evidence.taskPlan.executableTargets.length} 个安装位置`
2644
+ : 'pluginId' in ticket.intent ? ticket.intent.pluginId
2645
+ : 'sourceId' in ticket.intent ? ticket.intent.sourceId ?? 'Catalog 管理' : 'Catalog 来源配置',
2290
2646
  phase: '验证当前状态',
2291
2647
  },
2292
2648
  };
2293
2649
  this.#router.replace({ name: 'progress', operationId });
2294
2650
  this.#publish();
2295
2651
  try {
2296
- if (ticket.intent.kind !== 'pluginWrite' && ticket.intent.kind !== 'pluginWriteBatch') {
2652
+ if (ticket.intent.kind !== 'pluginWrite' && ticket.intent.kind !== 'pluginWriteBatch'
2653
+ && ticket.intent.kind !== 'pluginWriteTask') {
2297
2654
  await this.dependencies.validateIntent?.(structuredClone(ticket.intent), this.#abort.signal);
2298
2655
  }
2299
2656
  }
@@ -2325,6 +2682,7 @@ export class TuiController {
2325
2682
  if (!this.#operationIsCurrent(generation))
2326
2683
  return this.#detachedBlocked(operationId, ticket.intent);
2327
2684
  if (ticket.intent.kind !== 'pluginWrite' && ticket.intent.kind !== 'pluginWriteBatch'
2685
+ && ticket.intent.kind !== 'pluginWriteTask'
2328
2686
  && ticket.intent.kind !== 'catalogGeneration') {
2329
2687
  let current;
2330
2688
  try {
@@ -2358,6 +2716,63 @@ export class TuiController {
2358
2716
  ...executionProjection,
2359
2717
  sensitiveValues: collectOperationSensitiveValues(ticket.intent),
2360
2718
  });
2719
+ if (ticket.intent.kind === 'catalogManagement') {
2720
+ if (ticket.intent.action === 'devActivate' || ticket.intent.action === 'devDeactivate') {
2721
+ this.dependencies.beginCatalogSession?.();
2722
+ generation = ++this.#generation;
2723
+ try {
2724
+ const refreshed = await this.dependencies.loadStatus(this.#abort.signal);
2725
+ if (!this.#operationIsCurrent(generation)) {
2726
+ return this.#detachedBlocked(operationId, ticket.intent);
2727
+ }
2728
+ this.#applyStatus(refreshed);
2729
+ }
2730
+ catch (reloadError) {
2731
+ if (!this.#operationIsCurrent(generation)) {
2732
+ return this.#detachedBlocked(operationId, ticket.intent);
2733
+ }
2734
+ await this.#logErrorSafe(reloadError, 'tuiStateReloadFailed', {
2735
+ privateConfigOperation: true,
2736
+ });
2737
+ this.#status = undefined;
2738
+ this.#viewModel = undefined;
2739
+ this.#phase = 'error';
2740
+ this.#loadError = {
2741
+ code: 'tuiStateReloadFailed',
2742
+ message: 'Catalog 来源已变更,但插件状态刷新失败。请重新加载后再继续操作。',
2743
+ };
2744
+ return this.#setBlocked(operationId, 'tuiStateReloadFailed', {
2745
+ intent: ticket.intent,
2746
+ navigation: 'result',
2747
+ returnRoute: ticket.returnRoute,
2748
+ writeMayHaveCompleted: true,
2749
+ });
2750
+ }
2751
+ }
2752
+ const baseSummary = operationResultSummary(ticket.intent, execution);
2753
+ const summary = {
2754
+ ...baseSummary,
2755
+ elapsedMs: Math.max(0, (this.dependencies.now?.() ?? Date.now()) - startedAtMs),
2756
+ messages: [...this.#progress.messages, ...(baseSummary.messages ?? [])].slice(-8)
2757
+ .map((message) => sanitizeDiagnosticText(message, [], collectOperationSensitiveValues(ticket.intent))),
2758
+ };
2759
+ const projection = this.#operationDiagnostics.get(operationId);
2760
+ if (projection)
2761
+ projection.effects.finalVerification = 'completed';
2762
+ this.#progress = { ...this.#progress, elapsedMs: summary.elapsedMs };
2763
+ const result = {
2764
+ state: summary.code === 'operationPartial' ? 'partial' : 'succeeded',
2765
+ operationId,
2766
+ summary,
2767
+ operationKind: ticket.intent.kind,
2768
+ returnRoute: structuredClone(this.#normalizeRoute(ticket.returnRoute)),
2769
+ };
2770
+ this.#result = result;
2771
+ this.#activeReview = undefined;
2772
+ this.#showResult(result, ticket.intent);
2773
+ this.#publish();
2774
+ return cloneFrozen(result);
2775
+ }
2361
2776
  if (ticket.intent.kind === 'sourceMutation' && ticket.intent.action === 'update') {
2362
2777
  const confirmedAutoCheck = catalogSourceAutoCheckOnlyValue(ticket.intent.update);
2363
2778
  if (confirmedAutoCheck !== undefined) {
@@ -2453,6 +2868,17 @@ export class TuiController {
2453
2868
  if (!this.#operationIsCurrent(generation))
2454
2869
  return this.#detachedBlocked(operationId, ticket.intent);
2455
2870
  this.#applyStatus(refreshed);
2871
+ if (ticket.intent.kind === 'catalogGeneration') {
2872
+ const completed = typeof execution === 'object' && execution !== null
2873
+ ? execution.generation
2874
+ : undefined;
2875
+ const active = this.#catalogGenerations.get(ticket.intent.sourceId)?.active;
2876
+ if (completed && active
2877
+ && active.catalogVersion === completed.resolution.catalogVersion
2878
+ && active.resolvedCommit === completed.resolution.resolvedCommit) {
2879
+ this.#catalogSourcesSession.recordVerifiedGeneration(ticket.intent.sourceId, active);
2880
+ }
2881
+ }
2456
2882
  const verifiedProjection = this.#operationDiagnostics.get(operationId);
2457
2883
  if (verifiedProjection && verifiedProjection.effects.finalVerification !== 'failed') {
2458
2884
  verifiedProjection.effects.finalVerification = 'completed';
@@ -2463,6 +2889,7 @@ export class TuiController {
2463
2889
  }
2464
2890
  const pluginSummary = ticket.intent.kind === 'pluginWrite'
2465
2891
  || ticket.intent.kind === 'pluginWriteBatch'
2892
+ || ticket.intent.kind === 'pluginWriteTask'
2466
2893
  ? operationResultSummary(ticket.intent, execution)
2467
2894
  : undefined;
2468
2895
  const hasFailedDependencyTarget = pluginSummary?.targets?.some((target) => target.relation !== undefined && target.relation !== 'requested'
@@ -2503,9 +2930,10 @@ export class TuiController {
2503
2930
  intent: ticket.intent,
2504
2931
  navigation: 'result',
2505
2932
  returnRoute: ticket.returnRoute,
2506
- ...(ticket.intent.kind === 'pluginWriteBatch' || dependencyFailure || dependencyStillRequired
2933
+ ...(ticket.intent.kind === 'pluginWriteBatch' || ticket.intent.kind === 'pluginWriteTask'
2934
+ || dependencyFailure || dependencyStillRequired
2507
2935
  ? { summary: pluginSummary } : {}),
2508
- ...(ticket.intent.kind !== 'pluginWriteBatch'
2936
+ ...(ticket.intent.kind !== 'pluginWriteBatch' && ticket.intent.kind !== 'pluginWriteTask'
2509
2937
  && !dependencyFailure && !dependencyStillRequired && ['started', 'partial', 'completed', 'failed', 'unknown'].includes(this.#operationDiagnostics.get(operationId)?.effects.awWrite ?? 'unknown') ? { writeMayHaveCompleted: true } : {}),
2510
2938
  });
2511
2939
  }
@@ -2579,7 +3007,9 @@ export class TuiController {
2579
3007
  : ticket.intent.kind === 'sourceMutation' && ticket.intent.action === 'update'
2580
3008
  ? { name: 'sourceDetail', sourceId: ticket.intent.sourceId }
2581
3009
  : ticket.intent.kind === 'catalogGeneration'
2582
- ? { name: 'sourceDetail', sourceId: ticket.intent.sourceId }
3010
+ ? ticket.returnRoute.name === 'pluginWriteTask'
3011
+ ? ticket.returnRoute
3012
+ : { name: 'sourceDetail', sourceId: ticket.intent.sourceId }
2583
3013
  : ticket.returnRoute)),
2584
3014
  };
2585
3015
  if ((ticket.intent.kind === 'configSave' || ticket.intent.kind === 'configReset')
@@ -2620,6 +3050,14 @@ export class TuiController {
2620
3050
  this.#operationDiagnostics.delete(operationId);
2621
3051
  return this.#rebuildStaleReview(ticket, operationId, undefined, generation);
2622
3052
  }
3053
+ if (ticket.intent.kind === 'catalogManagement') {
3054
+ return this.#setBlocked(operationId, code, {
3055
+ intent: ticket.intent,
3056
+ navigation: 'result',
3057
+ returnRoute: ticket.returnRoute,
3058
+ writeMayHaveCompleted: true,
3059
+ });
3060
+ }
2623
3061
  generation = ++this.#generation;
2624
3062
  let finalVerification = 'completed';
2625
3063
  try {
@@ -2723,6 +3161,30 @@ export class TuiController {
2723
3161
  });
2724
3162
  }
2725
3163
  async #rebuildStaleReview(ticket, operationId, currentEvidence, generation = this.#generation) {
3164
+ if (ticket.intent.kind === 'catalogManagement') {
3165
+ let evidence = currentEvidence;
3166
+ try {
3167
+ evidence ??= await this.dependencies.captureEvidence(structuredClone(ticket.intent), this.#abort.signal);
3168
+ }
3169
+ catch (error) {
3170
+ const code = issueCode(error);
3171
+ await this.#logOperationError(error, code, ticket.intent);
3172
+ return this.#setBlocked(operationId, code, {
3173
+ intent: ticket.intent,
3174
+ returnRoute: ticket.returnRoute,
3175
+ });
3176
+ }
3177
+ const review = this.#storeOperationReview(ticket.intent, evidence, 'replace', ticket.returnRoute);
3178
+ const result = {
3179
+ state: 'stale',
3180
+ operationId,
3181
+ review,
3182
+ ...this.#operationIdentity(ticket.intent),
3183
+ };
3184
+ this.#result = result;
3185
+ this.#publish();
3186
+ return cloneFrozen(result);
3187
+ }
2726
3188
  this.dependencies.beginCatalogSession?.();
2727
3189
  generation = ++this.#generation;
2728
3190
  try {
@@ -2874,6 +3336,22 @@ export class TuiController {
2874
3336
  }
2875
3337
  }
2876
3338
  function operationResultSummaryUnsafe(intent, execution) {
3339
+ if (intent.kind === 'catalogManagement') {
3340
+ const result = execution;
3341
+ return {
3342
+ code: result.state === 'partial' ? 'operationPartial' : 'operationSucceeded',
3343
+ message: result.message,
3344
+ messages: result.messages,
3345
+ catalogManagement: {
3346
+ action: result.action,
3347
+ state: result.state,
3348
+ ...(result.catalogVersion ? { catalogVersion: result.catalogVersion } : {}),
3349
+ ...(result.planDigest ? { planDigest: result.planDigest } : {}),
3350
+ ...(result.store ? { store: structuredClone(result.store) } : {}),
3351
+ ...(result.publication ? { publication: structuredClone(result.publication) } : {}),
3352
+ },
3353
+ };
3354
+ }
2877
3355
  if (intent.kind === 'catalogSourcesRecovery') {
2878
3356
  const result = execution;
2879
3357
  return {
@@ -2950,6 +3428,131 @@ function operationResultSummaryUnsafe(intent, execution) {
2950
3428
  },
2951
3429
  };
2952
3430
  }
3431
+ if (intent.kind === 'pluginWriteTask') {
3432
+ const taskExecution = execution;
3433
+ const failedTargets = pluginWriteTaskFailedRequestedTargets(taskExecution);
3434
+ const failedKeys = new Set(failedTargets.map(pluginWriteTaskTargetKey));
3435
+ const technicalTargets = taskExecution.targetResults.map((entry) => {
3436
+ const placement = taskExecution.dependencyPlan?.targets.find((target) => target.pluginId === entry.target.pluginId && target.platform === entry.target.platform
3437
+ && (target.requestedScope ?? target.resolvedScope) === entry.target.scope);
3438
+ return {
3439
+ pluginId: entry.target.pluginId,
3440
+ platform: entry.target.platform,
3441
+ scope: entry.target.scope,
3442
+ state: entry.action === 'skipped' ? 'skipped'
3443
+ : entry.state === 'failed' ? 'failed'
3444
+ : entry.action === 'keep' ? 'kept' : 'completed',
3445
+ relation: entry.relation === 'requested' ? 'requested'
3446
+ : placement?.depth === 1 ? 'direct' : 'transitive',
3447
+ requiredBy: entry.requiredBy,
3448
+ ...(entry.errorCode ? { errorCode: safeDiagnosticCode(entry.errorCode, 'writeFailed') } : {}),
3449
+ };
3450
+ });
3451
+ const blocked = new Map(taskExecution.taskPlan.blockedTargets.map((entry) => [
3452
+ pluginWriteTaskTargetKey(entry.target), entry,
3453
+ ]));
3454
+ const relatedResults = (target) => {
3455
+ const key = pluginWriteTaskTargetKey(target);
3456
+ const source = taskExecution.sourceResults.find((candidate) => candidate.requestedTargets.some((requested) => pluginWriteTaskTargetKey(requested) === key));
3457
+ const related = source?.result.targetResults.filter((entry) => {
3458
+ const pluginId = entry.target.pluginId;
3459
+ if (!pluginId)
3460
+ return false;
3461
+ const executionKey = pluginWriteTaskTargetKey({
3462
+ pluginId,
3463
+ platform: entry.target.platform,
3464
+ scope: entry.target.scope,
3465
+ });
3466
+ if (executionKey === key)
3467
+ return true;
3468
+ if (taskExecution.taskPlan.causes.some((cause) => pluginWriteTaskTargetKey(cause.repairTarget) === executionKey
3469
+ && cause.consumers.some((consumer) => pluginWriteTaskTargetKey(consumer) === key)))
3470
+ return true;
3471
+ const placement = source.result.dependencyPlan?.targets.find((candidate) => candidate.pluginId === pluginId && candidate.platform === entry.target.platform
3472
+ && (candidate.requestedScope ?? candidate.resolvedScope) === entry.target.scope);
3473
+ return placement?.requiredByTargets?.some((consumer) => pluginWriteTaskTargetKey(consumer) === key) ?? false;
3474
+ }) ?? [];
3475
+ const targetResults = related.map((entry) => {
3476
+ const placement = source?.result.dependencyPlan?.targets.find((candidate) => candidate.pluginId === entry.target.pluginId
3477
+ && candidate.platform === entry.target.platform
3478
+ && (candidate.requestedScope ?? candidate.resolvedScope) === entry.target.scope);
3479
+ return {
3480
+ pluginId: entry.target.pluginId,
3481
+ platform: entry.target.platform,
3482
+ scope: entry.target.scope,
3483
+ state: entry.action === 'skipped' ? 'skipped'
3484
+ : entry.state === 'failed' ? 'failed'
3485
+ : entry.action === 'keep' ? 'kept' : 'completed',
3486
+ relation: entry.relation === 'requested' ? 'requested'
3487
+ : placement?.depth === 1 ? 'direct' : 'transitive',
3488
+ ...(entry.errorCode ? { errorCode: safeDiagnosticCode(entry.errorCode, 'writeFailed') } : {}),
3489
+ };
3490
+ });
3491
+ if (blocked.has(key) && !targetResults.some((entry) => entry.pluginId === target.pluginId && entry.platform === target.platform
3492
+ && entry.scope === target.scope)) {
3493
+ targetResults.unshift({
3494
+ pluginId: target.pluginId,
3495
+ platform: target.platform,
3496
+ scope: target.scope,
3497
+ state: 'skipped',
3498
+ relation: 'requested',
3499
+ });
3500
+ }
3501
+ const hasFailed = targetResults.some((entry) => entry.state === 'failed' || entry.state === 'skipped');
3502
+ const hasCompleted = targetResults.some((entry) => entry.state === 'completed' || entry.state === 'kept');
3503
+ const state = blocked.has(key) ? 'blocked'
3504
+ : !source ? 'notStarted'
3505
+ : hasFailed && hasCompleted ? 'partial'
3506
+ : hasFailed || failedKeys.has(key) ? 'failed'
3507
+ : targetResults.length > 0 && targetResults.every((entry) => entry.state === 'kept')
3508
+ ? 'kept' : targetResults.length > 0 ? 'completed' : 'notStarted';
3509
+ return { state, targets: targetResults };
3510
+ };
3511
+ const roots = [...new Set(taskExecution.taskPlan.requestedTargets
3512
+ .map((target) => target.pluginId))].map((pluginId) => {
3513
+ const requestedTargets = taskExecution.taskPlan.requestedTargets.filter((target) => target.pluginId === pluginId);
3514
+ const positions = requestedTargets.map((target) => relatedResults(target));
3515
+ const successfulPositions = positions.filter((position) => position.state === 'completed' || position.state === 'kept');
3516
+ const state = successfulPositions.length > 0
3517
+ ? successfulPositions.length === positions.length
3518
+ ? positions.every((position) => position.state === 'kept') ? 'kept' : 'completed'
3519
+ : 'partial'
3520
+ : positions.some((position) => position.state === 'failed' || position.state === 'partial')
3521
+ ? 'failed'
3522
+ : positions.some((position) => position.state === 'notStarted') ? 'notStarted' : 'blocked';
3523
+ const targets = [...new Map(positions.flatMap((position) => position.targets).map((target) => [[
3524
+ target.pluginId ?? '', target.platform, target.scope,
3525
+ ].join('\0'), target])).values()];
3526
+ const causeKeys = [...new Set(taskExecution.taskPlan.causes.filter((cause) => cause.consumers.some((consumer) => consumer.pluginId === pluginId))
3527
+ .map((cause) => cause.causeKey))];
3528
+ return { pluginId, state, causeKeys, targets };
3529
+ });
3530
+ const retryTargets = canonicalPluginWriteTaskTargets(roots.flatMap((root) => root.state === 'failed' || root.state === 'partial' || root.state === 'notStarted'
3531
+ ? failedTargets.filter((target) => target.pluginId === root.pluginId) : []));
3532
+ const completedCount = roots.filter((root) => root.state === 'completed'
3533
+ || root.state === 'kept' || root.state === 'partial').length;
3534
+ const failedCount = roots.filter((root) => root.state === 'failed' || root.state === 'partial'
3535
+ || root.state === 'notStarted' || root.state === 'blocked').length;
3536
+ const code = failedCount === 0 ? 'operationSucceeded'
3537
+ : completedCount > 0 ? 'operationPartial' : 'operationFailed';
3538
+ return {
3539
+ code,
3540
+ message: code === 'operationSucceeded' ? '任务已完成。'
3541
+ : code === 'operationPartial' ? '任务只完成了一部分。' : '任务没有完成。',
3542
+ warnings: taskExecution.warnings,
3543
+ recoveryActions: [...new Set(taskExecution.targetResults.flatMap((entry) => entry.recoveryActions ?? []))],
3544
+ ...(taskExecution.operationPlan?.planFingerprint
3545
+ ? { planFingerprint: taskExecution.operationPlan.planFingerprint } : {}),
3546
+ task: {
3547
+ goal: intent.goal,
3548
+ requestedTargets: taskExecution.taskPlan.requestedTargets,
3549
+ rootResults: roots,
3550
+ ...(retryTargets.length > 0 ? { retryTargets } : {}),
3551
+ blockedTargets: taskExecution.taskPlan.blockedTargets,
3552
+ },
3553
+ targets: technicalTargets,
3554
+ };
3555
+ }
2953
3556
  if (intent.kind === 'pluginWrite' || intent.kind === 'pluginWriteBatch') {
2954
3557
  const command = intent.action === 'refreshAndSync' ? 'sync' : intent.action;
2955
3558
  const rawExecution = typeof execution === 'object' && execution !== null
@@ -2995,6 +3598,52 @@ function operationResultSummaryUnsafe(intent, execution) {
2995
3598
  ? pluginWriteBatchFailedRequestedTargets(execution)
2996
3599
  : [];
2997
3600
  const failedRequestedPluginIds = canonicalPluginIds(retryTargets.map((target) => target.pluginId));
3601
+ const requestedTargetKeys = intent.kind === 'pluginWriteBatch'
3602
+ ? new Set(execution.groupResults.flatMap((group) => {
3603
+ const roots = new Set(group.requestedPluginIds);
3604
+ return group.result.targetResults.flatMap((entry) => entry.target.pluginId
3605
+ && roots.has(entry.target.pluginId)
3606
+ && entry.target.scope === group.scope
3607
+ && (group.platform === 'both' || entry.target.platform === group.platform)
3608
+ ? [[entry.target.pluginId, entry.target.platform, entry.target.scope].join('\0')]
3609
+ : []);
3610
+ }))
3611
+ : new Set();
3612
+ const rootResults = intent.kind === 'pluginWriteBatch'
3613
+ ? requestedPluginIds.map((pluginId) => {
3614
+ const observed = (result.targetResults ?? []).flatMap((entry) => entry.target.pluginId === pluginId
3615
+ && requestedTargetKeys.has([
3616
+ pluginId, entry.target.platform, entry.target.scope,
3617
+ ].join('\0')) ? [{
3618
+ pluginId,
3619
+ platform: entry.target.platform,
3620
+ scope: entry.target.scope,
3621
+ state: entry.action === 'skipped' ? 'skipped'
3622
+ : entry.state === 'failed' ? 'failed'
3623
+ : entry.action === 'keep' ? 'kept' : 'completed',
3624
+ relation: 'requested',
3625
+ ...(entry.errorCode
3626
+ ? { errorCode: safeDiagnosticCode(entry.errorCode, 'writeFailed') }
3627
+ : {}),
3628
+ }] : []);
3629
+ const targets = [...new Map([
3630
+ ...retryTargets.filter((target) => target.pluginId === pluginId).map((target) => ({
3631
+ ...target,
3632
+ state: 'skipped',
3633
+ relation: 'requested',
3634
+ })),
3635
+ ...observed,
3636
+ ].map((target) => [[target.pluginId, target.platform, target.scope].join('\0'), target]))
3637
+ .values()];
3638
+ const completed = targets.filter((target) => target.state === 'completed' || target.state === 'kept').length;
3639
+ const failed = targets.length - completed;
3640
+ const state = completed > 0
3641
+ ? failed > 0 ? 'partial'
3642
+ : targets.every((target) => target.state === 'kept') ? 'kept' : 'completed'
3643
+ : failed > 0 ? 'failed' : 'notStarted';
3644
+ return { pluginId, state, targets };
3645
+ })
3646
+ : [];
2998
3647
  return {
2999
3648
  code: result.state === 'succeeded' ? 'operationSucceeded'
3000
3649
  : result.state === 'partial' ? 'operationPartial' : 'operationFailed',
@@ -3017,6 +3666,7 @@ function operationResultSummaryUnsafe(intent, execution) {
3017
3666
  batch: {
3018
3667
  requestedPluginIds,
3019
3668
  failedRequestedPluginIds,
3669
+ rootResults,
3020
3670
  retryTargets,
3021
3671
  groupCount: execution.groupResults.length,
3022
3672
  },