@lesliechan721/aw 0.1.0-beta.10 → 0.1.0-beta.12

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 (227) hide show
  1. package/dist/authoring/assets/aw-config.d.mts +1 -50
  2. package/dist/authoring/assets/aw-config.mjs +58 -928
  3. package/dist/authoring/assets/aw-config.mjs.map +1 -1
  4. package/dist/authoring/scripts/init-plugin.mjs +28 -33
  5. package/dist/authoring/scripts/init-plugin.mjs.map +1 -1
  6. package/dist/authoring/scripts/lib/plugin-version-check.d.mts +1 -0
  7. package/dist/authoring/scripts/lib/plugin-version-check.mjs +229 -8
  8. package/dist/authoring/scripts/lib/plugin-version-check.mjs.map +1 -1
  9. package/dist/authoring/scripts/validate-plugin.mjs +159 -48
  10. package/dist/authoring/scripts/validate-plugin.mjs.map +1 -1
  11. package/dist/scripts/bundle-plugins.js +18 -4
  12. package/dist/scripts/bundle-plugins.js.map +1 -1
  13. package/dist/scripts/test-plugin-scripts.d.ts +2 -0
  14. package/dist/scripts/test-plugin-scripts.js +21 -5
  15. package/dist/scripts/test-plugin-scripts.js.map +1 -1
  16. package/dist/src/commands/aw-capabilities.d.ts +1 -1
  17. package/dist/src/commands/aw-capabilities.js +1 -1
  18. package/dist/src/commands/aw-capabilities.js.map +1 -1
  19. package/dist/src/commands/catalog-dev-source.d.ts +6 -3
  20. package/dist/src/commands/catalog-dev-source.js +96 -35
  21. package/dist/src/commands/catalog-dev-source.js.map +1 -1
  22. package/dist/src/commands/catalog-publication-inspection.d.ts +1 -1
  23. package/dist/src/commands/catalog-publication-inspection.js +54 -8
  24. package/dist/src/commands/catalog-publication-inspection.js.map +1 -1
  25. package/dist/src/commands/catalog-publication-journal.js +1 -1
  26. package/dist/src/commands/catalog-publication-journal.js.map +1 -1
  27. package/dist/src/commands/catalog-source.d.ts +19 -4
  28. package/dist/src/commands/catalog-source.js +121 -12
  29. package/dist/src/commands/catalog-source.js.map +1 -1
  30. package/dist/src/commands/catalog-store.d.ts +26 -7
  31. package/dist/src/commands/catalog-store.js +361 -42
  32. package/dist/src/commands/catalog-store.js.map +1 -1
  33. package/dist/src/commands/console.js +28 -17
  34. package/dist/src/commands/console.js.map +1 -1
  35. package/dist/src/commands/operation-plan.d.ts +2 -1
  36. package/dist/src/commands/operation-plan.js +24 -0
  37. package/dist/src/commands/operation-plan.js.map +1 -1
  38. package/dist/src/commands/operation-planner.d.ts +2 -2
  39. package/dist/src/commands/operation-planner.js +22 -20
  40. package/dist/src/commands/operation-planner.js.map +1 -1
  41. package/dist/src/commands/options.d.ts +2 -2
  42. package/dist/src/commands/options.js +18 -4
  43. package/dist/src/commands/options.js.map +1 -1
  44. package/dist/src/commands/plugin-capabilities.js +2 -2
  45. package/dist/src/commands/plugin-capabilities.js.map +1 -1
  46. package/dist/src/commands/rebuild-command.d.ts +19 -0
  47. package/dist/src/commands/rebuild-command.js +828 -283
  48. package/dist/src/commands/rebuild-command.js.map +1 -1
  49. package/dist/src/commands/rebuild-integration.d.ts +25 -47
  50. package/dist/src/commands/rebuild-integration.js +1176 -1016
  51. package/dist/src/commands/rebuild-integration.js.map +1 -1
  52. package/dist/src/commands/rebuild-operations.d.ts +50 -12
  53. package/dist/src/commands/rebuild-operations.js +207 -101
  54. package/dist/src/commands/rebuild-operations.js.map +1 -1
  55. package/dist/src/commands/status-v6.d.ts +21 -2
  56. package/dist/src/commands/status-v6.js +163 -31
  57. package/dist/src/commands/status-v6.js.map +1 -1
  58. package/dist/src/commands/sync-command.d.ts +17 -0
  59. package/dist/src/commands/sync-command.js +45 -37
  60. package/dist/src/commands/sync-command.js.map +1 -1
  61. package/dist/src/commands/write-commands.d.ts +5 -0
  62. package/dist/src/commands/write-commands.js +50 -3
  63. package/dist/src/commands/write-commands.js.map +1 -1
  64. package/dist/src/generators/local-plugin.js +14 -4
  65. package/dist/src/generators/local-plugin.js.map +1 -1
  66. package/dist/src/index.js +260 -91
  67. package/dist/src/index.js.map +1 -1
  68. package/dist/src/output/command-receipt.js +1 -3
  69. package/dist/src/output/command-receipt.js.map +1 -1
  70. package/dist/src/output/rebuild-progress.js +1 -3
  71. package/dist/src/output/rebuild-progress.js.map +1 -1
  72. package/dist/src/portable/materializer.js +10 -8
  73. package/dist/src/portable/materializer.js.map +1 -1
  74. package/dist/src/tui/catalog-generation.js +41 -1
  75. package/dist/src/tui/catalog-generation.js.map +1 -1
  76. package/dist/src/tui/catalog-sources/session.d.ts +1 -0
  77. package/dist/src/tui/catalog-sources/session.js +16 -3
  78. package/dist/src/tui/catalog-sources/session.js.map +1 -1
  79. package/dist/src/tui/catalog-sources/types.d.ts +3 -2
  80. package/dist/src/tui/components.d.ts +4 -0
  81. package/dist/src/tui/components.js +36 -24
  82. package/dist/src/tui/components.js.map +1 -1
  83. package/dist/src/tui/console-app-v4.d.ts +13 -4
  84. package/dist/src/tui/console-app-v4.js +2065 -857
  85. package/dist/src/tui/console-app-v4.js.map +1 -1
  86. package/dist/src/tui/controller-runtime.js +731 -62
  87. package/dist/src/tui/controller-runtime.js.map +1 -1
  88. package/dist/src/tui/controller.d.ts +156 -10
  89. package/dist/src/tui/controller.js +1187 -163
  90. package/dist/src/tui/controller.js.map +1 -1
  91. package/dist/src/tui/diagnostic-projection.d.ts +22 -0
  92. package/dist/src/tui/diagnostic-projection.js +437 -0
  93. package/dist/src/tui/diagnostic-projection.js.map +1 -0
  94. package/dist/src/tui/diagnostics.d.ts +1 -1
  95. package/dist/src/tui/diagnostics.js +7 -2
  96. package/dist/src/tui/diagnostics.js.map +1 -1
  97. package/dist/src/tui/glossary.d.ts +23 -0
  98. package/dist/src/tui/glossary.js +42 -0
  99. package/dist/src/tui/glossary.js.map +1 -0
  100. package/dist/src/tui/index.d.ts +4 -1
  101. package/dist/src/tui/index.js +4 -1
  102. package/dist/src/tui/index.js.map +1 -1
  103. package/dist/src/tui/keys.d.ts +44 -0
  104. package/dist/src/tui/keys.js +43 -15
  105. package/dist/src/tui/keys.js.map +1 -1
  106. package/dist/src/tui/lifecycle-action-view.d.ts +6 -0
  107. package/dist/src/tui/lifecycle-action-view.js +34 -0
  108. package/dist/src/tui/lifecycle-action-view.js.map +1 -0
  109. package/dist/src/tui/model.d.ts +12 -2
  110. package/dist/src/tui/model.js +128 -52
  111. package/dist/src/tui/model.js.map +1 -1
  112. package/dist/src/tui/operation-diagnostic-screens.d.ts +21 -0
  113. package/dist/src/tui/operation-diagnostic-screens.js +101 -0
  114. package/dist/src/tui/operation-diagnostic-screens.js.map +1 -0
  115. package/dist/src/tui/operation-history.d.ts +80 -0
  116. package/dist/src/tui/operation-history.js +130 -0
  117. package/dist/src/tui/operation-history.js.map +1 -0
  118. package/dist/src/tui/operation-review-planner.js +366 -99
  119. package/dist/src/tui/operation-review-planner.js.map +1 -1
  120. package/dist/src/tui/router.d.ts +82 -17
  121. package/dist/src/tui/router.js +186 -38
  122. package/dist/src/tui/router.js.map +1 -1
  123. package/dist/src/tui/screen-registry.d.ts +546 -0
  124. package/dist/src/tui/screen-registry.js +179 -0
  125. package/dist/src/tui/screen-registry.js.map +1 -0
  126. package/dist/src/tui/session-console-app.d.ts +6 -0
  127. package/dist/src/tui/session-console-app.js +13 -0
  128. package/dist/src/tui/session-console-app.js.map +1 -0
  129. package/dist/src/tui/session-host.d.ts +44 -0
  130. package/dist/src/tui/session-host.js +197 -0
  131. package/dist/src/tui/session-host.js.map +1 -0
  132. package/dist/src/types.d.ts +64 -3
  133. package/dist/src/types.js +1 -1
  134. package/dist/src/types.js.map +1 -1
  135. package/dist/src/utils/agent-version.js +1 -1
  136. package/dist/src/utils/agent-version.js.map +1 -1
  137. package/dist/src/utils/artifact-plan.d.ts +2 -2
  138. package/dist/src/utils/artifact-plan.js +8 -6
  139. package/dist/src/utils/artifact-plan.js.map +1 -1
  140. package/dist/src/utils/artifact-renderer.d.ts +2 -1
  141. package/dist/src/utils/artifact-renderer.js +139 -120
  142. package/dist/src/utils/artifact-renderer.js.map +1 -1
  143. package/dist/src/utils/artifact-request.d.ts +5 -0
  144. package/dist/src/utils/artifact-request.js +40 -0
  145. package/dist/src/utils/artifact-request.js.map +1 -1
  146. package/dist/src/utils/bundle-layout.js +4 -4
  147. package/dist/src/utils/bundle-layout.js.map +1 -1
  148. package/dist/src/utils/catalog-capability-projection.d.ts +8 -0
  149. package/dist/src/utils/catalog-capability-projection.js +14 -0
  150. package/dist/src/utils/catalog-capability-projection.js.map +1 -0
  151. package/dist/src/utils/catalog-generation-resolver.d.ts +14 -2
  152. package/dist/src/utils/catalog-generation-resolver.js +49 -27
  153. package/dist/src/utils/catalog-generation-resolver.js.map +1 -1
  154. package/dist/src/utils/catalog-generation-service.d.ts +8 -1
  155. package/dist/src/utils/catalog-generation-service.js +81 -47
  156. package/dist/src/utils/catalog-generation-service.js.map +1 -1
  157. package/dist/src/utils/catalog-generation-store.d.ts +1 -0
  158. package/dist/src/utils/catalog-generation-store.js +60 -5
  159. package/dist/src/utils/catalog-generation-store.js.map +1 -1
  160. package/dist/src/utils/catalog-operation-context.js +1 -0
  161. package/dist/src/utils/catalog-operation-context.js.map +1 -1
  162. package/dist/src/utils/catalog-public-error.js +4 -0
  163. package/dist/src/utils/catalog-public-error.js.map +1 -1
  164. package/dist/src/utils/catalog-source-impact.d.ts +3 -2
  165. package/dist/src/utils/catalog-source-impact.js +142 -20
  166. package/dist/src/utils/catalog-source-impact.js.map +1 -1
  167. package/dist/src/utils/catalog-source-references.js +95 -9
  168. package/dist/src/utils/catalog-source-references.js.map +1 -1
  169. package/dist/src/utils/catalog-sources.d.ts +1 -1
  170. package/dist/src/utils/catalog-sources.js +19 -3
  171. package/dist/src/utils/catalog-sources.js.map +1 -1
  172. package/dist/src/utils/codex-manager.js +8 -0
  173. package/dist/src/utils/codex-manager.js.map +1 -1
  174. package/dist/src/utils/codex-native-observation.d.ts +34 -1
  175. package/dist/src/utils/codex-native-observation.js +132 -0
  176. package/dist/src/utils/codex-native-observation.js.map +1 -1
  177. package/dist/src/utils/codex-reconciler.d.ts +0 -2
  178. package/dist/src/utils/codex-reconciler.js.map +1 -1
  179. package/dist/src/utils/codex-shared-native.d.ts +2 -26
  180. package/dist/src/utils/codex-shared-native.js +8 -82
  181. package/dist/src/utils/codex-shared-native.js.map +1 -1
  182. package/dist/src/utils/git-catalog-provider.d.ts +3 -0
  183. package/dist/src/utils/git-catalog-provider.js +40 -1
  184. package/dist/src/utils/git-catalog-provider.js.map +1 -1
  185. package/dist/src/utils/local-marketplace.d.ts +1 -0
  186. package/dist/src/utils/local-marketplace.js +69 -73
  187. package/dist/src/utils/local-marketplace.js.map +1 -1
  188. package/dist/src/utils/multi-source-catalog.js +1 -0
  189. package/dist/src/utils/multi-source-catalog.js.map +1 -1
  190. package/dist/src/utils/native-cli.d.ts +9 -4
  191. package/dist/src/utils/native-cli.js +280 -34
  192. package/dist/src/utils/native-cli.js.map +1 -1
  193. package/dist/src/utils/path-catalog-provider.js +2 -0
  194. package/dist/src/utils/path-catalog-provider.js.map +1 -1
  195. package/dist/src/utils/plugin-config-impact.d.ts +33 -0
  196. package/dist/src/utils/plugin-config-impact.js +298 -0
  197. package/dist/src/utils/plugin-config-impact.js.map +1 -0
  198. package/dist/src/utils/plugin-config-write.d.ts +36 -14
  199. package/dist/src/utils/plugin-config-write.js +709 -225
  200. package/dist/src/utils/plugin-config-write.js.map +1 -1
  201. package/dist/src/utils/plugin-config.d.ts +58 -4
  202. package/dist/src/utils/plugin-config.js +328 -60
  203. package/dist/src/utils/plugin-config.js.map +1 -1
  204. package/dist/src/utils/plugin-scripts.d.ts +20 -12
  205. package/dist/src/utils/plugin-scripts.js +389 -114
  206. package/dist/src/utils/plugin-scripts.js.map +1 -1
  207. package/dist/src/utils/process-tree.d.ts +2 -2
  208. package/dist/src/utils/process-tree.js +1 -1
  209. package/dist/src/utils/process-tree.js.map +1 -1
  210. package/dist/src/utils/project-boundary.d.ts +11 -1
  211. package/dist/src/utils/project-boundary.js +33 -11
  212. package/dist/src/utils/project-boundary.js.map +1 -1
  213. package/dist/src/utils/safe-operation-diagnostic.d.ts +15 -0
  214. package/dist/src/utils/safe-operation-diagnostic.js +33 -0
  215. package/dist/src/utils/safe-operation-diagnostic.js.map +1 -0
  216. package/dist/src/utils/sensitive-content.d.ts +4 -0
  217. package/dist/src/utils/sensitive-content.js +36 -0
  218. package/dist/src/utils/sensitive-content.js.map +1 -0
  219. package/dist/src/utils/windows-claude-shim.d.ts +9 -0
  220. package/dist/src/utils/windows-claude-shim.js +141 -0
  221. package/dist/src/utils/windows-claude-shim.js.map +1 -0
  222. package/dist/vitest.config.js +91 -10
  223. package/dist/vitest.config.js.map +1 -1
  224. package/package.json +9 -9
  225. package/dist/authoring/templates/aw-config-runtime.d.mts +0 -2
  226. package/dist/authoring/templates/aw-config-runtime.mjs +0 -407
  227. package/dist/authoring/templates/aw-config-runtime.mjs.map +0 -1
@@ -7,33 +7,54 @@ import { CATALOG_BETA_REF, CATALOG_LATEST_REF, OFFICIAL_CATALOG_SOURCE_SSH_URL,
7
7
  import { CATALOG_SOURCE_ID_PATTERN } from '../utils/catalog-sources.js';
8
8
  import { canonicalizeCatalogGitUrl } from '../utils/catalog-source-identity.js';
9
9
  import { normalizePortableAgents } from '../portable/commands.js';
10
- import { buildAllowedConfigActions, selectLifecycleRetryAction, } from '../commands/operation-planner.js';
10
+ import { selectLifecycleRetryAction, } from '../commands/operation-planner.js';
11
+ import { parseScriptTimeoutMs, SCRIPT_TIMEOUT_DEFAULT_MS, SCRIPT_TIMEOUT_MAX_MS, SCRIPT_TIMEOUT_MIN_MS, } from '../commands/options.js';
11
12
  import { projectCatalogSourceLocation } from '../utils/catalog-source-presentation.js';
12
- import { ActionRow, Callout, ChoiceRow, DiagnosticSummary, InputCard, PluginListRow, ProgressRow, ResultCard, ResultRow, ReviewCard, ReviewChangeRow, ScreenFrame, SectionCard, SectionRow, StaticRow, StatusCard, TaskRow, TechnicalDetails, ToggleRow, } from './components.js';
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';
13
14
  import { safeDiagnosticCode, sanitizeDiagnosticText, } from './diagnostics.js';
14
15
  import { extractPortableError, TuiConfigInvalidStoreError, userIssueKey, } from './controller.js';
15
16
  import { catalogSourceFreshnessLabel, catalogSourceListPresentation, } from './catalog-sources/screens.js';
17
+ import { operationDiagnosticLineCount, operationHistoryMeta, operationHistoryState, operationReportLineCount, OperationDiagnosticScreen, OperationReportPreviewScreen, } from './operation-diagnostic-screens.js';
16
18
  import { resolveTuiSizePolicy } from './layout.js';
19
+ import { platformLabel, scopeLabel } from './glossary.js';
17
20
  import { InputDisplay, removeLastGrapheme } from './input-display.js';
18
21
  import { catalogRefPolicyForPreset } from './source-ref-presets.js';
19
22
  import { buildPlatformActions, buildPluginDetailActions, buildPluginPreviewActions, configObjectHasSensitiveChild, hasInstalledPlacement, configValuesEqual, } from './model.js';
23
+ import { resolveTuiGlobalKeyAction, resolveTuiSessionNavigationKey } from './keys.js';
20
24
  import { tuiTheme } from './theme.js';
21
25
  import { contentRowBudget, rowViewport } from './viewport.js';
22
- export const CONSOLE_V4_SCREEN_IDS = [
23
- 'G01', 'G02', 'G03', 'G04', 'G05',
24
- 'P01', 'P02', 'P03', 'P04', 'P05', 'P06', 'P07', 'P08', 'P09', 'P10',
25
- 'P11', 'P12', 'P13', 'P14', 'P15', 'P16', 'P17', 'P18', 'P19', 'P20', 'P21',
26
- 'C01', 'C02', 'C03', 'C04', 'C05', 'C06', 'C07', 'C08', 'C09', 'C10',
27
- 'C11', 'C12', 'C13', 'C14', 'C15', 'C16', 'C17',
28
- 'S01', 'S02', 'S03', 'S04', 'S05', 'S06', 'S07', 'S08', 'S09',
29
- 'S10', 'S11', 'S12', 'S13', 'S14', 'S15', 'S16', 'S17', 'S18',
30
- 'I01', 'I02', 'I03', 'I04', 'I05', 'I06', 'I07', 'I08', 'I09',
31
- 'I10', 'I11', 'I12', 'I13',
32
- 'D01', 'D02', 'D03', 'D04', 'D05', 'D06', 'D07', 'D08', 'D09',
33
- 'D10', 'D11', 'D12', 'D13', 'D14', 'D15', 'D16', 'D17',
34
- 'U01', 'U02', 'U03', 'U04', 'U05', 'U06', 'U07', 'U08', 'U09', 'U10', 'U11',
35
- 'K01', 'K02', 'K03', 'K04', 'K05', 'K06', 'K07',
36
- ];
26
+ import { resolveRouteScreenId, routeDefinition, TUI_SCREEN_IDS, unreachableTuiRoute, } from './screen-registry.js';
27
+ export const CONSOLE_V4_SCREEN_IDS = TUI_SCREEN_IDS;
28
+ function sourceOperationBlockLabel(reason) {
29
+ switch (reason) {
30
+ case 'configDraft': return '未保存的插件设置';
31
+ case 'sourceDraft': return '未保存的来源修改';
32
+ case 'batchDraft': return '未完成的批量操作选择';
33
+ case 'portableDraft': return '未完成的通用 Skills 选择';
34
+ case 'activeReview': return '等待确认的操作';
35
+ case 'reviewPreparation': return '正在准备确认信息的操作';
36
+ case 'writeProgress': return '正在执行的写入操作';
37
+ }
38
+ }
39
+ function catalogGenerationStateLabel(state) {
40
+ switch (state) {
41
+ case 'ready': return '可用';
42
+ case 'activationRequired': return '需要启用';
43
+ case 'repairRequired': return '需要修复';
44
+ case 'configMismatch': return '来源设置已变化';
45
+ case 'stateInvalid': return '版本状态需要恢复';
46
+ case 'incompatible': return '当前 AW 不兼容';
47
+ case 'unavailable': return '不可用';
48
+ case undefined: return '尚未取得状态';
49
+ }
50
+ }
51
+ function productResultMessage(message) {
52
+ if (message === '后续:当前项目 sync')
53
+ return '后续:同步当前项目位置';
54
+ if (message === '后续:所有项目 sync')
55
+ return '后续:同步所有项目位置';
56
+ return message;
57
+ }
37
58
  function pluginMenuActionId(action) {
38
59
  return action.id === 'platform' || action.id === 'scopeConflict'
39
60
  ? `${action.id}:${action.platform}`
@@ -44,6 +65,8 @@ function pluginMenuActionId(action) {
44
65
  : action.id;
45
66
  }
46
67
  function isCharacterInputMode(route, searchMode) {
68
+ if (routeDefinition(route).keymap !== 'menuOrTextInput')
69
+ return false;
47
70
  return searchMode
48
71
  || route.name === 'portableAgentInput'
49
72
  || route.name === 'configFieldEditor'
@@ -51,9 +74,10 @@ function isCharacterInputMode(route, searchMode) {
51
74
  && route.mode !== 'choices'
52
75
  || route.name === 'configObjectPropertyEditor'
53
76
  && (route.stage === 'key' || route.stage === 'custom')
54
- || route.name === 'sourceAdd' && route.step !== 'type' && route.step !== 'refPolicy';
77
+ || route.name === 'sourceAdd' && route.step !== 'type' && route.step !== 'refPolicy'
78
+ || route.name === 'scriptTimeout';
55
79
  }
56
- function helpRowsForRoute(route, actionCount, supportsTechnicalDetails, busy) {
80
+ function helpRowsForRoute(route, actionCount, supportsTechnicalDetails, busy, sessionNavigation) {
57
81
  const rows = [];
58
82
  if (!busy && actionCount > 1)
59
83
  rows.push(['↑/↓ · j/k', '移动当前唯一焦点']);
@@ -71,8 +95,13 @@ function helpRowsForRoute(route, actionCount, supportsTechnicalDetails, busy) {
71
95
  rows.push(['Space', '切换当前 Skill']);
72
96
  rows.push(['a', '全选 Skills']);
73
97
  }
98
+ if (route.name === 'batchSelection') {
99
+ rows.push(['Space', '切换当前插件']);
100
+ rows.push(['a', '全选或清空当前列表']);
101
+ rows.push(['c', '继续选择位置和平台']);
102
+ }
74
103
  if (route.name === 'configFields' && route.layer.scope !== 'effective') {
75
- rows.push(['s', '保存设置']);
104
+ rows.push(['s', '查看保存汇总']);
76
105
  rows.push(['u', '恢复当前字段继承']);
77
106
  }
78
107
  if (route.name === 'sourceList' || route.name === 'sourceDetail') {
@@ -81,8 +110,16 @@ function helpRowsForRoute(route, actionCount, supportsTechnicalDetails, busy) {
81
110
  if (supportsTechnicalDetails) {
82
111
  rows.push(['d', route.name === 'progress' ? '打开技术进度' : '打开技术详情']);
83
112
  }
113
+ if (sessionNavigation && !busy && !isCharacterInputMode(route, false)) {
114
+ rows.push(['h', '返回首页']);
115
+ rows.push(['H', '重新读取本地状态']);
116
+ }
117
+ if (sessionNavigation && busy) {
118
+ rows.push(['h / H', '操作成功后返回首页或重新读取']);
119
+ rows.push(['Esc', '撤销等待中的页面跳转']);
120
+ }
84
121
  rows.push(['q / Ctrl-C', busy ? '操作完成后退出' : '退出控制台']);
85
- rows.push(['?', '关闭帮助']);
122
+ rows.push(['? / o', '关闭帮助 / 查看操作记录']);
86
123
  return rows;
87
124
  }
88
125
  function configOwnerKey(route) {
@@ -93,13 +130,24 @@ function configPageMatches(page, route) {
93
130
  && page.pluginId === route.pluginId
94
131
  && page.layer.scope === route.layer.scope);
95
132
  }
96
- function platformLabel(platform) {
97
- return platform === 'codex' ? 'Codex' : 'Claude Code';
133
+ function configFieldCanSet(field) {
134
+ return field.canSet ?? field.writable;
98
135
  }
99
- function scopeLabel(scope) {
100
- return scope === 'user' ? '所有项目' : '当前项目';
136
+ function configFieldCanUnset(field) {
137
+ return field.canUnset ?? (field.writable && field.configured);
138
+ }
139
+ function batchActionLabel(action) {
140
+ return {
141
+ install: '添加',
142
+ sync: '同步',
143
+ reinstall: '重新安装',
144
+ uninstall: '卸载',
145
+ cleanup: '清理',
146
+ }[action];
101
147
  }
102
148
  function statusText(plugin) {
149
+ if (plugin.configOnly)
150
+ return '已保存设置';
103
151
  return {
104
152
  available: '未添加',
105
153
  installed: '可用',
@@ -110,6 +158,11 @@ function statusText(plugin) {
110
158
  }[plugin.aggregateStatus];
111
159
  }
112
160
  function pluginPlacementSummary(plugin) {
161
+ if (plugin.configOnly) {
162
+ return plugin.savedConfigScopes.length > 0
163
+ ? `已保存${plugin.savedConfigScopes.map(scopeLabel).join('、')}设置`
164
+ : '已保存设置';
165
+ }
113
166
  return plugin.supportedPlatforms.map((platform) => {
114
167
  const label = platformLabel(platform);
115
168
  const placement = plugin.installedPlacements.find((entry) => entry.platform === platform);
@@ -145,6 +198,8 @@ function issueGroupLabel(group) {
145
198
  }[group];
146
199
  }
147
200
  function pluginRowStatus(plugin) {
201
+ if (plugin.configOnly)
202
+ return 'warning';
148
203
  if (plugin.aggregateStatus === 'needsAttention')
149
204
  return 'warning';
150
205
  if (plugin.aggregateStatus === 'installed')
@@ -303,12 +358,27 @@ function portableBlockerDiagnostic(route) {
303
358
  },
304
359
  };
305
360
  }
361
+ function configUnavailableCanRecoverSource(route) {
362
+ if (route.storedScopes?.length)
363
+ return false;
364
+ return new Set([
365
+ 'catalogSourceUnavailable',
366
+ 'bundleUnavailable',
367
+ 'sourceMissing',
368
+ 'catalogGenerationUnavailable',
369
+ 'catalogGenerationStateInvalid',
370
+ 'catalogGenerationStoreInvalid',
371
+ 'catalogStateInvalid',
372
+ 'catalogStoreInvalid',
373
+ 'configReadFailed',
374
+ ]).has(route.diagnosticCode ?? '');
375
+ }
306
376
  function configUnavailableDiagnostic(route) {
307
377
  const invalidLayer = (route.invalidStores?.length ?? 0) > 0;
308
378
  const invalidLayerLabel = route.invalidStores?.map((store) => store.scope === 'user' ? '个人默认' : '当前项目').join('、');
309
379
  return {
310
380
  severity: 'error',
311
- title: invalidLayer ? '设置层无效,已阻止编辑' : 'Manifest 或来源不可用',
381
+ title: invalidLayer ? '设置层无效,已阻止编辑' : '插件定义或来源不可用',
312
382
  userMessage: invalidLayer
313
383
  ? '高优先级设置层无效,不能静默使用较低层或默认值。'
314
384
  : '当前无法取得可信的字段定义,因此不能验证新值。',
@@ -322,7 +392,9 @@ function configUnavailableDiagnostic(route) {
322
392
  ],
323
393
  nextStep: invalidLayer
324
394
  ? '确认后重置整个目标设置层;不会创建备份。'
325
- : '重新检查来源,然后再次打开设置。',
395
+ : configUnavailableCanRecoverSource(route)
396
+ ? '重新检查来源,然后再次打开设置。'
397
+ : '查看技术详情,确认字段定义不可用的原因。',
326
398
  technical: {
327
399
  code: safeDiagnosticCode(route.diagnosticCode, 'configUnavailable'),
328
400
  stage: safeDiagnosticCode(route.diagnosticStage, 'read-config'),
@@ -334,20 +406,43 @@ function configUnavailableDiagnostic(route) {
334
406
  ],
335
407
  checks: invalidLayer
336
408
  ? ['只删除所选 user 或 project 设置层。', '重置后重新读取继承值和默认值。']
337
- : ['恢复插件来源和可信 Manifest。', '重新检查来源后再次打开设置。'],
409
+ : ['恢复插件来源和可信插件定义。', '重新检查来源后再次打开设置。'],
338
410
  },
339
411
  };
340
412
  }
341
413
  function sourceDiagnostic(sourceId, state, detail, generation, freshness) {
342
414
  const unavailable = state === 'unavailable' || state === 'error';
415
+ if (generation?.compatibility?.state === 'unsupported') {
416
+ const { requiredCapabilities, missingCapabilities } = generation.compatibility;
417
+ return {
418
+ severity: 'error',
419
+ title: '当前目录版本不兼容',
420
+ userMessage: `当前 AW 缺少 ${missingCapabilities.length} 项所需功能支持。`,
421
+ impact: [
422
+ { state: 'blocked', message: '这个目录版本不能用于浏览、安装或同步。' },
423
+ { state: 'preserved', message: '当前启用版本和已下载内容保持不变。' },
424
+ ],
425
+ nextStep: '升级 AW 到支持所需能力的版本,或修改来源后重新启用。',
426
+ technical: {
427
+ code: 'awCapabilityUnsupported',
428
+ stage: 'resolve-source',
429
+ sourceId,
430
+ context: [{
431
+ key: 'compatibility',
432
+ value: `required: ${requiredCapabilities.join('、')}; missing: ${missingCapabilities.join('、')}`,
433
+ }],
434
+ checks: ['检查所需和缺少的 Catalog capability。', '升级 AW 后重新加载来源。'],
435
+ },
436
+ };
437
+ }
343
438
  if (!unavailable && freshness?.state === 'checkFailed') {
344
439
  return {
345
440
  severity: 'warning',
346
441
  title: '更新检查失败',
347
- userMessage: '远端或本地候选检查失败,但当前 Active Catalog 仍可用。',
442
+ userMessage: '待检查版本检查失败,但当前目录版本仍可用。',
348
443
  impact: [
349
444
  { state: 'degraded', message: '当前无法判断是否有更新。' },
350
- { state: 'preserved', message: '浏览、安装和当前 Active 不受影响。' },
445
+ { state: 'preserved', message: '浏览、安装和当前启用版本不受影响。' },
351
446
  ],
352
447
  nextStep: '返回来源详情并选择“重新检查”。',
353
448
  technical: {
@@ -362,15 +457,15 @@ function sourceDiagnostic(sourceId, state, detail, generation, freshness) {
362
457
  const hasActive = generation?.active !== undefined;
363
458
  return {
364
459
  severity: 'error',
365
- title: '候选 Catalog 不兼容',
460
+ title: '待检查版本不兼容',
366
461
  userMessage: hasActive
367
- ? `当前 AW 缺少 ${freshness.missingCapabilities?.length ?? 0} 项候选 Catalog 能力。普通更新已阻止,当前 Active Catalog 保持不变。`
368
- : `当前 AW 缺少 ${freshness.missingCapabilities?.length ?? 0} 项候选 Catalog 能力。这个来源没有可用 Active Catalog。`,
462
+ ? `当前 AW 缺少 ${freshness.missingCapabilities?.length ?? 0} 项待检查版本所需功能。普通更新已阻止,当前目录版本保持不变。`
463
+ : `当前 AW 缺少 ${freshness.missingCapabilities?.length ?? 0} 项待检查版本所需功能。这个来源没有可用目录版本。`,
369
464
  impact: [
370
- { state: 'blocked', message: '不能直接切换到这个候选。' },
371
- ...(hasActive ? [{ state: 'preserved', message: '当前 Active Catalog 和插件浏览仍可用。' }] : []),
465
+ { state: 'blocked', message: '不能直接切换到这个待检查版本。' },
466
+ ...(hasActive ? [{ state: 'preserved', message: '当前目录版本和插件浏览仍可用。' }] : []),
372
467
  ],
373
- nextStep: '升级 AW 到支持候选能力的版本,或编辑来源后重新验证。',
468
+ nextStep: '升级 AW 到支持待检查版本所需功能的版本,或编辑来源后重新验证。',
374
469
  technical: {
375
470
  code: safeDiagnosticCode(freshness.errorCode, 'catalogincompatible'),
376
471
  stage: 'compare-candidate',
@@ -386,19 +481,19 @@ function sourceDiagnostic(sourceId, state, detail, generation, freshness) {
386
481
  || freshness?.state === 'rollbackDetected')) {
387
482
  return {
388
483
  severity: 'error',
389
- title: freshness.state === 'versionConflict' ? 'Catalog 版本冲突'
390
- : '检测到 Catalog 回退',
391
- userMessage: '普通更新已阻止。当前 Active Catalog 保持不变。',
484
+ title: freshness.state === 'versionConflict' ? '插件目录版本冲突'
485
+ : '检测到插件目录版本回退',
486
+ userMessage: '普通更新已阻止。当前目录版本保持不变。',
392
487
  impact: [
393
- { state: 'blocked', message: '不能直接切换到这个候选。' },
394
- { state: 'preserved', message: '当前 Active Catalog 和插件浏览仍可用。' },
488
+ { state: 'blocked', message: '不能直接切换到这个待检查版本。' },
489
+ { state: 'preserved', message: '当前目录版本和插件浏览仍可用。' },
395
490
  ],
396
- nextStep: '检查候选版本、来源 ref 和发布身份后,再决定是否修改来源。',
491
+ nextStep: '检查待检查版本、来源引用和发布身份后,再决定是否修改来源。',
397
492
  technical: {
398
493
  code: safeDiagnosticCode(freshness.errorCode, `catalog${freshness.state}`),
399
494
  stage: 'compare-candidate',
400
495
  sourceId,
401
- checks: ['检查当前版本和候选版本。', '检查来源 ref 和 Catalog logical identity。'],
496
+ checks: ['检查当前版本和待检查版本。', '检查来源引用和发布身份。'],
402
497
  },
403
498
  };
404
499
  }
@@ -407,7 +502,7 @@ function sourceDiagnostic(sourceId, state, detail, generation, freshness) {
407
502
  const metadataInvalid = generation?.errorCode === 'catalogGenerationMetadataInvalid';
408
503
  return {
409
504
  severity: 'error',
410
- title: 'Catalog 来源不可用',
505
+ title: '插件来源不可用',
411
506
  userMessage: '当前无法读取这个来源。',
412
507
  impact: [
413
508
  { state: 'blocked', message: '当前不能浏览或安装这个来源中的插件。' },
@@ -415,7 +510,7 @@ function sourceDiagnostic(sourceId, state, detail, generation, freshness) {
415
510
  ],
416
511
  nextStep: metadataInvalid && generation.recoveryEntryPath
417
512
  ? '在来源详情中选择“重新下载并修复”。'
418
- : '在来源详情中获取、修复或切换 Catalog;如仍失败,请编辑来源 URL、路径或 ref。',
513
+ : '在来源详情中重新取得或修复目录版本;如仍失败,请编辑来源地址、路径或引用。',
419
514
  technical: {
420
515
  code: safeDiagnosticCode(generation?.errorCode ?? detail?.errorCode, 'catalogSourceUnavailable'),
421
516
  stage: 'resolve-source',
@@ -438,11 +533,10 @@ function sourceDiagnostic(sourceId, state, detail, generation, freshness) {
438
533
  function capabilityDiagnostic(plugin) {
439
534
  const missingCapabilities = plugin.missingCapabilities?.join('、')
440
535
  ?? 'Status 未提供具体 capability';
441
- const recovery = plugin.capabilityRecovery
442
- ?? '升级 AW CLI 到支持插件所需 capability 的版本后重试。';
536
+ const recovery = '升级 AW CLI 到支持此插件的版本后重试。';
443
537
  return {
444
538
  severity: 'error',
445
- title: 'AW CLI 不支持所需 capability',
539
+ title: '当前 AW 版本不支持此插件',
446
540
  userMessage: '当前 AW CLI 不能安装或重新安装这个插件。来源保持可用。',
447
541
  impact: [
448
542
  { state: 'blocked', message: '安装和重新安装会保持阻断,直到 AW CLI 升级。' },
@@ -494,7 +588,7 @@ function SemanticRows(props) {
494
588
  }
495
589
  function PluginRows(props) {
496
590
  return (_jsx(Box, { flexDirection: "column", children: props.rows.slice(props.viewport.offset, props.viewport.offset + props.viewport.limit)
497
- .map((row, visibleIndex) => (_jsx(PluginListRow, { name: row.name, sourceLabel: row.sourceLabel, description: row.description, placementSummary: row.placementSummary, statusLabel: row.statusLabel, status: row.status, focused: props.viewport.offset + visibleIndex === props.focus }, row.pluginId))) }));
591
+ .map((row, visibleIndex) => row.menuAction ? (_jsx(TaskRow, { title: row.name, description: row.description, meta: row.statusLabel, status: row.status, focused: props.viewport.offset + visibleIndex === props.focus, compact: true }, row.pluginId)) : (_jsx(PluginListRow, { name: row.name, sourceLabel: row.sourceLabel, description: row.description, attention: row.attention, placementSummary: row.placementSummary, statusLabel: row.statusLabel, status: row.status, focused: props.viewport.offset + visibleIndex === props.focus }, row.pluginId))) }));
498
592
  }
499
593
  function IssueTechnicalDetails(props) {
500
594
  const visible = props.details.slice(0, props.compact ? 1 : 2);
@@ -576,7 +670,7 @@ function placementStatus(placement) {
576
670
  return {
577
671
  value: `${placement.desiredScopes?.map(scopeLabel).join('+') || scopeLabel(placement.scope)} · ${placement.actualState === 'needsSync' ? '需要同步' : placement.actualState === 'blocked' ? '已阻断' : placement.health === 'ready' ? '可用' : placement.health === 'incomplete' ? '需要处理' : '不可用'}`,
578
672
  status: placement.actualState === 'blocked' ? 'error' : placement.health === 'ready' ? 'success' : placement.health === 'incomplete' ? 'warning' : 'error',
579
- description: placement.actualState === 'needsSync' ? 'desired intent 与现场不同;可安全同步'
673
+ description: placement.actualState === 'needsSync' ? '已保存的使用设置与当前安装不同;可安全同步'
580
674
  : placement.actualState === 'blocked' ? '当前有安全阻断;查看恢复路径'
581
675
  : placement.health === 'ready' ? '当前检查正常' : '查看影响和建议操作',
582
676
  };
@@ -608,7 +702,7 @@ function dependencyPlacementLabel(item) {
608
702
  const state = entry.actualState === 'needsSync' ? '需同步'
609
703
  : entry.actualState === 'blocked' ? '已阻断'
610
704
  : entry.actualState === 'ready' ? '可用' : aggregate.value;
611
- return `${platformLabel(entry.platform)} · desired ${desired} · actual ${actual} · ${state}`;
705
+ return `${platformLabel(entry.platform)} · 已保存 ${desired} · 当前 ${actual} · ${state}`;
612
706
  }).join(' | ');
613
707
  }
614
708
  if (item.aggregateStatus !== 'ready')
@@ -637,7 +731,7 @@ function dependencySummaryText(summary) {
637
731
  function isCatalogSourcesBootstrapFlow(snapshot) {
638
732
  const route = snapshot.router.current.route;
639
733
  if (route.name === 'reviewPreparing') {
640
- return snapshot.reviewPreparation?.intent.kind === 'catalogSourcesRecovery';
734
+ return snapshot.reviewPreparation?.operationKind === 'catalogSourcesRecovery';
641
735
  }
642
736
  if (route.name === 'review' || route.name === 'progress') {
643
737
  return snapshot.activeReview?.operationKind === 'catalogSourcesRecovery';
@@ -647,6 +741,11 @@ function isCatalogSourcesBootstrapFlow(snapshot) {
647
741
  && 'operationKind' in snapshot.result
648
742
  && snapshot.result.operationKind === 'catalogSourcesRecovery';
649
743
  }
744
+ function isOperationHistoryRoute(route) {
745
+ return route.name === 'operationHistory'
746
+ || route.name === 'operationDiagnostic'
747
+ || route.name === 'operationReportPreview';
748
+ }
650
749
  function operationTargetStateLabel(state) {
651
750
  const labels = {
652
751
  completed: { label: '已完成', status: 'success' },
@@ -669,191 +768,14 @@ export function resolveConsoleV4ScreenId(snapshot, controller, overlay, tooSmall
669
768
  return 'G03';
670
769
  if (overlay === 'discard')
671
770
  return 'G04';
672
- if (snapshot.phase !== 'ready' && !isCatalogSourcesBootstrapFlow(snapshot))
771
+ if (snapshot.phase !== 'ready' && !isCatalogSourcesBootstrapFlow(snapshot)
772
+ && !isOperationHistoryRoute(snapshot.router.current.route))
673
773
  return 'G01';
674
- const route = snapshot.router.current.route;
675
- if (route.name === 'home')
676
- return 'G02';
677
- if (route.name === 'pluginList') {
678
- const items = controller.listPlugins({ mode: route.mode, sourceId: route.sourceId });
679
- if (route.mode === 'installed' && items.length === 0)
680
- return 'P12';
681
- if (route.sourceId
682
- && controller.getSourceOverview(route.sourceId).health?.state === 'unavailable')
683
- return 'P03';
684
- return route.mode === 'installed' ? 'P11' : searchMode ? 'P02' : 'P01';
685
- }
686
- if (route.name === 'pluginPreview') {
687
- return controller.getPluginOverview(route.pluginId)?.dependencySummary?.totalCount ? 'D01' : 'P04';
688
- }
689
- if (route.name === 'portableSkillSelect')
690
- return 'U01';
691
- if (route.name === 'portableDependencies')
692
- return 'U02';
693
- if (route.name === 'portableAgentMode')
694
- return 'U03';
695
- if (route.name === 'portableAgentInput')
696
- return 'U04';
697
- if (route.name === 'portableScope')
698
- return 'U05';
699
- if (route.name === 'portableBlocker') {
700
- if (route.code === 'requiredConfigMissing')
701
- return 'U09';
702
- if (route.code === 'portableSkillsCliMissing' || route.code === 'portableSkillsCliIncompatible')
703
- return 'U10';
704
- return 'U11';
705
- }
706
- if (route.name === 'pluginDependencies') {
707
- if (route.view === 'removeBlocked')
708
- return 'D12';
709
- const plugin = controller.getPluginOverview(route.pluginId);
710
- return plugin && hasInstalledPlacement(plugin) ? 'D10' : 'D02';
711
- }
712
- if (route.name === 'installPlacement') {
713
- return controller.getPluginOverview(route.pluginId)?.dependencySummary?.totalCount ? 'D03' : 'P05';
714
- }
715
- if (route.name === 'installTargets') {
716
- if (route.view === 'blocked')
717
- return 'D16';
718
- return controller.getPluginOverview(route.pluginId)?.dependencySummary?.totalCount ? 'D04' : 'P06';
719
- }
720
- if (route.name === 'dependencyPlacement')
721
- return 'D03';
722
- if (route.name === 'dependencyPlacementEditor')
723
- return 'D04';
724
- if (route.name === 'pluginDetail') {
725
- return controller.getPluginOverview(route.pluginId)?.dependencySummary?.totalCount ? 'D09' : 'P13';
726
- }
727
- if (route.name === 'platform') {
728
- const plugin = controller.getPluginOverview(route.pluginId);
729
- if (plugin?.aggregateStatus === 'unavailable')
730
- return 'P17';
731
- const placement = controller.getPlatformPlacement(route.pluginId, route.platform);
732
- if (!placement || placement.state === 'notInstalled')
733
- return 'P15';
734
- if (placement.state === 'installed' && placement.health === 'ready')
735
- return 'P14';
736
- return 'P16';
737
- }
738
- if (route.name === 'reviewPreparing')
739
- return 'P07';
740
- if (route.name === 'review' && snapshot.activeReview?.originIssue)
741
- return 'I10';
742
- const dependencyOperation = snapshot.activeReview?.operationKind === 'pluginWrite'
743
- && Boolean(snapshot.activeReview.changes.some((change) => change.relation === 'direct' || change.relation === 'transitive'
744
- || change.group === 'dependencies'));
745
- if (route.name === 'review' && dependencyOperation) {
746
- return snapshot.activeReview?.pluginAction === 'uninstall' || snapshot.activeReview?.pluginAction === 'cleanup'
747
- ? 'D11' : 'D05';
748
- }
749
- if (route.name === 'progress' && snapshot.progressTargets.length > 0)
750
- return 'D06';
751
- if (route.name === 'result' && snapshot.result) {
752
- const dependencyTargets = snapshot.result.state === 'succeeded' || snapshot.result.state === 'partial'
753
- ? snapshot.result.summary.targets
754
- : snapshot.result.state === 'blocked' && snapshot.result.code === 'operationFailed'
755
- ? snapshot.result.summary?.targets : undefined;
756
- if (dependencyTargets?.some((target) => target.relation !== undefined && target.relation !== 'requested')) {
757
- return snapshot.result.state === 'succeeded' ? 'D07' : 'D08';
758
- }
759
- }
760
- if (route.name === 'progress' && snapshot.activeReview?.originIssue)
761
- return 'I11';
762
- if (route.name === 'result' && snapshot.result?.originIssue) {
763
- return snapshot.result.state === 'succeeded' ? 'I12' : 'I13';
764
- }
765
- if (route.name === 'review' && (snapshot.activeReview?.operationKind === 'sourceMutation'
766
- || snapshot.activeReview?.operationKind === 'catalogGeneration') && snapshot.activeReview.sourceAction) {
767
- return { add: 'S10', update: 'S15', setPrimary: 'S16', remove: 'S18' }[snapshot.activeReview.sourceAction];
768
- }
769
- if (route.name === 'progress' && (snapshot.activeReview?.operationKind === 'sourceMutation'
770
- || snapshot.activeReview?.operationKind === 'catalogGeneration'
771
- || snapshot.activeReview?.operationKind === 'catalogSourcesRecovery'))
772
- return 'S11';
773
- if (route.name === 'result' && snapshot.result && 'operationKind' in snapshot.result
774
- && (snapshot.result.operationKind === 'sourceMutation'
775
- || snapshot.result.operationKind === 'catalogGeneration'
776
- || snapshot.result.operationKind === 'catalogSourcesRecovery')) {
777
- return snapshot.result.state === 'succeeded' ? 'S12' : 'S13';
778
- }
779
- if (route.name === 'review')
780
- return snapshot.activeReview?.operationKind === 'portableInstall' ? 'U06'
781
- : snapshot.activeReview?.operationKind === 'configSave'
782
- ? 'C12'
783
- : snapshot.activeReview?.operationKind === 'configReset'
784
- ? 'C16'
785
- : snapshot.activeReview?.operationKind === 'pluginWrite'
786
- && (snapshot.activeReview.pluginAction === 'refreshAndSync' || snapshot.activeReview.pluginAction === 'reinstall')
787
- ? 'P18'
788
- : snapshot.activeReview?.operationKind === 'pluginWrite'
789
- && (snapshot.activeReview.pluginAction === 'uninstall' || snapshot.activeReview.pluginAction === 'cleanup')
790
- ? 'P19' : 'P07';
791
- if (route.name === 'progress')
792
- return snapshot.activeReview?.operationKind === 'portableInstall' ? 'U07'
793
- : snapshot.activeReview?.operationKind === 'configSave'
794
- || snapshot.activeReview?.operationKind === 'configReset' ? 'C13' : 'P08';
795
- if (route.name === 'result')
796
- return snapshot.result?.operationKind === 'portableInstall' ? 'U08'
797
- : snapshot.result?.operationKind === 'configSave'
798
- || snapshot.result?.operationKind === 'configReset'
799
- ? snapshot.result.state === 'succeeded' ? 'C14' : 'C15'
800
- : snapshot.result?.state === 'partial' ? 'P10' : 'P09';
801
- if (route.name === 'pluginAbout')
802
- return 'P20';
803
- if (route.name === 'pluginTechnical')
804
- return 'P21';
805
- if (route.name === 'configLayer')
806
- return 'C01';
807
- if (route.name === 'configFields')
808
- return route.layer.scope === 'effective' ? 'C02' : 'C03';
809
- if (route.name === 'configFieldAction')
810
- return 'C04';
811
- if (route.name === 'configFieldEditor')
812
- return route.mode === 'choices' ? 'C05' : {
813
- boolean: 'C06', number: 'C07', string: 'C08', object: 'C09',
814
- }[route.editor];
815
- if (route.name === 'configObjectPropertyEditor')
816
- return 'C10';
817
- if (route.name === 'configUnavailable')
818
- return 'C17';
819
- if (route.name === 'sourceList')
820
- return 'S01';
821
- if (route.name === 'sourceDetail')
822
- return 'K02';
823
- if (route.name === 'sourceTechnical')
824
- return 'S03';
825
- if (route.name === 'sourceAdd')
826
- return {
827
- type: 'S04', id: 'S05', location: 'S06', refPolicy: 'S07', ref: 'S08', subdir: 'S09',
828
- }[route.step];
829
- if (route.name === 'sourceEdit')
830
- return 'S14';
831
- if (route.name === 'sourceRemoveBlocked')
832
- return 'S17';
833
- if (route.name === 'issueList') {
834
- const issues = controller.listUserIssues();
835
- return issues.length === 0 ? 'I02' : issues.some((issue) => issue.category.startsWith('dependency')) ? 'D13' : 'I01';
836
- }
837
- if (route.name === 'issueDetail') {
838
- const category = controller.listUserIssues().find((issue) => issue.issueId === route.issueId)?.category;
839
- return {
840
- incompleteInstallation: 'I03',
841
- artifactCollision: 'I09',
842
- sourceUnavailable: 'I04',
843
- sourceMissing: 'I05',
844
- sourceIdentityChanged: 'I06',
845
- requiredConfigMissing: 'I07',
846
- scopeConflict: 'I08',
847
- dependencyMissing: 'D14',
848
- dependencyNotReady: 'D15',
849
- dependencyTargetUnsupported: 'D16',
850
- dependencyStillRequired: 'D12',
851
- dependencyGraphInvalid: 'D17',
852
- legacyState: 'I09',
853
- diagnostic: 'I09',
854
- }[category ?? 'diagnostic'];
855
- }
856
- return 'G02';
774
+ return resolveRouteScreenId(snapshot.router.current.route, {
775
+ snapshot,
776
+ controller,
777
+ searchMode,
778
+ });
857
779
  }
858
780
  export function ConsoleAppV4(props) {
859
781
  const windowSize = useWindowSize();
@@ -863,6 +785,8 @@ export function ConsoleAppV4(props) {
863
785
  const [overlay, setOverlay] = useState();
864
786
  const [overlayFocus, setOverlayFocus] = useState(0);
865
787
  const [discardForExit, setDiscardForExit] = useState(false);
788
+ const [discardForSessionNavigation, setDiscardForSessionNavigation] = useState();
789
+ const [discardDraftConfirmation, setDiscardDraftConfirmation] = useState();
866
790
  const [focus, setFocus] = useState(0);
867
791
  const [scrollOffset, setScrollOffset] = useState(0);
868
792
  const [searchFocus, setSearchFocus] = useState(false);
@@ -882,6 +806,7 @@ export function ConsoleAppV4(props) {
882
806
  const [draftUpdates, setDraftUpdates] = useState({});
883
807
  const [draftRemoves, setDraftRemoves] = useState([]);
884
808
  const [objectDrafts, setObjectDrafts] = useState({});
809
+ const [changedObjectDraftKeys, setChangedObjectDraftKeys] = useState([]);
885
810
  const [objectError, setObjectError] = useState();
886
811
  const [inputValue, setInputValue] = useState('');
887
812
  const [inputError, setInputError] = useState();
@@ -889,25 +814,51 @@ export function ConsoleAppV4(props) {
889
814
  const [actionError, setActionError] = useState();
890
815
  const [configResetAvailability, setConfigResetAvailability] = useState({});
891
816
  const [sourceDraft, setSourceDraft] = useState();
817
+ const [batchDraft, setBatchDraft] = useState();
818
+ const [batchTargetOffset, setBatchTargetOffset] = useState(0);
892
819
  const [sourceInput, setSourceInput] = useState('');
893
820
  const [sourceInputError, setSourceInputError] = useState();
821
+ const [pendingReviewIntent, setPendingReviewIntent] = useState();
822
+ const pendingReviewIntentRef = useRef(undefined);
823
+ const [scriptTimeoutInput, setScriptTimeoutInput] = useState(String(SCRIPT_TIMEOUT_DEFAULT_MS));
824
+ const [scriptTimeoutError, setScriptTimeoutError] = useState();
894
825
  const [sourceReferenceSummary, setSourceReferenceSummary] = useState();
895
826
  const [sourceDependenciesExpanded, setSourceDependenciesExpanded] = useState(false);
896
827
  const [portableAgentInput, setPortableAgentInput] = useState('');
828
+ const [reportSaveMode, setReportSaveMode] = useState(false);
829
+ const [reportSavePath, setReportSavePath] = useState('');
830
+ const [reportSaveResult, setReportSaveResult] = useState();
831
+ const [reportSaveError, setReportSaveError] = useState();
897
832
  const [draftOwner, setDraftOwner] = useState();
898
833
  const [pendingPresetId, setPendingPresetId] = useState();
899
834
  const [pendingLayerFieldKey, setPendingLayerFieldKey] = useState();
900
835
  const [confirmedSharedReviewToken, setConfirmedSharedReviewToken] = useState();
901
836
  const dynamicMenuFocus = useRef(undefined);
837
+ const externalDraftRevision = useRef({ config: 0, source: 0, batch: 0 });
838
+ const previousExternalDraftValues = useRef({
839
+ draftUpdates, draftRemoves, objectDrafts, changedObjectDraftKeys, sourceDraft, batchDraft,
840
+ });
841
+ const externalDraftFlowIds = useRef(new Map());
902
842
  const exitSent = useRef(false);
903
843
  const operationBusy = useRef(false);
904
844
  const route = snapshot.router.current.route;
845
+ const definition = routeDefinition(route);
846
+ const rendererFamily = definition.rendererFamily;
905
847
  const routeKey = JSON.stringify(route);
906
848
  const showTechnical = technicalRouteKey === routeKey;
907
849
  const setShowTechnical = (next) => {
908
850
  const visible = typeof next === 'function' ? next(showTechnical) : next;
909
851
  setTechnicalRouteKey(visible ? routeKey : undefined);
910
852
  };
853
+ const resetRootUiState = () => {
854
+ setFocus(0);
855
+ setScrollOffset(0);
856
+ setSearchFocus(false);
857
+ setQuery('');
858
+ setShowTechnical(false);
859
+ setSourceDependenciesExpanded(false);
860
+ setBatchTargetOffset(0);
861
+ };
911
862
  const plugin = currentPlugin(props.controller, route);
912
863
  const issue = route.name === 'issueDetail'
913
864
  ? props.controller.listUserIssues().find((entry) => entry.issueId === route.issueId)
@@ -931,8 +882,10 @@ export function ConsoleAppV4(props) {
931
882
  useEffect(() => {
932
883
  const unsubscribe = props.controller.subscribe(setSnapshot);
933
884
  if (props.controller.snapshot.phase === 'idle') {
934
- void props.controller.load().catch(() => {
935
- setActionError('操作未完成。请重新检查后重试。');
885
+ void props.controller.load().catch((error) => {
886
+ if (error instanceof Error && error.name === 'AbortError')
887
+ return;
888
+ props.controller.recordUiFailure(error, { code: 'tuiLoadFailed', stage: 'ui-load' });
936
889
  });
937
890
  }
938
891
  return () => {
@@ -941,10 +894,6 @@ export function ConsoleAppV4(props) {
941
894
  props.controller.dispose();
942
895
  };
943
896
  }, [props.controller, props.ownsController]);
944
- useEffect(() => {
945
- props.controller.setExternalDraftState(sourceDraft ? 'sourceDraft' : dirty ? 'configDraft' : undefined);
946
- return () => props.controller.setExternalDraftState(undefined);
947
- }, [props.controller, dirty, sourceDraft]);
948
897
  useEffect(() => {
949
898
  if (!exitAfterBusy || !snapshot.result)
950
899
  return;
@@ -969,18 +918,49 @@ export function ConsoleAppV4(props) {
969
918
  || (snapshot.result.state !== 'succeeded' && snapshot.result.state !== 'partial')
970
919
  || !snapshot.result.operationKind)
971
920
  return;
972
- setDirty(false);
973
- setDraftUpdates({});
974
- setDraftRemoves([]);
975
- setObjectDrafts({});
976
- setObjectError(undefined);
977
- if (snapshot.result.operationKind === 'sourceMutation')
921
+ if (snapshot.result.operationKind === 'configSave'
922
+ || snapshot.result.operationKind === 'configReset') {
923
+ setDirty(false);
924
+ setDraftUpdates({});
925
+ setDraftRemoves([]);
926
+ setObjectDrafts({});
927
+ setChangedObjectDraftKeys([]);
928
+ setObjectError(undefined);
929
+ }
930
+ if ((snapshot.result.operationKind === 'sourceMutation'
931
+ || snapshot.result.operationKind === 'catalogGeneration') && sourceDraft) {
978
932
  setSourceDraft(undefined);
933
+ setDirty(Object.keys(draftUpdates).length + draftRemoves.length + changedObjectDraftKeys.length > 0);
934
+ }
979
935
  }, [route.name, snapshot.result]);
936
+ useEffect(() => {
937
+ if (route.name !== 'result' || snapshot.result?.operationKind !== 'pluginWriteBatch')
938
+ return;
939
+ setBatchDraft(undefined);
940
+ setDirty(false);
941
+ }, [route.name, snapshot.result?.operationKind]);
942
+ useEffect(() => {
943
+ if (!batchDraft || (route.name !== 'batchSelection' && route.name !== 'batchPlacement'))
944
+ return;
945
+ setDirty(true);
946
+ }, [batchDraft, route.name]);
980
947
  useEffect(() => {
981
948
  if (route.name !== 'sourceAdd')
982
949
  setSourceInput('');
983
950
  }, [route.name]);
951
+ useEffect(() => {
952
+ if (route.name === 'operationReportPreview')
953
+ return;
954
+ setReportSaveMode(false);
955
+ setReportSavePath('');
956
+ setReportSaveResult(undefined);
957
+ setReportSaveError(undefined);
958
+ }, [route.name]);
959
+ useEffect(() => {
960
+ if (route.name !== 'home')
961
+ return;
962
+ resetRootUiState();
963
+ }, [route.name]);
984
964
  useEffect(() => {
985
965
  if (route.name === 'portableAgentInput') {
986
966
  setPortableAgentInput(snapshot.portableDraft?.requestedAgents.join(' ') ?? '');
@@ -997,9 +977,96 @@ export function ConsoleAppV4(props) {
997
977
  || route.name === 'configFieldEditor'
998
978
  || route.name === 'configObjectPropertyEditor'
999
979
  || route.name === 'configFieldAction'
980
+ || route.name === 'configSave'
1000
981
  ? route
1001
982
  : undefined;
983
+ const configRouteOwner = configRoute ? configOwnerKey(configRoute) : undefined;
984
+ useEffect(() => {
985
+ const previous = previousExternalDraftValues.current;
986
+ if (previous.draftUpdates !== draftUpdates
987
+ || previous.draftRemoves !== draftRemoves
988
+ || previous.objectDrafts !== objectDrafts
989
+ || previous.changedObjectDraftKeys !== changedObjectDraftKeys) {
990
+ externalDraftRevision.current.config += 1;
991
+ }
992
+ if (previous.sourceDraft !== sourceDraft)
993
+ externalDraftRevision.current.source += 1;
994
+ if (previous.batchDraft !== batchDraft)
995
+ externalDraftRevision.current.batch += 1;
996
+ previousExternalDraftValues.current = {
997
+ draftUpdates, draftRemoves, objectDrafts, changedObjectDraftKeys, sourceDraft, batchDraft,
998
+ };
999
+ const currentFlowId = snapshot.router.currentFlowId;
1000
+ if (currentFlowId && configRouteOwner) {
1001
+ externalDraftFlowIds.current.set(configRouteOwner, currentFlowId);
1002
+ }
1003
+ if (currentFlowId && sourceDraft)
1004
+ externalDraftFlowIds.current.set(sourceDraft.owner, currentFlowId);
1005
+ if (currentFlowId && batchDraft)
1006
+ externalDraftFlowIds.current.set('batch-write', currentFlowId);
1007
+ const drafts = [];
1008
+ if ((dirty || changedObjectDraftKeys.length > 0) && draftOwner) {
1009
+ const flowId = externalDraftFlowIds.current.get(draftOwner);
1010
+ drafts.push({
1011
+ kind: 'configDraft', owner: draftOwner, revision: externalDraftRevision.current.config,
1012
+ ...(flowId ? { flowId } : {}),
1013
+ });
1014
+ }
1015
+ if (sourceDraft) {
1016
+ const flowId = externalDraftFlowIds.current.get(sourceDraft.owner);
1017
+ drafts.push({
1018
+ kind: 'sourceDraft', owner: sourceDraft.owner, revision: externalDraftRevision.current.source,
1019
+ ...(flowId ? { flowId } : {}),
1020
+ });
1021
+ }
1022
+ if (batchDraft) {
1023
+ const flowId = externalDraftFlowIds.current.get('batch-write');
1024
+ drafts.push({
1025
+ kind: 'batchDraft', owner: 'batch-write', revision: externalDraftRevision.current.batch,
1026
+ ...(flowId ? { flowId } : {}),
1027
+ });
1028
+ }
1029
+ props.controller.setExternalDraftState(drafts);
1030
+ }, [
1031
+ props.controller, batchDraft, changedObjectDraftKeys, configRouteOwner, dirty, draftOwner,
1032
+ draftRemoves, draftUpdates, objectDrafts, snapshot.router.currentFlowId, sourceDraft,
1033
+ ]);
1034
+ useEffect(() => () => props.controller.setExternalDraftState(), [props.controller]);
1002
1035
  const currentConfigPage = configPageMatches(configPage, configRoute) ? configPage : undefined;
1036
+ const configDraftIssues = (() => {
1037
+ if (!configRoute || !currentConfigPage)
1038
+ return [];
1039
+ const fields = new Map(currentConfigPage.groups.flatMap((group) => group.fields)
1040
+ .map((field) => [field.key, field]));
1041
+ const issues = [];
1042
+ for (const [key, value] of Object.entries(draftUpdates)) {
1043
+ const field = fields.get(key);
1044
+ if (!field || !configFieldCanSet(field)
1045
+ || validatePluginConfigValue(field.definition, value, key).length > 0) {
1046
+ issues.push({ key, source: 'update' });
1047
+ }
1048
+ }
1049
+ for (const key of draftRemoves) {
1050
+ const field = fields.get(key);
1051
+ if (!field || !configFieldCanUnset(field))
1052
+ issues.push({ key, source: 'remove' });
1053
+ }
1054
+ const owner = `${configOwnerKey(configRoute)}\0`;
1055
+ for (const objectKey of changedObjectDraftKeys) {
1056
+ if (!objectKey.startsWith(owner))
1057
+ continue;
1058
+ const key = objectKey.slice(owner.length);
1059
+ if (issues.some((issue) => issue.key === key))
1060
+ continue;
1061
+ const field = fields.get(key);
1062
+ const value = objectDrafts[objectKey];
1063
+ if (!field || !configFieldCanSet(field) || field.definition.type !== 'object' || !value
1064
+ || validatePluginConfigValue(field.definition, value, key).length > 0) {
1065
+ issues.push({ key, source: 'object' });
1066
+ }
1067
+ }
1068
+ return issues;
1069
+ })();
1003
1070
  useEffect(() => {
1004
1071
  if (route.name !== 'configLayer')
1005
1072
  return;
@@ -1031,6 +1098,7 @@ export function ConsoleAppV4(props) {
1031
1098
  setDraftUpdates({});
1032
1099
  setDraftRemoves([]);
1033
1100
  setObjectDrafts({});
1101
+ setChangedObjectDraftKeys([]);
1034
1102
  setObjectError(undefined);
1035
1103
  setDirty(false);
1036
1104
  setConfigActionError(undefined);
@@ -1065,6 +1133,17 @@ export function ConsoleAppV4(props) {
1065
1133
  diagnosticStage: 'read-config',
1066
1134
  ...(invalidStores ? { invalidStores } : {}),
1067
1135
  ...(layer ? { layer } : {}),
1136
+ ...(configRoute ? { resumeRoute: structuredClone(configRoute.name === 'configSave'
1137
+ ? { name: 'configFields', pluginId: configRoute.pluginId, layer: configRoute.layer }
1138
+ : configRoute) } : {}),
1139
+ ...(configRoute ? { resumeContext: {
1140
+ ...(snapshot.router.current.context.configGroupId !== undefined
1141
+ ? { configGroupId: snapshot.router.current.context.configGroupId } : {}),
1142
+ ...(snapshot.router.current.context.focusIndex !== undefined
1143
+ ? { focusIndex: snapshot.router.current.context.focusIndex } : {}),
1144
+ ...(snapshot.router.current.context.scrollOffset !== undefined
1145
+ ? { scrollOffset: snapshot.router.current.context.scrollOffset } : {}),
1146
+ } } : {}),
1068
1147
  });
1069
1148
  }
1070
1149
  });
@@ -1072,13 +1151,14 @@ export function ConsoleAppV4(props) {
1072
1151
  }, [configRoute?.pluginId, configRoute?.layer.scope, props.controller]);
1073
1152
  useEffect(() => {
1074
1153
  if (route.name !== 'configUnavailable'
1075
- || (!route.invalidStores?.length && !route.layer)) {
1154
+ || (!route.invalidStores?.length && !route.storedScopes?.length && !route.layer)) {
1076
1155
  setConfigResetAvailability({});
1077
1156
  return;
1078
1157
  }
1079
1158
  const abort = new AbortController();
1080
1159
  setConfigResetAvailability({});
1081
1160
  const scopes = route.invalidStores?.map(({ scope }) => scope)
1161
+ ?? route.storedScopes
1082
1162
  ?? (route.layer ? [route.layer.scope] : []);
1083
1163
  void Promise.all(scopes.map(async (scope) => {
1084
1164
  try {
@@ -1105,6 +1185,7 @@ export function ConsoleAppV4(props) {
1105
1185
  route.name === 'configUnavailable'
1106
1186
  ? route.invalidStores?.map((store) => `${store.scope}:${store.code}`).join('|') ?? ''
1107
1187
  : '',
1188
+ route.name === 'configUnavailable' ? route.storedScopes?.join('|') ?? '' : '',
1108
1189
  route.name === 'configUnavailable' ? route.layer?.scope ?? '' : '',
1109
1190
  ]);
1110
1191
  useEffect(() => {
@@ -1121,11 +1202,14 @@ export function ConsoleAppV4(props) {
1121
1202
  useEffect(() => {
1122
1203
  let count;
1123
1204
  if (route.name === 'sourceList')
1124
- count = props.controller.listSources().length + 1;
1205
+ count = props.controller.listSources().length + 2;
1125
1206
  else if (route.name === 'issueList') {
1126
1207
  const issueCount = props.controller.listUserIssues().length;
1127
1208
  count = issueCount || 2;
1128
1209
  }
1210
+ else if (route.name === 'operationHistory') {
1211
+ count = snapshot.operationHistorySummaries.length + 1;
1212
+ }
1129
1213
  else if (route.name === 'pluginList') {
1130
1214
  const itemCount = props.controller.listPlugins({
1131
1215
  mode: route.mode,
@@ -1133,7 +1217,8 @@ export function ConsoleAppV4(props) {
1133
1217
  ...(query ? { query } : {}),
1134
1218
  installFilter,
1135
1219
  }).length;
1136
- count = itemCount || 1;
1220
+ const menuCount = itemCount > 0 ? route.mode === 'installed' ? 2 : 1 : 0;
1221
+ count = itemCount + menuCount || 1;
1137
1222
  }
1138
1223
  if (count === undefined)
1139
1224
  return;
@@ -1149,11 +1234,32 @@ export function ConsoleAppV4(props) {
1149
1234
  route.name === 'pluginList' ? route.sourceId : '',
1150
1235
  snapshot.viewModel,
1151
1236
  ]);
1237
+ useEffect(() => {
1238
+ if (route.name !== 'operationReportPreview')
1239
+ return;
1240
+ setScrollOffset(0);
1241
+ }, [route.name, route.name === 'operationReportPreview' ? route.recordId : '']);
1242
+ useEffect(() => {
1243
+ setBatchTargetOffset(0);
1244
+ }, [route.name, route.name === 'review' || route.name === 'result' ? route.operationId : '']);
1152
1245
  const activeField = route.name === 'configFieldEditor'
1153
1246
  || route.name === 'configObjectPropertyEditor'
1154
1247
  || route.name === 'configFieldAction'
1155
1248
  ? currentConfigPage?.groups.flatMap((group) => group.fields).find((field) => field.key === route.fieldKey)
1156
1249
  : undefined;
1250
+ useEffect(() => {
1251
+ if (!configRoute
1252
+ || configRoute.name === 'configFields'
1253
+ || configRoute.name === 'configSave'
1254
+ || !currentConfigPage
1255
+ || activeField)
1256
+ return;
1257
+ props.controller.replace({
1258
+ name: 'configFields',
1259
+ pluginId: configRoute.pluginId,
1260
+ layer: configRoute.layer,
1261
+ }, snapshot.router.current.context);
1262
+ }, [props.controller, configRoute, currentConfigPage, activeField, snapshot.router.current.context]);
1157
1263
  const activeObjectKey = activeField?.type === 'object' && configRoute
1158
1264
  ? `${configOwnerKey(configRoute)}\0${activeField.key}`
1159
1265
  : undefined;
@@ -1219,6 +1325,14 @@ export function ConsoleAppV4(props) {
1219
1325
  : route.step === 'subdir' ? draft?.subdir ?? '' : '');
1220
1326
  setSourceInputError(undefined);
1221
1327
  }, [route.name, route.name === 'sourceAdd' ? `${route.owner}:${route.step}` : '', sourceDraft?.owner]);
1328
+ useEffect(() => {
1329
+ if (route.name !== 'scriptTimeout')
1330
+ return;
1331
+ setScriptTimeoutInput(String(pendingReviewIntent && 'scriptTimeoutMs' in pendingReviewIntent
1332
+ ? pendingReviewIntent.scriptTimeoutMs ?? SCRIPT_TIMEOUT_DEFAULT_MS
1333
+ : SCRIPT_TIMEOUT_DEFAULT_MS));
1334
+ setScriptTimeoutError(undefined);
1335
+ }, [route.name, route.name === 'scriptTimeout' ? route.owner : '']);
1222
1336
  useEffect(() => {
1223
1337
  if (route.name !== 'sourceRemoveBlocked')
1224
1338
  return;
@@ -1237,6 +1351,7 @@ export function ConsoleAppV4(props) {
1237
1351
  ? snapshot.portableDraft.candidates.filter(portableScopeVariantsDiffer)
1238
1352
  : [];
1239
1353
  const supportsTechnicalDetails = snapshot.phase === 'error'
1354
+ || Boolean(route.name === 'reviewPreparing' && snapshot.reviewPreparation?.code)
1240
1355
  || Boolean(route.name === 'progress' && snapshot.progress.messages.length > 0)
1241
1356
  || Boolean(route.name === 'review' && snapshot.activeReview?.technicalDetails?.length)
1242
1357
  || Boolean(route.name === 'issueDetail' && issue
@@ -1294,13 +1409,26 @@ export function ConsoleAppV4(props) {
1294
1409
  staticRows,
1295
1410
  });
1296
1411
  const dependencyPlacementStaticRows = policy.mode === 'compact' ? 3 : 10;
1412
+ const actionViewportStaticRows = route.name === 'dependencyPlacement'
1413
+ ? dependencyPlacementStaticRows
1414
+ : route.name === 'operationHistory' ? 4
1415
+ : route.name === 'operationReportPreview' ? 5 : 0;
1297
1416
  const responsiveBudget = (compactRows, standardRows) => {
1298
1417
  const baseline = policy.mode === 'compact' ? 18 : 24;
1299
1418
  const base = policy.mode === 'compact' ? compactRows : standardRows;
1300
1419
  return base + Math.max(0, size.rows - baseline);
1301
1420
  };
1302
1421
  const go = (next) => {
1303
- props.controller.updatePageContext({ focusIndex: focus, scrollOffset, query, installFilter, sourceTabId: route.name === 'pluginList' ? route.sourceId ?? 'all' : undefined });
1422
+ props.controller.updatePageContext({
1423
+ focusIndex: focus,
1424
+ scrollOffset,
1425
+ query,
1426
+ installFilter,
1427
+ sourceTabId: route.name === 'pluginList' ? route.sourceId ?? 'all' : undefined,
1428
+ focusedEntityId: route.name === 'pluginList'
1429
+ ? pluginRows[focus]?.pluginId
1430
+ : route.name === 'sourceList' ? props.controller.listSources()[focus]?.id : undefined,
1431
+ });
1304
1432
  props.controller.push(next);
1305
1433
  setFocus(0);
1306
1434
  setScrollOffset(0);
@@ -1308,6 +1436,51 @@ export function ConsoleAppV4(props) {
1308
1436
  setShowTechnical(false);
1309
1437
  setActionError(undefined);
1310
1438
  };
1439
+ const toggleTechnical = () => {
1440
+ setShowTechnical((value) => !value);
1441
+ setBatchTargetOffset(0);
1442
+ };
1443
+ const restorePageContext = () => {
1444
+ const restoredFrame = props.controller.snapshot.router.current;
1445
+ const restored = restoredFrame.context;
1446
+ let restoredFocus = restored.focusIndex ?? 0;
1447
+ let itemCount;
1448
+ if (restoredFrame.route.name === 'pluginList') {
1449
+ const items = props.controller.listPlugins({
1450
+ mode: restoredFrame.route.mode,
1451
+ ...(restoredFrame.route.sourceId ? { sourceId: restoredFrame.route.sourceId } : {}),
1452
+ ...(restored.query ? { query: restored.query } : {}),
1453
+ installFilter: restored.installFilter ?? 'all',
1454
+ });
1455
+ itemCount = items.length;
1456
+ const entityIndex = restored.focusedEntityId
1457
+ ? items.findIndex((item) => item.pluginId === restored.focusedEntityId)
1458
+ : -1;
1459
+ restoredFocus = entityIndex >= 0
1460
+ ? entityIndex
1461
+ : Math.min(restoredFocus, Math.max(0, items.length - 1));
1462
+ }
1463
+ else if (restoredFrame.route.name === 'sourceList') {
1464
+ const sources = props.controller.listSources();
1465
+ itemCount = sources.length + 2;
1466
+ const entityIndex = restored.focusedEntityId
1467
+ ? sources.findIndex((source) => source.id === restored.focusedEntityId)
1468
+ : -1;
1469
+ restoredFocus = entityIndex >= 0
1470
+ ? entityIndex
1471
+ : Math.min(restoredFocus, Math.max(0, itemCount - 1));
1472
+ }
1473
+ let restoredScroll = Math.min(restored.scrollOffset ?? 0, Math.max(0, (itemCount ?? 1) - 1));
1474
+ if (restoredFocus < restoredScroll)
1475
+ restoredScroll = restoredFocus;
1476
+ else if (restoredFocus >= restoredScroll + 4)
1477
+ restoredScroll = Math.max(0, restoredFocus - 3);
1478
+ setFocus(restoredFocus);
1479
+ setScrollOffset(restoredScroll);
1480
+ setQuery(restored.query ?? '');
1481
+ setInstallFilter(restored.installFilter ?? 'all');
1482
+ setSearchFocus(false);
1483
+ };
1311
1484
  const beginConfigFlow = (pluginId) => {
1312
1485
  props.controller.updatePageContext({
1313
1486
  focusIndex: focus,
@@ -1324,30 +1497,51 @@ export function ConsoleAppV4(props) {
1324
1497
  setActionError(undefined);
1325
1498
  };
1326
1499
  const back = () => {
1500
+ if (route.name === 'sourceEdit'
1501
+ && sourceDraft
1502
+ && sourceDraft.owner === route.owner
1503
+ && sourceDraftChanges(sourceDraft).length === 0)
1504
+ setSourceDraft(undefined);
1505
+ if (route.name === 'sourceAdd'
1506
+ && route.returnToDraft
1507
+ && sourceDraft
1508
+ && sourceDraft.owner === route.owner
1509
+ && sourceDraftChanges(sourceDraft).length === 0)
1510
+ setSourceDraft(undefined);
1327
1511
  if (route.name === 'result')
1328
1512
  props.controller.leaveResult();
1329
1513
  else
1330
1514
  props.controller.back();
1331
- const restored = props.controller.snapshot.router.current.context;
1332
- setFocus(restored.focusIndex ?? 0);
1333
- setScrollOffset(restored.scrollOffset ?? 0);
1334
- setQuery(restored.query ?? '');
1335
- setInstallFilter(restored.installFilter ?? 'all');
1336
- setSearchFocus(false);
1515
+ restorePageContext();
1516
+ setActionError(undefined);
1517
+ };
1518
+ const acceptSourceFieldAndBack = () => {
1519
+ props.controller.back();
1520
+ restorePageContext();
1337
1521
  setActionError(undefined);
1338
1522
  };
1523
+ const showSafeUiFailure = (error, code, stage) => {
1524
+ if (error instanceof Error && error.name === 'AbortError')
1525
+ return;
1526
+ const recordId = props.controller.recordUiFailure(error, { code, stage });
1527
+ go({ name: 'operationDiagnostic', recordId });
1528
+ };
1339
1529
  const leaveResult = (target) => {
1340
1530
  props.controller.leaveResult(target);
1341
- const restored = props.controller.snapshot.router.current.context;
1342
- setFocus(restored.focusIndex ?? 0);
1343
- setScrollOffset(restored.scrollOffset ?? 0);
1344
- setQuery(restored.query ?? '');
1345
- setInstallFilter(restored.installFilter ?? 'all');
1346
- setSearchFocus(false);
1531
+ restorePageContext();
1347
1532
  setShowTechnical(false);
1348
1533
  setActionError(undefined);
1349
1534
  };
1350
1535
  const beginReview = (intent) => {
1536
+ const supportsScriptTimeout = intent.kind === 'pluginWrite'
1537
+ && (intent.action === 'install' || intent.action === 'sync'
1538
+ || intent.action === 'refreshAndSync' || intent.action === 'reinstall')
1539
+ || intent.kind === 'pluginWriteBatch'
1540
+ && (intent.action === 'install' || intent.action === 'sync' || intent.action === 'reinstall')
1541
+ || intent.kind === 'configSave' && intent.apply;
1542
+ const pendingIntent = supportsScriptTimeout ? intent : undefined;
1543
+ pendingReviewIntentRef.current = pendingIntent;
1544
+ setPendingReviewIntent(pendingIntent);
1351
1545
  const underlyingResultRoute = route.name === 'result' && snapshot.result && 'returnRoute' in snapshot.result
1352
1546
  ? snapshot.result.returnRoute
1353
1547
  : undefined;
@@ -1362,6 +1556,28 @@ export function ConsoleAppV4(props) {
1362
1556
  setFocus(0);
1363
1557
  setScrollOffset(0);
1364
1558
  };
1559
+ const openBatchSelection = (action) => {
1560
+ props.controller.updatePageContext({
1561
+ focusIndex: focus,
1562
+ scrollOffset,
1563
+ query,
1564
+ installFilter,
1565
+ sourceTabId: route.name === 'pluginList' ? route.sourceId ?? 'all' : undefined,
1566
+ focusedEntityId: route.name === 'pluginList' ? pluginRows[focus]?.pluginId : undefined,
1567
+ });
1568
+ props.controller.beginPluginWriteBatch();
1569
+ setBatchDraft({
1570
+ ...(action ? { action } : {}),
1571
+ selection: 'selected',
1572
+ selectedPluginIds: [],
1573
+ platform: 'both',
1574
+ scope: 'project',
1575
+ });
1576
+ setDirty(true);
1577
+ setFocus(0);
1578
+ setScrollOffset(0);
1579
+ setSearchFocus(false);
1580
+ };
1365
1581
  const continueInstallAfterTargets = async (candidate, targets) => {
1366
1582
  if (props.controller.snapshot.installPlacementDraft?.pluginId !== candidate.pluginId) {
1367
1583
  props.controller.beginInstallFlow(candidate.pluginId, scope);
@@ -1460,6 +1676,22 @@ export function ConsoleAppV4(props) {
1460
1676
  recoveryDescription: action.description,
1461
1677
  });
1462
1678
  };
1679
+ const beginRestrictedForceCleanup = async (pluginId, platform, selectedScope) => {
1680
+ const candidate = await props.controller.getForceCleanupCandidate(pluginId, platform, selectedScope);
1681
+ if (!candidate) {
1682
+ setActionError('当前没有可安全确认的受限清理目标。共享、外来、符号链接和原生内容会保持不变。');
1683
+ return;
1684
+ }
1685
+ try {
1686
+ props.controller.startForceCleanupReview(candidate);
1687
+ setFocus(0);
1688
+ setScrollOffset(0);
1689
+ setActionError(undefined);
1690
+ }
1691
+ catch (error) {
1692
+ showSafeUiFailure(error, 'tuiForceCleanupReviewFailed', 'force-cleanup-review');
1693
+ }
1694
+ };
1463
1695
  const beginAddSource = () => {
1464
1696
  const owner = `source-add:${Date.now()}`;
1465
1697
  setSourceDraft({ owner, mode: 'add', sourceId: '', type: 'git', location: '', refPolicy: 'cliChannel', ref: '', subdir: '', autoCheck: true });
@@ -1541,35 +1773,103 @@ export function ConsoleAppV4(props) {
1541
1773
  path: draft.location,
1542
1774
  ...(draft.autoCheck ? {} : { autoCheck: false }),
1543
1775
  };
1544
- const beginSourceAddReviewWith = async (draft) => {
1545
- const source = sourceFromDraft(draft);
1546
- setDirty(false);
1547
- await beginReview({
1548
- kind: 'catalogGeneration',
1549
- action: 'activate',
1776
+ const sourceDraftFromSource = (source, owner) => source.type === 'git'
1777
+ ? {
1778
+ owner,
1779
+ mode: 'update',
1550
1780
  sourceId: source.id,
1551
- sourceMutation: { action: 'add', source },
1552
- });
1781
+ type: 'git',
1782
+ location: source.url,
1783
+ refPolicy: source.refPolicy.type,
1784
+ ref: source.refPolicy.type === 'explicit' ? source.refPolicy.ref : '',
1785
+ subdir: source.subdir ?? '',
1786
+ autoCheck: source.autoCheck !== false,
1787
+ }
1788
+ : {
1789
+ owner,
1790
+ mode: 'update',
1791
+ sourceId: source.id,
1792
+ type: 'path',
1793
+ location: source.path,
1794
+ refPolicy: 'cliChannel',
1795
+ ref: '',
1796
+ subdir: '',
1797
+ autoCheck: source.autoCheck !== false,
1798
+ };
1799
+ const sourceUpdateFromDraft = (draft) => draft.type === 'git'
1800
+ ? {
1801
+ type: 'git',
1802
+ url: draft.location,
1803
+ refPolicy: draft.refPolicy === 'cliChannel'
1804
+ ? { type: 'cliChannel' }
1805
+ : { type: 'explicit', ref: draft.ref },
1806
+ subdir: draft.subdir || null,
1807
+ autoCheck: draft.autoCheck,
1808
+ }
1809
+ : { type: 'path', path: draft.location, autoCheck: draft.autoCheck };
1810
+ const sourceDraftChanges = (draft) => {
1811
+ const current = draft.mode === 'update' ? props.controller.getConfiguredSource(draft.sourceId) : undefined;
1812
+ const location = (source) => source
1813
+ ? projectCatalogSourceLocation(source.type === 'git' ? source.url : source.path) : undefined;
1814
+ const rawLocation = (source) => source
1815
+ ? source.type === 'git' ? source.url : source.path : undefined;
1816
+ const ref = (source) => source?.type === 'git'
1817
+ ? source.refPolicy.type === 'cliChannel' ? '跟随 AW CLI' : source.refPolicy.ref
1818
+ : undefined;
1819
+ const candidates = [
1820
+ {
1821
+ title: draft.type === 'git' ? 'Git 仓库地址' : '本地目录',
1822
+ before: location(current),
1823
+ after: location(sourceFromDraft(draft)),
1824
+ changed: rawLocation(current) !== draft.location,
1825
+ },
1826
+ ...(draft.type === 'git' ? [
1827
+ { title: '版本引用', before: ref(current), after: draft.refPolicy === 'cliChannel' ? '跟随 AW CLI' : draft.ref, changed: ref(current) !== (draft.refPolicy === 'cliChannel' ? '跟随 AW CLI' : draft.ref) },
1828
+ { title: 'Catalog 子目录', before: current?.type === 'git' ? current.subdir ?? '未设置' : undefined, after: draft.subdir || '未设置', changed: current?.type !== 'git' || (current.subdir ?? '') !== draft.subdir },
1829
+ ] : []),
1830
+ { title: '自动检查更新', before: current ? current.autoCheck !== false ? '开启' : '关闭' : undefined, after: draft.autoCheck ? '开启' : '关闭', changed: !current || (current.autoCheck !== false) !== draft.autoCheck },
1831
+ ];
1832
+ return candidates.filter((change) => change.changed)
1833
+ .map(({ changed: _changed, ...change }) => change);
1553
1834
  };
1554
- const beginSourceUpdateReview = async (draft) => {
1555
- const detail = props.controller.getSourceOverview(draft.sourceId).detail;
1556
- if (!detail || !draft.editField)
1835
+ const beginSourceReview = (draft, activate) => {
1836
+ const mutation = draft.mode === 'add'
1837
+ ? { action: 'add', source: sourceFromDraft(draft) }
1838
+ : { action: 'update', update: sourceUpdateFromDraft(draft) };
1839
+ if (activate) {
1840
+ beginReview({
1841
+ kind: 'catalogGeneration',
1842
+ action: 'activate',
1843
+ sourceId: draft.sourceId,
1844
+ sourceMutation: mutation,
1845
+ });
1557
1846
  return;
1558
- const update = draft.type === 'path'
1559
- ? { type: 'path', path: draft.location }
1560
- : draft.editField === 'location'
1561
- ? { type: 'git', url: draft.location }
1562
- : draft.editField === 'refPolicy'
1563
- ? { type: 'git', refPolicy: draft.refPolicy === 'cliChannel' ? { type: 'cliChannel' } : { type: 'explicit', ref: draft.ref } }
1564
- : { type: 'git', subdir: draft.subdir || null };
1565
- setDirty(false);
1566
- await beginReview({
1567
- kind: 'catalogGeneration',
1847
+ }
1848
+ beginReview(draft.mode === 'add' ? {
1849
+ kind: 'sourceMutation',
1850
+ action: 'add',
1851
+ sourceId: draft.sourceId,
1852
+ source: mutation.action === 'add' ? mutation.source : undefined,
1853
+ changes: sourceDraftChanges(draft),
1854
+ } : {
1855
+ kind: 'sourceMutation',
1568
1856
  action: 'update',
1569
1857
  sourceId: draft.sourceId,
1570
- sourceMutation: { action: 'update', update },
1858
+ update: mutation.action === 'update' ? mutation.update : undefined,
1859
+ changes: sourceDraftChanges(draft),
1571
1860
  });
1572
1861
  };
1862
+ const openSourceEdit = (sourceId) => {
1863
+ const source = props.controller.getConfiguredSource(sourceId);
1864
+ if (!source) {
1865
+ setActionError('无法读取完整来源配置。请重新加载后重试。');
1866
+ return;
1867
+ }
1868
+ const owner = `source-update:${sourceId}:${Date.now()}`;
1869
+ setSourceDraft(sourceDraftFromSource(source, owner));
1870
+ setDirty(false);
1871
+ go({ name: 'sourceEdit', sourceId, owner });
1872
+ };
1573
1873
  const configLayerLabel = (layer) => layer.scope === 'effective' ? '有效设置'
1574
1874
  : layer.scope === 'user' ? '个人默认'
1575
1875
  : '当前项目';
@@ -1663,10 +1963,14 @@ export function ConsoleAppV4(props) {
1663
1963
  if (!activeObjectKey)
1664
1964
  return;
1665
1965
  setObjectDrafts((current) => ({ ...current, [activeObjectKey]: value }));
1966
+ const initial = objectValueFromUpdate ?? objectValueFromField ?? {};
1967
+ setChangedObjectDraftKeys((current) => configValuesEqual(initial, value)
1968
+ ? current.filter((key) => key !== activeObjectKey)
1969
+ : [...new Set([...current, activeObjectKey])]);
1666
1970
  setObjectError(undefined);
1667
1971
  setConfigActionError(undefined);
1668
1972
  };
1669
- const objectIssueText = (issue) => `${issue.path.property ?? issue.path.field} · ${issue.code} · ${issue.message}`;
1973
+ const objectIssueText = (issue) => `${issue.path.property ?? issue.path.field}:${issue.message}`;
1670
1974
  const commitObjectDraft = () => {
1671
1975
  if (!activeField || activeField.definition.type !== 'object' || !activeObjectDraft)
1672
1976
  return;
@@ -1676,6 +1980,9 @@ export function ConsoleAppV4(props) {
1676
1980
  return;
1677
1981
  }
1678
1982
  updateDraft(activeField.key, activeObjectDraft);
1983
+ if (activeObjectKey) {
1984
+ setChangedObjectDraftKeys((current) => current.filter((key) => key !== activeObjectKey));
1985
+ }
1679
1986
  back();
1680
1987
  };
1681
1988
  const objectPropertyStage = (definition) => definition.type === 'boolean' || definition.choices ? 'choices' : 'custom';
@@ -1860,9 +2167,33 @@ export function ConsoleAppV4(props) {
1860
2167
  };
1861
2168
  });
1862
2169
  };
2170
+ const discardInvalidConfigDraftChanges = () => {
2171
+ if (!configRoute || configDraftIssues.length === 0)
2172
+ return;
2173
+ const invalidKeys = new Set(configDraftIssues.map((issue) => issue.key));
2174
+ const nextUpdates = Object.fromEntries(Object.entries(draftUpdates)
2175
+ .filter(([key]) => !invalidKeys.has(key)));
2176
+ const nextRemoves = draftRemoves.filter((key) => !invalidKeys.has(key));
2177
+ const owner = `${configOwnerKey(configRoute)}\0`;
2178
+ const nextChangedObjectKeys = changedObjectDraftKeys.filter((key) => !key.startsWith(owner)
2179
+ || !invalidKeys.has(key.slice(owner.length)));
2180
+ setDraftUpdates(nextUpdates);
2181
+ setDraftRemoves(nextRemoves);
2182
+ setObjectDrafts((current) => Object.fromEntries(Object.entries(current)
2183
+ .filter(([key]) => !key.startsWith(owner)
2184
+ || !invalidKeys.has(key.slice(owner.length)))));
2185
+ setChangedObjectDraftKeys(nextChangedObjectKeys);
2186
+ setDirty(Object.keys(nextUpdates).length + nextRemoves.length + nextChangedObjectKeys.length > 0);
2187
+ setObjectError(undefined);
2188
+ setConfigActionError(undefined);
2189
+ };
1863
2190
  const beginConfigSave = async (apply) => {
1864
2191
  if (!configRoute || configRoute.layer.scope === 'effective' || !currentConfigPage || !dirty)
1865
2192
  return;
2193
+ if (configDraftIssues.length > 0) {
2194
+ setConfigActionError('字段已变化。请先放弃失效草稿,或返回继续检查来源。');
2195
+ return;
2196
+ }
1866
2197
  const fields = currentConfigPage.groups.flatMap((group) => group.fields);
1867
2198
  try {
1868
2199
  await beginReview({
@@ -1879,7 +2210,8 @@ export function ConsoleAppV4(props) {
1879
2210
  });
1880
2211
  setConfigActionError(undefined);
1881
2212
  }
1882
- catch {
2213
+ catch (error) {
2214
+ props.controller.recordUiFailure(error, { code: 'tuiConfigReviewFailed', stage: 'config-review' });
1883
2215
  setConfigActionError('设置值未通过验证。请重新检查修改。');
1884
2216
  }
1885
2217
  };
@@ -1890,10 +2222,10 @@ export function ConsoleAppV4(props) {
1890
2222
  const field = currentConfigPage.groups.flatMap((group) => group.fields)
1891
2223
  .find((candidate) => candidate.key === pendingLayerFieldKey);
1892
2224
  setPendingLayerFieldKey(undefined);
1893
- if (!field?.writable)
2225
+ if (!field || (!configFieldCanSet(field) && !configFieldCanUnset(field)))
1894
2226
  return;
1895
2227
  const layer = { scope: route.layer.scope };
1896
- props.controller.push(field.configured ? {
2228
+ props.controller.push(field.configured || !configFieldCanSet(field) ? {
1897
2229
  name: 'configFieldAction',
1898
2230
  pluginId: route.pluginId,
1899
2231
  layer,
@@ -1931,7 +2263,8 @@ export function ConsoleAppV4(props) {
1931
2263
  return;
1932
2264
  setPendingPresetId(undefined);
1933
2265
  }, [pendingPresetId, route.name, route.name === 'configFields' ? route.layer.scope : '']);
1934
- if (snapshot.phase === 'error' && !isCatalogSourcesBootstrapFlow(snapshot)) {
2266
+ if (snapshot.phase === 'error' && !isCatalogSourcesBootstrapFlow(snapshot)
2267
+ && !isOperationHistoryRoute(route)) {
1935
2268
  const sourcesConfigInvalid = snapshot.loadError?.code === 'catalogSourcesConfigInvalid';
1936
2269
  breadcrumb = sourcesConfigInvalid ? 'Catalog 来源恢复' : '启动失败';
1937
2270
  title = sourcesConfigInvalid ? 'Catalog 来源配置损坏' : '无法读取插件状态';
@@ -1943,19 +2276,20 @@ export function ConsoleAppV4(props) {
1943
2276
  : 'd 技术详情 · q 退出 · Ctrl-C 退出';
1944
2277
  actions = sourcesConfigInvalid ? [{
1945
2278
  label: '备份并恢复官方默认来源',
1946
- description: '先进入 Review;不会删除 Catalog state 或 store',
2279
+ description: '先进入确认页;不会删除目录版本状态或本地缓存',
1947
2280
  run: () => beginReview({ kind: 'catalogSourcesRecovery' }),
1948
2281
  }] : [];
1949
- content = _jsxs(Box, { flexDirection: "column", children: [_jsx(StatusCard, { status: "error", title: snapshot.loadError?.code ?? 'statusLoadFailed', description: "\u6CA1\u6709\u6267\u884C\u4EFB\u4F55\u5199\u64CD\u4F5C\u3002" }), actions.length > 0 ? _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' }) : null, showTechnical ? _jsx(TechnicalDetails, { children: _jsx(StaticRow, { title: "\u5B89\u5168\u8BCA\u65AD", meta: snapshot.loadError?.code ?? 'statusLoadFailed', compact: true }) }) : null] });
2282
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(StatusCard, { status: "error", title: "\u63D2\u4EF6\u72B6\u6001\u65E0\u6CD5\u8BFB\u53D6", description: "\u6CA1\u6709\u6267\u884C\u4EFB\u4F55\u5199\u64CD\u4F5C\u3002" }), actions.length > 0 ? _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' }) : null, showTechnical ? _jsx(TechnicalDetails, { children: _jsx(StaticRow, { title: "\u5B89\u5168\u8BCA\u65AD", meta: snapshot.loadError?.code ?? 'statusLoadFailed', compact: true }) }) : null] });
1950
2283
  }
1951
- else if (snapshot.phase !== 'ready' && !isCatalogSourcesBootstrapFlow(snapshot)) {
2284
+ else if (snapshot.phase !== 'ready' && !isCatalogSourcesBootstrapFlow(snapshot)
2285
+ && !isOperationHistoryRoute(route)) {
1952
2286
  breadcrumb = '启动';
1953
2287
  title = '正在检查插件状态';
1954
2288
  description = '单个来源失败不会阻止其他来源继续加载。';
1955
- help = 'Ctrl-C 完成后退出';
2289
+ help = 'Ctrl-C 退出';
1956
2290
  content = _jsxs(StatusCard, { status: "info", title: "\u51C6\u5907\u63A7\u5236\u53F0", children: [_jsx(ProgressRow, { title: "\u8BFB\u53D6\u63D2\u4EF6\u6765\u6E90", state: "active" }), _jsx(ProgressRow, { title: "\u68C0\u67E5\u5F53\u524D\u9879\u76EE", state: "pending" }), _jsx(ProgressRow, { title: "\u6C47\u603B\u53EF\u7528\u64CD\u4F5C", state: "pending" })] });
1957
2291
  }
1958
- else if (route.name === 'home') {
2292
+ else if (rendererFamily === 'home' && route.name === 'home') {
1959
2293
  const home = snapshot.viewModel.home;
1960
2294
  context = '当前项目';
1961
2295
  actions = [
@@ -1972,7 +2306,9 @@ export function ConsoleAppV4(props) {
1972
2306
  run: () => go({ name: 'sourceList' }),
1973
2307
  },
1974
2308
  ];
1975
- content = _jsxs(Box, { flexDirection: "column", children: [_jsxs(StatusCard, { status: home.issueCount > 0 || home.unavailableSourceCount > 0 ? 'warning' : 'success', title: "\u5F53\u524D\u72B6\u6001", children: [_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} 个 Catalog 正在检查`, status: "muted" }) : null, home.catalogUpdateCount > 0 ? _jsx(ResultRow, { title: `${home.catalogUpdateCount} 个 Catalog 有更新`, status: "warning" }) : null, home.catalogRecoveryCount > 0 ? _jsx(ResultRow, { title: `${home.catalogRecoveryCount} 个 Catalog 需要检查或恢复`, status: "warning" }) : null] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "task", compact: policy.mode === 'compact' })] });
2309
+ 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
+ ? _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' })] });
1976
2312
  }
1977
2313
  else if (route.name === 'pluginList') {
1978
2314
  const sourceId = route.sourceId ?? 'all';
@@ -1990,9 +2326,11 @@ export function ConsoleAppV4(props) {
1990
2326
  }).length;
1991
2327
  breadcrumb = route.mode === 'browse' ? '添加插件' : '我的插件';
1992
2328
  title = breadcrumb;
1993
- description = route.mode === 'browse' ? '选择希望 Agent 获得的能力。' : '查看状态,或选择一个插件进行管理。';
2329
+ description = route.mode === 'browse'
2330
+ ? '选择插件;列表末尾可进入批量添加。'
2331
+ : '查看状态;列表末尾可进入批量操作和项目同步。';
1994
2332
  context = `${items.length} 个插件`;
1995
- help = `↑↓ 移动 · Enter 查看 · ←→ 来源 · / 搜索${route.mode === 'browse' ? ' · f 未安装' : ''} · Esc 返回`;
2333
+ help = `↑↓ 移动 · Enter 查看 · m 批量${route.mode === 'installed' ? ' · a 项目全同步' : ''} · ←→ 来源 · / 搜索${route.mode === 'browse' ? ' · f 未安装' : ''} · Esc 返回`;
1996
2334
  const sourceError = sourceId !== 'all'
1997
2335
  && props.controller.getSourceOverview(sourceId).health?.state === 'unavailable';
1998
2336
  actions = sourceError
@@ -2002,13 +2340,50 @@ export function ConsoleAppV4(props) {
2002
2340
  pluginId: item.pluginId,
2003
2341
  name: item.displayName,
2004
2342
  sourceLabel: item.sourceLabel,
2005
- description: item.primaryAttention
2006
- ? `${item.primaryAttention.code} · ${item.primaryAttention.message}`
2343
+ description: item.configOnly
2344
+ ? '来源当前没有这个插件;可以查看已保存设置或定点重置设置层。'
2007
2345
  : item.description,
2346
+ ...(item.primaryAttention ? { attention: item.primaryAttention.message } : {}),
2008
2347
  placementSummary: pluginPlacementSummary(item),
2009
2348
  statusLabel: statusText(item),
2010
2349
  status: pluginRowStatus(item),
2011
2350
  }));
2351
+ if (!sourceError && items.length > 0) {
2352
+ pluginRows.push({
2353
+ pluginId: '@menu:batch',
2354
+ name: route.mode === 'browse' ? '选择多个插件添加…' : '批量操作…',
2355
+ sourceLabel: '',
2356
+ description: route.mode === 'browse'
2357
+ ? '选择多个插件,然后统一选择平台和位置'
2358
+ : '选择多个插件进行同步、重新安装、卸载或清理',
2359
+ placementSummary: '',
2360
+ statusLabel: '菜单',
2361
+ status: 'info',
2362
+ 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' });
2383
+ },
2384
+ });
2385
+ }
2386
+ }
2012
2387
  if (route.mode === 'installed' && items.length === 0 && !sourceError) {
2013
2388
  title = '还没有添加插件';
2014
2389
  description = `从已配置的 ${props.controller.listSources().length} 个来源中浏览可用能力。`;
@@ -2027,7 +2402,9 @@ export function ConsoleAppV4(props) {
2027
2402
  const viewport = sourceError || pluginRows.length === 0
2028
2403
  ? actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(pluginListStaticRows))
2029
2404
  : rowViewport({
2030
- rowHeights: pluginRows.map(() => 3),
2405
+ rowHeights: pluginRows.map((row) => row.menuAction
2406
+ ? 1
2407
+ : row.attention ? 4 : 3),
2031
2408
  focus,
2032
2409
  requestedOffset: scrollOffset,
2033
2410
  rowBudget: pageActionBudget(pluginListStaticRows),
@@ -2047,6 +2424,126 @@ export function ConsoleAppV4(props) {
2047
2424
  ? _jsx(PluginRows, { rows: pluginRows, focus: focus, viewport: viewport })
2048
2425
  : _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: viewport.offset, limit: viewport.limit })] }));
2049
2426
  }
2427
+ 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 = [
2435
+ {
2436
+ label: '同步当前项目全部插件',
2437
+ description: '从当前项目已保存的插件设置建立方案',
2438
+ 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);
2454
+ setFocus(0);
2455
+ setScrollOffset(0);
2456
+ },
2457
+ })),
2458
+ ];
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 });
2463
+ }
2464
+ 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 放弃';
2482
+ 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" });
2486
+ }
2487
+ }
2488
+ else if (route.name === 'batchPlacement') {
2489
+ breadcrumb = '我的插件 / 批量操作 / 位置';
2490
+ title = batchDraft?.selection === 'currentProjectIntentRoots'
2491
+ ? '同步当前项目全部保存目标'
2492
+ : `设置批量${batchDraft?.action ? batchActionLabel(batchDraft.action) : ''}位置`;
2493
+ description = '所有选择使用同一位置和平台。确认页会重新读取状态并冻结实际计划。';
2494
+ if (!batchDraft?.action) {
2495
+ actions = [{ label: '返回批量选择', run: back }];
2496
+ }
2497
+ else {
2498
+ const projectIntent = batchDraft.selection === 'currentProjectIntentRoots';
2499
+ actions = [
2500
+ {
2501
+ label: '当前项目', selected: batchDraft.scope === 'project',
2502
+ run: () => setBatchDraft((current) => current ? { ...current, scope: 'project' } : current),
2503
+ },
2504
+ {
2505
+ label: '所有项目', selected: batchDraft.scope === 'user',
2506
+ disabled: projectIntent ? '项目全部同步固定使用当前项目' : undefined,
2507
+ run: () => setBatchDraft((current) => current ? { ...current, scope: 'user' } : current),
2508
+ },
2509
+ ...['codex', 'claude', 'both'].map((platform) => ({
2510
+ label: platform === 'both' ? 'Codex 和 Claude Code' : platformLabel(platform),
2511
+ selected: batchDraft.platform === platform,
2512
+ run: () => setBatchDraft((current) => current ? { ...current, platform } : current),
2513
+ })),
2514
+ {
2515
+ label: `查看批量${batchActionLabel(batchDraft.action)}确认`,
2516
+ description: `${batchDraft.selection === 'selected' ? `${batchDraft.selectedPluginIds.length} 个所选插件` : '当前项目全部保存目标'} · ${scopeLabel(batchDraft.scope)}`,
2517
+ disabled: batchDraft.selection === 'selected' && batchDraft.selectedPluginIds.length === 0
2518
+ ? '请至少选择一个插件' : undefined,
2519
+ run: () => {
2520
+ const intent = {
2521
+ kind: 'pluginWriteBatch',
2522
+ action: batchDraft.action,
2523
+ selection: batchDraft.selection === 'selected'
2524
+ ? {
2525
+ kind: 'selected',
2526
+ pluginIds: [...new Set(batchDraft.selectedPluginIds)]
2527
+ .sort((left, right) => left.localeCompare(right, 'en')),
2528
+ }
2529
+ : { kind: 'currentProjectIntentRoots' },
2530
+ platform: batchDraft.platform,
2531
+ scope: batchDraft.scope,
2532
+ };
2533
+ beginReview(intent);
2534
+ setDirty(false);
2535
+ setFocus(0);
2536
+ setScrollOffset(0);
2537
+ },
2538
+ },
2539
+ ];
2540
+ }
2541
+ context = batchDraft?.selection === 'selected'
2542
+ ? `${batchDraft.selectedPluginIds.length} 个插件`
2543
+ : '保存的项目目标';
2544
+ help = '↑↓ 移动 · Enter 选择 · Esc 返回修改';
2545
+ content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "choice", compact: policy.mode === 'compact' });
2546
+ }
2050
2547
  else if (route.name === 'pluginPreview' && plugin) {
2051
2548
  breadcrumb = `添加插件 / ${plugin.displayName}`;
2052
2549
  context = plugin.sourceLabel;
@@ -2090,14 +2587,14 @@ export function ConsoleAppV4(props) {
2090
2587
  if (action.id === 'upgradeAwCli')
2091
2588
  return {
2092
2589
  label: '升级 AW CLI',
2093
- description: plugin.capabilityRecovery ?? '升级到支持所需 capability 的 AW CLI 版本后重试',
2590
+ description: '升级到支持此插件的 AW CLI 版本后重试',
2094
2591
  status: 'warning',
2095
2592
  run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
2096
2593
  };
2097
2594
  if (action.id === 'capabilityTechnical')
2098
2595
  return {
2099
- label: '查看 capability 技术详情',
2100
- description: '查看缺少的 capability 和安全恢复方向',
2596
+ label: '查看功能支持详情',
2597
+ description: '查看缺少的功能和安全恢复方向',
2101
2598
  run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
2102
2599
  };
2103
2600
  if (action.id === 'sourceProblem')
@@ -2237,13 +2734,13 @@ export function ConsoleAppV4(props) {
2237
2734
  description = '插件依赖用于生成,不能在这里取消。';
2238
2735
  actions = [
2239
2736
  { label: '返回 Skill 选择', run: back },
2240
- { label: showTechnical ? '隐藏技术详情' : '技术详情', run: () => setShowTechnical((value) => !value) },
2737
+ { label: showTechnical ? '隐藏技术详情' : '技术详情', run: () => toggleTechnical() },
2241
2738
  ];
2242
2739
  const dependencies = overview?.dependencies ?? [];
2243
2740
  const visibleDependencies = dependencies.slice(0, showTechnical ? 1 : responsiveBudget(2, 3));
2244
2741
  const technicalDependencies = dependencies.slice(0, 1);
2245
2742
  const compactTechnicalSummary = policy.mode === 'compact' && showTechnical;
2246
- content = _jsxs(Box, { flexDirection: "column", children: [compactTechnicalSummary ? _jsxs(SectionCard, { title: "provider \u6458\u8981", meta: `${dependencies.length + 1} 个`, compact: true, children: [_jsx(StaticRow, { title: plugin.displayName, meta: "\u6839 provider", compact: true }), _jsx(StaticRow, { title: `${dependencies.length} 个依赖 providers`, meta: "\u7528\u4E8E\u751F\u6210", compact: true })] }) : _jsx(SectionCard, { title: "\u5F53\u524D\u63D2\u4EF6", meta: "\u6839 provider", children: _jsx(StaticRow, { title: plugin.displayName, description: "\u63D0\u4F9B\u672C\u6B21\u5B89\u88C5\u5165\u53E3", meta: `贡献 ${snapshot.portableDraft.candidates.filter((candidate) => candidate.relation === 'requested').length} 个 Skills`, compact: policy.mode === 'compact' }) }), compactTechnicalSummary ? null : _jsxs(SectionCard, { title: "\u4F9D\u8D56 providers", meta: `${dependencies.length} 个`, children: [visibleDependencies.map((dependency) => _jsx(StaticRow, { title: dependency.displayName, description: `${dependency.relation === 'direct' ? '根 provider 的直接依赖' : '依赖链中的间接 provider'} · ${dependency.requiredBy.map((entry) => entry.displayName).join('、')} 需要它`, meta: `贡献 ${snapshot.portableDraft.candidates.filter((candidate) => candidate.providerPluginId === dependency.pluginId).length} 个 Skills`, compact: policy.mode === 'compact' }, dependency.pluginId)), dependencies.length > visibleDependencies.length ? _jsx(StaticRow, { title: `另有 ${dependencies.length - visibleDependencies.length} 个依赖 provider`, status: "muted", compact: true }) : null] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true }), showTechnical ? _jsx(TechnicalDetails, { children: technicalDependencies.map((entry) => _jsx(StaticRow, { title: entry.displayName, meta: `${entry.pluginId}${compactTechnicalSummary && dependencies.length > technicalDependencies.length ? ` · 另有 ${dependencies.length - technicalDependencies.length} 个依赖` : ''}`, compact: true }, entry.pluginId)) }) : null] });
2743
+ content = _jsxs(Box, { flexDirection: "column", children: [compactTechnicalSummary ? _jsxs(SectionCard, { title: "provider \u6458\u8981", meta: `${dependencies.length + 1} 个`, compact: true, children: [_jsx(StaticRow, { title: plugin.displayName, meta: "\u6839 provider", compact: true }), _jsx(StaticRow, { title: `${dependencies.length} 个依赖 providers`, meta: "\u7528\u4E8E\u751F\u6210", compact: true })] }) : _jsx(SectionCard, { title: "\u5F53\u524D\u63D2\u4EF6", children: _jsx(StaticRow, { title: plugin.displayName, description: "\u63D0\u4F9B\u672C\u6B21\u5B89\u88C5\u5165\u53E3", meta: `提供 ${snapshot.portableDraft.candidates.filter((candidate) => candidate.relation === 'requested').length} 个 Skills`, compact: policy.mode === 'compact' }) }), compactTechnicalSummary ? null : _jsxs(SectionCard, { title: "\u4F9D\u8D56\u63D2\u4EF6", meta: `${dependencies.length} 个`, children: [visibleDependencies.map((dependency) => _jsx(StaticRow, { title: dependency.displayName, description: `${dependency.relation === 'direct' ? '当前插件直接需要' : '由依赖插件继续需要'} · ${dependency.requiredBy.map((entry) => entry.displayName).join('、')} 需要它`, meta: `提供 ${snapshot.portableDraft.candidates.filter((candidate) => candidate.providerPluginId === dependency.pluginId).length} 个 Skills`, compact: policy.mode === 'compact' }, dependency.pluginId)), dependencies.length > visibleDependencies.length ? _jsx(StaticRow, { title: `另有 ${dependencies.length - visibleDependencies.length} 个依赖插件`, status: "muted", compact: true }) : null] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true }), showTechnical ? _jsx(TechnicalDetails, { children: technicalDependencies.map((entry) => _jsx(StaticRow, { title: entry.displayName, meta: `${entry.pluginId}${compactTechnicalSummary && dependencies.length > technicalDependencies.length ? ` · 另有 ${dependencies.length - technicalDependencies.length} 个依赖` : ''}`, compact: true }, entry.pluginId)) }) : null] });
2247
2744
  }
2248
2745
  else if (route.name === 'portableAgentMode' && plugin && snapshot.portableDraft) {
2249
2746
  breadcrumb = `${plugin.displayName} / 通用 Skills / Agent`;
@@ -2260,7 +2757,7 @@ export function ConsoleAppV4(props) {
2260
2757
  context = '指定';
2261
2758
  title = '输入 Agent ID';
2262
2759
  description = '多个 Agent 可使用逗号或空格分隔。';
2263
- help = '输入 Agent ID · Enter 保存 · Esc 返回';
2760
+ help = '输入 Agent ID · Enter 保存 · Ctrl-U 清空输入 · Esc 返回';
2264
2761
  content = _jsxs(Box, { flexDirection: "column", children: [_jsx(InputCard, { focused: true, title: _jsx(InputDisplay, { value: portableAgentInput, columns: Math.max(8, size.columns - 8) }), description: inputError ?? 'Enter 保存并继续 · Esc 返回;q 和 b 会作为输入。', status: inputError ? 'error' : undefined }), _jsx(StaticRow, { title: "\u793A\u4F8B", meta: "cursor, claude-code, codex", status: "muted", compact: true })] });
2265
2762
  }
2266
2763
  else if (route.name === 'portableScope' && plugin && snapshot.portableDraft) {
@@ -2286,7 +2783,7 @@ export function ConsoleAppV4(props) {
2286
2783
  setScrollOffset(0);
2287
2784
  } }] : []),
2288
2785
  ...(route.code === 'portableSkillsCliMissing' || route.code === 'portableSkillsCliIncompatible'
2289
- ? [{ label: showTechnical ? '返回问题摘要' : '查看安装说明', run: () => setShowTechnical((value) => !value) }]
2786
+ ? [{ label: showTechnical ? '返回问题摘要' : '查看安装说明', run: () => toggleTechnical() }]
2290
2787
  : []),
2291
2788
  ...(route.code === 'portableScopeUnavailable' ? [{ label: '返回 Skill 选择', run: () => {
2292
2789
  props.controller.resetPortableFlowToPreview(plugin.pluginId, { name: 'portableSkillSelect', pluginId: plugin.pluginId }, true);
@@ -2297,7 +2794,7 @@ export function ConsoleAppV4(props) {
2297
2794
  { label: '返回插件预览', run: () => props.controller.resetPortableFlowToPreview(plugin.pluginId) },
2298
2795
  ...(route.code === 'portableSkillsCliMissing' || route.code === 'portableSkillsCliIncompatible'
2299
2796
  ? []
2300
- : [{ label: showTechnical ? '返回问题摘要' : '技术详情', run: () => setShowTechnical((value) => !value) }]),
2797
+ : [{ label: showTechnical ? '返回问题摘要' : '技术详情', run: () => toggleTechnical() }]),
2301
2798
  ];
2302
2799
  const blockerActionViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, responsiveBudget(2, 4));
2303
2800
  content = _jsxs(Box, { flexDirection: "column", children: [_jsx(DiagnosticSummary, { diagnostic: diagnostic, technical: showTechnical, compact: policy.mode === 'compact' }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: blockerActionViewport.offset, limit: blockerActionViewport.limit })] });
@@ -2313,12 +2810,13 @@ export function ConsoleAppV4(props) {
2313
2810
  ? dependentCount > 0 ? 1 : 2
2314
2811
  : dependentCount > 0 ? 3 : 5;
2315
2812
  const dependentLimit = responsiveBudget(2, 4);
2316
- const dependencyRows = (entries, limit, dependent = false) => {
2317
- const visible = entries.slice(0, limit);
2318
- return _jsxs(_Fragment, { children: [visible.map((entry) => {
2813
+ const dependencyRows = (entries, limit, dependent = false, offset = 0) => {
2814
+ const safeOffset = Math.min(offset, Math.max(0, entries.length - 1));
2815
+ const visible = entries.slice(safeOffset, safeOffset + limit);
2816
+ return _jsxs(_Fragment, { children: [safeOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${safeOffset} 项`, status: "muted", compact: true }) : null, visible.map((entry) => {
2319
2817
  const state = dependencyStatus(entry);
2320
2818
  return _jsx(StaticRow, { title: entry.displayName, description: dependencyRelationDescription(entry), meta: dependent && route.view === 'removeBlocked' ? dependentTargetLabel(entry) : dependencyPlacementLabel(entry), status: state.status, compact: policy.mode === 'compact' }, entry.pluginId);
2321
- }), entries.length > visible.length ? _jsx(StaticRow, { title: `另有 ${entries.length - visible.length} 项`, status: "muted", compact: true }) : null] });
2819
+ }), entries.length > safeOffset + visible.length ? _jsx(StaticRow, { title: `↓ 后面还有 ${entries.length - safeOffset - visible.length} 项`, status: "muted", compact: true }) : null] });
2322
2820
  };
2323
2821
  if (route.view === 'removeBlocked') {
2324
2822
  breadcrumb = `${plugin.displayName} / 移除`;
@@ -2326,60 +2824,30 @@ export function ConsoleAppV4(props) {
2326
2824
  title = `无法移除 ${plugin.displayName}`;
2327
2825
  description = '仍有插件依赖它。请先处理依赖者,再重新尝试。';
2328
2826
  actions = [
2329
- ...(route.platform && route.scope ? [{
2330
- label: '强制清理…',
2331
- description: '依赖者会变为不完整;下一页仍需明确确认',
2332
- status: 'error',
2333
- run: () => beginReview({
2334
- kind: 'pluginWrite',
2335
- action: 'cleanup',
2336
- pluginId: plugin.pluginId,
2337
- platform: route.platform,
2338
- scope: route.scope,
2339
- force: true,
2340
- }),
2341
- }] : []),
2342
2827
  { label: '查看完整依赖关系', run: () => {
2343
2828
  props.controller.replace({ name: 'pluginDependencies', pluginId: plugin.pluginId });
2344
2829
  setFocus(0);
2345
2830
  setScrollOffset(0);
2346
2831
  } },
2347
2832
  { label: '返回插件详情', run: back },
2348
- { label: showTechnical ? '隐藏技术详情' : '技术详情', run: () => setShowTechnical((value) => !value) },
2833
+ { label: showTechnical ? '隐藏技术详情' : '技术详情', run: () => toggleTechnical() },
2349
2834
  ];
2350
2835
  const blockedRemovalViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(5));
2351
- content = _jsxs(Box, { flexDirection: "column", children: [_jsx(Callout, { status: "error", title: `${dependentCount} 个已安装插件仍在使用`, description: "\u6CA1\u6709\u4FEE\u6539\u4EFB\u4F55\u5B89\u88C5\u3002" }), showTechnical ? null : _jsx(SectionCard, { title: "\u4F7F\u7528\u8FD9\u4E2A\u4F9D\u8D56\u7684\u63D2\u4EF6", meta: "\u53CD\u5411\u5F15\u7528", children: dependencyRows(visibleDependents, dependentLimit, true) }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: blockedRemovalViewport.offset, limit: blockedRemovalViewport.limit }), showTechnical ? _jsx(IssueTechnicalDetails, { details: [{
2836
+ if (!showTechnical && visibleDependents.length > dependentLimit) {
2837
+ help = 'PgUp/PgDn 查看全部被依赖关系 · ↑↓ 移动 · Enter 选择 · Esc 返回';
2838
+ }
2839
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(Callout, { status: "error", title: `${dependentCount} 个已安装插件仍在使用`, description: "\u6CA1\u6709\u4FEE\u6539\u4EFB\u4F55\u5B89\u88C5\u3002" }), showTechnical ? null : _jsx(SectionCard, { title: "\u4F7F\u7528\u8FD9\u4E2A\u4F9D\u8D56\u7684\u63D2\u4EF6", meta: "\u53CD\u5411\u5F15\u7528", children: dependencyRows(visibleDependents, dependentLimit, true, batchTargetOffset) }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: blockedRemovalViewport.offset, limit: blockedRemovalViewport.limit }), showTechnical ? _jsx(IssueTechnicalDetails, { details: [{
2352
2840
  code: 'dependencyStillRequired',
2353
- message: '已安装插件仍在使用这个依赖;强制清理会使依赖者变为不完整。',
2841
+ message: '已安装插件仍在使用这个依赖;请先处理依赖者。',
2354
2842
  }], compact: policy.mode === 'compact' }) : null] });
2355
2843
  }
2356
2844
  else {
2357
2845
  breadcrumb = `${plugin.displayName} / 依赖关系`;
2358
2846
  context = overview ? `${overview.summary.totalCount + 1} 个插件` : '只读';
2359
2847
  title = hasInstalledPlacement(plugin) ? '依赖与被使用情况' : '安装所需插件';
2360
- description = '依赖由同一来源快照解析;调整前会进入 Review。';
2361
- const installedTargets = plugin.installedPlacements.filter((placement) => placement.state === 'installed');
2848
+ description = '依赖由同一来源快照解析;安装时的依赖位置选择会进入 Review。';
2362
2849
  actions = [
2363
- ...(hasInstalledPlacement(plugin) && dependencyCount > 0 && installedTargets.length > 0 ? [{
2364
- label: '调整依赖位置…',
2365
- description: '按平台设置 project/user;不会自动清理所有项目内容',
2366
- run: async () => {
2367
- const rootScope = installedTargets.some((placement) => placement.desiredScopes?.includes('project') || placement.scope === 'project')
2368
- ? 'project' : 'user';
2369
- const platforms = [...new Set(installedTargets.map((placement) => placement.platform))];
2370
- setScope(rootScope);
2371
- setSelectedTargets(platforms);
2372
- props.controller.beginInstallFlow(plugin.pluginId, rootScope, { adjustingExisting: true });
2373
- props.controller.setInstallPlatforms(platforms);
2374
- const configurable = await props.controller.prepareInstallDependencyPlacement();
2375
- if (!configurable) {
2376
- setActionError('当前依赖位置都由约束固定,没有可调整项。');
2377
- return;
2378
- }
2379
- go({ name: 'dependencyPlacement', pluginId: plugin.pluginId });
2380
- },
2381
- }] : []),
2382
- { label: '技术详情', description: '显示 canonical pluginId 和安全错误代码', run: () => setShowTechnical((value) => !value) },
2850
+ { label: '技术详情', description: '显示 canonical pluginId 和安全错误代码', run: () => toggleTechnical() },
2383
2851
  ];
2384
2852
  const rootStatus = plugin.aggregateStatus === 'installed' ? 'success'
2385
2853
  : plugin.aggregateStatus === 'needsAttention' ? 'warning'
@@ -2396,16 +2864,6 @@ export function ConsoleAppV4(props) {
2396
2864
  ];
2397
2865
  const technicalLineBudget = showTechnical ? 4 : 0;
2398
2866
  const relationItemBudget = Math.max(relationCategories.length, size.rows - 6 - 1 - technicalLineBudget - 4);
2399
- const relationBudgets = new Map(relationCategories.map((category) => [category.key, 1]));
2400
- let remainingRelationLines = Math.max(0, relationItemBudget - relationCategories.length);
2401
- for (const category of relationCategories) {
2402
- if (category.key === 'blocker')
2403
- continue;
2404
- while (remainingRelationLines > 0 && (relationBudgets.get(category.key) ?? 0) < category.count) {
2405
- relationBudgets.set(category.key, (relationBudgets.get(category.key) ?? 0) + 1);
2406
- remainingRelationLines -= 1;
2407
- }
2408
- }
2409
2867
  const prioritizedRelations = (entries) => [...entries].sort((left, right) => {
2410
2868
  const priority = (entry) => {
2411
2869
  const status = dependencyStatus(entry).status;
@@ -2413,17 +2871,25 @@ export function ConsoleAppV4(props) {
2413
2871
  };
2414
2872
  return priority(left) - priority(right);
2415
2873
  });
2416
- const budgetedRelationRows = (entries, prefix, budget, dependent = false) => {
2417
- const prioritized = prioritizedRelations(entries);
2418
- const visible = prioritized.slice(0, budget);
2419
- return visible.map((entry, index) => {
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) => {
2420
2877
  const state = dependencyStatus(entry);
2421
- const hidden = index === visible.length - 1 ? entries.length - visible.length : 0;
2422
- const relationMeta = dependent ? dependentTargetLabel(entry) : dependencyPlacementLabel(entry);
2423
- return _jsx(StaticRow, { title: `${prefix} · ${entry.displayName}`, description: dependencyRelationDescription(entry), meta: `${relationMeta}${hidden > 0 ? ` · 另有 ${hidden} 项` : ''}`, status: state.status, compact: true }, entry.pluginId);
2424
- });
2425
- };
2426
- 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 }), overview?.blockers.slice(0, relationBudgets.get('blocker') ?? 0).map((blocker, index, blockers) => _jsx(StaticRow, { title: `阻塞 · ${blocker.title}`, meta: index === blockers.length - 1 && blockerCount > blockers.length ? `另有 ${blockerCount - blockers.length} 项` : undefined, description: blocker.explanation, status: "error", compact: true }, blocker.code)), budgetedRelationRows(overview?.dependencies ?? [], '必需依赖', relationBudgets.get('dependency') ?? 0), budgetedRelationRows(visibleDependents, '被依赖', relationBudgets.get('dependent') ?? 0, true), 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] });
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
+ }),
2884
+ ];
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] });
2427
2893
  }
2428
2894
  }
2429
2895
  else if (route.name === 'installPlacement' && plugin) {
@@ -2474,7 +2940,7 @@ export function ConsoleAppV4(props) {
2474
2940
  const hasDependencies = (plugin.dependencySummary?.totalCount ?? 0) > 0;
2475
2941
  description = hasDependencies
2476
2942
  ? '根插件必须支持所选位置;每个依赖必须至少有一个合法位置。'
2477
- : '目标平台按所选位置验证;仅含 Codex native 的插件可同时启用两个位置。';
2943
+ : '目标平台按所选位置验证;仅含平台共享内容的插件可同时启用两个位置。';
2478
2944
  const choices = props.controller.listInstallTargets(plugin.pluginId, scope);
2479
2945
  const validSelectedTargets = selectedTargets.filter((platform) => choices.some((choice) => choice.platform === platform && choice.compatible));
2480
2946
  const allUnavailable = choices.length > 0 && choices.every((choice) => !choice.compatible);
@@ -2543,7 +3009,7 @@ export function ConsoleAppV4(props) {
2543
3009
  const dependencyActions = (draft?.targets ?? []).map((target) => ({
2544
3010
  label: `${target.displayName} · ${platformLabel(target.platform)}`,
2545
3011
  description: `${target.relation === 'direct' ? '直接依赖' : '间接依赖'} · ${target.explanation}`,
2546
- meta: `${scopeLabel(target.scope)}${target.assignmentSource === 'consumerPromotion' ? ' · fixed-point 提升' : ''}`,
3012
+ meta: `${scopeLabel(target.scope)}${target.assignmentSource === 'consumerPromotion' ? ' · 因依赖自动调整' : ''}`,
2547
3013
  selected: true,
2548
3014
  disabled: target.locked ? '位置由平台或上游约束固定' : undefined,
2549
3015
  run: () => go({
@@ -2563,7 +3029,7 @@ export function ConsoleAppV4(props) {
2563
3029
  },
2564
3030
  {
2565
3031
  label: '恢复推荐位置',
2566
- description: '恢复默认、健康 user 复用和 fixed-point 计算结果',
3032
+ description: '恢复依赖自动安排的建议位置',
2567
3033
  run: () => props.controller.resetDependencyScopes(),
2568
3034
  },
2569
3035
  { label: '返回目标平台', run: back },
@@ -2576,7 +3042,7 @@ export function ConsoleAppV4(props) {
2576
3042
  const reasonSummary = [
2577
3043
  ...(defaultCount > 0 ? [`默认 project ${defaultCount} 项`] : []),
2578
3044
  ...(reusedCount > 0 ? [`复用健康的所有项目安装 ${reusedCount} 项`] : []),
2579
- ...(promotedCount > 0 ? [`fixed-point 提升 ${promotedCount} 项`] : []),
3045
+ ...(promotedCount > 0 ? [`因依赖自动调整 ${promotedCount} 项`] : []),
2580
3046
  ].join(';');
2581
3047
  content = _jsxs(Box, { flexDirection: "column", children: [policy.mode === 'compact'
2582
3048
  ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: `根插件 · ${plugin.displayName}`, meta: draft ? scopeLabel(draft.rootScope) : '未就绪', compact: true }), _jsx(StaticRow, { title: `位置原因 · ${reasonSummary || '由平台或上游约束固定'}`, status: userCount > 0 ? 'warning' : 'info', compact: true }), _jsx(StaticRow, { title: "\u8303\u56F4\u63D0\u793A \u00B7 \u6240\u6709\u9879\u76EE\u4F4D\u7F6E\u4F1A\u5F71\u54CD\u5176\u4ED6\u9879\u76EE\u548C\u7528\u6237\u73AF\u5883", status: userCount > 0 ? 'warning' : 'muted', compact: true })] })
@@ -2596,7 +3062,7 @@ export function ConsoleAppV4(props) {
2596
3062
  selected: target?.scope === candidateScope,
2597
3063
  description: candidateScope === 'user'
2598
3064
  ? '更宽:对其他项目可见,并会改变你的用户环境'
2599
- : '仅供当前项目使用;适合项目共享 intent',
3065
+ : '仅供当前项目使用;适合随项目共享',
2600
3066
  disabled: unavailable ? '这个依赖或上游约束不允许此位置' : undefined,
2601
3067
  run: async () => {
2602
3068
  await props.controller.setDependencyScope(route.dependencyPluginId, route.platform, candidateScope);
@@ -2605,7 +3071,7 @@ export function ConsoleAppV4(props) {
2605
3071
  };
2606
3072
  });
2607
3073
  content = _jsxs(Box, { flexDirection: "column", children: [target?.assignmentSource === 'consumerPromotion'
2608
- ? _jsx(Callout, { status: "warning", title: "fixed-point \u4F4D\u7F6E\u63D0\u5347", description: `上游依赖需要至少使用${scopeLabel(target.minimumScope)}。` })
3074
+ ? _jsx(Callout, { status: "warning", title: "\u4F9D\u8D56\u9700\u8981\u66F4\u5927\u4F7F\u7528\u8303\u56F4", description: `上游依赖需要至少使用${scopeLabel(target.minimumScope)}。` })
2609
3075
  : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "choice", compact: policy.mode === 'compact' })] });
2610
3076
  help = '↑↓ 移动 · Enter 选择 · Esc 返回';
2611
3077
  }
@@ -2616,7 +3082,30 @@ export function ConsoleAppV4(props) {
2616
3082
  description = plugin.description;
2617
3083
  const dependencySummary = plugin.dependencySummary;
2618
3084
  const detailMenu = dynamicMenu;
2619
- actions = detailMenu.map((action) => {
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: '恢复来源后可重新读取字段定义;不会把此记录当作可安装插件。',
3100
+ 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) => {
2620
3109
  if (action.id === 'resolveScopeConflict')
2621
3110
  return {
2622
3111
  label: `保留${scopeLabel(action.keepScope)},移除${scopeLabel(action.scope)} root…`,
@@ -2657,14 +3146,14 @@ export function ConsoleAppV4(props) {
2657
3146
  if (action.id === 'upgradeAwCli')
2658
3147
  return {
2659
3148
  label: '升级 AW CLI',
2660
- description: plugin.capabilityRecovery ?? '升级到支持所需 capability 的 AW CLI 版本后重试',
3149
+ description: '升级到支持此插件的 AW CLI 版本后重试',
2661
3150
  status: 'warning',
2662
3151
  run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
2663
3152
  };
2664
3153
  if (action.id === 'capabilityTechnical')
2665
3154
  return {
2666
- label: '查看 capability 技术详情',
2667
- description: '查看缺少的 capability 和安全恢复方向',
3155
+ label: '查看功能支持详情',
3156
+ description: '查看缺少的功能和安全恢复方向',
2668
3157
  run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
2669
3158
  };
2670
3159
  if (action.id === 'sourceProblem')
@@ -2696,14 +3185,14 @@ export function ConsoleAppV4(props) {
2696
3185
  if (action.id === 'upgradeAwCli')
2697
3186
  return {
2698
3187
  label: '升级 AW CLI',
2699
- description: plugin.capabilityRecovery ?? '升级到支持所需 capability 的 AW CLI 版本后重试',
3188
+ description: '升级到支持此插件的 AW CLI 版本后重试',
2700
3189
  status: 'warning',
2701
3190
  run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
2702
3191
  };
2703
3192
  if (action.id === 'capabilityTechnical')
2704
3193
  return {
2705
- label: '查看 capability 技术详情',
2706
- description: '查看缺少的 capability 和安全恢复方向',
3194
+ label: '查看功能支持详情',
3195
+ description: '查看缺少的功能和安全恢复方向',
2707
3196
  run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
2708
3197
  };
2709
3198
  if (action.id === 'sourceProblem')
@@ -2725,11 +3214,11 @@ export function ConsoleAppV4(props) {
2725
3214
  if (action.id === 'addScope')
2726
3215
  return {
2727
3216
  label: `在${scopeLabel(action.scope)}中启用…`,
2728
- description: '只增加该 scope 的 activation;shared native 保持复用',
3217
+ description: '增加这个使用位置;现有共享内容保持不变',
2729
3218
  run: () => beginSecondScope(plugin, action.platform, action.scope),
2730
3219
  };
2731
3220
  if (action.id === 'about')
2732
- return { label: '查看支持信息', description: 'manifest 与平台能力', run: () => go({ name: 'pluginAbout', pluginId: plugin.pluginId }) };
3221
+ return { label: '查看支持信息', description: '插件定义与平台支持', run: () => go({ name: 'pluginAbout', pluginId: plugin.pluginId }) };
2733
3222
  if (action.id === 'source')
2734
3223
  return { label: '查看来源', run: () => go({ name: 'sourceDetail', sourceId: plugin.sourceId }) };
2735
3224
  if (action.id === 'outputCollision')
@@ -2751,10 +3240,10 @@ export function ConsoleAppV4(props) {
2751
3240
  run: () => beginScopeConflictRecovery(action),
2752
3241
  };
2753
3242
  if (action.id === 'refreshAndSync')
2754
- return { label: `刷新并同步到 ${action.availableVersion}…`, description: '先刷新相关来源,再对新 Active 内容进行 Review', run: () => beginReview({ kind: 'pluginWrite', action: 'refreshAndSync', pluginId: plugin.pluginId, platform: action.platform, scope: action.scope }) };
3243
+ return { label: `刷新并同步到 ${action.availableVersion}…`, description: '先刷新相关来源,再确认新的目录内容', run: () => beginReview({ kind: 'pluginWrite', action: 'refreshAndSync', pluginId: plugin.pluginId, platform: action.platform, scope: action.scope }) };
2755
3244
  if (action.id === 'sync')
2756
3245
  return {
2757
- label: '同步 desired intent…',
3246
+ label: '同步已保存的使用位置…',
2758
3247
  description: placement?.state === 'installed'
2759
3248
  ? placement.recovery?.find((entry) => entry.command === 'sync')?.description
2760
3249
  ?? '进入现有 Review、进度和结果链修复 needsSync 或 drift'
@@ -2768,15 +3257,55 @@ export function ConsoleAppV4(props) {
2768
3257
  if (action.id === 'remove')
2769
3258
  return { label: `从${scopeLabel(action.scope)}移除…`, description: '保留插件设置', status: 'error', run: () => beginPluginRemoval(plugin, action.platform, action.scope) };
2770
3259
  if (action.id === 'cleanup')
2771
- return { label: `清理${scopeLabel(action.scope)}中的遗留内容…`, description: '只清理这个插件目标的 AW-owned 内容', status: 'error', run: () => beginReview({ kind: 'pluginWrite', action: 'cleanup', pluginId: plugin.pluginId, platform: action.platform, scope: action.scope }) };
3260
+ return { label: `清理${scopeLabel(action.scope)}中的遗留内容…`, description: '只清理这个插件目标的 AW 管理内容', status: 'error', run: () => beginReview({ kind: 'pluginWrite', action: 'cleanup', pluginId: plugin.pluginId, platform: action.platform, scope: action.scope }) };
2772
3261
  if (action.id === 'technical')
2773
3262
  return typedIssue
2774
3263
  ? { label: '查看对应问题', description: typedIssue.explanation, run: () => go({ name: 'issueDetail', issueId: typedIssue.issueId }) }
2775
3264
  : { label: placement?.state === 'installed' && placement.health === 'error' ? '查看安全技术详情' : placement?.state === 'installed' && placement.health === 'incomplete' ? '查看影响' : '查看安装详情', description: '组件与诊断信息', run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }) };
2776
3265
  return { label: '返回插件', run: back };
2777
3266
  };
3267
+ const offlineCleanupAction = typedIssue?.actions.find((action) => action.id === 'cleanup');
3268
+ const offlineCleanup = typedIssue !== undefined
3269
+ && placement?.state === 'installed'
3270
+ && placement.blockingReason === undefined
3271
+ && (placement.actualState === 'orphaned' || placement.actualState === 'blocked')
3272
+ && offlineCleanupAction !== undefined
3273
+ && !platformMenu.some((action) => action.id === 'cleanup')
3274
+ && !platformMenu.some((action) => action.id === 'resolveScopeConflict')
3275
+ ? {
3276
+ label: `清理${scopeLabel(placement.scope)}中的遗留内容…`,
3277
+ description: '离线时也只清理这个插件目标中可证明属于 AW 的内容;完成后如需恢复,请单独确认重新安装。',
3278
+ status: 'error',
3279
+ run: () => {
3280
+ const intent = issueWriteIntent(typedIssue, offlineCleanupAction);
3281
+ if (intent)
3282
+ beginReview(intent);
3283
+ },
3284
+ }
3285
+ : undefined;
3286
+ const restrictedForceCleanup = placement?.state === 'installed'
3287
+ && placement.blockingReason === undefined
3288
+ && (placement.actualState === 'orphaned' || placement.actualState === 'blocked')
3289
+ && !platformMenu.some((action) => action.id === 'resolveScopeConflict')
3290
+ ? {
3291
+ label: '检查受限清理…',
3292
+ description: '只在当前单一目标有精确、专属的 AW 管理恢复证据时进入新的确认;不会清理共享或外来内容。',
3293
+ status: 'warning',
3294
+ run: () => beginRestrictedForceCleanup(plugin.pluginId, route.platform, placement.scope),
3295
+ }
3296
+ : undefined;
3297
+ const platformActions = () => [
3298
+ ...platformMenu.map(platformAction),
3299
+ ...(offlineCleanup ? [offlineCleanup] : []),
3300
+ ...(restrictedForceCleanup ? [restrictedForceCleanup] : []),
3301
+ ];
2778
3302
  breadcrumb = `${plugin.displayName} / ${platformLabel(route.platform)}`;
2779
- const installedPlacementRows = placement?.state === 'installed' ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: "Desired placement", meta: placement.desiredScopes?.map(scopeLabel).join('、') || scopeLabel(placement.scope), compact: true }), _jsx(StaticRow, { title: "Actual scope \u72B6\u6001", meta: placement.actualState ?? placement.health, status: placement.actualState === 'ready' ? 'success' : placement.actualState === 'blocked' ? 'error' : 'warning', compact: true }), placement.sharedNative ? _jsx(StaticRow, { title: "Shared native", meta: placement.sharedNative.state, status: placement.sharedNative.state === 'usable' ? 'success' : placement.sharedNative.state === 'blocked' ? 'error' : 'warning', compact: true }) : null, placement.activation ? _jsx(StaticRow, { title: "Activation", meta: `user ${placement.activation.user ? 'on' : 'off'} · project ${placement.activation.project ? 'on' : 'off'}`, compact: true }) : null, placement.extensions ? _jsx(StaticRow, { title: "AW-only extensions", meta: `${placement.extensions.state} · ${placement.extensions.kinds.join('、') || '无'}`, status: placement.extensions.state === 'ready' ? 'success' : placement.extensions.state === 'conflict' ? 'error' : 'warning', compact: true }) : null] }) : null;
3303
+ const installedPlacementRows = placement?.state === 'installed' ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: "\u4F4D\u7F6E", meta: placement.desiredScopes?.map(scopeLabel).join('、') || scopeLabel(placement.scope), compact: true }), _jsx(StaticRow, { title: "\u72B6\u6001", meta: placement.actualState === 'ready' ? '可用'
3304
+ : placement.actualState === 'needsSync' ? '需要同步'
3305
+ : placement.actualState === 'blocked' ? '已阻断'
3306
+ : placement.health === 'incomplete' ? '需要处理' : '暂时不可用', status: placement.actualState === 'ready' ? 'success' : placement.actualState === 'blocked' ? 'error' : 'warning', compact: true }), placement.health !== 'ready'
3307
+ ? _jsx(StaticRow, { title: "\u539F\u56E0", meta: placement.blockingReason === 'artifactCollision' ? '输出位置存在冲突内容' : '当前安装与已保存设置不一致', status: "warning", compact: true })
3308
+ : null] }) : null;
2780
3309
  if (plugin.aggregateStatus === 'unavailable') {
2781
3310
  const sourceOverview = capabilityUnavailable ? undefined : props.controller.getSourceOverview(plugin.sourceId);
2782
3311
  const diagnostic = capabilityUnavailable
@@ -2786,7 +3315,7 @@ export function ConsoleAppV4(props) {
2786
3315
  ? `需要升级 AW CLI 才能管理 ${platformLabel(route.platform)}`
2787
3316
  : `暂时无法管理 ${platformLabel(route.platform)}`;
2788
3317
  description = '';
2789
- actions = platformMenu.map(platformAction);
3318
+ actions = platformActions();
2790
3319
  content = _jsxs(Box, { flexDirection: "column", children: [diagnostic
2791
3320
  ? _jsx(DiagnosticSummary, { diagnostic: diagnostic, compact: policy.mode === 'compact' })
2792
3321
  : null, _jsx(SemanticRows, { actions: actions, focus: effectiveFocus, kind: "action", compact: policy.mode === 'compact' })] });
@@ -2794,7 +3323,7 @@ export function ConsoleAppV4(props) {
2794
3323
  else if (!placement || placement.state === 'notInstalled') {
2795
3324
  title = `尚未添加到 ${platformLabel(route.platform)}`;
2796
3325
  description = '此插件支持所列使用位置。';
2797
- actions = platformMenu.map(platformAction);
3326
+ actions = platformActions();
2798
3327
  content = _jsxs(Box, { flexDirection: "column", children: [_jsx(StatusCard, { status: "muted", title: "\u672A\u6DFB\u52A0", description: "\u63D2\u4EF6\u8BBE\u7F6E\u4FDD\u6301\u4E0D\u53D8" }), _jsx(SemanticRows, { actions: actions, focus: effectiveFocus, kind: "action", compact: policy.mode === 'compact' })] });
2799
3328
  }
2800
3329
  else if (placement.state === 'blocked'
@@ -2803,13 +3332,13 @@ export function ConsoleAppV4(props) {
2803
3332
  description = placement.state === 'installed'
2804
3333
  ? '受管内容仍可安全移除;输出位置同时存在冲突内容。'
2805
3334
  : '输出位置已有不属于当前插件的内容。';
2806
- actions = platformMenu.map(platformAction);
3335
+ actions = platformActions();
2807
3336
  content = _jsxs(Box, { flexDirection: "column", children: [_jsx(StatusCard, { status: "error", title: "\u8F93\u51FA\u51B2\u7A81", description: "AW \u4E0D\u4F1A\u8986\u76D6\u6216\u5220\u9664\u5360\u7528\u5185\u5BB9" }), _jsx(SemanticRows, { actions: actions, focus: effectiveFocus, kind: "action", compact: policy.mode === 'compact' })] });
2808
3337
  }
2809
3338
  else if (placement.state === 'scopeConflict') {
2810
3339
  title = '安装位置冲突';
2811
3340
  description = '正常状态只能保留一个位置。';
2812
- actions = platformMenu.map(platformAction);
3341
+ actions = platformActions();
2813
3342
  content = _jsx(SemanticRows, { actions: actions, focus: effectiveFocus, kind: "action", compact: policy.mode === 'compact' });
2814
3343
  }
2815
3344
  else if (placement.health === 'error') {
@@ -2834,13 +3363,13 @@ export function ConsoleAppV4(props) {
2834
3363
  };
2835
3364
  title = `${platformLabel(route.platform)} 暂时不可管理`;
2836
3365
  description = '';
2837
- actions = platformMenu.map(platformAction);
3366
+ actions = platformActions();
2838
3367
  content = _jsxs(Box, { flexDirection: "column", children: [_jsx(DiagnosticSummary, { diagnostic: diagnostic, compact: policy.mode === 'compact' }), _jsx(SemanticRows, { actions: actions, focus: effectiveFocus, kind: "action", compact: policy.mode === 'compact' })] });
2839
3368
  }
2840
3369
  else if (placement.health === 'incomplete') {
2841
3370
  title = `${platformLabel(route.platform)} ${placement.health === 'incomplete' ? '安装不完整' : '暂时不可用'}`;
2842
3371
  description = '缺少一项必要内容,插件可能无法稳定启动。';
2843
- actions = platformMenu.map(platformAction);
3372
+ actions = platformActions();
2844
3373
  const actualTitle = placement.actualState === 'needsSync' ? '需要同步'
2845
3374
  : placement.actualState === 'blocked' ? '当前有安全阻断' : '需要处理';
2846
3375
  content = _jsxs(Box, { flexDirection: "column", children: [_jsx(StatusCard, { status: placement.actualState === 'blocked' ? 'error' : 'warning', title: actualTitle, description: `${scopeLabel(placement.scope)} · ${plugin.sourceLabel}`, children: installedPlacementRows }), _jsx(SemanticRows, { actions: actions, focus: effectiveFocus, kind: "action", compact: policy.mode === 'compact' })] });
@@ -2849,7 +3378,7 @@ export function ConsoleAppV4(props) {
2849
3378
  title = `已安装到${scopeLabel(placement.scope)}`;
2850
3379
  const installedVersion = placement.revision.installedVersion;
2851
3380
  description = `当前检查正常${installedVersion ? `;已安装版本 ${installedVersion}` : ''}。`;
2852
- actions = platformMenu.map(platformAction);
3381
+ actions = platformActions();
2853
3382
  content = _jsxs(Box, { flexDirection: "column", children: [_jsx(StatusCard, { status: "success", title: "\u72B6\u6001\u6B63\u5E38", description: `${scopeLabel(placement.scope)}${installedVersion ? ` · 已安装版本 ${installedVersion}` : ''}`, children: installedPlacementRows }), _jsx(SemanticRows, { actions: actions, focus: effectiveFocus, kind: "action", compact: policy.mode === 'compact' })] });
2854
3383
  }
2855
3384
  }
@@ -2886,7 +3415,7 @@ export function ConsoleAppV4(props) {
2886
3415
  ? '↑↓ 选择配置 · Enter 修改 · ←→ 分组 · Esc 返回'
2887
3416
  : '↑↓ 移动 · Enter 修改 · ←→ 分组 · s 保存 · u 恢复继承 · Esc 返回';
2888
3417
  if (!currentConfigPage) {
2889
- content = _jsx(StatusCard, { status: "info", title: "\u6B63\u5728\u8BFB\u53D6\u53EF\u4FE1\u8BBE\u7F6E\u5B9A\u4E49", children: _jsx(ProgressRow, { title: "\u8BFB\u53D6 manifest \u8BBE\u7F6E\u5B57\u6BB5", state: "active" }) });
3418
+ content = _jsx(StatusCard, { status: "info", title: "\u6B63\u5728\u8BFB\u53D6\u53EF\u4FE1\u8BBE\u7F6E\u5B9A\u4E49", children: _jsx(ProgressRow, { title: "\u8BFB\u53D6\u63D2\u4EF6\u8BBE\u7F6E\u5B57\u6BB5", state: "active" }) });
2890
3419
  }
2891
3420
  else {
2892
3421
  visibleConfigTabs = configTabs(currentConfigPage);
@@ -2900,7 +3429,13 @@ export function ConsoleAppV4(props) {
2900
3429
  ? (activeGroup?.fields ?? []).filter((field) => field.sharedNativeRender && !field.writable)
2901
3430
  : [];
2902
3431
  if (projectSharedNativeFields.length > 0) {
2903
- description += ' 等待设置由个人默认控制共享 native;当前项目不可改。更改影响所有项目。';
3432
+ description += ' 此设置由个人默认控制平台共享内容;当前项目不可改。更改影响所有项目。';
3433
+ }
3434
+ if (projectSharedNativeFields.some(configFieldCanUnset)) {
3435
+ description += ' 发现不再支持的 project override;实际使用 user/default;请删除旧值。';
3436
+ }
3437
+ if (!readonly && configDraftIssues.length > 0) {
3438
+ description += ' 字段已变化;失效草稿不会保存。';
2904
3439
  }
2905
3440
  const mutableLayer = {
2906
3441
  scope: route.layer.scope,
@@ -2919,7 +3454,7 @@ export function ConsoleAppV4(props) {
2919
3454
  })
2920
3455
  : () => applyConfigPreset(currentConfigPage, preset.id),
2921
3456
  }))
2922
- : (activeGroup?.fields ?? []).filter((field) => readonly || field.writable).map((field) => ({
3457
+ : (activeGroup?.fields ?? []).filter((field) => readonly || configFieldCanSet(field) || configFieldCanUnset(field)).map((field) => ({
2923
3458
  label: field.title,
2924
3459
  description: field.invalidOverrideIssue
2925
3460
  ? field.invalidOverrideOwnedByLayer
@@ -2927,7 +3462,9 @@ export function ConsoleAppV4(props) {
2927
3462
  : field.invalidOverrideIssue
2928
3463
  : readonly
2929
3464
  ? field.sourceLabel === '未设置' ? '没有任何位置提供值' : `来自${field.sourceLabel}`
2930
- : `${field.description ? `${field.description} · ` : ''}当前有效:${effectiveFieldDisplay(field)} · ${field.effectiveSourceLabel}`,
3465
+ : !configFieldCanSet(field) && configFieldCanUnset(field)
3466
+ ? '不再支持的 project override · 实际使用 user/default · 可删除旧值'
3467
+ : `${field.description ? `${field.description} · ` : ''}当前有效:${effectiveFieldDisplay(field)} · ${field.effectiveSourceLabel}`,
2931
3468
  meta: readonly ? effectiveFieldDisplay(field) : currentFieldDisplay(field),
2932
3469
  configKey: field.key,
2933
3470
  run: readonly
@@ -2937,7 +3474,7 @@ export function ConsoleAppV4(props) {
2937
3474
  fieldKey: field.key,
2938
3475
  groupId: activeGroup.id,
2939
3476
  })
2940
- : () => go(field.configured ? {
3477
+ : () => go(field.configured || !configFieldCanSet(field) ? {
2941
3478
  name: 'configFieldAction',
2942
3479
  pluginId: plugin.pluginId,
2943
3480
  layer: mutableLayer,
@@ -2952,17 +3489,21 @@ export function ConsoleAppV4(props) {
2952
3489
  }),
2953
3490
  }));
2954
3491
  const operationActions = readonly ? [] : [
2955
- ...buildAllowedConfigActions(dirty).map((action) => ({
2956
- label: action.label,
2957
- description: route.layer.scope === 'user'
2958
- && [...Object.keys(draftUpdates), ...draftRemoves].some((key) => currentConfigPage.groups.flatMap((group) => group.fields)
2959
- .some((field) => field.key === key && field.sharedNativeRender))
2960
- ? action.apply
2961
- ? '保存并重新生成共享 Codex native Skill;影响所有启用项目'
2962
- : '只保存个人默认;共享 Codex native render 将待同步'
2963
- : action.apply ? '保存后同步当前可见安装' : '只保存所选设置层,不应用当前安装',
2964
- run: () => beginConfigSave(action.apply),
2965
- })),
3492
+ ...(configDraftIssues.length > 0 ? [{
3493
+ label: '放弃已变化字段的草稿',
3494
+ description: `${configDraftIssues.length} 项草稿的字段不存在、不可编辑或不再可删除;不会修改已保存设置。`,
3495
+ status: 'warning',
3496
+ run: discardInvalidConfigDraftChanges,
3497
+ }] : []),
3498
+ ...(dirty && configDraftIssues.length === 0 ? [{
3499
+ label: '查看保存汇总',
3500
+ description: '确认变化,再选择只保存或保存并应用',
3501
+ status: 'success',
3502
+ run: () => {
3503
+ setFocus(0);
3504
+ go({ name: 'configSave', pluginId: route.pluginId, layer: route.layer });
3505
+ },
3506
+ }] : []),
2966
3507
  ...(currentConfigPage.storedCount > 0 ? [{
2967
3508
  label: route.layer.scope === 'user'
2968
3509
  ? '清空个人默认…'
@@ -2993,29 +3534,47 @@ export function ConsoleAppV4(props) {
2993
3534
  ? activeGroup?.fields[effectiveConfigFocus]
2994
3535
  : undefined;
2995
3536
  if (!readonly) {
2996
- const canUnsetFocused = focusedConfigField?.configured === true
2997
- && (!focusedConfigField.invalidOverrideIssue
2998
- || focusedConfigField.invalidOverrideOwnedByLayer === true);
3537
+ const canUnsetFocused = focusedConfigField
3538
+ ? configFieldCanUnset(focusedConfigField)
3539
+ && (!focusedConfigField.invalidOverrideIssue
3540
+ || focusedConfigField.invalidOverrideOwnedByLayer === true)
3541
+ : false;
2999
3542
  help = canUnsetFocused
3000
- ? '↑↓ 移动 · Enter 修改 · ←→ 分组 · s 保存 · u 恢复继承 · Esc 返回'
3001
- : '↑↓ 移动 · Enter 修改 · ←→ 分组 · s 保存 · Esc 返回';
3543
+ ? '↑↓ 移动 · Enter 修改 · ←→ 分组 · s 查看汇总 · u 恢复继承 · Esc 返回'
3544
+ : '↑↓ 移动 · Enter 修改 · ←→ 分组 · s 查看汇总 · Esc 返回';
3002
3545
  }
3003
3546
  const fieldFocus = tabActions.length > 0
3004
3547
  ? effectiveConfigFocus < tabActions.length ? effectiveConfigFocus : Math.min(scrollOffset, tabActions.length - 1)
3005
3548
  : 0;
3006
3549
  const operationRows = operationActions.reduce((rows, action) => rows + (policy.mode === 'compact' || !action.description ? 1 : 2), 0);
3007
- const viewport = actionViewport(tabActions, policy.mode === 'compact', fieldFocus, scrollOffset, pageActionBudget(1 + operationRows + 3));
3550
+ const viewport = actionViewport(tabActions, policy.mode === 'compact', fieldFocus, scrollOffset, pageActionBudget(1 + operationRows + 3 + (configDraftIssues.length > 0 ? 3 : 0)));
3008
3551
  const totalFields = currentConfigPage.groups.reduce((count, group) => count + group.fields.length, 0);
3009
3552
  const countLabel = `${totalFields} 项${currentConfigPage.presets.length > 0 ? ` · ${currentConfigPage.presets.length} 个方案` : ''}`;
3010
3553
  context = readonly ? countLabel
3011
3554
  : dirty ? `${Object.keys(draftUpdates).length + draftRemoves.length} 项未保存` : countLabel;
3012
- content = _jsxs(Box, { flexDirection: "column", children: [activeTab ? _jsx(ConfigGroupTabs, { tabs: visibleConfigTabs, current: activeTab.id, compact: policy.mode === 'compact', columns: size.columns - 2 }) : null, activeTab && tabActions.length > 0
3555
+ content = _jsxs(Box, { flexDirection: "column", children: [!readonly && configDraftIssues.length > 0 ? _jsx(Callout, { status: "warning", title: "\u5B57\u6BB5\u5DF2\u53D8\u5316", description: "\u6765\u6E90\u6062\u590D\u540E\u5B57\u6BB5\u5B9A\u4E49\u5DF2\u66F4\u65B0\uFF1B\u5B57\u6BB5\u53EF\u80FD\u4E0D\u5B58\u5728\u3001\u4E0D\u53EF\u7F16\u8F91\u6216\u4E0D\u518D\u53EF\u5220\u9664\u3002\u8BF7\u663E\u5F0F\u653E\u5F03\u5931\u6548\u8349\u7A3F\uFF0C\u6216\u8FD4\u56DE\u7EE7\u7EED\u68C0\u67E5\u6765\u6E90\uFF1B\u4E0D\u4F1A\u81EA\u52A8\u4FDD\u5B58\u6216\u590D\u7528\u65E7\u786E\u8BA4\u3002" }) : null, activeTab ? _jsx(ConfigGroupTabs, { tabs: visibleConfigTabs, current: activeTab.id, compact: policy.mode === 'compact', columns: size.columns - 2 }) : null, activeTab && tabActions.length > 0
3013
3556
  ? _jsx(SectionCard, { title: activeTab.label, meta: viewport.hiddenBefore > 0 || viewport.hiddenAfter > 0 ? `${viewport.position}/${viewport.total}` : undefined, children: _jsx(SemanticRows, { actions: tabActions, focus: effectiveConfigFocus, kind: "section", compact: policy.mode === 'compact', offset: viewport.offset, limit: viewport.limit }) })
3014
3557
  : null, operationActions.length > 0
3015
3558
  ? _jsx(SemanticRows, { actions: actions, focus: effectiveConfigFocus, kind: "action", compact: policy.mode === 'compact', offset: tabActions.length, limit: operationActions.length })
3016
3559
  : null] });
3017
3560
  }
3018
3561
  }
3562
+ else if (route.name === 'configSave' && plugin && currentConfigPage) {
3563
+ 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));
3567
+ breadcrumb = `${plugin.displayName} / 设置 / 保存汇总`;
3568
+ context = `${changes.length} 项未保存`;
3569
+ 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) },
3574
+ { 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' })] });
3577
+ }
3019
3578
  else if (route.name === 'configFieldAction' && plugin && activeField) {
3020
3579
  const layer = configLayerLabel(route.layer);
3021
3580
  breadcrumb = `${plugin.displayName} / 设置 / ${layer} / ${activeField.title}`;
@@ -3026,23 +3585,25 @@ export function ConsoleAppV4(props) {
3026
3585
  ? '删除个人默认中的值'
3027
3586
  : '删除当前项目中的值';
3028
3587
  actions = [
3029
- {
3030
- label: '修改值…',
3031
- run: () => go({
3032
- name: 'configFieldEditor',
3033
- pluginId: plugin.pluginId,
3034
- layer: route.layer,
3035
- fieldKey: activeField.key,
3036
- editor: fieldEditor(activeField),
3037
- ...fieldEditorMode(activeField),
3038
- }),
3039
- },
3040
- ...(!activeField.invalidOverrideIssue || activeField.invalidOverrideOwnedByLayer ? [draftRemoves.includes(activeField.key)
3588
+ ...(configFieldCanSet(activeField) ? [{
3589
+ label: '修改值…',
3590
+ run: () => go({
3591
+ name: 'configFieldEditor',
3592
+ pluginId: plugin.pluginId,
3593
+ layer: route.layer,
3594
+ fieldKey: activeField.key,
3595
+ editor: fieldEditor(activeField),
3596
+ ...fieldEditorMode(activeField),
3597
+ }),
3598
+ }] : []),
3599
+ ...(configFieldCanUnset(activeField) && (!activeField.invalidOverrideIssue || activeField.invalidOverrideOwnedByLayer) ? [draftRemoves.includes(activeField.key)
3041
3600
  ? { label: '撤销删除', run: () => { cancelDraftRemoval(activeField.key); back(); } }
3042
3601
  : { label: deleteLabel, status: 'error', run: () => { removeDraft(activeField.key); back(); } }] : []),
3043
3602
  ];
3044
3603
  content = _jsxs(Box, { flexDirection: "column", children: [_jsxs(SectionCard, { children: [activeField.invalidOverrideIssue
3045
3604
  ? _jsx(StaticRow, { title: "\u5F53\u524D\u8986\u76D6\u65E0\u6548", meta: activeField.invalidOverrideIssue, status: "warning" })
3605
+ : null, !configFieldCanSet(activeField) && configFieldCanUnset(activeField)
3606
+ ? _jsx(StaticRow, { title: "\u4E0D\u518D\u652F\u6301\u7684 project override", meta: "\u5B9E\u9645\u4F7F\u7528 user/default\uFF1B\u5220\u9664\u65E7\u503C\u540E\u4FDD\u6301\u7EE7\u627F", status: "warning" })
3046
3607
  : null, _jsx(StaticRow, { title: `${layer}中的值`, meta: currentFieldDisplay(activeField) }), _jsx(StaticRow, { title: "\u5F53\u524D\u5B9E\u9645\u751F\u6548", meta: `${effectiveFieldDisplay(activeField)} · 来自${activeField.effectiveSourceLabel}` }), !activeField.invalidOverrideIssue || activeField.invalidOverrideOwnedByLayer
3047
3608
  ? _jsx(StaticRow, { title: "\u5220\u9664\u540E\u5C06\u6062\u590D", meta: `${activeField.fallbackDisplayValue ?? '未设置'} · 来自${activeField.fallbackSourceLabel ?? '未设置'}` })
3048
3609
  : null] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
@@ -3053,8 +3614,8 @@ export function ConsoleAppV4(props) {
3053
3614
  if (showChoices) {
3054
3615
  title = `选择${activeField.title}`;
3055
3616
  description = activeField.allowCustom
3056
- ? '先选择 manifest 方案,也可以继续自定义。'
3057
- : '此字段只允许使用 manifest 方案。';
3617
+ ? '先选择插件提供的方案,也可以继续自定义。'
3618
+ : '此字段只允许使用插件提供的方案。';
3058
3619
  help = '↑↓ 移动 · Enter 使用 · Esc 取消';
3059
3620
  const current = draftUpdates[activeField.key] ?? activeField.draftValue ?? activeField.value;
3060
3621
  const booleanCustomValues = activeField.type === 'boolean' && activeField.allowCustom
@@ -3164,7 +3725,7 @@ export function ConsoleAppV4(props) {
3164
3725
  }
3165
3726
  else {
3166
3727
  title = activeField.sensitive ? '输入敏感值' : `输入${activeField.title}`;
3167
- help = '输入 · Enter 使用 · Ctrl-U 删除本层值 · Esc 取消';
3728
+ help = '输入 · Enter 使用 · Ctrl-U 清空输入 · Esc 取消';
3168
3729
  description = route.editor === 'number'
3169
3730
  ? `允许范围:${activeField.min ?? '不限'}–${activeField.max ?? '不限'}${activeField.unit ? ` ${activeField.unit}` : ''}。`
3170
3731
  : activeField.sensitive ? '当前值不会回显;留空不会自动删除旧值。' : activeField.description ?? '';
@@ -3179,8 +3740,8 @@ export function ConsoleAppV4(props) {
3179
3740
  breadcrumb = `${plugin.displayName} / 设置 / ${activeField.title} / ${propertyTitle}`;
3180
3741
  if (route.stage === 'key') {
3181
3742
  title = '输入新键名';
3182
- description = inputError ?? '键名和属性数量会由 Core validator 检查。';
3183
- help = '输入 · Enter 继续 · Esc 取消';
3743
+ description = inputError ?? '键名和属性数量会按插件规则检查。';
3744
+ help = '输入 · Enter 继续 · Ctrl-U 清空输入 · Esc 取消';
3184
3745
  content = _jsx(InputCard, { focused: true, title: _jsx(InputDisplay, { value: inputValue, columns: Math.max(8, size.columns - 8) }), description: description, status: inputError ? 'error' : undefined });
3185
3746
  }
3186
3747
  else if (property !== undefined && definition && route.stage === 'action') {
@@ -3214,7 +3775,7 @@ export function ConsoleAppV4(props) {
3214
3775
  else if (property !== undefined && definition && route.stage === 'choices') {
3215
3776
  title = `选择${propertyTitle}`;
3216
3777
  description = definition.type !== 'boolean' && definition.choices?.allowCustom
3217
- ? '先选择 manifest 方案,也可以继续自定义。'
3778
+ ? '先选择插件提供的方案,也可以继续自定义。'
3218
3779
  : '只显示此属性允许的选择。';
3219
3780
  help = '↑↓ 移动 · Enter 使用 · Esc 取消';
3220
3781
  const current = activeObjectDraft[property];
@@ -3266,7 +3827,7 @@ export function ConsoleAppV4(props) {
3266
3827
  else if (property !== undefined && definition) {
3267
3828
  title = activePropertySensitive ? '输入敏感值' : `输入${propertyTitle}`;
3268
3829
  description = inputError ?? (activePropertySensitive ? '输入已隐藏。' : definition.description ?? '按 Enter 使用输入值。');
3269
- help = '输入 · Enter 使用 · Esc 取消';
3830
+ help = '输入 · Enter 使用 · Ctrl-U 清空输入 · Esc 取消';
3270
3831
  content = _jsx(InputCard, { focused: true, title: _jsx(InputDisplay, { value: inputValue, sensitive: activePropertySensitive, columns: Math.max(8, size.columns - 8) }), description: description, status: inputError ? 'error' : undefined });
3271
3832
  }
3272
3833
  }
@@ -3277,9 +3838,11 @@ export function ConsoleAppV4(props) {
3277
3838
  context = '暂时不可编辑';
3278
3839
  const diagnostic = configUnavailableDiagnostic(route);
3279
3840
  const invalidLayer = (route.invalidStores?.length ?? 0) > 0;
3841
+ const canRecoverSource = configUnavailableCanRecoverSource(route);
3280
3842
  title = diagnostic.title;
3281
3843
  description = '';
3282
3844
  const resetScopes = route.invalidStores?.map(({ scope }) => scope)
3845
+ ?? route.storedScopes
3283
3846
  ?? (route.layer ? [route.layer.scope] : []);
3284
3847
  actions = [
3285
3848
  ...resetScopes.flatMap((scope) => {
@@ -3298,19 +3861,23 @@ export function ConsoleAppV4(props) {
3298
3861
  }]
3299
3862
  : [];
3300
3863
  }),
3301
- ...(!invalidLayer ? [
3302
- { label: `在终端运行 aw catalog source sync ${unavailableSourceId}`, description: '同步成功后重新加载 TUI', run: back },
3303
- { label: showTechnical ? '返回问题摘要' : '技术详情', description: '查看脱敏错误代码和阶段', run: () => setShowTechnical((value) => !value) },
3864
+ ...(!invalidLayer && canRecoverSource ? [
3865
+ {
3866
+ label: '修复插件来源',
3867
+ description: `检查并恢复 ${unavailableSourceId};完成后返回当前设置位置。`,
3868
+ run: () => props.controller.beginConfigSourceRecovery(unavailableSourceId),
3869
+ },
3304
3870
  ] : []),
3871
+ { label: showTechnical ? '返回问题摘要' : '技术详情', description: '查看脱敏错误代码和阶段', run: () => toggleTechnical() },
3305
3872
  { label: '返回', run: back },
3306
3873
  ];
3307
3874
  const unsupportedScopes = resetScopes.filter((scope) => configResetAvailability[scope]?.resettable === false);
3308
3875
  const unsupportedRows = unsupportedScopes.map((scope) => _jsx(StaticRow, { title: `${scope === 'user' ? '个人默认' : '当前项目'}设置层不能自动重置:特殊节点,请手动移动`, status: "warning", compact: true }, `unsupported-reset:${scope}`));
3309
3876
  content = invalidLayer
3310
- ? _jsxs(Box, { flexDirection: "column", children: [_jsxs(TechnicalDetails, { children: [(route.invalidStores ?? []).flatMap((store) => [
3311
- _jsx(StaticRow, { title: store.scope === 'user' ? '个人默认' : '当前项目', meta: store.code, compact: true }, store.scope),
3312
- _jsx(StaticRow, { title: `路径:${store.pathSummary}`, compact: true }, `${store.scope}:path`),
3313
- ]), _jsx(StaticRow, { title: `${safeDiagnosticCode(route.diagnosticStage, 'read-config')} · 整层重置无备份`, meta: "\u6062\u590D\u7EE7\u627F/\u9ED8\u8BA4", compact: true })] }), unsupportedRows, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] })
3877
+ ? _jsxs(Box, { flexDirection: "column", children: [_jsx(SectionCard, { title: "\u4E0D\u53EF\u8BFB\u53D6\u7684\u8BBE\u7F6E\u5C42", children: (route.invalidStores ?? []).map((store) => _jsx(StaticRow, { title: store.scope === 'user' ? '个人默认设置' : '当前项目设置', meta: "\u9700\u8981\u91CD\u7F6E\u6216\u624B\u52A8\u79FB\u5F00", status: "warning", compact: true }, store.scope)) }), showTechnical ? _jsxs(TechnicalDetails, { children: [(route.invalidStores ?? []).flatMap((store) => [
3878
+ _jsx(StaticRow, { title: scopeLabel(store.scope), meta: store.code, compact: true }, store.scope),
3879
+ _jsx(StaticRow, { title: "path summary", meta: store.pathSummary, compact: true }, `${store.scope}:path`),
3880
+ ]), _jsx(StaticRow, { title: "stage", meta: safeDiagnosticCode(route.diagnosticStage, 'read-config'), compact: true })] }) : null, unsupportedRows, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] })
3314
3881
  : _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' })] });
3315
3882
  }
3316
3883
  else if (route.name === 'sourceList') {
@@ -3340,9 +3907,14 @@ export function ConsoleAppV4(props) {
3340
3907
  };
3341
3908
  }),
3342
3909
  { label: '+ 添加来源', description: 'Git 仓库或本地目录', run: beginAddSource },
3910
+ {
3911
+ label: '检查全部来源',
3912
+ description: '并行检查各来源;结果不会自动启用待检查版本',
3913
+ run: () => props.controller.recheckAllCatalogSources(),
3914
+ },
3343
3915
  ];
3344
3916
  const viewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, responsiveBudget(8, 12));
3345
- const sourceCount = Math.max(0, actions.length - 1);
3917
+ const sourceCount = Math.max(0, actions.length - 2);
3346
3918
  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 }) });
3347
3919
  }
3348
3920
  else if (route.name === 'sourceDetail') {
@@ -3356,92 +3928,103 @@ export function ConsoleAppV4(props) {
3356
3928
  const combinedRepairAndMismatch = generationState === 'repairRequired'
3357
3929
  && generation?.configMismatch === true;
3358
3930
  const logicalIdentityChanged = generation?.errorCode === 'catalogLogicalIdentityChanged';
3359
- const activationBlocked = props.controller.catalogActivationBlockReason();
3931
+ const activationBlocked = props.controller.catalogSourceRecoveryBlockReason();
3360
3932
  breadcrumb = `插件来源 / ${route.sourceId}`;
3361
3933
  context = detail?.type === 'path' ? '本地目录' : 'Git';
3362
3934
  title = route.sourceId;
3363
- description = freshness?.state === 'incompatible'
3364
- ? activeVersion
3365
- ? `当前 AW 缺少 ${freshness.missingCapabilities?.length ?? 0} 项候选 Catalog 能力;Active Catalog ${activeVersion} 仍可浏览和使用。`
3366
- : `当前 AW 缺少 ${freshness.missingCapabilities?.length ?? 0} 项候选 Catalog 能力;这个来源没有可用 Active Catalog。`
3367
- : freshness?.state === 'checkFailed' && activeVersion
3368
- ? `更新检查失败,但 Active Catalog ${activeVersion} 仍可浏览和使用。`
3369
- : freshness?.state === 'updateAvailable'
3370
- ? `当前使用 ${activeVersion ?? '未启用'};候选版本 ${candidateVersion ?? '未知'} 可更新。`
3371
- : freshness?.state === 'localChangesAvailable'
3372
- ? '本地 Catalog 内容已变化;确认前仍使用当前 Active。'
3373
- : activeVersion ? `当前使用 Active Catalog ${activeVersion}。`
3374
- : generationState === 'stateInvalid' ? 'Catalog generation state 无效。可在 Review 后恢复状态并启用这个来源。'
3375
- : generationState === 'repairRequired' ? combinedRepairAndMismatch
3376
- ? 'Active Catalog 条目损坏,且来源配置已变化。验证候选后将精确修复同一条目,或显式切换到新候选。'
3377
- : 'Active Catalog 条目缺失或损坏。可在这里精确修复。'
3378
- : generationState === 'configMismatch' ? '来源配置与 Active 不一致。可验证候选并切换。'
3379
- : logicalIdentityChanged ? 'Catalog logical identity 已变化。请编辑来源,或移除后重新添加。'
3380
- : '来源没有可用 Active。可在这里获取并启用。';
3935
+ description = generationState === 'incompatible'
3936
+ ? `当前目录版本与 AW 不兼容;缺少 ${generation?.compatibility?.state === 'unsupported' ? generation.compatibility.missingCapabilities.length : 0} 项功能支持。`
3937
+ : freshness?.state === 'incompatible'
3938
+ ? generationState === 'repairRequired'
3939
+ ? `待检查版本需要升级 AW 或修改来源后才能使用;损坏的当前启用版本仍可按旧版本精确修复。`
3940
+ : activeVersion
3941
+ ? `当前 AW 缺少 ${freshness.missingCapabilities?.length ?? 0} 项待检查版本所需功能;当前目录版本 ${activeVersion} 仍可浏览和使用。`
3942
+ : `当前 AW 缺少 ${freshness.missingCapabilities?.length ?? 0} 项待检查版本所需功能;这个来源没有可用目录版本。`
3943
+ : freshness?.state === 'checkFailed' && activeVersion
3944
+ ? `更新检查失败,但当前目录版本 ${activeVersion} 仍可浏览和使用。`
3945
+ : freshness?.state === 'updateAvailable'
3946
+ ? `当前使用 ${activeVersion ?? '未启用'};待检查版本 ${candidateVersion ?? '未知'} 可更新。`
3947
+ : freshness?.state === 'localChangesAvailable'
3948
+ ? '本地插件目录内容已变化;确认前仍使用当前启用版本。'
3949
+ : activeVersion ? `当前使用目录版本 ${activeVersion}。`
3950
+ : generationState === 'stateInvalid' ? '目录版本状态无效。确认恢复后可以重新启用这个来源。'
3951
+ : generationState === 'repairRequired' ? combinedRepairAndMismatch
3952
+ ? '当前目录版本损坏,且来源配置已变化。验证待检查版本后可精确修复同一版本,或明确切换到新版本。'
3953
+ : '当前目录版本缺失或损坏。可在这里精确修复。'
3954
+ : generationState === 'configMismatch' ? '来源配置与当前启用版本不一致。可验证待检查版本并切换。'
3955
+ : logicalIdentityChanged ? '来源发布身份已变化。请编辑来源,或移除后重新添加。'
3956
+ : '来源没有可用的启用版本。可在这里取得并启用。';
3381
3957
  help = '↑↓ 移动 · Enter 选择 · Esc 返回';
3382
- const generationAction = logicalIdentityChanged
3383
- || freshness?.state === 'incompatible' && generationState !== 'ready' ? []
3384
- : generationState === 'repairRequired' ? [{
3385
- label: '重新下载旧版本并精确修复',
3386
- description: '使用 Active 的精确 commit;成功后保持当前 Active 并停止',
3387
- disabled: activationBlocked ? `请先处理当前草稿或操作:${activationBlocked}` : undefined,
3388
- run: () => beginReview({
3389
- kind: 'catalogGeneration',
3390
- action: 'repair',
3391
- sourceId: route.sourceId,
3392
- }),
3393
- }, {
3394
- label: '使用当前候选替换损坏的 Active…',
3395
- description: '旧版本无法取得时使用;进入独立影响 Review',
3396
- disabled: activationBlocked ? `请先处理当前草稿或操作:${activationBlocked}` : undefined,
3397
- run: () => beginReview({
3398
- kind: 'catalogGeneration',
3399
- action: 'replaceDamaged',
3400
- sourceId: route.sourceId,
3401
- }),
3958
+ const generationAction = logicalIdentityChanged ? []
3959
+ : generationState === 'incompatible' ? [{
3960
+ label: '查看不兼容详情',
3961
+ description: '显示所需和缺少的功能支持',
3962
+ status: 'error',
3963
+ run: () => go({ name: 'sourceTechnical', sourceId: route.sourceId }),
3402
3964
  }]
3403
- : generationState !== 'ready' ? [{
3404
- label: generationState === 'stateInvalid' ? '恢复状态并启用'
3405
- : generationState === 'configMismatch' ? '验证并切换' : '获取并启用',
3406
- description: '先准备只读候选 Review;确认后才切换 Active',
3407
- disabled: activationBlocked ? `请先处理当前草稿或操作:${activationBlocked}` : undefined,
3965
+ : generationState === 'repairRequired' ? [{
3966
+ label: '重新下载旧版本并精确修复',
3967
+ description: '使用当前启用版本的精确提交;成功后保持此版本',
3968
+ disabled: activationBlocked ? `请先处理:${sourceOperationBlockLabel(activationBlocked)}` : undefined,
3408
3969
  run: () => beginReview({
3409
3970
  kind: 'catalogGeneration',
3410
- action: generationState === 'stateInvalid' ? 'recoverState'
3411
- : generationState === 'configMismatch' ? 'update' : 'activate',
3971
+ action: 'repair',
3412
3972
  sourceId: route.sourceId,
3413
3973
  }),
3414
- }]
3415
- : freshness?.state === 'updateAvailable' || freshness?.state === 'localChangesAvailable'
3416
- ? [{
3417
- label: freshness.state === 'localChangesAvailable'
3418
- ? '应用本地内容变化…' : `更新到 ${candidateVersion ?? '候选版本'}…`,
3419
- description: '进入 Review;确认前不会切换 Active',
3420
- disabled: activationBlocked ? `请先处理当前草稿或操作:${activationBlocked}` : undefined,
3421
- run: () => beginReview({ kind: 'catalogGeneration', action: 'update', sourceId: route.sourceId }),
3974
+ }, ...(freshness?.state === 'incompatible' ? [] : [{
3975
+ label: '使用待检查版本替换损坏版本…',
3976
+ description: '旧版本无法取得时使用;进入独立影响确认',
3977
+ disabled: activationBlocked ? `请先处理:${sourceOperationBlockLabel(activationBlocked)}` : undefined,
3978
+ run: () => beginReview({
3979
+ kind: 'catalogGeneration',
3980
+ action: 'replaceDamaged',
3981
+ sourceId: route.sourceId,
3982
+ }),
3983
+ }])]
3984
+ : freshness?.state === 'incompatible' && generationState !== 'ready' ? []
3985
+ : generationState !== 'ready' ? [{
3986
+ label: generationState === 'stateInvalid' ? '恢复状态并启用'
3987
+ : generationState === 'configMismatch' ? '验证并切换' : '获取并启用',
3988
+ description: '先准备只读的待检查版本确认;确认后才切换当前版本',
3989
+ disabled: activationBlocked ? `请先处理:${sourceOperationBlockLabel(activationBlocked)}` : undefined,
3990
+ run: () => beginReview({
3991
+ kind: 'catalogGeneration',
3992
+ action: generationState === 'stateInvalid' ? 'recoverState'
3993
+ : generationState === 'configMismatch' ? 'update' : 'activate',
3994
+ sourceId: route.sourceId,
3995
+ }),
3422
3996
  }]
3423
- : freshness?.state === 'versionConflict'
3424
- || freshness?.state === 'rollbackDetected'
3425
- || freshness?.state === 'incompatible'
3426
- ? [{
3427
- label: freshness.state === 'versionConflict' ? '查看版本冲突'
3428
- : freshness.state === 'rollbackDetected' ? '查看回退详情' : '查看不兼容详情',
3429
- description: '普通更新已阻止;当前 Active 保持不变',
3430
- status: 'error',
3431
- run: () => go({ name: 'sourceTechnical', sourceId: route.sourceId }),
3432
- }]
3433
- : [{
3434
- label: freshness?.state === 'checking' ? '正在检查…' : '重新检查',
3435
- description: '轻量检查这个来源;commit 变化时才完整验证',
3436
- disabled: freshness?.state === 'checking' ? '检查正在进行' : undefined,
3437
- run: () => props.controller.recheckCatalogSource(route.sourceId),
3438
- }];
3997
+ : freshness?.state === 'updateAvailable' || freshness?.state === 'localChangesAvailable'
3998
+ ? [{
3999
+ label: freshness.state === 'localChangesAvailable'
4000
+ ? '应用本地内容变化…' : `更新到 ${candidateVersion ?? '待检查版本'}…`,
4001
+ description: '进入确认;确认前不会切换当前版本',
4002
+ disabled: activationBlocked ? `请先处理:${sourceOperationBlockLabel(activationBlocked)}` : undefined,
4003
+ run: () => beginReview({ kind: 'catalogGeneration', action: 'update', sourceId: route.sourceId }),
4004
+ }]
4005
+ : freshness?.state === 'versionConflict'
4006
+ || freshness?.state === 'rollbackDetected'
4007
+ || freshness?.state === 'incompatible'
4008
+ ? [{
4009
+ label: freshness.state === 'versionConflict' ? '查看版本冲突'
4010
+ : freshness.state === 'rollbackDetected' ? '查看回退详情' : '查看不兼容详情',
4011
+ description: '普通更新已阻止;当前启用版本保持不变',
4012
+ status: 'error',
4013
+ run: () => go({ name: 'sourceTechnical', sourceId: route.sourceId }),
4014
+ }]
4015
+ : [{
4016
+ label: freshness?.state === 'checking' ? '正在检查…' : '重新检查',
4017
+ description: '轻量检查这个来源;提交发生变化时才完整验证',
4018
+ disabled: freshness?.state === 'checking' ? '检查正在进行' : undefined,
4019
+ run: () => props.controller.recheckCatalogSource(route.sourceId),
4020
+ }];
3439
4021
  actions = [
3440
4022
  ...generationAction,
3441
- { label: '编辑来源…', description: '修改前先 Review;默认保存并启用', run: () => go({ name: 'sourceEdit', sourceId: route.sourceId }) },
3442
- ...(freshness?.state === 'incompatible' && generationState !== 'ready' ? [] : [{
4023
+ { label: '编辑来源…', description: '集中修改地址、引用、子目录和自动检查', run: () => openSourceEdit(route.sourceId) },
4024
+ ...(generationState === 'incompatible'
4025
+ || (freshness?.state === 'incompatible' && generationState !== 'ready') ? [] : [{
3443
4026
  label: generationState === 'ready' ? '设为主来源…' : '启用并设为主来源…',
3444
- description: generationState === 'ready' ? '只改变默认 marketplace' : '先启用确认的候选,再设为主来源',
4027
+ description: generationState === 'ready' ? '只改变默认插件来源' : '先启用确认的待检查版本,再设为主来源',
3445
4028
  disabled: detail?.primary ? '当前已经是主来源'
3446
4029
  : logicalIdentityChanged ? '请编辑来源,或移除后重新添加' : undefined,
3447
4030
  run: () => generationState === 'ready'
@@ -3456,7 +4039,7 @@ export function ConsoleAppV4(props) {
3456
4039
  }),
3457
4040
  }]),
3458
4041
  { label: '移除来源…', description: '移除来源配置', status: 'error', disabled: detail?.primary ? '主来源不能直接移除' : undefined, run: () => beginReview({ kind: 'sourceMutation', action: 'remove', sourceId: route.sourceId, changes: [{ title: `移除来源 ${route.sourceId}` }] }) },
3459
- { label: '技术详情', description: '查看脱敏错误代码和 Active 证据', run: () => go({ name: 'sourceTechnical', sourceId: route.sourceId }) },
4042
+ { label: '技术详情', description: '查看脱敏错误代码和当前版本证据', run: () => go({ name: 'sourceTechnical', sourceId: route.sourceId }) },
3460
4043
  ...(generationState !== 'ready'
3461
4044
  || freshness?.state === 'updateAvailable'
3462
4045
  || freshness?.state === 'localChangesAvailable'
@@ -3464,7 +4047,7 @@ export function ConsoleAppV4(props) {
3464
4047
  || freshness?.state === 'rollbackDetected'
3465
4048
  || freshness?.state === 'incompatible' ? [{
3466
4049
  label: freshness?.state === 'checking' ? '正在检查…' : '重新检查',
3467
- description: '轻量检查这个来源;commit 变化时才完整验证',
4050
+ description: '轻量检查这个来源;提交发生变化时才完整验证',
3468
4051
  disabled: freshness?.state === 'checking' ? '检查正在进行' : undefined,
3469
4052
  run: () => props.controller.recheckCatalogSource(route.sourceId),
3470
4053
  }] : []),
@@ -3477,7 +4060,7 @@ export function ConsoleAppV4(props) {
3477
4060
  ];
3478
4061
  const sourceActionViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, responsiveBudget(2, 5));
3479
4062
  const freshnessLabel = catalogSourceFreshnessLabel(freshness);
3480
- content = _jsxs(Box, { flexDirection: "column", children: [_jsxs(SectionCard, { title: "Active Catalog", status: activeVersion ? 'success' : 'warning', compact: policy.mode === 'compact', children: [_jsx(StaticRow, { title: activeVersion ? `Catalog ${activeVersion}` : '未启用', meta: activeVersion ? generation?.active?.digestShort : generationState, compact: true }), candidateVersion ? _jsx(StaticRow, { title: `候选 Catalog ${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 })] });
4063
+ 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 })] });
3481
4064
  }
3482
4065
  else if (route.name === 'sourceTechnical') {
3483
4066
  const overview = props.controller.getSourceOverview(route.sourceId);
@@ -3501,9 +4084,9 @@ export function ConsoleAppV4(props) {
3501
4084
  context = '只读';
3502
4085
  title = '来源技术诊断';
3503
4086
  description = '';
3504
- help = 'Enter 返回 · Esc 返回';
4087
+ help = 'PgUp/PgDn 查看全部技术详情 · Enter 返回 · Esc 返回';
3505
4088
  actions = [{ label: '返回来源详情', run: back }];
3506
- content = _jsxs(Box, { flexDirection: "column", children: [_jsx(DiagnosticSummary, { diagnostic: diagnostic, technical: true, compact: policy.mode === 'compact' }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
4089
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(DiagnosticSummary, { diagnostic: diagnostic, technical: true, compact: policy.mode === 'compact', offset: batchTargetOffset, limit: responsiveBudget(5, 10) }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
3507
4090
  }
3508
4091
  else if (route.name === 'sourceAdd' && sourceDraft?.owner === route.owner) {
3509
4092
  breadcrumb = sourceDraft.mode === 'add' ? '插件来源 / 添加' : `插件来源 / ${sourceDraft.sourceId} / 编辑`;
@@ -3516,6 +4099,7 @@ export function ConsoleAppV4(props) {
3516
4099
  description = '来源类型创建后不能修改。';
3517
4100
  const selectType = (type, autoCheck) => {
3518
4101
  setSourceDraft({ ...sourceDraft, type, autoCheck });
4102
+ setDirty(true);
3519
4103
  go({ name: 'sourceAdd', step: sourceDraft.sourceId ? 'location' : 'id', owner: route.owner });
3520
4104
  };
3521
4105
  actions = [
@@ -3532,8 +4116,9 @@ export function ConsoleAppV4(props) {
3532
4116
  const useRefPreset = async (refPolicy, ref) => {
3533
4117
  const next = { ...sourceDraft, refPolicy, ref };
3534
4118
  setSourceDraft(next);
3535
- if (next.mode === 'update')
3536
- await beginSourceUpdateReview(next);
4119
+ setDirty(true);
4120
+ if (route.returnToDraft)
4121
+ acceptSourceFieldAndBack();
3537
4122
  else
3538
4123
  go({ name: 'sourceAdd', step: 'subdir', owner: route.owner });
3539
4124
  };
@@ -3541,7 +4126,12 @@ export function ConsoleAppV4(props) {
3541
4126
  { label: '跟随 AW CLI', description: `当前 CLI ${snapshot.cliChannel === 'beta' ? 'beta' : 'release'} → catalog/${snapshot.cliChannel === 'beta' ? 'beta' : 'latest'}`, selected: sourceDraft.refPolicy === 'cliChannel', run: () => { const policy = catalogRefPolicyForPreset('followCli'); return useRefPreset(policy.type, ''); } },
3542
4127
  { label: 'Release', description: '固定读取 catalog/latest', selected: sourceDraft.refPolicy === 'explicit' && sourceDraft.ref === CATALOG_LATEST_REF, run: () => { const policy = catalogRefPolicyForPreset('release'); return useRefPreset(policy.type, policy.type === 'explicit' ? policy.ref : ''); } },
3543
4128
  { label: 'Beta', description: '固定读取 catalog/beta', selected: sourceDraft.refPolicy === 'explicit' && sourceDraft.ref === CATALOG_BETA_REF, run: () => { const policy = catalogRefPolicyForPreset('beta'); return useRefPreset(policy.type, policy.type === 'explicit' ? policy.ref : ''); } },
3544
- { label: '指定 Git ref…', description: 'branch、tag、完整 ref 或 commit', selected: sourceDraft.refPolicy === 'explicit' && sourceDraft.ref !== CATALOG_LATEST_REF && sourceDraft.ref !== CATALOG_BETA_REF, run: () => { setSourceDraft({ ...sourceDraft, refPolicy: 'explicit' }); go({ name: 'sourceAdd', step: 'ref', owner: route.owner }); } },
4129
+ { label: '指定 Git ref…', description: 'branch、tag、完整 ref 或 commit', selected: sourceDraft.refPolicy === 'explicit' && sourceDraft.ref !== CATALOG_LATEST_REF && sourceDraft.ref !== CATALOG_BETA_REF, run: () => {
4130
+ if (route.returnToDraft)
4131
+ props.controller.replace({ name: 'sourceAdd', step: 'ref', owner: route.owner, returnToDraft: true });
4132
+ else
4133
+ go({ name: 'sourceAdd', step: 'ref', owner: route.owner });
4134
+ } },
3545
4135
  ];
3546
4136
  content = _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: tuiTheme.colors.muted, children: ["\u8DDF\u968F\u542B\u4E49\uFF1A\u5F53\u524D CLI ", snapshot.cliChannel === 'beta' ? 'beta → catalog/beta' : 'release → catalog/latest'] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "choice", compact: policy.mode === 'compact' })] });
3547
4137
  }
@@ -3558,60 +4148,115 @@ export function ConsoleAppV4(props) {
3558
4148
  }
3559
4149
  else if (route.name === 'sourceEdit') {
3560
4150
  const detail = props.controller.getSourceOverview(route.sourceId).detail;
3561
- breadcrumb = `插件来源 / ${route.sourceId} / 编辑`;
3562
- title = '选择要修改的内容';
3563
- description = '来源 ID 和类型只读。';
3564
- const startEdit = (field) => {
3565
- if (!detail)
3566
- return;
3567
- const owner = `source-update:${route.sourceId}:${field}:${Date.now()}`;
3568
- setSourceDraft({ owner, mode: 'update', sourceId: route.sourceId, type: detail.type, location: '', refPolicy: detail.refPolicy ?? 'cliChannel', ref: detail.explicitRef ?? '', subdir: detail.subdir ?? '', autoCheck: detail.autoCheck, editField: field });
3569
- setDirty(true);
3570
- go({ name: 'sourceAdd', step: field === 'location' ? 'location' : field === 'subdir' ? 'subdir' : 'refPolicy', owner });
4151
+ const activeDraft = route.owner && sourceDraft?.owner === route.owner ? sourceDraft : undefined;
4152
+ const startDraftField = (step) => {
4153
+ let draft = activeDraft;
4154
+ if (!draft) {
4155
+ const source = props.controller.getConfiguredSource(route.sourceId);
4156
+ if (!source)
4157
+ return;
4158
+ const owner = `source-update:${route.sourceId}:${Date.now()}`;
4159
+ draft = sourceDraftFromSource(source, owner);
4160
+ setSourceDraft(draft);
4161
+ props.controller.replace({ name: 'sourceEdit', sourceId: route.sourceId, owner });
4162
+ }
4163
+ go({ name: 'sourceAdd', step, owner: draft.owner, returnToDraft: true });
3571
4164
  };
3572
- const switchOfficialTransport = (location) => {
3573
- if (!detail || detail.type !== 'git'
3574
- || detail.locationSummary === projectCatalogSourceLocation(location))
4165
+ const draft = activeDraft;
4166
+ breadcrumb = `插件来源 / ${route.sourceId} / 编辑`;
4167
+ title = draft?.mode === 'add' ? `检查新来源 ${draft.sourceId}` : `编辑 ${route.sourceId}`;
4168
+ description = '地址、引用、子目录和自动检查组成一个草稿;保存前统一 Review。';
4169
+ const locationSummary = draft
4170
+ ? projectCatalogSourceLocation(draft.location)
4171
+ : detail?.locationSummary ?? '不可用';
4172
+ const setOfficialLocation = (location) => {
4173
+ const current = draft ?? (() => {
4174
+ const source = props.controller.getConfiguredSource(route.sourceId);
4175
+ if (!source)
4176
+ return undefined;
4177
+ return sourceDraftFromSource(source, `source-update:${route.sourceId}:${Date.now()}`);
4178
+ })();
4179
+ if (!current || current.type !== 'git' || current.location === location)
3575
4180
  return;
3576
- const owner = `source-update:${route.sourceId}:location:${Date.now()}`;
3577
- const next = {
3578
- owner,
3579
- mode: 'update',
3580
- sourceId: route.sourceId,
3581
- type: 'git',
3582
- location,
3583
- refPolicy: detail.refPolicy ?? 'cliChannel',
3584
- ref: detail.explicitRef ?? '',
3585
- subdir: detail.subdir ?? '',
3586
- autoCheck: detail.autoCheck,
3587
- editField: 'location',
3588
- };
4181
+ const next = { ...current, location };
3589
4182
  setSourceDraft(next);
3590
4183
  setDirty(true);
3591
- void beginSourceUpdateReview(next);
4184
+ props.controller.replace({ name: 'sourceEdit', sourceId: route.sourceId, owner: next.owner });
3592
4185
  };
3593
- actions = detail?.type === 'git' ? [
3594
- ...(route.sourceId === 'official' ? [
3595
- { label: 'HTTPS', description: '使用系统 Git credential helper', selected: detail.locationSummary === projectCatalogSourceLocation(OFFICIAL_CATALOG_SOURCE_URL), run: () => switchOfficialTransport(OFFICIAL_CATALOG_SOURCE_URL) },
3596
- { label: 'SSH', description: '使用本机 SSH key;Git 连接使用 BatchMode', selected: detail.locationSummary === projectCatalogSourceLocation(OFFICIAL_CATALOG_SOURCE_SSH_URL), run: () => switchOfficialTransport(OFFICIAL_CATALOG_SOURCE_SSH_URL) },
3597
- ] : [{ label: 'Git URL', description: detail.locationSummary, run: () => startEdit('location') }]),
3598
- { label: '版本引用', description: detail.refPolicy === 'explicit' ? `固定:${detail.explicitRef}` : '跟随 AW 版本通道', run: () => startEdit('refPolicy') },
3599
- { label: 'Catalog 子目录', description: detail.subdir ?? '未设置', run: () => startEdit('subdir') },
3600
- { label: '自动检查更新', description: detail.autoCheck ? '已开启' : '已关闭', selected: detail.autoCheck, run: () => beginReview({ kind: 'sourceMutation', action: 'update', sourceId: route.sourceId, update: { autoCheck: !detail.autoCheck }, changes: [{ title: '自动检查更新', before: detail.autoCheck ? '开启' : '关闭', after: detail.autoCheck ? '关闭' : '开启' }] }) },
3601
- ] : [
3602
- { label: '本地目录', description: '已选择本地目录', run: () => startEdit('location') },
3603
- { label: '自动检查更新', description: detail?.autoCheck ? '已开启' : '已关闭', selected: detail?.autoCheck, run: () => detail && beginReview({ kind: 'sourceMutation', action: 'update', sourceId: route.sourceId, update: { autoCheck: !detail.autoCheck }, changes: [{ title: '自动检查更新', before: detail.autoCheck ? '开启' : '关闭', after: detail.autoCheck ? '关闭' : '开启' }] }) },
4186
+ actions = [
4187
+ ...(route.sourceId === 'official' && (draft?.type === 'git' || !draft && detail?.type === 'git') ? [
4188
+ { label: 'HTTPS', description: '使用系统 Git credential helper', selected: locationSummary === projectCatalogSourceLocation(OFFICIAL_CATALOG_SOURCE_URL), run: () => setOfficialLocation(OFFICIAL_CATALOG_SOURCE_URL) },
4189
+ { label: 'SSH', description: '使用本机 SSH key;Git 连接使用 BatchMode', selected: locationSummary === projectCatalogSourceLocation(OFFICIAL_CATALOG_SOURCE_SSH_URL), run: () => setOfficialLocation(OFFICIAL_CATALOG_SOURCE_SSH_URL) },
4190
+ ] : [{ label: draft?.type === 'path' || detail?.type === 'path' ? '本地目录' : 'Git 仓库地址', description: locationSummary, run: () => startDraftField('location') }]),
4191
+ ...(draft?.type === 'git' || !draft && detail?.type === 'git' ? [
4192
+ { label: '版本引用', description: draft ? draft.refPolicy === 'explicit' ? `固定:${draft.ref}` : '跟随 AW 版本通道' : detail?.refPolicy === 'explicit' ? `固定:${detail.explicitRef}` : '跟随 AW 版本通道', run: () => startDraftField('refPolicy') },
4193
+ { label: 'Catalog 子目录', description: draft?.subdir || detail?.subdir || '未设置', run: () => startDraftField('subdir') },
4194
+ ] : []),
4195
+ { label: '自动检查更新', description: (draft?.autoCheck ?? detail?.autoCheck) ? '已开启' : '已关闭', selected: draft?.autoCheck ?? detail?.autoCheck, run: () => {
4196
+ if (!draft) {
4197
+ const source = props.controller.getConfiguredSource(route.sourceId);
4198
+ if (!source)
4199
+ return;
4200
+ const owner = `source-update:${route.sourceId}:${Date.now()}`;
4201
+ const next = sourceDraftFromSource(source, owner);
4202
+ next.autoCheck = !next.autoCheck;
4203
+ setSourceDraft(next);
4204
+ setDirty(true);
4205
+ props.controller.replace({ name: 'sourceEdit', sourceId: route.sourceId, owner });
4206
+ return;
4207
+ }
4208
+ setSourceDraft({ ...draft, autoCheck: !draft.autoCheck });
4209
+ setDirty(true);
4210
+ } },
4211
+ { label: '查看修改汇总', description: draft && sourceDraftChanges(draft).length > 0 ? `${sourceDraftChanges(draft).length} 项未保存` : '尚无修改', disabled: draft && sourceDraftChanges(draft).length > 0 ? undefined : '请先修改来源设置', status: 'success', run: () => {
4212
+ if (!draft)
4213
+ return;
4214
+ setFocus(0);
4215
+ go({ name: 'sourceSave', owner: draft.owner });
4216
+ } },
4217
+ ];
4218
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsxs(SectionCard, { title: "\u6765\u6E90\u8349\u7A3F", compact: policy.mode === 'compact', children: [_jsx(StaticRow, { title: "\u6765\u6E90 ID", meta: draft?.sourceId ?? route.sourceId, compact: true }), _jsx(StaticRow, { title: "\u7C7B\u578B", meta: (draft?.type ?? detail?.type) === 'git' ? 'Git 仓库' : '本地目录', compact: true })] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "section", compact: policy.mode === 'compact' }), route.sourceId === 'official' && detail?.type === 'git' ? _jsxs(SectionCard, { title: "\u6765\u6E90\u8EAB\u4EFD", children: [_jsx(StaticRow, { title: "\u903B\u8F91 Catalog", meta: "lesliechan721-arch/agent-workflow" }), _jsx(StaticRow, { title: "source identity", meta: "HTTPS / SSH \u4FDD\u6301\u4E00\u81F4" }), _jsx(StaticRow, { title: "\u660E\u6587 HTTP", meta: "\u4E0D\u652F\u6301", status: "warning" })] }) : null] });
4219
+ }
4220
+ else if (route.name === 'sourceSave' && sourceDraft?.owner === route.owner) {
4221
+ const changes = sourceDraftChanges(sourceDraft);
4222
+ breadcrumb = `插件来源 / ${sourceDraft.sourceId} / 修改汇总`;
4223
+ context = `${changes.length} 项未保存`;
4224
+ title = sourceDraft.mode === 'add' ? `保存新来源 ${sourceDraft.sourceId}?` : `保存 ${sourceDraft.sourceId} 的修改?`;
4225
+ description = '选择是否在保存后验证候选并启用。保存成功与启用成功会分别报告。';
4226
+ actions = [
4227
+ { label: '保存并启用', description: '保存来源配置,再验证并切换当前目录版本', status: 'success', run: () => beginSourceReview(sourceDraft, true) },
4228
+ { label: '只保存', description: '只保存来源配置;当前目录版本保持不变', run: () => beginSourceReview(sourceDraft, false) },
4229
+ { label: '返回修改', run: back },
3604
4230
  ];
3605
- content = _jsxs(Box, { flexDirection: "column", children: [_jsx(SemanticRows, { actions: actions, focus: focus, kind: "section", compact: policy.mode === 'compact' }), route.sourceId === 'official' && detail?.type === 'git' ? _jsxs(SectionCard, { title: "\u6765\u6E90\u8EAB\u4EFD", children: [_jsx(StaticRow, { title: "\u903B\u8F91 Catalog", meta: "lesliechan721-arch/agent-workflow" }), _jsx(StaticRow, { title: "source identity", meta: "HTTPS / SSH \u4FDD\u6301\u4E00\u81F4" }), _jsx(StaticRow, { title: "\u660E\u6587 HTTP", meta: "\u4E0D\u652F\u6301", status: "warning" })] }) : null] });
4231
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(SectionCard, { title: "\u6765\u6E90\u53D8\u5316", compact: policy.mode === 'compact', children: changes.map((change) => _jsx(ReviewChangeRow, { title: `~ ${change.title}`, description: `${change.before ?? '未设置'} → ${change.after}`, compact: true }, change.title)) }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
4232
+ }
4233
+ else if (route.name === 'scriptTimeout' && pendingReviewIntent) {
4234
+ breadcrumb = '高级选项 / 安装前检查脚本';
4235
+ context = '毫秒';
4236
+ title = '设置脚本超时';
4237
+ description = `输入 ${SCRIPT_TIMEOUT_MIN_MS}–${SCRIPT_TIMEOUT_MAX_MS} 毫秒。默认 ${SCRIPT_TIMEOUT_DEFAULT_MS} 毫秒。`;
4238
+ help = '输入 · Enter 应用并重新准备 Review · Ctrl-U 清空 · Esc 取消';
4239
+ content = _jsx(Box, { flexDirection: "column", children: _jsx(InputCard, { focused: true, title: _jsx(InputDisplay, { value: scriptTimeoutInput, columns: Math.max(8, size.columns - 8) }), description: scriptTimeoutError ?? '此值将绑定本次 Review。', status: scriptTimeoutError ? 'error' : undefined }) });
3606
4240
  }
3607
4241
  else if (route.name === 'sourceRemoveBlocked') {
3608
4242
  breadcrumb = `插件来源 / ${route.sourceId} / 移除`;
3609
4243
  context = '已阻止';
3610
4244
  title = `无法移除 ${route.sourceId}`;
3611
4245
  description = sourceReferenceSummary?.inventoryComplete === false ? '引用清单不完整,不能把未知引用当作零。' : '仍有内容依赖这个来源。';
3612
- actions = [{ label: sourceDependenciesExpanded ? '收起依赖内容' : '查看依赖内容', description: '只显示插件、来源、平台、位置和类别聚合', run: () => setSourceDependenciesExpanded((value) => !value) }, { label: '返回来源详情', run: back }];
4246
+ actions = [{ label: sourceDependenciesExpanded ? '收起依赖内容' : '查看依赖内容', description: '只显示插件、来源、平台、位置和类别聚合', run: () => {
4247
+ setSourceDependenciesExpanded((value) => !value);
4248
+ setBatchTargetOffset(0);
4249
+ } }, { label: '返回来源详情', run: back }];
3613
4250
  const countLabel = (value, unit) => value === undefined ? '未知' : `${value} ${unit}`;
3614
- content = _jsxs(Box, { flexDirection: "column", children: [_jsx(Callout, { status: "error", title: sourceReferenceSummary?.inventoryComplete === false ? '引用清单不完整' : '发现活动引用' }), _jsxs(SectionCard, { title: "\u5F15\u7528\u6C47\u603B", children: [_jsx(StaticRow, { title: "\u5B89\u88C5\u4F4D\u7F6E", meta: countLabel(sourceReferenceSummary?.installationCount, '个') }), _jsx(StaticRow, { title: "\u63D2\u4EF6\u8BBE\u7F6E", meta: countLabel(sourceReferenceSummary?.configCount, '份') }), _jsx(StaticRow, { title: "\u751F\u547D\u5468\u671F\u610F\u56FE", meta: countLabel(sourceReferenceSummary?.lifecycleCount, '项') }), _jsx(StaticRow, { title: "Marketplace", meta: countLabel(sourceReferenceSummary?.marketplaceCount, '个') }), sourceDependenciesExpanded ? sourceReferenceSummary?.dependencies.slice(0, Math.max(1, pageActionBudget(10))).map((dependency, index) => _jsx(StaticRow, { title: `${dependency.category} · ${dependency.pluginId ?? dependency.sourceId ?? '身份未知'}`, meta: `${dependency.platform ? platformLabel(dependency.platform) : ''}${dependency.scope ? ` · ${scopeLabel(dependency.scope)}` : ''}` }, `${dependency.category}:${dependency.pluginId ?? dependency.sourceId ?? index}`)) : null] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
4251
+ const referenceLimit = Math.max(1, pageActionBudget(10));
4252
+ const references = sourceReferenceSummary?.dependencies ?? [];
4253
+ const referenceOffset = Math.min(batchTargetOffset, Math.max(0, references.length - 1));
4254
+ const visibleReferences = references.slice(referenceOffset, referenceOffset + referenceLimit);
4255
+ const hiddenReferences = Math.max(0, references.length - referenceOffset - visibleReferences.length);
4256
+ if (sourceDependenciesExpanded && references.length > referenceLimit) {
4257
+ help = 'PgUp/PgDn 查看全部引用 · ↑↓ 移动 · Enter 选择 · Esc 返回';
4258
+ }
4259
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(Callout, { status: "error", title: sourceReferenceSummary?.inventoryComplete === false ? '引用清单不完整' : '发现活动引用' }), _jsxs(SectionCard, { title: "\u5F15\u7528\u6C47\u603B", children: [_jsx(StaticRow, { title: "\u5B89\u88C5\u4F4D\u7F6E", meta: countLabel(sourceReferenceSummary?.installationCount, '个') }), _jsx(StaticRow, { title: "\u63D2\u4EF6\u8BBE\u7F6E", meta: countLabel(sourceReferenceSummary?.configCount, '份') }), _jsx(StaticRow, { title: "\u751F\u547D\u5468\u671F\u610F\u56FE", meta: countLabel(sourceReferenceSummary?.lifecycleCount, '项') }), _jsx(StaticRow, { title: "Marketplace", meta: countLabel(sourceReferenceSummary?.marketplaceCount, '个') }), sourceDependenciesExpanded && referenceOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${referenceOffset} 项`, status: "muted", compact: true }) : null, sourceDependenciesExpanded ? visibleReferences.map((dependency, index) => _jsx(StaticRow, { title: `${dependency.category} · ${dependency.pluginId ?? dependency.sourceId ?? '身份未知'}`, meta: `${dependency.platform ? platformLabel(dependency.platform) : ''}${dependency.scope ? ` · ${scopeLabel(dependency.scope)}` : ''}` }, `${dependency.category}:${dependency.pluginId ?? dependency.sourceId ?? referenceOffset + index}`)) : null, sourceDependenciesExpanded && hiddenReferences > 0 ? _jsx(StaticRow, { title: `↓ 后面还有 ${hiddenReferences} 项`, status: "muted", compact: true }) : null] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
3615
4260
  }
3616
4261
  else if (route.name === 'issueList') {
3617
4262
  const issues = props.controller.listUserIssues();
@@ -3623,7 +4268,7 @@ export function ConsoleAppV4(props) {
3623
4268
  : '当前状态检查没有发现需要处理的安装、来源或设置问题。';
3624
4269
  if (issues.length === 0) {
3625
4270
  actions = [
3626
- { label: '返回首页', run: () => go({ name: 'home' }) },
4271
+ { label: '返回首页', run: () => requestSessionNavigation('home') },
3627
4272
  { label: '浏览插件', run: () => go({ name: 'pluginList', mode: 'browse' }) },
3628
4273
  ];
3629
4274
  }
@@ -3706,7 +4351,7 @@ export function ConsoleAppV4(props) {
3706
4351
  go({ name: 'pluginDependencies', pluginId: entry.pluginId });
3707
4352
  }
3708
4353
  else
3709
- setShowTechnical((value) => !value);
4354
+ toggleTechnical();
3710
4355
  };
3711
4356
  const issueHasTechnicalDetails = Boolean(issue.diagnostic.technical);
3712
4357
  actions = issue.actions.filter((entry) => entry.id !== 'viewTechnical' || issueHasTechnicalDetails).map((entry) => ({
@@ -3718,7 +4363,7 @@ export function ConsoleAppV4(props) {
3718
4363
  }));
3719
4364
  const issuePlugin = issue.pluginId ? props.controller.getPluginOverview(issue.pluginId) : undefined;
3720
4365
  const issueOverview = issue.pluginId ? props.controller.getPluginDependencyOverview(issue.pluginId) : undefined;
3721
- const issueDiagnosticSummary = _jsx(DiagnosticSummary, { diagnostic: issue.diagnostic, technical: showTechnical, compact: policy.mode === 'compact' || issue.category.startsWith('dependency')
4366
+ const issueDiagnosticSummary = _jsx(DiagnosticSummary, { diagnostic: issue.diagnostic, technical: showTechnical, offset: showTechnical ? batchTargetOffset : undefined, limit: showTechnical ? responsiveBudget(5, 10) : undefined, compact: policy.mode === 'compact' || issue.category.startsWith('dependency')
3722
4367
  || issue.category === 'scopeConflict' });
3723
4368
  const issueActionViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(6));
3724
4369
  const compactTechnicalActionViewport = policy.mode === 'compact' && showTechnical
@@ -3736,7 +4381,7 @@ export function ConsoleAppV4(props) {
3736
4381
  actions = [
3737
4382
  { label: '查看完整依赖关系', run: () => go({ name: 'pluginDependencies', pluginId: issuePlugin.pluginId }) },
3738
4383
  { label: '返回插件详情', run: () => go({ name: 'pluginDetail', pluginId: issuePlugin.pluginId }) },
3739
- ...(issueHasTechnicalDetails ? [{ label: showTechnical ? '隐藏技术详情' : '技术详情', run: () => setShowTechnical((value) => !value) }] : []),
4384
+ ...(issueHasTechnicalDetails ? [{ label: showTechnical ? '隐藏技术详情' : '技术详情', run: () => toggleTechnical() }] : []),
3740
4385
  ];
3741
4386
  const dependencyBlockedViewport = policy.mode === 'compact' && showTechnical
3742
4387
  ? { offset: Math.min(Math.max(0, focus - 1), Math.max(0, actions.length - 2)), limit: 2 }
@@ -3778,7 +4423,7 @@ export function ConsoleAppV4(props) {
3778
4423
  go({ name: 'installTargets', pluginId: issuePlugin.pluginId });
3779
4424
  } },
3780
4425
  { label: '查看依赖关系', run: () => go({ name: 'pluginDependencies', pluginId: issuePlugin.pluginId }) },
3781
- ...(issueHasTechnicalDetails ? [{ label: showTechnical ? '隐藏技术详情' : '技术详情', run: () => setShowTechnical((value) => !value) }] : []),
4426
+ ...(issueHasTechnicalDetails ? [{ label: showTechnical ? '隐藏技术详情' : '技术详情', run: () => toggleTechnical() }] : []),
3782
4427
  ];
3783
4428
  const unsupportedTargetViewport = policy.mode === 'compact' && showTechnical
3784
4429
  ? { offset: Math.min(Math.max(0, focus - 1), Math.max(0, actions.length - 2)), limit: 2 }
@@ -3806,24 +4451,105 @@ export function ConsoleAppV4(props) {
3806
4451
  : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: issueActionViewport.offset, limit: issueActionViewport.limit })] });
3807
4452
  }
3808
4453
  }
4454
+ else if (route.name === 'operationHistory') {
4455
+ const records = snapshot.operationHistorySummaries;
4456
+ breadcrumb = '操作记录';
4457
+ context = `本次会话 · ${records.length} 项`;
4458
+ title = records.length === 0 ? '还没有操作记录' : '最近操作';
4459
+ description = records.length === 0
4460
+ ? '完成操作或遇到问题后,安全摘要会保留在这里。'
4461
+ : '记录只包含安全摘要,不携带可重放的确认票据。';
4462
+ actions = records.length === 0
4463
+ ? [{ label: '返回首页', run: () => requestSessionNavigation('home') }]
4464
+ : [
4465
+ ...records.map((record) => ({
4466
+ label: record.message,
4467
+ description: `${operationHistoryState(record).label} · ${record.diagnosticCount} 项安全诊断`,
4468
+ meta: operationHistoryMeta(record),
4469
+ status: operationHistoryState(record).status,
4470
+ run: () => go({ name: 'operationDiagnostic', recordId: record.recordId }),
4471
+ })),
4472
+ { label: '返回上一页', run: back },
4473
+ ];
4474
+ const viewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(4));
4475
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(StatusCard, { status: records.some((record) => record.state !== 'succeeded') ? 'warning' : 'success', title: records.length === 0 ? '没有可回看的安全结果' : '操作结果与问题', description: "\u9009\u62E9\u4E00\u9879\u53EF\u67E5\u770B\u5B8C\u6574\u5B89\u5168\u8BE6\u60C5\u6216\u62A5\u544A\u9884\u89C8\u3002", compact: policy.mode === 'compact' }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: viewport.offset, limit: viewport.limit })] });
4476
+ }
4477
+ else if (route.name === 'operationDiagnostic') {
4478
+ const detail = props.controller.getOperationDiagnostic(route.recordId);
4479
+ breadcrumb = '操作记录 / 技术详情';
4480
+ context = '只读 · 已脱敏';
4481
+ title = detail ? '安全技术详情' : '操作记录已不可用';
4482
+ description = detail
4483
+ ? '可滚动查看所有目标、阶段、代码和已保留的安全原因。'
4484
+ : '这项会话记录已经不再可用。';
4485
+ const preview = detail ? props.controller.previewOperationReport(route.recordId) : undefined;
4486
+ actions = detail ? [
4487
+ preview
4488
+ ? { label: '预览安全报告', description: '确认内容后选择本地保存位置', run: () => go({ name: 'operationReportPreview', recordId: route.recordId }) }
4489
+ : { label: '报告当前不可用', disabled: '无法生成这项记录的报告' },
4490
+ { label: '返回操作记录', run: back },
4491
+ ] : [{ label: '返回操作记录', run: back }];
4492
+ const detailLineLimit = policy.mode === 'compact' ? 2 : Math.max(1, pageActionBudget(5));
4493
+ content = detail ? _jsxs(Box, { flexDirection: "column", children: [_jsx(OperationDiagnosticScreen, { detail: detail, compact: policy.mode === 'compact', offset: scrollOffset, limit: detailLineLimit }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] }) : _jsxs(Box, { flexDirection: "column", children: [_jsx(Callout, { status: "warning", title: "\u8BB0\u5F55\u5DF2\u4E0D\u53EF\u7528", description: "\u8FD4\u56DE\u64CD\u4F5C\u8BB0\u5F55\u540E\u53EF\u67E5\u770B\u4ECD\u5728\u672C\u6B21\u4F1A\u8BDD\u4E2D\u7684\u7ED3\u679C\u3002" }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
4494
+ }
4495
+ else if (route.name === 'operationReportPreview') {
4496
+ const preview = props.controller.previewOperationReport(route.recordId);
4497
+ breadcrumb = '操作记录 / 报告预览';
4498
+ context = '只读预览';
4499
+ title = reportSaveMode ? '选择报告保存位置' : preview ? '安全报告预览' : '报告已不可用';
4500
+ description = reportSaveMode
4501
+ ? '输入一个新的本地文件路径,然后按 Enter 保存。'
4502
+ : preview
4503
+ ? '保存前请确认报告内容。不会自动上传或发送。'
4504
+ : '这项会话报告已经不再可用。';
4505
+ if (reportSaveMode && preview) {
4506
+ help = 'Enter 保存 · Ctrl-U 清空输入 · Esc 返回报告预览';
4507
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(InputCard, { title: "\u672C\u5730\u6587\u4EF6\u8DEF\u5F84", focused: true, compact: policy.mode === 'compact', children: _jsx(InputDisplay, { value: reportSavePath, placeholder: "/tmp/aw-operation-report.md", columns: Math.max(12, size.columns - 10) }) }), reportSaveError ? _jsx(Callout, { status: "error", title: "\u62A5\u544A\u672A\u4FDD\u5B58", description: reportSaveError, compact: policy.mode === 'compact' }) : null, _jsx(StaticRow, { title: "\u4EC5\u4FDD\u5B58\u5F53\u524D\u9884\u89C8\u4E2D\u7684\u5B89\u5168\u5185\u5BB9\u3002", status: "muted", compact: true })] });
4508
+ }
4509
+ else if (preview) {
4510
+ actions = [
4511
+ { label: '选择本地保存位置…', description: '保存前输入明确的文件路径', run: () => {
4512
+ setReportSaveMode(true);
4513
+ setReportSaveError(undefined);
4514
+ setReportSaveResult(undefined);
4515
+ } },
4516
+ { label: '保存到临时目录', description: '使用唯一的临时文件名保存当前安全报告', run: () => {
4517
+ void props.controller.saveOperationReport(route.recordId).then(() => {
4518
+ setReportSaveResult('报告已保存到临时目录。');
4519
+ setReportSaveError(undefined);
4520
+ }).catch(() => {
4521
+ setReportSaveError('报告未保存。请检查保存位置后重试。');
4522
+ });
4523
+ } },
4524
+ { label: '返回技术详情', run: back },
4525
+ ];
4526
+ const reportLineLimit = policy.mode === 'compact' ? 2 : Math.max(1, pageActionBudget(5));
4527
+ const reportActionViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(5));
4528
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(OperationReportPreviewScreen, { preview: preview, compact: policy.mode === 'compact', offset: scrollOffset, limit: reportLineLimit }), reportSaveResult ? _jsx(Callout, { status: "success", title: "\u62A5\u544A\u5DF2\u4FDD\u5B58", description: reportSaveResult, compact: policy.mode === 'compact' }) : null, reportSaveError ? _jsx(Callout, { status: "error", title: "\u62A5\u544A\u672A\u4FDD\u5B58", description: reportSaveError, compact: policy.mode === 'compact' }) : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: reportActionViewport.offset, limit: reportActionViewport.limit })] });
4529
+ }
4530
+ else {
4531
+ actions = [{ label: '返回技术详情', run: back }];
4532
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(Callout, { status: "warning", title: "\u62A5\u544A\u5DF2\u4E0D\u53EF\u7528", description: "\u8FD4\u56DE\u6280\u672F\u8BE6\u60C5\u540E\u53EF\u67E5\u770B\u4ECD\u5728\u672C\u6B21\u4F1A\u8BDD\u4E2D\u7684\u8BB0\u5F55\u3002" }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
4533
+ }
4534
+ }
3809
4535
  else if (route.name === 'reviewPreparing' && snapshot.reviewPreparation) {
3810
4536
  const preparation = snapshot.reviewPreparation;
3811
- const labels = preparation.intent.kind === 'portableInstall'
4537
+ const labels = preparation.operationKind === 'portableInstall'
3812
4538
  ? ['通用 Skills / 安装', '正在准备通用 Skills 安装确认']
3813
- : preparation.intent.kind === 'configSave' || preparation.intent.kind === 'configReset'
4539
+ : preparation.operationKind === 'configSave' || preparation.operationKind === 'configReset'
3814
4540
  ? ['设置', '正在准备设置确认']
3815
- : preparation.intent.kind === 'sourceMutation' || preparation.intent.kind === 'catalogGeneration'
4541
+ : preparation.operationKind === 'sourceMutation' || preparation.operationKind === 'catalogGeneration'
3816
4542
  ? ['插件来源', '正在准备来源变更确认']
3817
4543
  : ['插件操作', '正在准备插件操作确认'];
3818
4544
  breadcrumb = `${labels[0]} / 准备确认`;
3819
4545
  context = '只读';
3820
4546
  title = preparation.state === 'failed' ? '无法准备确认信息' : labels[1];
3821
- const refreshPreparation = preparation.intent.kind === 'pluginWrite'
3822
- && preparation.intent.action === 'refreshAndSync';
4547
+ const refreshPreparation = preparation.operationKind === 'pluginWrite'
4548
+ && preparation.stages.some((stage) => stage.id === 'refresh');
3823
4549
  description = preparation.state === 'failed'
3824
4550
  ? preparation.message ?? '无法准备确认信息。请返回检查选择,或重试。'
3825
4551
  : refreshPreparation
3826
- ? '先刷新相关来源的弱 Active 状态,再读取并验证新的同步计划;不会修改插件安装或设置。'
4552
+ ? '先刷新相关来源的当前状态,再读取并验证新的同步计划;不会修改插件安装或设置。'
3827
4553
  : '这个阶段只读取和验证状态,不会修改任何安装或设置。';
3828
4554
  help = preparation.state === 'running'
3829
4555
  ? 'Esc 取消 · Ctrl-C 完成后退出'
@@ -3837,11 +4563,13 @@ export function ConsoleAppV4(props) {
3837
4563
  { label: '返回修改', run: back },
3838
4564
  ] : [];
3839
4565
  const stageState = (state) => state === 'running' ? 'active' : state === 'completed' ? 'complete' : state === 'failed' ? 'error' : 'pending';
3840
- content = _jsxs(Box, { flexDirection: "column", children: [_jsx(SectionCard, { title: "\u51C6\u5907\u6B65\u9AA4", children: preparation.stages.map((stage) => _jsx(ProgressRow, { title: stage.label, state: stageState(stage.state), meta: stage.state === 'running' ? '进行中' : stage.state === 'completed' ? '完成' : stage.state === 'failed' ? '失败' : '未开始', compact: policy.mode === 'compact' }, stage.id)) }), preparation.currentLabel ? _jsx(StaticRow, { title: "\u5F53\u524D", meta: preparation.currentLabel, status: "muted" }) : null, preparation.code && preparation.message
3841
- ? _jsx(Callout, { status: "error", title: preparation.code, description: preparation.message }) : null, actions.length > 0 ? _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' }) : null] });
4566
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(SectionCard, { title: "\u51C6\u5907\u6B65\u9AA4", children: preparation.stages.map((stage) => _jsx(ProgressRow, { title: stage.label, state: stageState(stage.state), meta: stage.state === 'running' ? '进行中' : stage.state === 'completed' ? '完成' : stage.state === 'failed' ? '失败' : '未开始', compact: policy.mode === 'compact' }, stage.id)) }), preparation.subject ? _jsx(StaticRow, { title: "\u5F53\u524D", meta: preparation.subject, status: "muted" }) : null, preparation.code && preparation.message
4567
+ ? _jsx(Callout, { status: "error", title: "\u786E\u8BA4\u4FE1\u606F\u672A\u51C6\u5907\u5B8C\u6210", description: preparation.message }) : null, showTechnical && preparation.code
4568
+ ? _jsx(TechnicalDetails, { children: _jsx(StaticRow, { title: "failure code", meta: preparation.code, compact: true }) }) : null, actions.length > 0 ? _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' }) : null] });
3842
4569
  }
3843
4570
  else if (route.name === 'review' && snapshot.activeReview) {
3844
4571
  const review = snapshot.activeReview;
4572
+ const batchReview = review.operationKind === 'pluginWriteBatch';
3845
4573
  const sharedImpactConfirmed = !review.requiresConfirmation
3846
4574
  || confirmedSharedReviewToken === review.reviewToken;
3847
4575
  breadcrumb = `${review.subject.name} / ${review.title}`;
@@ -3849,17 +4577,21 @@ export function ConsoleAppV4(props) {
3849
4577
  description = snapshot.result?.state === 'stale'
3850
4578
  ? '当前状态已变化。请检查新的变化后再次确认。'
3851
4579
  : review.requiresConfirmation ? '' : review.description;
4580
+ if (batchReview) {
4581
+ context = `${review.batch?.requestedPluginIds.length ?? 0} 个根插件 · ${review.batch?.groupCount ?? 0} 组`;
4582
+ help = 'PgUp/PgDn 查看全部根插件和依赖 · ↑↓ 移动 · Enter 选择 · Esc 返回';
4583
+ }
3852
4584
  actions = [
3853
4585
  ...(review.requiresConfirmation ? [{
3854
- label: sharedImpactConfirmed ? '已确认 shared impact' : '确认 shared impact',
3855
- description: review.sharedImpact.warning ?? '此操作会修改多个插件共享的 Codex native 内容',
4586
+ label: sharedImpactConfirmed ? '已确认共享影响' : '确认共享影响',
4587
+ description: review.sharedImpact.warning ?? '此操作会修改多个插件共同使用的 Codex 平台内容',
3856
4588
  selected: sharedImpactConfirmed,
3857
4589
  run: () => {
3858
4590
  setConfirmedSharedReviewToken(review.reviewToken);
3859
4591
  setFocus(1);
3860
4592
  },
3861
4593
  }] : []),
3862
- { label: review.primaryLabel, description: '执行前将再次检查当前状态', status: review.destructive ? 'error' : undefined, disabled: sharedImpactConfirmed ? undefined : '请先显式确认 shared impact', run: () => {
4594
+ { label: review.primaryLabel, description: '执行前将再次检查当前状态', status: review.destructive ? 'error' : undefined, disabled: sharedImpactConfirmed ? undefined : '请先确认共享影响', run: () => {
3863
4595
  if (operationBusy.current)
3864
4596
  return;
3865
4597
  operationBusy.current = true;
@@ -3870,6 +4602,23 @@ export function ConsoleAppV4(props) {
3870
4602
  operationBusy.current = false;
3871
4603
  });
3872
4604
  } },
4605
+ ...(sharedImpactConfirmed && (pendingReviewIntent?.kind === 'pluginWrite'
4606
+ && (pendingReviewIntent.action === 'install'
4607
+ || pendingReviewIntent.action === 'refreshAndSync'
4608
+ || pendingReviewIntent.action === 'reinstall'
4609
+ || pendingReviewIntent.action === 'sync')
4610
+ || pendingReviewIntent?.kind === 'pluginWriteBatch'
4611
+ && (pendingReviewIntent.action === 'install'
4612
+ || pendingReviewIntent.action === 'sync'
4613
+ || pendingReviewIntent.action === 'reinstall')
4614
+ || pendingReviewIntent?.kind === 'configSave' && pendingReviewIntent.apply) ? [{
4615
+ label: '高级选项:脚本超时',
4616
+ description: `${pendingReviewIntent.scriptTimeoutMs ?? SCRIPT_TIMEOUT_DEFAULT_MS} 毫秒`,
4617
+ run: () => {
4618
+ back();
4619
+ go({ name: 'scriptTimeout', owner: review.reviewToken });
4620
+ },
4621
+ }] : []),
3873
4622
  { label: '返回修改', run: review.operationKind === 'portableInstall' && snapshot.portableDraft
3874
4623
  ? async () => {
3875
4624
  if (await refreshPortableDraft(snapshot.portableDraft.pluginId))
@@ -3882,13 +4631,13 @@ export function ConsoleAppV4(props) {
3882
4631
  const groupTitle = {
3883
4632
  requested: review.operationKind === 'portableInstall' ? '已选择 Skills · 当前插件' : '你选择的插件',
3884
4633
  dependencies: review.operationKind === 'portableInstall' ? '已选择 Skills · 来自依赖插件' : '必需依赖',
3885
- projectIntent: 'Project intent',
3886
- userIntent: 'User intent',
3887
- sharedCodexNative: 'Shared Codex native',
3888
- userActivation: 'User activation',
3889
- projectActivation: 'Project activation',
3890
- awExtensions: 'AW extensions',
3891
- runtime: 'Codex runtime',
4634
+ projectIntent: '当前项目使用设置',
4635
+ userIntent: '所有项目使用设置',
4636
+ sharedCodexNative: 'Codex 平台共享内容',
4637
+ userActivation: '所有项目启用位置',
4638
+ projectActivation: '当前项目启用位置',
4639
+ awExtensions: 'AW 管理附加能力',
4640
+ runtime: '执行设置',
3892
4641
  portableTarget: '安装目标',
3893
4642
  portableInputs: '准备内容',
3894
4643
  portableWarnings: '覆盖冲突',
@@ -3934,7 +4683,7 @@ export function ConsoleAppV4(props) {
3934
4683
  allocations.set(group.key, current + added);
3935
4684
  remainingRows -= added;
3936
4685
  }
3937
- const changeGroups = visibleSpecs.map((group) => {
4686
+ const standardChangeGroups = visibleSpecs.map((group) => {
3938
4687
  const visibleCount = allocations.get(group.key) ?? 1;
3939
4688
  const hiddenCount = group.changes.length - visibleCount;
3940
4689
  return _jsxs(Box, { flexDirection: "column", children: [!compressedReview && group.label ? _jsx(Text, { bold: true, color: tuiTheme.colors.muted, children: group.label }) : null, group.changes.slice(0, visibleCount).map((change, index) => {
@@ -3952,12 +4701,23 @@ export function ConsoleAppV4(props) {
3952
4701
  return _jsx(ReviewChangeRow, { title: `${category}${reviewSymbol(change)} ${change.title}${description ? ` · ${description}` : ''}`, meta: count, status: changeStatus(change), compact: true }, `${change.title}:${index}`);
3953
4702
  })] }, group.key);
3954
4703
  });
4704
+ const batchChangeLimit = Math.max(1, responsiveBudget(6, 10) - actions.length);
4705
+ const batchChanges = review.changes.slice(batchTargetOffset, batchTargetOffset + batchChangeLimit);
4706
+ const batchHiddenBefore = batchTargetOffset;
4707
+ const prioritizedReviewChanges = [
4708
+ ...review.changes.filter((change) => change.kind === 'warning'),
4709
+ ...review.changes.filter((change) => change.kind !== 'warning'),
4710
+ ];
4711
+ const pagedReviewChanges = prioritizedReviewChanges.slice(batchTargetOffset, batchTargetOffset + batchChangeLimit);
4712
+ const changeGroups = batchReview || review.changes.length > staticRowBudget
4713
+ ? _jsxs(Box, { flexDirection: "column", children: [batchHiddenBefore > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${batchHiddenBefore} 项`, status: "muted", compact: true }) : null, (batchReview ? batchChanges : pagedReviewChanges).map((change, index) => _jsx(ReviewChangeRow, { title: `${change.sourceId ? `${change.sourceId} · ` : ''}${change.relation === 'requested' ? '根插件' : change.relation === 'direct' ? '直接依赖' : change.relation === 'transitive' ? '间接依赖' : ''} ${reviewSymbol(change)} ${change.title}`, meta: change.description, status: changeStatus(change), compact: true }, `${change.batchGroupId ?? ''}:${change.pluginId ?? change.title}:${index}`)), Math.max(0, review.changes.length - batchTargetOffset - (batchReview ? batchChanges : pagedReviewChanges).length) > 0 ? _jsx(StaticRow, { title: `↓ 后面还有 ${Math.max(0, review.changes.length - batchTargetOffset - (batchReview ? batchChanges : pagedReviewChanges).length)} 项`, status: "muted", compact: true }) : null] })
4714
+ : standardChangeGroups;
3955
4715
  const technicalDetails = review.technicalDetails ?? [];
3956
4716
  const technicalDetailLimit = responsiveBudget(1, 3);
3957
- const visibleTechnicalDetails = technicalDetails.slice(0, technicalDetailLimit);
3958
- const hiddenTechnicalDetails = technicalDetails.length - visibleTechnicalDetails.length;
4717
+ const visibleTechnicalDetails = technicalDetails.slice(batchTargetOffset, batchTargetOffset + technicalDetailLimit);
4718
+ const hiddenTechnicalDetails = Math.max(0, technicalDetails.length - batchTargetOffset - visibleTechnicalDetails.length);
3959
4719
  const technicalCardRows = 3 + visibleTechnicalDetails.length;
3960
- const technicalChangeBudget = Math.max(1, size.rows - 6 - 3 - (review.equivalentCommand ? 1 : 0) - actions.length - technicalCardRows);
4720
+ const technicalChangeBudget = Math.max(1, size.rows - 8 - 3 - (review.equivalentCommand ? 1 : 0) - actions.length - technicalCardRows);
3961
4721
  const prioritizedTechnicalChanges = [
3962
4722
  ...review.changes.filter((change) => change.kind === 'warning'),
3963
4723
  ...review.changes.filter((change) => change.kind !== 'warning'
@@ -3968,15 +4728,19 @@ export function ConsoleAppV4(props) {
3968
4728
  && change.group !== 'portableTarget'
3969
4729
  && (change.group === undefined || !technicalGroups.has(change.group))),
3970
4730
  ];
3971
- const visibleTechnicalChanges = prioritizedTechnicalChanges.slice(0, technicalChangeBudget);
3972
- const hiddenTechnicalChanges = review.changes.length - visibleTechnicalChanges.length;
4731
+ const visibleTechnicalChanges = prioritizedTechnicalChanges.slice(batchTargetOffset, batchTargetOffset + technicalChangeBudget);
4732
+ const hiddenTechnicalChanges = Math.max(0, review.changes.length - batchTargetOffset - visibleTechnicalChanges.length);
3973
4733
  const technicalChanges = visibleTechnicalChanges.map((change, index) => {
3974
4734
  const category = change.group ? groupTitle[change.group] : undefined;
3975
4735
  return _jsx(ReviewChangeRow, { title: `${category ? `${category} · ` : ''}${reviewSymbol(change)} ${change.title}${change.description ? ` · ${change.description}` : ''}`, meta: index === visibleTechnicalChanges.length - 1 && hiddenTechnicalChanges > 0
3976
4736
  ? `另有 ${hiddenTechnicalChanges} 项` : undefined, status: changeStatus(change), compact: true }, `${change.title}:${index}`);
3977
4737
  });
3978
- const showEquivalentCommand = Boolean(review.equivalentCommand && showTechnical);
3979
- 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: "! Shared impact \u9700\u8981\u663E\u5F0F\u786E\u8BA4", 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] }) });
4738
+ const showEquivalentCommand = Boolean(review.equivalentCommand && showTechnical && !review.requiresConfirmation);
4739
+ if (review.changes.length > staticRowBudget || showTechnical
4740
+ && (review.changes.length > technicalChangeBudget || technicalDetails.length > technicalDetailLimit)) {
4741
+ help = 'PgUp/PgDn 查看全部确认信息 · ↑↓ 移动 · Enter 选择 · Esc 返回';
4742
+ }
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] }) });
3980
4744
  }
3981
4745
  else if (route.name === 'progress') {
3982
4746
  const portableOperation = snapshot.activeReview?.operationKind === 'portableInstall';
@@ -3992,18 +4756,18 @@ export function ConsoleAppV4(props) {
3992
4756
  const catalogGenerationOperation = snapshot.activeReview?.operationKind === 'catalogGeneration';
3993
4757
  const catalogSourcesRecovery = snapshot.activeReview?.operationKind === 'catalogSourcesRecovery';
3994
4758
  title = portableOperation ? '正在安装通用 Skills' : issueOperation ? '正在处理问题'
3995
- : catalogGenerationOperation ? '正在验证并启用 Catalog'
3996
- : catalogSourcesRecovery ? '正在恢复 Catalog 来源配置'
4759
+ : catalogGenerationOperation ? '正在验证并启用目录版本'
4760
+ : catalogSourcesRecovery ? '正在恢复插件来源配置'
3997
4761
  : sourceOperation ? '正在保存来源配置'
3998
4762
  : configOperation ? configApply ? '正在保存并应用设置' : '正在保存设置'
3999
4763
  : '正在执行插件操作';
4000
- description = issueOperation ? '完成后会使用新的状态 generation 重新检查问题。'
4001
- : catalogGenerationOperation ? '正在验证并应用明确确认的 Catalog 操作。'
4764
+ description = issueOperation ? '完成后会重新读取状态并检查问题。'
4765
+ : catalogGenerationOperation ? '正在验证并应用已确认的目录版本操作。'
4002
4766
  : catalogSourcesRecovery ? '正在备份损坏配置并恢复官方默认来源。'
4003
4767
  : sourceOperation ? '只原子保存来源配置,不访问网络。'
4004
4768
  : configOperation ? configApply ? '先保存所选层,再重新应用当前可见安装位置。' : '只保存所选设置层。'
4005
4769
  : '请勿关闭当前终端。';
4006
- help = 'Ctrl-C 完成后退出';
4770
+ const batchProgress = snapshot.activeReview?.operationKind === 'pluginWriteBatch';
4007
4771
  const progress = snapshot.progress;
4008
4772
  const elapsedSeconds = Math.max(0, Math.floor(((progress.elapsedMs
4009
4773
  ?? (progress.startedAtMs === undefined ? 0 : Date.now() - progress.startedAtMs))) / 1_000));
@@ -4053,8 +4817,11 @@ export function ConsoleAppV4(props) {
4053
4817
  : 0;
4054
4818
  const progressTargetBudget = progressTargetAndMessageBudget - dependencyMessageLimit;
4055
4819
  const progressTargetLimit = prefixRowLimit(prioritizedProgressTargets.map(() => policy.mode === 'compact' ? 1 : 2), progressTargetBudget);
4056
- const visibleProgressTargets = prioritizedProgressTargets.slice(0, progressTargetLimit);
4057
- const hiddenProgressTargets = snapshot.progressTargets.length - visibleProgressTargets.length;
4820
+ help = prioritizedProgressTargets.length > progressTargetLimit
4821
+ ? 'PgUp/PgDn 查看全部目标 · Ctrl-C 完成后退出' : 'Ctrl-C 完成后退出';
4822
+ const progressTargetOffset = Math.min(batchTargetOffset, Math.max(0, prioritizedProgressTargets.length - 1));
4823
+ const visibleProgressTargets = prioritizedProgressTargets.slice(progressTargetOffset, progressTargetOffset + progressTargetLimit);
4824
+ const hiddenProgressTargets = Math.max(0, snapshot.progressTargets.length - progressTargetOffset - visibleProgressTargets.length);
4058
4825
  const targetStates = snapshot.progressTargets.map((entry) => entry.state);
4059
4826
  const executionComplete = targetStates.length > 0
4060
4827
  && targetStates.every((state) => state === 'completed' || state === 'kept');
@@ -4102,13 +4869,13 @@ export function ConsoleAppV4(props) {
4102
4869
  return step === 2 ? '正在安装' : step === 3 ? '正在验证' : step === 1 ? '正在生成' : '正在检查';
4103
4870
  };
4104
4871
  const compactPortableProgress = policy.mode === 'compact' && showTechnical;
4105
- 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} 个 provider 提供 ${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: [visibleProgressTargets.map((entry) => {
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) => {
4106
4873
  const state = progressLabel(entry);
4107
4874
  const relation = entry.relation === 'requested' ? '你选择的插件' : entry.relation === 'direct' ? '直接依赖' : '间接依赖';
4108
4875
  const title = props.controller.getPluginOverview(entry.pluginId)?.displayName
4109
4876
  ?? entry.pluginId.split('/').at(-1);
4110
4877
  return _jsx(ResultRow, { title: title, description: relation, meta: state.label, status: state.status ?? 'muted', compact: policy.mode === 'compact' }, `${entry.pluginId}:${entry.platform}:${entry.scope}`);
4111
- }), hiddenProgressTargets > 0 ? _jsx(StaticRow, { title: `另有 ${hiddenProgressTargets} 个目标`, status: "muted", compact: true }) : null] }) : null, !showTechnical && latestMessage && (!dependencyProgress || dependencyMessageLimit > 0) ? _jsx(StaticRow, { title: latestMessage, status: "muted", compact: true }) : null, showTechnical ? (dependencyProgress
4878
+ }), hiddenProgressTargets > 0 ? _jsx(StaticRow, { title: `${batchProgress ? '↓ 后面还有' : '另有'} ${hiddenProgressTargets} 个目标`, status: "muted", compact: true }) : null] }) : null, !showTechnical && latestMessage && (!dependencyProgress || dependencyMessageLimit > 0) ? _jsx(StaticRow, { title: latestMessage, status: "muted", compact: true }) : null, showTechnical ? (dependencyProgress
4112
4879
  ? dependencyMessageLimit > 0 ? technicalMessages.slice(-dependencyMessageLimit) : []
4113
4880
  : 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] });
4114
4881
  }
@@ -4122,6 +4889,7 @@ export function ConsoleAppV4(props) {
4122
4889
  : result.state === 'blocked' ? result.summary : undefined;
4123
4890
  const action = resultSummary?.action;
4124
4891
  const operationKind = 'operationKind' in result ? result.operationKind : undefined;
4892
+ const batchOperation = operationKind === 'pluginWriteBatch';
4125
4893
  const portableOperation = operationKind === 'portableInstall';
4126
4894
  const portableReceipt = result.state === 'succeeded' || result.state === 'partial'
4127
4895
  ? result.summary.portable : undefined;
@@ -4144,68 +4912,79 @@ export function ConsoleAppV4(props) {
4144
4912
  cleanup: '✓ 插件清理完成',
4145
4913
  sync: '✓ 插件同步完成',
4146
4914
  }[action ?? 'install'];
4147
- title = portableOperation
4148
- ? blocked ? '× 通用 Skills 安装被阻止'
4149
- : partial ? '! 通用 Skills 结果未完全完成' : '✓ 通用 Skills 已安装'
4150
- : blocked && result.writeMayHaveCompleted
4151
- ? '× 写入结果未确认'
4152
- : issueOperation
4153
- ? blocked || partial ? '× 问题仍未解决' : '✓ 问题已处理'
4154
- : sourceOperation
4155
- ? blocked ? '× 来源操作未完成'
4156
- : operationKind === 'catalogSourcesRecovery' ? '✓ Catalog 来源配置已恢复'
4157
- : resultSummary?.catalogAction === 'update'
4158
- ? resultSummary.catalogGeneration?.activeChanged
4159
- && resultSummary.catalogGeneration.outcome === 'activated'
4160
- ? '✓ Catalog 已更新' : '✓ Catalog 无需更新'
4161
- : resultSummary?.catalogAction === 'repair' ? '✓ Catalog 已修复'
4162
- : resultSummary?.catalogAction === 'replaceDamaged' ? '✓ 损坏的 Catalog 已替换'
4163
- : resultSummary?.catalogAction === 'recoverState' ? '✓ Catalog 状态已恢复'
4164
- : operationKind === 'catalogGeneration' ? '✓ Catalog 已启用'
4165
- : '✓ 来源配置已保存'
4166
- : configOperation
4167
- ? blocked
4168
- ? '× 设置未确认保存'
4169
- : partial ? resultSummary?.configApplySkippedReason === 'invalidConfig'
4170
- ? '! 设置已保存,但未应用'
4171
- : '! 设置已保存,但部分应用失败'
4172
- : resultSummary?.configApplied ? '✓ 设置已保存并应用' : '✓ 设置已保存'
4173
- : partial ? '! 插件操作只完成了一部分'
4174
- : blocked ? result.code === 'operationFailed' ? '× 插件操作没有完成' : '× 插件操作未完成'
4175
- : completedTitle;
4176
- description = portableOperation
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
4177
4948
  ? blocked
4178
- ? result.code === 'operationReviewExpired' || result.code === 'reviewTicketExpired'
4179
- ? 'Review 已过期,写入未开始。请返回修改并重新确认。'
4180
- : result.code === 'operationReviewStale'
4181
- ? '安装证据已变化,写入被阻止。请返回修改并重新确认。'
4182
- : result.writeMayHaveCompleted
4183
- ? '写入结果无法确认。请检查外部 Skills 状态后重试。'
4184
- : '安装在写入前被阻止。请返回修改后重试。'
4185
- : portableInstallFailed ? 'Skills CLI 安装失败,可能已有部分目标写入。可以修改 Agent 或使用原选择重试。' : partial ? '无法确认全部已选择 Skills。' : '所选 Skills 已通过安装验证。'
4186
- : blocked && result.writeMayHaveCompleted
4187
- ? result.message
4188
- : issueOperation
4189
- ? blocked
4190
- ? result.code === 'issueVerificationFailed'
4191
- ? '写入可能已完成,但当前无法确认。请重新检查状态。'
4192
- : result.code === 'issueStillPresent'
4193
- ? '写入已完成,但重新检查后原问题仍然存在。'
4194
- : '操作被安全阻止。可返回问题详情后重试。'
4195
- : '状态已重新加载;原问题已确认消失。'
4196
- : sourceOperation
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
4197
4968
  ? blocked
4198
- ? result.writeMayHaveCompleted
4199
- ? '来源配置可能已保存。请重新检查来源列表。'
4200
- : '来源操作未执行。草稿已保留,可返回修改。'
4201
- : partial ? resultSummary?.message ?? '来源配置已保存,但 Catalog 尚未启用。可立即重试。'
4202
- : resultSummary?.message ?? '来源配置已保存。'
4203
- : blocked ? result.message
4204
- : configOperation
4205
- ? partial
4206
- ? resultSummary?.message ?? '设置层已经保存;平台应用失败不会回滚设置。'
4207
- : '设置层已经更新。'
4208
- : '状态已刷新。';
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
+ : '状态已刷新。';
4209
4988
  const pluginId = resultSummary?.pluginId
4210
4989
  ?? ('pluginId' in result ? result.pluginId : undefined);
4211
4990
  const configResultPluginId = pluginId ?? ('returnRoute' in result
@@ -4254,7 +5033,8 @@ export function ConsoleAppV4(props) {
4254
5033
  : undefined;
4255
5034
  const retryAction = failedPluginId && failedTarget && failedTargetState
4256
5035
  ? selectLifecycleRetryAction({
4257
- operationKind: operationKind === 'catalogSourcesRecovery' ? undefined : operationKind,
5036
+ operationKind: operationKind === 'catalogSourcesRecovery' || operationKind === 'pluginWriteBatch'
5037
+ ? undefined : operationKind,
4258
5038
  originalAction: action,
4259
5039
  target: {
4260
5040
  pluginId: failedPluginId,
@@ -4274,131 +5054,146 @@ export function ConsoleAppV4(props) {
4274
5054
  && (candidate.platform ?? failedIssue.platform) === failedTarget?.platform
4275
5055
  && (candidate.scope ?? failedIssue.scope) === failedTarget?.scope)
4276
5056
  : undefined;
4277
- actions = portableOperation
4278
- ? blocked || partial ? [
4279
- { label: blocked || partial ? '返回修改' : portableOriginIsDetail ? '返回插件详情' : '返回插件预览', run: async () => {
4280
- if (!pluginId) {
4281
- leaveResult();
4282
- return;
4283
- }
4284
- if (!blocked && !partial) {
5057
+ actions = batchOperation
5058
+ ? [
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
+ },
5067
+ }] : []),
5068
+ { label: '返回我的插件', run: () => leaveResult() },
5069
+ { label: '查看本次记录', run: () => result.operationRecordId
5070
+ && go({ name: 'operationDiagnostic', recordId: result.operationRecordId }) },
5071
+ ]
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: () => {
4285
5107
  props.controller.discardPortableDraft();
4286
5108
  leaveResult();
4287
- return;
4288
- }
4289
- if (!await refreshPortableDraft(pluginId))
4290
- return;
4291
- if (portableInstallFailed && portableReceipt?.agentMode === 'explicit') {
4292
- props.controller.resetPortableFlowToPreview(pluginId, { name: 'portableAgentInput', pluginId }, true);
4293
- return;
4294
- }
4295
- props.controller.resetPortableFlowToPreview(pluginId, { name: 'portableSkillSelect', pluginId }, true);
4296
- } },
4297
- { label: blocked || partial ? '重试' : '再安装其他 Skills…', run: async () => {
4298
- if (!pluginId)
4299
- return;
4300
- if (blocked || partial) {
4301
- props.controller.resetPortableFlowToPreview(pluginId, { name: 'portableSkillSelect', pluginId }, true);
4302
- await beginPortableReview(pluginId, snapshot.portableDraft?.scope ?? 'project');
4303
- return;
4304
- }
4305
- props.controller.resetPortableFlowToPreview(pluginId);
4306
- await beginPortable(pluginId);
4307
- } },
4308
- { label: portableOriginIsDetail ? '返回插件详情' : '返回插件预览', run: () => { if (pluginId)
4309
- props.controller.resetPortableFlowToPreview(pluginId); } },
4310
- ] : [
4311
- { label: portableOriginIsDetail ? '返回插件详情' : '返回插件预览', run: () => {
4312
- props.controller.discardPortableDraft();
4313
- leaveResult();
4314
- } },
4315
- { label: '再安装其他 Skills…', run: async () => {
4316
- if (!pluginId)
4317
- return;
4318
- props.controller.resetPortableFlowToPreview(pluginId);
4319
- await beginPortable(pluginId);
4320
- } },
4321
- ]
4322
- : issueOperation && !dependencyRecovery
4323
- ? blocked || partial
4324
- ? [
4325
- { label: '返回问题详情', run: () => blocked
4326
- ? leaveResult()
4327
- : failedIssue
4328
- ? leaveResult({ name: 'issueDetail', issueId: failedIssue.issueId })
4329
- : pluginId && failedTarget
4330
- ? leaveResult({ name: 'platform', pluginId, platform: failedTarget.platform })
4331
- : leaveResult() },
4332
- { label: '重新检查问题', run: () => leaveResult({ name: 'issueList' }) },
4333
- ]
4334
- : [
4335
- { label: '查看问题列表', run: () => leaveResult({ name: 'issueList' }) },
4336
- { label: '返回首页', run: () => leaveResult({ name: 'home' }) },
4337
- ]
4338
- : sourceOperation
4339
- ? [{ label: blocked ? '返回修改' : partial ? '现在启用 / 重试' : '查看来源', run: () => blocked ? leaveResult() : leaveResult({ name: 'sourceList' }) }, { label: '返回首页', run: () => leaveResult({ name: 'home' }) }]
4340
- : configOperation && partial
5109
+ } },
5110
+ { label: '再安装其他 Skills…', run: async () => {
5111
+ if (!pluginId)
5112
+ return;
5113
+ props.controller.resetPortableFlowToPreview(pluginId);
5114
+ await beginPortable(pluginId);
5115
+ } },
5116
+ ]
5117
+ : issueOperation && !dependencyRecovery
5118
+ ? blocked || partial
4341
5119
  ? [
4342
- ...(configRetryAvailable ? [{
4343
- label: '重试应用',
4344
- description: '先重新进入 Review,并重新采集当前证据',
4345
- run: () => beginReview({
4346
- kind: 'configSave',
4347
- pluginId: configResultPluginId,
4348
- scope: resultSummary.configScope,
4349
- apply: true,
4350
- retryApply: true,
4351
- }),
4352
- }] : []),
4353
- { label: invalidConfigResult ? '返回设置' : '返回修改', description: '重新打开已保存的设置层', run: () => leaveResult() },
4354
- { label: '返回设置执行 unset / reset', description: '显式删除顶层字段或整个设置层', run: () => leaveResult() },
4355
- { label: '保留设置并退出', run: () => requestExit({ code: 1, reason: 'partial' }) },
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') },
4356
5132
  ]
4357
- : configOperation && blocked
5133
+ : sourceOperation
5134
+ ? [{ label: blocked ? '返回修改' : partial ? '现在启用 / 重试' : '查看来源', run: () => blocked ? props.controller.resumeResultDraft() : leaveResult() }, { label: '返回首页', run: () => requestSessionNavigation('home') }]
5135
+ : configOperation && partial
4358
5136
  ? [
4359
- { label: '返回设置', run: () => leaveResult() },
4360
- { label: '返回首页', run: () => leaveResult({ name: 'home' }) },
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' }) },
4361
5151
  ]
4362
- : [{ label: partial || blockedDependencyFailure ? '查看失败项' : successfulConfigOperation && configOrigin
4363
- ? configOrigin?.name === 'pluginPreview' ? '返回插件预览' : '返回插件详情'
4364
- : configOperation ? '查看插件'
4365
- : action === 'uninstall' || action === 'cleanup' ? '返回插件详情' : '查看插件', run: () => {
4366
- if (successfulConfigOperation && configOrigin) {
4367
- leaveResult();
4368
- return;
4369
- }
4370
- if ((partial || blockedDependencyFailure) && failedPluginId && failedTarget) {
4371
- if (failedIssue)
4372
- leaveResult({ name: 'issueDetail', issueId: failedIssue.issueId });
4373
- else if (failedPluginPlatforms?.includes(failedTarget.platform)) {
4374
- leaveResult({ name: 'platform', pluginId: failedPluginId, platform: failedTarget.platform });
5152
+ : configOperation && blocked
5153
+ ? [
5154
+ { label: '返回设置', run: () => props.controller.resumeResultDraft() },
5155
+ { label: '返回首页', run: () => requestSessionNavigation('home') },
5156
+ ]
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;
4375
5164
  }
4376
- else if (snapshot.viewModel?.plugins.some((entry) => entry.pluginId === failedPluginId)) {
4377
- leaveResult({ name: 'pluginDetail', pluginId: failedPluginId });
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 });
4378
5179
  }
4379
5180
  else
4380
5181
  leaveResult({ name: 'pluginList', mode: 'installed' });
4381
- }
4382
- else if (pluginId && snapshot.viewModel?.plugins.some((entry) => entry.pluginId === pluginId)) {
4383
- leaveResult({ name: 'pluginDetail', pluginId });
4384
- }
4385
- else
4386
- leaveResult({ name: 'pluginList', mode: 'installed' });
4387
- } }, ...((partial || blockedDependencyFailure) && failedPluginId && failedTarget && retryAction ? [{
4388
- label: '重试失败部分…',
4389
- run: () => {
4390
- const issueIntent = failedIssue && retryIssueAction
4391
- ? issueWriteIntent(failedIssue, retryIssueAction)
4392
- : undefined;
4393
- return beginReview(issueIntent ?? {
4394
- kind: 'pluginWrite',
4395
- action: retryAction,
4396
- pluginId: failedPluginId,
4397
- platform: failedTarget.platform,
4398
- scope: failedTarget.scope,
4399
- });
4400
- },
4401
- }] : []), { label: '返回首页', run: () => leaveResult({ name: 'home' }) }];
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') }];
4402
5197
  const visibleMessages = resultSummary?.messages?.slice(policy.mode === 'compact' ? -2 : -4) ?? [];
4403
5198
  const visibleChanges = resultSummary?.changes?.slice(0, policy.mode === 'compact' ? 2 : 4) ?? [];
4404
5199
  const resultTargetName = (target) => target.pluginId
@@ -4419,17 +5214,19 @@ export function ConsoleAppV4(props) {
4419
5214
  + visibleChanges.length
4420
5215
  + (partial && hasPreservedTarget && resultTargets.some((target) => target.state === 'skipped') ? 1 : 0);
4421
5216
  const resultTargetLimit = prefixRowLimit(prioritizedTargets.map(() => policy.mode === 'compact' ? 1 : 2), pageActionBudget() - 3 - resultActionViewport.limit - resultExtraRows);
4422
- const visibleResultTargets = prioritizedTargets.slice(0, resultTargetLimit);
4423
- const hiddenResultTargets = resultTargets.length - visibleResultTargets.length;
5217
+ const resultTargetOffset = Math.min(batchTargetOffset, Math.max(0, prioritizedTargets.length - 1));
5218
+ const visibleResultTargets = prioritizedTargets.slice(resultTargetOffset, resultTargetOffset + resultTargetLimit);
5219
+ const hiddenResultTargets = Math.max(0, resultTargets.length - resultTargetOffset - visibleResultTargets.length);
4424
5220
  const portableMissingSkills = new Set(portableReceipt?.missing.map((entry) => entry.skill) ?? []);
4425
5221
  const portableSelectedSkills = portableReceipt?.selectedSkills
4426
- ?? snapshot.portableDraft?.candidates.filter((candidate) => candidate.selected).map((candidate) => candidate.name)
5222
+ ?? snapshot.portableResultSkills
4427
5223
  ?? [];
4428
5224
  const prioritizedPortableSkills = [...portableSelectedSkills].sort((left, right) => Number(portableMissingSkills.has(right)) - Number(portableMissingSkills.has(left)));
4429
5225
  const portableTechnicalSummary = showTechnical && (partial || blocked);
4430
5226
  const portableSkillLimit = showTechnical ? 1 : responsiveBudget(2, 6);
4431
- const visiblePortableSkills = prioritizedPortableSkills.slice(0, portableSkillLimit);
4432
- const hiddenPortableSkills = prioritizedPortableSkills.length - visiblePortableSkills.length;
5227
+ const portableSkillOffset = Math.min(batchTargetOffset, Math.max(0, prioritizedPortableSkills.length - 1));
5228
+ const visiblePortableSkills = prioritizedPortableSkills.slice(portableSkillOffset, portableSkillOffset + portableSkillLimit);
5229
+ const hiddenPortableSkills = Math.max(0, prioritizedPortableSkills.length - portableSkillOffset - visiblePortableSkills.length);
4433
5230
  const portableRows = visiblePortableSkills.map((skill) => {
4434
5231
  const missing = portableMissingSkills.has(skill);
4435
5232
  const status = blocked || portableReceipt?.verificationUnknown ? 'warning'
@@ -4457,11 +5254,15 @@ export function ConsoleAppV4(props) {
4457
5254
  : resultSummary?.resetDisposition === 'quarantinedDirectory'
4458
5255
  ? `设置路径是目录;已隔离为 ${resultSummary.quarantineBasename ?? '安全隔离目录'}`
4459
5256
  : undefined;
4460
- content = portableOperation ? _jsxs(Box, { flexDirection: "column", children: [portableTechnicalSummary ? _jsxs(_Fragment, { children: [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: [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'
5257
+ if (batchOperation || resultTargets.length > resultTargetLimit
5258
+ || prioritizedPortableSkills.length > portableSkillLimit) {
5259
+ help = 'PgUp/PgDn 查看全部项目 · ↑↓ 移动 · Enter 选择 · Esc 返回';
5260
+ }
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'
4461
5262
  ? _jsx(StaticRow, { title: "\u5B89\u5168\u6062\u590D", meta: portableReceipt.recovery, status: "warning", compact: true })
4462
5263
  : _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
4463
5264
  ? _jsx(StaticRow, { title: "\u53E6\u6709\u6280\u672F\u5B57\u6BB5\u5DF2\u6298\u53E0", meta: "materialization digest \u00B7 elapsed", status: "muted", compact: true })
4464
- : _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: "materialization digest", meta: portableReceipt.materializationDigest, compact: true }), resultSummary?.elapsedMs !== undefined ? _jsx(StaticRow, { title: "elapsed", meta: `${resultSummary.elapsedMs} ms`, compact: true }) : null] })] }) : null, showTechnical && !portableReceipt ? _jsxs(TechnicalDetails, { children: [_jsx(StaticRow, { title: "failure code", meta: 'code' in result ? result.code : 'operationFailed', compact: true }), _jsx(StaticRow, { title: "\u5176\u4F59\u6280\u672F\u5B57\u6BB5", meta: "\u672C\u6B21\u5199\u5165\u6CA1\u6709\u751F\u6210\u5B89\u5168 receipt", status: "muted", compact: true })] }) : null] }) }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: resultActionViewport.offset, limit: resultActionViewport.limit })] }) : _jsxs(Box, { flexDirection: "column", children: [_jsxs(ResultCard, { status: resultStatus, title: blockedDependencyFailure ? '依赖安装没有完成' : blocked ? `${result.writeMayHaveCompleted ? '结果未确认' : '操作未完成'} · ${result.code}` : sourceOperation ? '来源配置写入已完成' : dependencyTargets ? partial ? '依赖安装没有全部完成' : '操作已完成' : resultSummary?.message ?? '状态已变化,请重新确认', children: [resetDispositionText ? _jsx(StaticRow, { title: resetDispositionText, status: "success", compact: true }) : null, visibleResultTargets.map((target) => { const state = operationTargetStateLabel(target.state); const relation = target.relation === 'requested' ? '你选择的插件' : target.relation === 'direct' ? '直接依赖' : target.relation === 'transitive' ? '间接依赖' : undefined; return _jsx(ResultRow, { title: resultTargetName(target), description: relation, meta: state.label, status: state.status ?? 'muted', compact: policy.mode === 'compact' }, `${target.pluginId ?? ''}:${target.platform}:${target.scope}`); }), hiddenResultTargets > 0 ? _jsx(StaticRow, { title: `另有 ${hiddenResultTargets} 个目标`, status: "muted", compact: true }) : null, partial && hasPreservedTarget && resultTargets.some((target) => target.state === 'skipped') ? _jsx(StaticRow, { title: "\u672A\u6267\u884C\u7684\u76EE\u6807\u6CA1\u6709\u5199\u5165\uFF1B\u5DF2\u5B8C\u6210\u6216\u5DF2\u4FDD\u7559\u7684\u76EE\u6807\u4FDD\u6301\u53EF\u7528\u3002", status: "muted", compact: true }) : null, configOperation && partial && !invalidConfigResult ? _jsx(Callout, { status: "warning", title: "\u6062\u590D\u6307\u5F15", description: "\u5982\u9700\u6E05\u7406\u6B8B\u7559\uFF0C\u8BF7\u5148\u663E\u5F0F\u8FD0\u884C cleanup\uFF1B\u7136\u540E\u8FD0\u884C reinstall \u6216 sync\u3002\u4E0D\u4F1A\u81EA\u52A8\u6E05\u7406\uFF0C\u4E5F\u6CA1\u6709 journal restore\u3002" }) : null, resultSummary?.elapsedMs !== undefined ? _jsx(StaticRow, { title: "\u5DF2\u7528\u65F6", meta: `${Math.floor(resultSummary.elapsedMs / 1_000)}s` }) : null, visibleMessages.map((message, index) => _jsx(StaticRow, { title: message, status: "muted", compact: true }, `${index}:${message}`))] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: resultActionViewport.offset, limit: resultActionViewport.limit })] });
5265
+ : _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: "materialization digest", meta: portableReceipt.materializationDigest, compact: true }), resultSummary?.elapsedMs !== undefined ? _jsx(StaticRow, { title: "elapsed", meta: `${resultSummary.elapsedMs} ms`, compact: true }) : null] })] }) : null, showTechnical && !portableReceipt ? _jsxs(TechnicalDetails, { children: [_jsx(StaticRow, { title: "failure code", meta: 'code' in result ? result.code : 'operationFailed', compact: true }), _jsx(StaticRow, { title: "\u5176\u4F59\u6280\u672F\u5B57\u6BB5", meta: "\u672C\u6B21\u5199\u5165\u6CA1\u6709\u751F\u6210\u5B89\u5168 receipt", status: "muted", compact: true })] }) : null] }) }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: resultActionViewport.offset, limit: resultActionViewport.limit })] }) : _jsxs(Box, { flexDirection: "column", children: [_jsxs(ResultCard, { status: resultStatus, title: blockedDependencyFailure ? '依赖安装没有完成' : blocked ? result.writeMayHaveCompleted ? '结果未确认' : '操作未完成' : sourceOperation ? '来源配置写入已完成' : dependencyTargets ? partial ? '依赖安装没有全部完成' : '操作已完成' : resultSummary?.message ?? '状态已变化,请重新确认', children: [resetDispositionText ? _jsx(StaticRow, { title: resetDispositionText, status: "success", compact: true }) : null, resultTargetOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${resultTargetOffset} 个目标`, status: "muted", compact: true }) : null, visibleResultTargets.map((target) => { const state = operationTargetStateLabel(target.state); const relation = target.relation === 'requested' ? '你选择的插件' : target.relation === 'direct' ? '直接依赖' : target.relation === 'transitive' ? '间接依赖' : undefined; return _jsx(ResultRow, { title: resultTargetName(target), description: relation, meta: state.label, status: state.status ?? 'muted', compact: policy.mode === 'compact' }, `${target.pluginId ?? ''}:${target.platform}:${target.scope}`); }), hiddenResultTargets > 0 ? _jsx(StaticRow, { title: "\u2193 \u540E\u9762\u8FD8\u6709", meta: `${hiddenResultTargets} 个目标`, status: "muted", compact: true }) : null, partial && hasPreservedTarget && resultTargets.some((target) => target.state === 'skipped') ? _jsx(StaticRow, { title: "\u672A\u6267\u884C\u7684\u76EE\u6807\u6CA1\u6709\u5199\u5165\uFF1B\u5DF2\u5B8C\u6210\u6216\u5DF2\u4FDD\u7559\u7684\u76EE\u6807\u4FDD\u6301\u53EF\u7528\u3002", status: "muted", compact: true }) : null, configOperation && partial && !invalidConfigResult ? _jsx(Callout, { status: "warning", title: "\u6062\u590D\u6307\u5F15", description: "\u5982\u9700\u6E05\u7406\u6B8B\u7559\uFF0C\u8BF7\u8FD4\u56DE\u63D2\u4EF6\u8BE6\u60C5\u9009\u62E9\u6E05\u7406\uFF1B\u7136\u540E\u9009\u62E9\u91CD\u65B0\u5B89\u88C5\u6216\u540C\u6B65\u3002\u7CFB\u7EDF\u4E0D\u4F1A\u81EA\u52A8\u5220\u9664\u73B0\u6709\u5185\u5BB9\u3002" }) : null, resultSummary?.elapsedMs !== undefined ? _jsx(StaticRow, { title: "\u5DF2\u7528\u65F6", meta: `${Math.floor(resultSummary.elapsedMs / 1_000)}s` }) : 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 })] });
4465
5266
  }
4466
5267
  else if (route.name === 'pluginAbout' && plugin) {
4467
5268
  breadcrumb = `${plugin.displayName} / 关于`;
@@ -4487,17 +5288,89 @@ export function ConsoleAppV4(props) {
4487
5288
  const capabilityRowCount = plugin.capabilityUnsupported ? 4 : 0;
4488
5289
  const componentRowBudget = Math.max(1, detailRowBudget - 2 - plugin.installedPlacements.length - capabilityRowCount);
4489
5290
  const componentLimit = prioritizedComponents.length > componentRowBudget
4490
- ? Math.max(1, componentRowBudget - 1)
5291
+ ? Math.max(1, componentRowBudget - 2)
4491
5292
  : componentRowBudget;
4492
- const visibleComponents = prioritizedComponents.slice(0, componentLimit);
4493
- const hiddenComponents = prioritizedComponents.length - visibleComponents.length;
4494
- content = _jsxs(Box, { flexDirection: "column", children: [_jsxs(TechnicalDetails, { children: [_jsx(StaticRow, { title: "canonical pluginId", meta: plugin.pluginId }), _jsx(StaticRow, { title: "\u6765\u6E90", meta: plugin.sourceId }), plugin.capabilityUnsupported ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: "failure code", meta: "awCapabilityUnsupported" }), _jsx(StaticRow, { title: "capability \u72B6\u6001", meta: "\u5F53\u524D AW CLI \u4E0D\u652F\u6301", status: "error" }), _jsx(StaticRow, { title: "\u7F3A\u5C11 capability", meta: plugin.missingCapabilities?.join('、') ?? 'Status 未提供具体 capability' }), _jsx(StaticRow, { title: "\u6062\u590D\u65B9\u5411", meta: plugin.capabilityRecovery ?? '升级 AW CLI 到支持所需 capability 的版本后重试。' })] }) : null, plugin.installedPlacements.map((placement) => _jsx(StaticRow, { title: `${platformLabel(placement.platform)} target`, meta: placementStatus(placement).value }, placement.platform)), visibleComponents.map((component) => _jsx(StaticRow, { title: `组件 · ${component.state === 'absent' ? '缺失' : component.state}${component.errorCode ? ` · ${component.errorCode}` : ''} · ${component.key} · ${platformLabel(component.platform)} · ${scopeLabel(component.scope)}` }, `${component.platform}:${component.scope}:${component.key}`)), hiddenComponents > 0 ? _jsx(StaticRow, { title: `另有 ${hiddenComponents} 个组件`, status: "muted", compact: true }) : null] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
5293
+ const componentOffset = Math.min(batchTargetOffset, Math.max(0, prioritizedComponents.length - 1));
5294
+ const visibleComponents = prioritizedComponents.slice(componentOffset, componentOffset + componentLimit);
5295
+ const hiddenComponents = Math.max(0, prioritizedComponents.length - componentOffset - visibleComponents.length);
5296
+ if (prioritizedComponents.length > componentLimit) {
5297
+ help = 'PgUp/PgDn 查看全部组件 · ↑↓ 移动 · Enter 选择 · Esc 返回';
5298
+ }
5299
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsxs(TechnicalDetails, { children: [_jsx(StaticRow, { title: "canonical pluginId", meta: plugin.pluginId }), _jsx(StaticRow, { title: "\u6765\u6E90", meta: plugin.sourceId }), plugin.capabilityUnsupported ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: "failure code", meta: "awCapabilityUnsupported" }), _jsx(StaticRow, { title: "capability \u72B6\u6001", meta: "\u5F53\u524D AW CLI \u4E0D\u652F\u6301", status: "error" }), _jsx(StaticRow, { title: "\u7F3A\u5C11 capability", meta: plugin.missingCapabilities?.join('、') ?? 'Status 未提供具体 capability' }), _jsx(StaticRow, { title: "\u6062\u590D\u65B9\u5411", meta: plugin.capabilityRecovery ?? '升级 AW CLI 到支持所需 capability 的版本后重试。' })] }) : null, plugin.installedPlacements.map((placement) => _jsx(StaticRow, { title: `${platformLabel(placement.platform)} target`, meta: placementStatus(placement).value }, placement.platform)), componentOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${componentOffset} 个组件`, status: "muted", compact: true }) : null, visibleComponents.map((component) => _jsx(StaticRow, { title: `组件 · ${component.state === 'absent' ? '缺失' : component.state}${component.errorCode ? ` · ${component.errorCode}` : ''} · ${component.key} · ${platformLabel(component.platform)} · ${scopeLabel(component.scope)}` }, `${component.platform}:${component.scope}:${component.key}`)), hiddenComponents > 0 ? _jsx(StaticRow, { title: `↓ 后面还有 ${hiddenComponents} 个组件`, status: "muted", compact: true }) : null] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
4495
5300
  }
4496
5301
  else {
4497
- breadcrumb = '后续阶段';
4498
- title = '此业务页将在后续阶段接入';
4499
- description = '当前阶段只实现插件发现、安装和维护页面。';
4500
- actions = [{ label: '返回上一页', run: back }];
5302
+ const missingPlugin = definition.entity === 'plugin' && plugin === undefined;
5303
+ const missingIssue = definition.entity === 'issue' && issue === undefined;
5304
+ const missingConfigField = definition.rendererFamily === 'config'
5305
+ && configRoute !== undefined
5306
+ && configRoute.name !== 'configFields'
5307
+ && configRoute.name !== 'configSave'
5308
+ && currentConfigPage !== undefined
5309
+ && activeField === undefined;
5310
+ let unavailable;
5311
+ switch (route.name) {
5312
+ case 'pluginPreview':
5313
+ case 'pluginDependencies':
5314
+ case 'installPlacement':
5315
+ case 'installTargets':
5316
+ case 'dependencyPlacement':
5317
+ case 'dependencyPlacementEditor':
5318
+ case 'pluginDetail':
5319
+ case 'platform':
5320
+ case 'pluginAbout':
5321
+ case 'pluginTechnical':
5322
+ case 'configLayer':
5323
+ unavailable = missingPlugin ? 'plugin' : 'flow';
5324
+ break;
5325
+ case 'portableSkillSelect':
5326
+ case 'portableDependencies':
5327
+ case 'portableAgentMode':
5328
+ case 'portableAgentInput':
5329
+ case 'portableScope':
5330
+ case 'portableBlocker':
5331
+ unavailable = missingPlugin ? 'plugin' : 'flow';
5332
+ break;
5333
+ case 'configFields':
5334
+ case 'configSave':
5335
+ case 'configFieldAction':
5336
+ case 'configFieldEditor':
5337
+ case 'configObjectPropertyEditor':
5338
+ unavailable = missingPlugin ? 'plugin' : missingConfigField ? 'config' : 'flow';
5339
+ break;
5340
+ case 'issueDetail':
5341
+ unavailable = missingIssue ? 'issue' : 'flow';
5342
+ break;
5343
+ case 'sourceAdd':
5344
+ case 'sourceSave':
5345
+ case 'scriptTimeout':
5346
+ case 'reviewPreparing':
5347
+ case 'review':
5348
+ case 'result':
5349
+ unavailable = 'flow';
5350
+ break;
5351
+ case 'home':
5352
+ throw new Error(`TUI renderer invariant failed for ${route.name}`);
5353
+ default:
5354
+ unreachableTuiRoute(route);
5355
+ }
5356
+ breadcrumb = unavailable === 'plugin' ? '插件不可用'
5357
+ : unavailable === 'issue' ? '问题不可用'
5358
+ : unavailable === 'config' ? '设置已变化' : '操作已变化';
5359
+ title = unavailable === 'plugin' ? '这个插件已不在当前目录中'
5360
+ : unavailable === 'issue' ? '这个问题已经变化'
5361
+ : unavailable === 'config' ? '这个设置字段已不再可用' : '这个操作已结束或变化';
5362
+ description = '状态已变化。请返回列表并重新选择。';
5363
+ actions = [{
5364
+ label: unavailable === 'plugin' ? '返回插件列表'
5365
+ : unavailable === 'issue' ? '返回问题列表'
5366
+ : unavailable === 'config' ? '返回设置列表' : '返回首页',
5367
+ run: () => unavailable === 'flow'
5368
+ ? requestSessionNavigation('home')
5369
+ : go(unavailable === 'plugin'
5370
+ ? { name: 'pluginList', mode: 'installed' }
5371
+ : unavailable === 'issue' ? { name: 'issueList' }
5372
+ : { name: 'configFields', pluginId: configRoute.pluginId, layer: configRoute.layer }),
5373
+ }];
4501
5374
  content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' });
4502
5375
  }
4503
5376
  if (canOpenHelp && supportsTechnicalDetails) {
@@ -4508,6 +5381,11 @@ export function ConsoleAppV4(props) {
4508
5381
  else if (canOpenHelp && !help.includes('? 帮助')) {
4509
5382
  help = `? 帮助 · ${help}`;
4510
5383
  }
5384
+ if (props.onSessionNavigation && !isCharacterInputMode(route, searchFocus)) {
5385
+ help = busy
5386
+ ? `h/H 完成后导航 · Esc 撤销等待 · ${help}`
5387
+ : `h 首页 · H 重载 · ${help}`;
5388
+ }
4511
5389
  const targetActionSignature = route.name === 'installTargets' && route.view !== 'blocked'
4512
5390
  ? actions.map((action) => String(Boolean(action.disabled))).join(',')
4513
5391
  : '';
@@ -4544,13 +5422,75 @@ export function ConsoleAppV4(props) {
4544
5422
  void pending.catch((error) => {
4545
5423
  if (error instanceof Error && error.name === 'AbortError')
4546
5424
  return;
4547
- setActionError('操作未完成。请重新检查后重试。');
5425
+ showSafeUiFailure(error, 'tuiActionFailed', 'ui-action');
4548
5426
  });
4549
5427
  }
4550
- catch {
4551
- setActionError('操作未完成。请重新检查后重试。');
5428
+ catch (error) {
5429
+ showSafeUiFailure(error, 'tuiActionFailed', 'ui-action');
4552
5430
  }
4553
5431
  };
5432
+ function requestSessionNavigation(navigation, discardDrafts = false) {
5433
+ if (!props.onSessionNavigation) {
5434
+ if (navigation !== 'home')
5435
+ return;
5436
+ if (!props.controller.goHome({ discardDrafts })) {
5437
+ setDiscardDraftConfirmation(props.controller.externalDrafts);
5438
+ setDiscardForSessionNavigation(navigation);
5439
+ setOverlayFocus(0);
5440
+ setOverlay('sessionNavigation');
5441
+ return;
5442
+ }
5443
+ resetRootUiState();
5444
+ if (discardDrafts)
5445
+ clearExternalDraftValues();
5446
+ return;
5447
+ }
5448
+ const draftConfirmation = discardDrafts ? discardDraftConfirmation : undefined;
5449
+ void props.onSessionNavigation(navigation, {
5450
+ discardDrafts,
5451
+ ...(draftConfirmation ? { draftConfirmation } : {}),
5452
+ }).then((result) => {
5453
+ if (result === 'draftBlocked') {
5454
+ setDiscardDraftConfirmation(props.controller.externalDrafts);
5455
+ setDiscardForSessionNavigation(navigation);
5456
+ setOverlayFocus(0);
5457
+ setOverlay('sessionNavigation');
5458
+ return;
5459
+ }
5460
+ if (result === 'completed') {
5461
+ if (navigation === 'home')
5462
+ resetRootUiState();
5463
+ if (discardDrafts)
5464
+ clearExternalDraftValues();
5465
+ setDiscardDraftConfirmation(undefined);
5466
+ }
5467
+ }).catch((error) => showSafeUiFailure(error, 'tuiSessionNavigationFailed', 'session-navigation'));
5468
+ }
5469
+ function clearExternalDraftValues() {
5470
+ setDirty(false);
5471
+ setDraftUpdates({});
5472
+ setDraftRemoves([]);
5473
+ setObjectDrafts({});
5474
+ setChangedObjectDraftKeys([]);
5475
+ setObjectError(undefined);
5476
+ setSourceDraft(undefined);
5477
+ setBatchDraft(undefined);
5478
+ }
5479
+ function continueEditingFromSessionNavigation() {
5480
+ if (props.pendingSessionNavigation)
5481
+ props.onCancelPendingSessionNavigation?.();
5482
+ setOverlay(undefined);
5483
+ setDiscardForSessionNavigation(undefined);
5484
+ setDiscardDraftConfirmation(undefined);
5485
+ }
5486
+ useEffect(() => {
5487
+ if (!props.pendingSessionNavigation || !props.pendingSessionNavigationDrafts)
5488
+ return;
5489
+ setDiscardForSessionNavigation(props.pendingSessionNavigation);
5490
+ setDiscardDraftConfirmation(props.pendingSessionNavigationDrafts);
5491
+ setOverlayFocus(0);
5492
+ setOverlay('sessionNavigation');
5493
+ }, [props.pendingSessionNavigation, props.pendingSessionNavigationDrafts]);
4554
5494
  useInput((input, key) => {
4555
5495
  if (!overlay && route.name === 'reviewPreparing' && snapshot.reviewPreparation?.state === 'failed'
4556
5496
  && (key.escape || input.toLowerCase() === 'b')) {
@@ -4558,7 +5498,11 @@ export function ConsoleAppV4(props) {
4558
5498
  return;
4559
5499
  }
4560
5500
  if (preparationRunning) {
4561
- if (key.escape || input.toLowerCase() === 'b') {
5501
+ const sessionNavigation = resolveTuiSessionNavigationKey({ input, key }, { characterInput: false });
5502
+ if (sessionNavigation) {
5503
+ requestSessionNavigation(sessionNavigation);
5504
+ }
5505
+ else if (key.escape || input.toLowerCase() === 'b') {
4562
5506
  back();
4563
5507
  }
4564
5508
  else if (input.toLowerCase() === 'q' || (key.ctrl && input.toLowerCase() === 'c')) {
@@ -4567,7 +5511,13 @@ export function ConsoleAppV4(props) {
4567
5511
  return;
4568
5512
  }
4569
5513
  if (busy) {
4570
- if (overlay === 'help') {
5514
+ const sessionNavigation = resolveTuiSessionNavigationKey({ input, key }, { characterInput: false });
5515
+ if ((key.pageUp || key.pageDown) && route.name === 'progress') {
5516
+ const total = snapshot.progressTargets.length;
5517
+ const pageRows = 1;
5518
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - 1), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5519
+ }
5520
+ else if (overlay === 'help') {
4571
5521
  if (key.escape || key.return || input.toLowerCase() === 'b' || input === '?') {
4572
5522
  setOverlay(undefined);
4573
5523
  }
@@ -4575,6 +5525,12 @@ export function ConsoleAppV4(props) {
4575
5525
  setExitAfterBusy(true);
4576
5526
  }
4577
5527
  }
5528
+ else if (key.escape && props.pendingSessionNavigation) {
5529
+ props.onCancelPendingSessionNavigation?.();
5530
+ }
5531
+ else if (sessionNavigation) {
5532
+ requestSessionNavigation(sessionNavigation);
5533
+ }
4578
5534
  else if (input.toLowerCase() === 'q' || (key.ctrl && input.toLowerCase() === 'c')) {
4579
5535
  setExitAfterBusy(true);
4580
5536
  }
@@ -4583,7 +5539,7 @@ export function ConsoleAppV4(props) {
4583
5539
  setOverlay('help');
4584
5540
  }
4585
5541
  else if (!tooSmall && supportsTechnicalDetails && input.toLowerCase() === 'd') {
4586
- setShowTechnical((value) => !value);
5542
+ toggleTechnical();
4587
5543
  }
4588
5544
  return;
4589
5545
  }
@@ -4598,25 +5554,49 @@ export function ConsoleAppV4(props) {
4598
5554
  requestExit({ code: snapshot.phase === 'error' ? 1 : 0, reason: snapshot.phase === 'error' ? 'initialError' : 'user' });
4599
5555
  return;
4600
5556
  }
4601
- if (key.escape || input.toLowerCase() === 'b' || input === '?') {
5557
+ if (overlay === 'help' && input.toLowerCase() === 'o') {
4602
5558
  setOverlay(undefined);
5559
+ go({ name: 'operationHistory' });
5560
+ return;
5561
+ }
5562
+ if (key.escape || input.toLowerCase() === 'b' || input === '?') {
5563
+ if (overlay === 'sessionNavigation') {
5564
+ continueEditingFromSessionNavigation();
5565
+ }
5566
+ else
5567
+ setOverlay(undefined);
4603
5568
  return;
4604
5569
  }
4605
5570
  if (key.leftArrow || key.rightArrow || key.upArrow || key.downArrow) {
4606
- if (overlay !== 'help')
4607
- setOverlayFocus((value) => value === 0 ? 1 : 0);
5571
+ const count = overlay === 'help' && props.onSessionNavigation ? 3 : overlay === 'help' ? 1 : 2;
5572
+ const direction = key.leftArrow || key.upArrow ? -1 : 1;
5573
+ setOverlayFocus((value) => moveIndex(count, value, direction));
4608
5574
  return;
4609
5575
  }
4610
5576
  if (key.return) {
4611
- if (overlay === 'help' || overlayFocus === 0)
5577
+ if (overlay === 'help') {
4612
5578
  setOverlay(undefined);
5579
+ if (props.onSessionNavigation && overlayFocus === 1)
5580
+ requestSessionNavigation('home');
5581
+ else if (props.onSessionNavigation && overlayFocus === 2)
5582
+ requestSessionNavigation('reload');
5583
+ }
5584
+ else if (overlayFocus === 0) {
5585
+ if (overlay === 'sessionNavigation')
5586
+ continueEditingFromSessionNavigation();
5587
+ else
5588
+ setOverlay(undefined);
5589
+ }
4613
5590
  else if (overlay === 'discard') {
4614
5591
  setOverlay(undefined);
4615
5592
  setDirty(false);
4616
5593
  setDraftUpdates({});
4617
5594
  setDraftRemoves([]);
5595
+ setObjectDrafts({});
5596
+ setChangedObjectDraftKeys([]);
4618
5597
  const discardedSourceDraft = sourceDraft;
4619
5598
  setSourceDraft(undefined);
5599
+ setBatchDraft(undefined);
4620
5600
  if (discardForExit) {
4621
5601
  requestExit({ code: 0, reason: 'user' });
4622
5602
  }
@@ -4629,6 +5609,12 @@ export function ConsoleAppV4(props) {
4629
5609
  back();
4630
5610
  }
4631
5611
  }
5612
+ else if (overlay === 'sessionNavigation' && discardForSessionNavigation) {
5613
+ const navigation = discardForSessionNavigation;
5614
+ setOverlay(undefined);
5615
+ setDiscardForSessionNavigation(undefined);
5616
+ requestSessionNavigation(navigation, true);
5617
+ }
4632
5618
  else
4633
5619
  requestExit({ code: snapshot.phase === 'error' ? 1 : 0, reason: snapshot.phase === 'error' ? 'initialError' : 'user' });
4634
5620
  }
@@ -4638,6 +5624,111 @@ export function ConsoleAppV4(props) {
4638
5624
  requestExit({ code: snapshot.phase === 'error' ? 1 : 0, reason: snapshot.phase === 'error' ? 'initialError' : 'user' });
4639
5625
  return;
4640
5626
  }
5627
+ if (route.name === 'operationReportPreview' && reportSaveMode) {
5628
+ if (key.escape) {
5629
+ setReportSaveMode(false);
5630
+ setReportSaveError(undefined);
5631
+ return;
5632
+ }
5633
+ if (key.return) {
5634
+ const savePath = reportSavePath.trim();
5635
+ if (!savePath) {
5636
+ setReportSaveError('请输入要保存到的本地文件路径。');
5637
+ return;
5638
+ }
5639
+ void props.controller.saveOperationReport(route.recordId, savePath).then(() => {
5640
+ setReportSaveMode(false);
5641
+ setReportSaveResult('报告已保存到你选择的位置。');
5642
+ setReportSaveError(undefined);
5643
+ }).catch(() => {
5644
+ setReportSaveMode(false);
5645
+ setReportSaveError('报告未保存。请检查保存位置后重试。');
5646
+ });
5647
+ return;
5648
+ }
5649
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl)) {
5650
+ setReportSavePath('');
5651
+ setReportSaveError(undefined);
5652
+ return;
5653
+ }
5654
+ if (key.backspace || key.delete)
5655
+ setReportSavePath(removeLastGrapheme);
5656
+ else if (input && !key.ctrl && !key.meta && input >= ' ')
5657
+ setReportSavePath((value) => value + input);
5658
+ setReportSaveError(undefined);
5659
+ return;
5660
+ }
5661
+ if ((key.pageUp || key.pageDown)
5662
+ && route.name === 'review'
5663
+ && snapshot.activeReview) {
5664
+ const total = Math.max(snapshot.activeReview.changes.length, showTechnical ? snapshot.activeReview.technicalDetails?.length ?? 0 : 0);
5665
+ const pageRows = 1;
5666
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - 1), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5667
+ return;
5668
+ }
5669
+ if ((key.pageUp || key.pageDown)
5670
+ && route.name === 'result'
5671
+ && snapshot.result) {
5672
+ const total = 'summary' in snapshot.result
5673
+ ? Math.max(snapshot.result.summary?.targets?.length ?? 0, snapshot.result.operationKind === 'portableInstall'
5674
+ ? snapshot.portableResultSkills?.length ?? 0
5675
+ : 0)
5676
+ : 0;
5677
+ const pageRows = 1;
5678
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - 1), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5679
+ return;
5680
+ }
5681
+ if ((key.pageUp || key.pageDown) && route.name === 'pluginTechnical' && plugin) {
5682
+ const total = plugin.technicalComponents.length;
5683
+ const pageRows = 1;
5684
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - 1), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5685
+ return;
5686
+ }
5687
+ if ((key.pageUp || key.pageDown) && route.name === 'issueDetail' && issue && showTechnical) {
5688
+ const total = diagnosticDetailRowCount(issue.diagnostic);
5689
+ const pageRows = responsiveBudget(5, 10);
5690
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - pageRows), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5691
+ return;
5692
+ }
5693
+ if ((key.pageUp || key.pageDown) && route.name === 'sourceTechnical') {
5694
+ const overview = props.controller.getSourceOverview(route.sourceId);
5695
+ const diagnostic = sourceDiagnostic(route.sourceId, overview.health?.state ?? overview.source?.state, overview.detail, props.controller.getCatalogGeneration(route.sourceId), props.controller.getCatalogFreshness(route.sourceId));
5696
+ const total = diagnostic ? diagnosticDetailRowCount(diagnostic) : 0;
5697
+ const pageRows = responsiveBudget(5, 10);
5698
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - pageRows), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5699
+ return;
5700
+ }
5701
+ if ((key.pageUp || key.pageDown) && route.name === 'sourceRemoveBlocked'
5702
+ && sourceDependenciesExpanded) {
5703
+ const total = sourceReferenceSummary?.dependencies.length ?? 0;
5704
+ const pageRows = Math.max(1, pageActionBudget(10));
5705
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - pageRows), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5706
+ return;
5707
+ }
5708
+ if ((key.pageUp || key.pageDown) && route.name === 'pluginDependencies' && plugin) {
5709
+ const overview = dependencyOverview(plugin);
5710
+ const dependents = route.view === 'removeBlocked' && route.platform && route.scope
5711
+ ? props.controller.getBlockingDependents(plugin.pluginId, route.platform, route.scope)
5712
+ : overview?.dependents ?? [];
5713
+ const total = route.view === 'removeBlocked'
5714
+ ? dependents.length
5715
+ : (overview?.blockers.length ?? 0) + (overview?.dependencies.length ?? 0) + dependents.length;
5716
+ const pageRows = 1;
5717
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - pageRows), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5718
+ return;
5719
+ }
5720
+ if ((route.name === 'operationDiagnostic' || route.name === 'operationReportPreview')
5721
+ && (key.pageUp || key.pageDown)) {
5722
+ const detail = route.name === 'operationDiagnostic'
5723
+ ? props.controller.getOperationDiagnostic(route.recordId) : undefined;
5724
+ const preview = route.name === 'operationReportPreview'
5725
+ ? props.controller.previewOperationReport(route.recordId) : undefined;
5726
+ const total = detail ? operationDiagnosticLineCount(detail)
5727
+ : preview ? operationReportLineCount(preview) : 0;
5728
+ const pageRows = policy.mode === 'compact' ? 2 : Math.max(1, pageActionBudget(5));
5729
+ setScrollOffset((value) => Math.min(Math.max(0, total - pageRows), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5730
+ return;
5731
+ }
4641
5732
  if (route.name === 'portableAgentInput') {
4642
5733
  if (key.escape) {
4643
5734
  back();
@@ -4656,6 +5747,11 @@ export function ConsoleAppV4(props) {
4656
5747
  }
4657
5748
  return;
4658
5749
  }
5750
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl)) {
5751
+ setPortableAgentInput('');
5752
+ setInputError(undefined);
5753
+ return;
5754
+ }
4659
5755
  if (key.backspace || key.delete)
4660
5756
  setPortableAgentInput(removeLastGrapheme);
4661
5757
  else if (input && !key.ctrl && !key.meta && input >= ' ')
@@ -4663,14 +5759,43 @@ export function ConsoleAppV4(props) {
4663
5759
  setInputError(undefined);
4664
5760
  return;
4665
5761
  }
5762
+ if (route.name === 'scriptTimeout') {
5763
+ if (key.escape) {
5764
+ const intent = pendingReviewIntentRef.current;
5765
+ back();
5766
+ if (intent)
5767
+ beginReview(intent);
5768
+ return;
5769
+ }
5770
+ const pendingIntent = pendingReviewIntentRef.current;
5771
+ if (key.return && pendingIntent) {
5772
+ try {
5773
+ const scriptTimeoutMs = parseScriptTimeoutMs(scriptTimeoutInput);
5774
+ const intent = { ...pendingIntent, scriptTimeoutMs };
5775
+ setScriptTimeoutError(undefined);
5776
+ back();
5777
+ beginReview(intent);
5778
+ }
5779
+ catch (error) {
5780
+ setScriptTimeoutError(error instanceof Error ? error.message : '脚本超时无效。');
5781
+ }
5782
+ return;
5783
+ }
5784
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl))
5785
+ setScriptTimeoutInput('');
5786
+ else if (key.backspace || key.delete)
5787
+ setScriptTimeoutInput(removeLastGrapheme);
5788
+ else if (input && !key.ctrl && !key.meta && input >= ' ')
5789
+ setScriptTimeoutInput((value) => value + input);
5790
+ setScriptTimeoutError(undefined);
5791
+ return;
5792
+ }
4666
5793
  if (route.name === 'sourceAdd'
4667
5794
  && route.step !== 'type'
4668
5795
  && route.step !== 'refPolicy'
4669
5796
  && sourceDraft?.owner === route.owner) {
4670
5797
  if (key.escape) {
4671
- setOverlayFocus(0);
4672
- setDiscardForExit(false);
4673
- setOverlay('discard');
5798
+ back();
4674
5799
  return;
4675
5800
  }
4676
5801
  if (key.return) {
@@ -4715,35 +5840,35 @@ export function ConsoleAppV4(props) {
4715
5840
  next.sourceId = sourceInput;
4716
5841
  else if (route.step === 'location')
4717
5842
  next.location = sourceInput;
4718
- else if (route.step === 'ref')
5843
+ else if (route.step === 'ref') {
5844
+ next.refPolicy = 'explicit';
4719
5845
  next.ref = sourceInput;
5846
+ }
4720
5847
  else
4721
5848
  next.subdir = sourceInput === '' ? '' : path.posix.normalize(sourceInput).replace(/^\.\//u, '').replace(/\/$/u, '');
4722
5849
  setSourceDraft(next);
5850
+ setDirty(true);
4723
5851
  setSourceInputError(undefined);
4724
- if (route.step === 'id')
5852
+ if (route.returnToDraft)
5853
+ acceptSourceFieldAndBack();
5854
+ else if (route.step === 'id')
4725
5855
  go({ name: 'sourceAdd', step: 'location', owner: route.owner });
4726
5856
  else if (route.step === 'location') {
4727
- if (next.mode === 'update')
4728
- void beginSourceUpdateReview(next).catch(() => setActionError('来源修改未通过验证。'));
4729
- else if (next.type === 'git')
5857
+ if (next.type === 'git')
4730
5858
  go({ name: 'sourceAdd', step: 'refPolicy', owner: route.owner });
4731
5859
  else
4732
- void beginSourceAddReviewWith(next).catch(() => setActionError('来源未通过验证。请检查输入后重试。'));
5860
+ go({ name: 'sourceEdit', sourceId: next.sourceId, owner: route.owner });
4733
5861
  }
4734
5862
  else if (route.step === 'ref') {
4735
- if (next.mode === 'update')
4736
- void beginSourceUpdateReview(next).catch(() => setActionError('来源修改未通过验证。'));
4737
- else
4738
- go({ name: 'sourceAdd', step: 'subdir', owner: route.owner });
5863
+ go({ name: 'sourceAdd', step: 'subdir', owner: route.owner });
4739
5864
  }
4740
- else if (next.mode === 'update')
4741
- void beginSourceUpdateReview(next).catch(() => setActionError('来源修改未通过验证。'));
4742
5865
  else
4743
- void beginSourceAddReviewWith(next).catch(() => setActionError('来源未通过验证。请检查输入后重试。'));
5866
+ go({ name: 'sourceEdit', sourceId: next.sourceId, owner: route.owner });
4744
5867
  return;
4745
5868
  }
4746
- if (key.backspace || key.delete)
5869
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl))
5870
+ setSourceInput('');
5871
+ else if (key.backspace || key.delete)
4747
5872
  setSourceInput(removeLastGrapheme);
4748
5873
  else if (input && !key.ctrl && !key.meta && input >= ' ')
4749
5874
  setSourceInput((value) => value + input);
@@ -4758,8 +5883,8 @@ export function ConsoleAppV4(props) {
4758
5883
  return;
4759
5884
  }
4760
5885
  if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl)) {
4761
- removeDraft(route.fieldKey);
4762
- back();
5886
+ setInputValue('');
5887
+ setInputError(undefined);
4763
5888
  return;
4764
5889
  }
4765
5890
  if (key.return && activeField) {
@@ -4780,7 +5905,9 @@ export function ConsoleAppV4(props) {
4780
5905
  back();
4781
5906
  return;
4782
5907
  }
4783
- if (key.backspace || key.delete)
5908
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl))
5909
+ setInputValue('');
5910
+ else if (key.backspace || key.delete)
4784
5911
  setInputValue(removeLastGrapheme);
4785
5912
  else if (input && !key.ctrl && !key.meta && input >= ' ')
4786
5913
  setInputValue((value) => value + input);
@@ -4795,6 +5922,11 @@ export function ConsoleAppV4(props) {
4795
5922
  back();
4796
5923
  return;
4797
5924
  }
5925
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl)) {
5926
+ setInputValue('');
5927
+ setInputError(undefined);
5928
+ return;
5929
+ }
4798
5930
  if (key.return && route.stage === 'key') {
4799
5931
  const propertyKey = inputValue;
4800
5932
  if (Object.hasOwn(activeObjectDraft, propertyKey)
@@ -4880,7 +6012,9 @@ export function ConsoleAppV4(props) {
4880
6012
  setScrollOffset(0);
4881
6013
  return;
4882
6014
  }
4883
- if (key.backspace || key.delete)
6015
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl))
6016
+ setQuery('');
6017
+ else if (key.backspace || key.delete)
4884
6018
  setQuery(removeLastGrapheme);
4885
6019
  else if (input && !key.ctrl && !key.meta && input >= ' ')
4886
6020
  setQuery((value) => value + input);
@@ -4894,26 +6028,50 @@ export function ConsoleAppV4(props) {
4894
6028
  || route.name === 'configObjectPropertyEditor')) {
4895
6029
  return;
4896
6030
  }
4897
- if (canOpenHelp && input === '?') {
6031
+ const sessionNavigation = resolveTuiSessionNavigationKey({ input, key }, { characterInput: isCharacterInputMode(route, searchFocus) });
6032
+ const globalAction = resolveTuiGlobalKeyAction({ input, key }, { mode: 'menu', allowBack: true });
6033
+ if (sessionNavigation) {
6034
+ requestSessionNavigation(sessionNavigation);
6035
+ }
6036
+ else if (route.name === 'sourceAdd' && key.escape && route.step !== 'type') {
6037
+ back();
6038
+ }
6039
+ else if (input.toLowerCase() === 'o'
6040
+ && (route.name === 'home'
6041
+ || route.name === 'issueList'
6042
+ || route.name === 'issueDetail'
6043
+ || route.name === 'result')) {
6044
+ go({ name: 'operationHistory' });
6045
+ }
6046
+ else if (canOpenHelp && globalAction === 'help') {
4898
6047
  setOverlayFocus(0);
4899
6048
  setOverlay('help');
4900
6049
  }
4901
- else if (input.toLowerCase() === 'q') {
6050
+ else if (globalAction === 'exit') {
4902
6051
  setOverlayFocus(0);
4903
6052
  setDiscardForExit(dirty);
4904
6053
  setOverlay(dirty ? 'discard' : 'exit');
4905
6054
  }
4906
6055
  else if (input.toLowerCase() === 'd' && supportsTechnicalDetails) {
4907
- setShowTechnical((value) => !value);
6056
+ toggleTechnical();
4908
6057
  }
4909
- else if ((key.escape || input.toLowerCase() === 'b')
6058
+ else if (globalAction === 'back'
4910
6059
  && (route.name === 'configFieldEditor'
4911
6060
  || route.name === 'configObjectPropertyEditor'
4912
6061
  || route.name === 'configFieldAction')) {
4913
6062
  back();
4914
6063
  }
4915
- else if (key.escape || input.toLowerCase() === 'b') {
4916
- if (dirty) {
6064
+ else if (globalAction === 'back' && route.name === 'batchPlacement') {
6065
+ back();
6066
+ }
6067
+ else if (globalAction === 'back'
6068
+ && (route.name === 'operationHistory'
6069
+ || route.name === 'operationDiagnostic'
6070
+ || route.name === 'operationReportPreview')) {
6071
+ back();
6072
+ }
6073
+ else if (globalAction === 'back') {
6074
+ if (dirty && route.name !== 'review') {
4917
6075
  setOverlayFocus(0);
4918
6076
  setDiscardForExit(false);
4919
6077
  setOverlay('discard');
@@ -4941,15 +6099,53 @@ export function ConsoleAppV4(props) {
4941
6099
  setFocus(retained >= 0 ? retained : 0);
4942
6100
  setScrollOffset(0);
4943
6101
  }
6102
+ else if (route.name === 'pluginList' && input.toLowerCase() === 'm') {
6103
+ openBatchSelection(route.mode === 'browse' ? 'install' : undefined);
6104
+ }
6105
+ 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' });
6115
+ }
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);
6120
+ setBatchDraft((current) => current ? {
6121
+ ...current,
6122
+ selectedPluginIds: current.selectedPluginIds.length === candidates.length
6123
+ ? [] : candidates.map((candidate) => candidate.pluginId),
6124
+ } : current);
6125
+ }
6126
+ else if (route.name === 'batchSelection' && batchDraft?.action && input.toLowerCase() === 'c') {
6127
+ if (batchDraft.selectedPluginIds.length > 0) {
6128
+ props.controller.push({ name: 'batchPlacement' });
6129
+ setFocus(0);
6130
+ setScrollOffset(0);
6131
+ }
6132
+ else
6133
+ setActionError('请至少选择一个插件。');
6134
+ }
6135
+ else if (route.name === 'batchSelection' && batchDraft?.action && input === ' ') {
6136
+ runAction(actions[effectiveFocus]);
6137
+ }
4944
6138
  else if (route.name === 'configFields' && route.layer.scope !== 'effective' && input.toLowerCase() === 's') {
4945
- if (dirty)
4946
- void beginConfigSave(false);
6139
+ if (dirty && configDraftIssues.length === 0) {
6140
+ setFocus(0);
6141
+ go({ name: 'configSave', pluginId: route.pluginId, layer: { scope: route.layer.scope } });
6142
+ }
4947
6143
  }
4948
6144
  else if (route.name === 'configFields' && route.layer.scope !== 'effective' && input.toLowerCase() === 'u') {
4949
6145
  const key = actions[focus]?.configKey;
4950
6146
  const field = currentConfigPage?.groups.flatMap((group) => group.fields)
4951
6147
  .find((candidate) => candidate.key === key);
4952
- if (field?.configured && (!field.invalidOverrideIssue || field.invalidOverrideOwnedByLayer)) {
6148
+ if (field && configFieldCanUnset(field) && (!field.invalidOverrideIssue || field.invalidOverrideOwnedByLayer)) {
4953
6149
  removeDraft(field.key);
4954
6150
  }
4955
6151
  }
@@ -4963,13 +6159,10 @@ export function ConsoleAppV4(props) {
4963
6159
  setScrollOffset(0);
4964
6160
  }
4965
6161
  else if (input.toLowerCase() === 'r' && route.name === 'sourceDetail') {
4966
- void props.controller.reloadCatalog({ sourceId: route.sourceId });
6162
+ void props.controller.recheckCatalogSource(route.sourceId);
4967
6163
  }
4968
6164
  else if (input.toLowerCase() === 'r' && route.name === 'sourceList') {
4969
- const source = props.controller.listSources()[focus];
4970
- if (source) {
4971
- void props.controller.reloadCatalog({ sourceId: source.id });
4972
- }
6165
+ void props.controller.recheckAllCatalogSources();
4973
6166
  }
4974
6167
  else if (route.name === 'pluginList' && (key.leftArrow || key.rightArrow || input === '[' || input === ']')) {
4975
6168
  const tabs = ['all', ...props.controller.listSources().map((source) => source.id)];
@@ -5011,7 +6204,7 @@ export function ConsoleAppV4(props) {
5011
6204
  const viewport = route.name === 'portableSkillSelect' && snapshot.portableDraft
5012
6205
  ? portableSkillCandidateViewport(actions.slice(0, snapshot.portableDraft.candidates.length), snapshot.portableDraft.candidates, policy.mode === 'compact', next, scrollOffset, pageActionBudget(actions.length - snapshot.portableDraft.candidates.length
5013
6206
  + (portableVariantCandidates.length > 0 ? 3 : 0)))
5014
- : actionViewport(actions, policy.mode === 'compact', next, scrollOffset, pageActionBudget(route.name === 'dependencyPlacement' ? dependencyPlacementStaticRows : 0));
6207
+ : actionViewport(actions, policy.mode === 'compact', next, scrollOffset, pageActionBudget(actionViewportStaticRows));
5015
6208
  setScrollOffset(viewport.offset);
5016
6209
  return next;
5017
6210
  });
@@ -5030,15 +6223,19 @@ export function ConsoleAppV4(props) {
5030
6223
  const viewport = route.name === 'portableSkillSelect' && snapshot.portableDraft
5031
6224
  ? portableSkillCandidateViewport(actions.slice(0, snapshot.portableDraft.candidates.length), snapshot.portableDraft.candidates, policy.mode === 'compact', next, scrollOffset, pageActionBudget(actions.length - snapshot.portableDraft.candidates.length
5032
6225
  + (portableVariantCandidates.length > 0 ? 3 : 0)))
5033
- : actionViewport(actions, policy.mode === 'compact', next, scrollOffset, pageActionBudget(route.name === 'dependencyPlacement' ? dependencyPlacementStaticRows : 0));
6226
+ : actionViewport(actions, policy.mode === 'compact', next, scrollOffset, pageActionBudget(actionViewportStaticRows));
5034
6227
  setScrollOffset(viewport.offset);
5035
6228
  return next;
5036
6229
  });
5037
6230
  }
5038
6231
  else if (key.return && route.name === 'pluginList' && pluginRows[focus]) {
5039
- go(route.mode === 'browse'
5040
- ? { name: 'pluginPreview', pluginId: pluginRows[focus].pluginId }
5041
- : { name: 'pluginDetail', pluginId: pluginRows[focus].pluginId });
6232
+ const row = pluginRows[focus];
6233
+ if (row.menuAction)
6234
+ row.menuAction();
6235
+ else
6236
+ go(route.mode === 'browse'
6237
+ ? { name: 'pluginPreview', pluginId: row.pluginId }
6238
+ : { name: 'pluginDetail', pluginId: row.pluginId });
5042
6239
  }
5043
6240
  else if (key.return) {
5044
6241
  runAction(actions[effectiveFocus]);
@@ -5052,15 +6249,26 @@ export function ConsoleAppV4(props) {
5052
6249
  const base = (_jsx(ScreenFrame, { title: title, description: description, breadcrumb: breadcrumb, context: context, help: help, size: size, children: content }));
5053
6250
  if (!overlay)
5054
6251
  return base;
5055
- const helpRows = helpRowsForRoute(route, actions.length, supportsTechnicalDetails, busy);
6252
+ const helpRows = helpRowsForRoute(route, actions.length, supportsTechnicalDetails, busy, props.onSessionNavigation !== undefined);
5056
6253
  const overlayActions = overlay === 'help'
5057
- ? [{ label: '关闭' }]
6254
+ ? [
6255
+ { label: '关闭' },
6256
+ ...(props.onSessionNavigation ? [
6257
+ { label: '返回首页' },
6258
+ { label: '返回首页并重新加载' },
6259
+ ] : []),
6260
+ ]
5058
6261
  : [
5059
- { label: overlay === 'discard' ? '继续编辑' : '继续使用' },
5060
- { label: overlay === 'discard' ? '放弃修改' : '退出', status: 'error' },
6262
+ { label: overlay === 'discard' || overlay === 'sessionNavigation' ? '继续编辑' : '继续使用' },
6263
+ { label: overlay === 'discard' || overlay === 'sessionNavigation' ? '放弃修改' : '退出', status: 'error' },
5061
6264
  ];
5062
- return (_jsxs(Box, { position: "relative", width: size.columns, height: size.rows, children: [_jsx(Box, { children: overlay === 'help' ? null : base }), _jsx(Box, { position: "absolute", marginTop: 5, marginLeft: Math.max(2, Math.floor(size.columns / 5)), width: Math.max(40, Math.floor(size.columns * 0.6)), flexDirection: "column", children: _jsxs(ReviewCard, { title: overlay === 'help' ? '当前页快捷键' : overlay === 'discard' ? '放弃未保存修改?' : '退出控制台?', children: [overlay === 'help'
6265
+ return (_jsxs(Box, { position: "relative", width: size.columns, height: size.rows, children: [_jsx(Box, { children: overlay === 'help' ? null : base }), _jsx(Box, { position: "absolute", marginTop: 5, marginLeft: Math.max(2, Math.floor(size.columns / 5)), width: Math.max(40, Math.floor(size.columns * 0.6)), flexDirection: "column", children: _jsxs(ReviewCard, { title: overlay === 'help' ? '当前页快捷键'
6266
+ : overlay === 'discard' || overlay === 'sessionNavigation' ? '放弃未保存修改?'
6267
+ : '退出控制台?', children: [overlay === 'help'
5063
6268
  ? _jsx(_Fragment, { children: helpRows.map(([key, value]) => _jsx(StaticRow, { title: `${key} ${value}`, compact: true }, key)) })
5064
- : _jsx(StaticRow, { title: overlay === 'discard' ? '放弃后将返回上一页。' : '进行中的写操作会先完成。', status: "muted" }), _jsx(SemanticRows, { actions: overlayActions, focus: overlayFocus, kind: "action", compact: true })] }) })] }));
6269
+ : _jsx(StaticRow, { title: overlay === 'discard' ? '放弃后将返回上一页。'
6270
+ : overlay === 'sessionNavigation'
6271
+ ? `放弃后将${discardForSessionNavigation === 'reload' ? '重新读取本地状态' : '返回首页'}。`
6272
+ : '进行中的写操作会先完成。', status: "muted" }), _jsx(SemanticRows, { actions: overlayActions, focus: overlayFocus, kind: "action", compact: true })] }) })] }));
5065
6273
  }
5066
6274
  //# sourceMappingURL=console-app-v4.js.map