@lesliechan721/aw 0.1.0-beta.25 → 0.1.0-beta.27

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 (54) hide show
  1. package/dist/src/portable/commands.d.ts +39 -5
  2. package/dist/src/portable/commands.js +249 -30
  3. package/dist/src/portable/commands.js.map +1 -1
  4. package/dist/src/portable/skills-cli.d.ts +15 -7
  5. package/dist/src/portable/skills-cli.js +35 -19
  6. package/dist/src/portable/skills-cli.js.map +1 -1
  7. package/dist/src/tui/catalog-sources/session.js +9 -0
  8. package/dist/src/tui/catalog-sources/session.js.map +1 -1
  9. package/dist/src/tui/catalog-store-task.d.ts +16 -0
  10. package/dist/src/tui/catalog-store-task.js +19 -0
  11. package/dist/src/tui/catalog-store-task.js.map +1 -0
  12. package/dist/src/tui/components.d.ts +15 -0
  13. package/dist/src/tui/components.js +24 -3
  14. package/dist/src/tui/components.js.map +1 -1
  15. package/dist/src/tui/console-app-v4.d.ts +2 -1
  16. package/dist/src/tui/console-app-v4.js +2147 -1476
  17. package/dist/src/tui/console-app-v4.js.map +1 -1
  18. package/dist/src/tui/controller-runtime.js +396 -16
  19. package/dist/src/tui/controller-runtime.js.map +1 -1
  20. package/dist/src/tui/controller.d.ts +131 -41
  21. package/dist/src/tui/controller.js +493 -33
  22. package/dist/src/tui/controller.js.map +1 -1
  23. package/dist/src/tui/dependency-graph.d.ts +11 -0
  24. package/dist/src/tui/dependency-graph.js +19 -0
  25. package/dist/src/tui/dependency-graph.js.map +1 -1
  26. package/dist/src/tui/diagnostic-projection.js +83 -0
  27. package/dist/src/tui/diagnostic-projection.js.map +1 -1
  28. package/dist/src/tui/model.d.ts +4 -1
  29. package/dist/src/tui/model.js +18 -13
  30. package/dist/src/tui/model.js.map +1 -1
  31. package/dist/src/tui/operation-diagnostic-screens.js +3 -2
  32. package/dist/src/tui/operation-diagnostic-screens.js.map +1 -1
  33. package/dist/src/tui/operation-review-planner.js +95 -8
  34. package/dist/src/tui/operation-review-planner.js.map +1 -1
  35. package/dist/src/tui/plugin-content.d.ts +23 -0
  36. package/dist/src/tui/plugin-content.js +336 -0
  37. package/dist/src/tui/plugin-content.js.map +1 -0
  38. package/dist/src/tui/router.d.ts +27 -9
  39. package/dist/src/tui/router.js +13 -9
  40. package/dist/src/tui/router.js.map +1 -1
  41. package/dist/src/tui/screen-registry.d.ts +81 -14
  42. package/dist/src/tui/screen-registry.js +46 -17
  43. package/dist/src/tui/screen-registry.js.map +1 -1
  44. package/dist/src/tui/session-console-app.js +1 -1
  45. package/dist/src/tui/session-console-app.js.map +1 -1
  46. package/dist/src/tui/session-host.d.ts +1 -0
  47. package/dist/src/tui/session-host.js +13 -1
  48. package/dist/src/tui/session-host.js.map +1 -1
  49. package/dist/src/utils/codex-reconciler.js +5 -1
  50. package/dist/src/utils/codex-reconciler.js.map +1 -1
  51. package/dist/src/utils/codex-surface-analysis.d.ts +1 -0
  52. package/dist/src/utils/codex-surface-analysis.js +2 -0
  53. package/dist/src/utils/codex-surface-analysis.js.map +1 -1
  54. package/package.json +3 -3
@@ -1,7 +1,7 @@
1
1
  import crypto from 'node:crypto';
2
2
  import { CatalogGenerationError } from '../utils/catalog-generation-error.js';
3
3
  import { readSafeOperationError } from '../utils/safe-operation-diagnostic.js';
4
- import { flattenStatusTargets, statusTargetActivation, statusTargetIntent, statusTargetOwnershipState, statusTargetSharedNative, } from '../commands/status-v6.js';
4
+ import { flattenStatusTargets, statusTargetActivation, statusTargetExtensions, statusTargetIntent, statusTargetOwnershipState, statusTargetSharedNative, } from '../commands/status-v6.js';
5
5
  import { buildRebuildReceiptJson, requestedIntentAction } from '../output/command-receipt.js';
6
6
  import { buildOperationResultModel } from '../commands/operation-planner.js';
7
7
  import { buildOperationReviewProjection } from './operation-review-planner.js';
@@ -86,6 +86,86 @@ function reviewPreparationStageLabel(stage) {
86
86
  evidence: '准备确认信息',
87
87
  }[stage];
88
88
  }
89
+ function projectReminderTargetKey(target) {
90
+ return [target.pluginId, target.platform, target.scope].join('\0');
91
+ }
92
+ export function projectCurrentIntentReminder(status) {
93
+ const actionableTargets = [];
94
+ const blockedTargets = [];
95
+ const unconfirmedTargets = [];
96
+ const projectRoots = new Map((status.projectIntentRoots ?? []).map((root) => [
97
+ root.pluginId,
98
+ new Set(root.platforms),
99
+ ]));
100
+ const projectTargets = flattenStatusTargets(status.payload.targets).filter((target) => {
101
+ const intent = statusTargetIntent(target);
102
+ return target.scope === 'project'
103
+ && projectRoots.get(target.pluginId)?.has(target.platform) === true
104
+ && intent.scopes.includes('project');
105
+ });
106
+ for (const target of projectTargets) {
107
+ if (target.state === 'ready')
108
+ continue;
109
+ const reminderTarget = {
110
+ pluginId: target.pluginId,
111
+ platform: target.platform,
112
+ scope: 'project',
113
+ };
114
+ const statusReasons = [
115
+ ...target.reasons,
116
+ ...(target.sharedReasons ?? []),
117
+ ];
118
+ const reasonCodes = [...new Set(statusReasons.flatMap((reason) => [
119
+ reason.code,
120
+ ...(reason.rootIssueCode ? [reason.rootIssueCode] : []),
121
+ ]))].sort((left, right) => left.localeCompare(right, 'en'));
122
+ const sharedNative = statusTargetSharedNative(target);
123
+ const activation = statusTargetActivation(target);
124
+ const extensions = statusTargetExtensions(target);
125
+ const ownership = statusTargetOwnershipState(target);
126
+ const blocking = target.state === 'blocked'
127
+ || [...target.reasons, ...(target.sharedReasons ?? [])]
128
+ .some((reason) => reason.severity === 'blocking');
129
+ const unobserved = ownership === 'error'
130
+ || (!blocking && (sharedNative?.state === 'unobserved'
131
+ || activation?.evidence === 'unobserved'
132
+ || reasonCodes.includes('codexSharedNativeUnobserved')
133
+ || target.components.some((component) => component.metadata?.rootIssueCode === 'codexSharedNativeUnobserved')));
134
+ const confirmedDifference = ownership === 'absent' || ownership === 'partial'
135
+ || sharedNative?.state === 'missing' || sharedNative?.state === 'replaceable'
136
+ || extensions?.state === 'missing' || extensions?.state === 'drifted'
137
+ || activation?.evidence === 'observed' && activation.project === false
138
+ || target.components.some((component) => component.kind === 'dependency'
139
+ && (component.state === 'absent' || component.state === 'incomplete'));
140
+ if (blocking && confirmedDifference) {
141
+ blockedTargets.push({ ...reminderTarget, reasonCodes });
142
+ }
143
+ else if (!unobserved && target.state === 'needsSync' && confirmedDifference) {
144
+ actionableTargets.push({ ...reminderTarget, reasonCodes });
145
+ }
146
+ else {
147
+ unconfirmedTargets.push(reminderTarget);
148
+ }
149
+ }
150
+ if (projectTargets.length === 0)
151
+ return undefined;
152
+ const canonicalActionable = actionableTargets.sort((left, right) => projectReminderTargetKey(left).localeCompare(projectReminderTargetKey(right), 'en'));
153
+ const canonicalBlocked = blockedTargets.sort((left, right) => projectReminderTargetKey(left).localeCompare(projectReminderTargetKey(right), 'en'));
154
+ const canonicalUnconfirmed = unconfirmedTargets.sort((left, right) => projectReminderTargetKey(left).localeCompare(projectReminderTargetKey(right), 'en'));
155
+ const projectRoot = status.projectRoot ?? projectTargets[0].cwd;
156
+ const signature = crypto.createHash('sha256').update(JSON.stringify({
157
+ projectRoot,
158
+ actionableTargets: canonicalActionable.map(projectReminderTargetKey),
159
+ blockedTargets: canonicalBlocked.map(projectReminderTargetKey),
160
+ })).digest('hex');
161
+ return {
162
+ projectRoot,
163
+ signature: `sha256:${signature}`,
164
+ actionableTargets: canonicalActionable,
165
+ blockedTargets: canonicalBlocked,
166
+ unconfirmedTargets: canonicalUnconfirmed,
167
+ };
168
+ }
89
169
  export class TuiOperationBlockedError extends Error {
90
170
  code;
91
171
  diagnostics;
@@ -140,10 +220,14 @@ function normalizeOperationIntent(intent) {
140
220
  }
141
221
  if (intent.kind !== 'pluginWriteBatch')
142
222
  return intent;
143
- if (intent.selection.kind === 'targets') {
223
+ if (intent.selection.kind === 'targets'
224
+ || intent.selection.kind === 'currentProjectIntentTargets') {
144
225
  return {
145
226
  ...intent,
146
- selection: { kind: 'targets', targets: canonicalBatchTargets(intent.selection.targets) },
227
+ selection: {
228
+ kind: intent.selection.kind,
229
+ targets: canonicalBatchTargets(intent.selection.targets),
230
+ },
147
231
  };
148
232
  }
149
233
  if (intent.selection.kind !== 'selected')
@@ -352,9 +436,6 @@ export function pluginWriteTaskFailedRequestedTargets(execution) {
352
436
  }));
353
437
  }
354
438
  function collectOperationSensitiveValues(intent) {
355
- if (intent.kind !== 'configSave')
356
- return [];
357
- const sensitiveKeys = new Set([...(intent.sensitiveKeys ?? []), ...(intent.privateKeys ?? [])]);
358
439
  const values = [];
359
440
  const visit = (value) => {
360
441
  if (typeof value === 'string')
@@ -364,9 +445,21 @@ function collectOperationSensitiveValues(intent) {
364
445
  else if (value && typeof value === 'object')
365
446
  Object.values(value).forEach(visit);
366
447
  };
367
- for (const [key, value] of Object.entries(intent.updates ?? {})) {
368
- if (sensitiveKeys.has(key))
369
- visit(value);
448
+ const collect = (operation) => {
449
+ const sensitiveKeys = new Set([
450
+ ...(operation.sensitiveKeys ?? []),
451
+ ...(operation.privateKeys ?? []),
452
+ ]);
453
+ for (const [key, value] of Object.entries(operation.updates ?? {})) {
454
+ if (sensitiveKeys.has(key))
455
+ visit(value);
456
+ }
457
+ };
458
+ if (intent.kind === 'configSave')
459
+ collect(intent);
460
+ else if (intent.kind === 'configSaveBatch') {
461
+ for (const group of intent.groups)
462
+ collect(group);
370
463
  }
371
464
  return values;
372
465
  }
@@ -399,6 +492,14 @@ function cloneFrozen(value) {
399
492
  return deepFreeze(structuredClone(value));
400
493
  }
401
494
  export function operationEvidenceFingerprint(evidence) {
495
+ if (evidence.catalogStore) {
496
+ return crypto.createHash('sha256')
497
+ .update(JSON.stringify(stableValue({
498
+ catalog: evidence.catalog,
499
+ authorizedItems: evidence.catalogStore.authorizedItems,
500
+ })))
501
+ .digest('hex');
502
+ }
402
503
  if ('taskPlan' in evidence) {
403
504
  const task = evidence.taskPlan;
404
505
  return crypto.createHash('sha256')
@@ -470,10 +571,19 @@ export function operationEvidenceFingerprint(evidence) {
470
571
  const config = staleEvidence.config
471
572
  ? (({ shareability: _shareability, ...identity }) => identity)(staleEvidence.config)
472
573
  : undefined;
574
+ const configBatch = staleEvidence.configBatch
575
+ ? {
576
+ groups: staleEvidence.configBatch.groups.map((group) => ({
577
+ ...group,
578
+ config: (({ shareability: _shareability, ...identity }) => identity)(group.config),
579
+ })),
580
+ }
581
+ : undefined;
473
582
  return crypto.createHash('sha256')
474
583
  .update(JSON.stringify(stableValue({
475
584
  ...staleEvidence,
476
585
  ...(config ? { config } : {}),
586
+ ...(configBatch ? { configBatch } : {}),
477
587
  })))
478
588
  .digest('hex');
479
589
  }
@@ -545,6 +655,24 @@ function issueCode(error) {
545
655
  const prefix = message.split(':', 1)[0];
546
656
  return /^[A-Za-z][A-Za-z0-9]*$/u.test(prefix) ? prefix : 'operationBlocked';
547
657
  }
658
+ function isPortableInstallReceipt(value) {
659
+ return typeof value === 'object' && value !== null
660
+ && value.receiptSchemaVersion === 5
661
+ && value.command === 'skill:install';
662
+ }
663
+ function completedSinglePluginId(intent, result) {
664
+ if (result.state !== 'succeeded' || result.summary.warnings?.length)
665
+ return undefined;
666
+ if (intent.kind === 'pluginWrite')
667
+ return intent.originIssue ? undefined : intent.pluginId;
668
+ const pluginIds = intent.kind === 'pluginWriteBatch'
669
+ ? result.summary.batch?.requestedPluginIds ?? []
670
+ : intent.kind === 'pluginWriteTask'
671
+ ? intent.targets.map((target) => target.pluginId)
672
+ : [];
673
+ const unique = [...new Set(pluginIds)];
674
+ return unique.length === 1 ? unique[0] : undefined;
675
+ }
548
676
  export function extractPortableError(error) {
549
677
  const typedCode = typeof error === 'object' && error !== null
550
678
  && 'code' in error && typeof error.code === 'string'
@@ -572,6 +700,9 @@ export class TuiController {
572
700
  #reviewPreparationIntents = new Map();
573
701
  #inflightOperations = new Map();
574
702
  #pendingReads = new Set();
703
+ #pluginContent = new Map();
704
+ #pluginContentInflight = new Map();
705
+ #pluginContentEpoch = 0;
575
706
  #abort = new AbortController();
576
707
  #progressUnsubscribe;
577
708
  #progressFeed;
@@ -591,6 +722,10 @@ export class TuiController {
591
722
  #result;
592
723
  #resultAnchor;
593
724
  #portableResultSkills;
725
+ #portableResultReceipt;
726
+ #portableResultIntent;
727
+ #completionNotice;
728
+ #projectIntentReminder;
594
729
  #loadError;
595
730
  #portableDraft;
596
731
  #recoverablePortableDraft;
@@ -700,6 +835,10 @@ export class TuiController {
700
835
  : {}),
701
836
  } }
702
837
  : {}),
838
+ ...(this.#completionNotice ? { completionNotice: this.#completionNotice } : {}),
839
+ ...(this.#projectIntentReminder
840
+ ? { projectIntentReminder: this.#projectIntentReminder }
841
+ : {}),
703
842
  });
704
843
  }
705
844
  subscribe(listener) {
@@ -828,6 +967,7 @@ export class TuiController {
828
967
  this.#result = undefined;
829
968
  this.#resultAnchor = undefined;
830
969
  this.#portableResultSkills = undefined;
970
+ this.#completionNotice = undefined;
831
971
  this.#progress = { messages: [] };
832
972
  this.#progressTargets = [];
833
973
  this.#router.resetToRoot();
@@ -1022,6 +1162,7 @@ export class TuiController {
1022
1162
  #project() {
1023
1163
  if (!this.#status)
1024
1164
  return;
1165
+ this.#projectIntentReminder = projectCurrentIntentReminder(this.#status);
1025
1166
  this.#viewModel = buildTuiViewModel({
1026
1167
  status: this.#status.payload,
1027
1168
  pluginSummaries: this.#status.pluginSummaries,
@@ -1031,6 +1172,10 @@ export class TuiController {
1031
1172
  catalogFreshness: this.#catalogFreshness,
1032
1173
  catalogGenerations: this.#catalogGenerations,
1033
1174
  dependencyOverviews: this.#status.dependencyOverviews,
1175
+ contentOverviews: {
1176
+ ...(this.#status.contentOverviews ?? {}),
1177
+ ...Object.fromEntries(this.#pluginContent),
1178
+ },
1034
1179
  portablePluginIds: this.#status.portablePluginIds,
1035
1180
  configStoreInventory: this.#status.configStoreInventory,
1036
1181
  });
@@ -1088,7 +1233,13 @@ export class TuiController {
1088
1233
  this.#router.beginPortableFlow(intent.pluginId);
1089
1234
  return;
1090
1235
  }
1091
- if (intent.kind === 'configSave' || intent.kind === 'configReset') {
1236
+ if (intent.kind === 'catalogStorePrune') {
1237
+ if (active?.kind === 'catalogStore')
1238
+ return;
1239
+ this.#router.beginFlow('catalogStore', { name: 'catalogStoreOverview' });
1240
+ return;
1241
+ }
1242
+ if (intent.kind === 'configSave' || intent.kind === 'configSaveBatch' || intent.kind === 'configReset') {
1092
1243
  if (active?.kind === 'configEdit')
1093
1244
  return;
1094
1245
  const route = this.#router.snapshot.current.route;
@@ -1120,6 +1271,15 @@ export class TuiController {
1120
1271
  }
1121
1272
  #stableResultRoute(intent, result) {
1122
1273
  const active = this.#router.activeFlow();
1274
+ if (intent.kind === 'pluginWriteTask' && active?.kind === 'pluginWriteTask'
1275
+ && (active.parentFlowId || active.returnTarget.name === 'pluginWriteTask'
1276
+ && active.returnTarget.goal !== intent.goal)) {
1277
+ return this.#normalizeRoute(active.returnTarget);
1278
+ }
1279
+ const completedPluginId = completedSinglePluginId(intent, result);
1280
+ if (completedPluginId) {
1281
+ return this.#normalizeRoute({ name: 'pluginDetail', pluginId: completedPluginId });
1282
+ }
1123
1283
  if (intent.kind === 'pluginWriteTask'
1124
1284
  && result.state === 'succeeded'
1125
1285
  && 'returnRoute' in result
@@ -1128,10 +1288,15 @@ export class TuiController {
1128
1288
  && result.returnRoute.batchOrigin?.owner === 'batch-write') {
1129
1289
  return { name: 'pluginList', mode: 'installed' };
1130
1290
  }
1291
+ if (intent.kind === 'portableInstall' && result.state === 'succeeded') {
1292
+ return this.#normalizeRoute({ name: 'pluginDetail', pluginId: intent.pluginId });
1293
+ }
1294
+ if (intent.kind === 'catalogStorePrune')
1295
+ return { name: 'catalogStoreOverview' };
1131
1296
  if (active?.parentFlowId)
1132
1297
  return this.#normalizeRoute(active.returnTarget);
1133
1298
  if (active && (intent.kind === 'portableInstall'
1134
- || intent.kind === 'configSave' || intent.kind === 'configReset')) {
1299
+ || intent.kind === 'configSave' || intent.kind === 'configSaveBatch' || intent.kind === 'configReset')) {
1135
1300
  return this.#normalizeRoute(active.returnTarget);
1136
1301
  }
1137
1302
  if (intent.kind === 'pluginWrite') {
@@ -1139,11 +1304,18 @@ export class TuiController {
1139
1304
  return { name: 'issueList' };
1140
1305
  return this.#normalizeRoute({ name: 'pluginDetail', pluginId: intent.pluginId });
1141
1306
  }
1307
+ if (intent.kind === 'pluginWriteBatch' && intent.selection.kind === 'targets') {
1308
+ const pluginIds = [...new Set(intent.selection.targets.map((target) => target.pluginId))];
1309
+ if (pluginIds.length === 1) {
1310
+ return this.#normalizeRoute({ name: 'pluginDetail', pluginId: pluginIds[0] });
1311
+ }
1312
+ }
1142
1313
  if (intent.kind === 'pluginWriteBatch')
1143
1314
  return { name: 'pluginList', mode: 'installed' };
1144
1315
  if (intent.kind === 'pluginWriteTask')
1145
1316
  return { name: 'pluginList', mode: 'installed' };
1146
- if (intent.kind === 'portableInstall' || intent.kind === 'configSave' || intent.kind === 'configReset') {
1317
+ if (intent.kind === 'portableInstall' || intent.kind === 'configSave'
1318
+ || intent.kind === 'configSaveBatch' || intent.kind === 'configReset') {
1147
1319
  return this.#normalizeRoute({ name: 'pluginDetail', pluginId: intent.pluginId });
1148
1320
  }
1149
1321
  if (intent.kind === 'sourceMutation' && intent.action === 'update') {
@@ -1188,7 +1360,9 @@ export class TuiController {
1188
1360
  ? {
1189
1361
  ...structuredClone(intent),
1190
1362
  selection: {
1191
- kind: 'targets',
1363
+ kind: intent.selection.kind === 'currentProjectIntentRoots'
1364
+ || intent.selection.kind === 'currentProjectIntentTargets'
1365
+ ? 'currentProjectIntentTargets' : 'targets',
1192
1366
  targets: structuredClone(result.summary.batch.retryTargets),
1193
1367
  },
1194
1368
  }
@@ -1214,13 +1388,45 @@ export class TuiController {
1214
1388
  && result.returnRoute.batchOrigin?.owner === 'batch-write'
1215
1389
  ? result.returnRoute.batchOrigin.flowId
1216
1390
  : undefined;
1391
+ const completedPluginId = completedSinglePluginId(intent, result);
1392
+ if (completedPluginId && target.name === 'pluginDetail') {
1393
+ this.#recordTerminalResult();
1394
+ const action = intent.kind === 'pluginWrite' || intent.kind === 'pluginWriteBatch'
1395
+ ? intent.action : 'sync';
1396
+ this.#completionNotice = {
1397
+ pluginId: completedPluginId,
1398
+ title: intent.kind === 'pluginWriteTask'
1399
+ ? intent.goal === 'update' ? '插件更新完成' : '插件恢复完成'
1400
+ : action === 'uninstall' ? '插件卸载完成'
1401
+ : action === 'install' ? '插件安装完成'
1402
+ : action === 'reinstall' ? '插件重新安装完成'
1403
+ : action === 'refreshAndSync' ? '插件更新完成'
1404
+ : action === 'cleanup' ? '插件清理完成' : '插件同步完成',
1405
+ description: action === 'uninstall'
1406
+ ? '所选实际位置已处理。插件设置保持不变。'
1407
+ : '状态已重新检查。下方详情显示当前实际位置。',
1408
+ ...(result.operationRecordId ? { operationRecordId: result.operationRecordId } : {}),
1409
+ };
1410
+ const finished = this.#router.finishFlow(target);
1411
+ if (!finished)
1412
+ this.#router.replace(target);
1413
+ return;
1414
+ }
1415
+ const portableParentFlowId = result.state === 'succeeded'
1416
+ && intent.kind === 'portableInstall'
1417
+ && this.#router.activeFlow()?.parentFlowId;
1217
1418
  const finished = batchFlowId
1218
1419
  ? this.#router.finishFlowById(batchFlowId, 'batchWrite', target, terminal)
1219
- : this.#router.finishFlow(target, terminal);
1420
+ : portableParentFlowId
1421
+ ? this.#router.finishFlowById(portableParentFlowId, 'pluginInstall', target, terminal)
1422
+ : this.#router.finishFlow(target, terminal);
1220
1423
  if (!finished)
1221
1424
  this.#router.replace(terminal);
1222
1425
  }
1223
1426
  #applyStatus(status) {
1427
+ this.#pluginContentEpoch += 1;
1428
+ this.#pluginContent.clear();
1429
+ this.#pluginContentInflight.clear();
1224
1430
  this.#status = structuredClone(status);
1225
1431
  this.#statusReadAtMs = this.dependencies.now?.() ?? Date.now();
1226
1432
  if (status.catalogGenerations) {
@@ -1287,6 +1493,9 @@ export class TuiController {
1287
1493
  this.#assertAcceptingActions();
1288
1494
  this.dependencies.beginCatalogSession?.();
1289
1495
  const generation = ++this.#generation;
1496
+ this.#pluginContentEpoch += 1;
1497
+ this.#pluginContent.clear();
1498
+ this.#pluginContentInflight.clear();
1290
1499
  this.#phase = 'loading';
1291
1500
  this.#publish();
1292
1501
  let status;
@@ -1320,7 +1529,7 @@ export class TuiController {
1320
1529
  && this.#router.activeFlow()?.kind !== 'sourceEdit') {
1321
1530
  this.#router.beginFlow('sourceEdit', this.#router.snapshot.current.route);
1322
1531
  }
1323
- this.#router.push(route, context);
1532
+ this.#router.push(route.name === 'pluginPreview' ? this.#normalizeRoute(route) : route, context);
1324
1533
  this.#publish();
1325
1534
  }
1326
1535
  hasPortableSkills(pluginId) {
@@ -1439,6 +1648,14 @@ export class TuiController {
1439
1648
  this.#portableDraft.canContinue = this.#portableDraft.totalCount > 0;
1440
1649
  this.#publish();
1441
1650
  }
1651
+ clearPortableSkills() {
1652
+ if (!this.#portableDraft)
1653
+ return;
1654
+ this.#portableDraft.candidates = this.#portableDraft.candidates.map((candidate) => ({ ...candidate, selected: false }));
1655
+ this.#portableDraft.selectedCount = 0;
1656
+ this.#portableDraft.canContinue = false;
1657
+ this.#publish();
1658
+ }
1442
1659
  setPortableAgentMode(mode, requestedAgents = []) {
1443
1660
  if (!this.#portableDraft)
1444
1661
  return;
@@ -1483,6 +1700,63 @@ export class TuiController {
1483
1700
  const intent = this.#portableInstallIntent();
1484
1701
  return this.startReviewPreparation(intent, this.#router.portableOrigin(intent.pluginId));
1485
1702
  }
1703
+ async recheckPortableResult() {
1704
+ this.#assertAcceptingActions();
1705
+ if (this.#router.snapshot.current.route.name !== 'result'
1706
+ || this.#result?.operationKind !== 'portableInstall'
1707
+ || !this.#portableResultReceipt
1708
+ || !this.#portableResultIntent
1709
+ || !this.dependencies.recheckPortableInstall) {
1710
+ throw new TuiOperationBlockedError('portableRecheckUnavailable', '当前结果不能执行只读复查。');
1711
+ }
1712
+ const checked = await this.#trackRead(this.dependencies.recheckPortableInstall(structuredClone(this.#portableResultReceipt), this.#abort.signal));
1713
+ this.#portableResultReceipt = structuredClone(checked);
1714
+ const summary = operationResultSummary(this.#portableResultIntent, checked);
1715
+ const previous = this.#result;
1716
+ this.#result = {
1717
+ state: checked.status === 'succeeded' ? 'succeeded' : 'partial',
1718
+ operationId: previous.operationId,
1719
+ summary: {
1720
+ ...summary,
1721
+ elapsedMs: 'summary' in previous && previous.summary?.elapsedMs !== undefined
1722
+ ? previous.summary.elapsedMs : summary.elapsedMs,
1723
+ },
1724
+ operationKind: 'portableInstall',
1725
+ pluginId: this.#portableResultIntent.pluginId,
1726
+ returnRoute: 'returnRoute' in previous && previous.returnRoute
1727
+ ? structuredClone(previous.returnRoute)
1728
+ : { name: 'pluginDetail', pluginId: this.#portableResultIntent.pluginId },
1729
+ };
1730
+ this.#publish();
1731
+ return cloneFrozen(checked);
1732
+ }
1733
+ startPortableRetryReviewPreparation() {
1734
+ this.#assertAcceptingActions();
1735
+ const receipt = this.#portableResultReceipt;
1736
+ const original = this.#portableResultIntent;
1737
+ if (this.#router.snapshot.current.route.name !== 'result' || !receipt || !original) {
1738
+ throw new TuiOperationBlockedError('portableRetryUnavailable', '当前结果不能准备精确重试。');
1739
+ }
1740
+ if ((receipt.verification.unknown?.length ?? 0) > 0) {
1741
+ throw new TuiOperationBlockedError('portableRetryNotExact', '结果仍有未知目标。请先只读复查。');
1742
+ }
1743
+ const retryPairs = receipt.verification.missing;
1744
+ if (retryPairs.length === 0 || retryPairs.some((target) => !target.agent)) {
1745
+ throw new TuiOperationBlockedError('portableRetryNotExact', '没有可绑定到 Skill 与 Agent 的精确失败目标。');
1746
+ }
1747
+ const intent = {
1748
+ kind: 'portableInstall',
1749
+ pluginId: original.pluginId,
1750
+ scope: original.scope,
1751
+ selectedSkills: [...new Set(retryPairs.map((target) => target.skill))],
1752
+ requestedAgents: [...new Set(retryPairs.flatMap((target) => target.agent ? [target.agent] : []))],
1753
+ retry: { receipt: structuredClone(receipt) },
1754
+ };
1755
+ return this.startReviewPreparation(intent, {
1756
+ name: 'pluginDetail',
1757
+ pluginId: original.pluginId,
1758
+ });
1759
+ }
1486
1760
  discardPortableDraft() {
1487
1761
  this.#cancelPortableRequest();
1488
1762
  this.#portableDraft = undefined;
@@ -1494,11 +1768,11 @@ export class TuiController {
1494
1768
  this.#cancelInstallPlacementRequest();
1495
1769
  const current = this.#router.snapshot.current.route;
1496
1770
  if (current.name === 'portableBlocker' && current.code === 'requiredConfigMissing'
1497
- && route.name === 'configLayer' && route.pluginId === current.pluginId) {
1771
+ && route.name === 'configFields' && route.pluginId === current.pluginId) {
1498
1772
  this.#portableRepreparePluginId = current.pluginId;
1499
1773
  }
1500
1774
  this.#discardPortableDraftForRoute(route);
1501
- this.#router.replace(route, context);
1775
+ this.#router.replace(route.name === 'pluginPreview' ? this.#normalizeRoute(route) : route, context);
1502
1776
  this.#publish();
1503
1777
  }
1504
1778
  beginConfigFlow(pluginId) {
@@ -1660,6 +1934,7 @@ export class TuiController {
1660
1934
  this.#result = undefined;
1661
1935
  this.#resultAnchor = undefined;
1662
1936
  this.#portableResultSkills = undefined;
1937
+ this.#completionNotice = undefined;
1663
1938
  this.#publish();
1664
1939
  return;
1665
1940
  }
@@ -1671,7 +1946,8 @@ export class TuiController {
1671
1946
  return;
1672
1947
  }
1673
1948
  if (this.#result?.state === 'succeeded'
1674
- && (this.#result.operationKind === 'configSave' || this.#result.operationKind === 'configReset')
1949
+ && (this.#result.operationKind === 'configSave' || this.#result.operationKind === 'configSaveBatch'
1950
+ || this.#result.operationKind === 'configReset')
1675
1951
  && this.#result.pluginId
1676
1952
  && this.#router.resetConfigFlow(this.#result.pluginId, target)) {
1677
1953
  this.#result = undefined;
@@ -1687,6 +1963,7 @@ export class TuiController {
1687
1963
  this.#result = undefined;
1688
1964
  this.#resultAnchor = undefined;
1689
1965
  this.#portableResultSkills = undefined;
1966
+ this.#completionNotice = undefined;
1690
1967
  this.#recoverableInstallPlacementDraft = undefined;
1691
1968
  this.#recoverablePortableDraft = undefined;
1692
1969
  this.#publish();
@@ -1698,7 +1975,8 @@ export class TuiController {
1698
1975
  return;
1699
1976
  const returnRoute = this.#normalizeRoute(result.returnRoute);
1700
1977
  this.#router.back();
1701
- const kind = result.operationKind === 'configSave' || result.operationKind === 'configReset'
1978
+ const kind = result.operationKind === 'configSave' || result.operationKind === 'configSaveBatch'
1979
+ || result.operationKind === 'configReset'
1702
1980
  ? 'configEdit'
1703
1981
  : result.operationKind === 'portableInstall'
1704
1982
  ? 'portableInstall'
@@ -1728,6 +2006,7 @@ export class TuiController {
1728
2006
  if (readOnlyHistoryRoute && this.#router.activeFlow()?.kind === 'portableInstall')
1729
2007
  return;
1730
2008
  const portableRoute = route.name === 'portableSkillSelect'
2009
+ || route.name === 'portableSkillSummary'
1731
2010
  || route.name === 'portableDependencies'
1732
2011
  || route.name === 'portableAgentMode'
1733
2012
  || route.name === 'portableAgentInput'
@@ -1962,18 +2241,73 @@ export class TuiController {
1962
2241
  }
1963
2242
  listPlugins(options) {
1964
2243
  const query = options.query?.trim().toLocaleLowerCase('en');
1965
- const catalogPluginIds = new Set(this.#status?.pluginSummaries.map((plugin) => plugin.pluginId));
1966
2244
  return cloneFrozen((this.#viewModel?.plugins ?? []).filter((plugin) => (!options.sourceId || plugin.sourceId === options.sourceId)
1967
- && (options.mode === 'browse' ? catalogPluginIds.has(plugin.pluginId)
1968
- : hasInstalledPlacement(plugin) || plugin.savedConfigScopes.length > 0)
2245
+ && (options.installFilter !== 'installed'
2246
+ || hasInstalledPlacement(plugin))
1969
2247
  && (options.installFilter !== 'uninstalled' || !hasInstalledPlacement(plugin))
1970
2248
  && (options.installFilter !== 'updateAvailable' || plugin.update?.state === 'available')
2249
+ && (options.installFilter !== 'attention'
2250
+ || plugin.aggregateStatus === 'needsAttention' || plugin.aggregateStatus === 'unavailable'
2251
+ || plugin.primaryAttention !== undefined || plugin.sourceUnavailable === true
2252
+ || plugin.capabilityUnsupported === true)
1971
2253
  && (!query || `${plugin.displayName}\n${plugin.description}\n${plugin.sourceId}`.toLocaleLowerCase('en').includes(query))));
1972
2254
  }
1973
2255
  getPluginOverview(pluginId) {
1974
2256
  const plugin = this.#viewModel?.plugins.find((entry) => entry.pluginId === pluginId);
1975
2257
  return plugin ? cloneFrozen(plugin) : undefined;
1976
2258
  }
2259
+ getPluginContentOverview(pluginId) {
2260
+ const overview = this.#viewModel?.contentOverviews[pluginId];
2261
+ return overview ? cloneFrozen(overview) : undefined;
2262
+ }
2263
+ getPluginContentState(pluginId) {
2264
+ if (this.#viewModel?.contentOverviews[pluginId])
2265
+ return 'ready';
2266
+ return this.#pluginContentInflight.has(pluginId) ? 'loading' : 'idle';
2267
+ }
2268
+ loadPluginContent(pluginId) {
2269
+ if (this.#viewModel?.contentOverviews[pluginId])
2270
+ return Promise.resolve();
2271
+ const current = this.#pluginContentInflight.get(pluginId);
2272
+ if (current)
2273
+ return current;
2274
+ const loader = this.dependencies.loadPluginContent;
2275
+ if (!loader)
2276
+ return Promise.resolve();
2277
+ const generation = this.#generation;
2278
+ const contentEpoch = this.#pluginContentEpoch;
2279
+ const promise = this.#trackRead(loader(pluginId, this.#abort.signal))
2280
+ .then((overview) => {
2281
+ if (this.#disposed || generation !== this.#generation
2282
+ || contentEpoch !== this.#pluginContentEpoch)
2283
+ return;
2284
+ this.#pluginContent.set(pluginId, structuredClone(overview));
2285
+ this.#project();
2286
+ })
2287
+ .catch(() => {
2288
+ if (this.#disposed || generation !== this.#generation
2289
+ || contentEpoch !== this.#pluginContentEpoch)
2290
+ return;
2291
+ this.#pluginContent.set(pluginId, {
2292
+ pluginId,
2293
+ pluginVersion: '待确认',
2294
+ catalogVersion: '待确认',
2295
+ evidence: 'unavailable',
2296
+ items: [],
2297
+ notices: ['当前无法安全读取这个插件的内容。'],
2298
+ });
2299
+ this.#project();
2300
+ })
2301
+ .finally(() => {
2302
+ if (this.#pluginContentInflight.get(pluginId) !== promise)
2303
+ return;
2304
+ this.#pluginContentInflight.delete(pluginId);
2305
+ this.#publish();
2306
+ });
2307
+ this.#pluginContentInflight.set(pluginId, promise);
2308
+ this.#publish();
2309
+ return promise;
2310
+ }
1977
2311
  getPluginSourceIdentity(pluginId) {
1978
2312
  return flattenStatusTargets(this.#status?.payload.targets ?? []).find((target) => target.pluginId === pluginId)?.sourceIdentity;
1979
2313
  }
@@ -2207,7 +2541,9 @@ export class TuiController {
2207
2541
  this.#publish();
2208
2542
  }
2209
2543
  startPluginWriteBatchReview(intent) {
2210
- return this.startReviewPreparation(intent, { name: 'batchPlacement' });
2544
+ return this.startReviewPreparation(intent, intent.action === 'install'
2545
+ ? { name: 'batchInstallScope' }
2546
+ : { name: 'batchPlacement' });
2211
2547
  }
2212
2548
  startPluginWriteTaskReview(intent, returnRoute) {
2213
2549
  return this.startReviewPreparation(intent, returnRoute);
@@ -2347,6 +2683,13 @@ export class TuiController {
2347
2683
  }
2348
2684
  return cloneFrozen(await this.#trackRead(this.dependencies.inspectSourceRemoval(sourceId, this.#abort.signal)));
2349
2685
  }
2686
+ async inspectCatalogStore() {
2687
+ this.#assertAcceptingActions();
2688
+ if (!this.dependencies.inspectCatalogStore) {
2689
+ throw new TuiOperationBlockedError('catalogStoreUnavailable', 'Catalog 本地存储检查不可用。');
2690
+ }
2691
+ return cloneFrozen(await this.#trackRead(this.dependencies.inspectCatalogStore(this.#abort.signal)));
2692
+ }
2350
2693
  async getConfigGroups(pluginId, layer) {
2351
2694
  this.#assertAcceptingActions();
2352
2695
  if (!this.dependencies.getConfigGroups) {
@@ -2412,6 +2755,7 @@ export class TuiController {
2412
2755
  }
2413
2756
  startReviewPreparation(intent, returnRoute) {
2414
2757
  this.#assertAcceptingActions();
2758
+ this.#completionNotice = undefined;
2415
2759
  intent = normalizeOperationIntent(intent);
2416
2760
  if (intent.kind === 'catalogGeneration' && !intent.sourceMutation) {
2417
2761
  const blockReason = this.catalogSourceRecoveryBlockReason(intent);
@@ -2492,10 +2836,25 @@ export class TuiController {
2492
2836
  }
2493
2837
  }
2494
2838
  if (intent.kind === 'pluginWriteBatch') {
2495
- if (intent.selection.kind === 'currentProjectIntentRoots'
2839
+ if ((intent.selection.kind === 'currentProjectIntentRoots'
2840
+ || intent.selection.kind === 'currentProjectIntentTargets')
2496
2841
  && (intent.action !== 'sync' || intent.scope !== 'project')) {
2497
2842
  throw new TuiOperationBlockedError('batchSelectionInvalid', '当前项目全部同步只支持 project scope sync。');
2498
2843
  }
2844
+ if (intent.selection.kind === 'currentProjectIntentTargets') {
2845
+ const targets = intent.selection.targets;
2846
+ const canonical = [...new Map(targets.map((target) => [
2847
+ projectReminderTargetKey(target), target,
2848
+ ])).values()].sort((left, right) => projectReminderTargetKey(left)
2849
+ .localeCompare(projectReminderTargetKey(right), 'en'));
2850
+ if (targets.length === 0
2851
+ || targets.some((target) => target.scope !== 'project')
2852
+ || JSON.stringify(targets) !== JSON.stringify(canonical)) {
2853
+ throw new TuiOperationBlockedError('batchSelectionInvalid', '当前项目同步目标必须是非空、规范且精确的 project 目标集合。');
2854
+ }
2855
+ for (const target of targets)
2856
+ sourceIdFromPluginId(target.pluginId);
2857
+ }
2499
2858
  if (intent.selection.kind === 'selected') {
2500
2859
  const canonical = canonicalPluginIds(intent.selection.pluginIds);
2501
2860
  if (canonical.length === 0) {
@@ -2513,6 +2872,8 @@ export class TuiController {
2513
2872
  #reviewPreparationLabel(intent) {
2514
2873
  if (intent.kind === 'catalogSourcesRecovery')
2515
2874
  return 'Catalog 来源配置';
2875
+ if (intent.kind === 'catalogStorePrune')
2876
+ return 'Catalog 本地存储';
2516
2877
  if (intent.kind === 'pluginWriteTask')
2517
2878
  return `${intent.targets.length} 个安装位置`;
2518
2879
  if (intent.kind === 'pluginWriteBatch') {
@@ -2686,7 +3047,8 @@ export class TuiController {
2686
3047
  }
2687
3048
  async #logOperationError(error, code, intent) {
2688
3049
  await this.#logErrorSafe(error, code, {
2689
- privateConfigOperation: intent.kind === 'configSave' || intent.kind === 'configReset'
3050
+ privateConfigOperation: intent.kind === 'configSave' || intent.kind === 'configSaveBatch'
3051
+ || intent.kind === 'configReset'
2690
3052
  || intent.kind === 'sourceMutation',
2691
3053
  });
2692
3054
  }
@@ -2863,7 +3225,8 @@ export class TuiController {
2863
3225
  : ticket.intent.kind === 'pluginWriteTask'
2864
3226
  ? `${ticket.evidence.taskPlan.executableTargets.length} 个安装位置`
2865
3227
  : 'pluginId' in ticket.intent ? ticket.intent.pluginId
2866
- : 'sourceId' in ticket.intent ? ticket.intent.sourceId ?? 'Catalog 管理' : 'Catalog 来源配置',
3228
+ : 'sourceId' in ticket.intent ? ticket.intent.sourceId ?? 'Catalog 管理'
3229
+ : ticket.intent.kind === 'catalogStorePrune' ? 'Catalog 本地存储' : 'Catalog 来源配置',
2867
3230
  phase: '验证当前状态',
2868
3231
  },
2869
3232
  };
@@ -2932,6 +3295,10 @@ export class TuiController {
2932
3295
  const execution = ticket.intent.kind === 'catalogGeneration'
2933
3296
  ? await this.dependencies.executeIntent(structuredClone(ticket.intent), structuredClone(ticket.evidence), this.#abort.signal)
2934
3297
  : await this.dependencies.executeIntent(structuredClone(ticket.intent), structuredClone(ticket.evidence));
3298
+ if (ticket.intent.kind === 'portableInstall' && isPortableInstallReceipt(execution)) {
3299
+ this.#portableResultReceipt = structuredClone(execution);
3300
+ this.#portableResultIntent = structuredClone(ticket.intent);
3301
+ }
2935
3302
  const executionProjection = projectExecutionDiagnostics(execution, `${this.#sessionEpoch}:${operationId}`, collectOperationSensitiveValues(ticket.intent), ticket.intent.kind);
2936
3303
  this.#operationDiagnostics.set(operationId, {
2937
3304
  ...executionProjection,
@@ -2989,8 +3356,12 @@ export class TuiController {
2989
3356
  return this.#detachedBlocked(operationId, ticket.intent);
2990
3357
  const configCommitted = typeof execution === 'object'
2991
3358
  && execution !== null
2992
- && execution.configWrite === 'succeeded';
2993
- if ((ticket.intent.kind === 'configSave' || ticket.intent.kind === 'configReset')
3359
+ && (execution.configWrite === 'succeeded'
3360
+ || Array.isArray(execution.groups)
3361
+ && (execution.groups
3362
+ .some((group) => group.configSaved)));
3363
+ if ((ticket.intent.kind === 'configSave' || ticket.intent.kind === 'configSaveBatch'
3364
+ || ticket.intent.kind === 'configReset')
2994
3365
  && configCommitted) {
2995
3366
  const baseSummary = operationResultSummary(ticket.intent, execution);
2996
3367
  const elapsedMs = Math.max(0, (this.dependencies.now?.() ?? Date.now()) - startedAtMs);
@@ -3095,6 +3466,7 @@ export class TuiController {
3095
3466
  navigation: 'result',
3096
3467
  returnRoute: ticket.returnRoute,
3097
3468
  ...(ticket.intent.kind === 'pluginWriteBatch' || ticket.intent.kind === 'pluginWriteTask'
3469
+ || ticket.intent.kind === 'pluginWrite' && ticket.intent.action === 'uninstall'
3098
3470
  || dependencyFailure || dependencyStillRequired
3099
3471
  ? { summary: pluginSummary } : {}),
3100
3472
  ...(ticket.intent.kind !== 'pluginWriteBatch' && ticket.intent.kind !== 'pluginWriteTask'
@@ -3149,6 +3521,15 @@ export class TuiController {
3149
3521
  this.#publish();
3150
3522
  return cloneFrozen(result);
3151
3523
  }
3524
+ if (ticket.intent.kind === 'configSaveBatch') {
3525
+ return this.#setBlocked(operationId, 'operationFailed', {
3526
+ intent: ticket.intent,
3527
+ navigation: 'result',
3528
+ returnRoute: ticket.returnRoute,
3529
+ summary,
3530
+ writeMayHaveCompleted: false,
3531
+ });
3532
+ }
3152
3533
  return this.#setBlocked(operationId, 'operationFailed', {
3153
3534
  intent: ticket.intent,
3154
3535
  navigation: 'result',
@@ -3176,8 +3557,13 @@ export class TuiController {
3176
3557
  : { name: 'sourceDetail', sourceId: ticket.intent.sourceId }
3177
3558
  : ticket.returnRoute)),
3178
3559
  };
3179
- if ((ticket.intent.kind === 'configSave' || ticket.intent.kind === 'configReset')
3180
- && this.#portableRepreparePluginId === ticket.intent.pluginId) {
3560
+ const configResultNeedsAttention = result.state === 'succeeded' && (result.summary.messages?.some((message) => message.startsWith('警告:')
3561
+ || message === '提示:其它项目没有被扫描,可能需要在对应项目目录中运行 aw sync。') ?? false);
3562
+ if ((ticket.intent.kind === 'configSave' || ticket.intent.kind === 'configSaveBatch'
3563
+ || ticket.intent.kind === 'configReset')
3564
+ && this.#portableRepreparePluginId === ticket.intent.pluginId
3565
+ && result.state === 'succeeded'
3566
+ && !configResultNeedsAttention) {
3181
3567
  this.#operationDiagnostics.delete(operationId);
3182
3568
  this.#portableRepreparePluginId = undefined;
3183
3569
  this.#activeReview = undefined;
@@ -3471,6 +3857,20 @@ export class TuiController {
3471
3857
  }
3472
3858
  }
3473
3859
  function operationResultSummaryUnsafe(intent, execution) {
3860
+ if (intent.kind === 'catalogStorePrune') {
3861
+ const result = execution;
3862
+ return {
3863
+ code: result.overallState === 'succeeded' ? 'operationSucceeded' : 'operationPartial',
3864
+ message: result.overallState === 'succeeded'
3865
+ ? `Catalog 本地存储清理完成;删除 ${result.deleted.length} 项。`
3866
+ : `Catalog 本地存储部分清理完成;删除 ${result.deleted.length} 项。`,
3867
+ messages: [
3868
+ result.deletedBytes === null ? '已删除大小未知。' : `已删除 ${result.deletedBytes} bytes。`,
3869
+ `保留 ${result.retained.length} 项。`,
3870
+ ...(result.errors.length > 0 ? [`${result.errors.length} 项未能安全删除。`] : []),
3871
+ ],
3872
+ };
3873
+ }
3474
3874
  if (intent.kind === 'catalogSourcesRecovery') {
3475
3875
  const result = execution;
3476
3876
  return {
@@ -3519,9 +3919,10 @@ function operationResultSummaryUnsafe(intent, execution) {
3519
3919
  }
3520
3920
  if (intent.kind === 'portableInstall') {
3521
3921
  const receipt = execution;
3522
- const verificationUnknown = receipt.failures.some((failure) => failure.code === 'portableSkillsCliVerificationFailed'
3523
- && receipt.verification.records.length === 0
3524
- && receipt.verification.missing.length === 0);
3922
+ const verificationUnknown = (receipt.verification.unknown?.length ?? 0) > 0
3923
+ || receipt.failures.some((failure) => failure.code === 'portableSkillsCliVerificationFailed'
3924
+ && receipt.verification.records.length === 0
3925
+ && receipt.verification.missing.length === 0);
3525
3926
  return {
3526
3927
  code: receipt.status === 'succeeded' ? 'operationSucceeded'
3527
3928
  : receipt.status === 'partial' ? 'operationPartial' : 'operationFailed',
@@ -3540,7 +3941,9 @@ function operationResultSummaryUnsafe(intent, execution) {
3540
3941
  scope: receipt.scope,
3541
3942
  materializationDigest: receipt.materializationDigest,
3542
3943
  skillsCliVersion: receipt.skillsCliVersion,
3944
+ verified: receipt.verification.records,
3543
3945
  missing: receipt.verification.missing,
3946
+ unknown: receipt.verification.unknown ?? [],
3544
3947
  verificationUnknown,
3545
3948
  ...(receipt.failures[0]?.recovery ? { recovery: receipt.failures[0].recovery } : {}),
3546
3949
  ...(receipt.failures[0]?.code ? { failureCode: receipt.failures[0].code } : {}),
@@ -3805,6 +4208,62 @@ function operationResultSummaryUnsafe(intent, execution) {
3805
4208
  })),
3806
4209
  };
3807
4210
  }
4211
+ if (intent.kind === 'configSaveBatch') {
4212
+ const batch = execution;
4213
+ const savedCount = batch.groups.filter((group) => group.configSaved).length;
4214
+ const failedCount = batch.groups.filter((group) => group.status === 'failed').length;
4215
+ const retryScopes = batch.groups.filter((group) => group.configSaved && group.applyStatus === 'failed' && group.errorCode !== 'invalidConfig');
4216
+ const invalidConfig = savedCount === batch.groups.length
4217
+ && batch.groups.some((group) => group.configSaved && group.errorCode === 'invalidConfig')
4218
+ && retryScopes.length === 0;
4219
+ const publicWarnings = [...new Set(batch.groups.flatMap((group) => {
4220
+ if (!group.execution || typeof group.execution !== 'object'
4221
+ || !Array.isArray(group.execution.warnings))
4222
+ return [];
4223
+ return group.execution.warnings.filter((warning) => typeof warning === 'string' && warning.startsWith('projectConfigShareabilityUnverified:'));
4224
+ }))];
4225
+ const otherProjectsMayRequireSync = batch.groups.some((group) => group.scope === 'user' && group.execution && typeof group.execution === 'object'
4226
+ && group.execution.otherProjectsMayRequireSync === true);
4227
+ return {
4228
+ code: failedCount > 0
4229
+ ? savedCount > 0 ? 'operationPartial' : 'operationFailed'
4230
+ : 'operationSucceeded',
4231
+ message: invalidConfig
4232
+ ? '设置已保存,但没有应用。当前配置仍无效。'
4233
+ : failedCount > 0
4234
+ ? savedCount > 0
4235
+ ? `已保存 ${savedCount} 个设置层;${failedCount} 个设置层失败。`
4236
+ : '设置没有保存。'
4237
+ : '设置已保存。',
4238
+ pluginId: intent.pluginId,
4239
+ configApplyRetryAvailable: retryScopes.length > 0,
4240
+ ...(invalidConfig ? { configApplySkippedReason: 'invalidConfig' } : {}),
4241
+ configApplied: intent.apply
4242
+ ? batch.groups.some((group) => group.applyStatus === 'succeeded')
4243
+ : false,
4244
+ configGroups: batch.groups.map((group) => ({
4245
+ scope: group.scope,
4246
+ configSaved: group.configSaved,
4247
+ applyRequested: group.applyRequested,
4248
+ applyStatus: group.applyStatus,
4249
+ ...(group.errorCode ? { errorCode: safeDiagnosticCode(group.errorCode, 'configWriteFailed') } : {}),
4250
+ })),
4251
+ messages: [
4252
+ ...batch.groups.map((group) => {
4253
+ const label = group.scope === 'project' ? '当前项目' : '个人默认';
4254
+ return group.configSaved
4255
+ ? `${label}已保存${group.errorCode === 'invalidConfig'
4256
+ ? ',但当前配置无效,未应用'
4257
+ : group.applyStatus === 'failed' ? ',但应用失败' : ''}。`
4258
+ : `${label}保存失败。`;
4259
+ }),
4260
+ ...publicWarnings.map((warning) => `警告:${warning}`),
4261
+ ...(otherProjectsMayRequireSync
4262
+ ? ['提示:其它项目没有被扫描,可能需要在对应项目目录中运行 aw sync。']
4263
+ : []),
4264
+ ],
4265
+ };
4266
+ }
3808
4267
  if (intent.kind === 'configSave' || intent.kind === 'configReset') {
3809
4268
  const rawApply = typeof execution === 'object' && execution !== null
3810
4269
  && typeof execution.apply === 'object'
@@ -3878,6 +4337,7 @@ function operationResultSummaryUnsafe(intent, execution) {
3878
4337
  configScope: intent.scope,
3879
4338
  configApplyRetryAvailable: intent.kind === 'configSave'
3880
4339
  && intent.apply && !invalidConfig && (applyState === 'partial' || applyState === 'unavailable'),
4340
+ ...(intent.kind === 'configSave' && intent.retryApply ? { configRetryApply: true } : {}),
3881
4341
  ...(publicMessages.length > 0 ? { messages: publicMessages } : {}),
3882
4342
  ...(intent.kind === 'configSave'
3883
4343
  ? { configApplied: applyState === 'succeeded' || applyState === 'partial' }