@lesliechan721/aw 0.1.0-beta.18 → 0.1.0-beta.20

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 (47) hide show
  1. package/dist/src/commands/operation-planner.d.ts +3 -3
  2. package/dist/src/commands/rebuild-command.js +0 -5
  3. package/dist/src/commands/rebuild-command.js.map +1 -1
  4. package/dist/src/commands/rebuild-integration.d.ts +0 -1
  5. package/dist/src/commands/rebuild-integration.js +3 -152
  6. package/dist/src/commands/rebuild-integration.js.map +1 -1
  7. package/dist/src/commands/status-v6.d.ts +81 -31
  8. package/dist/src/commands/status-v6.js +494 -183
  9. package/dist/src/commands/status-v6.js.map +1 -1
  10. package/dist/src/commands/wizard.d.ts +2 -2
  11. package/dist/src/index.js +2 -2
  12. package/dist/src/tui/catalog-sources/projection.d.ts +2 -2
  13. package/dist/src/tui/catalog-sources/screens.js +17 -10
  14. package/dist/src/tui/catalog-sources/screens.js.map +1 -1
  15. package/dist/src/tui/console-app-v4.js +1300 -437
  16. package/dist/src/tui/console-app-v4.js.map +1 -1
  17. package/dist/src/tui/controller-runtime.d.ts +3 -3
  18. package/dist/src/tui/controller-runtime.js +114 -36
  19. package/dist/src/tui/controller-runtime.js.map +1 -1
  20. package/dist/src/tui/controller.d.ts +17 -7
  21. package/dist/src/tui/controller.js +269 -24
  22. package/dist/src/tui/controller.js.map +1 -1
  23. package/dist/src/tui/dependency-graph.d.ts +3 -0
  24. package/dist/src/tui/dependency-graph.js +37 -1
  25. package/dist/src/tui/dependency-graph.js.map +1 -1
  26. package/dist/src/tui/diagnostic-projection.d.ts +1 -0
  27. package/dist/src/tui/diagnostic-projection.js +51 -4
  28. package/dist/src/tui/diagnostic-projection.js.map +1 -1
  29. package/dist/src/tui/model.d.ts +22 -12
  30. package/dist/src/tui/model.js +251 -86
  31. package/dist/src/tui/model.js.map +1 -1
  32. package/dist/src/tui/operation-history.d.ts +1 -0
  33. package/dist/src/tui/operation-history.js +5 -0
  34. package/dist/src/tui/operation-history.js.map +1 -1
  35. package/dist/src/tui/operation-review-planner.js +67 -2
  36. package/dist/src/tui/operation-review-planner.js.map +1 -1
  37. package/dist/src/tui/plugin-write-task.d.ts +2 -0
  38. package/dist/src/tui/plugin-write-task.js.map +1 -1
  39. package/dist/src/tui/router.d.ts +6 -0
  40. package/dist/src/tui/router.js +56 -12
  41. package/dist/src/tui/router.js.map +1 -1
  42. package/dist/src/tui/screen-registry.d.ts +7 -0
  43. package/dist/src/utils/plugin-config-write.js +3 -3
  44. package/dist/src/utils/plugin-config-write.js.map +1 -1
  45. package/dist/src/utils/target-eligibility.d.ts +2 -2
  46. package/dist/src/utils/target-eligibility.js.map +1 -1
  47. package/package.json +3 -3
@@ -3,7 +3,7 @@ import type { CatalogActivationMode, CatalogSource, CatalogGenerationActiveV1, C
3
3
  import type { PortableReview } from '../portable/commands.js';
4
4
  import type { CatalogSourceUpdate } from '../utils/catalog-sources.js';
5
5
  import type { CatalogGenerationFreshnessState, CatalogGenerationFreshnessResult } from '../utils/catalog-generation-service.js';
6
- import type { StatusV7Payload, StatusV7PluginSummary, StatusV7Target, StatusTargetRevision } from '../commands/status-v6.js';
6
+ import type { StatusV8Payload, StatusV8PluginSummary, StatusScopedTarget, StatusTargetRevision } from '../commands/status-v6.js';
7
7
  import type { ReconcileOperationPlan } from '../commands/reconcile-operation-plan.js';
8
8
  import type { RebuildCommandResult } from '../commands/rebuild-command.js';
9
9
  import type { OfflineCleanupTargetEvidence } from '../commands/offline-cleanup-evidence.js';
@@ -11,7 +11,7 @@ import type { CodexReconcilePlan } from '../utils/codex-reconciler.js';
11
11
  import type { OperationPlan } from '../commands/operation-plan.js';
12
12
  import type { RebuildProgressFeed, RebuildProgressSnapshot } from '../output/rebuild-progress.js';
13
13
  import { type ConfigPageViewModel, type ConfigGroupViewModel, type InstallPlacementDraftViewModel, type PluginListItemViewModel, type PluginDependencyOverviewViewModel, type PortableSkillSelectionViewModel, type SourceHealthViewModel, type SourceDetailViewModel, type TuiViewModel, type UserIssueCategory, type UserIssueViewModel } from './model.js';
14
- import { OperationHistoryStore, type OperationDiagnosticDetail, type OperationEffectFacts, type OperationHistorySummary, type OperationReportPreview, type SaveOperationReportResult } from './operation-history.js';
14
+ import { OperationHistoryStore, type OperationDiagnosticDetail, type OperationEffectFacts, type OperationHistorySummary, type OperationReportPreview, type SafeOperationDiagnostic, type SaveOperationReportResult } from './operation-history.js';
15
15
  import { type ConfigInvalidStoreEvidence, type PluginInstallFilter, type PortableBlockerCode, type TuiPageContext, type TuiRoute, type TuiRouterSnapshot } from './router.js';
16
16
  import { type CatalogSourceGenerationViewModel } from './catalog-generation.js';
17
17
  import type { CatalogSourceFreshnessViewModel } from './catalog-freshness.js';
@@ -42,7 +42,11 @@ export interface PluginWriteBatchTarget {
42
42
  platform: PlatformName;
43
43
  scope: Scope;
44
44
  }
45
- export declare function pluginWriteBatchTargets(action: Exclude<BatchPluginWriteAction, 'install'>, pluginIds: readonly string[], statusTargets: readonly StatusV7Target[]): PluginWriteBatchTarget[];
45
+ export interface PluginInstallBatchTarget extends PluginWriteBatchTarget {
46
+ state: 'add' | 'existing' | 'blocked';
47
+ reason?: string;
48
+ }
49
+ export declare function pluginWriteBatchTargets(action: Exclude<BatchPluginWriteAction, 'install'>, pluginIds: readonly string[], statusTargets: readonly StatusScopedTarget[]): PluginWriteBatchTarget[];
46
50
  export interface PluginWriteBatchIntent {
47
51
  kind: 'pluginWriteBatch';
48
52
  action: BatchPluginWriteAction;
@@ -340,6 +344,7 @@ export interface OperationReviewModel {
340
344
  version?: string;
341
345
  };
342
346
  changes: OperationReviewChange[];
347
+ confirmationDetails?: string[];
343
348
  technicalDetails?: string[];
344
349
  equivalentCommand?: string;
345
350
  primaryLabel: string;
@@ -561,8 +566,8 @@ export interface PluginOperationProgressLine {
561
566
  }
562
567
  export interface TuiStatusLoadResult {
563
568
  cliChannel?: 'latest' | 'beta';
564
- payload: StatusV7Payload;
565
- pluginSummaries: StatusV7PluginSummary[];
569
+ payload: StatusV8Payload;
570
+ pluginSummaries: StatusV8PluginSummary[];
566
571
  targetRevisions?: StatusTargetRevision[];
567
572
  sourceDetails?: SourceDetailViewModel[];
568
573
  configuredSources?: CatalogSource[];
@@ -677,7 +682,8 @@ export interface TuiControllerSnapshot {
677
682
  }
678
683
  export declare class TuiOperationBlockedError extends Error {
679
684
  readonly code: string;
680
- constructor(code: string, message: string);
685
+ readonly diagnostics?: SafeOperationDiagnostic[];
686
+ constructor(code: string, message: string, diagnostics?: readonly SafeOperationDiagnostic[]);
681
687
  }
682
688
  export declare class TuiConfigInvalidStoreError extends Error {
683
689
  readonly code = "invalidConfigStores";
@@ -764,14 +770,17 @@ export declare class TuiController {
764
770
  installFilter?: PluginInstallFilter;
765
771
  }): PluginListItemViewModel[];
766
772
  getPluginOverview(pluginId: string): PluginListItemViewModel | undefined;
773
+ getPluginSourceIdentity(pluginId: string): string | undefined;
767
774
  listCurrentProjectIntentRoots(): string[];
768
775
  listPluginWriteBatchTargets(action: Exclude<BatchPluginWriteAction, 'install'>, pluginIds: readonly string[]): PluginWriteBatchTarget[];
776
+ listPluginInstallBatchTargets(pluginIds: readonly string[], platform: PlatformName | 'both', scope: Scope): PluginInstallBatchTarget[];
777
+ pluginHasInstallableTarget(pluginId: string): boolean;
769
778
  getPluginDependencyOverview(pluginId: string): PluginDependencyOverviewViewModel | undefined;
770
779
  listPluginDependencyOverviews(): PluginDependencyOverviewViewModel[];
771
780
  getBlockingDependents(pluginId: string, platform: PlatformName, scope: Scope): PluginDependencyOverviewViewModel['dependents'];
772
781
  getPlatformPlacement(pluginId: string, platform: PlatformName): PluginListItemViewModel['installedPlacements'][number] | undefined;
773
782
  listInstallTargets(pluginId: string, scope: Scope): InstallTargetOption[];
774
- getTargetState(pluginId: string, platform: PlatformName, scope: Scope): StatusV7Payload['targets'][number]['state'] | undefined;
783
+ getTargetState(pluginId: string, platform: PlatformName, scope: Scope): StatusV8Payload['targets'][number]['state'] | undefined;
775
784
  listSources(): TuiViewModel['sourceTabs'];
776
785
  getSourceOverview(sourceId: string): {
777
786
  source?: TuiViewModel['sourceTabs'][number];
@@ -794,6 +803,7 @@ export declare class TuiController {
794
803
  pluginWriteTaskReturnRoute(route: Extract<TuiRoute, {
795
804
  name: 'pluginWriteTask';
796
805
  }>): TuiRoute;
806
+ returnToPluginWriteTaskBatchSelection(flowId: string): boolean;
797
807
  retryFailedPluginWriteBatch(): ReviewPreparationSnapshot | undefined;
798
808
  retryFailedPluginWriteTask(): ReviewPreparationSnapshot | undefined;
799
809
  catalogActivationBlockReason(): CatalogActivationBlockReason | undefined;
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
2
2
  import { CatalogManagementError, } from './catalog-management.js';
3
3
  import { CatalogGenerationError } from '../utils/catalog-generation-error.js';
4
4
  import { readSafeOperationError } from '../utils/safe-operation-diagnostic.js';
5
- import { statusTargetActivation, statusTargetIntent, statusTargetOwnershipState, } from '../commands/status-v6.js';
5
+ import { flattenStatusTargets, statusTargetActivation, statusTargetIntent, statusTargetOwnershipState, statusTargetSharedNative, } from '../commands/status-v6.js';
6
6
  import { buildRebuildReceiptJson, requestedIntentAction } from '../output/command-receipt.js';
7
7
  import { buildOperationResultModel } from '../commands/operation-planner.js';
8
8
  import { buildOperationReviewProjection } from './operation-review-planner.js';
@@ -89,10 +89,13 @@ function reviewPreparationStageLabel(stage) {
89
89
  }
90
90
  export class TuiOperationBlockedError extends Error {
91
91
  code;
92
- constructor(code, message) {
92
+ diagnostics;
93
+ constructor(code, message, diagnostics) {
93
94
  super(message);
94
95
  this.name = 'TuiOperationBlockedError';
95
96
  this.code = code;
97
+ if (diagnostics?.length)
98
+ this.diagnostics = [...structuredClone(diagnostics)];
96
99
  }
97
100
  }
98
101
  export class TuiConfigInvalidStoreError extends Error {
@@ -545,6 +548,18 @@ export function userIssueKey(issue) {
545
548
  issue.sourceId ?? '', issue.sourceIdentity ?? '', issue.platform ?? '', issue.scope ?? '']
546
549
  .join('\0');
547
550
  }
551
+ function originIssueRemains(issue, origin) {
552
+ if (!origin.consumerPluginId || !origin.consumerPlatform || !origin.consumerScope) {
553
+ return userIssueKey(issue) === origin.issueKey;
554
+ }
555
+ return issue.category === origin.category
556
+ && issue.dependencyPluginId === origin.dependencyPluginId
557
+ && issue.dependencyPlatform === origin.dependencyPlatform
558
+ && issue.dependencyScope === origin.dependencyScope
559
+ && issue.affectedTargets?.some((target) => target.pluginId === origin.consumerPluginId
560
+ && target.platform === origin.consumerPlatform
561
+ && target.scope === origin.consumerScope) === true;
562
+ }
548
563
  function issueCode(error) {
549
564
  const safeOperationError = readSafeOperationError(error);
550
565
  if (safeOperationError)
@@ -923,8 +938,12 @@ export class TuiController {
923
938
  }
924
939
  #captureThrownProjection(operationId, error, code, stage, intent, effects) {
925
940
  const sensitiveValues = intent ? collectOperationSensitiveValues(intent) : [];
941
+ const carried = error instanceof TuiOperationBlockedError ? error.diagnostics : undefined;
926
942
  this.#operationDiagnostics.set(operationId, {
927
- diagnostics: [projectThrownOperationDiagnostic({
943
+ diagnostics: carried?.length ? carried.map((diagnostic, index) => ({
944
+ ...structuredClone(diagnostic),
945
+ id: `${this.#sessionEpoch}:${operationId}:failure:${index}`,
946
+ })) : [projectThrownOperationDiagnostic({
928
947
  error,
929
948
  id: `${this.#sessionEpoch}:${operationId}:failure`,
930
949
  stage,
@@ -941,6 +960,89 @@ export class TuiController {
941
960
  sensitiveValues,
942
961
  });
943
962
  }
963
+ #appendThrownProjection(operationId, error, code, stage, intent) {
964
+ const existing = this.#operationDiagnostics.get(operationId);
965
+ if (!existing) {
966
+ this.#captureThrownProjection(operationId, error, code, stage, intent);
967
+ return;
968
+ }
969
+ const carried = error instanceof TuiOperationBlockedError ? error.diagnostics : undefined;
970
+ const diagnostics = carried?.length ? carried : [projectThrownOperationDiagnostic({
971
+ error,
972
+ id: `${this.#sessionEpoch}:${operationId}:failure:${existing.diagnostics.length}`,
973
+ stage,
974
+ fallbackCode: code,
975
+ sensitiveValues: existing.sensitiveValues,
976
+ })];
977
+ const offset = existing.diagnostics.length;
978
+ const appended = diagnostics.map((diagnostic, index) => ({
979
+ ...structuredClone(diagnostic),
980
+ id: `${this.#sessionEpoch}:${operationId}:failure:${offset + index}`,
981
+ }));
982
+ existing.diagnostics.push(...appended);
983
+ const targetDiagnostics = appended.filter((diagnostic) => diagnostic.target !== undefined);
984
+ if (targetDiagnostics.length > 0) {
985
+ existing.targets = [...new Map([
986
+ ...(existing.targets ?? []).map((target) => [[
987
+ target.pluginId ?? '', target.platform, target.scope,
988
+ ].join('\0'), target]),
989
+ ...targetDiagnostics.map((diagnostic) => [[
990
+ diagnostic.target.pluginId ?? '', diagnostic.target.platform, diagnostic.target.scope,
991
+ ].join('\0'), {
992
+ ...diagnostic.target,
993
+ state: 'skipped',
994
+ errorCode: diagnostic.code,
995
+ diagnosticIds: targetDiagnostics.filter((candidate) => candidate.target
996
+ && candidate.target.pluginId === diagnostic.target.pluginId
997
+ && candidate.target.platform === diagnostic.target.platform
998
+ && candidate.target.scope === diagnostic.target.scope).map((candidate) => candidate.id),
999
+ }]),
1000
+ ]).values()];
1001
+ }
1002
+ }
1003
+ #recordStaleAttempt(operationId, intent) {
1004
+ if (!this.#operationDiagnostics.has(operationId)) {
1005
+ this.#captureThrownProjection(operationId, undefined, 'operationReviewStale', 'reviewValidation', intent);
1006
+ }
1007
+ const projected = this.#operationDiagnostics.get(operationId);
1008
+ const recordId = `${this.#sessionEpoch}:${operationId}`;
1009
+ const diagnosticsByTarget = new Map();
1010
+ for (const diagnostic of projected.diagnostics) {
1011
+ if (!diagnostic.target)
1012
+ continue;
1013
+ const key = [diagnostic.target.pluginId ?? '', diagnostic.target.platform, diagnostic.target.scope].join('\0');
1014
+ diagnosticsByTarget.set(key, [...(diagnosticsByTarget.get(key) ?? []), diagnostic]);
1015
+ }
1016
+ const targets = projected.targets ?? [...diagnosticsByTarget.entries()].map(([key, diagnostics]) => {
1017
+ const [pluginId, platform, scope] = key.split('\0');
1018
+ return {
1019
+ ...(pluginId ? { pluginId } : {}),
1020
+ platform,
1021
+ scope,
1022
+ state: 'skipped',
1023
+ errorCode: diagnostics[0].code,
1024
+ diagnosticIds: diagnostics.map((diagnostic) => diagnostic.id),
1025
+ };
1026
+ });
1027
+ this.#operationHistory.record({
1028
+ summary: {
1029
+ recordId,
1030
+ epoch: this.#sessionEpoch,
1031
+ operationId,
1032
+ createdAtMs: this.dependencies.now?.() ?? Date.now(),
1033
+ state: 'blocked',
1034
+ code: projected.diagnostics[0]?.code ?? 'operationReviewStale',
1035
+ message: '状态已变化,原确认没有执行。已准备新的确认信息。',
1036
+ operationKind: intent.kind,
1037
+ ...('pluginId' in intent ? { pluginId: intent.pluginId } : {}),
1038
+ diagnosticCount: projected.diagnostics.length,
1039
+ effects: projected.effects,
1040
+ },
1041
+ diagnostics: projected.diagnostics,
1042
+ targets,
1043
+ }, { sensitiveValues: projected.sensitiveValues });
1044
+ this.#operationDiagnostics.delete(operationId);
1045
+ }
944
1046
  async #logErrorSafe(error, code, context) {
945
1047
  try {
946
1048
  await this.dependencies.logError?.(error, code, context);
@@ -1056,6 +1158,14 @@ export class TuiController {
1056
1158
  }
1057
1159
  #stableResultRoute(intent, result) {
1058
1160
  const active = this.#router.activeFlow();
1161
+ if (intent.kind === 'pluginWriteTask'
1162
+ && result.state === 'succeeded'
1163
+ && 'returnRoute' in result
1164
+ && result.returnRoute?.name === 'pluginWriteTask'
1165
+ && result.returnRoute.goal === result.summary.task?.goal
1166
+ && result.returnRoute.batchOrigin?.owner === 'batch-write') {
1167
+ return { name: 'pluginList', mode: 'installed' };
1168
+ }
1059
1169
  if (active?.parentFlowId)
1060
1170
  return this.#normalizeRoute(active.returnTarget);
1061
1171
  if (active && (intent.kind === 'portableInstall'
@@ -1145,7 +1255,18 @@ export class TuiController {
1145
1255
  executionPolicy: 'all',
1146
1256
  }
1147
1257
  : undefined;
1148
- if (!this.#router.finishFlow(target, terminal))
1258
+ const batchFlowId = result.state === 'succeeded'
1259
+ && intent.kind === 'pluginWriteTask'
1260
+ && 'returnRoute' in result
1261
+ && result.returnRoute?.name === 'pluginWriteTask'
1262
+ && result.returnRoute.goal === result.summary.task?.goal
1263
+ && result.returnRoute.batchOrigin?.owner === 'batch-write'
1264
+ ? result.returnRoute.batchOrigin.flowId
1265
+ : undefined;
1266
+ const finished = batchFlowId
1267
+ ? this.#router.finishFlowById(batchFlowId, 'batchWrite', target, terminal)
1268
+ : this.#router.finishFlow(target, terminal);
1269
+ if (!finished)
1149
1270
  this.#router.replace(terminal);
1150
1271
  }
1151
1272
  #applyStatus(status) {
@@ -1900,15 +2021,93 @@ export class TuiController {
1900
2021
  const plugin = this.#viewModel?.plugins.find((entry) => entry.pluginId === pluginId);
1901
2022
  return plugin ? cloneFrozen(plugin) : undefined;
1902
2023
  }
2024
+ getPluginSourceIdentity(pluginId) {
2025
+ return flattenStatusTargets(this.#status?.payload.targets ?? []).find((target) => target.pluginId === pluginId)?.sourceIdentity;
2026
+ }
1903
2027
  listCurrentProjectIntentRoots() {
1904
- return cloneFrozen([...new Set((this.#status?.payload.targets ?? []).flatMap((target) => {
2028
+ return cloneFrozen([...new Set(flattenStatusTargets(this.#status?.payload.targets ?? []).flatMap((target) => {
1905
2029
  const intent = statusTargetIntent(target);
1906
2030
  return intent.relation === 'root' && intent.scopes.includes('project')
1907
2031
  ? [target.pluginId] : [];
1908
2032
  }))].sort((left, right) => left.localeCompare(right, 'en')));
1909
2033
  }
1910
2034
  listPluginWriteBatchTargets(action, pluginIds) {
1911
- return cloneFrozen(pluginWriteBatchTargets(action, pluginIds, this.#status?.payload.targets ?? []));
2035
+ return cloneFrozen(pluginWriteBatchTargets(action, pluginIds, flattenStatusTargets(this.#status?.payload.targets ?? [])));
2036
+ }
2037
+ listPluginInstallBatchTargets(pluginIds, platform, scope) {
2038
+ const platforms = platform === 'both' ? ['codex', 'claude'] : [platform];
2039
+ return cloneFrozen([...new Set(pluginIds)].flatMap((pluginId) => {
2040
+ const plugin = this.#viewModel?.plugins.find((entry) => entry.pluginId === pluginId);
2041
+ return platforms.map((targetPlatform) => {
2042
+ const target = { pluginId, platform: targetPlatform, scope };
2043
+ const statusTargets = flattenStatusTargets(this.#status?.payload.targets ?? []).filter((entry) => entry.pluginId === pluginId && entry.platform === targetPlatform);
2044
+ const exactTarget = statusTargets.find((entry) => entry.scope === scope);
2045
+ const alreadyConfigured = statusTargets.some((entry) => {
2046
+ const activation = statusTargetActivation(entry);
2047
+ return statusTargetIntent(entry).scopes.includes(scope)
2048
+ || (targetPlatform === 'codex'
2049
+ && activation?.evidence === 'observed'
2050
+ && activation[scope] === true);
2051
+ });
2052
+ const exactOwnership = exactTarget ? statusTargetOwnershipState(exactTarget) : 'absent';
2053
+ if (alreadyConfigured || exactOwnership === 'present' || exactOwnership === 'partial') {
2054
+ return { ...target, state: 'existing' };
2055
+ }
2056
+ if (!plugin)
2057
+ return { ...target, state: 'blocked', reason: '插件已不在当前状态中' };
2058
+ if (plugin.configOnly) {
2059
+ return { ...target, state: 'blocked', reason: '来源中没有可信插件定义' };
2060
+ }
2061
+ if (plugin.sourceUnavailable) {
2062
+ return { ...target, state: 'blocked', reason: '插件来源不可用' };
2063
+ }
2064
+ if (plugin.capabilityUnsupported) {
2065
+ return { ...target, state: 'blocked', reason: '当前 AW CLI 不支持插件所需功能' };
2066
+ }
2067
+ const option = this.listInstallTargets(pluginId, scope)
2068
+ .find((entry) => entry.platform === targetPlatform);
2069
+ if (!option?.compatible) {
2070
+ return { ...target, state: 'blocked', reason: option?.disabled ?? '插件不支持所选工具或位置' };
2071
+ }
2072
+ const representativeTarget = exactTarget
2073
+ ?? (targetPlatform === 'codex' ? statusTargets[0] : undefined);
2074
+ const uncertainObservation = (representativeTarget
2075
+ && statusTargetActivation(representativeTarget)?.evidence === 'unobserved')
2076
+ || statusTargets.some((entry) => statusTargetSharedNative(entry)?.state === 'unobserved');
2077
+ if (uncertainObservation) {
2078
+ return { ...target, state: 'blocked', reason: '无法确认所选位置是否缺失' };
2079
+ }
2080
+ if (!representativeTarget || representativeTarget.state === 'blocked') {
2081
+ return {
2082
+ ...target,
2083
+ state: 'blocked',
2084
+ reason: representativeTarget?.reasons.find((reason) => reason.severity === 'blocking')?.message
2085
+ ?? '无法确认所选位置是否缺失',
2086
+ };
2087
+ }
2088
+ if (targetPlatform === 'codex') {
2089
+ const intent = statusTargetIntent(representativeTarget);
2090
+ const activation = statusTargetActivation(representativeTarget);
2091
+ const confirmedMissing = activation?.evidence === 'observed' && activation[scope] === false
2092
+ || intent.enabled && !intent.scopes.includes(scope)
2093
+ || representativeTarget.state === 'absent'
2094
+ && statusTargetOwnershipState(representativeTarget) === 'absent'
2095
+ && !intent.enabled;
2096
+ if (!confirmedMissing) {
2097
+ return { ...target, state: 'blocked', reason: '无法确认所选位置是否缺失' };
2098
+ }
2099
+ }
2100
+ else if (representativeTarget.state !== 'absent') {
2101
+ return { ...target, state: 'blocked', reason: '无法确认所选位置是否缺失' };
2102
+ }
2103
+ return { ...target, state: 'add' };
2104
+ });
2105
+ }).sort((left, right) => [left.pluginId, left.platform, left.scope].join('\0')
2106
+ .localeCompare([right.pluginId, right.platform, right.scope].join('\0'), 'en')));
2107
+ }
2108
+ pluginHasInstallableTarget(pluginId) {
2109
+ return ['project', 'user'].some((scope) => this.listPluginInstallBatchTargets([pluginId], 'both', scope)
2110
+ .some((target) => target.state === 'add'));
1912
2111
  }
1913
2112
  getPluginDependencyOverview(pluginId) {
1914
2113
  const overview = this.#viewModel?.dependencyOverviews.find((entry) => entry.pluginId === pluginId);
@@ -1918,6 +2117,7 @@ export class TuiController {
1918
2117
  return cloneFrozen(this.#viewModel?.dependencyOverviews ?? []);
1919
2118
  }
1920
2119
  getBlockingDependents(pluginId, platform, scope) {
2120
+ const statusTargets = flattenStatusTargets(this.#status?.payload.targets ?? []);
1921
2121
  const dependents = this.#viewModel?.dependencyOverviews.find((entry) => entry.pluginId === pluginId)?.dependents ?? [];
1922
2122
  return cloneFrozen(dependents.flatMap((dependent) => {
1923
2123
  const references = (dependent.placementReferences ?? []).filter((reference) => reference.dependency.pluginId === pluginId
@@ -1925,7 +2125,7 @@ export class TuiController {
1925
2125
  && reference.dependency.scope === scope
1926
2126
  && (scope === 'user' || reference.dependency.project === 'current')
1927
2127
  && (reference.consumer.project === 'other'
1928
- || this.#status?.payload.targets.find((target) => target.pluginId === reference.consumer.pluginId
2128
+ || statusTargets.find((target) => target.pluginId === reference.consumer.pluginId
1929
2129
  && target.platform === reference.consumer.platform
1930
2130
  && target.scope === reference.consumer.scope)?.state !== 'absent'));
1931
2131
  return references.length > 0 ? [{ ...dependent, placementReferences: references }] : [];
@@ -1946,7 +2146,8 @@ export class TuiController {
1946
2146
  }));
1947
2147
  }
1948
2148
  return plugin.supportedPlatforms.map((platform) => {
1949
- const platformTargets = this.#status?.payload.targets.filter((target) => target.pluginId === pluginId && target.platform === platform) ?? [];
2149
+ const allStatusTargets = flattenStatusTargets(this.#status?.payload.targets ?? []);
2150
+ const platformTargets = allStatusTargets.filter((target) => target.pluginId === pluginId && target.platform === platform) ?? [];
1950
2151
  const rootSummary = this.#status?.pluginSummaries.find((summary) => summary.pluginId === pluginId);
1951
2152
  const rootSupportedScopes = rootSummary?.supportedTargets?.[platform]
1952
2153
  ?? platformTargets.filter((target) => !target.reasons.some((error) => error.code === 'unsupportedTarget')).map((target) => target.scope);
@@ -1962,7 +2163,7 @@ export class TuiController {
1962
2163
  for (const dependency of overview?.dependencies ?? []) {
1963
2164
  const dependencySummary = this.#status?.pluginSummaries.find((summary) => summary.pluginId === dependency.pluginId);
1964
2165
  const allowedScopes = dependencySummary?.supportedTargets?.[platform]
1965
- ?? (this.#status?.payload.targets.filter((target) => target.pluginId === dependency.pluginId
2166
+ ?? (allStatusTargets.filter((target) => target.pluginId === dependency.pluginId
1966
2167
  && target.platform === platform
1967
2168
  && !target.reasons.some((error) => error.code === 'unsupportedTarget'))
1968
2169
  .map((target) => target.scope) ?? []);
@@ -1975,7 +2176,7 @@ export class TuiController {
1975
2176
  };
1976
2177
  }
1977
2178
  const collisionAtEveryViableScope = viableScopes.every((candidateScope) => {
1978
- const target = this.#status?.payload.targets.find((entry) => entry.pluginId === dependency.pluginId
2179
+ const target = allStatusTargets.find((entry) => entry.pluginId === dependency.pluginId
1979
2180
  && entry.platform === platform
1980
2181
  && entry.scope === candidateScope);
1981
2182
  return Boolean(target && targetHasArtifactCollision(target));
@@ -1992,7 +2193,7 @@ export class TuiController {
1992
2193
  });
1993
2194
  }
1994
2195
  getTargetState(pluginId, platform, scope) {
1995
- return this.#status?.payload.targets.find((target) => target.pluginId === pluginId
2196
+ return flattenStatusTargets(this.#status?.payload.targets ?? []).find((target) => target.pluginId === pluginId
1996
2197
  && target.platform === platform
1997
2198
  && target.scope === scope)?.state;
1998
2199
  }
@@ -2075,6 +2276,23 @@ export class TuiController {
2075
2276
  ? active.returnTarget
2076
2277
  : route);
2077
2278
  }
2279
+ returnToPluginWriteTaskBatchSelection(flowId) {
2280
+ this.#assertAcceptingActions();
2281
+ const route = this.#router.snapshot.current.route;
2282
+ if (route.name !== 'review'
2283
+ || this.#activeReview?.operationId !== route.operationId
2284
+ || this.#activeReview.operationKind !== 'pluginWriteTask')
2285
+ return false;
2286
+ const reviewToken = this.#activeReview.reviewToken;
2287
+ const returned = this.#router.returnToBatchSelection(flowId);
2288
+ if (!returned)
2289
+ return false;
2290
+ this.#cancelReviewPreparation();
2291
+ this.#tickets.delete(reviewToken);
2292
+ this.#activeReview = undefined;
2293
+ this.#publish();
2294
+ return true;
2295
+ }
2078
2296
  retryFailedPluginWriteBatch() {
2079
2297
  if (!this.#batchRetryIntent)
2080
2298
  return undefined;
@@ -2496,13 +2714,24 @@ export class TuiController {
2496
2714
  ? '本次加载时 Catalog 来源不可用。请刷新来源并重新加载后重试。'
2497
2715
  : '无法准备确认信息。请返回检查选择,或重试。';
2498
2716
  const preparationRecordId = `${this.#sessionEpoch}:${active.snapshot.id}`;
2499
- const preparationDiagnostic = projectThrownOperationDiagnostic({
2500
- error,
2501
- id: `${preparationRecordId}:failure`,
2502
- stage: active.snapshot.currentStage ?? 'reviewPreparation',
2503
- fallbackCode: code,
2504
- sensitiveValues: collectOperationSensitiveValues(intent),
2505
- });
2717
+ const carriedDiagnostics = error instanceof TuiOperationBlockedError
2718
+ ? error.diagnostics : undefined;
2719
+ if (carriedDiagnostics?.length) {
2720
+ active.snapshot.message = `无法准备确认信息:${carriedDiagnostics.slice(0, 3)
2721
+ .map((diagnostic) => sanitizeDiagnosticText(`${diagnostic.target?.pluginId ? `${diagnostic.target.pluginId}:` : ''}${diagnostic.safeMessage}(${safeDiagnosticCode(diagnostic.code, 'operationBlocked')})`, [], collectOperationSensitiveValues(intent))).join(';')}`;
2722
+ }
2723
+ const preparationDiagnostics = carriedDiagnostics?.length
2724
+ ? carriedDiagnostics.map((diagnostic, index) => ({
2725
+ ...structuredClone(diagnostic),
2726
+ id: `${preparationRecordId}:failure:${index}`,
2727
+ }))
2728
+ : [projectThrownOperationDiagnostic({
2729
+ error,
2730
+ id: `${preparationRecordId}:failure`,
2731
+ stage: active.snapshot.currentStage ?? 'reviewPreparation',
2732
+ fallbackCode: code,
2733
+ sensitiveValues: collectOperationSensitiveValues(intent),
2734
+ })];
2506
2735
  this.#operationHistory.record({
2507
2736
  summary: {
2508
2737
  recordId: preparationRecordId,
@@ -2510,18 +2739,29 @@ export class TuiController {
2510
2739
  operationId: active.snapshot.id,
2511
2740
  createdAtMs: this.dependencies.now?.() ?? Date.now(),
2512
2741
  state: 'blocked',
2513
- code: preparationDiagnostic.code,
2742
+ code: preparationDiagnostics[0].code,
2514
2743
  message: active.snapshot.message,
2515
2744
  operationKind: intent.kind,
2516
2745
  ...('pluginId' in intent ? { pluginId: intent.pluginId } : {}),
2517
- diagnosticCount: 1,
2746
+ diagnosticCount: preparationDiagnostics.length,
2518
2747
  effects: {
2519
2748
  configCommit: 'notApplicable', awWrite: 'notStarted',
2520
2749
  preApplyExternalEffect: 'notApplicable', finalVerification: 'notStarted',
2521
2750
  },
2522
2751
  },
2523
- diagnostics: [preparationDiagnostic],
2524
- targets: [],
2752
+ diagnostics: preparationDiagnostics,
2753
+ targets: [...new Map(preparationDiagnostics.flatMap((diagnostic) => diagnostic.target ? [[
2754
+ [diagnostic.target.pluginId ?? '', diagnostic.target.platform, diagnostic.target.scope].join('\0'),
2755
+ {
2756
+ ...diagnostic.target,
2757
+ state: 'skipped',
2758
+ errorCode: diagnostic.code,
2759
+ diagnosticIds: preparationDiagnostics.filter((candidate) => candidate.target
2760
+ && candidate.target.pluginId === diagnostic.target.pluginId
2761
+ && candidate.target.platform === diagnostic.target.platform
2762
+ && candidate.target.scope === diagnostic.target.scope).map((candidate) => candidate.id),
2763
+ },
2764
+ ]] : [])).values()],
2525
2765
  }, { sensitiveValues: collectOperationSensitiveValues(intent) });
2526
2766
  this.#publish();
2527
2767
  await this.#logOperationError(error, code, intent);
@@ -3025,7 +3265,7 @@ export class TuiController {
3025
3265
  }
3026
3266
  const originIssue = ticket.intent.kind === 'pluginWrite' ? ticket.intent.originIssue : undefined;
3027
3267
  if (originIssue && !dependencyPartial
3028
- && this.#viewModel?.issues.some((issue) => userIssueKey(issue) === originIssue.issueKey)) {
3268
+ && this.#viewModel?.issues.some((issue) => originIssueRemains(issue, originIssue))) {
3029
3269
  return this.#setBlocked(operationId, 'issueStillPresent', {
3030
3270
  intent: ticket.intent,
3031
3271
  navigation: 'result',
@@ -3133,7 +3373,6 @@ export class TuiController {
3133
3373
  if (!this.#operationIsCurrent(generation))
3134
3374
  return this.#detachedBlocked(operationId, ticket.intent);
3135
3375
  if (code === 'operationReviewStale' || code === 'catalogGenerationReviewStale') {
3136
- this.#operationDiagnostics.delete(operationId);
3137
3376
  return this.#rebuildStaleReview(ticket, operationId, undefined, generation);
3138
3377
  }
3139
3378
  if (ticket.intent.kind === 'catalogManagement') {
@@ -3254,6 +3493,7 @@ export class TuiController {
3254
3493
  }
3255
3494
  catch (error) {
3256
3495
  const code = issueCode(error);
3496
+ this.#appendThrownProjection(operationId, error, code, 'reviewPreparation', ticket.intent);
3257
3497
  await this.#logOperationError(error, code, ticket.intent);
3258
3498
  return this.#setBlocked(operationId, code, {
3259
3499
  intent: ticket.intent,
@@ -3267,6 +3507,7 @@ export class TuiController {
3267
3507
  review,
3268
3508
  ...this.#operationIdentity(ticket.intent),
3269
3509
  };
3510
+ this.#recordStaleAttempt(operationId, ticket.intent);
3270
3511
  this.#result = result;
3271
3512
  this.#publish();
3272
3513
  return cloneFrozen(result);
@@ -3282,6 +3523,7 @@ export class TuiController {
3282
3523
  catch (error) {
3283
3524
  if (!this.#operationIsCurrent(generation))
3284
3525
  return this.#detachedBlocked(operationId, ticket.intent);
3526
+ this.#appendThrownProjection(operationId, error, 'tuiStateReloadFailed', 'statusReload', ticket.intent);
3285
3527
  await this.#logErrorSafe(error, 'tuiStateReloadFailed', { privateConfigOperation: true });
3286
3528
  return this.#setBlocked(operationId, 'tuiStateReloadFailed', {
3287
3529
  intent: ticket.intent,
@@ -3297,6 +3539,7 @@ export class TuiController {
3297
3539
  if (!this.#operationIsCurrent(generation))
3298
3540
  return this.#detachedBlocked(operationId, ticket.intent);
3299
3541
  const code = issueCode(error);
3542
+ this.#appendThrownProjection(operationId, error, code, 'validate', intent);
3300
3543
  await this.#logOperationError(error, code, intent);
3301
3544
  if (!this.#operationIsCurrent(generation))
3302
3545
  return this.#detachedBlocked(operationId, ticket.intent);
@@ -3325,6 +3568,7 @@ export class TuiController {
3325
3568
  if (!this.#operationIsCurrent(generation))
3326
3569
  return this.#detachedBlocked(operationId, ticket.intent);
3327
3570
  const code = issueCode(error);
3571
+ this.#appendThrownProjection(operationId, error, code, 'reviewPreparation', intent);
3328
3572
  await this.#logOperationError(error, code, intent);
3329
3573
  if (!this.#operationIsCurrent(generation))
3330
3574
  return this.#detachedBlocked(operationId, ticket.intent);
@@ -3349,6 +3593,7 @@ export class TuiController {
3349
3593
  return this.#detachedBlocked(operationId, ticket.intent);
3350
3594
  const review = this.#storeOperationReview(intent, evidence, 'replace', ticket.returnRoute);
3351
3595
  const result = { state: 'stale', operationId, review, ...this.#operationIdentity(intent) };
3596
+ this.#recordStaleAttempt(operationId, intent);
3352
3597
  this.#result = result;
3353
3598
  this.#publish();
3354
3599
  return cloneFrozen(result);