@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
@@ -1,5 +1,5 @@
1
1
  import { matchPluginConfigPreset, stablePluginConfigStringify, } from '@lesliechan721/aw-plugin-core/manifest';
2
- import { statusTargetActivation, statusTargetExtensions, statusTargetIntent, statusTargetOwnershipState, statusTargetSharedNative, } from '../commands/status-v6.js';
2
+ import { flattenStatusTargets, statusTargetActivation, statusTargetExtensions, statusTargetIntent, statusTargetOwnershipState, statusTargetSharedNative, } from '../commands/status-v6.js';
3
3
  import { buildAllowedLifecycleActions } from '../commands/operation-planner.js';
4
4
  import { safeDiagnosticCode, } from './diagnostics.js';
5
5
  import { projectCatalogSourceState } from './catalog-sources/projection.js';
@@ -184,10 +184,35 @@ function missingPureNativeScopes(plugin, placement) {
184
184
  const supported = plugin.supportedScopes?.codex ?? ['user', 'project'];
185
185
  return supported.filter((scope) => !placement.desiredScopes?.includes(scope) && placement.activation?.[scope] !== true);
186
186
  }
187
+ function hasScopedExtensionOwnership(target) {
188
+ return target.components.some((component) => component.kind === 'ownership'
189
+ && component.key !== 'native'
190
+ && !component.key.startsWith('activation-')
191
+ && !component.key.startsWith('marketplace-')
192
+ && (component.state === 'present' || component.state === 'partial'));
193
+ }
187
194
  function isInstalledScopeEvidence(target) {
195
+ const activation = statusTargetActivation(target);
196
+ const extensions = statusTargetExtensions(target);
188
197
  return statusTargetIntent(target).enabled
189
- || statusTargetOwnershipState(target) === 'present'
190
- || statusTargetOwnershipState(target) === 'partial';
198
+ || target.platform === 'claude' && (statusTargetOwnershipState(target) === 'present'
199
+ || statusTargetOwnershipState(target) === 'partial')
200
+ || target.platform === 'codex' && hasScopedExtensionOwnership(target)
201
+ || activation?.[target.scope] === true
202
+ || extensions?.state === 'ready'
203
+ || extensions?.state === 'drifted'
204
+ || extensions?.state === 'conflict';
205
+ }
206
+ function hasActualScopeEvidence(target) {
207
+ const activation = statusTargetActivation(target);
208
+ const extensions = statusTargetExtensions(target);
209
+ return target.platform === 'claude' && (statusTargetOwnershipState(target) === 'present'
210
+ || statusTargetOwnershipState(target) === 'partial')
211
+ || target.platform === 'codex' && hasScopedExtensionOwnership(target)
212
+ || activation?.[target.scope] === true
213
+ || extensions?.state === 'ready'
214
+ || extensions?.state === 'drifted'
215
+ || extensions?.state === 'conflict';
191
216
  }
192
217
  function dependencyAssignmentExplanation(assignmentSource, requiredBy) {
193
218
  const consumer = requiredBy[0];
@@ -296,6 +321,25 @@ export function projectPlatformPlacement(platform, targets, revisions = []) {
296
321
  revision: revisionFor(errorTarget),
297
322
  };
298
323
  }
324
+ if (installed.length > 1) {
325
+ const projected = installed.map((target) => projectPlatformPlacement(platform, [target], revisions));
326
+ return {
327
+ ...(projected.find((placement) => placement.health === 'error')
328
+ ?? projected.find((placement) => placement.health === 'incomplete')
329
+ ?? projected[0]),
330
+ scopedPlacements: projected.map((placement) => ({
331
+ scope: placement.scope,
332
+ health: placement.health,
333
+ revision: placement.revision,
334
+ ...(placement.actualState ? { actualState: placement.actualState } : {}),
335
+ ...(placement.recovery ? { recovery: placement.recovery } : {}),
336
+ ...(placement.blockingReason ? { blockingReason: placement.blockingReason } : {}),
337
+ })),
338
+ desiredScopes: [...new Set(projected.flatMap((placement) => placement.desiredScopes ?? []))].sort(compareText),
339
+ actualScopes: [...new Set(installed.filter(hasActualScopeEvidence)
340
+ .map((target) => target.scope))].sort(compareText),
341
+ };
342
+ }
299
343
  const target = installed.find((candidate) => candidate.state === 'blocked')
300
344
  ?? installed.find((candidate) => candidate.state === 'needsSync')
301
345
  ?? installed.find((candidate) => candidate.state === 'orphaned')
@@ -314,6 +358,7 @@ export function projectPlatformPlacement(platform, targets, revisions = []) {
314
358
  health: targetHealth(target),
315
359
  revision: revisionFor(target),
316
360
  desiredScopes: [...intent.scopes],
361
+ actualScopes: hasActualScopeEvidence(target) ? [target.scope] : [],
317
362
  relation: intent.relation,
318
363
  actualState: target.state,
319
364
  ...(activation ? { activation } : {}),
@@ -325,6 +370,9 @@ export function projectPlatformPlacement(platform, targets, revisions = []) {
325
370
  }
326
371
  function hasIncompletePlacement(plugin) {
327
372
  return plugin.installedPlacements.some((placement) => placement.state === 'scopeConflict'
373
+ || placement.state === 'installed'
374
+ && placement.scopedPlacements?.some((entry) => entry.health !== 'ready'
375
+ || entry.blockingReason !== undefined)
328
376
  || placement.state === 'installed'
329
377
  && (placement.health !== 'ready' || placement.blockingReason !== undefined));
330
378
  }
@@ -436,6 +484,17 @@ export function buildPlatformActions(plugin, platform) {
436
484
  return [{ id: 'add' }, { id: 'about' }];
437
485
  if (placement.state === 'scopeConflict')
438
486
  return [{ id: 'scopeConflict', platform }];
487
+ if (placement.scopedPlacements) {
488
+ const actions = placement.scopedPlacements.flatMap((entry) => buildPlatformActions({
489
+ ...plugin,
490
+ installedPlacements: [{
491
+ ...placement,
492
+ ...entry,
493
+ scopedPlacements: undefined,
494
+ }],
495
+ }, platform));
496
+ return [...new Map(actions.map((action) => [JSON.stringify(action), action])).values()];
497
+ }
439
498
  const scopedRecovery = scopeConflictRecoveryMenuActions(placement);
440
499
  if (scopedRecovery.length > 0)
441
500
  return [...scopedRecovery, { id: 'technical' }];
@@ -473,7 +532,9 @@ export function buildPlatformActions(plugin, platform) {
473
532
  : [];
474
533
  }
475
534
  if (action.id === 'sync')
476
- return [{ id: 'sync' }];
535
+ return [{
536
+ id: 'sync', platform: action.platform, scope: action.scope,
537
+ }];
477
538
  if (action.id === 'reinstall')
478
539
  return [{ id: 'reinstall', platform: action.platform, scope: action.scope }];
479
540
  if (action.id === 'uninstall')
@@ -494,24 +555,33 @@ export function buildPlatformActions(plugin, platform) {
494
555
  export function hasInstalledPlacement(plugin) {
495
556
  return plugin.installedPlacements.some((placement) => placement.state === 'installed' || placement.state === 'scopeConflict');
496
557
  }
497
- function sourceIssue(health, impactedTargets = 0) {
558
+ function sourceIssue(health, targetIssues = []) {
498
559
  if (health.state !== 'unavailable')
499
560
  return undefined;
500
561
  const issueId = health.issueId ?? `source:${health.sourceId}:unavailable`;
562
+ const affectedTargets = [...new Map(targetIssues.flatMap((issue) => issue.affectedTargets ?? [])
563
+ .map((target) => [`${target.pluginId}\0${target.platform}\0${target.scope}`, target])).values()]
564
+ .sort((left, right) => compareText(left.pluginId, right.pluginId)
565
+ || compareText(left.platform, right.platform)
566
+ || compareText(left.scope, right.scope));
567
+ const cleanupActions = targetIssues.flatMap((issue) => issue.actions)
568
+ .filter((action) => action.id === 'cleanup');
501
569
  return {
502
570
  issueId,
503
571
  category: 'sourceUnavailable',
504
572
  title: `${health.sourceId} · 来源不可用`,
505
573
  explanation: '来源检查失败。',
506
574
  impact: ['当前不能浏览或安装这个来源的插件。',
507
- ...(impactedTargets > 0 ? [`${impactedTargets} 个安装位置受此来源根问题影响。`] : [])],
575
+ ...(affectedTargets.length > 0 ? [`${affectedTargets.length} 个安装位置受此来源根问题影响。`] : [])],
508
576
  entity: { kind: 'source', id: health.sourceId },
509
577
  sourceId: health.sourceId,
510
- actions: [
578
+ ...(affectedTargets.length > 0 ? { affectedTargets } : {}),
579
+ actions: dedupeIssueActions([
580
+ ...cleanupActions,
511
581
  { id: 'editSource', label: '编辑来源', description: '检查 URL、路径或 ref' },
512
582
  { id: 'refreshSource', label: '稍后重试', description: '只重新检查来源' },
513
583
  { id: 'viewTechnical', label: '技术详情' },
514
- ],
584
+ ]),
515
585
  technicalDetails: [{
516
586
  code: 'catalogSourceUnavailable',
517
587
  message: '来源检查失败。',
@@ -565,8 +635,9 @@ function lifecycleIssueActions(target, options = {}) {
565
635
  }
566
636
  function targetIssues(payload) {
567
637
  const issues = [];
638
+ const statusTargets = flattenStatusTargets(payload.targets);
568
639
  const canonicalRootsByEntity = new Map();
569
- for (const target of payload.targets) {
640
+ for (const target of statusTargets) {
570
641
  for (const error of target.reasons) {
571
642
  if (!error.rootIssueId || !error.rootIssueCode)
572
643
  continue;
@@ -588,46 +659,98 @@ function targetIssues(payload) {
588
659
  }
589
660
  }
590
661
  for (const target of payload.targets) {
591
- const intent = statusTargetIntent(target);
592
- const scopedConflictRecovery = target.reasons.some((reason) => reason.code === 'codexExtensionScopeConflict')
593
- ? target.recoveryActions.flatMap((recovery) => {
594
- const parsed = parsedScopedUninstallRecovery(recovery, target.platform);
595
- return parsed ? [{ ...parsed, keepScope: parsed.scope === 'user'
596
- ? 'project' : 'user' }] : [];
597
- })
598
- : [];
599
- if (scopedConflictRecovery.length > 0) {
600
- issues.push({
601
- issueId: `scope:${target.pluginId}:${target.platform}`,
602
- category: 'scopeConflict',
603
- title: `${target.pluginId} · ${target.platform === 'codex' ? 'Codex' : 'Claude Code'} · 扩展位置冲突`,
604
- explanation: 'AW 管理附加能力不能同时使用所有项目和当前项目位置。',
605
- impact: ['选择要保留的使用位置。系统将移除另一位置,再同步保留的位置。'],
606
- entity: { kind: 'plugin', id: target.pluginId },
607
- pluginId: target.pluginId,
608
- platform: target.platform,
609
- scope: target.scope,
610
- sourceId: target.sourceId,
611
- sourceIdentity: target.sourceIdentity,
612
- placements: [...intent.scopes].sort(compareText),
613
- actions: [
614
- ...scopedConflictRecovery.map((recovery) => ({
615
- id: recovery.keepScope === 'user' ? 'keepUserScope' : 'keepProjectScope',
616
- label: `保留${recovery.keepScope === 'user' ? '所有项目' : '当前项目'}`,
617
- description: recovery.description,
618
- pluginId: recovery.pluginId,
619
- platform: target.platform,
620
- scope: recovery.scope,
621
- writeAction: 'uninstall',
622
- })),
623
- { id: 'viewTechnical', label: '查看冲突详情' },
624
- ],
625
- technicalDetails: [{
626
- code: 'codexExtensionScopeConflict',
627
- message: '扩展同时使用 user 和 project scope。',
628
- }],
629
- });
630
- }
662
+ if (target.platform !== 'codex' || !target.reasons.some((reason) => reason.code === 'codexExtensionScopeConflict'))
663
+ continue;
664
+ const recoveries = target.recoveryActions.flatMap((recovery) => {
665
+ const parsed = parsedScopedUninstallRecovery(recovery, target.platform);
666
+ return parsed ? [{ ...parsed, keepScope: parsed.scope === 'user'
667
+ ? 'project' : 'user' }] : [];
668
+ });
669
+ issues.push({
670
+ issueId: `scope:${target.pluginId}:${target.platform}`,
671
+ category: 'scopeConflict',
672
+ title: `${target.pluginId} · Codex · 扩展位置冲突`,
673
+ explanation: 'AW 管理附加能力不能同时使用所有项目和当前项目位置。',
674
+ impact: ['选择要保留的使用位置。系统将移除另一位置,再同步保留的位置。'],
675
+ entity: { kind: 'plugin', id: target.pluginId },
676
+ pluginId: target.pluginId,
677
+ platform: target.platform,
678
+ sourceId: target.sourceId,
679
+ sourceIdentity: target.sourceIdentity,
680
+ placements: target.placements.map((placement) => placement.scope).sort(compareText),
681
+ actions: [
682
+ ...recoveries.map((recovery) => ({
683
+ id: recovery.keepScope === 'user' ? 'keepUserScope' : 'keepProjectScope',
684
+ label: `保留${recovery.keepScope === 'user' ? '所有项目' : '当前项目'}`,
685
+ description: recovery.description,
686
+ pluginId: recovery.pluginId,
687
+ platform: target.platform,
688
+ scope: recovery.scope,
689
+ writeAction: 'uninstall',
690
+ })),
691
+ { id: 'viewTechnical', label: '查看冲突详情' },
692
+ ],
693
+ technicalDetails: [{
694
+ code: 'codexExtensionScopeConflict',
695
+ message: '扩展同时使用 user 和 project scope。',
696
+ }],
697
+ });
698
+ }
699
+ for (const target of payload.targets) {
700
+ if (target.platform !== 'codex')
701
+ continue;
702
+ const sharedReasons = target.reasons.filter((reason) => reason.code.startsWith('codexSharedNative') || reason.code === 'staleCacheObserved');
703
+ if (sharedReasons.length === 0 || target.state === 'ready' || target.state === 'absent')
704
+ continue;
705
+ const codes = new Set(sharedReasons.map((reason) => reason.code));
706
+ const sharedNative = statusTargetSharedNative(target);
707
+ const statusIssue = codes.has('codexSharedNativeForeign')
708
+ ? { title: '平台共享内容被占用', explanation: 'Codex 平台共享位置包含 foreign 内容。' }
709
+ : codes.has('codexSharedNativeUpdateRequired')
710
+ ? { title: '版本待同步', explanation: 'Codex 平台共享内容版本与当前插件版本不一致。' }
711
+ : codes.has('codexSharedNativeUnobserved')
712
+ ? { title: '状态未确认', explanation: '未取得有效的 Codex 共享内容状态证据。' }
713
+ : { title: '平台共享内容待同步', explanation: 'Codex 平台共享内容缺失、损坏或与当前渲染输入不一致。' };
714
+ const recoveryActions = target.recoveryActions.flatMap((recovery) => {
715
+ if (!recovery.scope)
716
+ return [];
717
+ const scopedTarget = statusTargets.find((candidate) => candidate.pluginId === target.pluginId
718
+ && candidate.platform === target.platform && candidate.scope === recovery.scope);
719
+ return scopedTarget && isInstalledScopeEvidence(scopedTarget)
720
+ ? lifecycleIssueActions({ ...scopedTarget, recoveryActions: [recovery] })
721
+ : [];
722
+ });
723
+ issues.push({
724
+ issueId: `sharedNative:${target.pluginId}`,
725
+ category: target.state === 'blocked' ? 'diagnostic' : 'incompleteInstallation',
726
+ title: `${target.pluginId} · Codex · ${statusIssue.title}`,
727
+ explanation: statusIssue.explanation,
728
+ impact: [sharedNative?.state === 'blocked'
729
+ ? '共享内容恢复前,当前插件不能安全同步。'
730
+ : '共享内容恢复前,相关 Codex 使用位置可能不可用。'],
731
+ entity: { kind: 'plugin', id: target.pluginId },
732
+ pluginId: target.pluginId,
733
+ platform: target.platform,
734
+ sourceId: target.sourceId,
735
+ sourceIdentity: target.sourceIdentity,
736
+ placements: target.placements.map((placement) => placement.scope).sort(compareText),
737
+ actions: [
738
+ ...new Map(recoveryActions.map((action) => [
739
+ `${action.id}\0${action.scope ?? ''}`,
740
+ action,
741
+ ])).values(),
742
+ { id: 'viewTechnical', label: '技术详情' },
743
+ ],
744
+ technicalDetails: sharedReasons.map((reason) => ({
745
+ code: reason.code,
746
+ message: reason.message,
747
+ })),
748
+ });
749
+ }
750
+ for (const target of statusTargets) {
751
+ if (target.reasons.length === 0 && (target.sharedReasons?.length ?? 0) > 0
752
+ && isPureNativeCodexTarget(target))
753
+ continue;
631
754
  if (target.state !== 'needsSync' && target.state !== 'blocked'
632
755
  && !target.reasons.some((error) => error.code === 'artifactCollision'
633
756
  || error.dependencyPluginId !== undefined && error.rootIssueId !== undefined))
@@ -736,37 +859,35 @@ function targetIssues(payload) {
736
859
  const dependencyScope = rootError?.dependencyScope;
737
860
  const dependencyProject = rootError?.dependencyProject;
738
861
  const dependencyCategory = root.category.startsWith('dependency');
739
- const dependencyTarget = dependencyPluginId && dependencyPlatform && dependencyScope
740
- ? payload.targets.find((candidate) => candidate.pluginId === dependencyPluginId
741
- && candidate.platform === dependencyPlatform && candidate.scope === dependencyScope)
742
- : undefined;
743
- const dependencyLifecycleActions = dependencyPluginId && dependencyPlatform && dependencyScope
744
- ? lifecycleIssueActions(dependencyTarget ?? {
745
- pluginId: dependencyPluginId,
746
- platform: dependencyPlatform,
747
- scope: dependencyScope,
748
- state: 'absent',
749
- recoveryActions: [],
750
- }, { dependency: true, project: dependencyProject })
751
- : [];
752
- const directLifecycleActions = dependencyCategory ? [] : lifecycleIssueActions(target);
862
+ const directLifecycleActions = lifecycleIssueActions(target);
863
+ const desiredConsumer = statusTargetIntent(target).enabled
864
+ && statusTargetIntent(target).scopes.includes(target.scope);
865
+ const consumerRecoveryActions = dependencyCategory
866
+ ? directLifecycleActions.filter((action) => desiredConsumer
867
+ ? action.id === 'sync' || action.id === 'reinstall' || action.id === 'refreshAndSync'
868
+ : action.id === 'cleanup')
869
+ : directLifecycleActions;
753
870
  const actions = root.category === 'dependencyMissing'
754
871
  ? [
755
- ...dependencyLifecycleActions,
872
+ ...consumerRecoveryActions,
873
+ ...(dependencyPluginId ? [
874
+ { id: 'viewDependencyIssue', label: '查看依赖插件问题', pluginId: dependencyPluginId },
875
+ ] : []),
756
876
  { id: 'viewDependencies', label: '查看依赖关系' },
757
877
  { id: 'viewTechnical', label: '技术详情' },
758
878
  ]
759
879
  : root.category === 'dependencyNotReady'
760
880
  ? [
881
+ ...consumerRecoveryActions,
761
882
  ...(dependencyPluginId ? [
762
883
  { id: 'viewDependencyIssue', label: '查看依赖插件问题', pluginId: dependencyPluginId },
763
884
  ] : []),
764
- ...dependencyLifecycleActions.filter((action) => action.id === 'reinstallDependency'),
765
885
  { id: 'viewDependencies', label: '查看依赖关系' },
766
886
  { id: 'viewTechnical', label: '技术详情' },
767
887
  ]
768
888
  : dependencyCategory
769
889
  ? [
890
+ ...consumerRecoveryActions,
770
891
  { id: 'viewDependencies', label: '查看依赖关系' },
771
892
  { id: 'viewTechnical', label: '技术详情' },
772
893
  ]
@@ -777,12 +898,12 @@ function targetIssues(payload) {
777
898
  ]
778
899
  : root.category === 'incompleteInstallation'
779
900
  ? [
780
- ...directLifecycleActions,
901
+ ...consumerRecoveryActions,
781
902
  { id: 'viewTechnical', label: '技术详情' },
782
903
  ]
783
904
  : root.category === 'sourceMissing'
784
905
  ? [
785
- ...directLifecycleActions,
906
+ ...consumerRecoveryActions,
786
907
  { id: 'reAddSource', label: '重新添加同一来源', description: '保留来源 ID;来源类型和位置由你选择' },
787
908
  { id: 'viewTechnical', label: '技术详情' },
788
909
  ]
@@ -793,7 +914,7 @@ function targetIssues(payload) {
793
914
  ]
794
915
  : root.category === 'sourceUnavailable'
795
916
  ? [
796
- ...directLifecycleActions.filter((action) => action.id === 'cleanup'),
917
+ ...consumerRecoveryActions.filter((action) => action.id === 'cleanup'),
797
918
  { id: 'editSource', label: '查看来源', description: root.code === 'bundleUnavailable'
798
919
  ? '检查当前插件目录或稍后重试'
799
920
  : root.code === 'codexIntentSourceUnavailable'
@@ -801,7 +922,7 @@ function targetIssues(payload) {
801
922
  : '修复来源配置或稍后重试' },
802
923
  { id: 'viewTechnical', label: '技术详情' },
803
924
  ]
804
- : [...directLifecycleActions,
925
+ : [...consumerRecoveryActions,
805
926
  { id: 'viewTechnical', label: '技术详情' },
806
927
  { id: 'retryLater', label: '稍后重试' }];
807
928
  const bundleUnavailable = root.code === 'bundleUnavailable';
@@ -840,7 +961,7 @@ function targetIssues(payload) {
840
961
  scope: target.scope,
841
962
  relationship: relationship.relationship,
842
963
  }],
843
- title: `${target.pluginId} · ${target.platform === 'codex' ? 'Codex' : 'Claude Code'} · ${sharedNativeProjectionIssue ? '平台共享内容待同步' : root.category === 'artifactCollision' ? '输出冲突' : dependencyCategory ? {
964
+ title: `${target.pluginId} · ${target.platform === 'codex' ? 'Codex' : 'Claude Code'} · ${dependencyCategory ? `${target.scope === 'user' ? '所有项目' : '当前项目'} · ` : ''}${sharedNativeProjectionIssue ? '平台共享内容待同步' : root.category === 'artifactCollision' ? '输出冲突' : dependencyCategory ? {
844
965
  dependencyMissing: '缺少依赖',
845
966
  dependencyNotReady: '依赖安装不完整',
846
967
  dependencyTargetUnsupported: '依赖目标不兼容',
@@ -851,8 +972,10 @@ function targetIssues(payload) {
851
972
  ? '目标位置已有不属于当前插件的内容。AW 不会覆盖或删除这些内容。'
852
973
  : dependencyCategory
853
974
  ? dependencyPluginId
854
- ? `${dependencyPluginId} 导致当前依赖检查不能通过。`
855
- : '当前依赖检查不能安全通过。'
975
+ ? `依赖 ${dependencyPluginId}${dependencyPlatform && dependencyScope
976
+ ? `(${dependencyPlatform === 'codex' ? 'Codex' : 'Claude Code'} · ${dependencyScope === 'user' ? '所有项目' : '当前项目'})`
977
+ : ''}未就绪;影响 ${target.pluginId}(${target.platform === 'codex' ? 'Codex' : 'Claude Code'} · ${target.scope === 'user' ? '所有项目' : '当前项目'})。`
978
+ : `依赖检查未通过;影响 ${target.pluginId}(${target.platform === 'codex' ? 'Codex' : 'Claude Code'} · ${target.scope === 'user' ? '所有项目' : '当前项目'})。`
856
979
  : sharedNativeDamaged
857
980
  ? 'Codex 平台共享生成内容缺失或损坏。'
858
981
  : sharedNativeRenderDrift
@@ -1216,6 +1339,14 @@ function issueReasonPriority(issue) {
1216
1339
  diagnostic: 9,
1217
1340
  }[code] ?? 9;
1218
1341
  }
1342
+ function dedupeIssueActions(actions) {
1343
+ return [...new Map(actions.map((action) => [[
1344
+ action.id,
1345
+ action.pluginId ?? '',
1346
+ action.platform ?? '',
1347
+ action.scope ?? '',
1348
+ ].join('\0'), action])).values()];
1349
+ }
1219
1350
  function collapseRootIssues(issues) {
1220
1351
  const byId = new Map();
1221
1352
  for (const issue of issues)
@@ -1253,6 +1384,7 @@ function collapseRootIssues(issues) {
1253
1384
  impact: [...new Set(ordered.flatMap((issue) => issue.impact))],
1254
1385
  technicalDetails: [...new Map(ordered.flatMap((issue) => issue.technicalDetails)
1255
1386
  .map((detail) => [`${detail.code}\0${detail.message}`, detail])).values()],
1387
+ actions: dedupeIssueActions(ordered.flatMap((issue) => issue.actions)),
1256
1388
  };
1257
1389
  });
1258
1390
  }
@@ -1262,7 +1394,7 @@ export function buildTuiViewModel(options) {
1262
1394
  const summaryById = new Map(options.pluginSummaries.map((summary) => [summary.pluginId, summary]));
1263
1395
  const sourceById = new Map(options.status.catalog.sources.map((source) => [source.id, source]));
1264
1396
  const targetsByPlugin = new Map();
1265
- for (const target of options.status.targets) {
1397
+ for (const target of flattenStatusTargets(options.status.targets)) {
1266
1398
  targetsByPlugin.set(target.pluginId, [...(targetsByPlugin.get(target.pluginId) ?? []), target]);
1267
1399
  }
1268
1400
  const configScopesByPlugin = new Map();
@@ -1285,6 +1417,21 @@ export function buildTuiViewModel(options) {
1285
1417
  const configOnly = summary === undefined && targets.length === 0 && savedConfigScopes.length > 0;
1286
1418
  const placements = ['codex', 'claude'].flatMap((platform) => {
1287
1419
  const platformTargets = targets.filter((target) => target.platform === platform);
1420
+ const aggregateConflict = platform === 'codex' && options.status.targets.some((target) => target.pluginId === pluginId
1421
+ && target.platform === platform
1422
+ && target.state === 'blocked'
1423
+ && target.reasons.some((reason) => reason.code === 'codexExtensionScopeConflict'));
1424
+ if (aggregateConflict) {
1425
+ const scoped = platformTargets.flatMap((target) => {
1426
+ const projected = projectPlatformPlacement(platform, [target], options.targetRevisions);
1427
+ return projected.state === 'installed' ? [{
1428
+ scope: projected.scope,
1429
+ health: projected.health,
1430
+ revision: projected.revision,
1431
+ }] : [];
1432
+ });
1433
+ return [{ platform, state: 'scopeConflict', placements: scoped }];
1434
+ }
1288
1435
  return platformTargets.length > 0
1289
1436
  ? [projectPlatformPlacement(platform, platformTargets, options.targetRevisions)]
1290
1437
  : [];
@@ -1351,15 +1498,31 @@ export function buildTuiViewModel(options) {
1351
1498
  issueCount: summary.dependencyIssueCount ?? 0,
1352
1499
  } }
1353
1500
  : {}),
1354
- technicalComponents: targets.flatMap((target) => target.components.map((component) => ({
1355
- platform: target.platform,
1356
- scope: target.scope,
1357
- key: component.key,
1358
- state: component.state,
1359
- ...(typeof component.metadata?.errorCode === 'string'
1360
- ? { errorCode: component.metadata.errorCode }
1361
- : {}),
1362
- }))),
1501
+ technicalComponents: [
1502
+ ...targets.flatMap((target) => [
1503
+ ...target.components,
1504
+ ...(target.sharedComponents ?? []).filter((component) => component.kind === 'systemConsumer'),
1505
+ ].map((component) => ({
1506
+ platform: target.platform,
1507
+ scope: target.scope,
1508
+ key: component.key,
1509
+ state: component.state,
1510
+ ...(typeof component.metadata?.errorCode === 'string'
1511
+ ? { errorCode: component.metadata.errorCode }
1512
+ : {}),
1513
+ }))),
1514
+ ...new Map(targets.flatMap((target) => (target.sharedComponents ?? [])
1515
+ .filter((component) => component.kind === 'sharedNative')
1516
+ .map((component) => [`${target.platform}\0${component.key}`, {
1517
+ platform: target.platform,
1518
+ shared: true,
1519
+ key: component.key,
1520
+ state: component.state,
1521
+ ...(typeof component.metadata?.errorCode === 'string'
1522
+ ? { errorCode: component.metadata.errorCode }
1523
+ : {}),
1524
+ }]))).values(),
1525
+ ],
1363
1526
  update,
1364
1527
  };
1365
1528
  }).sort((left, right) => compareText(left.displayName, right.displayName)
@@ -1375,11 +1538,11 @@ export function buildTuiViewModel(options) {
1375
1538
  .filter((health) => health.state === 'unavailable')
1376
1539
  .map((health) => health.sourceId));
1377
1540
  const allTargetIssues = targetIssues(options.status);
1378
- const targetImpactCounts = new Map();
1541
+ const mergedTargetIssues = new Map();
1379
1542
  for (const issue of allTargetIssues) {
1380
1543
  if (!issue.mergeEligible || !issue.sourceId || !sourceRootIds.has(issue.sourceId))
1381
1544
  continue;
1382
- targetImpactCounts.set(issue.sourceId, (targetImpactCounts.get(issue.sourceId) ?? 0) + 1);
1545
+ mergedTargetIssues.set(issue.sourceId, [...(mergedTargetIssues.get(issue.sourceId) ?? []), issue]);
1383
1546
  }
1384
1547
  const sourceDetails = new Map((options.sourceDetails ?? []).map((detail) => [detail.id, detail]));
1385
1548
  const issueDrafts = [
@@ -1388,7 +1551,7 @@ export function buildTuiViewModel(options) {
1388
1551
  ...statusIssues(options.status),
1389
1552
  ...configIssues(options.pluginSummaries),
1390
1553
  ...dependencyOverviewIssues(options.dependencyOverviews ?? [], options.pluginSummaries, options.status),
1391
- ...[...(options.sourceHealth?.values() ?? [])].flatMap((health) => sourceIssue(health, targetImpactCounts.get(health.sourceId) ?? 0) ?? []),
1554
+ ...[...(options.sourceHealth?.values() ?? [])].flatMap((health) => sourceIssue(health, mergedTargetIssues.get(health.sourceId) ?? []) ?? []),
1392
1555
  ];
1393
1556
  const stableIssueDrafts = collapseRootIssues(issueDrafts).sort((left, right) => issueGroupOrder[issueGroup(left)] - issueGroupOrder[issueGroup(right)]
1394
1557
  || issueReasonPriority(left) - issueReasonPriority(right)
@@ -1429,7 +1592,9 @@ export function buildTuiViewModel(options) {
1429
1592
  dependencyOverviews: options.dependencyOverviews ?? [],
1430
1593
  home: {
1431
1594
  availablePlugins: pluginsWithAttention.filter((plugin) => plugin.aggregateStatus === 'available').length,
1432
- installedPlugins: pluginsWithAttention.filter(hasInstalledPlacement).length,
1595
+ installedPlugins: new Set(pluginsWithAttention
1596
+ .filter(hasInstalledPlacement)
1597
+ .map((plugin) => plugin.pluginId)).size,
1433
1598
  issueCount: issues.length,
1434
1599
  sourceCount: sourceTabs.length,
1435
1600
  unavailableSourceCount: options.status.catalog.sources.filter((source) => source.error