@lesliechan721/aw 0.1.0-beta.17 → 0.1.0-beta.19

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 (48) hide show
  1. package/dist/src/commands/operation-planner.d.ts +3 -3
  2. package/dist/src/commands/rebuild-command.js +4 -5
  3. package/dist/src/commands/rebuild-command.js.map +1 -1
  4. package/dist/src/commands/status-v6.d.ts +83 -33
  5. package/dist/src/commands/status-v6.js +531 -191
  6. package/dist/src/commands/status-v6.js.map +1 -1
  7. package/dist/src/commands/wizard.d.ts +2 -2
  8. package/dist/src/index.js +4 -4
  9. package/dist/src/index.js.map +1 -1
  10. package/dist/src/tui/catalog-sources/projection.d.ts +2 -2
  11. package/dist/src/tui/catalog-sources/screens.js +17 -10
  12. package/dist/src/tui/catalog-sources/screens.js.map +1 -1
  13. package/dist/src/tui/console-app-v4.js +1296 -437
  14. package/dist/src/tui/console-app-v4.js.map +1 -1
  15. package/dist/src/tui/controller-runtime.d.ts +3 -3
  16. package/dist/src/tui/controller-runtime.js +85 -27
  17. package/dist/src/tui/controller-runtime.js.map +1 -1
  18. package/dist/src/tui/controller.d.ts +17 -7
  19. package/dist/src/tui/controller.js +265 -24
  20. package/dist/src/tui/controller.js.map +1 -1
  21. package/dist/src/tui/dependency-graph.d.ts +3 -0
  22. package/dist/src/tui/dependency-graph.js +37 -1
  23. package/dist/src/tui/dependency-graph.js.map +1 -1
  24. package/dist/src/tui/diagnostic-projection.d.ts +1 -0
  25. package/dist/src/tui/diagnostic-projection.js +51 -4
  26. package/dist/src/tui/diagnostic-projection.js.map +1 -1
  27. package/dist/src/tui/model.d.ts +22 -12
  28. package/dist/src/tui/model.js +276 -89
  29. package/dist/src/tui/model.js.map +1 -1
  30. package/dist/src/tui/operation-history.d.ts +1 -0
  31. package/dist/src/tui/operation-history.js +5 -0
  32. package/dist/src/tui/operation-history.js.map +1 -1
  33. package/dist/src/tui/operation-review-planner.js +67 -2
  34. package/dist/src/tui/operation-review-planner.js.map +1 -1
  35. package/dist/src/tui/plugin-write-task.d.ts +2 -0
  36. package/dist/src/tui/plugin-write-task.js.map +1 -1
  37. package/dist/src/tui/router.d.ts +6 -0
  38. package/dist/src/tui/router.js +56 -12
  39. package/dist/src/tui/router.js.map +1 -1
  40. package/dist/src/tui/screen-registry.d.ts +7 -0
  41. package/dist/src/utils/codex-manager.d.ts +5 -7
  42. package/dist/src/utils/codex-manager.js +24 -97
  43. package/dist/src/utils/codex-manager.js.map +1 -1
  44. package/dist/src/utils/plugin-config-write.js +3 -3
  45. package/dist/src/utils/plugin-config-write.js.map +1 -1
  46. package/dist/src/utils/target-eligibility.d.ts +2 -2
  47. package/dist/src/utils/target-eligibility.js.map +1 -1
  48. 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))
@@ -638,6 +761,28 @@ function targetIssues(payload) {
638
761
  const sharedNativeDamaged = target.state === 'needsSync'
639
762
  && codes.has('codexSharedNativeCorrupt');
640
763
  const sharedNativeProjectionIssue = sharedNativeRenderDrift || sharedNativeDamaged;
764
+ const sharedNative = statusTargetSharedNative(target);
765
+ const systemConsumer = statusTargetActivation(target)?.systemConsumer;
766
+ const statusUnconfirmed = systemConsumer?.state === 'unresolved'
767
+ || codes.has('codexSharedNativeUnobserved')
768
+ || codes.has('codexNativeObservationFailed')
769
+ || (sharedNative?.state === 'blocked' && sharedNative.reason === 'unreadable');
770
+ const contentMissing = codes.has('missingContent')
771
+ || sharedNative?.state === 'missing'
772
+ || statusTargetExtensions(target)?.state === 'missing'
773
+ || target.components.some((component) => component.kind === 'ownership'
774
+ && component.state === 'absent');
775
+ const statusIssue = systemConsumer?.state === 'incompatible'
776
+ ? { title: 'Codex 版本不兼容', explanation: '当前 Codex 版本低于要求,请升级后重试。' }
777
+ : statusUnconfirmed
778
+ ? { title: '状态未确认', explanation: '未取得有效的 Codex 状态证据,暂时无法确认插件是否可用。' }
779
+ : codes.has('codexActivationDrift')
780
+ ? { title: '激活范围不一致', explanation: 'Codex 激活配置与期望的使用范围不一致。' }
781
+ : codes.has('codexSharedNativeUpdateRequired')
782
+ ? { title: '版本待同步', explanation: '已安装的 Codex 平台共享内容版本与当前插件版本不一致。' }
783
+ : sharedNativeDamaged || contentMissing
784
+ ? { title: '安装不完整', explanation: '必要内容缺失或损坏,需要运行同步。' }
785
+ : { title: '状态待同步', explanation: '现场状态与期望状态不一致,需要重新检查并同步。' };
641
786
  const dependencyCodes = [
642
787
  'dependencyMissing',
643
788
  'dependencyNotReady',
@@ -714,37 +859,35 @@ function targetIssues(payload) {
714
859
  const dependencyScope = rootError?.dependencyScope;
715
860
  const dependencyProject = rootError?.dependencyProject;
716
861
  const dependencyCategory = root.category.startsWith('dependency');
717
- const dependencyTarget = dependencyPluginId && dependencyPlatform && dependencyScope
718
- ? payload.targets.find((candidate) => candidate.pluginId === dependencyPluginId
719
- && candidate.platform === dependencyPlatform && candidate.scope === dependencyScope)
720
- : undefined;
721
- const dependencyLifecycleActions = dependencyPluginId && dependencyPlatform && dependencyScope
722
- ? lifecycleIssueActions(dependencyTarget ?? {
723
- pluginId: dependencyPluginId,
724
- platform: dependencyPlatform,
725
- scope: dependencyScope,
726
- state: 'absent',
727
- recoveryActions: [],
728
- }, { dependency: true, project: dependencyProject })
729
- : [];
730
- 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;
731
870
  const actions = root.category === 'dependencyMissing'
732
871
  ? [
733
- ...dependencyLifecycleActions,
872
+ ...consumerRecoveryActions,
873
+ ...(dependencyPluginId ? [
874
+ { id: 'viewDependencyIssue', label: '查看依赖插件问题', pluginId: dependencyPluginId },
875
+ ] : []),
734
876
  { id: 'viewDependencies', label: '查看依赖关系' },
735
877
  { id: 'viewTechnical', label: '技术详情' },
736
878
  ]
737
879
  : root.category === 'dependencyNotReady'
738
880
  ? [
881
+ ...consumerRecoveryActions,
739
882
  ...(dependencyPluginId ? [
740
883
  { id: 'viewDependencyIssue', label: '查看依赖插件问题', pluginId: dependencyPluginId },
741
884
  ] : []),
742
- ...dependencyLifecycleActions.filter((action) => action.id === 'reinstallDependency'),
743
885
  { id: 'viewDependencies', label: '查看依赖关系' },
744
886
  { id: 'viewTechnical', label: '技术详情' },
745
887
  ]
746
888
  : dependencyCategory
747
889
  ? [
890
+ ...consumerRecoveryActions,
748
891
  { id: 'viewDependencies', label: '查看依赖关系' },
749
892
  { id: 'viewTechnical', label: '技术详情' },
750
893
  ]
@@ -755,12 +898,12 @@ function targetIssues(payload) {
755
898
  ]
756
899
  : root.category === 'incompleteInstallation'
757
900
  ? [
758
- ...directLifecycleActions,
901
+ ...consumerRecoveryActions,
759
902
  { id: 'viewTechnical', label: '技术详情' },
760
903
  ]
761
904
  : root.category === 'sourceMissing'
762
905
  ? [
763
- ...directLifecycleActions,
906
+ ...consumerRecoveryActions,
764
907
  { id: 'reAddSource', label: '重新添加同一来源', description: '保留来源 ID;来源类型和位置由你选择' },
765
908
  { id: 'viewTechnical', label: '技术详情' },
766
909
  ]
@@ -771,7 +914,7 @@ function targetIssues(payload) {
771
914
  ]
772
915
  : root.category === 'sourceUnavailable'
773
916
  ? [
774
- ...directLifecycleActions.filter((action) => action.id === 'cleanup'),
917
+ ...consumerRecoveryActions.filter((action) => action.id === 'cleanup'),
775
918
  { id: 'editSource', label: '查看来源', description: root.code === 'bundleUnavailable'
776
919
  ? '检查当前插件目录或稍后重试'
777
920
  : root.code === 'codexIntentSourceUnavailable'
@@ -779,7 +922,7 @@ function targetIssues(payload) {
779
922
  : '修复来源配置或稍后重试' },
780
923
  { id: 'viewTechnical', label: '技术详情' },
781
924
  ]
782
- : [...directLifecycleActions,
925
+ : [...consumerRecoveryActions,
783
926
  { id: 'viewTechnical', label: '技术详情' },
784
927
  { id: 'retryLater', label: '稍后重试' }];
785
928
  const bundleUnavailable = root.code === 'bundleUnavailable';
@@ -818,25 +961,27 @@ function targetIssues(payload) {
818
961
  scope: target.scope,
819
962
  relationship: relationship.relationship,
820
963
  }],
821
- 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 ? {
822
965
  dependencyMissing: '缺少依赖',
823
966
  dependencyNotReady: '依赖安装不完整',
824
967
  dependencyTargetUnsupported: '依赖目标不兼容',
825
968
  dependencyStillRequired: '依赖仍被使用',
826
969
  dependencyGraphInvalid: '依赖关系无效',
827
- }[root.category] : target.state === 'needsSync' ? '安装不完整' : bundleUnavailable ? '当前插件目录不再包含此插件' : codexIntentSourceUnavailable ? '已保存使用设置不能解析' : '检查失败'}`,
970
+ }[root.category] : target.state === 'needsSync' || statusUnconfirmed || systemConsumer?.state === 'incompatible' ? statusIssue.title : bundleUnavailable ? '当前插件目录不再包含此插件' : codexIntentSourceUnavailable ? '已保存使用设置不能解析' : '检查失败'}`,
828
971
  explanation: root.category === 'artifactCollision'
829
972
  ? '目标位置已有不属于当前插件的内容。AW 不会覆盖或删除这些内容。'
830
973
  : dependencyCategory
831
974
  ? dependencyPluginId
832
- ? `${dependencyPluginId} 导致当前依赖检查不能通过。`
833
- : '当前依赖检查不能安全通过。'
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' ? '所有项目' : '当前项目'})。`
834
979
  : sharedNativeDamaged
835
980
  ? 'Codex 平台共享生成内容缺失或损坏。'
836
981
  : sharedNativeRenderDrift
837
982
  ? 'Codex 平台共享生成内容与当前渲染输入不一致。'
838
- : target.state === 'needsSync'
839
- ? '必要内容缺失,插件可能不能稳定运行。'
983
+ : target.state === 'needsSync' || statusUnconfirmed || systemConsumer?.state === 'incompatible'
984
+ ? statusIssue.explanation
840
985
  : bundleUnavailable
841
986
  ? '当前插件目录不再包含此插件,但仍发现 AW 管理的已安装内容。'
842
987
  : codexIntentSourceUnavailable
@@ -1194,6 +1339,14 @@ function issueReasonPriority(issue) {
1194
1339
  diagnostic: 9,
1195
1340
  }[code] ?? 9;
1196
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
+ }
1197
1350
  function collapseRootIssues(issues) {
1198
1351
  const byId = new Map();
1199
1352
  for (const issue of issues)
@@ -1231,6 +1384,7 @@ function collapseRootIssues(issues) {
1231
1384
  impact: [...new Set(ordered.flatMap((issue) => issue.impact))],
1232
1385
  technicalDetails: [...new Map(ordered.flatMap((issue) => issue.technicalDetails)
1233
1386
  .map((detail) => [`${detail.code}\0${detail.message}`, detail])).values()],
1387
+ actions: dedupeIssueActions(ordered.flatMap((issue) => issue.actions)),
1234
1388
  };
1235
1389
  });
1236
1390
  }
@@ -1240,7 +1394,7 @@ export function buildTuiViewModel(options) {
1240
1394
  const summaryById = new Map(options.pluginSummaries.map((summary) => [summary.pluginId, summary]));
1241
1395
  const sourceById = new Map(options.status.catalog.sources.map((source) => [source.id, source]));
1242
1396
  const targetsByPlugin = new Map();
1243
- for (const target of options.status.targets) {
1397
+ for (const target of flattenStatusTargets(options.status.targets)) {
1244
1398
  targetsByPlugin.set(target.pluginId, [...(targetsByPlugin.get(target.pluginId) ?? []), target]);
1245
1399
  }
1246
1400
  const configScopesByPlugin = new Map();
@@ -1263,6 +1417,21 @@ export function buildTuiViewModel(options) {
1263
1417
  const configOnly = summary === undefined && targets.length === 0 && savedConfigScopes.length > 0;
1264
1418
  const placements = ['codex', 'claude'].flatMap((platform) => {
1265
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
+ }
1266
1435
  return platformTargets.length > 0
1267
1436
  ? [projectPlatformPlacement(platform, platformTargets, options.targetRevisions)]
1268
1437
  : [];
@@ -1329,15 +1498,31 @@ export function buildTuiViewModel(options) {
1329
1498
  issueCount: summary.dependencyIssueCount ?? 0,
1330
1499
  } }
1331
1500
  : {}),
1332
- technicalComponents: targets.flatMap((target) => target.components.map((component) => ({
1333
- platform: target.platform,
1334
- scope: target.scope,
1335
- key: component.key,
1336
- state: component.state,
1337
- ...(typeof component.metadata?.errorCode === 'string'
1338
- ? { errorCode: component.metadata.errorCode }
1339
- : {}),
1340
- }))),
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
+ ],
1341
1526
  update,
1342
1527
  };
1343
1528
  }).sort((left, right) => compareText(left.displayName, right.displayName)
@@ -1353,11 +1538,11 @@ export function buildTuiViewModel(options) {
1353
1538
  .filter((health) => health.state === 'unavailable')
1354
1539
  .map((health) => health.sourceId));
1355
1540
  const allTargetIssues = targetIssues(options.status);
1356
- const targetImpactCounts = new Map();
1541
+ const mergedTargetIssues = new Map();
1357
1542
  for (const issue of allTargetIssues) {
1358
1543
  if (!issue.mergeEligible || !issue.sourceId || !sourceRootIds.has(issue.sourceId))
1359
1544
  continue;
1360
- targetImpactCounts.set(issue.sourceId, (targetImpactCounts.get(issue.sourceId) ?? 0) + 1);
1545
+ mergedTargetIssues.set(issue.sourceId, [...(mergedTargetIssues.get(issue.sourceId) ?? []), issue]);
1361
1546
  }
1362
1547
  const sourceDetails = new Map((options.sourceDetails ?? []).map((detail) => [detail.id, detail]));
1363
1548
  const issueDrafts = [
@@ -1366,7 +1551,7 @@ export function buildTuiViewModel(options) {
1366
1551
  ...statusIssues(options.status),
1367
1552
  ...configIssues(options.pluginSummaries),
1368
1553
  ...dependencyOverviewIssues(options.dependencyOverviews ?? [], options.pluginSummaries, options.status),
1369
- ...[...(options.sourceHealth?.values() ?? [])].flatMap((health) => sourceIssue(health, targetImpactCounts.get(health.sourceId) ?? 0) ?? []),
1554
+ ...[...(options.sourceHealth?.values() ?? [])].flatMap((health) => sourceIssue(health, mergedTargetIssues.get(health.sourceId) ?? []) ?? []),
1370
1555
  ];
1371
1556
  const stableIssueDrafts = collapseRootIssues(issueDrafts).sort((left, right) => issueGroupOrder[issueGroup(left)] - issueGroupOrder[issueGroup(right)]
1372
1557
  || issueReasonPriority(left) - issueReasonPriority(right)
@@ -1407,7 +1592,9 @@ export function buildTuiViewModel(options) {
1407
1592
  dependencyOverviews: options.dependencyOverviews ?? [],
1408
1593
  home: {
1409
1594
  availablePlugins: pluginsWithAttention.filter((plugin) => plugin.aggregateStatus === 'available').length,
1410
- installedPlugins: pluginsWithAttention.filter(hasInstalledPlacement).length,
1595
+ installedPlugins: new Set(pluginsWithAttention
1596
+ .filter(hasInstalledPlacement)
1597
+ .map((plugin) => plugin.pluginId)).size,
1411
1598
  issueCount: issues.length,
1412
1599
  sourceCount: sourceTabs.length,
1413
1600
  unavailableSourceCount: options.status.catalog.sources.filter((source) => source.error