@lesliechan721/aw 0.1.0-beta.31 → 0.1.0-beta.33

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 (50) hide show
  1. package/dist/authoring/scripts/lib/plugin-version-check.mjs +1 -1
  2. package/dist/authoring/scripts/validate-plugin.mjs +5 -2
  3. package/dist/authoring/scripts/validate-plugin.mjs.map +1 -1
  4. package/dist/src/commands/aw-capabilities.d.ts +1 -1
  5. package/dist/src/commands/aw-capabilities.js +2 -0
  6. package/dist/src/commands/aw-capabilities.js.map +1 -1
  7. package/dist/src/tui/catalog-sources/session.d.ts +5 -1
  8. package/dist/src/tui/catalog-sources/session.js +6 -1
  9. package/dist/src/tui/catalog-sources/session.js.map +1 -1
  10. package/dist/src/tui/catalog-sources/types.d.ts +1 -0
  11. package/dist/src/tui/clipboard.d.ts +13 -0
  12. package/dist/src/tui/clipboard.js +95 -0
  13. package/dist/src/tui/clipboard.js.map +1 -0
  14. package/dist/src/tui/components.d.ts +21 -11
  15. package/dist/src/tui/components.js +69 -56
  16. package/dist/src/tui/components.js.map +1 -1
  17. package/dist/src/tui/config-value-presentation.d.ts +19 -0
  18. package/dist/src/tui/config-value-presentation.js +77 -0
  19. package/dist/src/tui/config-value-presentation.js.map +1 -0
  20. package/dist/src/tui/console-app-v4.d.ts +1 -1
  21. package/dist/src/tui/console-app-v4.js +1205 -1377
  22. package/dist/src/tui/console-app-v4.js.map +1 -1
  23. package/dist/src/tui/controller-runtime.js +5 -0
  24. package/dist/src/tui/controller-runtime.js.map +1 -1
  25. package/dist/src/tui/controller.d.ts +12 -1
  26. package/dist/src/tui/controller.js +171 -46
  27. package/dist/src/tui/controller.js.map +1 -1
  28. package/dist/src/tui/diagnostics.d.ts +2 -1
  29. package/dist/src/tui/diagnostics.js +8 -1
  30. package/dist/src/tui/diagnostics.js.map +1 -1
  31. package/dist/src/tui/input-display.d.ts +1 -0
  32. package/dist/src/tui/input-display.js +13 -0
  33. package/dist/src/tui/input-display.js.map +1 -1
  34. package/dist/src/tui/model.d.ts +5 -3
  35. package/dist/src/tui/model.js +17 -33
  36. package/dist/src/tui/model.js.map +1 -1
  37. package/dist/src/tui/operation-diagnostic-screens.d.ts +6 -3
  38. package/dist/src/tui/operation-diagnostic-screens.js +31 -9
  39. package/dist/src/tui/operation-diagnostic-screens.js.map +1 -1
  40. package/dist/src/tui/operation-history.js +5 -4
  41. package/dist/src/tui/operation-history.js.map +1 -1
  42. package/dist/src/tui/router.d.ts +5 -7
  43. package/dist/src/tui/router.js.map +1 -1
  44. package/dist/src/tui/screen-registry.d.ts +13 -22
  45. package/dist/src/tui/screen-registry.js +3 -4
  46. package/dist/src/tui/screen-registry.js.map +1 -1
  47. package/dist/src/tui/viewport.d.ts +1 -0
  48. package/dist/src/tui/viewport.js +23 -0
  49. package/dist/src/tui/viewport.js.map +1 -1
  50. package/package.json +3 -3
@@ -54,6 +54,7 @@ export interface PluginInstallBatchTarget extends PluginWriteBatchTarget {
54
54
  export declare function pluginWriteBatchTargets(action: Exclude<BatchPluginWriteAction, 'install'>, pluginIds: readonly string[], statusTargets: readonly StatusScopedTarget[]): PluginWriteBatchTarget[];
55
55
  export interface PluginWriteBatchIntent {
56
56
  kind: 'pluginWriteBatch';
57
+ dependencyScopeOverrides?: DependencyScopeOverride[];
57
58
  action: BatchPluginWriteAction;
58
59
  selection: PluginWriteBatchSelection;
59
60
  platform: PlatformName | 'both';
@@ -408,6 +409,14 @@ export interface OperationReviewModel {
408
409
  blockedTargetCount: number;
409
410
  causeCount: number;
410
411
  };
412
+ rootSummary?: Array<{
413
+ pluginId: string;
414
+ version?: string;
415
+ targets: Array<{
416
+ platform: PlatformName | 'both';
417
+ scope: Scope;
418
+ }>;
419
+ }>;
411
420
  }
412
421
  export type ReviewPreparationStageId = 'refresh' | 'validate' | 'status' | 'catalog' | 'inventory' | 'dependencyPlan' | 'config' | 'portable' | 'evidence';
413
422
  export interface ReviewPreparationStage {
@@ -749,6 +758,7 @@ export interface TuiControllerSnapshot {
749
758
  activeReview?: OperationReviewModel;
750
759
  reviewPreparation?: ReviewPreparationSnapshot;
751
760
  result?: OperationExecuteResult;
761
+ canResumeInstallDraft?: boolean;
752
762
  operationHistorySummaries: OperationHistorySummary[];
753
763
  portableResultSkills?: string[];
754
764
  loadError?: {
@@ -857,11 +867,12 @@ export declare class TuiController {
857
867
  beginInstallFlow(pluginId: string, rootScope: Scope): void;
858
868
  setInstallRootScope(rootScope: Scope): void;
859
869
  setInstallPlatforms(platforms: PlatformName[]): void;
870
+ setInstallRootTargets(targets: PluginWriteBatchTarget[]): void;
860
871
  prepareInstallDependencyPlacement(): Promise<boolean>;
861
872
  setDependencyScope(pluginId: string, platform: PlatformName, scope: Scope): Promise<void>;
862
873
  resetDependencyScopes(): Promise<void>;
863
874
  installPlacementIntent(): Extract<OperationIntent, {
864
- kind: 'pluginWrite';
875
+ kind: 'pluginWrite' | 'pluginWriteBatch';
865
876
  }>;
866
877
  listPlugins(options: {
867
878
  mode: 'browse' | 'installed';
@@ -768,8 +768,11 @@ export class TuiController {
768
768
  this.#project();
769
769
  this.#publish();
770
770
  },
771
- onError: (error, code) => {
771
+ onError: (error, code, context) => {
772
772
  void this.#logErrorSafe(error, code);
773
+ return this.#recordStandaloneFailure(code, context.fullValidation
774
+ ? 'validate-catalog-source'
775
+ : context.automatic ? 'auto-check-catalog-source' : 'check-catalog-source', error);
773
776
  },
774
777
  now: this.dependencies.now,
775
778
  });
@@ -822,6 +825,7 @@ export class TuiController {
822
825
  ? { reviewPreparation: this.#reviewPreparation.snapshot }
823
826
  : {}),
824
827
  ...(this.#result ? { result: this.#result } : {}),
828
+ ...(this.#recoverableInstallPlacementDraft ? { canResumeInstallDraft: true } : {}),
825
829
  operationHistorySummaries: this.#operationHistory.list(),
826
830
  ...(this.#portableResultSkills ? { portableResultSkills: this.#portableResultSkills } : {}),
827
831
  ...(this.#loadError ? { loadError: this.#loadError } : {}),
@@ -830,6 +834,8 @@ export class TuiController {
830
834
  ? { installPlacementDraft: {
831
835
  pluginId: this.#installPlacementDraft.pluginId,
832
836
  rootScope: this.#installPlacementDraft.rootScope,
837
+ ...(this.#installPlacementDraft.exactRootTargets
838
+ ? { exactRootTargets: this.#installPlacementDraft.exactRootTargets } : {}),
833
839
  platforms: this.#installPlacementDraft.platforms,
834
840
  dependencyScopeOverrides: this.#installPlacementDraft.dependencyScopeOverrides,
835
841
  state: this.#installPlacementDraft.state,
@@ -1229,6 +1235,10 @@ export class TuiController {
1229
1235
  if (intent.kind === 'pluginWriteBatch') {
1230
1236
  if (active?.kind === 'batchWrite')
1231
1237
  return;
1238
+ if (active?.kind === 'pluginInstall' && intent.action === 'install'
1239
+ && intent.selection.kind === 'targets' && this.#installPlacementDraft
1240
+ && intent.selection.targets.every((target) => target.pluginId === this.#installPlacementDraft.pluginId))
1241
+ return;
1232
1242
  this.#router.beginFlow('batchWrite', { name: 'pluginList', mode: 'installed' });
1233
1243
  return;
1234
1244
  }
@@ -1284,14 +1294,10 @@ export class TuiController {
1284
1294
  #stableResultRoute(intent, result) {
1285
1295
  const active = this.#router.activeFlow();
1286
1296
  if (intent.kind === 'pluginWriteTask' && active?.kind === 'pluginWriteTask'
1287
- && (active.parentFlowId || active.returnTarget.name === 'pluginWriteTask'
1288
- && active.returnTarget.goal !== intent.goal)) {
1297
+ && active.returnTarget.name === 'pluginWriteTask'
1298
+ && active.returnTarget.goal !== intent.goal) {
1289
1299
  return this.#normalizeRoute(active.returnTarget);
1290
1300
  }
1291
- const completedPluginId = completedSinglePluginId(intent, result);
1292
- if (completedPluginId) {
1293
- return this.#normalizeRoute({ name: 'pluginDetail', pluginId: completedPluginId });
1294
- }
1295
1301
  if (intent.kind === 'pluginWriteTask'
1296
1302
  && result.state === 'succeeded'
1297
1303
  && 'returnRoute' in result
@@ -1300,6 +1306,14 @@ export class TuiController {
1300
1306
  && result.returnRoute.batchOrigin?.owner === 'batch-write') {
1301
1307
  return { name: 'pluginList', mode: 'installed' };
1302
1308
  }
1309
+ const completedPluginId = completedSinglePluginId(intent, result);
1310
+ if (completedPluginId) {
1311
+ return this.#normalizeRoute({ name: 'pluginDetail', pluginId: completedPluginId });
1312
+ }
1313
+ if (intent.kind === 'pluginWriteTask' && active?.kind === 'pluginWriteTask'
1314
+ && active.parentFlowId) {
1315
+ return this.#normalizeRoute(active.returnTarget);
1316
+ }
1303
1317
  if (intent.kind === 'portableInstall' && result.state === 'succeeded') {
1304
1318
  return this.#normalizeRoute({ name: 'pluginDetail', pluginId: intent.pluginId });
1305
1319
  }
@@ -1349,10 +1363,17 @@ export class TuiController {
1349
1363
  this.#cancelReviewPreparation();
1350
1364
  this.#progress = { messages: [] };
1351
1365
  this.#progressTargets = [];
1366
+ const installDraft = this.#installPlacementDraft;
1367
+ const ownsInstallDraft = installDraft && (intent.kind === 'pluginWrite'
1368
+ && intent.action === 'install' && installDraft.pluginId === intent.pluginId
1369
+ || intent.kind === 'pluginWriteBatch' && intent.action === 'install'
1370
+ && this.#router.activeFlow()?.kind === 'pluginInstall'
1371
+ && intent.selection.kind === 'targets' && installDraft.exactRootTargets
1372
+ && JSON.stringify(canonicalBatchTargets(intent.selection.targets))
1373
+ === JSON.stringify(installDraft.exactRootTargets));
1352
1374
  this.#recoverableInstallPlacementDraft = result.state === 'blocked'
1353
- && intent.kind === 'pluginWrite' && intent.action === 'install'
1354
- && this.#installPlacementDraft?.pluginId === intent.pluginId
1355
- ? structuredClone(this.#installPlacementDraft)
1375
+ && 'returnRoute' in result && result.returnRoute && ownsInstallDraft
1376
+ ? structuredClone(installDraft)
1356
1377
  : undefined;
1357
1378
  this.#installPlacementDraft = undefined;
1358
1379
  this.#recoverablePortableDraft = result.state !== 'succeeded'
@@ -1556,13 +1577,19 @@ export class TuiController {
1556
1577
  if (!this.hasPortableSkills(pluginId)) {
1557
1578
  throw new TuiOperationBlockedError('portableUnsupported', '插件不提供通用 Skills。');
1558
1579
  }
1580
+ const requestKey = `prepare:${pluginId}`;
1581
+ if (this.#portablePending?.key === requestKey) {
1582
+ return this.#portablePending.promise;
1583
+ }
1559
1584
  this.#router.beginPortableFlow(pluginId);
1585
+ this.#router[navigation]({ name: 'portablePreparing', pluginId });
1586
+ this.#publish();
1560
1587
  const expectedRoute = this.#router.snapshot.current.route;
1561
- return this.#runPortableRequest(`prepare:${pluginId}`, async (signal, token) => {
1588
+ return this.#runPortableRequest(requestKey, async (signal, token) => {
1562
1589
  const draft = await this.#buildPortableDraft(pluginId, false, signal);
1563
1590
  this.#assertPortableRequestCurrent(token, expectedRoute);
1564
1591
  this.#portableDraft = draft;
1565
- this.#router[navigation]({ name: 'portableSkillSelect', pluginId });
1592
+ this.#router.replace({ name: 'portableSkillSelect', pluginId });
1566
1593
  this.#publish();
1567
1594
  return cloneFrozen(draft);
1568
1595
  });
@@ -1573,6 +1600,10 @@ export class TuiController {
1573
1600
  return this.#runPortableRequest(`refresh:${pluginId}`, async (signal, token) => {
1574
1601
  const draft = await this.#buildPortableDraft(pluginId, true, signal);
1575
1602
  this.#assertPortableRequestCurrent(token, expectedRoute);
1603
+ if (this.#activeReview?.operationKind === 'portableInstall') {
1604
+ this.#tickets.delete(this.#activeReview.reviewToken);
1605
+ this.#activeReview = undefined;
1606
+ }
1576
1607
  this.#portableDraft = draft;
1577
1608
  this.#publish();
1578
1609
  return cloneFrozen(draft);
@@ -1990,9 +2021,15 @@ export class TuiController {
1990
2021
  this.#tickets.delete(this.#activeReview.reviewToken);
1991
2022
  this.#activeReview = undefined;
1992
2023
  }
1993
- this.#router.back();
1994
- if (activeFlow
2024
+ if (route.name === 'batchSelection' && activeFlow?.kind === 'batchWrite'
1995
2025
  && this.#router.snapshot.currentFrameId === activeFlow.originFrameId) {
2026
+ this.#router.finishFlow();
2027
+ }
2028
+ else
2029
+ this.#router.back();
2030
+ if (activeFlow
2031
+ && this.#router.snapshot.currentFrameId === activeFlow.originFrameId
2032
+ && this.#router.snapshot.currentFlowId !== activeFlow.id) {
1996
2033
  this.#router.abandonFlow(activeFlow.id);
1997
2034
  if (activeFlow.kind === 'pluginInstall')
1998
2035
  this.#installPlacementDraft = undefined;
@@ -2074,21 +2111,31 @@ export class TuiController {
2074
2111
  if (this.#router.snapshot.current.route.name !== 'result' || !result
2075
2112
  || !('returnRoute' in result) || !result.returnRoute)
2076
2113
  return;
2114
+ if (result.operationKind === 'pluginWriteBatch' && !this.#recoverableInstallPlacementDraft)
2115
+ return;
2077
2116
  this.#invalidatePortableResultRecheck();
2078
2117
  const returnRoute = this.#normalizeRoute(result.returnRoute);
2079
2118
  this.#router.back();
2080
- const kind = result.operationKind === 'configSave' || result.operationKind === 'configSaveBatch'
2081
- || result.operationKind === 'configReset'
2082
- ? 'configEdit'
2083
- : result.operationKind === 'portableInstall'
2084
- ? 'portableInstall'
2085
- : result.operationKind === 'sourceMutation' || result.operationKind === 'catalogGeneration'
2086
- || result.operationKind === 'catalogSourcesRecovery'
2087
- ? 'sourceEdit'
2088
- : 'pluginMaintenance';
2119
+ const kind = this.#recoverableInstallPlacementDraft
2120
+ ? 'pluginInstall'
2121
+ : result.operationKind === 'configSave' || result.operationKind === 'configSaveBatch'
2122
+ || result.operationKind === 'configReset'
2123
+ ? 'configEdit'
2124
+ : result.operationKind === 'portableInstall'
2125
+ ? 'portableInstall'
2126
+ : result.operationKind === 'sourceMutation' || result.operationKind === 'catalogGeneration'
2127
+ || result.operationKind === 'catalogSourcesRecovery'
2128
+ ? 'sourceEdit'
2129
+ : 'pluginMaintenance';
2089
2130
  this.#router.beginFlow(kind, this.#router.snapshot.current.route);
2090
- if (result.operationKind === 'pluginWrite') {
2131
+ if (this.#recoverableInstallPlacementDraft) {
2091
2132
  this.#installPlacementDraft = this.#recoverableInstallPlacementDraft;
2133
+ if (this.#installPlacementDraft.exactRootTargets) {
2134
+ this.#installPlacementDraft.state = 'selecting';
2135
+ this.#installPlacementDraft.targets = [];
2136
+ this.#installPlacementDraft.canContinue = false;
2137
+ this.#installPlacementDraft.error = undefined;
2138
+ }
2092
2139
  }
2093
2140
  if (result.operationKind === 'portableInstall') {
2094
2141
  this.#portableDraft = this.#recoverablePortableDraft;
@@ -2107,8 +2154,8 @@ export class TuiController {
2107
2154
  || route.name === 'operationReportPreview';
2108
2155
  if (readOnlyHistoryRoute && this.#router.activeFlow()?.kind === 'portableInstall')
2109
2156
  return;
2110
- const portableRoute = route.name === 'portableSkillSelect'
2111
- || route.name === 'portableSkillSummary'
2157
+ const portableRoute = route.name === 'portablePreparing'
2158
+ || route.name === 'portableSkillSelect'
2112
2159
  || route.name === 'portableDependencies'
2113
2160
  || route.name === 'portableAgentMode'
2114
2161
  || route.name === 'portableAgentInput'
@@ -2209,8 +2256,9 @@ export class TuiController {
2209
2256
  return;
2210
2257
  this.#cancelInstallPlacementRequest();
2211
2258
  draft.rootScope = rootScope;
2259
+ draft.exactRootTargets = undefined;
2260
+ draft.dependencyScopeOverrides = draft.dependencyScopeOverrides.filter((override) => rootScope !== 'user' || override.scope === 'user');
2212
2261
  draft.platforms = [];
2213
- draft.plan = undefined;
2214
2262
  draft.targets = [];
2215
2263
  draft.state = 'selecting';
2216
2264
  draft.canContinue = false;
@@ -2224,21 +2272,41 @@ export class TuiController {
2224
2272
  this.#cancelInstallPlacementRequest();
2225
2273
  const selected = [...new Set(platforms)].sort();
2226
2274
  draft.platforms = selected;
2275
+ draft.exactRootTargets = undefined;
2227
2276
  draft.dependencyScopeOverrides = draft.dependencyScopeOverrides.filter((override) => selected.includes(override.platform));
2228
- draft.plan = undefined;
2229
2277
  draft.targets = [];
2230
2278
  draft.state = 'selecting';
2231
2279
  draft.canContinue = selected.length > 0;
2232
2280
  draft.error = undefined;
2233
2281
  this.#publish();
2234
2282
  }
2283
+ setInstallRootTargets(targets) {
2284
+ const draft = this.#installPlacementDraft;
2285
+ if (!draft)
2286
+ return;
2287
+ const selected = canonicalBatchTargets(structuredClone(targets));
2288
+ if (selected.length === 0 || selected.some((target) => target.pluginId !== draft.pluginId)
2289
+ || new Set(selected.map((target) => target.platform)).size !== selected.length) {
2290
+ throw new TuiOperationBlockedError('installPlacementIncomplete', '请为每个所选平台确定一个根插件位置。');
2291
+ }
2292
+ this.#cancelInstallPlacementRequest();
2293
+ draft.exactRootTargets = selected;
2294
+ draft.rootScope = selected[0].scope;
2295
+ draft.platforms = selected.map((target) => target.platform);
2296
+ draft.dependencyScopeOverrides = draft.dependencyScopeOverrides.filter((override) => selected.some((target) => target.platform === override.platform
2297
+ && (target.scope === 'project' || override.scope === 'user')));
2298
+ draft.targets = [];
2299
+ draft.state = 'selecting';
2300
+ draft.canContinue = true;
2301
+ draft.error = undefined;
2302
+ this.#publish();
2303
+ }
2235
2304
  #cancelInstallPlacementRequest() {
2236
2305
  const cancelled = this.#installPlacementRequestAbort !== undefined;
2237
2306
  this.#installPlacementRequestToken += 1;
2238
2307
  this.#installPlacementRequestAbort?.abort();
2239
2308
  this.#installPlacementRequestAbort = undefined;
2240
2309
  if (cancelled && this.#installPlacementDraft?.state === 'loading') {
2241
- this.#installPlacementDraft.plan = undefined;
2242
2310
  this.#installPlacementDraft.targets = [];
2243
2311
  this.#installPlacementDraft.state = 'selecting';
2244
2312
  this.#installPlacementDraft.canContinue = false;
@@ -2250,6 +2318,16 @@ export class TuiController {
2250
2318
  if (!draft || draft.platforms.length === 0) {
2251
2319
  throw new TuiOperationBlockedError('installPlacementIncomplete', '请先明确选择目标平台。');
2252
2320
  }
2321
+ if (draft.exactRootTargets)
2322
+ return {
2323
+ kind: 'pluginWriteBatch',
2324
+ action: 'install',
2325
+ selection: { kind: 'targets', targets: structuredClone(draft.exactRootTargets) },
2326
+ platform: draft.platforms.length === 2 ? 'both' : draft.platforms[0],
2327
+ scope: draft.rootScope,
2328
+ ...(draft.dependencyScopeOverrides.length > 0
2329
+ ? { dependencyScopeOverrides: structuredClone(draft.dependencyScopeOverrides) } : {}),
2330
+ };
2253
2331
  return {
2254
2332
  kind: 'pluginWrite',
2255
2333
  action: 'install',
@@ -2284,19 +2362,22 @@ export class TuiController {
2284
2362
  throw new DOMException('Dependency placement preview was cancelled.', 'AbortError');
2285
2363
  }
2286
2364
  };
2287
- const plan = await this.#trackRead(preview(this.#installPlacementIntent()));
2288
- throwIfCancelled();
2289
- if (!plan) {
2290
- draft.state = 'ready';
2291
- draft.plan = undefined;
2292
- draft.targets = [];
2293
- draft.canContinue = true;
2294
- this.#publish();
2295
- return false;
2365
+ const placementIntent = this.#installPlacementIntent();
2366
+ const intents = placementIntent.kind === 'pluginWrite'
2367
+ ? [placementIntent]
2368
+ : (draft.exactRootTargets ?? []).map((target) => ({
2369
+ kind: 'pluginWrite', action: 'install', ...target,
2370
+ dependencyScopeOverrides: draft.dependencyScopeOverrides.filter((override) => override.platform === target.platform),
2371
+ }));
2372
+ const plans = [];
2373
+ for (const intent of intents) {
2374
+ const plan = await this.#trackRead(preview(intent));
2375
+ throwIfCancelled();
2376
+ if (plan)
2377
+ plans.push(plan);
2296
2378
  }
2297
- draft.plan = structuredClone(plan);
2298
- draft.targets = dependencyPlacementTargets(plan);
2299
- const blocker = plan.targets.find((target) => target.action === 'block');
2379
+ draft.targets = plans.flatMap(dependencyPlacementTargets);
2380
+ const blocker = plans.flatMap((plan) => plan.targets).find((target) => target.action === 'block');
2300
2381
  draft.state = blocker ? 'blocked' : 'ready';
2301
2382
  draft.canContinue = !blocker;
2302
2383
  draft.error = blocker
@@ -2306,7 +2387,10 @@ export class TuiController {
2306
2387
  }
2307
2388
  : undefined;
2308
2389
  this.#publish();
2309
- return draft.targets.some((target) => !target.locked);
2390
+ const editableDependencies = draft.targets.filter((target) => !target.locked);
2391
+ return editableDependencies.some((target) => draft.targets.some((candidate) => candidate.pluginId === target.pluginId
2392
+ && candidate.platform !== target.platform
2393
+ && candidate.scope !== target.scope));
2310
2394
  }
2311
2395
  catch (error) {
2312
2396
  if (error instanceof Error && error.name === 'AbortError')
@@ -2616,7 +2700,16 @@ export class TuiController {
2616
2700
  }
2617
2701
  recheckCatalogSource(sourceId) {
2618
2702
  this.#assertAcceptingActions();
2619
- return this.#catalogSourcesSession.recheck(sourceId);
2703
+ return this.#catalogSourcesSession.refresh(sourceId, this.#sourceCheckNeedsFullValidation(sourceId));
2704
+ }
2705
+ #sourceCheckNeedsFullValidation(sourceId) {
2706
+ const generation = this.#catalogGenerations.get(sourceId);
2707
+ const freshness = this.#catalogFreshness.get(sourceId);
2708
+ const activeReady = generation?.state === 'ready';
2709
+ return !activeReady
2710
+ || freshness?.state === 'incompatible'
2711
+ || freshness?.state === 'versionConflict'
2712
+ || freshness?.state === 'rollbackDetected';
2620
2713
  }
2621
2714
  async recheckAllCatalogSources() {
2622
2715
  this.#assertAcceptingActions();
@@ -2627,8 +2720,9 @@ export class TuiController {
2627
2720
  const worker = async () => {
2628
2721
  while (next < sourceIds.length) {
2629
2722
  const sourceId = sourceIds[next++];
2630
- if (sourceId)
2631
- await this.#catalogSourcesSession.recheck(sourceId);
2723
+ if (sourceId) {
2724
+ await this.#catalogSourcesSession.refresh(sourceId, this.#sourceCheckNeedsFullValidation(sourceId));
2725
+ }
2632
2726
  }
2633
2727
  };
2634
2728
  await Promise.all(Array.from({ length: Math.min(2, sourceIds.length) }, () => worker()));
@@ -2645,7 +2739,7 @@ export class TuiController {
2645
2739
  beginPluginWriteBatch() {
2646
2740
  this.#assertAcceptingActions();
2647
2741
  this.#router.beginBatchFlow();
2648
- this.#router.push({ name: 'batchSelection' });
2742
+ this.#router.replace({ name: 'batchSelection' }, this.#router.snapshot.current.context);
2649
2743
  this.#publish();
2650
2744
  }
2651
2745
  startPluginWriteBatchReview(intent) {
@@ -3210,6 +3304,36 @@ export class TuiController {
3210
3304
  ? evidence.source?.pluginVersion
3211
3305
  ?? this.#viewModel?.plugins.find((plugin) => plugin.pluginId === intent.pluginId)?.version
3212
3306
  : undefined;
3307
+ const rootSummary = intent.kind === 'pluginWriteTask'
3308
+ ? [...new Set(evidence.taskPlan.requestedTargets
3309
+ .map((target) => target.pluginId))].map((rootPluginId) => {
3310
+ const targets = evidence.taskPlan.requestedTargets
3311
+ .filter((target) => target.pluginId === rootPluginId);
3312
+ const versions = [...new Set(targets.flatMap((target) => target.expectedUpdate?.activeVersion ? [target.expectedUpdate.activeVersion] : []))];
3313
+ return {
3314
+ pluginId: rootPluginId,
3315
+ ...(versions.length === 1 ? { version: versions[0] } : {}),
3316
+ targets: targets.map(({ platform, scope }) => ({ platform, scope })),
3317
+ };
3318
+ })
3319
+ : intent.kind === 'pluginWriteBatch'
3320
+ ? evidence.requestedPluginIds.map((rootPluginId) => {
3321
+ const batchEvidence = evidence;
3322
+ const targetVersions = batchEvidence.targetVersions?.filter((target) => target.pluginId === rootPluginId) ?? [];
3323
+ const versions = [...new Set(targetVersions.flatMap((target) => {
3324
+ const version = intent.action === 'uninstall'
3325
+ ? target.installedVersion
3326
+ : target.availableVersion ?? target.installedVersion;
3327
+ return version ? [version] : [];
3328
+ }))];
3329
+ return {
3330
+ pluginId: rootPluginId,
3331
+ ...(versions.length === 1 ? { version: versions[0] } : {}),
3332
+ targets: batchEvidence.groups.filter((group) => group.requestedPluginIds.includes(rootPluginId))
3333
+ .map(({ platform, scope }) => ({ platform, scope })),
3334
+ };
3335
+ })
3336
+ : undefined;
3213
3337
  const review = {
3214
3338
  operationId,
3215
3339
  reviewToken,
@@ -3250,6 +3374,7 @@ export class TuiController {
3250
3374
  causeCount: evidence.taskPlan.causes.length,
3251
3375
  },
3252
3376
  } : {}),
3377
+ ...(rootSummary && rootSummary.length > 0 ? { rootSummary } : {}),
3253
3378
  ...(evidence.source && 'catalogVersion' in evidence.source
3254
3379
  && typeof evidence.source.catalogVersion === 'string'
3255
3380
  && evidence.source.snapshotDigest