@lesliechan721/aw 0.1.0-beta.13 → 0.1.0-beta.15

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 (66) hide show
  1. package/dist/src/commands/catalog-dev-source.d.ts +31 -0
  2. package/dist/src/commands/catalog-dev-source.js +165 -5
  3. package/dist/src/commands/catalog-dev-source.js.map +1 -1
  4. package/dist/src/commands/catalog-generation.d.ts +5 -0
  5. package/dist/src/commands/catalog-generation.js.map +1 -1
  6. package/dist/src/commands/catalog-publication-journal.d.ts +12 -0
  7. package/dist/src/commands/catalog-publication-journal.js +77 -0
  8. package/dist/src/commands/catalog-publication-journal.js.map +1 -1
  9. package/dist/src/commands/catalog-store.d.ts +5 -0
  10. package/dist/src/commands/catalog-store.js +19 -1
  11. package/dist/src/commands/catalog-store.js.map +1 -1
  12. package/dist/src/commands/dependency-operation-plan.d.ts +7 -0
  13. package/dist/src/commands/dependency-operation-plan.js +63 -27
  14. package/dist/src/commands/dependency-operation-plan.js.map +1 -1
  15. package/dist/src/commands/rebuild-command.d.ts +22 -8
  16. package/dist/src/commands/rebuild-command.js +562 -250
  17. package/dist/src/commands/rebuild-command.js.map +1 -1
  18. package/dist/src/commands/reconcile-operation-plan.d.ts +11 -9
  19. package/dist/src/commands/reconcile-operation-plan.js +25 -6
  20. package/dist/src/commands/reconcile-operation-plan.js.map +1 -1
  21. package/dist/src/commands/status-v6.d.ts +2 -1
  22. package/dist/src/commands/status-v6.js +7 -1
  23. package/dist/src/commands/status-v6.js.map +1 -1
  24. package/dist/src/commands/write-commands.d.ts +2 -1
  25. package/dist/src/commands/write-commands.js +24 -1
  26. package/dist/src/commands/write-commands.js.map +1 -1
  27. package/dist/src/tui/catalog-management-screens.d.ts +45 -0
  28. package/dist/src/tui/catalog-management-screens.js +25 -0
  29. package/dist/src/tui/catalog-management-screens.js.map +1 -0
  30. package/dist/src/tui/catalog-management.d.ts +240 -0
  31. package/dist/src/tui/catalog-management.js +856 -0
  32. package/dist/src/tui/catalog-management.js.map +1 -0
  33. package/dist/src/tui/catalog-sources/session.d.ts +5 -0
  34. package/dist/src/tui/catalog-sources/session.js +49 -1
  35. package/dist/src/tui/catalog-sources/session.js.map +1 -1
  36. package/dist/src/tui/catalog-sources/types.d.ts +11 -0
  37. package/dist/src/tui/console-app-v4.d.ts +1 -1
  38. package/dist/src/tui/console-app-v4.js +2597 -587
  39. package/dist/src/tui/console-app-v4.js.map +1 -1
  40. package/dist/src/tui/controller-runtime.js +668 -50
  41. package/dist/src/tui/controller-runtime.js.map +1 -1
  42. package/dist/src/tui/controller.d.ts +136 -7
  43. package/dist/src/tui/controller.js +752 -19
  44. package/dist/src/tui/controller.js.map +1 -1
  45. package/dist/src/tui/dependency-graph.d.ts +67 -0
  46. package/dist/src/tui/dependency-graph.js +399 -0
  47. package/dist/src/tui/dependency-graph.js.map +1 -0
  48. package/dist/src/tui/lifecycle-action-view.js +4 -2
  49. package/dist/src/tui/lifecycle-action-view.js.map +1 -1
  50. package/dist/src/tui/model.d.ts +37 -0
  51. package/dist/src/tui/model.js +163 -7
  52. package/dist/src/tui/model.js.map +1 -1
  53. package/dist/src/tui/operation-review-planner.js +266 -9
  54. package/dist/src/tui/operation-review-planner.js.map +1 -1
  55. package/dist/src/tui/plugin-write-task.d.ts +67 -0
  56. package/dist/src/tui/plugin-write-task.js +282 -0
  57. package/dist/src/tui/plugin-write-task.js.map +1 -0
  58. package/dist/src/tui/router.d.ts +39 -2
  59. package/dist/src/tui/router.js +13 -5
  60. package/dist/src/tui/router.js.map +1 -1
  61. package/dist/src/tui/screen-registry.d.ts +125 -6
  62. package/dist/src/tui/screen-registry.js +35 -4
  63. package/dist/src/tui/screen-registry.js.map +1 -1
  64. package/dist/src/utils/catalog-generation-service.d.ts +4 -0
  65. package/dist/src/utils/catalog-generation-service.js.map +1 -1
  66. package/package.json +3 -3
@@ -13,9 +13,12 @@ import { projectCatalogSourceLocation } from '../utils/catalog-source-presentati
13
13
  import { ActionRow, Callout, ChoiceRow, diagnosticDetailRowCount, DiagnosticSummary, InputCard, PluginListRow, ProgressRow, ResultCard, ResultRow, ReviewCard, ReviewChangeRow, ScreenFrame, SectionCard, SectionRow, StaticRow, StatusCard, TaskRow, TechnicalDetails, ToggleRow, } from './components.js';
14
14
  import { safeDiagnosticCode, sanitizeDiagnosticText, } from './diagnostics.js';
15
15
  import { extractPortableError, TuiConfigInvalidStoreError, userIssueKey, } from './controller.js';
16
+ import { pluginWriteTaskVersionEvidence } from './plugin-write-task.js';
16
17
  import { catalogSourceFreshnessLabel, catalogSourceListPresentation, } from './catalog-sources/screens.js';
18
+ import { CatalogManagementIntro, CatalogPublicationSummary, CatalogStoreSummary, CatalogWorkspaceSummary, } from './catalog-management-screens.js';
17
19
  import { operationDiagnosticLineCount, operationHistoryMeta, operationHistoryState, operationReportLineCount, OperationDiagnosticScreen, OperationReportPreviewScreen, } from './operation-diagnostic-screens.js';
18
20
  import { resolveTuiSizePolicy } from './layout.js';
21
+ import { buildDependencyGraphProjection, } from './dependency-graph.js';
19
22
  import { platformLabel, scopeLabel } from './glossary.js';
20
23
  import { InputDisplay, removeLastGrapheme } from './input-display.js';
21
24
  import { catalogRefPolicyForPreset } from './source-ref-presets.js';
@@ -30,6 +33,7 @@ function sourceOperationBlockLabel(reason) {
30
33
  case 'configDraft': return '未保存的插件设置';
31
34
  case 'sourceDraft': return '未保存的来源修改';
32
35
  case 'batchDraft': return '未完成的批量操作选择';
36
+ case 'catalogDraft': return '未完成的 Catalog 管理输入';
33
37
  case 'portableDraft': return '未完成的通用 Skills 选择';
34
38
  case 'activeReview': return '等待确认的操作';
35
39
  case 'reviewPreparation': return '正在准备确认信息的操作';
@@ -75,6 +79,11 @@ function isCharacterInputMode(route, searchMode) {
75
79
  || route.name === 'configObjectPropertyEditor'
76
80
  && (route.stage === 'key' || route.stage === 'custom')
77
81
  || route.name === 'sourceAdd' && route.step !== 'type' && route.step !== 'refPolicy'
82
+ || route.name === 'catalogWorkspace'
83
+ && (route.view === 'select' || route.view === 'create'
84
+ || route.view === 'version' && route.step === 'catalogVersion')
85
+ || route.name === 'catalogDev' && route.step !== undefined && route.step !== 'overview'
86
+ || route.name === 'catalogPublish' && route.step !== 'overview'
78
87
  || route.name === 'scriptTimeout';
79
88
  }
80
89
  function helpRowsForRoute(route, actionCount, supportsTechnicalDetails, busy, sessionNavigation) {
@@ -104,7 +113,7 @@ function helpRowsForRoute(route, actionCount, supportsTechnicalDetails, busy, se
104
113
  rows.push(['s', '查看保存汇总']);
105
114
  rows.push(['u', '恢复当前字段继承']);
106
115
  }
107
- if (route.name === 'sourceList' || route.name === 'sourceDetail') {
116
+ if (route.name === 'sourceList' || route.name === 'sourceDetail' || route.name === 'sourceMore') {
108
117
  rows.push(['r', '重新检查来源']);
109
118
  }
110
119
  if (supportsTechnicalDetails) {
@@ -145,9 +154,52 @@ function batchActionLabel(action) {
145
154
  cleanup: '清理',
146
155
  }[action];
147
156
  }
157
+ function pluginWriteTaskLabel(goal) {
158
+ return goal === 'update' ? '更新' : '恢复';
159
+ }
160
+ function pluginWriteTaskBlockLabel(reason) {
161
+ return {
162
+ catalogActivationRequired: '需要先更新并启用来源目录',
163
+ updateEvidenceIncomplete: '更新版本证据不完整',
164
+ updateIncompatible: '候选版本与当前 AW 不兼容',
165
+ restoreVersionChanged: '恢复会改变版本,需要单独决定',
166
+ restoreVersionUnverified: '当前安装版本无法验证',
167
+ manualRecoveryRequired: '需要选择手动处理方式',
168
+ taskActionConflict: '同一根因需要不同处理方式',
169
+ targetUnavailable: '这个安装位置已不可用',
170
+ }[reason ?? 'targetUnavailable'];
171
+ }
172
+ function pluginWriteTaskMethodLabel(method) {
173
+ return method === 'reinstall' ? '重新生成缺失内容' : '同步当前使用位置';
174
+ }
175
+ function pluginWriteTaskTargetKey(candidate) {
176
+ const { pluginId, platform, scope } = candidate.target;
177
+ return [pluginId, platform, scope].join('\0');
178
+ }
179
+ function dependencyGraphContextKey(pluginId, direction) {
180
+ return `${pluginId}\0${direction}`;
181
+ }
182
+ function dependencyGraphActionIds(projection) {
183
+ return [
184
+ ...projection.rows.map((row) => `row:${row.id}`),
185
+ ...projection.scopeSummary.unknownConsumerPluginIds.map((pluginId) => `unverified:${pluginId}`),
186
+ 'direction',
187
+ 'technical',
188
+ ];
189
+ }
148
190
  function statusText(plugin) {
149
191
  if (plugin.configOnly)
150
192
  return '已保存设置';
193
+ const updateTargets = (plugin.update?.targets ?? []).filter((target) => target.availableVersion !== undefined);
194
+ if (updateTargets.length === 1) {
195
+ const target = updateTargets[0];
196
+ return `可更新 ${target.installedVersion ?? '待确认'} → ${target.availableVersion}${target.requiresCatalogActivation ? '(需先更新来源)' : ''}`;
197
+ }
198
+ if (updateTargets.length > 1)
199
+ return `${updateTargets.length} 个位置可更新`;
200
+ const incompatibleCandidate = plugin.update?.targets.find((target) => target.candidateVersion !== undefined && (target.missingCapabilities?.length ?? 0) > 0);
201
+ if (incompatibleCandidate)
202
+ return `新版 ${incompatibleCandidate.candidateVersion} 暂不可用`;
151
203
  return {
152
204
  available: '未添加',
153
205
  installed: '可用',
@@ -157,6 +209,21 @@ function statusText(plugin) {
157
209
  : '来源不可用',
158
210
  }[plugin.aggregateStatus];
159
211
  }
212
+ function pluginUpdateDescription(plugin) {
213
+ const targets = (plugin.update?.targets ?? []).filter((target) => target.availableVersion !== undefined);
214
+ if (targets.length === 0) {
215
+ const incompatibleCandidate = plugin.update?.targets.find((target) => target.candidateVersion !== undefined && (target.missingCapabilities?.length ?? 0) > 0);
216
+ return incompatibleCandidate
217
+ ? `候选版本缺少 ${incompatibleCandidate.missingCapabilities.length} 项 AW 功能支持;当前启用版本可继续使用。`
218
+ : undefined;
219
+ }
220
+ if (targets.some((target) => target.requiresCatalogActivation)) {
221
+ return plugin.update?.lastSuccessful
222
+ ? `上次成功检查:${plugin.update.checkedAt ? new Date(plugin.update.checkedAt).toLocaleString('zh-CN', { hour12: false }) : '时间未知'};需先更新来源目录。`
223
+ : '已验证待检查版本;需先更新来源目录。';
224
+ }
225
+ return '当前启用目录已验证;可直接进入更新确认。';
226
+ }
160
227
  function pluginPlacementSummary(plugin) {
161
228
  if (plugin.configOnly) {
162
229
  return plugin.savedConfigScopes.length > 0
@@ -728,6 +795,17 @@ function dependencyRelationDescription(item) {
728
795
  function dependencySummaryText(summary) {
729
796
  return `${summary.directCount} 个直接 · ${summary.totalCount} 个总计`;
730
797
  }
798
+ function dependencyGraphRowStatus(row) {
799
+ if (row.nodeState === 'ready')
800
+ return { value: '正常', status: 'success' };
801
+ if (row.nodeState === 'missing')
802
+ return { value: '缺失', status: 'error' };
803
+ if (row.nodeState === 'needsAttention')
804
+ return { value: '待恢复', status: 'warning' };
805
+ if (row.nodeState === 'unavailable')
806
+ return { value: '不可用', status: 'error' };
807
+ return { value: '未检查', status: 'muted' };
808
+ }
731
809
  function isCatalogSourcesBootstrapFlow(snapshot) {
732
810
  const route = snapshot.router.current.route;
733
811
  if (route.name === 'reviewPreparing') {
@@ -790,7 +868,7 @@ export function ConsoleAppV4(props) {
790
868
  const [focus, setFocus] = useState(0);
791
869
  const [scrollOffset, setScrollOffset] = useState(0);
792
870
  const [searchFocus, setSearchFocus] = useState(false);
793
- const [query, setQuery] = useState('');
871
+ const [query, setQuery] = useState(props.controller.snapshot.router.current.context.query ?? '');
794
872
  const [installFilter, setInstallFilter] = useState(props.controller.snapshot.router.current.context.installFilter ?? 'all');
795
873
  const [scope, setScope] = useState('user');
796
874
  const [selectedTargets, setSelectedTargets] = useState([]);
@@ -803,6 +881,9 @@ export function ConsoleAppV4(props) {
803
881
  const [dirty, setDirty] = useState(false);
804
882
  const [configPage, setConfigPage] = useState();
805
883
  const [configLayerCounts, setConfigLayerCounts] = useState({});
884
+ const [configLayerPages, setConfigLayerPages] = useState({});
885
+ const [configSaveImpact, setConfigSaveImpact] = useState();
886
+ const [configSaveImpactError, setConfigSaveImpactError] = useState(false);
806
887
  const [draftUpdates, setDraftUpdates] = useState({});
807
888
  const [draftRemoves, setDraftRemoves] = useState([]);
808
889
  const [objectDrafts, setObjectDrafts] = useState({});
@@ -816,8 +897,24 @@ export function ConsoleAppV4(props) {
816
897
  const [sourceDraft, setSourceDraft] = useState();
817
898
  const [batchDraft, setBatchDraft] = useState();
818
899
  const [batchTargetOffset, setBatchTargetOffset] = useState(0);
900
+ const [expandedDependencyPluginIds, setExpandedDependencyPluginIds] = useState();
819
901
  const [sourceInput, setSourceInput] = useState('');
820
902
  const [sourceInputError, setSourceInputError] = useState();
903
+ const [catalogInput, setCatalogInput] = useState('');
904
+ const [catalogInputError, setCatalogInputError] = useState();
905
+ const [catalogWorkspace, setCatalogWorkspace] = useState();
906
+ const [catalogValidation, setCatalogValidation] = useState();
907
+ const [catalogDev, setCatalogDev] = useState();
908
+ const [catalogStore, setCatalogStore] = useState();
909
+ const [catalogPublication, setCatalogPublication] = useState();
910
+ const [catalogLoading, setCatalogLoading] = useState(false);
911
+ const [catalogWorkspaceDraft, setCatalogWorkspaceDraft] = useState({
912
+ ownerName: '', ownerUrl: '', displayName: '', catalogVersion: '0.1.0',
913
+ });
914
+ const [catalogPublishDraft, setCatalogPublishDraft] = useState({
915
+ targetKind: 'remote', target: '', branch: 'catalog',
916
+ });
917
+ const [catalogDevSourceId, setCatalogDevSourceId] = useState('');
821
918
  const [pendingReviewIntent, setPendingReviewIntent] = useState();
822
919
  const pendingReviewIntentRef = useRef(undefined);
823
920
  const [scriptTimeoutInput, setScriptTimeoutInput] = useState(String(SCRIPT_TIMEOUT_DEFAULT_MS));
@@ -834,9 +931,11 @@ export function ConsoleAppV4(props) {
834
931
  const [pendingLayerFieldKey, setPendingLayerFieldKey] = useState();
835
932
  const [confirmedSharedReviewToken, setConfirmedSharedReviewToken] = useState();
836
933
  const dynamicMenuFocus = useRef(undefined);
837
- const externalDraftRevision = useRef({ config: 0, source: 0, batch: 0 });
934
+ const dependencyGraphContexts = useRef(new Map());
935
+ const externalDraftRevision = useRef({ config: 0, source: 0, batch: 0, catalog: 0 });
838
936
  const previousExternalDraftValues = useRef({
839
937
  draftUpdates, draftRemoves, objectDrafts, changedObjectDraftKeys, sourceDraft, batchDraft,
938
+ catalogInput, catalogWorkspaceDraft, catalogPublishDraft, catalogDevSourceId,
840
939
  });
841
940
  const externalDraftFlowIds = useRef(new Map());
842
941
  const exitSent = useRef(false);
@@ -939,6 +1038,36 @@ export function ConsoleAppV4(props) {
939
1038
  setBatchDraft(undefined);
940
1039
  setDirty(false);
941
1040
  }, [route.name, snapshot.result?.operationKind]);
1041
+ useEffect(() => {
1042
+ if (route.name !== 'result'
1043
+ || snapshot.result?.state !== 'succeeded'
1044
+ || snapshot.result.operationKind !== 'pluginWriteTask'
1045
+ || !('returnRoute' in snapshot.result)
1046
+ || snapshot.result.returnRoute?.name !== 'pluginWriteTask'
1047
+ || snapshot.result.returnRoute.goal !== snapshot.result.summary.task?.goal
1048
+ || snapshot.result.returnRoute.batchOrigin?.owner !== 'batch-write'
1049
+ || snapshot.result.returnRoute.batchOrigin.flowId !== batchDraft?.flowId)
1050
+ return;
1051
+ setBatchDraft(undefined);
1052
+ setDirty(false);
1053
+ }, [batchDraft?.flowId, route.name, snapshot.result]);
1054
+ useEffect(() => {
1055
+ if (route.name !== 'result'
1056
+ || snapshot.result?.state !== 'succeeded'
1057
+ || snapshot.result.operationKind !== 'catalogManagement')
1058
+ return;
1059
+ const action = snapshot.result.summary.catalogManagement?.action;
1060
+ if (action === 'workspaceInit') {
1061
+ setCatalogInput('');
1062
+ setCatalogWorkspaceDraft({
1063
+ ownerName: '', ownerUrl: '', displayName: '', catalogVersion: '0.1.0',
1064
+ });
1065
+ }
1066
+ if (action === 'publishApply') {
1067
+ setCatalogInput('');
1068
+ setCatalogPublishDraft({ targetKind: 'remote', target: '', branch: 'catalog' });
1069
+ }
1070
+ }, [route.name, snapshot.result]);
942
1071
  useEffect(() => {
943
1072
  if (!batchDraft || (route.name !== 'batchSelection' && route.name !== 'batchPlacement'))
944
1073
  return;
@@ -948,6 +1077,74 @@ export function ConsoleAppV4(props) {
948
1077
  if (route.name !== 'sourceAdd')
949
1078
  setSourceInput('');
950
1079
  }, [route.name]);
1080
+ useEffect(() => {
1081
+ if (route.name !== 'catalogWorkspace' || route.view !== 'overview')
1082
+ return;
1083
+ let current = true;
1084
+ setCatalogLoading(true);
1085
+ setCatalogInputError(undefined);
1086
+ void props.controller.getCatalogWorkspaceManagement()
1087
+ .then((value) => { if (current)
1088
+ setCatalogWorkspace(value); })
1089
+ .catch((error) => {
1090
+ if (current)
1091
+ setCatalogInputError(sanitizeDiagnosticText(error instanceof Error ? error.message : String(error), [], [catalogInput]));
1092
+ })
1093
+ .finally(() => { if (current)
1094
+ setCatalogLoading(false); });
1095
+ return () => { current = false; };
1096
+ }, [props.controller, route.name, route.name === 'catalogWorkspace' ? route.view : '']);
1097
+ useEffect(() => {
1098
+ if (route.name !== 'catalogDev' || route.step && route.step !== 'overview')
1099
+ return;
1100
+ let current = true;
1101
+ setCatalogLoading(true);
1102
+ setCatalogInputError(undefined);
1103
+ void props.controller.getCatalogDevManagement()
1104
+ .then((value) => { if (current)
1105
+ setCatalogDev(value); })
1106
+ .catch((error) => {
1107
+ if (current)
1108
+ setCatalogInputError(sanitizeDiagnosticText(error instanceof Error ? error.message : String(error), [], [catalogInput]));
1109
+ })
1110
+ .finally(() => { if (current)
1111
+ setCatalogLoading(false); });
1112
+ return () => { current = false; };
1113
+ }, [props.controller, route.name, route.name === 'catalogDev' ? route.step : '']);
1114
+ useEffect(() => {
1115
+ if (route.name !== 'catalogStore')
1116
+ return;
1117
+ let current = true;
1118
+ setCatalogLoading(true);
1119
+ setCatalogInputError(undefined);
1120
+ void props.controller.getCatalogStoreManagement()
1121
+ .then((value) => { if (current)
1122
+ setCatalogStore(value); })
1123
+ .catch((error) => {
1124
+ if (current)
1125
+ setCatalogInputError(sanitizeDiagnosticText(error instanceof Error ? error.message : String(error), [], []));
1126
+ })
1127
+ .finally(() => { if (current)
1128
+ setCatalogLoading(false); });
1129
+ return () => { current = false; };
1130
+ }, [props.controller, route.name]);
1131
+ useEffect(() => {
1132
+ if (route.name !== 'catalogPublish' || route.step !== 'overview')
1133
+ return;
1134
+ let current = true;
1135
+ setCatalogLoading(true);
1136
+ setCatalogInputError(undefined);
1137
+ void props.controller.getCatalogPublicationManagement()
1138
+ .then((value) => { if (current)
1139
+ setCatalogPublication(value); })
1140
+ .catch((error) => {
1141
+ if (current)
1142
+ setCatalogInputError(sanitizeDiagnosticText(error instanceof Error ? error.message : String(error), [], [catalogPublishDraft.target]));
1143
+ })
1144
+ .finally(() => { if (current)
1145
+ setCatalogLoading(false); });
1146
+ return () => { current = false; };
1147
+ }, [props.controller, route.name, route.name === 'catalogPublish' ? route.step : '']);
951
1148
  useEffect(() => {
952
1149
  if (route.name === 'operationReportPreview')
953
1150
  return;
@@ -993,8 +1190,14 @@ export function ConsoleAppV4(props) {
993
1190
  externalDraftRevision.current.source += 1;
994
1191
  if (previous.batchDraft !== batchDraft)
995
1192
  externalDraftRevision.current.batch += 1;
1193
+ if (previous.catalogInput !== catalogInput
1194
+ || previous.catalogWorkspaceDraft !== catalogWorkspaceDraft
1195
+ || previous.catalogPublishDraft !== catalogPublishDraft
1196
+ || previous.catalogDevSourceId !== catalogDevSourceId)
1197
+ externalDraftRevision.current.catalog += 1;
996
1198
  previousExternalDraftValues.current = {
997
1199
  draftUpdates, draftRemoves, objectDrafts, changedObjectDraftKeys, sourceDraft, batchDraft,
1200
+ catalogInput, catalogWorkspaceDraft, catalogPublishDraft, catalogDevSourceId,
998
1201
  };
999
1202
  const currentFlowId = snapshot.router.currentFlowId;
1000
1203
  if (currentFlowId && configRouteOwner) {
@@ -1002,8 +1205,6 @@ export function ConsoleAppV4(props) {
1002
1205
  }
1003
1206
  if (currentFlowId && sourceDraft)
1004
1207
  externalDraftFlowIds.current.set(sourceDraft.owner, currentFlowId);
1005
- if (currentFlowId && batchDraft)
1006
- externalDraftFlowIds.current.set('batch-write', currentFlowId);
1007
1208
  const drafts = [];
1008
1209
  if ((dirty || changedObjectDraftKeys.length > 0) && draftOwner) {
1009
1210
  const flowId = externalDraftFlowIds.current.get(draftOwner);
@@ -1020,16 +1221,31 @@ export function ConsoleAppV4(props) {
1020
1221
  });
1021
1222
  }
1022
1223
  if (batchDraft) {
1023
- const flowId = externalDraftFlowIds.current.get('batch-write');
1024
1224
  drafts.push({
1025
1225
  kind: 'batchDraft', owner: 'batch-write', revision: externalDraftRevision.current.batch,
1026
- ...(flowId ? { flowId } : {}),
1226
+ flowId: batchDraft.flowId,
1027
1227
  });
1028
1228
  }
1229
+ const catalogWorkspaceDirty = catalogWorkspaceDraft.ownerName !== ''
1230
+ || catalogWorkspaceDraft.ownerUrl !== '' || catalogWorkspaceDraft.displayName !== '';
1231
+ const catalogPublishDirty = catalogPublishDraft.target !== '';
1232
+ const catalogDraftActive = route.name === 'catalogWorkspace'
1233
+ && (route.view === 'create' || route.view === 'version' && route.step === 'catalogVersion')
1234
+ || route.name === 'catalogPublish' && route.step !== 'overview'
1235
+ || route.name === 'catalogDev' && route.step !== undefined && route.step !== 'overview'
1236
+ || route.name === 'catalogWorkspace' && route.view === 'overview' && catalogWorkspaceDirty
1237
+ || route.name === 'catalogPublish' && route.step === 'overview' && catalogPublishDirty;
1238
+ if (catalogDraftActive)
1239
+ drafts.push({
1240
+ kind: 'catalogDraft', owner: 'catalog-management', revision: externalDraftRevision.current.catalog,
1241
+ });
1029
1242
  props.controller.setExternalDraftState(drafts);
1030
1243
  }, [
1031
1244
  props.controller, batchDraft, changedObjectDraftKeys, configRouteOwner, dirty, draftOwner,
1032
1245
  draftRemoves, draftUpdates, objectDrafts, snapshot.router.currentFlowId, sourceDraft,
1246
+ catalogInput, catalogWorkspaceDraft, catalogPublishDraft, catalogDevSourceId, route.name,
1247
+ route.name === 'catalogWorkspace' ? `${route.view}:${route.step ?? ''}` : '',
1248
+ route.name === 'catalogPublish' || route.name === 'catalogDev' ? route.step : '',
1033
1249
  ]);
1034
1250
  useEffect(() => () => props.controller.setExternalDraftState(), [props.controller]);
1035
1251
  const currentConfigPage = configPageMatches(configPage, configRoute) ? configPage : undefined;
@@ -1071,17 +1287,19 @@ export function ConsoleAppV4(props) {
1071
1287
  if (route.name !== 'configLayer')
1072
1288
  return;
1073
1289
  const abort = new AbortController();
1290
+ setConfigLayerPages({});
1074
1291
  const layers = [
1075
1292
  { key: 'user', layer: { scope: 'user' } },
1076
1293
  { key: 'project', layer: { scope: 'project' } },
1077
1294
  ];
1078
1295
  void Promise.allSettled(layers.map(async ({ key, layer }) => ({
1079
1296
  key,
1080
- count: (await props.controller.getConfigPage(route.pluginId, layer, abort.signal)).storedCount,
1297
+ page: await props.controller.getConfigPage(route.pluginId, layer, abort.signal),
1081
1298
  }))).then((results) => {
1082
1299
  if (abort.signal.aborted)
1083
1300
  return;
1084
- setConfigLayerCounts(Object.fromEntries(results.flatMap((result) => result.status === 'fulfilled' ? [[result.value.key, result.value.count]] : [])));
1301
+ setConfigLayerCounts(Object.fromEntries(results.flatMap((result) => result.status === 'fulfilled' ? [[result.value.key, result.value.page.storedCount]] : [])));
1302
+ setConfigLayerPages(Object.fromEntries(results.flatMap((result) => result.status === 'fulfilled' ? [[result.value.key, result.value.page]] : [])));
1085
1303
  });
1086
1304
  return () => abort.abort();
1087
1305
  }, [props.controller, route.name, route.name === 'configLayer' ? route.pluginId : '']);
@@ -1202,7 +1420,7 @@ export function ConsoleAppV4(props) {
1202
1420
  useEffect(() => {
1203
1421
  let count;
1204
1422
  if (route.name === 'sourceList')
1205
- count = props.controller.listSources().length + 2;
1423
+ count = props.controller.listSources().length + 3;
1206
1424
  else if (route.name === 'issueList') {
1207
1425
  const issueCount = props.controller.listUserIssues().length;
1208
1426
  count = issueCount || 2;
@@ -1217,7 +1435,18 @@ export function ConsoleAppV4(props) {
1217
1435
  ...(query ? { query } : {}),
1218
1436
  installFilter,
1219
1437
  }).length;
1220
- const menuCount = itemCount > 0 ? route.mode === 'installed' ? 2 : 1 : 0;
1438
+ const listed = new Set(props.controller.listPlugins({
1439
+ mode: route.mode,
1440
+ ...(route.sourceId ? { sourceId: route.sourceId } : {}),
1441
+ ...(query ? { query } : {}),
1442
+ installFilter,
1443
+ }).map((item) => item.pluginId));
1444
+ const menuCount = itemCount > 0 && !query ? 1
1445
+ + Number(route.mode === 'installed' && props.controller.listPluginWriteTaskCandidates('update')
1446
+ .some((candidate) => candidate.eligible && listed.has(candidate.target.pluginId)))
1447
+ + Number(route.mode === 'installed' && props.controller.listCurrentProjectIntentRoots().length > 0)
1448
+ + Number(route.mode === 'installed' && props.controller.listPluginWriteTaskCandidates('restore')
1449
+ .some((candidate) => candidate.target.scope === 'project')) : 0;
1221
1450
  count = itemCount + menuCount || 1;
1222
1451
  }
1223
1452
  if (count === undefined)
@@ -1239,6 +1468,74 @@ export function ConsoleAppV4(props) {
1239
1468
  return;
1240
1469
  setScrollOffset(0);
1241
1470
  }, [route.name, route.name === 'operationReportPreview' ? route.recordId : '']);
1471
+ useEffect(() => {
1472
+ if (route.name !== 'pluginDependencies')
1473
+ return;
1474
+ const direction = route.direction ?? 'dependencies';
1475
+ const key = dependencyGraphContextKey(route.pluginId, direction);
1476
+ const stored = dependencyGraphContexts.current.get(key);
1477
+ const overviews = props.controller.listPluginDependencyOverviews();
1478
+ const knownPluginIds = new Set(overviews.flatMap((overview) => [
1479
+ overview.pluginId,
1480
+ ...overview.dependencies.map((item) => item.pluginId),
1481
+ ...overview.dependents.map((item) => item.pluginId),
1482
+ ]));
1483
+ const expandedPluginIds = stored
1484
+ ? new Set([...stored.expandedPluginIds].filter((pluginId) => knownPluginIds.has(pluginId)))
1485
+ : undefined;
1486
+ const projection = buildDependencyGraphProjection({
1487
+ overviews,
1488
+ rootPluginId: route.pluginId,
1489
+ direction,
1490
+ ...(expandedPluginIds ? { expandedPluginIds } : {}),
1491
+ });
1492
+ const actionIds = dependencyGraphActionIds(projection);
1493
+ const normalizedExpanded = expandedPluginIds ?? new Set(projection.defaultExpandedPluginIds);
1494
+ const focusedIndex = stored?.focusedActionId
1495
+ ? actionIds.indexOf(stored.focusedActionId)
1496
+ : 0;
1497
+ const nextFocus = focusedIndex >= 0 ? focusedIndex : 0;
1498
+ const anchorIndex = stored?.scrollAnchorActionId
1499
+ ? actionIds.indexOf(stored.scrollAnchorActionId)
1500
+ : 0;
1501
+ const nextScroll = Math.max(0, anchorIndex >= 0 ? Math.min(anchorIndex, nextFocus) : 0);
1502
+ setExpandedDependencyPluginIds(normalizedExpanded);
1503
+ setFocus(nextFocus);
1504
+ setScrollOffset(nextScroll);
1505
+ dependencyGraphContexts.current.set(key, {
1506
+ expandedPluginIds: normalizedExpanded,
1507
+ ...(actionIds[nextFocus] ? { focusedActionId: actionIds[nextFocus] } : {}),
1508
+ ...(actionIds[nextScroll] ? { scrollAnchorActionId: actionIds[nextScroll] } : {}),
1509
+ });
1510
+ }, [
1511
+ props.controller,
1512
+ route.name,
1513
+ route.name === 'pluginDependencies' ? route.pluginId : '',
1514
+ route.name === 'pluginDependencies' ? route.direction ?? 'dependencies' : '',
1515
+ snapshot.viewModel,
1516
+ ]);
1517
+ useEffect(() => {
1518
+ if (route.name !== 'pluginList' || snapshot.phase !== 'ready'
1519
+ || snapshot.router.current.context.focusIndex !== undefined)
1520
+ return;
1521
+ const items = props.controller.listPlugins({
1522
+ mode: route.mode,
1523
+ ...(route.sourceId ? { sourceId: route.sourceId } : {}),
1524
+ ...(query ? { query } : {}),
1525
+ installFilter,
1526
+ });
1527
+ if (items.length === 0)
1528
+ return;
1529
+ const listed = new Set(items.map((item) => item.pluginId));
1530
+ const menuCount = query ? 0 : 1
1531
+ + Number(route.mode === 'installed' && props.controller.listPluginWriteTaskCandidates('update')
1532
+ .some((candidate) => candidate.eligible && listed.has(candidate.target.pluginId)))
1533
+ + Number(route.mode === 'installed' && props.controller.listCurrentProjectIntentRoots().length > 0)
1534
+ + Number(route.mode === 'installed' && props.controller.listPluginWriteTaskCandidates('restore')
1535
+ .some((candidate) => candidate.target.scope === 'project'));
1536
+ setFocus(menuCount);
1537
+ }, [props.controller, route.name, route.name === 'pluginList' ? route.mode : '',
1538
+ route.name === 'pluginList' ? route.sourceId : '', snapshot.phase]);
1242
1539
  useEffect(() => {
1243
1540
  setBatchTargetOffset(0);
1244
1541
  }, [route.name, route.name === 'review' || route.name === 'result' ? route.operationId : '']);
@@ -1383,11 +1680,9 @@ export function ConsoleAppV4(props) {
1383
1680
  let activeConfigTabId;
1384
1681
  const dynamicMenu = plugin && route.name === 'pluginPreview'
1385
1682
  ? buildPluginPreviewActions(plugin)
1386
- : plugin && route.name === 'pluginDetail'
1387
- ? buildPluginDetailActions(plugin)
1388
- : plugin && route.name === 'platform'
1389
- ? buildPlatformActions(plugin, route.platform)
1390
- : undefined;
1683
+ : plugin && route.name === 'platform'
1684
+ ? buildPlatformActions(plugin, route.platform)
1685
+ : undefined;
1391
1686
  const dynamicActionIds = dynamicMenu?.map(pluginMenuActionId);
1392
1687
  const dynamicActionSignature = dynamicActionIds?.join('\0');
1393
1688
  const previousDynamicFocus = dynamicMenuFocus.current;
@@ -1418,6 +1713,17 @@ export function ConsoleAppV4(props) {
1418
1713
  const base = policy.mode === 'compact' ? compactRows : standardRows;
1419
1714
  return base + Math.max(0, size.rows - baseline);
1420
1715
  };
1716
+ const pluginListMenuCount = (mode, items, queryValue = query) => {
1717
+ if (items.length === 0 || queryValue)
1718
+ return 0;
1719
+ const listed = new Set(items.map((item) => item.pluginId));
1720
+ return 1
1721
+ + Number(mode === 'installed' && props.controller.listPluginWriteTaskCandidates('update')
1722
+ .some((candidate) => candidate.eligible && listed.has(candidate.target.pluginId)))
1723
+ + Number(mode === 'installed' && props.controller.listCurrentProjectIntentRoots().length > 0)
1724
+ + Number(mode === 'installed' && props.controller.listPluginWriteTaskCandidates('restore')
1725
+ .some((candidate) => candidate.target.scope === 'project'));
1726
+ };
1421
1727
  const go = (next) => {
1422
1728
  props.controller.updatePageContext({
1423
1729
  focusIndex: focus,
@@ -1457,12 +1763,12 @@ export function ConsoleAppV4(props) {
1457
1763
  ? items.findIndex((item) => item.pluginId === restored.focusedEntityId)
1458
1764
  : -1;
1459
1765
  restoredFocus = entityIndex >= 0
1460
- ? entityIndex
1461
- : Math.min(restoredFocus, Math.max(0, items.length - 1));
1766
+ ? pluginListMenuCount(restoredFrame.route.mode, items, restored.query ?? '') + entityIndex
1767
+ : Math.min(restoredFocus, Math.max(0, pluginListMenuCount(restoredFrame.route.mode, items, restored.query ?? '') + items.length - 1));
1462
1768
  }
1463
1769
  else if (restoredFrame.route.name === 'sourceList') {
1464
1770
  const sources = props.controller.listSources();
1465
- itemCount = sources.length + 2;
1771
+ itemCount = sources.length + 3;
1466
1772
  const entityIndex = restored.focusedEntityId
1467
1773
  ? sources.findIndex((source) => source.id === restored.focusedEntityId)
1468
1774
  : -1;
@@ -1532,19 +1838,20 @@ export function ConsoleAppV4(props) {
1532
1838
  setShowTechnical(false);
1533
1839
  setActionError(undefined);
1534
1840
  };
1535
- const beginReview = (intent) => {
1841
+ const beginReview = (intent, returnRoute) => {
1536
1842
  const supportsScriptTimeout = intent.kind === 'pluginWrite'
1537
1843
  && (intent.action === 'install' || intent.action === 'sync'
1538
1844
  || intent.action === 'refreshAndSync' || intent.action === 'reinstall')
1539
1845
  || intent.kind === 'pluginWriteBatch'
1540
1846
  && (intent.action === 'install' || intent.action === 'sync' || intent.action === 'reinstall')
1847
+ || intent.kind === 'pluginWriteTask'
1541
1848
  || intent.kind === 'configSave' && intent.apply;
1542
1849
  const pendingIntent = supportsScriptTimeout ? intent : undefined;
1543
1850
  pendingReviewIntentRef.current = pendingIntent;
1544
1851
  setPendingReviewIntent(pendingIntent);
1545
- const underlyingResultRoute = route.name === 'result' && snapshot.result && 'returnRoute' in snapshot.result
1852
+ const underlyingResultRoute = returnRoute ?? (route.name === 'result' && snapshot.result && 'returnRoute' in snapshot.result
1546
1853
  ? snapshot.result.returnRoute
1547
- : undefined;
1854
+ : undefined);
1548
1855
  props.controller.updatePageContext({
1549
1856
  focusIndex: focus,
1550
1857
  scrollOffset,
@@ -1556,6 +1863,38 @@ export function ConsoleAppV4(props) {
1556
1863
  setFocus(0);
1557
1864
  setScrollOffset(0);
1558
1865
  };
1866
+ useEffect(() => {
1867
+ if (route.name !== 'pluginWriteTask' || route.goal !== 'restore')
1868
+ return;
1869
+ if (snapshot.router.current.context.autoPreparedRestore)
1870
+ return;
1871
+ const retainedTargetKeys = route.targetKeys ? new Set(route.targetKeys) : undefined;
1872
+ const allCandidates = props.controller.listPluginWriteTaskCandidates('restore');
1873
+ const currentTargetKeys = new Set(allCandidates.map(pluginWriteTaskTargetKey));
1874
+ if (route.targetKeys?.some((targetKey) => !currentTargetKeys.has(targetKey)))
1875
+ return;
1876
+ const candidates = allCandidates
1877
+ .filter((candidate) => retainedTargetKeys
1878
+ ? retainedTargetKeys.has(pluginWriteTaskTargetKey(candidate))
1879
+ : candidate.target.pluginId === route.pluginId);
1880
+ if (candidates.length === 0 || candidates.some((candidate) => !candidate.eligible))
1881
+ return;
1882
+ props.controller.updatePageContext({ autoPreparedRestore: true });
1883
+ beginReview({
1884
+ kind: 'pluginWriteTask',
1885
+ goal: 'restore',
1886
+ targets: candidates.map((candidate) => candidate.target),
1887
+ executionPolicy: 'all',
1888
+ }, props.controller.pluginWriteTaskReturnRoute(route));
1889
+ }, [
1890
+ props.controller,
1891
+ route.name,
1892
+ route.name === 'pluginWriteTask' ? route.goal : '',
1893
+ route.name === 'pluginWriteTask' ? route.pluginId : '',
1894
+ route.name === 'pluginWriteTask' ? route.targetKeys?.join('\0') ?? '' : '',
1895
+ snapshot.router.current.context.autoPreparedRestore,
1896
+ snapshot.viewModel,
1897
+ ]);
1559
1898
  const openBatchSelection = (action) => {
1560
1899
  props.controller.updatePageContext({
1561
1900
  focusIndex: focus,
@@ -1566,28 +1905,128 @@ export function ConsoleAppV4(props) {
1566
1905
  focusedEntityId: route.name === 'pluginList' ? pluginRows[focus]?.pluginId : undefined,
1567
1906
  });
1568
1907
  props.controller.beginPluginWriteBatch();
1908
+ const flowId = props.controller.snapshot.router.currentFlowId;
1909
+ if (!flowId)
1910
+ throw new Error('批量插件流程缺少 flowId。');
1569
1911
  setBatchDraft({
1912
+ flowId,
1570
1913
  ...(action ? { action } : {}),
1914
+ mode: route.name === 'pluginList' ? route.mode : action === 'install' ? 'browse' : 'installed',
1915
+ stage: 'select',
1571
1916
  selection: 'selected',
1572
1917
  selectedPluginIds: [],
1573
1918
  platform: 'both',
1574
1919
  scope: 'project',
1920
+ ...(route.name === 'pluginList' && route.sourceId ? { sourceId: route.sourceId } : {}),
1921
+ query,
1922
+ installFilter,
1923
+ showSelected: false,
1575
1924
  });
1576
1925
  setDirty(true);
1577
- setFocus(0);
1926
+ setFocus(1);
1578
1927
  setScrollOffset(0);
1579
1928
  setSearchFocus(false);
1580
1929
  };
1930
+ const batchCandidates = (draft) => {
1931
+ const mode = draft.mode;
1932
+ const all = props.controller.listPlugins({ mode });
1933
+ if (draft.showSelected) {
1934
+ const byId = new Map(all.map((candidate) => [candidate.pluginId, candidate]));
1935
+ return draft.selectedPluginIds.map((pluginId) => ({
1936
+ pluginId,
1937
+ ...(byId.get(pluginId) ? { plugin: byId.get(pluginId) } : {}),
1938
+ }));
1939
+ }
1940
+ return props.controller.listPlugins({
1941
+ mode,
1942
+ ...(draft.sourceId ? { sourceId: draft.sourceId } : {}),
1943
+ ...(draft.query ? { query: draft.query } : {}),
1944
+ installFilter: draft.installFilter,
1945
+ }).map((plugin) => ({ pluginId: plugin.pluginId, plugin }));
1946
+ };
1947
+ const batchCandidateBlockReason = (draft, candidate) => !candidate.plugin
1948
+ ? '插件已不在当前状态中,可取消选择'
1949
+ : candidate.plugin.configOnly
1950
+ ? '来源中没有可信插件定义'
1951
+ : draft.mode === 'browse' && hasInstalledPlacement(candidate.plugin)
1952
+ ? '这个插件已经添加'
1953
+ : undefined;
1954
+ const invalidBatchSelection = (draft) => {
1955
+ const byId = new Map(props.controller.listPlugins({ mode: draft.mode })
1956
+ .map((plugin) => [plugin.pluginId, plugin]));
1957
+ return draft.selectedPluginIds.filter((pluginId) => batchCandidateBlockReason(draft, {
1958
+ pluginId,
1959
+ ...(byId.get(pluginId) ? { plugin: byId.get(pluginId) } : {}),
1960
+ }) !== undefined);
1961
+ };
1962
+ const persistDependencyGraphContext = (graphRoute, projection, expandedPluginIds, focusIndex, offset) => {
1963
+ const direction = graphRoute.direction ?? 'dependencies';
1964
+ const actionIds = dependencyGraphActionIds(projection);
1965
+ dependencyGraphContexts.current.set(dependencyGraphContextKey(graphRoute.pluginId, direction), {
1966
+ expandedPluginIds: new Set(expandedPluginIds),
1967
+ ...(actionIds[focusIndex] ? { focusedActionId: actionIds[focusIndex] } : {}),
1968
+ ...(actionIds[offset] ? { scrollAnchorActionId: actionIds[offset] } : {}),
1969
+ });
1970
+ };
1971
+ const toggleBatchPlugin = (pluginId) => {
1972
+ setBatchDraft((current) => current ? {
1973
+ ...current,
1974
+ selectedPluginIds: current.selectedPluginIds.includes(pluginId)
1975
+ ? current.selectedPluginIds.filter((candidate) => candidate !== pluginId)
1976
+ : [...current.selectedPluginIds, pluginId],
1977
+ } : current);
1978
+ };
1979
+ const prepareCurrentProjectPlugins = () => {
1980
+ if (props.controller.listCurrentProjectIntentRoots().length === 0) {
1981
+ setActionError('当前项目没有已保存的插件清单。');
1982
+ return;
1983
+ }
1984
+ beginReview({
1985
+ kind: 'pluginWriteBatch',
1986
+ action: 'sync',
1987
+ selection: { kind: 'currentProjectIntentRoots' },
1988
+ platform: 'both',
1989
+ scope: 'project',
1990
+ });
1991
+ };
1992
+ const openSelectedPluginTask = (goal, pluginIds, scopeConstraint) => {
1993
+ if (batchDraft && invalidBatchSelection(batchDraft).length > 0) {
1994
+ setActionError('已选插件状态已变化。请返回选择页检查并取消失效项。');
1995
+ return;
1996
+ }
1997
+ const selected = new Set(pluginIds);
1998
+ const candidates = props.controller.listPluginWriteTaskCandidates(goal)
1999
+ .filter((candidate) => selected.has(candidate.target.pluginId)
2000
+ && (!scopeConstraint || candidate.target.scope === scopeConstraint));
2001
+ const first = candidates[0];
2002
+ if (!first) {
2003
+ setActionError(goal === 'update'
2004
+ ? '所选插件没有经过版本证据确认的更新。'
2005
+ : '所选插件没有可恢复的现有位置。');
2006
+ return;
2007
+ }
2008
+ go({
2009
+ name: 'pluginWriteTask',
2010
+ pluginId: first.target.pluginId,
2011
+ goal,
2012
+ targetKeys: candidates.map(pluginWriteTaskTargetKey),
2013
+ ...(batchDraft ? {
2014
+ batchOrigin: { flowId: batchDraft.flowId, owner: 'batch-write' },
2015
+ } : {}),
2016
+ ...(goal === 'update'
2017
+ ? { versionEvidence: Object.assign({}, ...[...selected].map((pluginId) => {
2018
+ const candidatePlugin = props.controller.getPluginOverview(pluginId);
2019
+ return candidatePlugin ? pluginWriteTaskVersionEvidence(candidatePlugin, candidates.filter((candidate) => candidate.target.pluginId === pluginId)) : {};
2020
+ })) }
2021
+ : {}),
2022
+ });
2023
+ };
1581
2024
  const continueInstallAfterTargets = async (candidate, targets) => {
1582
2025
  if (props.controller.snapshot.installPlacementDraft?.pluginId !== candidate.pluginId) {
1583
2026
  props.controller.beginInstallFlow(candidate.pluginId, scope);
1584
2027
  }
1585
2028
  props.controller.setInstallPlatforms(targets);
1586
- const hasConfigurableDependencies = await props.controller.prepareInstallDependencyPlacement();
1587
- if (hasConfigurableDependencies) {
1588
- go({ name: 'dependencyPlacement', pluginId: candidate.pluginId });
1589
- return;
1590
- }
2029
+ await props.controller.prepareInstallDependencyPlacement();
1591
2030
  beginReview(props.controller.installPlacementIntent());
1592
2031
  };
1593
2032
  const beginPortable = async (pluginId) => {
@@ -2069,18 +2508,19 @@ export function ConsoleAppV4(props) {
2069
2508
  const openConfigLayerTarget = (layer) => {
2070
2509
  if (route.name !== 'configLayer')
2071
2510
  return;
2072
- const field = route.fieldKey && configPage
2073
- ? configPage.groups.flatMap((group) => group.fields).find((candidate) => candidate.key === route.fieldKey)
2511
+ const definitionPage = configPage ?? configLayerPages.user ?? configLayerPages.project;
2512
+ const field = route.fieldKey && definitionPage
2513
+ ? definitionPage.groups.flatMap((group) => group.fields).find((candidate) => candidate.key === route.fieldKey)
2074
2514
  : undefined;
2075
- const fieldTarget = configPage ? configFieldTarget(configPage, route.fieldKey) : undefined;
2076
- const preset = route.presetId && configPage
2077
- ? configPage.presets.find((candidate) => candidate.id === route.presetId)
2515
+ const fieldTarget = definitionPage ? configFieldTarget(definitionPage, route.fieldKey) : undefined;
2516
+ const preset = route.presetId && definitionPage
2517
+ ? definitionPage.presets.find((candidate) => candidate.id === route.presetId)
2078
2518
  : undefined;
2079
2519
  const presetTarget = preset
2080
- ? configFieldTarget(configPage, Object.keys(preset.values)[0])
2520
+ ? configFieldTarget(definitionPage, Object.keys(preset.values)[0])
2081
2521
  : undefined;
2082
2522
  const target = fieldTarget ?? (route.groupId ? { groupId: route.groupId, focusIndex: 0 } : undefined) ?? presetTarget
2083
- ?? (configPage?.groups[0] ? { groupId: configPage.groups[0].id, focusIndex: 0 } : undefined);
2523
+ ?? (definitionPage?.groups[0] ? { groupId: definitionPage.groups[0].id, focusIndex: 0 } : undefined);
2084
2524
  const context = target
2085
2525
  ? { configGroupId: target.groupId, focusIndex: target.focusIndex, scrollOffset: 0 }
2086
2526
  : {};
@@ -2187,27 +2627,33 @@ export function ConsoleAppV4(props) {
2187
2627
  setObjectError(undefined);
2188
2628
  setConfigActionError(undefined);
2189
2629
  };
2190
- const beginConfigSave = async (apply) => {
2630
+ const configSaveIntent = (apply) => {
2191
2631
  if (!configRoute || configRoute.layer.scope === 'effective' || !currentConfigPage || !dirty)
2192
2632
  return;
2633
+ const fields = currentConfigPage.groups.flatMap((group) => group.fields);
2634
+ return {
2635
+ kind: 'configSave',
2636
+ pluginId: configRoute.pluginId,
2637
+ scope: configRoute.layer.scope,
2638
+ apply,
2639
+ updates: draftUpdates,
2640
+ removeKeys: draftRemoves,
2641
+ sensitiveKeys: fields.filter((field) => field.sensitive).map((field) => field.key),
2642
+ privateKeys: fields.filter((field) => configObjectHasSensitiveChild(field.definition))
2643
+ .map((field) => field.key),
2644
+ changes: configChanges(),
2645
+ };
2646
+ };
2647
+ const beginConfigSave = async (apply) => {
2193
2648
  if (configDraftIssues.length > 0) {
2194
2649
  setConfigActionError('字段已变化。请先放弃失效草稿,或返回继续检查来源。');
2195
2650
  return;
2196
2651
  }
2197
- const fields = currentConfigPage.groups.flatMap((group) => group.fields);
2652
+ const intent = configSaveIntent(apply);
2653
+ if (!intent)
2654
+ return;
2198
2655
  try {
2199
- await beginReview({
2200
- kind: 'configSave',
2201
- pluginId: configRoute.pluginId,
2202
- scope: configRoute.layer.scope,
2203
- apply,
2204
- updates: draftUpdates,
2205
- removeKeys: draftRemoves,
2206
- sensitiveKeys: fields.filter((field) => field.sensitive).map((field) => field.key),
2207
- privateKeys: fields.filter((field) => configObjectHasSensitiveChild(field.definition))
2208
- .map((field) => field.key),
2209
- changes: configChanges(),
2210
- });
2656
+ await beginReview(intent);
2211
2657
  setConfigActionError(undefined);
2212
2658
  }
2213
2659
  catch (error) {
@@ -2263,6 +2709,35 @@ export function ConsoleAppV4(props) {
2263
2709
  return;
2264
2710
  setPendingPresetId(undefined);
2265
2711
  }, [pendingPresetId, route.name, route.name === 'configFields' ? route.layer.scope : '']);
2712
+ useEffect(() => {
2713
+ if (route.name !== 'configSave') {
2714
+ setConfigSaveImpact(undefined);
2715
+ setConfigSaveImpactError(false);
2716
+ return;
2717
+ }
2718
+ const intent = configSaveIntent(false);
2719
+ if (!intent)
2720
+ return;
2721
+ const abort = new AbortController();
2722
+ setConfigSaveImpact(undefined);
2723
+ setConfigSaveImpactError(false);
2724
+ void props.controller.previewConfigSaveImpact(intent, abort.signal)
2725
+ .then((impact) => {
2726
+ if (!abort.signal.aborted)
2727
+ setConfigSaveImpact(impact);
2728
+ })
2729
+ .catch(() => {
2730
+ if (!abort.signal.aborted)
2731
+ setConfigSaveImpactError(true);
2732
+ });
2733
+ return () => abort.abort();
2734
+ }, [
2735
+ props.controller,
2736
+ route.name,
2737
+ route.name === 'configSave' ? `${route.pluginId}:${route.layer.scope}` : '',
2738
+ draftRemoves,
2739
+ draftUpdates,
2740
+ ]);
2266
2741
  if (snapshot.phase === 'error' && !isCatalogSourcesBootstrapFlow(snapshot)
2267
2742
  && !isOperationHistoryRoute(route)) {
2268
2743
  const sourcesConfigInvalid = snapshot.loadError?.code === 'catalogSourcesConfigInvalid';
@@ -2294,7 +2769,7 @@ export function ConsoleAppV4(props) {
2294
2769
  context = '当前项目';
2295
2770
  actions = [
2296
2771
  { label: '添加插件', description: '浏览多个来源中的 Agent 能力', meta: String(home.availablePlugins), run: () => go({ name: 'pluginList', mode: 'browse' }) },
2297
- { label: '我的插件', description: '查看状态、设置和移除已添加插件', meta: String(home.installedPlugins), run: () => go({ name: 'pluginList', mode: 'installed' }) },
2772
+ { label: '我的插件', description: '查看状态、设置和移除已添加插件', meta: home.pluginUpdateCount > 0 ? `${home.installedPlugins} · ${home.pluginUpdateCount} 可更新` : String(home.installedPlugins), status: home.pluginUpdateCount > 0 ? 'warning' : undefined, run: () => go({ name: 'pluginList', mode: 'installed' }) },
2298
2773
  { label: '需要处理', description: '解决安装、来源或设置问题', meta: String(home.issueCount), status: 'warning', run: () => go({ name: 'issueList' }) },
2299
2774
  {
2300
2775
  label: '插件来源',
@@ -2308,7 +2783,7 @@ export function ConsoleAppV4(props) {
2308
2783
  ];
2309
2784
  content = _jsxs(Box, { flexDirection: "column", children: [_jsxs(StatusCard, { status: home.issueCount > 0 || home.unavailableSourceCount > 0 ? 'warning' : 'success', title: "\u5F53\u524D\u72B6\u6001", children: [snapshot.statusReadAtMs !== undefined
2310
2785
  ? _jsx(StaticRow, { title: "\u6700\u8FD1\u6210\u529F\u8BFB\u53D6\u72B6\u6001", meta: new Date(snapshot.statusReadAtMs).toLocaleString('zh-CN', { hour12: false }), compact: true })
2311
- : null, _jsx(ResultRow, { title: `${home.installedPlugins} 个插件可用`, status: "success" }), _jsx(ResultRow, { title: `${home.issueCount} 项需要处理`, status: home.issueCount > 0 ? 'warning' : 'muted' }), home.unavailableSourceCount > 0 ? _jsx(ResultRow, { title: `${home.unavailableSourceCount} 个来源不可用`, status: "error" }) : null, home.catalogCheckingCount > 0 ? _jsx(ResultRow, { title: `${home.catalogCheckingCount} 个插件来源正在检查`, status: "muted" }) : null, home.catalogUpdateCount > 0 ? _jsx(ResultRow, { title: `${home.catalogUpdateCount} 个插件来源有更新`, status: "warning" }) : null, home.catalogRecoveryCount > 0 ? _jsx(ResultRow, { title: `${home.catalogRecoveryCount} 个插件来源需要检查或恢复`, status: "warning" }) : null] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "task", compact: policy.mode === 'compact' })] });
2786
+ : null, _jsx(ResultRow, { title: `${home.installedPlugins} 个插件可用`, status: "success" }), _jsx(ResultRow, { title: `${home.issueCount} 项需要处理`, status: home.issueCount > 0 ? 'warning' : 'muted' }), home.unavailableSourceCount > 0 ? _jsx(ResultRow, { title: `${home.unavailableSourceCount} 个来源不可用`, status: "error" }) : null, home.catalogCheckingCount > 0 ? _jsx(ResultRow, { title: `${home.catalogCheckingCount} 个插件来源正在检查`, status: "muted" }) : null, home.catalogUpdateCount > 0 ? _jsx(ResultRow, { title: `${home.catalogUpdateCount} 个插件来源有更新`, status: "warning" }) : null, home.pluginUpdateCount > 0 ? _jsx(ResultRow, { title: `${home.pluginUpdateCount} 个插件可更新${home.directPluginUpdateCount < home.pluginUpdateCount ? `;${home.directPluginUpdateCount} 个可直接确认` : ''}`, status: "warning" }) : null, home.catalogRecoveryCount > 0 ? _jsx(ResultRow, { title: `${home.catalogRecoveryCount} 个插件来源需要检查或恢复`, status: "warning" }) : null] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "task", compact: policy.mode === 'compact' })] });
2312
2787
  }
2313
2788
  else if (route.name === 'pluginList') {
2314
2789
  const sourceId = route.sourceId ?? 'all';
@@ -2324,67 +2799,103 @@ export function ConsoleAppV4(props) {
2324
2799
  ...(query ? { query } : {}),
2325
2800
  installFilter: 'uninstalled',
2326
2801
  }).length;
2802
+ const updateCount = props.controller.listPlugins({
2803
+ mode: route.mode,
2804
+ ...(sourceId !== 'all' ? { sourceId } : {}),
2805
+ ...(query ? { query } : {}),
2806
+ installFilter: 'updateAvailable',
2807
+ }).length;
2327
2808
  breadcrumb = route.mode === 'browse' ? '添加插件' : '我的插件';
2328
2809
  title = breadcrumb;
2329
2810
  description = route.mode === 'browse'
2330
- ? '选择插件;列表末尾可进入批量添加。'
2331
- : '查看状态;列表末尾可进入批量操作和项目同步。';
2811
+ ? '选择一个插件,或从列表前部选择多个。'
2812
+ : '先处理推荐任务,或查看一个插件。';
2332
2813
  context = `${items.length} 个插件`;
2333
- help = `↑↓ 移动 · Enter 查看 · m 批量${route.mode === 'installed' ? ' · a 项目全同步' : ''} · ←→ 来源 · / 搜索${route.mode === 'browse' ? ' · f 未安装' : ''} · Esc 返回`;
2814
+ help = `↑↓ 移动 · Enter 查看 · m 批量${route.mode === 'installed' ? ' · a 按项目清单准备 · f 可更新' : ''} · ←→ 来源 · / 搜索${route.mode === 'browse' ? ' · f 未安装' : ''} · Esc 返回`;
2334
2815
  const sourceError = sourceId !== 'all'
2335
2816
  && props.controller.getSourceOverview(sourceId).health?.state === 'unavailable';
2336
2817
  actions = sourceError
2337
2818
  ? [{ label: '查看来源问题', description: '当前不能浏览这个来源', run: () => go({ name: 'issueDetail', issueId: `source:${sourceId}:unavailable` }) }]
2338
2819
  : [];
2339
- pluginRows = sourceError ? [] : items.map((item) => ({
2340
- pluginId: item.pluginId,
2341
- name: item.displayName,
2342
- sourceLabel: item.sourceLabel,
2343
- description: item.configOnly
2344
- ? '来源当前没有这个插件;可以查看已保存设置或定点重置设置层。'
2345
- : item.description,
2346
- ...(item.primaryAttention ? { attention: item.primaryAttention.message } : {}),
2347
- placementSummary: pluginPlacementSummary(item),
2348
- statusLabel: statusText(item),
2349
- status: pluginRowStatus(item),
2350
- }));
2351
- if (!sourceError && items.length > 0) {
2352
- pluginRows.push({
2820
+ const listedPluginIds = new Set(items.map((item) => item.pluginId));
2821
+ const recommendedUpdateCandidates = props.controller.listPluginWriteTaskCandidates('update')
2822
+ .filter((candidate) => candidate.eligible && listedPluginIds.has(candidate.target.pluginId));
2823
+ const recommendedUpdatePluginIds = [...new Set(recommendedUpdateCandidates
2824
+ .map((candidate) => candidate.target.pluginId))];
2825
+ const projectRestoreCandidates = props.controller.listPluginWriteTaskCandidates('restore')
2826
+ .filter((candidate) => candidate.target.scope === 'project');
2827
+ const projectIntentRoots = props.controller.listCurrentProjectIntentRoots();
2828
+ const menuRows = sourceError || items.length === 0 || query ? [] : [
2829
+ ...(route.mode === 'installed' && recommendedUpdatePluginIds.length > 0 ? [{
2830
+ pluginId: '@menu:update',
2831
+ name: `更新 ${recommendedUpdatePluginIds.length} 个插件…`,
2832
+ sourceLabel: '',
2833
+ description: '使用已验证版本和现有安装位置,直接查看任务确认',
2834
+ placementSummary: '',
2835
+ statusLabel: '推荐',
2836
+ status: 'warning',
2837
+ menuAction: () => openSelectedPluginTask('update', recommendedUpdatePluginIds),
2838
+ }] : []),
2839
+ ...(route.mode === 'installed' && projectIntentRoots.length > 0 ? [{
2840
+ pluginId: '@menu:restore-project',
2841
+ name: '按项目清单准备…',
2842
+ sourceLabel: '',
2843
+ description: `${projectIntentRoots.length} 个已保存根插件;允许补齐当前项目缺少的安装`,
2844
+ placementSummary: '',
2845
+ statusLabel: '项目',
2846
+ status: 'info',
2847
+ menuAction: prepareCurrentProjectPlugins,
2848
+ }] : []),
2849
+ ...(route.mode === 'installed' && projectRestoreCandidates.length > 0 ? [{
2850
+ pluginId: '@menu:repair-project',
2851
+ name: '修复本项目异常…',
2852
+ sourceLabel: '',
2853
+ description: '严格按已确认的现有版本修复异常,不改变插件版本',
2854
+ placementSummary: '',
2855
+ statusLabel: '推荐',
2856
+ status: 'warning',
2857
+ menuAction: () => openSelectedPluginTask('restore', [...new Set(projectRestoreCandidates
2858
+ .map((candidate) => candidate.target.pluginId))], 'project'),
2859
+ }] : []),
2860
+ {
2353
2861
  pluginId: '@menu:batch',
2354
- name: route.mode === 'browse' ? '选择多个插件添加…' : '批量操作…',
2862
+ name: route.mode === 'browse' ? '选择多个插件添加…' : '选择多个…',
2355
2863
  sourceLabel: '',
2356
2864
  description: route.mode === 'browse'
2357
- ? '选择多个插件,然后统一选择平台和位置'
2358
- : '选择多个插件进行同步、重新安装、卸载或清理',
2865
+ ? '先选择插件,再选择安装位置和平台'
2866
+ : '先选择插件,再选择更新、恢复、移除或维护任务',
2359
2867
  placementSummary: '',
2360
2868
  statusLabel: '菜单',
2361
2869
  status: 'info',
2362
2870
  menuAction: () => openBatchSelection(route.mode === 'browse' ? 'install' : undefined),
2363
- });
2364
- if (route.mode === 'installed') {
2365
- pluginRows.push({
2366
- pluginId: '@menu:sync-project',
2367
- name: '同步当前项目全部插件…',
2368
- sourceLabel: '',
2369
- description: '按已保存的项目设置恢复 Codex 和 Claude Code',
2370
- placementSummary: '',
2371
- statusLabel: '菜单',
2372
- status: 'info',
2373
- menuAction: () => {
2374
- openBatchSelection();
2375
- setBatchDraft({
2376
- action: 'sync',
2377
- selection: 'currentProjectIntentRoots',
2378
- selectedPluginIds: [],
2379
- platform: 'both',
2380
- scope: 'project',
2381
- });
2382
- props.controller.push({ name: 'batchPlacement' });
2871
+ },
2872
+ ];
2873
+ pluginRows = sourceError ? [] : [...menuRows, ...items.map((item) => ({
2874
+ pluginId: item.pluginId,
2875
+ name: item.displayName,
2876
+ sourceLabel: item.sourceLabel,
2877
+ description: item.configOnly
2878
+ ? '来源当前没有这个插件;可以查看已保存设置或定点重置设置层。'
2879
+ : item.description,
2880
+ ...(item.primaryAttention ? { attention: item.primaryAttention.message } : {}),
2881
+ placementSummary: pluginPlacementSummary(item),
2882
+ statusLabel: statusText(item),
2883
+ status: pluginRowStatus(item),
2884
+ }))];
2885
+ if (route.mode === 'installed' && installFilter === 'updateAvailable'
2886
+ && items.length === 0 && !sourceError) {
2887
+ title = '没有可更新插件';
2888
+ description = '当前筛选范围内没有经过版本证据确认的更新。';
2889
+ actions = [{
2890
+ label: '查看全部我的插件',
2891
+ description: '按 f 返回完整列表',
2892
+ run: () => {
2893
+ setInstallFilter('all');
2894
+ props.controller.updatePageContext({ installFilter: 'all' });
2383
2895
  },
2384
- });
2385
- }
2896
+ }];
2386
2897
  }
2387
- if (route.mode === 'installed' && items.length === 0 && !sourceError) {
2898
+ else if (route.mode === 'installed' && items.length === 0 && !sourceError) {
2388
2899
  title = '还没有添加插件';
2389
2900
  description = `从已配置的 ${props.controller.listSources().length} 个来源中浏览可用能力。`;
2390
2901
  actions = [{ label: '浏览插件', description: '开始添加第一个插件', run: () => go({ name: 'pluginList', mode: 'browse' }) }];
@@ -2398,7 +2909,7 @@ export function ConsoleAppV4(props) {
2398
2909
  disabled: '没有可打开的插件',
2399
2910
  }];
2400
2911
  }
2401
- const pluginListStaticRows = route.mode === 'browse' ? 3 : 2;
2912
+ const pluginListStaticRows = 3;
2402
2913
  const viewport = sourceError || pluginRows.length === 0
2403
2914
  ? actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(pluginListStaticRows))
2404
2915
  : rowViewport({
@@ -2416,73 +2927,201 @@ export function ConsoleAppV4(props) {
2416
2927
  ? _jsx(InputDisplay, { value: query, placeholder: "\u8F93\u5165\u641C\u7D22\u5185\u5BB9", columns: Math.max(8, size.columns - 14) })
2417
2928
  : _jsxs(_Fragment, { children: [query || '搜索名称、用途或来源…', query ? ' / 编辑' : ''] })] }), route.mode === 'browse'
2418
2929
  ? _jsxs(Text, { color: tuiTheme.colors.muted, children: ["\u72B6\u6001 ", _jsx(Text, { color: installFilter === 'all' ? tuiTheme.colors.accent : tuiTheme.colors.muted, children: installFilter === 'all' ? '[全部]' : '全部' }), " ", _jsxs(Text, { color: installFilter === 'uninstalled' ? tuiTheme.colors.accent : tuiTheme.colors.muted, children: [installFilter === 'uninstalled' ? '[未安装]' : '未安装', " ", uninstalledCount] }), " f \u5207\u6362"] })
2419
- : null, sourceError
2930
+ : _jsxs(Text, { color: tuiTheme.colors.muted, children: ["\u72B6\u6001 ", _jsx(Text, { color: installFilter === 'all' ? tuiTheme.colors.accent : tuiTheme.colors.muted, children: installFilter === 'all' ? '[全部]' : '全部' }), " ", _jsxs(Text, { color: installFilter === 'updateAvailable' ? tuiTheme.colors.accent : tuiTheme.colors.muted, children: [installFilter === 'updateAvailable' ? '[可更新]' : '可更新', " ", updateCount] }), " f \u5207\u6362"] }), sourceError
2420
2931
  ? _jsx(Callout, { status: "error", title: `${sourceId} 暂时无法提供插件`, description: "\u5F53\u524D\u4E0D\u80FD\u6D4F\u89C8\u8FD9\u4E2A\u6765\u6E90\u3002" })
2421
2932
  : route.mode === 'installed' && items.length === 0
2422
- ? _jsx(Callout, { title: "\u5F00\u59CB\u6DFB\u52A0\u7B2C\u4E00\u4E2A\u63D2\u4EF6", description: "\u63D2\u4EF6\u53EF\u4EE5\u5B89\u88C5\u5230 Codex\u3001Claude Code \u6216\u4E24\u8005\u3002" })
2933
+ ? installFilter === 'updateAvailable'
2934
+ ? _jsx(Callout, { title: "\u6CA1\u6709\u53EF\u66F4\u65B0\u63D2\u4EF6", description: "\u5F53\u524D\u7B5B\u9009\u8303\u56F4\u5185\u6CA1\u6709\u7ECF\u8FC7\u7248\u672C\u8BC1\u636E\u786E\u8BA4\u7684\u66F4\u65B0\u3002" })
2935
+ : _jsx(Callout, { title: "\u5F00\u59CB\u6DFB\u52A0\u7B2C\u4E00\u4E2A\u63D2\u4EF6", description: "\u63D2\u4EF6\u53EF\u4EE5\u5B89\u88C5\u5230 Codex\u3001Claude Code \u6216\u4E24\u8005\u3002" })
2423
2936
  : null, pluginRows.length > 0
2424
2937
  ? _jsx(PluginRows, { rows: pluginRows, focus: focus, viewport: viewport })
2425
2938
  : _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: viewport.offset, limit: viewport.limit })] }));
2426
2939
  }
2427
2940
  else if (route.name === 'batchSelection') {
2428
- breadcrumb = '我的插件 / 批量操作';
2429
- title = batchDraft?.action ? `选择要${batchActionLabel(batchDraft.action)}的插件` : '选择批量操作';
2430
- description = batchDraft?.action
2431
- ? 'Space 或 Enter 切换当前插件,c 继续。每个根插件及其依赖会在确认页逐目标列出。'
2432
- : '一次批量操作只使用一种动作。强制清理只在单插件页面提供。';
2433
- if (!batchDraft?.action) {
2434
- actions = [
2941
+ breadcrumb = `${batchDraft?.mode === 'browse' ? '添加插件' : '我的插件'} / 选择多个`;
2942
+ title = batchDraft?.stage === 'task' ? `为 ${batchDraft.selectedPluginIds.length} 个插件选择任务`
2943
+ : batchDraft?.stage === 'maintenance' ? '其他维护方式'
2944
+ : batchDraft?.mode === 'browse' ? '选择要添加的插件' : '选择插件';
2945
+ description = batchDraft?.stage === 'select'
2946
+ ? 'Space 切换选择,Enter 查看插件。选择会跨来源、搜索和筛选保留。'
2947
+ : batchDraft?.stage === 'task'
2948
+ ? '更新和恢复使用现有精确位置;移除会明确使用已有位置。'
2949
+ : '同步、重新安装和普通清理使用所选插件的已有位置。';
2950
+ if (!batchDraft) {
2951
+ actions = [{ label: '返回插件列表', run: back }];
2952
+ content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true });
2953
+ }
2954
+ else if (batchDraft.stage === 'select') {
2955
+ const candidates = batchCandidates(batchDraft);
2956
+ const selected = new Set(batchDraft.selectedPluginIds);
2957
+ const currentIds = new Set(candidates.map((candidate) => candidate.pluginId));
2958
+ const hiddenSelected = batchDraft.selectedPluginIds.filter((pluginId) => !currentIds.has(pluginId)).length;
2959
+ const candidateActions = candidates.map((candidate) => {
2960
+ const blockReason = batchCandidateBlockReason(batchDraft, candidate);
2961
+ const isSelected = selected.has(candidate.pluginId);
2962
+ return {
2963
+ label: candidate.plugin?.displayName ?? `${candidate.pluginId}(已不可用,可取消选择)`,
2964
+ description: blockReason
2965
+ ? `${candidate.plugin?.sourceLabel ?? '状态已失效'} · ${isSelected ? '可取消选择' : '不可选择'}:${blockReason}`
2966
+ : `${candidate.plugin.sourceLabel} · ${pluginPlacementSummary(candidate.plugin)}`,
2967
+ selected: isSelected,
2968
+ pluginId: candidate.pluginId,
2969
+ canSelectNew: blockReason === undefined,
2970
+ status: blockReason ? 'muted' : undefined,
2971
+ run: candidate.plugin ? () => go(batchDraft.mode === 'browse'
2972
+ ? { name: 'pluginPreview', pluginId: candidate.pluginId }
2973
+ : { name: 'pluginDetail', pluginId: candidate.pluginId }) : undefined,
2974
+ };
2975
+ });
2976
+ const selectableIds = candidates
2977
+ .filter((candidate) => batchCandidateBlockReason(batchDraft, candidate) === undefined)
2978
+ .map((candidate) => candidate.pluginId);
2979
+ const continueSelection = () => {
2980
+ if (batchDraft.selectedPluginIds.length === 0) {
2981
+ setActionError('请至少选择一个插件。');
2982
+ return;
2983
+ }
2984
+ const invalid = invalidBatchSelection(batchDraft);
2985
+ if (invalid.length > 0) {
2986
+ setActionError(`${invalid.length} 个已选插件已不可用。请先在“查看全部已选”中取消选择。`);
2987
+ return;
2988
+ }
2989
+ if (batchDraft.mode === 'browse') {
2990
+ setBatchDraft((current) => current ? { ...current, action: 'install' } : current);
2991
+ props.controller.push({ name: 'batchPlacement' });
2992
+ }
2993
+ else {
2994
+ setBatchDraft((current) => current ? { ...current, stage: 'task' } : current);
2995
+ }
2996
+ setFocus(0);
2997
+ setScrollOffset(0);
2998
+ };
2999
+ const continueAction = {
3000
+ label: `查看确认(${batchDraft.selectedPluginIds.length})`,
3001
+ description: batchDraft.selectedPluginIds.length > 0 ? '继续选择任务和确认范围' : '先选择至少一个插件',
3002
+ run: continueSelection,
3003
+ };
3004
+ const utilityActions = [
2435
3005
  {
2436
- label: '同步当前项目全部插件',
2437
- description: '从当前项目已保存的插件设置建立方案',
3006
+ label: batchDraft.showSelected ? '返回当前结果' : `查看全部已选(${batchDraft.selectedPluginIds.length})`,
3007
+ description: hiddenSelected > 0 ? `${hiddenSelected} 个选择在其他筛选中` : '完整 canonical 选择集合',
2438
3008
  run: () => {
2439
- setBatchDraft({
2440
- action: 'sync',
2441
- selection: 'currentProjectIntentRoots',
2442
- selectedPluginIds: [],
2443
- platform: 'both',
2444
- scope: 'project',
2445
- });
2446
- props.controller.push({ name: 'batchPlacement' });
2447
- },
2448
- },
2449
- ...['install', 'sync', 'reinstall', 'uninstall', 'cleanup'].map((action) => ({
2450
- label: `选择多个插件并${batchActionLabel(action)}`,
2451
- description: action === 'cleanup' ? '执行普通清理;保留不属于 AW 管理的内容' : undefined,
2452
- run: () => {
2453
- setBatchDraft((current) => current ? { ...current, action } : current);
3009
+ setBatchDraft((current) => current ? { ...current, showSelected: !current.showSelected } : current);
2454
3010
  setFocus(0);
2455
3011
  setScrollOffset(0);
2456
3012
  },
2457
- })),
3013
+ },
3014
+ {
3015
+ label: '全选当前结果',
3016
+ description: `${selectableIds.length} 个当前筛选下可选结果,包括滚动范围外`,
3017
+ run: () => setBatchDraft((current) => current ? {
3018
+ ...current,
3019
+ selectedPluginIds: [...new Set([...current.selectedPluginIds, ...selectableIds])],
3020
+ } : current),
3021
+ },
3022
+ {
3023
+ label: '取消当前结果',
3024
+ description: '保留其他来源或搜索中的选择',
3025
+ run: () => setBatchDraft((current) => current ? {
3026
+ ...current,
3027
+ selectedPluginIds: current.selectedPluginIds.filter((pluginId) => !currentIds.has(pluginId)),
3028
+ } : current),
3029
+ },
3030
+ {
3031
+ label: '清空全部已选',
3032
+ run: () => setBatchDraft((current) => current ? { ...current, selectedPluginIds: [] } : current),
3033
+ },
2458
3034
  ];
2459
- const viewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(1));
2460
- context = '选择 1 种动作';
2461
- help = '↑↓ 移动 · Enter 选择 · Esc 放弃';
2462
- content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: viewport.offset, limit: viewport.limit });
3035
+ actions = [continueAction, ...candidateActions, ...utilityActions];
3036
+ const batchFocus = Math.min(focus, actions.length - 1);
3037
+ context = `已选 ${batchDraft.selectedPluginIds.length} 个 · 显示 ${candidates.length} 个${hiddenSelected > 0 ? ` · 隐藏 ${hiddenSelected} 个` : ''}`;
3038
+ help = '↑↓ 移动 · Space 选择 · Enter 查看/操作 · c 查看确认 · ←→ 来源 · / 搜索 · Esc 放弃';
3039
+ const scrollActions = [...candidateActions, ...utilityActions];
3040
+ const viewport = actionViewport(scrollActions, policy.mode === 'compact', Math.max(0, batchFocus - 1), Math.max(0, scrollOffset - 1), Math.max(1, pageActionBudget(5)));
3041
+ const focusedCandidate = batchFocus > 0 && batchFocus <= candidateActions.length
3042
+ ? candidates[batchFocus - 1]?.plugin : undefined;
3043
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(SourceTabs, { controller: props.controller, current: batchDraft.showSelected ? 'all' : batchDraft.sourceId ?? 'all', compact: policy.mode === 'compact', columns: size.columns - 2, mode: batchDraft.mode, query: batchDraft.showSelected ? undefined : batchDraft.query || undefined, installFilter: batchDraft.showSelected ? 'all' : batchDraft.installFilter }), _jsxs(Text, { color: searchFocus ? tuiTheme.colors.accent : tuiTheme.colors.muted, children: ["\u641C\u7D22 / ", searchFocus
3044
+ ? _jsx(InputDisplay, { value: batchDraft.query, placeholder: "\u8F93\u5165\u641C\u7D22\u5185\u5BB9", columns: Math.max(8, size.columns - 14) })
3045
+ : _jsxs(_Fragment, { children: [batchDraft.query || '搜索名称、用途或来源…', batchDraft.query ? ' / 编辑' : ''] })] }), _jsx(SemanticRows, { actions: [continueAction], focus: batchFocus, kind: "action", compact: true }), focusedCandidate
3046
+ ? _jsx(StaticRow, { title: `用途 · ${focusedCandidate.description}`, status: "info", compact: true })
3047
+ : null, _jsx(SemanticRows, { actions: scrollActions, focus: batchFocus - 1, kind: "toggle", compact: policy.mode === 'compact', offset: viewport.offset, limit: viewport.limit })] });
2463
3048
  }
2464
3049
  else {
2465
- const candidates = props.controller.listPlugins({
2466
- mode: batchDraft.action === 'install' ? 'browse' : 'installed',
2467
- }).filter((candidate) => !candidate.configOnly);
2468
- const selected = new Set(batchDraft.selectedPluginIds);
2469
- actions = candidates.map((candidate) => ({
2470
- label: candidate.displayName,
2471
- description: `${candidate.sourceLabel} · ${pluginPlacementSummary(candidate)}`,
2472
- selected: selected.has(candidate.pluginId),
2473
- run: () => setBatchDraft((current) => current ? {
2474
- ...current,
2475
- selectedPluginIds: current.selectedPluginIds.includes(candidate.pluginId)
2476
- ? current.selectedPluginIds.filter((pluginId) => pluginId !== candidate.pluginId)
2477
- : [...current.selectedPluginIds, candidate.pluginId],
2478
- } : current),
2479
- }));
2480
- context = `${batchDraft.selectedPluginIds.length}/${candidates.length} 个插件`;
2481
- help = '↑↓ 移动 · Space/Enter 选择 · a 全选 · c 继续 · Esc 放弃';
3050
+ const selectedIds = batchDraft.selectedPluginIds;
3051
+ const taskCandidates = (goal) => {
3052
+ const selected = new Set(selectedIds);
3053
+ return props.controller.listPluginWriteTaskCandidates(goal)
3054
+ .filter((candidate) => selected.has(candidate.target.pluginId));
3055
+ };
3056
+ const startExistingBatch = (action) => {
3057
+ if (invalidBatchSelection(batchDraft).length > 0) {
3058
+ setActionError('已选插件状态已变化。请返回选择页检查并取消失效项。');
3059
+ return;
3060
+ }
3061
+ const exactTargets = props.controller.listPluginWriteBatchTargets(action, selectedIds);
3062
+ if (exactTargets.length === 0) {
3063
+ setActionError('所选插件没有可用的已有安装位置。');
3064
+ return;
3065
+ }
3066
+ beginReview({
3067
+ kind: 'pluginWriteBatch', action,
3068
+ selection: { kind: 'targets', targets: exactTargets },
3069
+ platform: exactTargets[0].platform,
3070
+ scope: exactTargets[0].scope,
3071
+ });
3072
+ };
3073
+ const syncTargets = props.controller.listPluginWriteBatchTargets('sync', selectedIds);
3074
+ const uninstallTargets = props.controller.listPluginWriteBatchTargets('uninstall', selectedIds);
3075
+ if (batchDraft.stage === 'maintenance') {
3076
+ actions = [
3077
+ { label: '同步已保存使用设置…', description: `重新应用 ${syncTargets.length} 个已保存或现场目标`, run: () => startExistingBatch('sync') },
3078
+ { label: '重新安装…', description: `重新生成 ${props.controller.listPluginWriteBatchTargets('reinstall', selectedIds).length} 个已保存或现场目标`, run: () => startExistingBatch('reinstall') },
3079
+ { label: '清理遗留内容…', description: `${props.controller.listPluginWriteBatchTargets('cleanup', selectedIds).length} 个有受管证据的位置;保留 foreign 和 shared 内容`, run: () => startExistingBatch('cleanup') },
3080
+ {
3081
+ label: '返回任务选择',
3082
+ run: () => {
3083
+ setBatchDraft((current) => current ? { ...current, stage: 'task' } : current);
3084
+ setFocus(0);
3085
+ setScrollOffset(0);
3086
+ },
3087
+ },
3088
+ ];
3089
+ }
3090
+ else {
3091
+ const updates = taskCandidates('update');
3092
+ const restores = taskCandidates('restore');
3093
+ const updateRoots = new Set(updates.map((candidate) => candidate.target.pluginId)).size;
3094
+ const restoreRoots = new Set(restores.map((candidate) => candidate.target.pluginId)).size;
3095
+ actions = [
3096
+ ...(updates.length > 0 ? [{
3097
+ label: `更新所选中的 ${updateRoots} 个可更新插件…`,
3098
+ description: '使用已验证版本和现有精确位置',
3099
+ status: 'warning',
3100
+ run: () => openSelectedPluginTask('update', selectedIds),
3101
+ }] : []),
3102
+ ...(restores.length > 0 ? [{
3103
+ label: `恢复所选中的 ${restoreRoots} 个插件…`,
3104
+ description: '按当前问题和现有精确位置准备方案',
3105
+ status: 'warning',
3106
+ run: () => openSelectedPluginTask('restore', selectedIds),
3107
+ }] : []),
3108
+ { label: '移除所选插件…', description: `${uninstallTargets.length} 个现场目标;执行前检查所有权;保留设置`, status: 'error', run: () => startExistingBatch('uninstall') },
3109
+ {
3110
+ label: '其他维护方式…',
3111
+ description: '同步、重新安装和普通清理',
3112
+ run: () => {
3113
+ setBatchDraft((current) => current ? { ...current, stage: 'maintenance' } : current);
3114
+ setFocus(0);
3115
+ setScrollOffset(0);
3116
+ },
3117
+ },
3118
+ { label: '返回调整选择', run: () => setBatchDraft((current) => current ? { ...current, stage: 'select' } : current) },
3119
+ ];
3120
+ }
3121
+ context = `${selectedIds.length} 个插件 · ${syncTargets.length} 个已保存或现场目标`;
3122
+ help = '↑↓ 移动 · Enter 选择 · Esc 返回修改';
2482
3123
  const viewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(1));
2483
- content = candidates.length > 0
2484
- ? _jsx(SemanticRows, { actions: actions, focus: focus, kind: "toggle", compact: policy.mode === 'compact', offset: viewport.offset, limit: viewport.limit })
2485
- : _jsx(Callout, { status: "warning", title: "\u6CA1\u6709\u53EF\u9009\u62E9\u7684\u63D2\u4EF6", description: "\u8FD4\u56DE\u63D2\u4EF6\u5217\u8868\u5E76\u68C0\u67E5\u6765\u6E90\u6216\u5B89\u88C5\u72B6\u6001\u3002" });
3124
+ content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "task", compact: policy.mode === 'compact', offset: viewport.offset, limit: viewport.limit });
2486
3125
  }
2487
3126
  }
2488
3127
  else if (route.name === 'batchPlacement') {
@@ -2517,6 +3156,10 @@ export function ConsoleAppV4(props) {
2517
3156
  disabled: batchDraft.selection === 'selected' && batchDraft.selectedPluginIds.length === 0
2518
3157
  ? '请至少选择一个插件' : undefined,
2519
3158
  run: () => {
3159
+ if (invalidBatchSelection(batchDraft).length > 0) {
3160
+ setActionError('已选插件状态已变化。请返回选择页检查并取消失效项。');
3161
+ return;
3162
+ }
2520
3163
  const intent = {
2521
3164
  kind: 'pluginWriteBatch',
2522
3165
  action: batchDraft.action,
@@ -2615,7 +3258,25 @@ export function ConsoleAppV4(props) {
2615
3258
  return { label: '查看来源', run: () => go({ name: 'sourceDetail', sourceId: plugin.sourceId }) };
2616
3259
  };
2617
3260
  const previewMenu = dynamicMenu;
2618
- actions = previewMenu.map(previewAction);
3261
+ const advancedPreviewActions = previewMenu.map(previewAction);
3262
+ const primaryPreviewAction = advancedPreviewActions[0] ?? {
3263
+ label: '查看处理建议',
3264
+ run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
3265
+ };
3266
+ const previewSettingsAction = previewMenu.findIndex((action) => action.id === 'settings');
3267
+ actions = [
3268
+ primaryPreviewAction,
3269
+ ...(previewSettingsAction > 0 ? [advancedPreviewActions[previewSettingsAction]] : [{
3270
+ label: '使用说明',
3271
+ description: '查看用途、支持平台和来源',
3272
+ run: () => go({ name: 'pluginAbout', pluginId: plugin.pluginId }),
3273
+ }]),
3274
+ {
3275
+ label: '更多操作…',
3276
+ description: '安装方式、依赖、通用 Skills、来源和诊断',
3277
+ run: () => go({ name: 'pluginMore', pluginId: plugin.pluginId }),
3278
+ },
3279
+ ];
2619
3280
  const previewViewport = actionViewport(actions, true, effectiveFocus, scrollOffset, responsiveBudget(4, 2));
2620
3281
  const dependencyGroups = ['direct', 'transitive'].map((relation) => ({
2621
3282
  relation,
@@ -2806,9 +3467,6 @@ export function ConsoleAppV4(props) {
2806
3467
  : overview?.dependents ?? [];
2807
3468
  const dependencyCount = overview?.dependencies.length ?? 0;
2808
3469
  const dependentCount = visibleDependents.length;
2809
- const dependencyLimit = policy.mode === 'compact'
2810
- ? dependentCount > 0 ? 1 : 2
2811
- : dependentCount > 0 ? 3 : 5;
2812
3470
  const dependentLimit = responsiveBudget(2, 4);
2813
3471
  const dependencyRows = (entries, limit, dependent = false, offset = 0) => {
2814
3472
  const safeOffset = Math.min(offset, Math.max(0, entries.length - 1));
@@ -2843,62 +3501,136 @@ export function ConsoleAppV4(props) {
2843
3501
  }
2844
3502
  else {
2845
3503
  breadcrumb = `${plugin.displayName} / 依赖关系`;
2846
- context = overview ? `${overview.summary.totalCount + 1} 个插件` : '只读';
3504
+ const direction = route.direction ?? 'dependencies';
3505
+ const projection = buildDependencyGraphProjection({
3506
+ overviews: props.controller.listPluginDependencyOverviews(),
3507
+ rootPluginId: plugin.pluginId,
3508
+ direction,
3509
+ ...(expandedDependencyPluginIds ? { expandedPluginIds: expandedDependencyPluginIds } : {}),
3510
+ });
3511
+ const visibleRows = projection.rows;
3512
+ context = direction === 'dependencies' ? '它需要的' : '谁在使用它';
2847
3513
  title = hasInstalledPlacement(plugin) ? '依赖与被使用情况' : '安装所需插件';
2848
- description = '依赖由同一来源快照解析;安装时的依赖位置选择会进入 Review。';
2849
- actions = [
2850
- { label: '技术详情', description: '显示 canonical pluginId 和安全错误代码', run: () => toggleTechnical() },
2851
- ];
2852
- const rootStatus = plugin.aggregateStatus === 'installed' ? 'success'
2853
- : plugin.aggregateStatus === 'needsAttention' ? 'warning'
2854
- : plugin.aggregateStatus === 'unavailable' ? 'error' : 'muted';
2855
- const dependencyActionViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(6));
2856
- const blockerCount = overview?.blockers.length ?? 0;
2857
- const useBudgetedRelations = policy.mode === 'compact'
2858
- || showTechnical
2859
- || dependencyCount + dependentCount + blockerCount > 7;
2860
- const relationCategories = [
2861
- ...(blockerCount > 0 ? [{ key: 'blocker', count: blockerCount }] : []),
2862
- ...(dependencyCount > 0 ? [{ key: 'dependency', count: dependencyCount }] : []),
2863
- ...(dependentCount > 0 ? [{ key: 'dependent', count: dependentCount }] : []),
2864
- ];
2865
- const technicalLineBudget = showTechnical ? 4 : 0;
2866
- const relationItemBudget = Math.max(relationCategories.length, size.rows - 6 - 1 - technicalLineBudget - 4);
2867
- const prioritizedRelations = (entries) => [...entries].sort((left, right) => {
2868
- const priority = (entry) => {
2869
- const status = dependencyStatus(entry).status;
2870
- return status === 'error' ? 0 : status === 'warning' ? 1 : 2;
3514
+ description = direction === 'dependencies'
3515
+ ? '当前方向:它需要的。缩进表示上一层需要它。'
3516
+ : '当前方向:谁在使用它。只显示已知的实际引用。';
3517
+ const rowActions = visibleRows.map((row) => {
3518
+ const state = row.depth === 0
3519
+ ? { value: statusText(plugin), status: plugin.aggregateStatus === 'needsAttention'
3520
+ ? 'warning' : plugin.aggregateStatus === 'unavailable' ? 'error' : 'success' }
3521
+ : dependencyGraphRowStatus(row);
3522
+ const branch = row.depth === 0 ? ''
3523
+ : `${row.ancestorHasNextSibling.map((next) => next ? '│ ' : ' ').join('')}${row.lastChild ? '└─' : '├─'} `;
3524
+ const compactBranch = policy.mode === 'compact' && row.depth > 2 ? '… └─ ' : branch;
3525
+ return {
3526
+ label: row.depth === 0
3527
+ ? `根插件 · ${row.label}(当前插件)`
3528
+ : `${compactBranch}${direction === 'dependencies' ? '必需依赖 · ' : '实际使用者 · '}${row.label}${row.sharedWithRowId ? ' ↗ [same]' : ''}`,
3529
+ description: row.hiddenDescendantCount > 0
3530
+ ? `另有 ${row.hiddenDescendantCount} 个${direction === 'dependencies' ? '所需插件' : '实际使用者'}`
3531
+ : row.depth === 0 ? '当前插件' : undefined,
3532
+ meta: state.value,
3533
+ status: state.status,
3534
+ run: () => {
3535
+ persistDependencyGraphContext(route, projection, expandedDependencyPluginIds ?? new Set(projection.defaultExpandedPluginIds), focus, scrollOffset);
3536
+ go({
3537
+ name: 'pluginDependencyNode',
3538
+ pluginId: plugin.pluginId,
3539
+ nodePluginId: row.pluginId,
3540
+ direction,
3541
+ });
3542
+ },
2871
3543
  };
2872
- return priority(left) - priority(right);
2873
3544
  });
2874
- const allRelationRows = [
2875
- ...(overview?.blockers ?? []).map((blocker) => _jsx(StaticRow, { title: `阻塞 · ${blocker.title}`, description: blocker.explanation, status: "error", compact: true }, `blocker:${blocker.code}`)),
2876
- ...prioritizedRelations(overview?.dependencies ?? []).map((entry) => {
2877
- const state = dependencyStatus(entry);
2878
- return _jsx(StaticRow, { title: `必需依赖 · ${entry.displayName}`, description: dependencyRelationDescription(entry), meta: dependencyPlacementLabel(entry), status: state.status, compact: true }, `dependency:${entry.pluginId}`);
2879
- }),
2880
- ...prioritizedRelations(visibleDependents).map((entry) => {
2881
- const state = dependencyStatus(entry);
2882
- return _jsx(StaticRow, { title: `被依赖 · ${entry.displayName}`, description: dependencyRelationDescription(entry), meta: dependentTargetLabel(entry), status: state.status, compact: true }, `dependent:${entry.pluginId}`);
2883
- }),
3545
+ const unverifiedConsumerActions = direction === 'dependents'
3546
+ ? projection.scopeSummary.unknownConsumerPluginIds.map((pluginId) => {
3547
+ const declared = overview?.dependents.find((entry) => entry.pluginId === pluginId);
3548
+ return {
3549
+ label: `未验证声明者 · ${declared?.displayName ?? pluginId}`,
3550
+ description: '目录声明它依赖当前插件,但当前没有实际引用证据',
3551
+ meta: declared?.aggregateStatus === 'notInstalled' ? '未安装' : '未验证',
3552
+ status: 'muted',
3553
+ run: () => {
3554
+ persistDependencyGraphContext(route, projection, expandedDependencyPluginIds ?? new Set(projection.defaultExpandedPluginIds), focus, scrollOffset);
3555
+ go({
3556
+ name: 'pluginDependencyNode',
3557
+ pluginId: plugin.pluginId,
3558
+ nodePluginId: pluginId,
3559
+ direction,
3560
+ });
3561
+ },
3562
+ };
3563
+ })
3564
+ : [];
3565
+ actions = [
3566
+ ...rowActions,
3567
+ ...unverifiedConsumerActions,
3568
+ {
3569
+ label: direction === 'dependencies' ? '切换到“谁在使用它”' : '切换到“它需要的”',
3570
+ description: '切换关系方向',
3571
+ run: () => {
3572
+ persistDependencyGraphContext(route, projection, expandedDependencyPluginIds ?? new Set(projection.defaultExpandedPluginIds), focus, scrollOffset);
3573
+ props.controller.replace({
3574
+ name: 'pluginDependencies',
3575
+ pluginId: plugin.pluginId,
3576
+ direction: direction === 'dependencies' ? 'dependents' : 'dependencies',
3577
+ });
3578
+ },
3579
+ },
3580
+ { label: showTechnical ? '隐藏检查详情' : '查看检查详情', run: () => toggleTechnical() },
2884
3581
  ];
2885
- const relationLimit = Math.max(1, Math.min(relationItemBudget, showTechnical ? responsiveBudget(1, 6) : responsiveBudget(3, 7)));
2886
- const relationOffset = Math.min(batchTargetOffset, Math.max(0, allRelationRows.length - 1));
2887
- const visibleRelationRows = allRelationRows.slice(relationOffset, relationOffset + relationLimit);
2888
- const hiddenRelationRows = Math.max(0, allRelationRows.length - relationOffset - visibleRelationRows.length);
2889
- if (useBudgetedRelations && allRelationRows.length > relationLimit) {
2890
- help = 'PgUp/PgDn 查看全部依赖关系 · ↑↓ 移动 · Enter 选择 · Esc 返回';
2891
- }
2892
- content = _jsxs(Box, { flexDirection: "column", children: [useBudgetedRelations ? _jsxs(SectionCard, { title: "\u4F9D\u8D56\u5173\u7CFB", compact: true, children: [_jsx(StaticRow, { title: `根插件 · ${plugin.displayName}`, meta: statusText(plugin), status: rootStatus, compact: true }), relationOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${relationOffset} 项`, status: "muted", compact: true }) : null, visibleRelationRows, hiddenRelationRows > 0 ? _jsx(StaticRow, { title: `↓ 后面还有 ${hiddenRelationRows} 项`, status: "muted", compact: true }) : null, dependencyCount + dependentCount + blockerCount === 0 ? _jsx(StaticRow, { title: "\u6CA1\u6709\u53EF\u663E\u793A\u7684\u4F9D\u8D56\u5173\u7CFB", status: "muted", compact: true }) : null] }) : _jsxs(_Fragment, { children: [_jsx(SectionCard, { title: "\u4F60\u9009\u62E9\u7684\u63D2\u4EF6", meta: "\u6839\u8282\u70B9", children: _jsx(StaticRow, { title: plugin.displayName, meta: statusText(plugin), status: rootStatus }) }), dependencyCount > 0 ? _jsx(SectionCard, { title: "\u5FC5\u9700\u4F9D\u8D56", meta: "\u4F9D\u8D56\u4F18\u5148", children: dependencyRows(overview?.dependencies ?? [], dependencyLimit) }) : null, dependentCount > 0 ? _jsx(SectionCard, { title: "\u4F7F\u7528\u5B83\u7684\u63D2\u4EF6", meta: "\u53CD\u5411\u5F15\u7528", children: dependencyRows(visibleDependents, dependentLimit, true) }) : null, overview?.blockers.map((blocker) => _jsx(Callout, { status: "error", title: blocker.title, description: blocker.explanation }, blocker.code)), dependencyCount + dependentCount + blockerCount === 0 ? _jsx(Callout, { status: "muted", title: "\u6CA1\u6709\u53EF\u663E\u793A\u7684\u4F9D\u8D56\u5173\u7CFB" }) : null] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: useBudgetedRelations, offset: dependencyActionViewport.offset, limit: dependencyActionViewport.limit }), showTechnical ? _jsx(TechnicalDetails, { children: _jsx(StaticRow, { title: `${dependencyCount} 依赖 · ${dependentCount} 被依赖 · 已折叠`, meta: plugin.pluginId, compact: true }) }) : null] });
3582
+ const scopeText = projection.scopeSummary.references.length > 0
3583
+ ? projection.scopeSummary.references.map((entry) => `${platformLabel(entry.platform)} · ${scopeLabel(entry.scope)}`).join(';')
3584
+ : '当前加载范围;其他项目引用未检查';
3585
+ const relationViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, showTechnical ? responsiveBudget(3, 6) : responsiveBudget(7, 11));
3586
+ help = '↑↓ 选择 · → 展开 · ← 收起 · Enter 节点详情 · Esc 返回';
3587
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(StaticRow, { title: `当前检查范围 · ${scopeText}`, status: "muted", compact: true }), projection.status.state === 'invalid'
3588
+ ? _jsx(Callout, { status: "error", title: "\u4F9D\u8D56\u5173\u7CFB\u65E0\u6548", description: projection.status.message }) : null, unverifiedConsumerActions.length > 0
3589
+ ? _jsx(StaticRow, { title: `未验证声明者 · ${unverifiedConsumerActions.length} 个`, description: "\u4EE5\u4E0B\u58F0\u660E\u8005\u4E0D\u8BA1\u5165\u5B9E\u9645\u4F7F\u7528\u8005", status: "muted", compact: true })
3590
+ : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: relationViewport.offset, limit: relationViewport.limit }), showTechnical ? _jsxs(TechnicalDetails, { children: [_jsx(StaticRow, { title: `${dependencyCount} 个依赖 · ${projection.scopeSummary.actualConsumerPluginIds.length} 个实际使用者`, meta: plugin.pluginId, compact: true }), _jsx(StaticRow, { title: `未验证声明者 · ${projection.scopeSummary.unknownConsumerPluginIds.length} 个`, compact: true })] }) : null] });
2893
3591
  }
2894
3592
  }
3593
+ else if (route.name === 'pluginDependencyNode' && plugin) {
3594
+ const node = props.controller.getPluginOverview(route.nodePluginId);
3595
+ const overview = dependencyOverview(plugin);
3596
+ const item = route.nodePluginId === plugin.pluginId ? undefined
3597
+ : route.direction === 'dependencies'
3598
+ ? overview?.dependencies.find((entry) => entry.pluginId === route.nodePluginId)
3599
+ : overview?.dependents.find((entry) => entry.pluginId === route.nodePluginId);
3600
+ const verifiedDependent = route.direction !== 'dependents'
3601
+ || (item?.placementReferences?.length ?? 0) > 0;
3602
+ const state = item ? dependencyStatus(item) : undefined;
3603
+ breadcrumb = `${plugin.displayName} / 依赖关系 / ${node?.displayName ?? route.nodePluginId}`;
3604
+ context = route.direction === 'dependencies' ? '它需要的' : '谁在使用它';
3605
+ title = node?.displayName ?? route.nodePluginId;
3606
+ description = route.nodePluginId === plugin.pluginId
3607
+ ? '这是当前关系图的根插件。'
3608
+ : route.direction === 'dependencies'
3609
+ ? `${item?.requiredBy.map((entry) => entry.displayName).join('、') || plugin.displayName} 需要这个插件。`
3610
+ : verifiedDependent
3611
+ ? `${node?.displayName ?? route.nodePluginId} 的已安装位置实际引用当前插件。`
3612
+ : `${item?.displayName ?? route.nodePluginId} 声明依赖当前插件,但当前没有实际引用证据。`;
3613
+ actions = [
3614
+ ...(route.nodePluginId !== plugin.pluginId ? [{
3615
+ label: '以此节点查看关系',
3616
+ description: '打开局部子树;面包屑保留来路',
3617
+ run: () => go({
3618
+ name: 'pluginDependencies',
3619
+ pluginId: route.nodePluginId,
3620
+ direction: route.direction,
3621
+ }),
3622
+ }] : []),
3623
+ { label: '返回关系图', run: back },
3624
+ ];
3625
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsxs(SectionCard, { title: "\u8282\u70B9\u72B6\u6001", children: [_jsx(StaticRow, { title: node?.displayName ?? route.nodePluginId, meta: state?.value ?? (node ? statusText(node) : '未检查'), status: state?.status ?? 'muted', compact: true }), _jsx(StaticRow, { title: route.nodePluginId, description: "canonical pluginId", status: "muted", compact: true }), item?.placementReferences?.slice(0, responsiveBudget(1, 3)).map((reference) => _jsx(StaticRow, { title: `${platformLabel(reference.consumer.platform)} · ${scopeLabel(reference.consumer.scope)}`, description: `${reference.consumer.pluginId} → ${reference.dependency.pluginId}`, compact: true }, `${reference.consumer.pluginId}:${reference.consumer.platform}:${reference.consumer.scope}`))] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
3626
+ }
2895
3627
  else if (route.name === 'installPlacement' && plugin) {
2896
3628
  breadcrumb = `${plugin.displayName} / 添加`;
2897
3629
  context = '第 1 步,共 2 步';
2898
3630
  title = `你希望在哪里使用 ${plugin.displayName}?`;
2899
3631
  const hasDependencies = (plugin.dependencySummary?.totalCount ?? 0) > 0;
2900
3632
  description = hasDependencies
2901
- ? '这里只选择根插件位置。依赖可以在下一步单独调整;依赖不能比根插件位置更窄。'
3633
+ ? '这里只选择根插件位置。依赖将采用 planner 推荐位置,并在 Review 中显示。'
2902
3634
  : '选择位置后,再选择一个或多个目标平台。';
2903
3635
  const availableScopes = ['user', 'project'].filter((candidateScope) => props.controller.listInstallTargets(plugin.pluginId, candidateScope)
2904
3636
  .some((entry) => entry.compatible));
@@ -2907,8 +3639,8 @@ export function ConsoleAppV4(props) {
2907
3639
  label: scopeLabel(candidateScope),
2908
3640
  selected: candidateScope === scope,
2909
3641
  description: candidateScope === 'user'
2910
- ? hasDependencies ? '根插件在任意项目中可用;依赖位置下一步确认' : '在任意项目中可使用,适合个人常用能力'
2911
- : hasDependencies ? '根插件只在当前项目使用;依赖可单独调整为所有项目' : '仅在当前项目中使用,适合团队共享',
3642
+ ? hasDependencies ? '根插件在任意项目中可用;依赖位置自动计算' : '在任意项目中可使用,适合个人常用能力'
3643
+ : hasDependencies ? '根插件只在当前项目使用;Review 会说明更宽的依赖位置' : '仅在当前项目中使用,适合团队共享',
2912
3644
  meta: candidateScope === 'user' ? '推荐' : undefined,
2913
3645
  run: () => {
2914
3646
  const choices = props.controller.listInstallTargets(plugin.pluginId, candidateScope);
@@ -2931,7 +3663,7 @@ export function ConsoleAppV4(props) {
2931
3663
  },
2932
3664
  })),
2933
3665
  ];
2934
- content = _jsxs(Box, { flexDirection: "column", children: [_jsx(SemanticRows, { actions: actions, focus: focus, kind: "choice", compact: policy.mode === 'compact' }), hasDependencies ? _jsx(SectionCard, { title: "\u4F9D\u8D56\u4F4D\u7F6E", children: _jsx(StaticRow, { title: `${plugin.dependencySummary?.totalCount ?? 0} 个依赖将在下一步单独确认`, status: "info" }) }) : null] });
3666
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(SemanticRows, { actions: actions, focus: focus, kind: "choice", compact: policy.mode === 'compact' }), hasDependencies ? _jsx(SectionCard, { title: "\u4F9D\u8D56\u4F4D\u7F6E", children: _jsx(StaticRow, { title: `${plugin.dependencySummary?.totalCount ?? 0} 个依赖将采用推荐位置`, description: "Review \u4E2D\u53EF\u67E5\u770B\u6216\u8C03\u6574", status: "info" }) }) : null] });
2935
3667
  }
2936
3668
  else if (route.name === 'installTargets' && plugin) {
2937
3669
  breadcrumb = `${plugin.displayName} / 添加 / 目标平台`;
@@ -2939,7 +3671,7 @@ export function ConsoleAppV4(props) {
2939
3671
  title = '选择目标平台';
2940
3672
  const hasDependencies = (plugin.dependencySummary?.totalCount ?? 0) > 0;
2941
3673
  description = hasDependencies
2942
- ? '根插件必须支持所选位置;每个依赖必须至少有一个合法位置。'
3674
+ ? '根插件必须支持所选位置;确认后会自动规划依赖并进入 Review。'
2943
3675
  : '目标平台按所选位置验证;仅含平台共享内容的插件可同时启用两个位置。';
2944
3676
  const choices = props.controller.listInstallTargets(plugin.pluginId, scope);
2945
3677
  const validSelectedTargets = selectedTargets.filter((platform) => choices.some((choice) => choice.platform === platform && choice.compatible));
@@ -3075,93 +3807,428 @@ export function ConsoleAppV4(props) {
3075
3807
  : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "choice", compact: policy.mode === 'compact' })] });
3076
3808
  help = '↑↓ 移动 · Enter 选择 · Esc 返回';
3077
3809
  }
3810
+ else if (route.name === 'pluginWriteTask' && plugin) {
3811
+ const retainedTargetKeys = route.targetKeys ? new Set(route.targetKeys) : undefined;
3812
+ const allTaskCandidates = props.controller.listPluginWriteTaskCandidates(route.goal);
3813
+ const currentTargetKeys = new Set(allTaskCandidates.map(pluginWriteTaskTargetKey));
3814
+ const missingTargetKeys = route.targetKeys?.filter((targetKey) => !currentTargetKeys.has(targetKey)) ?? [];
3815
+ const candidates = allTaskCandidates
3816
+ .filter((candidate) => retainedTargetKeys
3817
+ ? retainedTargetKeys.has(pluginWriteTaskTargetKey(candidate))
3818
+ : candidate.target.pluginId === route.pluginId);
3819
+ const executable = candidates.filter((candidate) => candidate.eligible);
3820
+ const blockedCandidates = candidates.filter((candidate) => !candidate.eligible);
3821
+ const taskPluginIds = [...new Set(candidates.map((candidate) => candidate.target.pluginId))];
3822
+ const currentVersionEvidence = Object.assign({}, ...taskPluginIds.map((pluginId) => {
3823
+ const candidatePlugin = props.controller.getPluginOverview(pluginId);
3824
+ return candidatePlugin ? pluginWriteTaskVersionEvidence(candidatePlugin, candidates.filter((candidate) => candidate.target.pluginId === pluginId)) : {};
3825
+ }));
3826
+ const versionEvidenceChanged = route.goal === 'update' && route.versionEvidence !== undefined
3827
+ && (Object.entries(currentVersionEvidence).some(([key, version]) => route.versionEvidence?.[key] !== version)
3828
+ || Object.keys(route.versionEvidence).some((key) => currentVersionEvidence[key] === undefined));
3829
+ const activationSources = [...new Set(blockedCandidates
3830
+ .filter((candidate) => candidate.blockReason === 'catalogActivationRequired')
3831
+ .map((candidate) => candidate.sourceId))];
3832
+ const taskIssues = new Map(props.controller.listUserIssues().map((issue) => [issue.issueId, issue]));
3833
+ const recoveryActions = [...new Map(blockedCandidates.flatMap((candidate) => {
3834
+ if (candidate.blockReason === 'catalogActivationRequired')
3835
+ return [];
3836
+ const target = candidate.target;
3837
+ const targetLabel = `${candidate.displayName} · ${platformLabel(target.platform)} · ${scopeLabel(target.scope)}`;
3838
+ const linkedIssue = candidate.issueIds.map((issueId) => taskIssues.get(issueId)).find(Boolean);
3839
+ const sourceIssue = linkedIssue && (linkedIssue.group === 'sourceRemoved'
3840
+ || linkedIssue.group === 'sourceUnavailable')
3841
+ ? linkedIssue
3842
+ : [...taskIssues.values()].find((entry) => (entry.category === 'sourceUnavailable' || entry.category === 'sourceMissing'
3843
+ || entry.category === 'sourceIdentityChanged')
3844
+ && (entry.sourceId === candidate.sourceId || entry.pluginId === target.pluginId));
3845
+ const issue = linkedIssue ?? sourceIssue;
3846
+ const candidatePlugin = props.controller.getPluginOverview(target.pluginId);
3847
+ const configField = issue?.requiredFields?.[0];
3848
+ if (issue?.group === 'config' || configField) {
3849
+ const fieldKey = configField?.key;
3850
+ return [[`config:${target.pluginId}:${fieldKey ?? ''}`, {
3851
+ label: fieldKey ? `完成设置:${configField?.title ?? fieldKey}` : `检查 ${candidate.displayName} 的设置`,
3852
+ description: `${targetLabel};保存后返回任务重新读取当前证据`,
3853
+ status: 'warning',
3854
+ run: () => {
3855
+ beginConfigFlow(target.pluginId);
3856
+ props.controller.replace({
3857
+ name: 'configLayer',
3858
+ pluginId: target.pluginId,
3859
+ ...(fieldKey ? { fieldKey } : {}),
3860
+ });
3861
+ },
3862
+ }]];
3863
+ }
3864
+ if (candidate.blockReason === 'restoreVersionChanged') {
3865
+ const updateCandidates = props.controller.listPluginWriteTaskCandidates('update')
3866
+ .filter((entry) => pluginWriteTaskTargetKey(entry) === pluginWriteTaskTargetKey(candidate));
3867
+ const updatePlugin = props.controller.getPluginOverview(target.pluginId);
3868
+ if (updateCandidates.length > 0 && updatePlugin) {
3869
+ return [[`update:${pluginWriteTaskTargetKey(candidate)}`, {
3870
+ label: `更新并恢复 ${targetLabel}…`,
3871
+ description: '改用当前候选版本建立新任务;进入 Review 前仍会重新检查证据',
3872
+ status: 'warning',
3873
+ run: () => props.controller.beginNestedPluginWriteTask({
3874
+ name: 'pluginWriteTask',
3875
+ pluginId: target.pluginId,
3876
+ goal: 'update',
3877
+ targetKeys: updateCandidates.map(pluginWriteTaskTargetKey),
3878
+ ...(route.batchOrigin ? { batchOrigin: route.batchOrigin } : {}),
3879
+ versionEvidence: pluginWriteTaskVersionEvidence(updatePlugin, updateCandidates),
3880
+ }),
3881
+ }]];
3882
+ }
3883
+ }
3884
+ if (sourceIssue || candidatePlugin?.sourceUnavailable) {
3885
+ const sourceId = sourceIssue?.sourceId ?? candidate.sourceId;
3886
+ const sourceNeedsIssue = sourceIssue?.category === 'sourceMissing'
3887
+ || sourceIssue?.category === 'sourceIdentityChanged'
3888
+ || sourceIssue?.group === 'sourceRemoved';
3889
+ if (sourceNeedsIssue && sourceIssue) {
3890
+ return [[`source-issue:${sourceIssue.issueId}`, {
3891
+ label: sourceIssue.category === 'sourceMissing'
3892
+ ? `重新添加来源 ${sourceId}` : `核对来源身份 ${sourceId}`,
3893
+ description: `${targetLabel};先查看来源问题的安全恢复选择`,
3894
+ status: 'warning',
3895
+ run: () => go({ name: 'issueDetail', issueId: sourceIssue.issueId }),
3896
+ }]];
3897
+ }
3898
+ return [[`source:${sourceId}`, {
3899
+ label: `核对来源 ${sourceId}`,
3900
+ description: `${targetLabel};修复或重新检查后返回任务`,
3901
+ status: 'warning',
3902
+ run: () => go({ name: 'sourceDetail', sourceId }),
3903
+ }]];
3904
+ }
3905
+ if (candidate.blockReason === 'restoreVersionUnverified'
3906
+ || candidate.blockReason === 'updateEvidenceIncomplete') {
3907
+ return [
3908
+ [`reload:${pluginWriteTaskTargetKey(candidate)}`, {
3909
+ label: `重新检查 ${targetLabel}`,
3910
+ description: '重新读取安装、版本、来源和受管内容证据',
3911
+ run: async () => {
3912
+ await props.controller.load();
3913
+ setFocus(0);
3914
+ setScrollOffset(0);
3915
+ },
3916
+ }],
3917
+ [`diagnostic:${pluginWriteTaskTargetKey(candidate)}`, {
3918
+ label: `查看 ${targetLabel} 的诊断`,
3919
+ description: '版本仍无法确认时查看组件状态和安全恢复方向',
3920
+ run: () => go({ name: 'pluginTechnical', pluginId: target.pluginId }),
3921
+ }],
3922
+ ];
3923
+ }
3924
+ if (issue) {
3925
+ return [[`issue:${issue.issueId}`, {
3926
+ label: `处理问题:${issue.title}`,
3927
+ description: `${targetLabel};只提供该问题允许的恢复动作`,
3928
+ status: 'warning',
3929
+ run: () => go({ name: 'issueDetail', issueId: issue.issueId }),
3930
+ }]];
3931
+ }
3932
+ if (candidate.blockReason === 'targetUnavailable'
3933
+ || candidate.blockReason === 'manualRecoveryRequired'
3934
+ || candidate.blockReason === 'taskActionConflict') {
3935
+ return [[`maintenance:${pluginWriteTaskTargetKey(candidate)}`, {
3936
+ label: `维护 ${targetLabel}`,
3937
+ description: '检查这个精确平台位置可用的安全维护动作',
3938
+ status: 'warning',
3939
+ run: () => go({ name: 'platform', pluginId: target.pluginId, platform: target.platform }),
3940
+ }]];
3941
+ }
3942
+ return [[`diagnostic:${target.pluginId}`, {
3943
+ label: `查看 ${candidate.displayName} 的诊断`,
3944
+ description: `${pluginWriteTaskBlockLabel(candidate.blockReason)};当前没有可安全自动执行的动作`,
3945
+ run: () => go({ name: 'pluginTechnical', pluginId: target.pluginId }),
3946
+ }]];
3947
+ })).values()];
3948
+ const taskLabel = pluginWriteTaskLabel(route.goal);
3949
+ const startTaskReview = (executionPolicy) => {
3950
+ if (missingTargetKeys.length > 0) {
3951
+ setActionError(`${missingTargetKeys.length} 个冻结位置已失效或无需处理。请返回调整后重新开始。`);
3952
+ return;
3953
+ }
3954
+ props.controller.startPluginWriteTaskReview({
3955
+ kind: 'pluginWriteTask',
3956
+ goal: route.goal,
3957
+ targets: candidates.map((candidate) => candidate.target),
3958
+ executionPolicy,
3959
+ }, props.controller.pluginWriteTaskReturnRoute(route));
3960
+ setFocus(0);
3961
+ setScrollOffset(0);
3962
+ };
3963
+ breadcrumb = taskPluginIds.length > 1
3964
+ ? `我的插件 / ${taskLabel} ${taskPluginIds.length} 个插件`
3965
+ : `我的插件 / ${plugin.displayName} / ${taskLabel}`;
3966
+ context = '只读准备';
3967
+ title = taskPluginIds.length > 1
3968
+ ? `${taskLabel} ${taskPluginIds.length} 个插件`
3969
+ : `${taskLabel} ${plugin.displayName}`;
3970
+ description = route.goal === 'update'
3971
+ ? '只使用已验证的插件版本和精确安装位置。来源目录不会在准备时自动启用。'
3972
+ : '根据当前问题选择最小恢复方法,不自动改版本、位置或来源。';
3973
+ help = candidates.length > (policy.mode === 'compact' ? 3 : 6)
3974
+ ? 'PgUp/PgDn 查看全部位置 · ↑↓ 移动 · Enter 选择 · Esc 返回'
3975
+ : '↑↓ 移动 · Enter 选择 · Esc 返回';
3976
+ actions = [
3977
+ ...(missingTargetKeys.length === 0 && !versionEvidenceChanged
3978
+ && blockedCandidates.length === 0 && executable.length > 0 ? [{
3979
+ label: `确认${taskLabel} ${executable.length} 个位置…`,
3980
+ description: '进入统一影响确认;执行前再次检查当前证据',
3981
+ status: 'success',
3982
+ run: () => startTaskReview('all'),
3983
+ }] : []),
3984
+ ...(missingTargetKeys.length === 0 && !versionEvidenceChanged
3985
+ && blockedCandidates.length > 0 && executable.length > 0 ? [{
3986
+ label: `先处理可执行的 ${executable.length} 个位置…`,
3987
+ description: `${blockedCandidates.length} 个受阻位置保留,不会无声跳过`,
3988
+ status: 'warning',
3989
+ run: () => startTaskReview('executableSubset'),
3990
+ }] : []),
3991
+ ...activationSources.map((sourceId) => ({
3992
+ label: `先更新来源目录 ${sourceId}…`,
3993
+ description: '先单独确认目录变更;启用完成后重新准备插件更新',
3994
+ status: 'warning',
3995
+ run: () => beginReview({ kind: 'catalogGeneration', action: 'update', sourceId }),
3996
+ })),
3997
+ ...recoveryActions,
3998
+ ...(versionEvidenceChanged ? [{
3999
+ label: '使用当前版本重新准备…',
4000
+ description: '来源启用后的插件版本与先前候选不同;先明确采用当前版本',
4001
+ status: 'warning',
4002
+ run: () => {
4003
+ props.controller.replace({ ...route, versionEvidence: currentVersionEvidence });
4004
+ setFocus(0);
4005
+ setScrollOffset(0);
4006
+ },
4007
+ }] : []),
4008
+ { label: '返回插件详情', run: back },
4009
+ ];
4010
+ const taskTargetLimit = policy.mode === 'compact' ? 3 : 6;
4011
+ const taskTargetOffset = Math.min(batchTargetOffset, Math.max(0, candidates.length - 1));
4012
+ const visibleTaskCandidates = candidates.slice(taskTargetOffset, taskTargetOffset + taskTargetLimit);
4013
+ const targetRows = visibleTaskCandidates.map((candidate) => {
4014
+ const target = candidate.target;
4015
+ const version = target.expectedUpdate
4016
+ ? `${target.expectedUpdate.installedVersion} → ${target.expectedUpdate.activeVersion}`
4017
+ : '保持当前版本';
4018
+ return _jsx(StaticRow, { title: `${taskPluginIds.length > 1 ? `${candidate.displayName} · ${candidate.sourceId} · ` : ''}${platformLabel(target.platform)} · ${scopeLabel(target.scope)}${!candidate.eligible && policy.mode === 'compact'
4019
+ ? ` · ${pluginWriteTaskBlockLabel(candidate.blockReason)}` : ''}`, description: candidate.eligible
4020
+ ? pluginWriteTaskMethodLabel(candidate.recommendedMethod)
4021
+ : pluginWriteTaskBlockLabel(candidate.blockReason), meta: candidate.eligible ? version : '需要决定', status: candidate.eligible ? 'success' : 'warning', compact: policy.mode === 'compact' }, `${target.pluginId}:${target.platform}:${target.scope}`);
4022
+ });
4023
+ const taskViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(Math.min(candidates.length, policy.mode === 'compact' ? 3 : 6) + 2));
4024
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsxs(SectionCard, { title: "\u5904\u7406\u8303\u56F4", meta: `${executable.length} 个可执行 · ${blockedCandidates.length} 个需决定`, compact: policy.mode === 'compact', children: [taskTargetOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${taskTargetOffset} 个位置`, status: "muted", compact: true }) : null, targetRows.length > 0 ? targetRows
4025
+ : _jsx(StaticRow, { title: "\u5F53\u524D\u6CA1\u6709\u53EF\u6267\u884C\u76EE\u6807", status: "muted", compact: true }), candidates.length > taskTargetOffset + targetRows.length
4026
+ ? _jsx(StaticRow, { title: `↓ 后面还有 ${candidates.length - taskTargetOffset - targetRows.length} 个位置`, status: "muted", compact: true }) : null] }), blockedCandidates.length > 0 ? _jsx(Callout, { status: "warning", title: `${blockedCandidates.length} 个位置需要先处理或决定`, description: executable.length > 0
4027
+ ? '可以先处理可执行项,或进入下方对应入口处理受阻位置。'
4028
+ : '请进入下方对应入口处理;返回后会重新读取当前证据。', compact: policy.mode === 'compact' }) : null, missingTargetKeys.length > 0 ? _jsx(Callout, { status: "warning", title: `${missingTargetKeys.length} 个冻结位置已失效或无需处理`, description: "\u4E0D\u4F1A\u628A\u8FD9\u4E9B\u4F4D\u7F6E\u5F53\u4F5C\u5DF2\u5B8C\u6210\uFF0C\u4E5F\u4E0D\u4F1A\u81EA\u52A8\u7F29\u5C0F\u672C\u6B21\u8303\u56F4\u3002\u8BF7\u8FD4\u56DE\u8C03\u6574\u9009\u62E9\u540E\u91CD\u65B0\u5F00\u59CB\u3002", compact: policy.mode === 'compact' }) : null, versionEvidenceChanged ? _jsx(Callout, { status: "warning", title: "\u63D2\u4EF6\u76EE\u6807\u7248\u672C\u5DF2\u53D8\u5316", description: "\u4E0D\u4F1A\u6CBF\u7528\u6765\u6E90\u542F\u7528\u524D\u7684\u7248\u672C\u8BC1\u636E\uFF0C\u4E5F\u4E0D\u4F1A\u81EA\u52A8\u6267\u884C\u65B0\u7248\u672C\u3002", compact: policy.mode === 'compact' }) : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: taskViewport.offset, limit: taskViewport.limit })] });
4029
+ }
3078
4030
  else if (route.name === 'pluginDetail' && plugin) {
3079
4031
  breadcrumb = `我的插件 / ${plugin.displayName}`;
3080
4032
  context = `${plugin.sourceLabel}${plugin.version ? ` · ${plugin.version}` : ''}`;
3081
4033
  title = plugin.displayName;
3082
4034
  description = plugin.description;
4035
+ const updateDescription = pluginUpdateDescription(plugin);
4036
+ const updateTaskCandidates = plugin.configOnly ? []
4037
+ : props.controller.listPluginWriteTaskCandidates('update')
4038
+ .filter((candidate) => candidate.target.pluginId === plugin.pluginId);
4039
+ const restoreTaskCandidates = plugin.configOnly ? []
4040
+ : props.controller.listPluginWriteTaskCandidates('restore')
4041
+ .filter((candidate) => candidate.target.pluginId === plugin.pluginId);
4042
+ const updateAction = updateTaskCandidates.length > 0 ? {
4043
+ label: updateTaskCandidates.length === 1 && updateTaskCandidates[0]?.target.expectedUpdate
4044
+ ? `更新到 ${updateTaskCandidates[0].target.expectedUpdate.activeVersion}…`
4045
+ : updateTaskCandidates.some((candidate) => candidate.target.expectedUpdate)
4046
+ ? `更新 ${plugin.displayName}…` : '准备插件更新…',
4047
+ description: updateTaskCandidates.some((candidate) => candidate.blockReason === 'catalogActivationRequired')
4048
+ ? '需要先单独确认来源目录变更' : '使用精确版本和现有安装位置',
4049
+ status: 'warning',
4050
+ run: () => go({
4051
+ name: 'pluginWriteTask', pluginId: plugin.pluginId, goal: 'update',
4052
+ targetKeys: updateTaskCandidates.map(pluginWriteTaskTargetKey),
4053
+ versionEvidence: pluginWriteTaskVersionEvidence(plugin, updateTaskCandidates),
4054
+ }),
4055
+ } : undefined;
4056
+ const executableRestoreCandidates = restoreTaskCandidates.filter((candidate) => candidate.eligible);
4057
+ const restoreAction = executableRestoreCandidates.length > 0 ? {
4058
+ label: '恢复正常…',
4059
+ description: `${restoreTaskCandidates.length} 个现有位置需要检查`,
4060
+ status: 'warning',
4061
+ run: () => go({
4062
+ name: 'pluginWriteTask', pluginId: plugin.pluginId, goal: 'restore',
4063
+ targetKeys: restoreTaskCandidates.map(pluginWriteTaskTargetKey),
4064
+ }),
4065
+ } : undefined;
4066
+ const settingsAction = plugin.configFieldCount > 0
4067
+ || plugin.savedConfigScopes.length > 0 ? {
4068
+ label: plugin.configOnly ? '查看已保存设置' : '插件设置',
4069
+ description: plugin.configOnly
4070
+ ? '检查已保存设置,并可重置记录中的设置层'
4071
+ : '查看当前实际生效的配置和值来源',
4072
+ meta: plugin.configFieldCount > 0 ? `${plugin.configFieldCount} 项配置` : undefined,
4073
+ run: () => {
4074
+ if (plugin.configOnly)
4075
+ props.controller.beginSavedConfigRecovery(plugin.pluginId);
4076
+ else
4077
+ beginConfigFlow(plugin.pluginId);
4078
+ setFocus(0);
4079
+ setScrollOffset(0);
4080
+ },
4081
+ } : undefined;
4082
+ const issueForPlugin = props.controller.listUserIssues().find((candidate) => candidate.pluginId === plugin.pluginId || candidate.rootEntity?.pluginId === plugin.pluginId);
4083
+ const exceptionalAction = plugin.aggregateStatus === 'unavailable'
4084
+ ? plugin.capabilityUnsupported && !plugin.sourceUnavailable ? {
4085
+ label: '需要升级 AW CLI',
4086
+ description: '查看缺少的功能和安全恢复方向',
4087
+ status: 'warning',
4088
+ run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
4089
+ } : {
4090
+ label: '恢复插件来源…',
4091
+ description: '来源恢复后再重新检查插件状态',
4092
+ status: 'warning',
4093
+ run: () => go({ name: 'sourceDetail', sourceId: plugin.sourceId }),
4094
+ }
4095
+ : plugin.aggregateStatus === 'needsAttention' && !restoreAction ? {
4096
+ label: '查看处理建议',
4097
+ description: '当前问题需要先确认原因或处理方式',
4098
+ status: 'warning',
4099
+ run: () => issueForPlugin
4100
+ ? go({ name: 'issueDetail', issueId: issueForPlugin.issueId })
4101
+ : go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
4102
+ } : undefined;
4103
+ const primary = exceptionalAction ?? restoreAction ?? updateAction
4104
+ ?? settingsAction ?? {
4105
+ label: '使用说明',
4106
+ description: '查看用途、支持平台和来源',
4107
+ run: () => go({ name: 'pluginAbout', pluginId: plugin.pluginId }),
4108
+ };
4109
+ actions = [
4110
+ primary,
4111
+ ...(settingsAction && primary !== settingsAction ? [settingsAction] : [{
4112
+ label: '使用说明',
4113
+ description: '查看用途、支持平台和来源',
4114
+ run: () => go({ name: 'pluginAbout', pluginId: plugin.pluginId }),
4115
+ }]),
4116
+ {
4117
+ label: '更多操作…',
4118
+ description: '平台维护、依赖、通用 Skills、来源和诊断',
4119
+ run: () => go({ name: 'pluginMore', pluginId: plugin.pluginId }),
4120
+ },
4121
+ ];
4122
+ const dependencyOverviewForDetail = dependencyOverview(plugin);
4123
+ const dependencyIssue = dependencyOverviewForDetail?.dependencies.find((entry) => entry.aggregateStatus !== 'ready');
4124
+ const dependencyDetailText = dependencyIssue
4125
+ ? `${dependencyIssue.aggregateStatus === 'notInstalled' ? '缺少' : '异常'} ${dependencyIssue.displayName},${plugin.displayName} 暂时不可用。恢复会一并处理。`
4126
+ : dependencyOverviewForDetail && dependencyOverviewForDetail.summary.totalCount > 0
4127
+ ? `自动使用 ${dependencyOverviewForDetail.summary.totalCount} 个所需插件,全部就绪。查看关系…`
4128
+ : undefined;
4129
+ const detailViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(4));
4130
+ content = _jsxs(Box, { flexDirection: "column", children: [updateDescription ? _jsx(Callout, { status: "warning", title: statusText(plugin), description: updateDescription }) : null, dependencyDetailText ? _jsx(StaticRow, { title: dependencyDetailText, status: dependencyIssue ? 'warning' : 'success', compact: true }) : null, _jsx(SectionCard, { title: "\u63D2\u4EF6\u64CD\u4F5C", children: _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: detailViewport.offset, limit: detailViewport.limit }) })] });
4131
+ }
4132
+ else if (route.name === 'pluginMore' && plugin) {
4133
+ breadcrumb = `我的插件 / ${plugin.displayName} / 更多操作`;
4134
+ context = plugin.sourceLabel;
4135
+ title = '更多操作';
4136
+ description = '选择平台可使用同步、重新安装、移除和受限清理。';
3083
4137
  const dependencySummary = plugin.dependencySummary;
3084
- const detailMenu = dynamicMenu;
3085
- actions = plugin.configOnly ? detailMenu.map((action) => {
3086
- if (action.id === 'settings')
3087
- return {
3088
- label: '查看已保存设置',
3089
- description: '来源恢复前可检查问题,并只对记录中的设置层执行重置。',
3090
- run: () => {
3091
- props.controller.beginSavedConfigRecovery(plugin.pluginId);
3092
- setFocus(0);
3093
- setScrollOffset(0);
3094
- },
3095
- };
3096
- if (action.id === 'sourceProblem')
3097
- return {
3098
- label: '查看来源问题',
3099
- description: '恢复来源后可重新读取字段定义;不会把此记录当作可安装插件。',
4138
+ const detailMenu = buildPluginDetailActions(plugin);
4139
+ const updateTaskCandidates = props.controller.listPluginWriteTaskCandidates('update')
4140
+ .filter((candidate) => candidate.target.pluginId === plugin.pluginId);
4141
+ const restoreTaskCandidates = props.controller.listPluginWriteTaskCandidates('restore')
4142
+ .filter((candidate) => candidate.target.pluginId === plugin.pluginId);
4143
+ actions = [
4144
+ ...(updateTaskCandidates.length > 0 ? [{
4145
+ label: '更新插件…',
4146
+ description: '使用精确版本和现有安装位置',
3100
4147
  status: 'warning',
3101
- run: () => go({ name: 'sourceDetail', sourceId: plugin.sourceId }),
3102
- };
3103
- return {
3104
- label: '关于与技术详情',
3105
- description: '查看保存设置和来源的安全摘要',
3106
- run: () => go({ name: 'pluginAbout', pluginId: plugin.pluginId }),
3107
- };
3108
- }) : detailMenu.map((action) => {
3109
- if (action.id === 'resolveScopeConflict')
3110
- return {
3111
- label: `保留${scopeLabel(action.keepScope)},移除${scopeLabel(action.scope)} root…`,
3112
- description: action.description,
3113
- status: 'error',
3114
- run: () => beginScopeConflictRecovery(action),
3115
- };
3116
- if (action.id === 'platform' || action.id === 'scopeConflict') {
3117
- const placement = plugin.installedPlacements.find((entry) => entry.platform === action.platform);
3118
- const projected = placementStatus(placement);
3119
- return {
3120
- label: platformLabel(action.platform),
3121
- description: projected.description,
3122
- meta: projected.value,
3123
- status: projected.status,
3124
- run: () => action.id === 'scopeConflict'
3125
- ? go({ name: 'issueDetail', issueId: `scope:${plugin.pluginId}:${action.platform}` })
3126
- : go({ name: 'platform', pluginId: plugin.pluginId, platform: action.platform }),
3127
- };
3128
- }
3129
- if (action.id === 'dependencies')
3130
- return {
3131
- label: '必需依赖',
3132
- description: dependencySummary.issueCount > 0 ? `${dependencySummary.issueCount} 项需要处理` : '查看直接、间接依赖和被使用情况',
3133
- meta: dependencySummaryText(dependencySummary),
3134
- status: dependencySummary.issueCount > 0 ? 'warning' : undefined,
3135
- run: () => go({ name: 'pluginDependencies', pluginId: plugin.pluginId }),
3136
- };
3137
- if (action.id === 'portableSkills')
3138
- return {
3139
- label: '通用 Skills',
3140
- description: '选择 Skill 和 Agent 后交给 Skills CLI 安装',
3141
- meta: dependencySummary ? `可安装 · ${dependencySummary.totalCount} 个插件依赖` : '可安装',
3142
- run: () => beginPortable(plugin.pluginId),
3143
- };
3144
- if (action.id === 'settings')
3145
- return { label: '设置', description: '查看当前实际生效的配置和值来源', meta: `${plugin.configFieldCount} 项配置`, run: () => beginConfigFlow(plugin.pluginId) };
3146
- if (action.id === 'upgradeAwCli')
3147
- return {
3148
- label: '升级 AW CLI',
3149
- description: '升级到支持此插件的 AW CLI 版本后重试',
4148
+ run: () => go({
4149
+ name: 'pluginWriteTask', pluginId: plugin.pluginId, goal: 'update',
4150
+ targetKeys: updateTaskCandidates.map(pluginWriteTaskTargetKey),
4151
+ versionEvidence: pluginWriteTaskVersionEvidence(plugin, updateTaskCandidates),
4152
+ }),
4153
+ }] : []),
4154
+ ...(restoreTaskCandidates.length > 0 ? [{
4155
+ label: '恢复正常…',
4156
+ description: `${restoreTaskCandidates.length} 个现有位置需要检查`,
3150
4157
  status: 'warning',
3151
- run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
3152
- };
3153
- if (action.id === 'capabilityTechnical')
4158
+ run: () => go({
4159
+ name: 'pluginWriteTask', pluginId: plugin.pluginId, goal: 'restore',
4160
+ targetKeys: restoreTaskCandidates.map(pluginWriteTaskTargetKey),
4161
+ }),
4162
+ }] : []),
4163
+ ...detailMenu.filter((action) => action.id !== 'refreshAndSync' && action.id !== 'about')
4164
+ .map((action) => {
4165
+ if (action.id === 'platform' || action.id === 'scopeConflict') {
4166
+ const placement = plugin.installedPlacements.find((entry) => entry.platform === action.platform);
4167
+ const projected = placementStatus(placement);
4168
+ return {
4169
+ label: `${platformLabel(action.platform)} 平台与维护`,
4170
+ description: `${projected.description};同步、重装、移除和清理入口`,
4171
+ meta: projected.value,
4172
+ status: projected.status,
4173
+ run: () => action.id === 'scopeConflict'
4174
+ ? go({ name: 'issueDetail', issueId: `scope:${plugin.pluginId}:${action.platform}` })
4175
+ : go({ name: 'platform', pluginId: plugin.pluginId, platform: action.platform }),
4176
+ };
4177
+ }
4178
+ if (action.id === 'resolveScopeConflict')
4179
+ return {
4180
+ label: `处理 ${platformLabel(action.platform)} 位置冲突…`,
4181
+ description: action.description,
4182
+ status: 'error',
4183
+ run: () => beginScopeConflictRecovery(action),
4184
+ };
4185
+ if (action.id === 'dependencies')
4186
+ return {
4187
+ label: '依赖与被使用情况',
4188
+ description: dependencySummary?.issueCount ? `${dependencySummary.issueCount} 项需要处理` : '查看完整关系',
4189
+ meta: dependencySummary ? dependencySummaryText(dependencySummary) : undefined,
4190
+ run: () => go({ name: 'pluginDependencies', pluginId: plugin.pluginId }),
4191
+ };
4192
+ if (action.id === 'portableSkills')
4193
+ return {
4194
+ label: '在其他 Agent 使用',
4195
+ description: '通过 Skills CLI 选择和安装通用 Skills',
4196
+ run: () => beginPortable(plugin.pluginId),
4197
+ };
4198
+ if (action.id === 'settings')
4199
+ return {
4200
+ label: plugin.configOnly ? '查看已保存设置' : '插件设置',
4201
+ run: () => plugin.configOnly
4202
+ ? props.controller.beginSavedConfigRecovery(plugin.pluginId)
4203
+ : beginConfigFlow(plugin.pluginId),
4204
+ };
4205
+ if (action.id === 'sourceProblem')
4206
+ return {
4207
+ label: '恢复插件来源…', status: 'warning',
4208
+ run: () => go({ name: 'sourceDetail', sourceId: plugin.sourceId }),
4209
+ };
4210
+ if (action.id === 'upgradeAwCli' || action.id === 'capabilityTechnical')
4211
+ return {
4212
+ label: action.id === 'upgradeAwCli' ? '升级 AW CLI' : '查看功能支持详情',
4213
+ status: action.id === 'upgradeAwCli' ? 'warning' : undefined,
4214
+ run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
4215
+ };
3154
4216
  return {
3155
- label: '查看功能支持详情',
3156
- description: '查看缺少的功能和安全恢复方向',
4217
+ label: '查看插件详情',
3157
4218
  run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
3158
4219
  };
3159
- if (action.id === 'sourceProblem')
3160
- return { label: '查看来源问题', status: 'warning', run: () => go({ name: 'sourceDetail', sourceId: plugin.sourceId }) };
3161
- return { label: '关于与技术详情', description: '版本、来源和受管内容', run: () => go({ name: 'pluginAbout', pluginId: plugin.pluginId }) };
3162
- });
3163
- const detailViewport = actionViewport(actions, policy.mode === 'compact', effectiveFocus, scrollOffset, pageActionBudget(4));
3164
- content = _jsx(SectionCard, { title: "\u63D2\u4EF6\u64CD\u4F5C", children: _jsx(SemanticRows, { actions: actions, focus: effectiveFocus, kind: "action", compact: policy.mode === 'compact', offset: detailViewport.offset, limit: detailViewport.limit }) });
4220
+ }),
4221
+ ...(!detailMenu.some((action) => action.id === 'dependencies') ? [{
4222
+ label: '依赖与被使用情况',
4223
+ description: '查看必需依赖和已知使用者;当前没有声明的必需依赖',
4224
+ run: () => go({ name: 'pluginDependencies', pluginId: plugin.pluginId }),
4225
+ }] : []),
4226
+ { label: '使用说明', run: () => go({ name: 'pluginAbout', pluginId: plugin.pluginId }) },
4227
+ { label: '查看插件来源', run: () => go({ name: 'sourceDetail', sourceId: plugin.sourceId }) },
4228
+ { label: '诊断与受管内容', description: '查看组件状态和安全错误代码', run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }) },
4229
+ ];
4230
+ const moreViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(1));
4231
+ content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: moreViewport.offset, limit: moreViewport.limit });
3165
4232
  }
3166
4233
  else if (route.name === 'platform' && plugin) {
3167
4234
  const placement = props.controller.getPlatformPlacement(plugin.pluginId, route.platform);
@@ -3240,7 +4307,20 @@ export function ConsoleAppV4(props) {
3240
4307
  run: () => beginScopeConflictRecovery(action),
3241
4308
  };
3242
4309
  if (action.id === 'refreshAndSync')
3243
- return { label: `刷新并同步到 ${action.availableVersion}…`, description: '先刷新相关来源,再确认新的目录内容', run: () => beginReview({ kind: 'pluginWrite', action: 'refreshAndSync', pluginId: plugin.pluginId, platform: action.platform, scope: action.scope }) };
4310
+ return {
4311
+ label: `更新到 ${action.availableVersion}…`,
4312
+ description: '使用当前启用目录中的已确认版本',
4313
+ run: () => beginReview({
4314
+ kind: 'pluginWrite', action: 'sync', pluginId: plugin.pluginId,
4315
+ platform: action.platform, scope: action.scope,
4316
+ expectedUpdate: {
4317
+ installedVersion: action.installedVersion,
4318
+ activeVersion: action.availableVersion,
4319
+ sourceIdentity: action.sourceIdentity,
4320
+ generationKey: action.generationKey,
4321
+ },
4322
+ }),
4323
+ };
3244
4324
  if (action.id === 'sync')
3245
4325
  return {
3246
4326
  label: '同步已保存的使用位置…',
@@ -3384,11 +4464,12 @@ export function ConsoleAppV4(props) {
3384
4464
  }
3385
4465
  else if (route.name === 'configLayer' && plugin) {
3386
4466
  breadcrumb = `${plugin.displayName} / 设置`;
3387
- const selectedField = route.fieldKey && configPage
3388
- ? configPage.groups.flatMap((group) => group.fields).find((field) => field.key === route.fieldKey)
4467
+ const configLayerDefinitionPage = configPage ?? configLayerPages.user ?? configLayerPages.project;
4468
+ const selectedField = route.fieldKey && configLayerDefinitionPage
4469
+ ? configLayerDefinitionPage.groups.flatMap((group) => group.fields).find((field) => field.key === route.fieldKey)
3389
4470
  : undefined;
3390
- const selectedPreset = route.presetId && configPage
3391
- ? configPage.presets.find((preset) => preset.id === route.presetId)
4471
+ const selectedPreset = route.presetId && configLayerDefinitionPage
4472
+ ? configLayerDefinitionPage.presets.find((preset) => preset.id === route.presetId)
3392
4473
  : undefined;
3393
4474
  title = selectedField ? `修改“${selectedField.title}”`
3394
4475
  : selectedPreset ? `应用“${selectedPreset.title}”` : `修改 ${plugin.displayName} 设置`;
@@ -3396,11 +4477,52 @@ export function ConsoleAppV4(props) {
3396
4477
  ? `当前:${effectiveFieldDisplay(selectedField)} · ${selectedField.effectiveSourceLabel}。选择保存位置后直接编辑。`
3397
4478
  : selectedPreset ? '选择保存位置后,方案会应用到该位置的草稿。' : '选择修改保存到哪里。';
3398
4479
  help = '↑↓ 移动 · Enter 打开 · Esc 返回';
4480
+ const canUseLayer = (scope) => {
4481
+ const page = configLayerPages[scope];
4482
+ if (!page) {
4483
+ if (scope === 'user')
4484
+ return true;
4485
+ if (selectedField)
4486
+ return !selectedField.sensitive && !selectedField.sharedNativeRender;
4487
+ if (selectedPreset && configLayerDefinitionPage) {
4488
+ const effectiveFields = configLayerDefinitionPage.groups.flatMap((group) => group.fields);
4489
+ return Object.keys(selectedPreset.values).some((key) => {
4490
+ const field = effectiveFields.find((candidate) => candidate.key === key);
4491
+ return Boolean(field && !field.sensitive && !field.sharedNativeRender);
4492
+ });
4493
+ }
4494
+ return true;
4495
+ }
4496
+ const fields = page.groups.flatMap((group) => group.fields);
4497
+ if (route.fieldKey) {
4498
+ const field = fields.find((candidate) => candidate.key === route.fieldKey);
4499
+ return Boolean(field && (configFieldCanSet(field) || configFieldCanUnset(field)));
4500
+ }
4501
+ if (selectedPreset) {
4502
+ return Object.keys(selectedPreset.values).some((key) => {
4503
+ const field = fields.find((candidate) => candidate.key === key);
4504
+ return Boolean(field && configFieldCanSet(field));
4505
+ });
4506
+ }
4507
+ return fields.some((field) => configFieldCanSet(field) || configFieldCanUnset(field));
4508
+ };
3399
4509
  actions = [
3400
- { label: '个人默认', description: '在所有项目中作为默认值', meta: configLayerCounts.user === undefined ? undefined : `${configLayerCounts.user} 个值`, run: () => openConfigLayerTarget({ scope: 'user' }) },
3401
- { label: '当前项目', description: '随仓库共享,适合团队约定', meta: configLayerCounts.project === undefined ? undefined : `${configLayerCounts.project} 个值`, run: () => openConfigLayerTarget({ scope: 'project' }) },
4510
+ ...(canUseLayer('user') ? [{
4511
+ label: '个人默认',
4512
+ description: '影响个人的所有项目;项目设置仍可覆盖',
4513
+ meta: configLayerCounts.user === undefined ? undefined : `${configLayerCounts.user} 个值`,
4514
+ run: () => openConfigLayerTarget({ scope: 'user' }),
4515
+ }] : []),
4516
+ ...(canUseLayer('project') ? [{
4517
+ label: '当前项目',
4518
+ description: '修改仅对当前项目生效;适合随仓库共享的约定',
4519
+ meta: configLayerCounts.project === undefined ? undefined : `${configLayerCounts.project} 个值`,
4520
+ run: () => openConfigLayerTarget({ scope: 'project' }),
4521
+ }] : []),
3402
4522
  ];
3403
- content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "section", compact: policy.mode === 'compact' });
4523
+ content = actions.length > 0
4524
+ ? _jsx(SemanticRows, { actions: actions, focus: focus, kind: "section", compact: policy.mode === 'compact' })
4525
+ : _jsx(StatusCard, { status: "info", title: "\u6B63\u5728\u68C0\u67E5\u53EF\u7528\u4FDD\u5B58\u4F4D\u7F6E" });
3404
4526
  }
3405
4527
  else if (route.name === 'configFields' && plugin) {
3406
4528
  const readonly = route.layer.scope === 'effective';
@@ -3561,19 +4683,43 @@ export function ConsoleAppV4(props) {
3561
4683
  }
3562
4684
  else if (route.name === 'configSave' && plugin && currentConfigPage) {
3563
4685
  const changes = configChanges();
3564
- const sharedNative = route.layer.scope === 'user'
3565
- && [...Object.keys(draftUpdates), ...draftRemoves].some((key) => currentConfigPage.groups.flatMap((group) => group.fields)
3566
- .some((field) => field.key === key && field.sharedNativeRender));
4686
+ const affectedTargets = configSaveImpact?.affectedTargets ?? [];
4687
+ const hasConsumers = configSaveImpact?.impactState === 'known' && affectedTargets.length > 0;
4688
+ const targetSummary = affectedTargets
4689
+ .map((target) => `${platformLabel(target.platform)} · ${scopeLabel(target.scope)}`)
4690
+ .join('、');
3567
4691
  breadcrumb = `${plugin.displayName} / 设置 / 保存汇总`;
3568
4692
  context = `${changes.length} 项未保存`;
3569
4693
  title = `保存 ${changes.length} 项设置修改?`;
3570
- description = '保存后可选择是否立即应用到当前可见安装。';
3571
- actions = [
3572
- { label: '保存并应用', description: sharedNative ? '保存并重新生成 Codex 平台共享 Skill;影响所有启用项目' : '保存设置并同步当前可见安装', status: 'success', run: () => beginConfigSave(true) },
3573
- { label: '只保存', description: sharedNative ? '只保存个人默认;平台共享 Skill 将等待同步' : '只保存所选设置层,不应用当前安装', run: () => beginConfigSave(false) },
4694
+ description = route.layer.scope === 'project'
4695
+ ? '修改将保存到当前项目。'
4696
+ : '修改将保存为个人默认,并影响个人的所有项目;项目设置仍可覆盖。';
4697
+ actions = configSaveImpact ? [
4698
+ hasConsumers ? {
4699
+ label: '保存并生效',
4700
+ description: `保存设置,并重新应用:${targetSummary}`,
4701
+ status: 'success',
4702
+ run: () => beginConfigSave(true),
4703
+ } : {
4704
+ label: '保存设置',
4705
+ description: '当前修改没有需要重新应用的已安装目标',
4706
+ status: 'success',
4707
+ run: () => beginConfigSave(false),
4708
+ },
4709
+ ...(hasConsumers ? [{
4710
+ label: '只保存,稍后应用',
4711
+ description: '保存所选设置层,不重新应用当前目标',
4712
+ run: () => beginConfigSave(false),
4713
+ }] : []),
3574
4714
  { label: '返回修改', run: back },
3575
- ];
3576
- content = _jsxs(Box, { flexDirection: "column", children: [_jsx(SectionCard, { title: "\u8BBE\u7F6E\u53D8\u66F4", compact: policy.mode === 'compact', children: changes.map((change) => _jsx(ReviewChangeRow, { title: `~ ${draftRemoves.includes(change.key) ? `删除 ${change.title} 中的值` : change.title}`, description: `${change.before} → ${change.after}`, compact: policy.mode === 'compact' }, change.key)) }), _jsxs(SectionCard, { title: "\u5E94\u7528\u5F71\u54CD", compact: policy.mode === 'compact', children: [_jsx(StaticRow, { title: "\u4FDD\u5B58\u5E76\u5E94\u7528\u53EA\u5904\u7406\u5F53\u524D\u53EF\u89C1\u5B89\u88C5\uFF1B\u5176\u4ED6\u9879\u76EE\u4E0D\u4F1A\u88AB\u81EA\u52A8\u626B\u63CF", compact: true }), _jsx(StaticRow, { title: "\u5176\u4ED6\u8BBE\u7F6E\u5C42\u4FDD\u6301\u4E0D\u53D8", status: "success", compact: true })] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
4715
+ ] : [{ label: '返回修改', run: back }];
4716
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(SectionCard, { title: "\u8BBE\u7F6E\u53D8\u66F4", compact: policy.mode === 'compact', children: changes.map((change) => _jsx(ReviewChangeRow, { title: `~ ${draftRemoves.includes(change.key) ? `删除 ${change.title} 中的值` : change.title}`, description: `${change.before} → ${change.after}`, compact: policy.mode === 'compact' }, change.key)) }), _jsxs(SectionCard, { title: "\u5E94\u7528\u5F71\u54CD", compact: policy.mode === 'compact', children: [!configSaveImpact && !configSaveImpactError
4717
+ ? _jsx(ProgressRow, { title: "\u5206\u6790\u5B9E\u9645\u914D\u7F6E\u6D88\u8D39\u8005", state: "active" })
4718
+ : hasConsumers
4719
+ ? affectedTargets.map((target) => _jsx(StaticRow, { title: `${platformLabel(target.platform)} · ${scopeLabel(target.scope)}`, meta: "\u9700\u8981\u91CD\u65B0\u5E94\u7528", status: "warning", compact: true }, `${target.platform}:${target.scope}`))
4720
+ : _jsx(StaticRow, { title: configSaveImpactError || configSaveImpact?.impactState === 'unknown'
4721
+ ? '无法确认需要重新应用的目标;本次仅保存设置'
4722
+ : '没有需要重新应用的已安装目标', status: configSaveImpactError || configSaveImpact?.impactState === 'unknown' ? 'warning' : 'success', compact: true }), _jsx(StaticRow, { title: "\u5176\u4ED6\u8BBE\u7F6E\u5C42\u4FDD\u6301\u4E0D\u53D8", status: "success", compact: true })] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
3577
4723
  }
3578
4724
  else if (route.name === 'configFieldAction' && plugin && activeField) {
3579
4725
  const layer = configLayerLabel(route.layer);
@@ -3880,6 +5026,217 @@ export function ConsoleAppV4(props) {
3880
5026
  ]), _jsx(StaticRow, { title: "stage", meta: safeDiagnosticCode(route.diagnosticStage, 'read-config'), compact: true })] }) : null, unsupportedRows, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] })
3881
5027
  : _jsxs(Box, { flexDirection: "column", children: [_jsx(DiagnosticSummary, { diagnostic: diagnostic, technical: showTechnical, compact: policy.mode === 'compact' }), unsupportedRows, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
3882
5028
  }
5029
+ else if (route.name === 'catalogManagement') {
5030
+ breadcrumb = '插件来源 / 目录管理';
5031
+ title = '目录管理';
5032
+ description = '选择任务后才读取对应的本地状态。';
5033
+ actions = [
5034
+ { label: 'Catalog 工作区', description: '选择、创建、查看状态、修改版本、构建和验证', run: () => go({ name: 'catalogWorkspace', view: 'overview' }) },
5035
+ { label: '开发来源', description: '查看、启用或停用当前 Workspace 的开发来源', run: () => go({ name: 'catalogDev' }) },
5036
+ { label: '本地存储', description: '查看保留项并预览可安全清理的空间', run: () => go({ name: 'catalogStore' }) },
5037
+ { label: 'Catalog 发布', description: '准备计划、独立应用和恢复可信 pending 计划', run: () => go({ name: 'catalogPublish', step: 'overview' }) },
5038
+ ];
5039
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(CatalogManagementIntro, {}), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "task", compact: policy.mode === 'compact' })] });
5040
+ }
5041
+ else if (route.name === 'catalogWorkspace' && route.view === 'overview') {
5042
+ const model = catalogWorkspace ? {
5043
+ label: catalogWorkspace.label,
5044
+ state: catalogWorkspace.state,
5045
+ workspaceIdentity: catalogWorkspace.identity ?? undefined,
5046
+ catalogVersion: catalogWorkspace.catalog.version ?? undefined,
5047
+ channel: catalogWorkspace.catalog.channel ?? undefined,
5048
+ pluginCount: catalogWorkspace.plugins.length,
5049
+ snapshotState: catalogWorkspace.snapshot.state,
5050
+ gitState: !catalogWorkspace.git.checked ? '未检查'
5051
+ : !catalogWorkspace.git.hasHead ? '没有 HEAD'
5052
+ : catalogWorkspace.git.clean ? 'clean' : '有未提交变化',
5053
+ issues: catalogWorkspace.issues.map((issue) => issue.message),
5054
+ } : undefined;
5055
+ breadcrumb = '目录管理 / Catalog 工作区';
5056
+ title = 'Catalog 工作区';
5057
+ description = '状态读取不会 build、fetch、创建生成目录或运行插件代码。';
5058
+ actions = [
5059
+ { label: '选择其他工作区…', description: '输入路径;界面只显示目录名', run: () => { setCatalogInput(''); go({ name: 'catalogWorkspace', view: 'select' }); } },
5060
+ ...(catalogWorkspace?.state !== 'ready' ? [{
5061
+ label: '在所选目录创建工作区…', description: '输入 owner、显示名称和初始 Catalog 版本',
5062
+ run: () => { setCatalogInput(catalogWorkspaceDraft.ownerName); go({ name: 'catalogWorkspace', view: 'create', step: 'ownerName' }); },
5063
+ }] : []),
5064
+ ...(catalogWorkspace?.state === 'ready' ? [
5065
+ { label: '修改 Catalog 版本…', description: '选择 bump,或输入稳定版和精确 beta 版本', run: () => go({ name: 'catalogWorkspace', view: 'version' }) },
5066
+ ...(catalogWorkspace.snapshot.state === 'absent' ? [{
5067
+ label: '构建 Catalog…', description: `bundle 并验证 ${catalogWorkspace.plugins.length} 个 canonical plugins;写入 .aw/catalog`,
5068
+ run: () => beginReview({ kind: 'catalogManagement', action: 'workspaceBuild' }),
5069
+ }] : catalogWorkspace.snapshot.valid ? [{
5070
+ label: '重新构建 Catalog…', description: '替换已证明为生成物的 .aw/catalog;不删除其他内容',
5071
+ status: 'warning',
5072
+ run: () => beginReview({ kind: 'catalogManagement', action: 'workspaceBuild', force: true }),
5073
+ }] : [{
5074
+ label: '生成目录无效,不能自动替换', description: '请检查问题并移开无法证明为生成物的目录后再构建',
5075
+ status: 'error', disabled: '现有生成目录未通过安全验证',
5076
+ }]),
5077
+ { label: '完整验证生成目录', description: '只读验证现有 .aw/catalog,不重新构建', run: async () => {
5078
+ setCatalogLoading(true);
5079
+ setCatalogValidation(undefined);
5080
+ setCatalogInputError(undefined);
5081
+ try {
5082
+ setCatalogValidation(await props.controller.validateCatalogWorkspace());
5083
+ }
5084
+ catch (error) {
5085
+ setCatalogInputError(sanitizeDiagnosticText(error instanceof Error ? error.message : String(error), [], []));
5086
+ }
5087
+ finally {
5088
+ setCatalogLoading(false);
5089
+ }
5090
+ } },
5091
+ ] : []),
5092
+ ];
5093
+ const viewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(6));
5094
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(CatalogWorkspaceSummary, { model: model, loading: catalogLoading }), catalogValidation ? _jsxs(SectionCard, { title: "\u5B8C\u6574\u9A8C\u8BC1\u7ED3\u679C", children: [_jsx(StaticRow, { title: catalogValidation.state === 'valid' ? '生成目录有效' : catalogValidation.state === 'absent' ? '生成目录不存在' : '生成目录无效', meta: catalogValidation.catalogVersion ?? undefined, status: catalogValidation.state === 'valid' ? 'success' : 'warning', compact: true }), catalogValidation.issues.map((issue) => _jsx(StaticRow, { title: issue.message, meta: issue.code, status: "warning", compact: true }, issue.code))] }) : null, catalogInputError ? _jsx(Callout, { status: "error", title: "\u65E0\u6CD5\u8BFB\u53D6\u5DE5\u4F5C\u533A", description: catalogInputError }) : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: viewport.offset, limit: viewport.limit })] });
5095
+ }
5096
+ else if (route.name === 'catalogWorkspace' && route.view === 'select') {
5097
+ breadcrumb = '目录管理 / Catalog 工作区 / 选择';
5098
+ title = '输入 Workspace 路径';
5099
+ description = '相对路径从当前目录解析;输入保持在本地,不写入配置。';
5100
+ help = '输入 · Enter 选择 · Ctrl-U 清空 · Esc 取消';
5101
+ content = _jsx(InputCard, { focused: true, title: _jsx(InputDisplay, { value: privatePathInput(catalogInput), columns: Math.max(8, size.columns - 8) }), description: catalogInputError ?? '留空可恢复从当前目录自动发现。', status: catalogInputError ? 'error' : undefined });
5102
+ }
5103
+ else if (route.name === 'catalogWorkspace' && route.view === 'create') {
5104
+ const step = route.step ?? 'ownerName';
5105
+ breadcrumb = '目录管理 / Catalog 工作区 / 创建';
5106
+ context = { ownerName: '第 1 步,共 4 步', ownerUrl: '第 2 步,共 4 步', displayName: '第 3 步,共 4 步', catalogVersion: '第 4 步,共 4 步' }[step];
5107
+ title = { ownerName: '输入 owner 名称', ownerUrl: '输入 owner URL', displayName: '输入 Catalog 显示名称', catalogVersion: '输入初始 Catalog 版本' }[step];
5108
+ description = step === 'catalogVersion' ? '允许稳定 x.y.z 或精确 x.y.z-beta.N。' : '所有字段会在创建确认中再次显示。';
5109
+ help = '输入 · Enter 继续 · Ctrl-U 清空 · Esc 取消';
5110
+ const displayValue = step === 'ownerUrl'
5111
+ ? catalogInput ? `已输入 owner URL(${Array.from(catalogInput).length} 字符)` : ''
5112
+ : catalogInput;
5113
+ content = _jsx(InputCard, { focused: true, title: _jsx(InputDisplay, { value: displayValue, columns: Math.max(8, size.columns - 8) }), description: catalogInputError ?? '按 Enter 继续。', status: catalogInputError ? 'error' : undefined });
5114
+ }
5115
+ else if (route.name === 'catalogWorkspace' && route.view === 'version') {
5116
+ breadcrumb = '目录管理 / Catalog 工作区 / 版本';
5117
+ title = route.step === 'catalogVersion' ? '输入精确 Catalog 版本' : '修改 Catalog 版本';
5118
+ description = route.step === 'catalogVersion' ? '允许稳定 x.y.z 或精确 x.y.z-beta.N。' : `当前版本:${catalogWorkspace?.catalog.version ?? '未知'}`;
5119
+ if (route.step === 'catalogVersion') {
5120
+ help = '输入 · Enter 确认 · Ctrl-U 清空 · Esc 取消';
5121
+ content = _jsx(InputCard, { focused: true, title: _jsx(InputDisplay, { value: catalogInput, columns: Math.max(8, size.columns - 8) }), description: catalogInputError ?? '此动作会进入独立确认。', status: catalogInputError ? 'error' : undefined });
5122
+ }
5123
+ else {
5124
+ actions = [
5125
+ { label: '提高 patch 版本', run: () => beginReview({ kind: 'catalogManagement', action: 'workspaceVersion', bump: 'patch' }, { name: 'catalogWorkspace', view: 'overview' }) },
5126
+ { label: '提高 minor 版本', run: () => beginReview({ kind: 'catalogManagement', action: 'workspaceVersion', bump: 'minor' }, { name: 'catalogWorkspace', view: 'overview' }) },
5127
+ { label: '提高 major 版本', run: () => beginReview({ kind: 'catalogManagement', action: 'workspaceVersion', bump: 'major' }, { name: 'catalogWorkspace', view: 'overview' }) },
5128
+ { label: '输入精确版本…', description: '用于指定稳定版或精确 beta.N', run: () => { setCatalogInput(''); go({ name: 'catalogWorkspace', view: 'version', step: 'catalogVersion' }); } },
5129
+ ];
5130
+ content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "choice", compact: policy.mode === 'compact' });
5131
+ }
5132
+ }
5133
+ else if (route.name === 'catalogDev' && (!route.step || route.step === 'overview')) {
5134
+ const snapshotReady = catalogDev?.snapshot.state === 'current' && catalogDev.snapshot.valid;
5135
+ breadcrumb = '目录管理 / 开发来源';
5136
+ title = '开发来源';
5137
+ description = '状态只读取所选 Workspace、managed Path source 和 Active digest。';
5138
+ actions = [
5139
+ { label: '设置开发来源 ID…', description: catalogDevSourceId || '使用安全默认值', run: () => { setCatalogInput(catalogDevSourceId); go({ name: 'catalogDev', step: 'sourceId' }); } },
5140
+ ...!snapshotReady ? catalogDev?.snapshot.state === 'absent' ? [
5141
+ { label: '先构建 Catalog…', description: '先完成独立构建确认;构建成功后返回此页再确认启用',
5142
+ run: () => beginReview({ kind: 'catalogManagement', action: 'workspaceBuild' }, { name: 'catalogDev' }) },
5143
+ ] : catalogDev?.snapshot.state === 'stale' && catalogDev.snapshot.valid ? [
5144
+ { label: '先重新构建 Catalog…', description: '独立确认替换受管的旧 .aw/catalog;成功后返回此页再确认启用',
5145
+ status: 'warning',
5146
+ run: () => beginReview({ kind: 'catalogManagement', action: 'workspaceBuild', force: true }, { name: 'catalogDev' }) },
5147
+ ] : [
5148
+ { label: '生成目录无效,不能自动替换', description: '请先在 Workspace 管理页检查问题;移开无法证明为生成物的目录后再构建',
5149
+ status: 'error', disabled: '现有生成目录未通过安全验证' },
5150
+ ] : catalogDev?.source.exists ? [
5151
+ ...!catalogDev.ready && snapshotReady ? [{
5152
+ label: '重新启用并同步开发来源…', description: '来源配置已存在;重新使用当前有效 snapshot 完成 sync 和 Active 切换',
5153
+ status: 'warning',
5154
+ run: () => beginReview({ kind: 'catalogManagement', action: 'devActivate',
5155
+ ...(catalogDevSourceId ? { sourceId: catalogDevSourceId } : {}) }, { name: 'catalogDev' }),
5156
+ }] : [],
5157
+ { label: '停用开发来源…', description: '保留 .aw/catalog;检查安装、设置和 Marketplace 引用', run: () => beginReview({ kind: 'catalogManagement', action: 'devDeactivate' }) },
5158
+ { label: '停用并选择替代主来源…', description: '开发来源为主来源时使用', run: () => { setCatalogInput(''); go({ name: 'catalogDev', step: 'replacementPrimary' }); } },
5159
+ ] : [
5160
+ { label: '启用开发来源…', description: '使用当前有效 snapshot 注册 managed Path source 并 sync', run: () => beginReview({ kind: 'catalogManagement', action: 'devActivate', ...(catalogDevSourceId ? { sourceId: catalogDevSourceId } : {}) }) },
5161
+ { label: '启用并设为主来源…', description: '将来源启用和主来源变化放在同一影响确认中', run: () => beginReview({ kind: 'catalogManagement', action: 'devActivate', primary: true, ...(catalogDevSourceId ? { sourceId: catalogDevSourceId } : {}) }) },
5162
+ ],
5163
+ ];
5164
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(StatusCard, { status: catalogDev?.ready ? 'success' : catalogDev?.state === 'needsAttention' ? 'warning' : 'info', title: catalogLoading ? '正在读取开发来源' : catalogDev?.ready ? '开发来源可用' : catalogDev?.state === 'inactive' ? '开发来源未启用' : '开发来源需要处理', children: catalogDev ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: "\u751F\u6210\u76EE\u5F55", meta: catalogDev.snapshot.state, compact: true }), _jsx(StaticRow, { title: "\u6765\u6E90", meta: catalogDev.source.id ?? '未注册', compact: true }), _jsx(StaticRow, { title: "Active", meta: catalogDev.active.ready ? '可用' : catalogDev.active.exists ? '需要处理' : '不存在', compact: true })] }) : null }), catalogInputError ? _jsx(Callout, { status: "error", title: "\u65E0\u6CD5\u8BFB\u53D6\u5F00\u53D1\u6765\u6E90", description: catalogInputError }) : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
5165
+ }
5166
+ else if (route.name === 'catalogDev') {
5167
+ const replacement = route.step === 'replacementPrimary';
5168
+ breadcrumb = '目录管理 / 开发来源 / 输入';
5169
+ title = replacement ? '输入替代主来源 ID' : '输入开发来源 ID';
5170
+ description = replacement ? '停用和主来源变化会进入同一次影响确认。' : '留空可使用安全默认 ID。';
5171
+ help = '输入 · Enter 继续 · Ctrl-U 清空 · Esc 取消';
5172
+ content = _jsx(InputCard, { focused: true, title: _jsx(InputDisplay, { value: catalogInput, columns: Math.max(8, size.columns - 8) }), description: catalogInputError ?? '按 Enter 继续。', status: catalogInputError ? 'error' : undefined });
5173
+ }
5174
+ else if (route.name === 'catalogStore') {
5175
+ const entries = catalogStore?.entries ?? [];
5176
+ const pageRows = responsiveBudget(3, 7);
5177
+ const entryOffset = Math.min(batchTargetOffset, Math.max(0, entries.length - 1));
5178
+ const entryEnd = Math.min(entries.length, entryOffset + pageRows);
5179
+ const reclaimable = entries.filter((entry) => entry.status === 'reclaimable');
5180
+ breadcrumb = '目录管理 / 本地存储';
5181
+ title = '本地 Catalog Store';
5182
+ description = '只清理已确认且执行时仍安全的弱状态;未知、foreign 和 Active 内容始终保留。';
5183
+ if (entries.length > pageRows)
5184
+ help = 'PgUp/PgDn 查看全部条目 · ↑↓ 移动 · Enter 选择 · Esc 返回';
5185
+ actions = [{
5186
+ label: '预览并清理可安全释放的空间…',
5187
+ description: `${reclaimable.length} 项;确认后新增候选不进入本次删除集合`,
5188
+ disabled: reclaimable.length ? undefined : '当前没有可安全清理的条目',
5189
+ run: () => beginReview({ kind: 'catalogManagement', action: 'storePrune', items: reclaimable.map((entry) => entry.item) }),
5190
+ }];
5191
+ content = _jsxs(Box, { flexDirection: "column", children: [catalogStore ? _jsx(CatalogStoreSummary, { reclaimableCount: catalogStore.totals.reclaimableEntries, reclaimableBytes: catalogStore.totals.reclaimableBytes ?? 0, retainedCount: catalogStore.totals.retainedEntries, entries: entries.map((entry) => ({ key: entry.itemKey, title: `${entry.category} · ${entry.itemKey}`, meta: `${entry.reason}${entry.reclaimableBytes === null ? '' : ` · ${entry.reclaimableBytes} bytes`}`, status: entry.status === 'error' ? 'unsafe' : entry.status === 'active' ? 'retained' : entry.status })), before: entryOffset, after: entryEnd }) : _jsx(StatusCard, { status: "info", title: "\u6B63\u5728\u8BFB\u53D6\u672C\u5730 Catalog Store" }), catalogInputError ? _jsx(Callout, { status: "error", title: "\u65E0\u6CD5\u8BFB\u53D6 Store", description: catalogInputError }) : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
5192
+ }
5193
+ else if (route.name === 'catalogPublish' && route.step === 'overview') {
5194
+ const records = catalogPublication?.records ?? [];
5195
+ const pageRows = responsiveBudget(3, 7);
5196
+ const recordOffset = Math.min(batchTargetOffset, Math.max(0, records.length - 1));
5197
+ const recordEnd = Math.min(records.length, recordOffset + pageRows);
5198
+ breadcrumb = '目录管理 / Catalog 发布';
5199
+ title = 'Catalog 发布';
5200
+ description = '准备计划与应用计划是两个独立写入动作。';
5201
+ if (records.length > pageRows)
5202
+ help = 'PgUp/PgDn 查看全部记录 · ↑↓ 移动 · Enter 选择 · Esc 返回';
5203
+ actions = [
5204
+ { label: '准备新发布计划…', description: '下一步输入 remote 或 URL 与 branch;执行 plan 会写 version-scoped journal 和隔离仓库', run: () => { setCatalogInput(catalogPublishDraft.expectedPlanDigest ? '' : catalogPublishDraft.target); setCatalogPublishDraft((draft) => draft.expectedPlanDigest ? { targetKind: 'remote', target: '', branch: 'catalog' } : draft); go({ name: 'catalogPublish', step: 'target' }); } },
5205
+ ...records.filter((record) => record.canApply && record.planDigest).map((record) => ({
5206
+ label: `${record.state === 'uncertain' ? '对账或继续' : '应用'} ${record.catalogVersion}…`,
5207
+ description: record.requiresRemoteInput
5208
+ ? '重新输入原 remote 名称或 URL;只恢复现有可信 plan digest'
5209
+ : '只使用现有可信 plan digest;不会从当前源码重建计划',
5210
+ status: record.state === 'uncertain' ? 'warning' : undefined,
5211
+ run: () => record.requiresRemoteInput
5212
+ ? (() => {
5213
+ setCatalogPublishDraft({ targetKind: 'remote', target: '', branch: record.branch ?? 'catalog', expectedPlanDigest: record.planDigest });
5214
+ setCatalogInput('');
5215
+ go({ name: 'catalogPublish', step: 'target' });
5216
+ })()
5217
+ : beginReview({ kind: 'catalogManagement', action: 'publishApply', expectedPlanDigest: record.planDigest, ...(record.remote ? { remote: record.remote } : {}), ...(record.branch ? { branch: record.branch } : {}) }, { name: 'catalogPublish', step: 'overview' }),
5218
+ })),
5219
+ ...records.filter((record) => record.mustIncreaseVersion).map((record) => ({
5220
+ label: `${record.catalogVersion} 无法恢复`, description: 'journal 或 pending repository 缺失或损坏;请提高 Catalog 版本后创建新计划', status: 'error', disabled: '不能重建或复用此版本',
5221
+ })),
5222
+ ];
5223
+ const viewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(5));
5224
+ content = _jsxs(Box, { flexDirection: "column", children: [catalogPublication ? _jsx(CatalogPublicationSummary, { records: records.map((record) => ({ key: `${record.catalogVersion}:${record.state}`, catalogVersion: record.catalogVersion, state: record.state, branch: record.branch ?? undefined, message: record.issue?.message })), before: recordOffset, after: recordEnd }) : _jsx(StatusCard, { status: "info", title: "\u6B63\u5728\u8BFB\u53D6\u672C\u5730\u53D1\u5E03\u8BB0\u5F55" }), catalogInputError ? _jsx(Callout, { status: "error", title: "\u65E0\u6CD5\u8BFB\u53D6\u53D1\u5E03\u8BB0\u5F55", description: catalogInputError }) : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: viewport.offset, limit: viewport.limit })] });
5225
+ }
5226
+ else if (route.name === 'catalogPublish') {
5227
+ breadcrumb = '目录管理 / Catalog 发布 / 准备计划';
5228
+ title = route.step === 'target' ? '输入 remote 名称或 remote URL' : '输入发布 branch';
5229
+ description = route.step === 'target'
5230
+ ? catalogPublishDraft.expectedPlanDigest
5231
+ ? '重新输入原目标。后端会与现有 plan digest 中的目标身份核对。'
5232
+ : 'URL 只显示脱敏摘要;remote 名称按原样使用。'
5233
+ : '留空使用固定默认 branch:catalog。';
5234
+ help = '输入 · Enter 继续 · Ctrl-U 清空 · Esc 取消';
5235
+ const displayValue = route.step === 'target'
5236
+ ? catalogInput ? `已输入远程目标(${Array.from(catalogInput).length} 字符)` : ''
5237
+ : catalogInput;
5238
+ content = _jsx(InputCard, { focused: true, title: _jsx(InputDisplay, { value: displayValue, columns: Math.max(8, size.columns - 8) }), description: catalogInputError ?? (route.step === 'branch' ? '执行 plan 会写 publication journal 和隔离仓库;取消 Review 不写入。' : '按 Enter 继续。'), status: catalogInputError ? 'error' : undefined });
5239
+ }
3883
5240
  else if (route.name === 'sourceList') {
3884
5241
  breadcrumb = '插件来源';
3885
5242
  title = '插件来源';
@@ -3912,9 +5269,14 @@ export function ConsoleAppV4(props) {
3912
5269
  description: '并行检查各来源;结果不会自动启用待检查版本',
3913
5270
  run: () => props.controller.recheckAllCatalogSources(),
3914
5271
  },
5272
+ {
5273
+ label: '目录管理…',
5274
+ description: 'Catalog 工作区、开发来源、本地存储和发布',
5275
+ run: () => go({ name: 'catalogManagement' }),
5276
+ },
3915
5277
  ];
3916
5278
  const viewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, responsiveBudget(8, 12));
3917
- const sourceCount = Math.max(0, actions.length - 2);
5279
+ const sourceCount = Math.max(0, actions.length - 3);
3918
5280
  content = _jsx(Box, { flexDirection: "column", children: _jsx(SemanticRows, { actions: actions, focus: focus, kind: focus < sourceCount ? 'section' : 'action', compact: policy.mode === 'compact', offset: viewport.offset, limit: viewport.limit }) });
3919
5281
  }
3920
5282
  else if (route.name === 'sourceDetail') {
@@ -4018,8 +5380,7 @@ export function ConsoleAppV4(props) {
4018
5380
  disabled: freshness?.state === 'checking' ? '检查正在进行' : undefined,
4019
5381
  run: () => props.controller.recheckCatalogSource(route.sourceId),
4020
5382
  }];
4021
- actions = [
4022
- ...generationAction,
5383
+ const managementActions = [
4023
5384
  { label: '编辑来源…', description: '集中修改地址、引用、子目录和自动检查', run: () => openSourceEdit(route.sourceId) },
4024
5385
  ...(generationState === 'incompatible'
4025
5386
  || (freshness?.state === 'incompatible' && generationState !== 'ready') ? [] : [{
@@ -4058,10 +5419,101 @@ export function ConsoleAppV4(props) {
4058
5419
  run: () => props.controller.fullValidateCatalogSource(route.sourceId),
4059
5420
  },
4060
5421
  ];
5422
+ actions = [
5423
+ ...generationAction.slice(0, 1),
5424
+ managementActions[0],
5425
+ {
5426
+ label: '更多处理方式…',
5427
+ description: '设为主来源、移除、精确修复、替换、重新检查和完整验证',
5428
+ run: () => go({ name: 'sourceMore', sourceId: route.sourceId }),
5429
+ },
5430
+ ];
4061
5431
  const sourceActionViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, responsiveBudget(2, 5));
4062
5432
  const freshnessLabel = catalogSourceFreshnessLabel(freshness);
4063
5433
  content = _jsxs(Box, { flexDirection: "column", children: [_jsxs(SectionCard, { title: "\u5F53\u524D\u76EE\u5F55\u7248\u672C", status: generationState === 'incompatible' ? 'error' : activeVersion ? 'success' : 'warning', compact: policy.mode === 'compact', children: [_jsx(StaticRow, { title: activeVersion ? `版本 ${activeVersion}` : '未启用', meta: activeVersion ? generation?.active?.digestShort : catalogGenerationStateLabel(generationState), status: generationState === 'incompatible' ? 'error' : undefined, compact: true }), candidateVersion ? _jsx(StaticRow, { title: `待检查版本 ${candidateVersion}`, meta: freshnessLabel, status: freshness?.state === 'updateAvailable' || freshness?.state === 'localChangesAvailable' ? 'warning' : undefined, compact: true }) : _jsx(StaticRow, { title: freshnessLabel, status: freshness?.state === 'checkFailed' ? 'warning' : undefined, compact: true })] }), _jsxs(SectionCard, { title: "\u6765\u6E90\u914D\u7F6E", compact: policy.mode === 'compact', children: [_jsx(StaticRow, { title: "\u4F4D\u7F6E", meta: detail?.locationSummary ?? '不可用', compact: true }), _jsx(StaticRow, { title: "\u5F15\u7528", meta: generation?.active?.requestedRef ?? detail?.explicitRef ?? detail?.requestedRef ?? '本地目录', compact: true }), _jsx(StaticRow, { title: "\u81EA\u52A8\u68C0\u67E5\u66F4\u65B0", meta: detail?.autoCheck === false ? '关闭' : '开启', compact: true })] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: sourceActionViewport.offset, limit: sourceActionViewport.limit })] });
4064
5434
  }
5435
+ else if (route.name === 'sourceMore') {
5436
+ const overview = props.controller.getSourceOverview(route.sourceId);
5437
+ const detail = overview.detail;
5438
+ const generation = props.controller.getCatalogGeneration(route.sourceId);
5439
+ const freshness = props.controller.getCatalogFreshness(route.sourceId);
5440
+ const generationState = generation?.state ?? (generation?.active ? 'ready' : 'unavailable');
5441
+ const combinedRepairAndMismatch = generationState === 'repairRequired'
5442
+ && generation?.configMismatch === true;
5443
+ const logicalIdentityChanged = generation?.errorCode === 'catalogLogicalIdentityChanged';
5444
+ const activationBlocked = props.controller.catalogSourceRecoveryBlockReason();
5445
+ const blocked = activationBlocked ? `请先处理:${sourceOperationBlockLabel(activationBlocked)}` : undefined;
5446
+ breadcrumb = `插件来源 / ${route.sourceId} / 更多处理方式`;
5447
+ title = '管理来源';
5448
+ description = '选择精确操作。每个写入动作都会进入独立确认。';
5449
+ help = '↑↓ 移动 · Enter 选择 · Esc 返回';
5450
+ actions = [
5451
+ ...(generationState === 'repairRequired' ? [{
5452
+ label: '重新下载旧版本并精确修复',
5453
+ description: '使用当前启用版本的精确提交;成功后保持此版本',
5454
+ disabled: blocked,
5455
+ run: () => beginReview({ kind: 'catalogGeneration', action: 'repair', sourceId: route.sourceId }),
5456
+ }, ...freshness?.state === 'incompatible' ? [] : [{
5457
+ label: '使用待检查版本替换损坏版本…',
5458
+ description: '旧版本无法取得时使用;这会进入独立风险确认',
5459
+ disabled: blocked,
5460
+ run: () => beginReview({ kind: 'catalogGeneration', action: 'replaceDamaged', sourceId: route.sourceId }),
5461
+ }]] : []),
5462
+ ...(generationState !== 'ready' && generationState !== 'repairRequired'
5463
+ && generationState !== 'incompatible' && !logicalIdentityChanged ? [{
5464
+ label: generationState === 'stateInvalid' ? '恢复状态并启用'
5465
+ : generationState === 'configMismatch' ? '验证并切换' : '获取并启用',
5466
+ description: '先准备只读候选;确认后才切换当前目录版本',
5467
+ disabled: blocked,
5468
+ run: () => beginReview({
5469
+ kind: 'catalogGeneration',
5470
+ action: generationState === 'stateInvalid' ? 'recoverState'
5471
+ : generationState === 'configMismatch' ? 'update' : 'activate',
5472
+ sourceId: route.sourceId,
5473
+ }),
5474
+ }] : []),
5475
+ ...(generationState === 'ready'
5476
+ && (freshness?.state === 'updateAvailable' || freshness?.state === 'localChangesAvailable') ? [{
5477
+ label: freshness.state === 'localChangesAvailable' ? '应用本地内容变化…'
5478
+ : `更新到 ${freshness.candidate?.catalogVersion ?? '待检查版本'}…`,
5479
+ description: '确认前不会切换当前版本',
5480
+ disabled: blocked,
5481
+ run: () => beginReview({ kind: 'catalogGeneration', action: 'update', sourceId: route.sourceId }),
5482
+ }] : []),
5483
+ {
5484
+ label: generationState === 'ready' ? '设为主来源…' : '启用并设为主来源…',
5485
+ description: generationState === 'ready' ? '只改变默认插件来源' : '启用已验证候选后改变默认来源',
5486
+ disabled: detail?.primary ? '当前已经是主来源'
5487
+ : logicalIdentityChanged ? '请编辑来源,或移除后重新添加' : blocked,
5488
+ run: () => generationState === 'ready'
5489
+ ? beginReview({ kind: 'sourceMutation', action: 'setPrimary', sourceId: route.sourceId,
5490
+ changes: [{ title: '主来源', before: snapshot.viewModel?.sourceTabs.find((source) => source.primary)?.id, after: route.sourceId }] })
5491
+ : beginReview({
5492
+ kind: 'catalogGeneration',
5493
+ action: generationState === 'stateInvalid' ? 'recoverState'
5494
+ : generationState === 'repairRequired' && !combinedRepairAndMismatch ? 'repair'
5495
+ : generationState === 'configMismatch' || combinedRepairAndMismatch ? 'update' : 'activate',
5496
+ sourceId: route.sourceId,
5497
+ setPrimaryAfterActivation: true,
5498
+ }),
5499
+ },
5500
+ { label: '移除来源…', description: '检查当前、历史和未知引用后再确认', status: 'error',
5501
+ disabled: detail?.primary ? '主来源不能直接移除' : undefined,
5502
+ run: () => beginReview({ kind: 'sourceMutation', action: 'remove', sourceId: route.sourceId,
5503
+ changes: [{ title: `移除来源 ${route.sourceId}` }] }) },
5504
+ { label: freshness?.state === 'checking' ? '正在检查…' : '重新检查',
5505
+ description: '轻量检查这个来源;不会自动启用更新',
5506
+ disabled: freshness?.state === 'checking' ? '检查正在进行' : undefined,
5507
+ run: () => props.controller.recheckCatalogSource(route.sourceId) },
5508
+ { label: '完整验证', description: '强制重新取得并完整验证这个来源',
5509
+ disabled: freshness?.state === 'checking' ? '检查正在进行' : undefined,
5510
+ run: () => props.controller.fullValidateCatalogSource(route.sourceId) },
5511
+ { label: '技术详情', description: '查看脱敏错误代码和版本证据',
5512
+ run: () => go({ name: 'sourceTechnical', sourceId: route.sourceId }) },
5513
+ ];
5514
+ const viewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(1));
5515
+ content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: viewport.offset, limit: viewport.limit });
5516
+ }
4065
5517
  else if (route.name === 'sourceTechnical') {
4066
5518
  const overview = props.controller.getSourceOverview(route.sourceId);
4067
5519
  const detail = overview.detail;
@@ -4097,16 +5549,14 @@ export function ConsoleAppV4(props) {
4097
5549
  if (route.step === 'type') {
4098
5550
  title = '选择来源类型';
4099
5551
  description = '来源类型创建后不能修改。';
4100
- const selectType = (type, autoCheck) => {
4101
- setSourceDraft({ ...sourceDraft, type, autoCheck });
5552
+ const selectType = (type) => {
5553
+ setSourceDraft({ ...sourceDraft, type });
4102
5554
  setDirty(true);
4103
5555
  go({ name: 'sourceAdd', step: sourceDraft.sourceId ? 'location' : 'id', owner: route.owner });
4104
5556
  };
4105
5557
  actions = [
4106
- { label: 'Git 仓库', description: '自动检查更新(推荐)', run: () => selectType('git', true) },
4107
- { label: '本地目录', description: '自动检查本地内容变化(推荐)', run: () => selectType('path', true) },
4108
- { label: 'Git 仓库 · 关闭自动检查', description: '仍可从详情页手动检查', run: () => selectType('git', false) },
4109
- { label: '本地目录 · 关闭自动检查', description: '仍可从详情页手动检查', run: () => selectType('path', false) },
5558
+ { label: 'Git 仓库', description: '使用现有自动检查默认值;可在修改汇总前单独更改', run: () => selectType('git') },
5559
+ { label: '本地目录', description: '使用现有自动检查默认值;可在修改汇总前单独更改', run: () => selectType('path') },
4110
5560
  ];
4111
5561
  content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "choice", compact: policy.mode === 'compact' });
4112
5562
  }
@@ -4534,13 +5984,15 @@ export function ConsoleAppV4(props) {
4534
5984
  }
4535
5985
  else if (route.name === 'reviewPreparing' && snapshot.reviewPreparation) {
4536
5986
  const preparation = snapshot.reviewPreparation;
4537
- const labels = preparation.operationKind === 'portableInstall'
4538
- ? ['通用 Skills / 安装', '正在准备通用 Skills 安装确认']
4539
- : preparation.operationKind === 'configSave' || preparation.operationKind === 'configReset'
4540
- ? ['设置', '正在准备设置确认']
4541
- : preparation.operationKind === 'sourceMutation' || preparation.operationKind === 'catalogGeneration'
4542
- ? ['插件来源', '正在准备来源变更确认']
4543
- : ['插件操作', '正在准备插件操作确认'];
5987
+ const labels = preparation.operationKind === 'pluginWriteTask'
5988
+ ? ['插件任务', '正在准备任务影响确认']
5989
+ : preparation.operationKind === 'portableInstall'
5990
+ ? ['通用 Skills / 安装', '正在准备通用 Skills 安装确认']
5991
+ : preparation.operationKind === 'configSave' || preparation.operationKind === 'configReset'
5992
+ ? ['设置', '正在准备设置确认']
5993
+ : preparation.operationKind === 'sourceMutation' || preparation.operationKind === 'catalogGeneration'
5994
+ ? ['插件来源', '正在准备来源变更确认']
5995
+ : ['插件操作', '正在准备插件操作确认'];
4544
5996
  breadcrumb = `${labels[0]} / 准备确认`;
4545
5997
  context = '只读';
4546
5998
  title = preparation.state === 'failed' ? '无法准备确认信息' : labels[1];
@@ -4569,22 +6021,37 @@ export function ConsoleAppV4(props) {
4569
6021
  }
4570
6022
  else if (route.name === 'review' && snapshot.activeReview) {
4571
6023
  const review = snapshot.activeReview;
6024
+ const installReviewPluginId = pendingReviewIntent?.kind === 'pluginWrite'
6025
+ && pendingReviewIntent.action === 'install'
6026
+ ? pendingReviewIntent.pluginId
6027
+ : review.pluginAction === 'install' ? snapshot.installPlacementDraft?.pluginId : undefined;
4572
6028
  const batchReview = review.operationKind === 'pluginWriteBatch';
6029
+ const taskReview = review.operationKind === 'pluginWriteTask';
6030
+ const catalogManagementReview = review.operationKind === 'catalogManagement';
4573
6031
  const sharedImpactConfirmed = !review.requiresConfirmation
4574
6032
  || confirmedSharedReviewToken === review.reviewToken;
4575
6033
  breadcrumb = `${review.subject.name} / ${review.title}`;
4576
- title = `${snapshot.result?.state === 'stale' ? '状态已变化 · ' : ''}${review.operationKind === 'portableInstall' ? review.title : `确认${review.title}`} ${review.subject.name}`;
6034
+ title = `${snapshot.result?.state === 'stale' ? '状态已变化 · ' : ''}${review.operationKind === 'portableInstall' || taskReview
6035
+ ? review.title : `确认${review.title}`} ${review.subject.name}`;
4577
6036
  description = snapshot.result?.state === 'stale'
4578
6037
  ? '当前状态已变化。请检查新的变化后再次确认。'
4579
6038
  : review.requiresConfirmation ? '' : review.description;
4580
- if (batchReview) {
6039
+ if (taskReview) {
6040
+ context = '精确目标 · 用户任务';
6041
+ help = 'PgUp/PgDn 查看全部影响 · ↑↓ 移动 · Enter 选择 · Esc 返回';
6042
+ }
6043
+ else if (batchReview) {
4581
6044
  context = `${review.batch?.requestedPluginIds.length ?? 0} 个根插件 · ${review.batch?.groupCount ?? 0} 组`;
4582
6045
  help = 'PgUp/PgDn 查看全部根插件和依赖 · ↑↓ 移动 · Enter 选择 · Esc 返回';
4583
6046
  }
4584
6047
  actions = [
4585
6048
  ...(review.requiresConfirmation ? [{
4586
- label: sharedImpactConfirmed ? '已确认共享影响' : '确认共享影响',
4587
- description: review.sharedImpact.warning ?? '此操作会修改多个插件共同使用的 Codex 平台内容',
6049
+ label: catalogManagementReview
6050
+ ? sharedImpactConfirmed ? '已确认本次写入' : '确认本次写入'
6051
+ : sharedImpactConfirmed ? '已确认共享影响' : '确认共享影响',
6052
+ description: catalogManagementReview
6053
+ ? '我已检查上方的精确写入、远端影响和保留项。'
6054
+ : review.sharedImpact.warning ?? '此操作会修改多个插件共同使用的 Codex 平台内容',
4588
6055
  selected: sharedImpactConfirmed,
4589
6056
  run: () => {
4590
6057
  setConfirmedSharedReviewToken(review.reviewToken);
@@ -4602,6 +6069,19 @@ export function ConsoleAppV4(props) {
4602
6069
  operationBusy.current = false;
4603
6070
  });
4604
6071
  } },
6072
+ ...(installReviewPluginId
6073
+ && (snapshot.installPlacementDraft?.targets.length ?? 0) > 0 ? [{
6074
+ label: '查看/调整依赖位置',
6075
+ description: '当前已采用 planner 推荐位置;调整后会重新建立 Review',
6076
+ run: () => {
6077
+ props.controller.push({
6078
+ name: 'dependencyPlacement',
6079
+ pluginId: installReviewPluginId,
6080
+ });
6081
+ setFocus(0);
6082
+ setScrollOffset(0);
6083
+ },
6084
+ }] : []),
4605
6085
  ...(sharedImpactConfirmed && (pendingReviewIntent?.kind === 'pluginWrite'
4606
6086
  && (pendingReviewIntent.action === 'install'
4607
6087
  || pendingReviewIntent.action === 'refreshAndSync'
@@ -4611,6 +6091,7 @@ export function ConsoleAppV4(props) {
4611
6091
  && (pendingReviewIntent.action === 'install'
4612
6092
  || pendingReviewIntent.action === 'sync'
4613
6093
  || pendingReviewIntent.action === 'reinstall')
6094
+ || pendingReviewIntent?.kind === 'pluginWriteTask'
4614
6095
  || pendingReviewIntent?.kind === 'configSave' && pendingReviewIntent.apply) ? [{
4615
6096
  label: '高级选项:脚本超时',
4616
6097
  description: `${pendingReviewIntent.scriptTimeoutMs ?? SCRIPT_TIMEOUT_DEFAULT_MS} 毫秒`,
@@ -4736,13 +6217,26 @@ export function ConsoleAppV4(props) {
4736
6217
  ? `另有 ${hiddenTechnicalChanges} 项` : undefined, status: changeStatus(change), compact: true }, `${change.title}:${index}`);
4737
6218
  });
4738
6219
  const showEquivalentCommand = Boolean(review.equivalentCommand && showTechnical && !review.requiresConfirmation);
6220
+ const installDependencyTargets = installReviewPluginId
6221
+ ? snapshot.installPlacementDraft?.targets ?? [] : [];
6222
+ const installDependencyPluginIds = new Set(installDependencyTargets.map((target) => target.pluginId));
6223
+ const reusedDependencyPluginIds = new Set(installDependencyTargets
6224
+ .filter((target) => target.action === 'keep')
6225
+ .map((target) => target.pluginId));
6226
+ const newDependencyCount = [...installDependencyPluginIds]
6227
+ .filter((pluginId) => !reusedDependencyPluginIds.has(pluginId)).length;
6228
+ const widenedDependencyCount = new Set(installDependencyTargets
6229
+ .filter((target) => target.affectsOtherProjects && target.action !== 'keep')
6230
+ .map((target) => target.pluginId)).size;
4739
6231
  if (review.changes.length > staticRowBudget || showTechnical
4740
6232
  && (review.changes.length > technicalChangeBudget || technicalDetails.length > technicalDetailLimit)) {
4741
6233
  help = 'PgUp/PgDn 查看全部确认信息 · ↑↓ 移动 · Enter 选择 · Esc 返回';
4742
6234
  }
4743
- content = _jsx(Box, { flexDirection: "column", children: _jsxs(ReviewCard, { title: review.requiresConfirmation ? undefined : review.subject.name, meta: review.requiresConfirmation ? undefined : `${review.subject.sourceLabel ?? ''} ${review.subject.version ?? ''}`, description: showEquivalentCommand ? review.description : undefined, children: [showEquivalentCommand ? _jsxs(Text, { children: ["\u7B49\u4EF7\u547D\u4EE4\uFF1A", review.equivalentCommand] }) : null, showTechnical && review.catalogBinding ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: `Catalog:${review.catalogBinding.sourceId} ${review.catalogBinding.catalogVersion} · 当前使用`, meta: `generation ${review.catalogBinding.generationDigestShort}`, status: "success", compact: true }), _jsx(StaticRow, { title: "\u64CD\u4F5C\u671F\u95F4\u4E0D\u68C0\u67E5\u8FDC\u7AEF Catalog", status: "success", compact: true })] }) : null, review.requiresConfirmation ? _jsx(StaticRow, { title: "! \u9700\u8981\u786E\u8BA4\u5171\u4EAB\u5F71\u54CD", meta: review.sharedImpact.pluginIds.length > 0 ? `${review.sharedImpact.pluginIds.length} 个插件` : undefined, description: review.sharedImpact.warning, status: "warning", compact: true }) : null, showTechnical ? technicalChanges : changeGroups, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true }), compressedReview || showTechnical ? null : _jsx(StaticRow, { title: "\u6280\u672F\u8BE6\u60C5\uFF08d\uFF09", status: "muted" }), showTechnical ? _jsx(TechnicalDetails, { children: visibleTechnicalDetails.map((detail, index) => _jsx(StaticRow, { title: detail, meta: index === visibleTechnicalDetails.length - 1 && hiddenTechnicalDetails > 0 ? `另有 ${hiddenTechnicalDetails} 项` : undefined, compact: true }, detail)) }) : null] }) });
6235
+ content = _jsx(Box, { flexDirection: "column", children: _jsxs(ReviewCard, { title: review.requiresConfirmation ? undefined : review.subject.name, meta: review.requiresConfirmation ? undefined : `${review.subject.sourceLabel ?? ''} ${review.subject.version ?? ''}`, description: showEquivalentCommand ? review.description : undefined, children: [showEquivalentCommand ? _jsxs(Text, { children: ["\u7B49\u4EF7\u547D\u4EE4\uFF1A", review.equivalentCommand] }) : null, showTechnical && review.catalogBinding ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: `Catalog:${review.catalogBinding.sourceId} ${review.catalogBinding.catalogVersion} · 当前使用`, meta: `generation ${review.catalogBinding.generationDigestShort}`, status: "success", compact: true }), _jsx(StaticRow, { title: "\u64CD\u4F5C\u671F\u95F4\u4E0D\u68C0\u67E5\u8FDC\u7AEF Catalog", status: "success", compact: true })] }) : null, installDependencyPluginIds.size > 0 ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: `需要 ${installDependencyPluginIds.size} 个额外插件;${reusedDependencyPluginIds.size} 个复用、${newDependencyCount} 个新增`, status: "info", compact: true }), _jsx(StaticRow, { title: widenedDependencyCount > 0 ? `${widenedDependencyCount} 个依赖会扩大到所有项目` : '没有依赖会扩大到所有项目', status: widenedDependencyCount > 0 ? 'warning' : 'success', compact: true })] }) : null, review.requiresConfirmation ? _jsx(StaticRow, { title: catalogManagementReview ? '! 需要确认本次写入' : '! 需要确认共享影响', meta: !catalogManagementReview && review.sharedImpact.pluginIds.length > 0 ? `${review.sharedImpact.pluginIds.length} 个插件` : undefined, description: catalogManagementReview ? review.description : review.sharedImpact.warning, status: "warning", compact: true }) : null, showTechnical ? technicalChanges : changeGroups, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true }), compressedReview || showTechnical ? null : _jsx(StaticRow, { title: "\u6280\u672F\u8BE6\u60C5\uFF08d\uFF09", status: "muted" }), showTechnical ? _jsx(TechnicalDetails, { children: visibleTechnicalDetails.map((detail, index) => _jsx(StaticRow, { title: detail, meta: index === visibleTechnicalDetails.length - 1 && hiddenTechnicalDetails > 0 ? `另有 ${hiddenTechnicalDetails} 项` : undefined, compact: true }, detail)) }) : null] }) });
4744
6236
  }
4745
6237
  else if (route.name === 'progress') {
6238
+ const taskOperation = snapshot.activeReview?.operationKind === 'pluginWriteTask';
6239
+ const catalogManagementOperation = snapshot.activeReview?.operationKind === 'catalogManagement';
4746
6240
  const portableOperation = snapshot.activeReview?.operationKind === 'portableInstall';
4747
6241
  const configOperation = snapshot.activeReview?.operationKind === 'configSave'
4748
6242
  || snapshot.activeReview?.operationKind === 'configReset';
@@ -4750,24 +6244,25 @@ export function ConsoleAppV4(props) {
4750
6244
  || snapshot.activeReview?.operationKind === 'catalogGeneration'
4751
6245
  || snapshot.activeReview?.operationKind === 'catalogSourcesRecovery';
4752
6246
  const issueOperation = snapshot.activeReview?.originIssue;
4753
- breadcrumb = portableOperation ? '通用 Skills / 进度' : issueOperation ? '需要处理 / 进度' : sourceOperation ? '插件来源 / 保存' : configOperation ? '设置 / 进度' : '插件操作 / 进度';
6247
+ breadcrumb = catalogManagementOperation ? '目录管理 / 进度' : taskOperation ? '插件任务 / 进度' : portableOperation ? '通用 Skills / 进度' : issueOperation ? '需要处理 / 进度' : sourceOperation ? '插件来源 / 保存' : configOperation ? '设置 / 进度' : '插件操作 / 进度';
4754
6248
  const configApply = snapshot.activeReview?.operationKind === 'configSave'
4755
6249
  && snapshot.activeReview.changes.some((change) => change.group === 'configImpact' && change.kind === 'change');
4756
6250
  const catalogGenerationOperation = snapshot.activeReview?.operationKind === 'catalogGeneration';
4757
6251
  const catalogSourcesRecovery = snapshot.activeReview?.operationKind === 'catalogSourcesRecovery';
4758
- title = portableOperation ? '正在安装通用 Skills' : issueOperation ? '正在处理问题'
6252
+ title = catalogManagementOperation ? '正在执行 Catalog 管理操作' : taskOperation ? '正在执行插件任务' : portableOperation ? '正在安装通用 Skills' : issueOperation ? '正在处理问题'
4759
6253
  : catalogGenerationOperation ? '正在验证并启用目录版本'
4760
6254
  : catalogSourcesRecovery ? '正在恢复插件来源配置'
4761
6255
  : sourceOperation ? '正在保存来源配置'
4762
6256
  : configOperation ? configApply ? '正在保存并应用设置' : '正在保存设置'
4763
6257
  : '正在执行插件操作';
4764
- description = issueOperation ? '完成后会重新读取状态并检查问题。'
4765
- : catalogGenerationOperation ? '正在验证并应用已确认的目录版本操作。'
4766
- : catalogSourcesRecovery ? '正在备份损坏配置并恢复官方默认来源。'
4767
- : sourceOperation ? '只原子保存来源配置,不访问网络。'
4768
- : configOperation ? configApply ? '先保存所选层,再重新应用当前可见安装位置。' : '只保存所选设置层。'
4769
- : '请勿关闭当前终端。';
4770
- const batchProgress = snapshot.activeReview?.operationKind === 'pluginWriteBatch';
6258
+ description = catalogManagementOperation ? '正在执行已确认的精确 Workspace、Store、开发来源或发布操作。'
6259
+ : issueOperation ? '完成后会重新读取状态并检查问题。'
6260
+ : catalogGenerationOperation ? '正在验证并应用已确认的目录版本操作。'
6261
+ : catalogSourcesRecovery ? '正在备份损坏配置并恢复官方默认来源。'
6262
+ : sourceOperation ? '只原子保存来源配置,不访问网络。'
6263
+ : configOperation ? configApply ? '先保存所选层,再重新应用当前可见安装位置。' : '只保存所选设置层。'
6264
+ : '请勿关闭当前终端。';
6265
+ const batchProgress = snapshot.activeReview?.operationKind === 'pluginWriteBatch' || taskOperation;
4771
6266
  const progress = snapshot.progress;
4772
6267
  const elapsedSeconds = Math.max(0, Math.floor(((progress.elapsedMs
4773
6268
  ?? (progress.startedAtMs === undefined ? 0 : Date.now() - progress.startedAtMs))) / 1_000));
@@ -4869,7 +6364,7 @@ export function ConsoleAppV4(props) {
4869
6364
  return step === 2 ? '正在安装' : step === 3 ? '正在验证' : step === 1 ? '正在生成' : '正在检查';
4870
6365
  };
4871
6366
  const compactPortableProgress = policy.mode === 'compact' && showTechnical;
4872
- content = portableOperation ? _jsxs(StatusCard, { status: failedProgress ? 'error' : 'info', title: "Skills CLI", children: [_jsx(ProgressRow, { title: "\u68C0\u67E5\u63D2\u4EF6\u548C\u4F9D\u8D56", description: `${snapshot.activeReview?.subject.name ?? '当前插件'} · ${portableDraft?.dependencySummary?.directCount ?? 0} 个直接依赖`, meta: portableProgressMeta(0), state: portableProgressState(0), compact: compactPortableProgress }), _jsx(ProgressRow, { title: "\u751F\u6210\u5DF2\u9009\u5019\u9009 Skills", description: `${portableProviderCount} 个来源或依赖插件提供 ${portableSelectedCount} 个 Skills`, meta: portableProgressMeta(1), state: portableProgressState(1), compact: compactPortableProgress }), _jsx(ProgressRow, { title: `安装已选择的 ${portableSelectedCount} 个 Skills`, description: `Agent:${portableDraft?.agentMode === 'explicit' ? portableDraft.requestedAgents.join('、') : 'Skills CLI 默认选择'}`, meta: portableProgressMeta(2), state: portableProgressState(2), compact: compactPortableProgress }), _jsx(ProgressRow, { title: "\u9A8C\u8BC1\u7ED3\u679C", description: "\u9010\u9879\u786E\u8BA4\u6240\u9009 Skill \u7684\u5B89\u88C5\u76EE\u6807", meta: portableProgressMeta(3), state: portableProgressState(3), compact: compactPortableProgress }), showTechnical ? _jsxs(TechnicalDetails, { children: [technicalMessages.map((message, index) => _jsx(StaticRow, { title: message, compact: true }, `${index}:${message}`)), hiddenTechnicalMessages > 0 ? _jsx(StaticRow, { title: `另有 ${hiddenTechnicalMessages} 条技术消息`, status: "muted", compact: true }) : null] }) : null] }) : _jsxs(StatusCard, { status: "info", children: [dependencyProgress ? _jsxs(_Fragment, { children: [_jsx(ProgressRow, { title: "\u68C0\u67E5\u4F9D\u8D56\u5173\u7CFB", state: relationStep }), _jsx(ProgressRow, { title: "\u751F\u6210\u6267\u884C\u8BA1\u5212", state: planStep }), _jsx(ProgressRow, { title: "\u6267\u884C\u4F9D\u8D56\u95ED\u5305", description: progress.current?.target ? `当前:${progress.current.target}` : undefined, meta: `${elapsedSeconds}s`, state: closureStep, compact: policy.mode === 'compact' }), _jsx(ProgressRow, { title: "\u9A8C\u8BC1\u5B8C\u6574\u7ED3\u679C", state: verificationStep })] }) : _jsxs(_Fragment, { children: [_jsx(ProgressRow, { title: sourceOperation ? '检查来源操作输入' : configOperation ? '验证设置值' : '检查兼容性', state: !currentPhase ? 'pending' : preflightActive ? 'active' : 'complete' }), _jsx(ProgressRow, { title: progress.current?.phase || (sourceOperation ? '保存来源配置' : configOperation ? '保存设置层并重新应用' : '连接到所选目标平台'), description: progress.current?.target ? `当前:${progress.current.target}` : undefined, meta: `${elapsedSeconds}s`, state: failedProgress ? 'error' : executionActive ? 'active' : validationActive ? 'complete' : 'pending', compact: policy.mode === 'compact' }), _jsx(ProgressRow, { title: "\u9A8C\u8BC1\u7ED3\u679C", state: failedProgress ? 'error' : validationActive ? 'active' : 'pending' })] }), dependencyProgress ? _jsxs(SectionCard, { title: "\u6267\u884C\u76EE\u6807", meta: `${snapshot.progressTargets.length} 个目标`, children: [progressTargetOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${progressTargetOffset} 个目标`, status: "muted", compact: true }) : null, visibleProgressTargets.map((entry) => {
6367
+ content = catalogManagementOperation ? _jsxs(StatusCard, { status: failedProgress ? 'error' : 'info', title: "Catalog \u7BA1\u7406", children: [_jsx(ProgressRow, { title: "\u786E\u8BA4\u8BA1\u5212", meta: "\u5B8C\u6210", state: "complete" }), _jsx(ProgressRow, { title: currentPhase || '准备执行', description: progress.current?.target, meta: `${elapsedSeconds}s`, state: failedProgress ? 'error' : currentPhase ? 'active' : 'pending', compact: policy.mode === 'compact' }), _jsx(ProgressRow, { title: "\u9A8C\u8BC1\u5E76\u8BB0\u5F55\u7ED3\u679C", state: validationActive ? 'active' : 'pending' }), !showTechnical && latestMessage ? _jsx(StaticRow, { title: latestMessage, status: "muted", compact: true }) : null, showTechnical ? technicalMessages.map((message, index) => _jsx(StaticRow, { title: message, compact: true }, `${index}:${message}`)) : null, exitAfterBusy ? _jsx(Callout, { status: "warning", title: "\u64CD\u4F5C\u5B8C\u6210\u540E\u9000\u51FA" }) : null] }) : portableOperation ? _jsxs(StatusCard, { status: failedProgress ? 'error' : 'info', title: "Skills CLI", children: [_jsx(ProgressRow, { title: "\u68C0\u67E5\u63D2\u4EF6\u548C\u4F9D\u8D56", description: `${snapshot.activeReview?.subject.name ?? '当前插件'} · ${portableDraft?.dependencySummary?.directCount ?? 0} 个直接依赖`, meta: portableProgressMeta(0), state: portableProgressState(0), compact: compactPortableProgress }), _jsx(ProgressRow, { title: "\u751F\u6210\u5DF2\u9009\u5019\u9009 Skills", description: `${portableProviderCount} 个来源或依赖插件提供 ${portableSelectedCount} 个 Skills`, meta: portableProgressMeta(1), state: portableProgressState(1), compact: compactPortableProgress }), _jsx(ProgressRow, { title: `安装已选择的 ${portableSelectedCount} 个 Skills`, description: `Agent:${portableDraft?.agentMode === 'explicit' ? portableDraft.requestedAgents.join('、') : 'Skills CLI 默认选择'}`, meta: portableProgressMeta(2), state: portableProgressState(2), compact: compactPortableProgress }), _jsx(ProgressRow, { title: "\u9A8C\u8BC1\u7ED3\u679C", description: "\u9010\u9879\u786E\u8BA4\u6240\u9009 Skill \u7684\u5B89\u88C5\u76EE\u6807", meta: portableProgressMeta(3), state: portableProgressState(3), compact: compactPortableProgress }), showTechnical ? _jsxs(TechnicalDetails, { children: [technicalMessages.map((message, index) => _jsx(StaticRow, { title: message, compact: true }, `${index}:${message}`)), hiddenTechnicalMessages > 0 ? _jsx(StaticRow, { title: `另有 ${hiddenTechnicalMessages} 条技术消息`, status: "muted", compact: true }) : null] }) : null] }) : _jsxs(StatusCard, { status: "info", children: [dependencyProgress ? _jsxs(_Fragment, { children: [_jsx(ProgressRow, { title: "\u68C0\u67E5\u4F9D\u8D56\u5173\u7CFB", state: relationStep }), _jsx(ProgressRow, { title: "\u751F\u6210\u6267\u884C\u8BA1\u5212", state: planStep }), _jsx(ProgressRow, { title: "\u6267\u884C\u4F9D\u8D56\u95ED\u5305", description: progress.current?.target ? `当前:${progress.current.target}` : undefined, meta: `${elapsedSeconds}s`, state: closureStep, compact: policy.mode === 'compact' }), _jsx(ProgressRow, { title: "\u9A8C\u8BC1\u5B8C\u6574\u7ED3\u679C", state: verificationStep })] }) : _jsxs(_Fragment, { children: [_jsx(ProgressRow, { title: sourceOperation ? '检查来源操作输入' : configOperation ? '验证设置值' : '检查兼容性', state: !currentPhase ? 'pending' : preflightActive ? 'active' : 'complete' }), _jsx(ProgressRow, { title: progress.current?.phase || (sourceOperation ? '保存来源配置' : configOperation ? '保存设置层并重新应用' : '连接到所选目标平台'), description: progress.current?.target ? `当前:${progress.current.target}` : undefined, meta: `${elapsedSeconds}s`, state: failedProgress ? 'error' : executionActive ? 'active' : validationActive ? 'complete' : 'pending', compact: policy.mode === 'compact' }), _jsx(ProgressRow, { title: "\u9A8C\u8BC1\u7ED3\u679C", state: failedProgress ? 'error' : validationActive ? 'active' : 'pending' })] }), dependencyProgress ? _jsxs(SectionCard, { title: "\u6267\u884C\u76EE\u6807", meta: `${snapshot.progressTargets.length} 个目标`, children: [progressTargetOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${progressTargetOffset} 个目标`, status: "muted", compact: true }) : null, visibleProgressTargets.map((entry) => {
4873
6368
  const state = progressLabel(entry);
4874
6369
  const relation = entry.relation === 'requested' ? '你选择的插件' : entry.relation === 'direct' ? '直接依赖' : '间接依赖';
4875
6370
  const title = props.controller.getPluginOverview(entry.pluginId)?.displayName
@@ -4889,6 +6384,42 @@ export function ConsoleAppV4(props) {
4889
6384
  : result.state === 'blocked' ? result.summary : undefined;
4890
6385
  const action = resultSummary?.action;
4891
6386
  const operationKind = 'operationKind' in result ? result.operationKind : undefined;
6387
+ const catalogManagementOperation = operationKind === 'catalogManagement';
6388
+ const catalogManagementResult = catalogManagementOperation
6389
+ ? resultSummary?.catalogManagement : undefined;
6390
+ const taskOperation = operationKind === 'pluginWriteTask';
6391
+ const taskSummary = resultSummary?.task;
6392
+ const taskBlockedCount = taskSummary?.blockedTargets.length ?? 0;
6393
+ const frozenTaskReturnRoute = 'returnRoute' in result
6394
+ && result.returnRoute?.name === 'pluginWriteTask'
6395
+ ? result.returnRoute : undefined;
6396
+ const nestedTaskReturn = frozenTaskReturnRoute !== undefined
6397
+ && taskSummary !== undefined
6398
+ && frozenTaskReturnRoute.goal !== taskSummary.goal;
6399
+ const taskBatchOrigin = frozenTaskReturnRoute?.batchOrigin;
6400
+ const remainingTaskTargets = taskSummary?.blockedTargets.map((entry) => entry.target) ?? [];
6401
+ const taskReturnRoute = nestedTaskReturn
6402
+ ? frozenTaskReturnRoute
6403
+ : taskSummary && remainingTaskTargets[0] ? {
6404
+ name: 'pluginWriteTask',
6405
+ pluginId: remainingTaskTargets[0].pluginId,
6406
+ goal: taskSummary.goal,
6407
+ targetKeys: remainingTaskTargets.map((target) => [target.pluginId, target.platform, target.scope].join('\0')),
6408
+ ...(taskBatchOrigin ? { batchOrigin: taskBatchOrigin } : {}),
6409
+ ...(taskSummary.goal === 'update' ? {
6410
+ versionEvidence: 'returnRoute' in result
6411
+ && result.returnRoute?.name === 'pluginWriteTask'
6412
+ && result.returnRoute.versionEvidence
6413
+ ? result.returnRoute.versionEvidence
6414
+ : Object.fromEntries(remainingTaskTargets.flatMap((target) => target.expectedUpdate
6415
+ ? [[[target.pluginId, target.platform, target.scope].join('\0'), JSON.stringify({
6416
+ version: target.expectedUpdate.activeVersion,
6417
+ sourceIdentity: target.expectedUpdate.sourceIdentity,
6418
+ binding: target.expectedUpdate.generationKey,
6419
+ })]]
6420
+ : [])),
6421
+ } : {}),
6422
+ } : undefined;
4892
6423
  const batchOperation = operationKind === 'pluginWriteBatch';
4893
6424
  const portableOperation = operationKind === 'portableInstall';
4894
6425
  const portableReceipt = result.state === 'succeeded' || result.state === 'partial'
@@ -4912,79 +6443,96 @@ export function ConsoleAppV4(props) {
4912
6443
  cleanup: '✓ 插件清理完成',
4913
6444
  sync: '✓ 插件同步完成',
4914
6445
  }[action ?? 'install'];
4915
- title = batchOperation
4916
- ? blocked ? '× 批量操作没有完成'
4917
- : partial ? '! 批量操作只完成了一部分' : `✓ 批量${batchActionLabel(action)}完成`
4918
- : portableOperation
4919
- ? blocked ? '× 通用 Skills 安装被阻止'
4920
- : partial ? '! 通用 Skills 结果未完全完成' : '✓ 通用 Skills 已安装'
4921
- : blocked && result.writeMayHaveCompleted
4922
- ? '× 写入结果未确认'
4923
- : issueOperation
4924
- ? blocked || partial ? '× 问题仍未解决' : '✓ 问题已处理'
4925
- : sourceOperation
4926
- ? blocked ? '× 来源操作未完成'
4927
- : operationKind === 'catalogSourcesRecovery' ? '✓ Catalog 来源配置已恢复'
4928
- : resultSummary?.catalogAction === 'update'
4929
- ? resultSummary.catalogGeneration?.activeChanged
4930
- && resultSummary.catalogGeneration.outcome === 'activated'
4931
- ? '✓ Catalog 已更新' : '✓ Catalog 无需更新'
4932
- : resultSummary?.catalogAction === 'repair' ? '✓ Catalog 已修复'
4933
- : resultSummary?.catalogAction === 'replaceDamaged' ? '✓ 损坏的 Catalog 已替换'
4934
- : resultSummary?.catalogAction === 'recoverState' ? '✓ Catalog 状态已恢复'
4935
- : operationKind === 'catalogGeneration' ? '✓ Catalog 已启用'
4936
- : '✓ 来源配置已保存'
4937
- : configOperation
4938
- ? blocked
4939
- ? '× 设置未确认保存'
4940
- : partial ? resultSummary?.configApplySkippedReason === 'invalidConfig'
4941
- ? '! 设置已保存,但未应用'
4942
- : '! 设置已保存,但部分应用失败'
4943
- : resultSummary?.configApplied ? '✓ 设置已保存并应用' : '✓ 设置已保存'
4944
- : partial ? '! 插件操作只完成了一部分'
4945
- : blocked ? result.code === 'operationFailed' ? '× 插件操作没有完成' : '× 插件操作未完成'
4946
- : completedTitle;
4947
- description = batchOperation
4948
- ? blocked
4949
- ? '批量写入被阻止。已完成和未完成目标按实际结果保留。'
4950
- : partial
4951
- ? (resultSummary?.batch?.retryTargets?.length ?? 0) > 0
4952
- ? `${resultSummary?.batch?.retryTargets?.length ?? 0} 个根目标失败。成功目标不会重放。`
4953
- : '失败目标无法精确定位。请查看本次记录后重新操作。'
4954
- : `${resultSummary?.batch?.requestedPluginIds.length ?? 0} 个根插件已完成。`
4955
- : portableOperation
4956
- ? blocked
4957
- ? result.code === 'operationReviewExpired' || result.code === 'reviewTicketExpired'
4958
- ? 'Review 已过期,写入未开始。请返回修改并重新确认。'
4959
- : result.code === 'operationReviewStale'
4960
- ? '安装证据已变化,写入被阻止。请返回修改并重新确认。'
4961
- : result.writeMayHaveCompleted
4962
- ? '写入结果无法确认。请检查外部 Skills 状态后重试。'
4963
- : '安装在写入前被阻止。请返回修改后重试。'
4964
- : portableInstallFailed ? 'Skills CLI 安装失败,可能已有部分目标写入。可以修改 Agent 或使用原选择重试。' : partial ? '无法确认全部已选择 Skills。' : '所选 Skills 已通过安装验证。'
4965
- : blocked && result.writeMayHaveCompleted
4966
- ? result.message
4967
- : issueOperation
6446
+ title = catalogManagementOperation
6447
+ ? blocked ? '× Catalog 管理操作未完成'
6448
+ : partial ? '! Catalog 管理操作只完成了一部分'
6449
+ : catalogManagementResult?.action === 'publishPlan' ? '✓ 发布计划已准备'
6450
+ : catalogManagementResult?.action === 'publishApply' ? '✓ Catalog 发布完成'
6451
+ : catalogManagementResult?.action === 'storePrune' ? '✓ Store 清理完成'
6452
+ : '✓ Catalog 管理操作完成'
6453
+ : taskOperation
6454
+ ? blocked ? `× 插件${pluginWriteTaskLabel(taskSummary?.goal ?? 'restore')}没有完成`
6455
+ : partial ? `! 插件${pluginWriteTaskLabel(taskSummary?.goal ?? 'restore')}只完成了一部分`
6456
+ : `✓ 插件${pluginWriteTaskLabel(taskSummary?.goal ?? 'restore')}完成`
6457
+ : batchOperation
6458
+ ? blocked ? '× 批量操作没有完成'
6459
+ : partial ? '! 批量操作只完成了一部分' : `✓ 批量${batchActionLabel(action)}完成`
6460
+ : portableOperation
6461
+ ? blocked ? '× 通用 Skills 安装被阻止'
6462
+ : partial ? '! 通用 Skills 结果未完全完成' : '✓ 通用 Skills 已安装'
6463
+ : blocked && result.writeMayHaveCompleted
6464
+ ? '× 写入结果未确认'
6465
+ : issueOperation
6466
+ ? blocked || partial ? '× 问题仍未解决' : '✓ 问题已处理'
6467
+ : sourceOperation
6468
+ ? blocked ? '× 来源操作未完成'
6469
+ : operationKind === 'catalogSourcesRecovery' ? '✓ Catalog 来源配置已恢复'
6470
+ : resultSummary?.catalogAction === 'update'
6471
+ ? resultSummary.catalogGeneration?.activeChanged
6472
+ && resultSummary.catalogGeneration.outcome === 'activated'
6473
+ ? '✓ Catalog 已更新' : '✓ Catalog 无需更新'
6474
+ : resultSummary?.catalogAction === 'repair' ? '✓ Catalog 已修复'
6475
+ : resultSummary?.catalogAction === 'replaceDamaged' ? '✓ 损坏的 Catalog 已替换'
6476
+ : resultSummary?.catalogAction === 'recoverState' ? '✓ Catalog 状态已恢复'
6477
+ : operationKind === 'catalogGeneration' ? '✓ Catalog 已启用'
6478
+ : '✓ 来源配置已保存'
6479
+ : configOperation
6480
+ ? blocked
6481
+ ? '× 设置未确认保存'
6482
+ : partial ? resultSummary?.configApplySkippedReason === 'invalidConfig'
6483
+ ? '! 设置已保存,但未应用'
6484
+ : '! 设置已保存,但部分应用失败'
6485
+ : resultSummary?.configApplied ? '✓ 设置已保存并应用' : '✓ 设置已保存'
6486
+ : partial ? '! 插件操作只完成了一部分'
6487
+ : blocked ? result.code === 'operationFailed' ? '× 插件操作没有完成' : '× 插件操作未完成'
6488
+ : completedTitle;
6489
+ description = catalogManagementOperation
6490
+ ? blocked ? result.message : resultSummary?.message ?? 'Catalog 状态已更新。'
6491
+ : taskOperation
6492
+ ? blocked ? '任务被安全阻止。已完成和未完成目标按实际结果保留。'
6493
+ : partial ? `${taskSummary?.retryTargets?.length ?? 0} 个失败目标可重试 · ${taskBlockedCount} 个目标需要决定。`
6494
+ : `${taskSummary?.rootResults.length ?? 0} 个根插件已完成。`
6495
+ : batchOperation
6496
+ ? blocked
6497
+ ? '批量写入被阻止。已完成和未完成目标按实际结果保留。'
6498
+ : partial
6499
+ ? (resultSummary?.batch?.retryTargets?.length ?? 0) > 0
6500
+ ? `${resultSummary?.batch?.failedRequestedPluginIds.length ?? 0} 个根插件失败,涉及 ${resultSummary?.batch?.retryTargets?.length ?? 0} 个位置。成功目标不会重放。`
6501
+ : '失败目标无法精确定位。请查看本次记录后重新操作。'
6502
+ : `${resultSummary?.batch?.requestedPluginIds.length ?? 0} 个根插件已完成。`
6503
+ : portableOperation
4968
6504
  ? blocked
4969
- ? result.code === 'issueVerificationFailed'
4970
- ? '写入可能已完成,但当前无法确认。请重新检查状态。'
4971
- : result.code === 'issueStillPresent'
4972
- ? '写入已完成,但重新检查后原问题仍然存在。'
4973
- : '操作被安全阻止。可返回问题详情后重试。'
4974
- : '状态已重新加载;原问题已确认消失。'
4975
- : sourceOperation
4976
- ? blocked
4977
- ? result.writeMayHaveCompleted
4978
- ? '来源配置可能已保存。请重新检查来源列表。'
4979
- : '来源操作未执行。草稿已保留,可返回修改。'
4980
- : partial ? resultSummary?.message ?? '来源配置已保存,但 Catalog 尚未启用。可立即重试。'
4981
- : resultSummary?.message ?? '来源配置已保存。'
4982
- : blocked ? result.message
4983
- : configOperation
4984
- ? partial
4985
- ? resultSummary?.message ?? '设置层已经保存;平台应用失败不会回滚设置。'
4986
- : '设置层已经更新。'
4987
- : '状态已刷新。';
6505
+ ? result.code === 'operationReviewExpired' || result.code === 'reviewTicketExpired'
6506
+ ? 'Review 已过期,写入未开始。请返回修改并重新确认。'
6507
+ : result.code === 'operationReviewStale'
6508
+ ? '安装证据已变化,写入被阻止。请返回修改并重新确认。'
6509
+ : result.writeMayHaveCompleted
6510
+ ? '写入结果无法确认。请检查外部 Skills 状态后重试。'
6511
+ : '安装在写入前被阻止。请返回修改后重试。'
6512
+ : portableInstallFailed ? 'Skills CLI 安装失败,可能已有部分目标写入。可以修改 Agent 或使用原选择重试。' : partial ? '无法确认全部已选择 Skills。' : '所选 Skills 已通过安装验证。'
6513
+ : blocked && result.writeMayHaveCompleted
6514
+ ? result.message
6515
+ : issueOperation
6516
+ ? blocked
6517
+ ? result.code === 'issueVerificationFailed'
6518
+ ? '写入可能已完成,但当前无法确认。请重新检查状态。'
6519
+ : result.code === 'issueStillPresent'
6520
+ ? '写入已完成,但重新检查后原问题仍然存在。'
6521
+ : '操作被安全阻止。可返回问题详情后重试。'
6522
+ : '状态已重新加载;原问题已确认消失。'
6523
+ : sourceOperation
6524
+ ? blocked
6525
+ ? result.writeMayHaveCompleted
6526
+ ? '来源配置可能已保存。请重新检查来源列表。'
6527
+ : '来源操作未执行。草稿已保留,可返回修改。'
6528
+ : partial ? resultSummary?.message ?? '来源配置已保存,但 Catalog 尚未启用。可立即重试。'
6529
+ : resultSummary?.message ?? '来源配置已保存。'
6530
+ : blocked ? result.message
6531
+ : configOperation
6532
+ ? partial
6533
+ ? resultSummary?.message ?? '设置层已经保存;平台应用失败不会回滚设置。'
6534
+ : '设置层已经更新。'
6535
+ : '状态已刷新。';
4988
6536
  const pluginId = resultSummary?.pluginId
4989
6537
  ?? ('pluginId' in result ? result.pluginId : undefined);
4990
6538
  const configResultPluginId = pluginId ?? ('returnRoute' in result
@@ -5034,6 +6582,7 @@ export function ConsoleAppV4(props) {
5034
6582
  const retryAction = failedPluginId && failedTarget && failedTargetState
5035
6583
  ? selectLifecycleRetryAction({
5036
6584
  operationKind: operationKind === 'catalogSourcesRecovery' || operationKind === 'pluginWriteBatch'
6585
+ || operationKind === 'pluginWriteTask' || operationKind === 'catalogManagement'
5037
6586
  ? undefined : operationKind,
5038
6587
  originalAction: action,
5039
6588
  target: {
@@ -5054,146 +6603,221 @@ export function ConsoleAppV4(props) {
5054
6603
  && (candidate.platform ?? failedIssue.platform) === failedTarget?.platform
5055
6604
  && (candidate.scope ?? failedIssue.scope) === failedTarget?.scope)
5056
6605
  : undefined;
5057
- actions = batchOperation
6606
+ const preparedPublication = catalogManagementResult?.action === 'publishPlan'
6607
+ && catalogManagementResult.planDigest && catalogManagementResult.publication
6608
+ ? catalogManagementResult : undefined;
6609
+ actions = catalogManagementOperation
5058
6610
  ? [
5059
- ...((partial || blocked) && (resultSummary?.batch?.retryTargets?.length ?? 0) > 0 ? [{
5060
- label: '仅重试失败目标…',
5061
- description: '重新读取失败根目标及其依赖,并进入新的确认页',
5062
- run: () => {
5063
- props.controller.retryFailedPluginWriteBatch();
5064
- setFocus(0);
5065
- setScrollOffset(0);
5066
- },
6611
+ ...(preparedPublication ? [{
6612
+ label: '应用此发布计划…',
6613
+ description: '进入独立确认;取消确认不会写入远端',
6614
+ status: 'success',
6615
+ run: () => beginReview({
6616
+ kind: 'catalogManagement',
6617
+ action: 'publishApply',
6618
+ expectedPlanDigest: preparedPublication.planDigest,
6619
+ ...(catalogPublishDraft.target
6620
+ ? catalogPublishDraft.targetKind === 'remote'
6621
+ ? { remote: catalogPublishDraft.target }
6622
+ : { remoteUrl: catalogPublishDraft.target }
6623
+ : {}),
6624
+ branch: catalogPublishDraft.branch,
6625
+ }, { name: 'catalogPublish', step: 'overview' }),
5067
6626
  }] : []),
5068
- { label: '返回我的插件', run: () => leaveResult() },
6627
+ { label: catalogManagementResult?.action === 'storePrune' ? '返回本地存储'
6628
+ : catalogManagementResult?.action === 'devActivate' || catalogManagementResult?.action === 'devDeactivate' ? '返回开发来源'
6629
+ : catalogManagementResult?.action === 'publishPlan' || catalogManagementResult?.action === 'publishApply' ? '返回发布管理'
6630
+ : '返回 Catalog 工作区',
6631
+ run: () => leaveResult(catalogManagementResult?.action === 'storePrune'
6632
+ ? { name: 'catalogStore' }
6633
+ : catalogManagementResult?.action === 'devActivate' || catalogManagementResult?.action === 'devDeactivate'
6634
+ ? { name: 'catalogDev' }
6635
+ : catalogManagementResult?.action === 'publishPlan' || catalogManagementResult?.action === 'publishApply'
6636
+ ? { name: 'catalogPublish', step: 'overview' }
6637
+ : 'returnRoute' in result && result.returnRoute ? result.returnRoute
6638
+ : { name: 'catalogManagement' }) },
5069
6639
  { label: '查看本次记录', run: () => result.operationRecordId
5070
6640
  && go({ name: 'operationDiagnostic', recordId: result.operationRecordId }) },
5071
6641
  ]
5072
- : portableOperation
5073
- ? blocked || partial ? [
5074
- { label: blocked || partial ? '返回修改' : portableOriginIsDetail ? '返回插件详情' : '返回插件预览', run: async () => {
5075
- if (!pluginId) {
5076
- leaveResult();
5077
- return;
5078
- }
5079
- if (!blocked && !partial) {
5080
- props.controller.discardPortableDraft();
5081
- leaveResult();
5082
- return;
5083
- }
5084
- if (!await refreshPortableDraft(pluginId))
5085
- return;
5086
- if (portableInstallFailed && portableReceipt?.agentMode === 'explicit') {
5087
- props.controller.resetPortableFlowToPreview(pluginId, { name: 'portableAgentInput', pluginId }, true);
5088
- return;
5089
- }
5090
- props.controller.resetPortableFlowToPreview(pluginId, { name: 'portableSkillSelect', pluginId }, true);
5091
- } },
5092
- { label: blocked || partial ? '重试' : '再安装其他 Skills…', run: async () => {
5093
- if (!pluginId)
5094
- return;
5095
- if (blocked || partial) {
5096
- props.controller.resetPortableFlowToPreview(pluginId, { name: 'portableSkillSelect', pluginId }, true);
5097
- await beginPortableReview(pluginId, snapshot.portableDraft?.scope ?? 'project');
5098
- return;
5099
- }
5100
- props.controller.resetPortableFlowToPreview(pluginId);
5101
- await beginPortable(pluginId);
5102
- } },
5103
- { label: portableOriginIsDetail ? '返回插件详情' : '返回插件预览', run: () => { if (pluginId)
5104
- props.controller.resetPortableFlowToPreview(pluginId); } },
5105
- ] : [
5106
- { label: portableOriginIsDetail ? '返回插件详情' : '返回插件预览', run: () => {
5107
- props.controller.discardPortableDraft();
5108
- leaveResult();
5109
- } },
5110
- { label: '再安装其他 Skills…', run: async () => {
5111
- if (!pluginId)
5112
- return;
5113
- props.controller.resetPortableFlowToPreview(pluginId);
5114
- await beginPortable(pluginId);
5115
- } },
6642
+ : taskOperation
6643
+ ? [
6644
+ ...(nestedTaskReturn && taskReturnRoute ? [{
6645
+ label: '返回剩余恢复任务',
6646
+ description: '保留父任务的冻结目标;返回后重新读取当前证据',
6647
+ status: 'success',
6648
+ run: () => leaveResult(taskReturnRoute),
6649
+ }] : []),
6650
+ ...((partial || blocked) && (taskSummary?.retryTargets?.length ?? 0) > 0 ? [{
6651
+ label: `继续处理剩余 ${taskSummary.retryTargets.length} 项…`,
6652
+ description: '重新检查失败的精确目标和必要依赖,并进入新的确认页',
6653
+ run: () => {
6654
+ props.controller.retryFailedPluginWriteTask();
6655
+ setFocus(0);
6656
+ setScrollOffset(0);
6657
+ },
6658
+ }] : []),
6659
+ ...(taskBlockedCount > 0 && taskReturnRoute ? [{
6660
+ label: `返回任务处理 ${taskBlockedCount} 个需决定项`,
6661
+ description: '受阻目标没有加入失败重试;返回后可查看原因和可用的下一步',
6662
+ status: 'warning',
6663
+ run: () => leaveResult(taskReturnRoute),
6664
+ }] : []),
6665
+ ...(!nestedTaskReturn ? [{ label: '完成,返回我的插件', run: () => {
6666
+ if (taskBatchOrigin?.owner === 'batch-write'
6667
+ && taskBatchOrigin.flowId === batchDraft?.flowId) {
6668
+ setBatchDraft(undefined);
6669
+ setDirty(false);
6670
+ }
6671
+ leaveResult({ name: 'pluginList', mode: 'installed' });
6672
+ } }] : []),
6673
+ { label: '查看本次记录', run: () => result.operationRecordId
6674
+ && go({ name: 'operationDiagnostic', recordId: result.operationRecordId }) },
5116
6675
  ]
5117
- : issueOperation && !dependencyRecovery
5118
- ? blocked || partial
5119
- ? [
5120
- { label: '返回问题详情', run: () => blocked
5121
- ? leaveResult()
5122
- : failedIssue
5123
- ? leaveResult({ name: 'issueDetail', issueId: failedIssue.issueId })
5124
- : pluginId && failedTarget
5125
- ? leaveResult({ name: 'platform', pluginId, platform: failedTarget.platform })
5126
- : leaveResult() },
5127
- { label: '重新检查问题', run: () => leaveResult({ name: 'issueList' }) },
5128
- ]
5129
- : [
5130
- { label: '查看问题列表', run: () => leaveResult({ name: 'issueList' }) },
5131
- { label: '返回首页', run: () => requestSessionNavigation('home') },
6676
+ : batchOperation
6677
+ ? [
6678
+ ...((partial || blocked) && (resultSummary?.batch?.retryTargets?.length ?? 0) > 0 ? [{
6679
+ label: '仅重试失败目标…',
6680
+ description: '重新读取失败根目标及其依赖,并进入新的确认页',
6681
+ run: () => {
6682
+ props.controller.retryFailedPluginWriteBatch();
6683
+ setFocus(0);
6684
+ setScrollOffset(0);
6685
+ },
6686
+ }] : []),
6687
+ { label: '返回我的插件', run: () => leaveResult() },
6688
+ { label: '查看本次记录', run: () => result.operationRecordId
6689
+ && go({ name: 'operationDiagnostic', recordId: result.operationRecordId }) },
6690
+ ]
6691
+ : portableOperation
6692
+ ? blocked || partial ? [
6693
+ { label: blocked || partial ? '返回修改' : portableOriginIsDetail ? '返回插件详情' : '返回插件预览', run: async () => {
6694
+ if (!pluginId) {
6695
+ leaveResult();
6696
+ return;
6697
+ }
6698
+ if (!blocked && !partial) {
6699
+ props.controller.discardPortableDraft();
6700
+ leaveResult();
6701
+ return;
6702
+ }
6703
+ if (!await refreshPortableDraft(pluginId))
6704
+ return;
6705
+ if (portableInstallFailed && portableReceipt?.agentMode === 'explicit') {
6706
+ props.controller.resetPortableFlowToPreview(pluginId, { name: 'portableAgentInput', pluginId }, true);
6707
+ return;
6708
+ }
6709
+ props.controller.resetPortableFlowToPreview(pluginId, { name: 'portableSkillSelect', pluginId }, true);
6710
+ } },
6711
+ { label: blocked || partial ? '重试' : '再安装其他 Skills…', run: async () => {
6712
+ if (!pluginId)
6713
+ return;
6714
+ if (blocked || partial) {
6715
+ props.controller.resetPortableFlowToPreview(pluginId, { name: 'portableSkillSelect', pluginId }, true);
6716
+ await beginPortableReview(pluginId, snapshot.portableDraft?.scope ?? 'project');
6717
+ return;
6718
+ }
6719
+ props.controller.resetPortableFlowToPreview(pluginId);
6720
+ await beginPortable(pluginId);
6721
+ } },
6722
+ { label: portableOriginIsDetail ? '返回插件详情' : '返回插件预览', run: () => { if (pluginId)
6723
+ props.controller.resetPortableFlowToPreview(pluginId); } },
6724
+ ] : [
6725
+ { label: portableOriginIsDetail ? '返回插件详情' : '返回插件预览', run: () => {
6726
+ props.controller.discardPortableDraft();
6727
+ leaveResult();
6728
+ } },
6729
+ { label: '再安装其他 Skills…', run: async () => {
6730
+ if (!pluginId)
6731
+ return;
6732
+ props.controller.resetPortableFlowToPreview(pluginId);
6733
+ await beginPortable(pluginId);
6734
+ } },
5132
6735
  ]
5133
- : sourceOperation
5134
- ? [{ label: blocked ? '返回修改' : partial ? '现在启用 / 重试' : '查看来源', run: () => blocked ? props.controller.resumeResultDraft() : leaveResult() }, { label: '返回首页', run: () => requestSessionNavigation('home') }]
5135
- : configOperation && partial
5136
- ? [
5137
- ...(configRetryAvailable ? [{
5138
- label: '重试应用',
5139
- description: '先重新进入 Review,并重新采集当前证据',
5140
- run: () => beginReview({
5141
- kind: 'configSave',
5142
- pluginId: configResultPluginId,
5143
- scope: resultSummary.configScope,
5144
- apply: true,
5145
- retryApply: true,
5146
- }),
5147
- }] : []),
5148
- { label: invalidConfigResult ? '返回设置' : '返回修改', description: '重新打开已保存的设置层', run: () => leaveResult() },
5149
- { label: '返回设置删除字段或重置设置层', description: '显式删除顶层字段或整个设置层', run: () => leaveResult() },
5150
- { label: '保留设置并退出', run: () => requestExit({ code: 1, reason: 'partial' }) },
5151
- ]
5152
- : configOperation && blocked
6736
+ : issueOperation && !dependencyRecovery
6737
+ ? blocked || partial
5153
6738
  ? [
5154
- { label: '返回设置', run: () => props.controller.resumeResultDraft() },
6739
+ { label: '返回问题详情', run: () => blocked
6740
+ ? leaveResult()
6741
+ : failedIssue
6742
+ ? leaveResult({ name: 'issueDetail', issueId: failedIssue.issueId })
6743
+ : pluginId && failedTarget
6744
+ ? leaveResult({ name: 'platform', pluginId, platform: failedTarget.platform })
6745
+ : leaveResult() },
6746
+ { label: '重新检查问题', run: () => leaveResult({ name: 'issueList' }) },
6747
+ ]
6748
+ : [
6749
+ { label: '查看问题列表', run: () => leaveResult({ name: 'issueList' }) },
5155
6750
  { label: '返回首页', run: () => requestSessionNavigation('home') },
5156
6751
  ]
5157
- : [{ label: partial || blockedDependencyFailure ? '查看失败项' : successfulConfigOperation && configOrigin
5158
- ? configOrigin?.name === 'pluginPreview' ? '返回插件预览' : '返回插件详情'
5159
- : configOperation ? '查看插件'
5160
- : action === 'uninstall' || action === 'cleanup' ? '返回插件详情' : '查看插件', run: () => {
5161
- if (successfulConfigOperation && configOrigin) {
5162
- leaveResult();
5163
- return;
5164
- }
5165
- if ((partial || blockedDependencyFailure) && failedPluginId && failedTarget) {
5166
- if (failedIssue)
5167
- leaveResult({ name: 'issueDetail', issueId: failedIssue.issueId });
5168
- else if (failedPluginPlatforms?.includes(failedTarget.platform)) {
5169
- leaveResult({ name: 'platform', pluginId: failedPluginId, platform: failedTarget.platform });
5170
- }
5171
- else if (snapshot.viewModel?.plugins.some((entry) => entry.pluginId === failedPluginId)) {
5172
- leaveResult({ name: 'pluginDetail', pluginId: failedPluginId });
5173
- }
5174
- else
5175
- leaveResult({ name: 'pluginList', mode: 'installed' });
5176
- }
5177
- else if (pluginId && snapshot.viewModel?.plugins.some((entry) => entry.pluginId === pluginId)) {
5178
- leaveResult({ name: 'pluginDetail', pluginId });
5179
- }
5180
- else
5181
- leaveResult({ name: 'pluginList', mode: 'installed' });
5182
- } }, ...((partial || blockedDependencyFailure) && failedPluginId && failedTarget && retryAction ? [{
5183
- label: '重试失败部分…',
5184
- run: () => {
5185
- const issueIntent = failedIssue && retryIssueAction
5186
- ? issueWriteIntent(failedIssue, retryIssueAction)
5187
- : undefined;
5188
- return beginReview(issueIntent ?? {
5189
- kind: 'pluginWrite',
5190
- action: retryAction,
5191
- pluginId: failedPluginId,
5192
- platform: failedTarget.platform,
5193
- scope: failedTarget.scope,
5194
- });
5195
- },
5196
- }] : []), { label: '返回首页', run: () => requestSessionNavigation('home') }];
6752
+ : sourceOperation
6753
+ ? [{
6754
+ label: !blocked && 'returnRoute' in result && result.returnRoute?.name === 'pluginWriteTask'
6755
+ ? '重新准备插件更新' : blocked ? '返回修改' : partial ? '现在启用 / 重试' : '查看来源',
6756
+ run: () => blocked ? props.controller.resumeResultDraft() : leaveResult(),
6757
+ }, { label: '返回首页', run: () => requestSessionNavigation('home') }]
6758
+ : configOperation && partial
6759
+ ? [
6760
+ ...(configRetryAvailable ? [{
6761
+ label: '重试应用',
6762
+ description: '先重新进入 Review,并重新采集当前证据',
6763
+ run: () => beginReview({
6764
+ kind: 'configSave',
6765
+ pluginId: configResultPluginId,
6766
+ scope: resultSummary.configScope,
6767
+ apply: true,
6768
+ retryApply: true,
6769
+ }),
6770
+ }] : []),
6771
+ { label: invalidConfigResult ? '返回设置' : '返回修改', description: '重新打开已保存的设置层', run: () => leaveResult() },
6772
+ { label: '返回设置删除字段或重置设置层', description: '显式删除顶层字段或整个设置层', run: () => leaveResult() },
6773
+ { label: '保留设置并退出', run: () => requestExit({ code: 1, reason: 'partial' }) },
6774
+ ]
6775
+ : configOperation && blocked
6776
+ ? [
6777
+ { label: '返回设置', run: () => props.controller.resumeResultDraft() },
6778
+ { label: '返回首页', run: () => requestSessionNavigation('home') },
6779
+ ]
6780
+ : [{ label: partial || blockedDependencyFailure ? '查看失败项' : successfulConfigOperation && configOrigin
6781
+ ? configOrigin.name === 'pluginPreview' ? '返回插件预览'
6782
+ : configOrigin.name === 'pluginWriteTask' ? '返回恢复任务' : '返回插件详情'
6783
+ : configOperation ? '查看插件'
6784
+ : action === 'uninstall' || action === 'cleanup' ? '返回插件详情' : '查看插件', run: () => {
6785
+ if (successfulConfigOperation && configOrigin) {
6786
+ leaveResult();
6787
+ return;
6788
+ }
6789
+ if ((partial || blockedDependencyFailure) && failedPluginId && failedTarget) {
6790
+ if (failedIssue)
6791
+ leaveResult({ name: 'issueDetail', issueId: failedIssue.issueId });
6792
+ else if (failedPluginPlatforms?.includes(failedTarget.platform)) {
6793
+ leaveResult({ name: 'platform', pluginId: failedPluginId, platform: failedTarget.platform });
6794
+ }
6795
+ else if (snapshot.viewModel?.plugins.some((entry) => entry.pluginId === failedPluginId)) {
6796
+ leaveResult({ name: 'pluginDetail', pluginId: failedPluginId });
6797
+ }
6798
+ else
6799
+ leaveResult({ name: 'pluginList', mode: 'installed' });
6800
+ }
6801
+ else if (pluginId && snapshot.viewModel?.plugins.some((entry) => entry.pluginId === pluginId)) {
6802
+ leaveResult({ name: 'pluginDetail', pluginId });
6803
+ }
6804
+ else
6805
+ leaveResult({ name: 'pluginList', mode: 'installed' });
6806
+ } }, ...((partial || blockedDependencyFailure) && failedPluginId && failedTarget && retryAction ? [{
6807
+ label: '重试失败部分…',
6808
+ run: () => {
6809
+ const issueIntent = failedIssue && retryIssueAction
6810
+ ? issueWriteIntent(failedIssue, retryIssueAction)
6811
+ : undefined;
6812
+ return beginReview(issueIntent ?? {
6813
+ kind: 'pluginWrite',
6814
+ action: retryAction,
6815
+ pluginId: failedPluginId,
6816
+ platform: failedTarget.platform,
6817
+ scope: failedTarget.scope,
6818
+ });
6819
+ },
6820
+ }] : []), { label: '返回首页', run: () => requestSessionNavigation('home') }];
5197
6821
  const visibleMessages = resultSummary?.messages?.slice(policy.mode === 'compact' ? -2 : -4) ?? [];
5198
6822
  const visibleChanges = resultSummary?.changes?.slice(0, policy.mode === 'compact' ? 2 : 4) ?? [];
5199
6823
  const resultTargetName = (target) => target.pluginId
@@ -5254,11 +6878,68 @@ export function ConsoleAppV4(props) {
5254
6878
  : resultSummary?.resetDisposition === 'quarantinedDirectory'
5255
6879
  ? `设置路径是目录;已隔离为 ${resultSummary.quarantineBasename ?? '安全隔离目录'}`
5256
6880
  : undefined;
5257
- if (batchOperation || resultTargets.length > resultTargetLimit
6881
+ const taskRootResults = taskSummary?.rootResults ?? [];
6882
+ const blockedTaskTargets = new Map((taskSummary?.blockedTargets ?? []).map((entry) => [
6883
+ [entry.target.pluginId, entry.target.platform, entry.target.scope].join('\0'), entry,
6884
+ ]));
6885
+ const taskRootState = (state) => {
6886
+ if (state === 'completed')
6887
+ return { label: '完成', status: 'success' };
6888
+ if (state === 'kept')
6889
+ return { label: '无需更改', status: 'success' };
6890
+ if (state === 'partial')
6891
+ return { label: '部分完成', status: 'warning' };
6892
+ if (state === 'failed')
6893
+ return { label: '失败', status: 'error' };
6894
+ if (state === 'notStarted')
6895
+ return { label: '未开始', status: 'muted' };
6896
+ return { label: '待确认', status: 'warning' };
6897
+ };
6898
+ const taskResultRows = taskRootResults.flatMap((root) => {
6899
+ const rootState = taskRootState(root.state);
6900
+ const rootName = props.controller.getPluginOverview(root.pluginId)?.displayName
6901
+ ?? root.pluginId.split('/').at(-1);
6902
+ const requestedTargets = taskSummary?.requestedTargets.filter((target) => target.pluginId === root.pluginId) ?? [];
6903
+ return [
6904
+ _jsx(ResultRow, { title: rootName, description: `${requestedTargets.length} 个精确安装位置`, meta: rootState.label, status: rootState.status, compact: true }, `root:${root.pluginId}`),
6905
+ ...requestedTargets.map((target) => {
6906
+ const key = [target.pluginId, target.platform, target.scope].join('\0');
6907
+ const blockedTarget = blockedTaskTargets.get(key);
6908
+ const targetResult = root.targets.find((entry) => entry.pluginId === target.pluginId && entry.platform === target.platform
6909
+ && entry.scope === target.scope);
6910
+ const targetState = blockedTarget ? { label: '待确认', status: 'warning' }
6911
+ : targetResult?.state === 'completed' ? { label: '完成', status: 'success' }
6912
+ : targetResult?.state === 'kept' ? { label: '无需更改', status: 'success' }
6913
+ : targetResult?.state === 'failed' ? { label: '失败', status: 'error' }
6914
+ : { label: '未开始', status: 'muted' };
6915
+ return _jsx(ResultRow, { title: `${platformLabel(target.platform)} · ${scopeLabel(target.scope)}`, description: blockedTarget
6916
+ ? pluginWriteTaskBlockLabel(blockedTarget.reason)
6917
+ : targetResult?.errorCode, meta: targetState.label, status: targetState.status, compact: !blockedTarget }, `target:${key}`);
6918
+ }),
6919
+ ];
6920
+ });
6921
+ const batchResultRows = (resultSummary?.batch?.rootResults ?? []).map((root) => {
6922
+ const rootState = taskRootState(root.state);
6923
+ const rootName = props.controller.getPluginOverview(root.pluginId)?.displayName
6924
+ ?? root.pluginId.split('/').at(-1);
6925
+ return _jsx(ResultRow, { title: rootName, description: `${root.targets.length} 个精确安装位置`, meta: rootState.label, status: rootState.status, compact: true }, `batch-root:${root.pluginId}`);
6926
+ }).concat(prioritizedTargets.map((target) => {
6927
+ const state = operationTargetStateLabel(target.state);
6928
+ const relation = target.relation === 'requested' ? '你选择的插件'
6929
+ : target.relation === 'direct' ? '直接依赖'
6930
+ : target.relation === 'transitive' ? '间接依赖' : undefined;
6931
+ return _jsx(ResultRow, { title: `${resultTargetName(target)} · ${platformLabel(target.platform)} · ${scopeLabel(target.scope)}`, description: relation, meta: state.label, status: state.status ?? 'muted', compact: true }, `batch-target:${target.pluginId ?? ''}:${target.platform}:${target.scope}`);
6932
+ }));
6933
+ const taskResultLimit = Math.max(1, pageActionBudget(5) - resultActionViewport.limit);
6934
+ const rootResultRows = taskOperation ? taskResultRows : batchResultRows;
6935
+ const taskResultOffset = Math.min(batchTargetOffset, Math.max(0, rootResultRows.length - 1));
6936
+ const visibleTaskResultRows = rootResultRows.slice(taskResultOffset, taskResultOffset + taskResultLimit);
6937
+ if (taskOperation || batchOperation || resultTargets.length > resultTargetLimit
5258
6938
  || prioritizedPortableSkills.length > portableSkillLimit) {
5259
6939
  help = 'PgUp/PgDn 查看全部项目 · ↑↓ 移动 · Enter 选择 · Esc 返回';
5260
6940
  }
5261
- content = portableOperation ? _jsxs(Box, { flexDirection: "column", children: [portableTechnicalSummary ? _jsxs(_Fragment, { children: [portableSkillOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${portableSkillOffset} 个 Skills`, status: "muted", compact: true }) : null, portableRows, hiddenPortableSkills > 0 ? _jsx(StaticRow, { title: `另有 ${hiddenPortableSkills} 个 Skills`, status: "muted", compact: true }) : null, portableReceipt?.recovery ? _jsx(StaticRow, { title: "\u5B89\u5168\u6062\u590D", meta: portableReceipt.recovery, status: "warning", compact: true }) : null, _jsx(TechnicalDetails, { children: compactTechnicalRows })] }) : _jsx(_Fragment, { children: _jsxs(ResultCard, { status: resultStatus, title: blocked ? '安装未开始或结果未确认' : partial ? '部分 Skills 未确认' : '通用 Skills 安装结果', children: [portableSkillOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${portableSkillOffset} 个 Skills`, status: "muted", compact: true }) : null, portableRows, hiddenPortableSkills > 0 ? _jsx(StaticRow, { title: `另有 ${hiddenPortableSkills} 个 Skills`, status: "muted", compact: true }) : null, portableReceipt ? _jsx(StaticRow, { title: `Agent:${portableReceipt.agentMode === 'automatic' ? 'Skills CLI 默认选择' : portableReceipt.requestedAgents.join('、')}`, meta: `范围:${scopeLabel(portableReceipt.scope)}`, compact: true }) : null, portableReceipt?.recovery ? policy.mode === 'compact'
6941
+ content = catalogManagementOperation ? _jsxs(Box, { flexDirection: "column", children: [_jsxs(ResultCard, { status: resultStatus, title: resultSummary?.message ?? 'Catalog 管理结果', children: [catalogManagementResult?.catalogVersion ? _jsx(StaticRow, { title: "Catalog \u7248\u672C", meta: catalogManagementResult.catalogVersion, compact: true }) : null, catalogManagementResult?.store ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: "\u5B9E\u9645\u5220\u9664", meta: `${catalogManagementResult.store.deletedEntries} 项 · ${catalogManagementResult.store.deletedBytes} bytes`, status: "success", compact: true }), _jsx(StaticRow, { title: "\u5B9E\u9645\u4FDD\u7559", meta: `${catalogManagementResult.store.retainedEntries} 项`, compact: true })] }) : null, catalogManagementResult?.publication ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: "\u53D1\u5E03\u72B6\u6001", meta: catalogManagementResult.publication.state, status: catalogManagementResult.publication.state === 'uncertain' ? 'warning' : 'success', compact: true }), _jsx(StaticRow, { title: "\u76EE\u6807", meta: `${catalogManagementResult.publication.remote} · ${catalogManagementResult.publication.branch}`, compact: true }), _jsx(StaticRow, { title: "\u8BA1\u5212\u6807\u8BC6", meta: catalogManagementResult.publication.planDigest.slice(0, 15), compact: true })] }) : catalogManagementResult?.planDigest ? _jsx(StaticRow, { title: "\u8BA1\u5212\u5DF2\u51C6\u5907", meta: catalogManagementResult.planDigest.slice(0, 15), status: "success", compact: true }) : null, visibleMessages.map((message, index) => _jsx(StaticRow, { title: productResultMessage(message), status: "muted", compact: true }, `${index}:${message}`))] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: resultActionViewport.offset, limit: resultActionViewport.limit })] }) : taskOperation || batchOperation ? _jsxs(Box, { flexDirection: "column", children: [_jsxs(ResultCard, { status: resultStatus, title: batchOperation ? '根插件与目标结果' : '根插件结果', children: [taskResultOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${taskResultOffset} 项`, status: "muted", compact: true }) : null, visibleTaskResultRows, rootResultRows.length > taskResultOffset + visibleTaskResultRows.length
6942
+ ? _jsx(StaticRow, { title: "\u2193 \u540E\u9762\u8FD8\u6709", meta: `${rootResultRows.length - taskResultOffset - visibleTaskResultRows.length} 项`, status: "muted", compact: true }) : null] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: resultActionViewport.offset, limit: resultActionViewport.limit })] }) : portableOperation ? _jsxs(Box, { flexDirection: "column", children: [portableTechnicalSummary ? _jsxs(_Fragment, { children: [portableSkillOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${portableSkillOffset} 个 Skills`, status: "muted", compact: true }) : null, portableRows, hiddenPortableSkills > 0 ? _jsx(StaticRow, { title: `另有 ${hiddenPortableSkills} 个 Skills`, status: "muted", compact: true }) : null, portableReceipt?.recovery ? _jsx(StaticRow, { title: "\u5B89\u5168\u6062\u590D", meta: portableReceipt.recovery, status: "warning", compact: true }) : null, _jsx(TechnicalDetails, { children: compactTechnicalRows })] }) : _jsx(_Fragment, { children: _jsxs(ResultCard, { status: resultStatus, title: blocked ? '安装未开始或结果未确认' : partial ? '部分 Skills 未确认' : '通用 Skills 安装结果', children: [portableSkillOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${portableSkillOffset} 个 Skills`, status: "muted", compact: true }) : null, portableRows, hiddenPortableSkills > 0 ? _jsx(StaticRow, { title: `另有 ${hiddenPortableSkills} 个 Skills`, status: "muted", compact: true }) : null, portableReceipt ? _jsx(StaticRow, { title: `Agent:${portableReceipt.agentMode === 'automatic' ? 'Skills CLI 默认选择' : portableReceipt.requestedAgents.join('、')}`, meta: `范围:${scopeLabel(portableReceipt.scope)}`, compact: true }) : null, portableReceipt?.recovery ? policy.mode === 'compact'
5262
6943
  ? _jsx(StaticRow, { title: "\u5B89\u5168\u6062\u590D", meta: portableReceipt.recovery, status: "warning", compact: true })
5263
6944
  : _jsx(Callout, { status: "warning", title: "\u5B89\u5168\u6062\u590D", description: portableReceipt.recovery }) : null, showTechnical && portableReceipt ? _jsxs(TechnicalDetails, { children: [portableReceipt.failureCode ? _jsx(StaticRow, { title: "failure code", meta: portableReceipt.failureCode, compact: true }) : null, _jsx(StaticRow, { title: "verification missing", meta: String(portableReceipt.missing.length), compact: true }), _jsx(StaticRow, { title: "Skills CLI version", meta: portableReceipt.skillsCliVersion, compact: true }), partial || blocked
5264
6945
  ? _jsx(StaticRow, { title: "\u53E6\u6709\u6280\u672F\u5B57\u6BB5\u5DF2\u6298\u53E0", meta: "materialization digest \u00B7 elapsed", status: "muted", compact: true })
@@ -5311,11 +6992,14 @@ export function ConsoleAppV4(props) {
5311
6992
  switch (route.name) {
5312
6993
  case 'pluginPreview':
5313
6994
  case 'pluginDependencies':
6995
+ case 'pluginDependencyNode':
5314
6996
  case 'installPlacement':
5315
6997
  case 'installTargets':
5316
6998
  case 'dependencyPlacement':
5317
6999
  case 'dependencyPlacementEditor':
7000
+ case 'pluginWriteTask':
5318
7001
  case 'pluginDetail':
7002
+ case 'pluginMore':
5319
7003
  case 'platform':
5320
7004
  case 'pluginAbout':
5321
7005
  case 'pluginTechnical':
@@ -5343,6 +7027,7 @@ export function ConsoleAppV4(props) {
5343
7027
  case 'sourceAdd':
5344
7028
  case 'sourceSave':
5345
7029
  case 'scriptTimeout':
7030
+ case 'catalogWorkspace':
5346
7031
  case 'reviewPreparing':
5347
7032
  case 'review':
5348
7033
  case 'result':
@@ -5389,6 +7074,22 @@ export function ConsoleAppV4(props) {
5389
7074
  const targetActionSignature = route.name === 'installTargets' && route.view !== 'blocked'
5390
7075
  ? actions.map((action) => String(Boolean(action.disabled))).join(',')
5391
7076
  : '';
7077
+ const batchSelectionActionSignature = route.name === 'batchSelection'
7078
+ && batchDraft?.stage === 'select'
7079
+ ? actions.map((action) => action.pluginId ?? action.label).join('\0')
7080
+ : '';
7081
+ useEffect(() => {
7082
+ if (!batchSelectionActionSignature)
7083
+ return;
7084
+ const lastActionIndex = Math.max(0, actions.length - 1);
7085
+ if (focus > lastActionIndex) {
7086
+ setFocus(lastActionIndex);
7087
+ setScrollOffset(lastActionIndex);
7088
+ return;
7089
+ }
7090
+ if (scrollOffset > lastActionIndex)
7091
+ setScrollOffset(focus);
7092
+ }, [batchSelectionActionSignature, focus, scrollOffset]);
5392
7093
  useEffect(() => {
5393
7094
  if (!targetActionSignature || !actions[focus]?.disabled)
5394
7095
  return;
@@ -5475,6 +7176,10 @@ export function ConsoleAppV4(props) {
5475
7176
  setObjectError(undefined);
5476
7177
  setSourceDraft(undefined);
5477
7178
  setBatchDraft(undefined);
7179
+ setCatalogInput('');
7180
+ setCatalogWorkspaceDraft({ ownerName: '', ownerUrl: '', displayName: '', catalogVersion: '0.1.0' });
7181
+ setCatalogPublishDraft({ targetKind: 'remote', target: '', branch: 'catalog' });
7182
+ setCatalogDevSourceId('');
5478
7183
  }
5479
7184
  function continueEditingFromSessionNavigation() {
5480
7185
  if (props.pendingSessionNavigation)
@@ -5491,6 +7196,9 @@ export function ConsoleAppV4(props) {
5491
7196
  setOverlayFocus(0);
5492
7197
  setOverlay('sessionNavigation');
5493
7198
  }, [props.pendingSessionNavigation, props.pendingSessionNavigationDrafts]);
7199
+ const normalizedEffectiveFocus = route.name === 'batchSelection' && batchDraft?.stage === 'select'
7200
+ ? Math.min(effectiveFocus, Math.max(0, actions.length - 1))
7201
+ : effectiveFocus;
5494
7202
  useInput((input, key) => {
5495
7203
  if (!overlay && route.name === 'reviewPreparing' && snapshot.reviewPreparation?.state === 'failed'
5496
7204
  && (key.escape || input.toLowerCase() === 'b')) {
@@ -5658,6 +7366,14 @@ export function ConsoleAppV4(props) {
5658
7366
  setReportSaveError(undefined);
5659
7367
  return;
5660
7368
  }
7369
+ if ((key.pageUp || key.pageDown)
7370
+ && route.name === 'pluginWriteTask') {
7371
+ const total = props.controller.listPluginWriteTaskCandidates(route.goal)
7372
+ .filter((candidate) => candidate.target.pluginId === route.pluginId
7373
+ && (!route.targetKeys || route.targetKeys.includes(pluginWriteTaskTargetKey(candidate)))).length;
7374
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - 1), Math.max(0, value + (key.pageUp ? -1 : 1))));
7375
+ return;
7376
+ }
5661
7377
  if ((key.pageUp || key.pageDown)
5662
7378
  && route.name === 'review'
5663
7379
  && snapshot.activeReview) {
@@ -5669,8 +7385,10 @@ export function ConsoleAppV4(props) {
5669
7385
  if ((key.pageUp || key.pageDown)
5670
7386
  && route.name === 'result'
5671
7387
  && snapshot.result) {
5672
- const total = 'summary' in snapshot.result
5673
- ? Math.max(snapshot.result.summary?.targets?.length ?? 0, snapshot.result.operationKind === 'portableInstall'
7388
+ const pageSummary = 'summary' in snapshot.result ? snapshot.result.summary : undefined;
7389
+ const total = pageSummary
7390
+ ? Math.max(pageSummary.targets?.length ?? 0, pageSummary.task?.rootResults.reduce((count, root) => count + 1 + pageSummary.task.requestedTargets.filter((target) => target.pluginId === root.pluginId).length, 0) ?? 0, (pageSummary.batch?.rootResults.length ?? 0)
7391
+ + (pageSummary.targets?.length ?? 0), snapshot.result.operationKind === 'portableInstall'
5674
7392
  ? snapshot.portableResultSkills?.length ?? 0
5675
7393
  : 0)
5676
7394
  : 0;
@@ -5705,6 +7423,15 @@ export function ConsoleAppV4(props) {
5705
7423
  setBatchTargetOffset((value) => Math.min(Math.max(0, total - pageRows), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5706
7424
  return;
5707
7425
  }
7426
+ if ((key.pageUp || key.pageDown) && (route.name === 'catalogStore'
7427
+ || route.name === 'catalogPublish' && route.step === 'overview')) {
7428
+ const total = route.name === 'catalogStore'
7429
+ ? catalogStore?.entries.length ?? 0
7430
+ : catalogPublication?.records.length ?? 0;
7431
+ const pageRows = responsiveBudget(3, 7);
7432
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - pageRows), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
7433
+ return;
7434
+ }
5708
7435
  if ((key.pageUp || key.pageDown) && route.name === 'pluginDependencies' && plugin) {
5709
7436
  const overview = dependencyOverview(plugin);
5710
7437
  const dependents = route.view === 'removeBlocked' && route.platform && route.scope
@@ -5875,6 +7602,180 @@ export function ConsoleAppV4(props) {
5875
7602
  setSourceInputError(undefined);
5876
7603
  return;
5877
7604
  }
7605
+ if (route.name === 'catalogWorkspace' && (route.view === 'select'
7606
+ || route.view === 'create' || route.view === 'version' && route.step === 'catalogVersion')) {
7607
+ if (key.escape) {
7608
+ if (route.view === 'create') {
7609
+ const step = route.step ?? 'ownerName';
7610
+ const next = { ...catalogWorkspaceDraft, [step]: catalogInput };
7611
+ setCatalogWorkspaceDraft(next);
7612
+ const previous = { ownerUrl: 'ownerName', displayName: 'ownerUrl', catalogVersion: 'displayName' }[step];
7613
+ if (previous) {
7614
+ setCatalogInput(next[previous]);
7615
+ props.controller.replace({ ...route, step: previous });
7616
+ }
7617
+ else
7618
+ back();
7619
+ }
7620
+ else
7621
+ back();
7622
+ return;
7623
+ }
7624
+ if (key.return) {
7625
+ const value = catalogInput.trim();
7626
+ setCatalogInputError(undefined);
7627
+ if (route.view === 'select') {
7628
+ setCatalogLoading(true);
7629
+ void props.controller.selectCatalogWorkspace(value || undefined).then((workspace) => {
7630
+ setCatalogWorkspace(workspace);
7631
+ props.controller.replace({ name: 'catalogWorkspace', view: 'overview' });
7632
+ setFocus(0);
7633
+ setScrollOffset(0);
7634
+ setCatalogInput('');
7635
+ }).catch((error) => setCatalogInputError(sanitizeDiagnosticText(error instanceof Error ? error.message : String(error), [], [catalogInput]))).finally(() => setCatalogLoading(false));
7636
+ return;
7637
+ }
7638
+ if (route.view === 'version') {
7639
+ if (!/^\d+\.\d+\.\d+(?:-beta\.\d+)?$/u.test(value)) {
7640
+ setCatalogInputError('版本必须是 x.y.z 或精确 x.y.z-beta.N。');
7641
+ return;
7642
+ }
7643
+ beginReview({ kind: 'catalogManagement', action: 'workspaceVersion', version: value }, { name: 'catalogWorkspace', view: 'overview' });
7644
+ return;
7645
+ }
7646
+ const step = route.step ?? 'ownerName';
7647
+ if (!value) {
7648
+ setCatalogInputError('此字段不能为空。');
7649
+ return;
7650
+ }
7651
+ if (step === 'ownerUrl') {
7652
+ try {
7653
+ const url = new URL(value);
7654
+ if (url.protocol !== 'https:' && url.protocol !== 'http:')
7655
+ throw new Error();
7656
+ }
7657
+ catch {
7658
+ setCatalogInputError('请输入有效的 HTTP 或 HTTPS owner URL。');
7659
+ return;
7660
+ }
7661
+ }
7662
+ if (step === 'catalogVersion' && !/^\d+\.\d+\.\d+(?:-beta\.\d+)?$/u.test(value)) {
7663
+ setCatalogInputError('版本必须是 x.y.z 或精确 x.y.z-beta.N。');
7664
+ return;
7665
+ }
7666
+ const next = { ...catalogWorkspaceDraft, [step]: value };
7667
+ setCatalogWorkspaceDraft(next);
7668
+ setCatalogInput('');
7669
+ if (step === 'ownerName')
7670
+ props.controller.replace({ ...route, step: 'ownerUrl' });
7671
+ else if (step === 'ownerUrl')
7672
+ props.controller.replace({ ...route, step: 'displayName' });
7673
+ else if (step === 'displayName') {
7674
+ setCatalogInput('0.1.0');
7675
+ props.controller.replace({ ...route, step: 'catalogVersion' });
7676
+ }
7677
+ else
7678
+ beginReview({ kind: 'catalogManagement', action: 'workspaceInit', ...next }, { name: 'catalogWorkspace', view: 'overview' });
7679
+ return;
7680
+ }
7681
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl))
7682
+ setCatalogInput('');
7683
+ else if (key.backspace || key.delete)
7684
+ setCatalogInput(removeLastGrapheme);
7685
+ else if (input && !key.ctrl && !key.meta && input >= ' ')
7686
+ setCatalogInput((value) => value + input);
7687
+ setCatalogInputError(undefined);
7688
+ return;
7689
+ }
7690
+ if (route.name === 'catalogDev' && route.step && route.step !== 'overview') {
7691
+ if (key.escape) {
7692
+ back();
7693
+ return;
7694
+ }
7695
+ if (key.return) {
7696
+ const value = catalogInput.trim();
7697
+ if (value && !CATALOG_SOURCE_ID_PATTERN.test(value)) {
7698
+ setCatalogInputError('格式必须是小写字母、数字或连字符,最多 32 字符。');
7699
+ return;
7700
+ }
7701
+ if (route.step === 'sourceId') {
7702
+ setCatalogDevSourceId(value);
7703
+ back();
7704
+ }
7705
+ else {
7706
+ if (!value) {
7707
+ setCatalogInputError('请输入替代主来源 ID。');
7708
+ return;
7709
+ }
7710
+ beginReview({ kind: 'catalogManagement', action: 'devDeactivate', replacementPrimary: value }, { name: 'catalogDev' });
7711
+ }
7712
+ return;
7713
+ }
7714
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl))
7715
+ setCatalogInput('');
7716
+ else if (key.backspace || key.delete)
7717
+ setCatalogInput(removeLastGrapheme);
7718
+ else if (input && !key.ctrl && !key.meta && input >= ' ')
7719
+ setCatalogInput((value) => value + input);
7720
+ setCatalogInputError(undefined);
7721
+ return;
7722
+ }
7723
+ if (route.name === 'catalogPublish' && route.step !== 'overview') {
7724
+ if (key.escape) {
7725
+ if (route.step === 'branch') {
7726
+ setCatalogPublishDraft((draft) => ({ ...draft, branch: catalogInput.trim() || 'catalog' }));
7727
+ setCatalogInput(catalogPublishDraft.target);
7728
+ props.controller.replace({ name: 'catalogPublish', step: 'target' });
7729
+ }
7730
+ else {
7731
+ const value = catalogInput.trim();
7732
+ const targetKind = value.includes('://') || /^[^@\s]+@[^:\s]+:/u.test(value) || value.startsWith('/')
7733
+ ? 'remoteUrl' : 'remote';
7734
+ setCatalogPublishDraft((draft) => ({ ...draft, targetKind, target: value }));
7735
+ back();
7736
+ }
7737
+ return;
7738
+ }
7739
+ if (key.return) {
7740
+ const value = catalogInput.trim();
7741
+ if (route.step === 'target') {
7742
+ if (/\s/u.test(value)) {
7743
+ setCatalogInputError('remote 名称或 URL 不能包含空白。');
7744
+ return;
7745
+ }
7746
+ const targetKind = value.includes('://') || /^[^@\s]+@[^:\s]+:/u.test(value) || value.startsWith('/')
7747
+ ? 'remoteUrl' : 'remote';
7748
+ setCatalogPublishDraft((draft) => ({ ...draft, targetKind, target: value }));
7749
+ setCatalogInput(catalogPublishDraft.expectedPlanDigest ? catalogPublishDraft.branch : 'catalog');
7750
+ props.controller.replace({ name: 'catalogPublish', step: 'branch' });
7751
+ }
7752
+ else {
7753
+ const branch = value || 'catalog';
7754
+ if (/\s|\.\.|[~^:?*\[\\]/u.test(branch) || branch.startsWith('-') || branch.endsWith('/')) {
7755
+ setCatalogInputError('请输入安全的 Git branch 名称。');
7756
+ return;
7757
+ }
7758
+ const draft = { ...catalogPublishDraft, branch };
7759
+ setCatalogPublishDraft(draft);
7760
+ const target = draft.target
7761
+ ? draft.targetKind === 'remote' ? { remote: draft.target } : { remoteUrl: draft.target }
7762
+ : {};
7763
+ beginReview(draft.expectedPlanDigest
7764
+ ? { kind: 'catalogManagement', action: 'publishApply', expectedPlanDigest: draft.expectedPlanDigest,
7765
+ ...target, branch }
7766
+ : { kind: 'catalogManagement', action: 'publishPlan', ...target, branch }, { name: 'catalogPublish', step: 'overview' });
7767
+ }
7768
+ return;
7769
+ }
7770
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl))
7771
+ setCatalogInput('');
7772
+ else if (key.backspace || key.delete)
7773
+ setCatalogInput(removeLastGrapheme);
7774
+ else if (input && !key.ctrl && !key.meta && input >= ' ')
7775
+ setCatalogInput((value) => value + input);
7776
+ setCatalogInputError(undefined);
7777
+ return;
7778
+ }
5878
7779
  if (route.name === 'configFieldEditor'
5879
7780
  && (route.editor === 'string' || route.editor === 'number')
5880
7781
  && ((activeField?.choices?.length ?? 0) === 0 || route.mode === 'custom')) {
@@ -5982,18 +7883,22 @@ export function ConsoleAppV4(props) {
5982
7883
  setInputError(undefined);
5983
7884
  return;
5984
7885
  }
5985
- if (searchFocus && route.name === 'pluginList') {
7886
+ if (searchFocus && (route.name === 'pluginList' || route.name === 'batchSelection')) {
5986
7887
  if (key.escape) {
5987
7888
  setSearchFocus(false);
5988
7889
  return;
5989
7890
  }
5990
7891
  if (key.return) {
5991
7892
  setSearchFocus(false);
5992
- const row = pluginRows[focus];
5993
- if (row)
5994
- go(route.mode === 'browse'
5995
- ? { name: 'pluginPreview', pluginId: row.pluginId }
5996
- : { name: 'pluginDetail', pluginId: row.pluginId });
7893
+ if (route.name === 'pluginList') {
7894
+ const row = pluginRows[focus];
7895
+ if (row?.menuAction)
7896
+ row.menuAction();
7897
+ else if (row)
7898
+ go(route.mode === 'browse'
7899
+ ? { name: 'pluginPreview', pluginId: row.pluginId }
7900
+ : { name: 'pluginDetail', pluginId: row.pluginId });
7901
+ }
5997
7902
  return;
5998
7903
  }
5999
7904
  if (key.upArrow || key.downArrow) {
@@ -6004,21 +7909,44 @@ export function ConsoleAppV4(props) {
6004
7909
  if (key.leftArrow || key.rightArrow) {
6005
7910
  setSearchFocus(false);
6006
7911
  const tabs = ['all', ...props.controller.listSources().map((source) => source.id)];
6007
- const current = route.sourceId ?? 'all';
7912
+ const current = route.name === 'pluginList' ? route.sourceId ?? 'all' : batchDraft?.sourceId ?? 'all';
6008
7913
  const direction = key.leftArrow ? -1 : 1;
6009
7914
  const next = tabs[(tabs.indexOf(current) + direction + tabs.length) % tabs.length];
6010
- props.controller.replace({ name: 'pluginList', mode: route.mode, ...(next !== 'all' ? { sourceId: next } : {}) }, { ...snapshot.router.current.context, query, installFilter, sourceTabId: next });
7915
+ if (route.name === 'pluginList') {
7916
+ props.controller.replace({ name: 'pluginList', mode: route.mode, ...(next !== 'all' ? { sourceId: next } : {}) }, { ...snapshot.router.current.context, query, installFilter, sourceTabId: next });
7917
+ }
7918
+ else {
7919
+ setBatchDraft((draft) => draft ? {
7920
+ ...draft,
7921
+ ...(next === 'all' ? { sourceId: undefined } : { sourceId: next }),
7922
+ showSelected: false,
7923
+ } : draft);
7924
+ }
6011
7925
  setFocus(0);
6012
7926
  setScrollOffset(0);
6013
7927
  return;
6014
7928
  }
6015
- if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl))
6016
- setQuery('');
6017
- else if (key.backspace || key.delete)
6018
- setQuery(removeLastGrapheme);
6019
- else if (input && !key.ctrl && !key.meta && input >= ' ')
6020
- setQuery((value) => value + input);
6021
- setFocus(0);
7929
+ if (route.name === 'pluginList') {
7930
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl))
7931
+ setQuery('');
7932
+ else if (key.backspace || key.delete)
7933
+ setQuery(removeLastGrapheme);
7934
+ else if (input && !key.ctrl && !key.meta && input >= ' ')
7935
+ setQuery((value) => value + input);
7936
+ }
7937
+ else {
7938
+ setBatchDraft((draft) => draft ? {
7939
+ ...draft,
7940
+ query: input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl)
7941
+ ? ''
7942
+ : key.backspace || key.delete
7943
+ ? removeLastGrapheme(draft.query)
7944
+ : input && !key.ctrl && !key.meta && input >= ' '
7945
+ ? draft.query + input : draft.query,
7946
+ showSelected: false,
7947
+ } : draft);
7948
+ }
7949
+ setFocus(route.name === 'batchSelection' ? 1 : 0);
6022
7950
  setScrollOffset(0);
6023
7951
  return;
6024
7952
  }
@@ -6028,6 +7956,36 @@ export function ConsoleAppV4(props) {
6028
7956
  || route.name === 'configObjectPropertyEditor')) {
6029
7957
  return;
6030
7958
  }
7959
+ if (route.name === 'pluginDependencies' && route.view !== 'removeBlocked'
7960
+ && (key.leftArrow || key.rightArrow)) {
7961
+ const projection = buildDependencyGraphProjection({
7962
+ overviews: props.controller.listPluginDependencyOverviews(),
7963
+ rootPluginId: route.pluginId,
7964
+ direction: route.direction ?? 'dependencies',
7965
+ ...(expandedDependencyPluginIds ? { expandedPluginIds: expandedDependencyPluginIds } : {}),
7966
+ });
7967
+ const row = projection.rows[focus];
7968
+ if (!row)
7969
+ return;
7970
+ const next = new Set(expandedDependencyPluginIds ?? projection.defaultExpandedPluginIds);
7971
+ if (key.rightArrow && row.canExpand)
7972
+ next.add(row.pluginId);
7973
+ if (key.leftArrow && row.canExpand)
7974
+ next.delete(row.pluginId);
7975
+ const nextProjection = buildDependencyGraphProjection({
7976
+ overviews: props.controller.listPluginDependencyOverviews(),
7977
+ rootPluginId: route.pluginId,
7978
+ direction: route.direction ?? 'dependencies',
7979
+ expandedPluginIds: next,
7980
+ });
7981
+ const nextFocus = Math.max(0, nextProjection.rows.findIndex((candidate) => candidate.id === row.id));
7982
+ const nextScroll = Math.min(scrollOffset, nextFocus);
7983
+ setExpandedDependencyPluginIds(next);
7984
+ setFocus(nextFocus);
7985
+ setScrollOffset(nextScroll);
7986
+ persistDependencyGraphContext(route, nextProjection, next, nextFocus, nextScroll);
7987
+ return;
7988
+ }
6031
7989
  const sessionNavigation = resolveTuiSessionNavigationKey({ input, key }, { characterInput: isCharacterInputMode(route, searchFocus) });
6032
7990
  const globalAction = resolveTuiGlobalKeyAction({ input, key }, { mode: 'menu', allowBack: true });
6033
7991
  if (sessionNavigation) {
@@ -6071,7 +8029,8 @@ export function ConsoleAppV4(props) {
6071
8029
  back();
6072
8030
  }
6073
8031
  else if (globalAction === 'back') {
6074
- if (dirty && route.name !== 'review') {
8032
+ if (dirty && route.name !== 'review'
8033
+ && !(batchDraft && (route.name === 'pluginPreview' || route.name === 'pluginDetail'))) {
6075
8034
  setOverlayFocus(0);
6076
8035
  setDiscardForExit(false);
6077
8036
  setOverlay('discard');
@@ -6082,9 +8041,11 @@ export function ConsoleAppV4(props) {
6082
8041
  else if (route.name === 'pluginList' && input === '/') {
6083
8042
  setSearchFocus(true);
6084
8043
  }
6085
- else if (route.name === 'pluginList' && route.mode === 'browse' && input.toLowerCase() === 'f') {
8044
+ else if (route.name === 'pluginList' && input.toLowerCase() === 'f') {
6086
8045
  const selectedPluginId = pluginRows[focus]?.pluginId;
6087
- const nextFilter = installFilter === 'all' ? 'uninstalled' : 'all';
8046
+ const nextFilter = installFilter === 'all'
8047
+ ? route.mode === 'browse' ? 'uninstalled' : 'updateAvailable'
8048
+ : 'all';
6088
8049
  const nextItems = props.controller.listPlugins({
6089
8050
  mode: route.mode,
6090
8051
  ...(route.sourceId ? { sourceId: route.sourceId } : {}),
@@ -6096,44 +8057,68 @@ export function ConsoleAppV4(props) {
6096
8057
  : -1;
6097
8058
  setInstallFilter(nextFilter);
6098
8059
  props.controller.updatePageContext({ installFilter: nextFilter });
6099
- setFocus(retained >= 0 ? retained : 0);
8060
+ setFocus(retained >= 0 ? pluginListMenuCount(route.mode, nextItems) + retained : 0);
6100
8061
  setScrollOffset(0);
6101
8062
  }
6102
8063
  else if (route.name === 'pluginList' && input.toLowerCase() === 'm') {
6103
8064
  openBatchSelection(route.mode === 'browse' ? 'install' : undefined);
6104
8065
  }
6105
8066
  else if (route.name === 'pluginList' && route.mode === 'installed' && input.toLowerCase() === 'a') {
6106
- openBatchSelection();
6107
- setBatchDraft({
6108
- action: 'sync',
6109
- selection: 'currentProjectIntentRoots',
6110
- selectedPluginIds: [],
6111
- platform: 'both',
6112
- scope: 'project',
6113
- });
6114
- props.controller.push({ name: 'batchPlacement' });
8067
+ prepareCurrentProjectPlugins();
6115
8068
  }
6116
- else if (route.name === 'batchSelection' && batchDraft?.action && input.toLowerCase() === 'a') {
6117
- const candidates = props.controller.listPlugins({
6118
- mode: batchDraft.action === 'install' ? 'browse' : 'installed',
6119
- }).filter((candidate) => !candidate.configOnly);
8069
+ else if (route.name === 'batchSelection' && batchDraft?.stage === 'select' && input.toLowerCase() === 'a') {
8070
+ const candidates = batchCandidates(batchDraft)
8071
+ .filter((candidate) => batchCandidateBlockReason(batchDraft, candidate) === undefined);
6120
8072
  setBatchDraft((current) => current ? {
6121
8073
  ...current,
6122
- selectedPluginIds: current.selectedPluginIds.length === candidates.length
6123
- ? [] : candidates.map((candidate) => candidate.pluginId),
8074
+ selectedPluginIds: [...new Set([
8075
+ ...current.selectedPluginIds,
8076
+ ...candidates.map((candidate) => candidate.pluginId),
8077
+ ])],
6124
8078
  } : current);
6125
8079
  }
6126
- else if (route.name === 'batchSelection' && batchDraft?.action && input.toLowerCase() === 'c') {
8080
+ else if (route.name === 'batchSelection' && batchDraft?.stage === 'select' && input.toLowerCase() === 'c') {
6127
8081
  if (batchDraft.selectedPluginIds.length > 0) {
6128
- props.controller.push({ name: 'batchPlacement' });
8082
+ const invalid = invalidBatchSelection(batchDraft);
8083
+ if (invalid.length > 0) {
8084
+ setActionError(`${invalid.length} 个已选插件已不可用。请先在“查看全部已选”中取消选择。`);
8085
+ return;
8086
+ }
8087
+ if (batchDraft.mode === 'browse') {
8088
+ setBatchDraft((current) => current ? { ...current, action: 'install' } : current);
8089
+ props.controller.push({ name: 'batchPlacement' });
8090
+ }
8091
+ else
8092
+ setBatchDraft((current) => current ? { ...current, stage: 'task' } : current);
6129
8093
  setFocus(0);
6130
8094
  setScrollOffset(0);
6131
8095
  }
6132
8096
  else
6133
8097
  setActionError('请至少选择一个插件。');
6134
8098
  }
6135
- else if (route.name === 'batchSelection' && batchDraft?.action && input === ' ') {
6136
- runAction(actions[effectiveFocus]);
8099
+ else if (route.name === 'batchSelection' && batchDraft?.stage === 'select' && input === ' ') {
8100
+ const action = actions[normalizedEffectiveFocus];
8101
+ if (action?.pluginId
8102
+ && (batchDraft.selectedPluginIds.includes(action.pluginId) || action.canSelectNew)) {
8103
+ toggleBatchPlugin(action.pluginId);
8104
+ }
8105
+ }
8106
+ else if (route.name === 'batchSelection' && batchDraft?.stage === 'select' && input === '/') {
8107
+ setSearchFocus(true);
8108
+ }
8109
+ else if (route.name === 'batchSelection' && batchDraft?.stage === 'select'
8110
+ && (key.leftArrow || key.rightArrow || input === '[' || input === ']')) {
8111
+ const tabs = ['all', ...props.controller.listSources().map((source) => source.id)];
8112
+ const current = batchDraft.sourceId ?? 'all';
8113
+ const direction = key.leftArrow || input === '[' ? -1 : 1;
8114
+ const next = tabs[(tabs.indexOf(current) + direction + tabs.length) % tabs.length];
8115
+ setBatchDraft((draft) => draft ? {
8116
+ ...draft,
8117
+ ...(next === 'all' ? { sourceId: undefined } : { sourceId: next }),
8118
+ showSelected: false,
8119
+ } : draft);
8120
+ setFocus(1);
8121
+ setScrollOffset(0);
6137
8122
  }
6138
8123
  else if (route.name === 'configFields' && route.layer.scope !== 'effective' && input.toLowerCase() === 's') {
6139
8124
  if (dirty && configDraftIssues.length === 0) {
@@ -6167,20 +8152,17 @@ export function ConsoleAppV4(props) {
6167
8152
  else if (route.name === 'pluginList' && (key.leftArrow || key.rightArrow || input === '[' || input === ']')) {
6168
8153
  const tabs = ['all', ...props.controller.listSources().map((source) => source.id)];
6169
8154
  const current = route.sourceId ?? 'all';
6170
- const selectedPluginId = props.controller.listPlugins({
6171
- mode: route.mode,
6172
- ...(current !== 'all' ? { sourceId: current } : {}),
6173
- ...(query ? { query } : {}),
6174
- installFilter,
6175
- })[focus]?.pluginId;
8155
+ const selectedPluginId = pluginRows[focus]?.pluginId.startsWith('@menu:')
8156
+ ? undefined : pluginRows[focus]?.pluginId;
6176
8157
  const direction = key.leftArrow || input === '[' ? -1 : 1;
6177
8158
  const index = tabs.indexOf(current);
6178
8159
  const next = tabs[(index + direction + tabs.length) % tabs.length];
6179
8160
  const nextItems = props.controller.listPlugins({ mode: route.mode, ...(next !== 'all' ? { sourceId: next } : {}), ...(query ? { query } : {}), installFilter });
6180
8161
  const retained = selectedPluginId ? nextItems.findIndex((item) => item.pluginId === selectedPluginId) : -1;
6181
8162
  props.controller.replace({ name: 'pluginList', mode: route.mode, ...(next !== 'all' ? { sourceId: next } : {}) }, { ...snapshot.router.current.context, query, installFilter, sourceTabId: next });
6182
- setFocus(retained >= 0 ? retained : 0);
6183
- setScrollOffset(retained >= 4 ? retained - 3 : 0);
8163
+ const retainedFocus = retained >= 0 ? pluginListMenuCount(route.mode, nextItems) + retained : 0;
8164
+ setFocus(retainedFocus);
8165
+ setScrollOffset(retainedFocus >= 4 ? retainedFocus - 3 : 0);
6184
8166
  }
6185
8167
  else if (input.toLowerCase() === 'a' && route.name === 'portableSkillSelect') {
6186
8168
  props.controller.selectAllPortableSkills();
@@ -6192,7 +8174,8 @@ export function ConsoleAppV4(props) {
6192
8174
  }
6193
8175
  else if (key.upArrow || input.toLowerCase() === 'k') {
6194
8176
  setFocus((value) => {
6195
- const current = dynamicActionIds ? effectiveFocus : value;
8177
+ const current = route.name === 'batchSelection' && batchDraft?.stage === 'select'
8178
+ ? normalizedEffectiveFocus : dynamicActionIds ? effectiveFocus : value;
6196
8179
  const next = pluginRows.length > 0 ? moveIndex(pluginRows.length, current, -1) : moveFocus(actions, current, -1);
6197
8180
  if (dynamicActionIds && dynamicActionSignature !== undefined) {
6198
8181
  dynamicMenuFocus.current = {
@@ -6201,17 +8184,31 @@ export function ConsoleAppV4(props) {
6201
8184
  focusedActionId: dynamicActionIds[next],
6202
8185
  };
6203
8186
  }
6204
- const viewport = route.name === 'portableSkillSelect' && snapshot.portableDraft
6205
- ? portableSkillCandidateViewport(actions.slice(0, snapshot.portableDraft.candidates.length), snapshot.portableDraft.candidates, policy.mode === 'compact', next, scrollOffset, pageActionBudget(actions.length - snapshot.portableDraft.candidates.length
6206
- + (portableVariantCandidates.length > 0 ? 3 : 0)))
6207
- : actionViewport(actions, policy.mode === 'compact', next, scrollOffset, pageActionBudget(actionViewportStaticRows));
6208
- setScrollOffset(viewport.offset);
8187
+ const viewport = route.name === 'batchSelection' && batchDraft?.stage === 'select'
8188
+ ? actionViewport(actions.slice(1), policy.mode === 'compact', Math.max(0, next - 1), Math.max(0, scrollOffset - 1), Math.max(1, pageActionBudget(5)))
8189
+ : route.name === 'portableSkillSelect' && snapshot.portableDraft
8190
+ ? portableSkillCandidateViewport(actions.slice(0, snapshot.portableDraft.candidates.length), snapshot.portableDraft.candidates, policy.mode === 'compact', next, scrollOffset, pageActionBudget(actions.length - snapshot.portableDraft.candidates.length
8191
+ + (portableVariantCandidates.length > 0 ? 3 : 0)))
8192
+ : actionViewport(actions, policy.mode === 'compact', next, scrollOffset, pageActionBudget(actionViewportStaticRows));
8193
+ if (route.name === 'pluginDependencies' && route.view !== 'removeBlocked') {
8194
+ const projection = buildDependencyGraphProjection({
8195
+ overviews: props.controller.listPluginDependencyOverviews(),
8196
+ rootPluginId: route.pluginId,
8197
+ direction: route.direction ?? 'dependencies',
8198
+ ...(expandedDependencyPluginIds ? { expandedPluginIds: expandedDependencyPluginIds } : {}),
8199
+ });
8200
+ persistDependencyGraphContext(route, projection, expandedDependencyPluginIds ?? new Set(projection.defaultExpandedPluginIds), next, viewport.offset);
8201
+ }
8202
+ setScrollOffset(route.name === 'batchSelection' && batchDraft?.stage === 'select'
8203
+ ? next === 0 ? 0 : viewport.offset + 1
8204
+ : viewport.offset);
6209
8205
  return next;
6210
8206
  });
6211
8207
  }
6212
8208
  else if (key.downArrow || input.toLowerCase() === 'j') {
6213
8209
  setFocus((value) => {
6214
- const current = dynamicActionIds ? effectiveFocus : value;
8210
+ const current = route.name === 'batchSelection' && batchDraft?.stage === 'select'
8211
+ ? normalizedEffectiveFocus : dynamicActionIds ? effectiveFocus : value;
6215
8212
  const next = pluginRows.length > 0 ? moveIndex(pluginRows.length, current, 1) : moveFocus(actions, current, 1);
6216
8213
  if (dynamicActionIds && dynamicActionSignature !== undefined) {
6217
8214
  dynamicMenuFocus.current = {
@@ -6220,11 +8217,24 @@ export function ConsoleAppV4(props) {
6220
8217
  focusedActionId: dynamicActionIds[next],
6221
8218
  };
6222
8219
  }
6223
- const viewport = route.name === 'portableSkillSelect' && snapshot.portableDraft
6224
- ? portableSkillCandidateViewport(actions.slice(0, snapshot.portableDraft.candidates.length), snapshot.portableDraft.candidates, policy.mode === 'compact', next, scrollOffset, pageActionBudget(actions.length - snapshot.portableDraft.candidates.length
6225
- + (portableVariantCandidates.length > 0 ? 3 : 0)))
6226
- : actionViewport(actions, policy.mode === 'compact', next, scrollOffset, pageActionBudget(actionViewportStaticRows));
6227
- setScrollOffset(viewport.offset);
8220
+ const viewport = route.name === 'batchSelection' && batchDraft?.stage === 'select'
8221
+ ? actionViewport(actions.slice(1), policy.mode === 'compact', Math.max(0, next - 1), Math.max(0, scrollOffset - 1), Math.max(1, pageActionBudget(5)))
8222
+ : route.name === 'portableSkillSelect' && snapshot.portableDraft
8223
+ ? portableSkillCandidateViewport(actions.slice(0, snapshot.portableDraft.candidates.length), snapshot.portableDraft.candidates, policy.mode === 'compact', next, scrollOffset, pageActionBudget(actions.length - snapshot.portableDraft.candidates.length
8224
+ + (portableVariantCandidates.length > 0 ? 3 : 0)))
8225
+ : actionViewport(actions, policy.mode === 'compact', next, scrollOffset, pageActionBudget(actionViewportStaticRows));
8226
+ if (route.name === 'pluginDependencies' && route.view !== 'removeBlocked') {
8227
+ const projection = buildDependencyGraphProjection({
8228
+ overviews: props.controller.listPluginDependencyOverviews(),
8229
+ rootPluginId: route.pluginId,
8230
+ direction: route.direction ?? 'dependencies',
8231
+ ...(expandedDependencyPluginIds ? { expandedPluginIds: expandedDependencyPluginIds } : {}),
8232
+ });
8233
+ persistDependencyGraphContext(route, projection, expandedDependencyPluginIds ?? new Set(projection.defaultExpandedPluginIds), next, viewport.offset);
8234
+ }
8235
+ setScrollOffset(route.name === 'batchSelection' && batchDraft?.stage === 'select'
8236
+ ? next === 0 ? 0 : viewport.offset + 1
8237
+ : viewport.offset);
6228
8238
  return next;
6229
8239
  });
6230
8240
  }
@@ -6238,7 +8248,7 @@ export function ConsoleAppV4(props) {
6238
8248
  : { name: 'pluginDetail', pluginId: row.pluginId });
6239
8249
  }
6240
8250
  else if (key.return) {
6241
- runAction(actions[effectiveFocus]);
8251
+ runAction(actions[normalizedEffectiveFocus]);
6242
8252
  }
6243
8253
  });
6244
8254
  if (tooSmall) {