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

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 (218) 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 +165 -45
  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 +11 -19
  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 +7 -0
  47. package/dist/src/commands/rebuild-command.js +769 -279
  48. package/dist/src/commands/rebuild-command.js.map +1 -1
  49. package/dist/src/commands/rebuild-integration.d.ts +24 -47
  50. package/dist/src/commands/rebuild-integration.js +1074 -1011
  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 +216 -85
  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 +6 -6
  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 +2067 -839
  85. package/dist/src/tui/console-app-v4.js.map +1 -1
  86. package/dist/src/tui/controller-runtime.js +585 -33
  87. package/dist/src/tui/controller-runtime.js.map +1 -1
  88. package/dist/src/tui/controller.d.ts +152 -6
  89. package/dist/src/tui/controller.js +1168 -156
  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 +6 -1
  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 +306 -86
  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-renderer.d.ts +2 -1
  138. package/dist/src/utils/artifact-renderer.js +52 -52
  139. package/dist/src/utils/artifact-renderer.js.map +1 -1
  140. package/dist/src/utils/bundle-layout.js +4 -4
  141. package/dist/src/utils/bundle-layout.js.map +1 -1
  142. package/dist/src/utils/catalog-capability-projection.d.ts +8 -0
  143. package/dist/src/utils/catalog-capability-projection.js +14 -0
  144. package/dist/src/utils/catalog-capability-projection.js.map +1 -0
  145. package/dist/src/utils/catalog-generation-resolver.d.ts +14 -2
  146. package/dist/src/utils/catalog-generation-resolver.js +49 -27
  147. package/dist/src/utils/catalog-generation-resolver.js.map +1 -1
  148. package/dist/src/utils/catalog-generation-service.d.ts +8 -1
  149. package/dist/src/utils/catalog-generation-service.js +81 -47
  150. package/dist/src/utils/catalog-generation-service.js.map +1 -1
  151. package/dist/src/utils/catalog-generation-store.d.ts +1 -0
  152. package/dist/src/utils/catalog-generation-store.js +60 -5
  153. package/dist/src/utils/catalog-generation-store.js.map +1 -1
  154. package/dist/src/utils/catalog-operation-context.js +1 -0
  155. package/dist/src/utils/catalog-operation-context.js.map +1 -1
  156. package/dist/src/utils/catalog-public-error.js +4 -0
  157. package/dist/src/utils/catalog-public-error.js.map +1 -1
  158. package/dist/src/utils/catalog-source-impact.d.ts +3 -2
  159. package/dist/src/utils/catalog-source-impact.js +142 -20
  160. package/dist/src/utils/catalog-source-impact.js.map +1 -1
  161. package/dist/src/utils/catalog-source-references.js +95 -9
  162. package/dist/src/utils/catalog-source-references.js.map +1 -1
  163. package/dist/src/utils/catalog-sources.d.ts +1 -1
  164. package/dist/src/utils/catalog-sources.js +19 -3
  165. package/dist/src/utils/catalog-sources.js.map +1 -1
  166. package/dist/src/utils/codex-manager.js +8 -0
  167. package/dist/src/utils/codex-manager.js.map +1 -1
  168. package/dist/src/utils/codex-native-observation.d.ts +34 -1
  169. package/dist/src/utils/codex-native-observation.js +132 -0
  170. package/dist/src/utils/codex-native-observation.js.map +1 -1
  171. package/dist/src/utils/codex-reconciler.d.ts +0 -2
  172. package/dist/src/utils/codex-reconciler.js.map +1 -1
  173. package/dist/src/utils/codex-shared-native.d.ts +2 -26
  174. package/dist/src/utils/codex-shared-native.js +8 -82
  175. package/dist/src/utils/codex-shared-native.js.map +1 -1
  176. package/dist/src/utils/git-catalog-provider.d.ts +3 -0
  177. package/dist/src/utils/git-catalog-provider.js +40 -1
  178. package/dist/src/utils/git-catalog-provider.js.map +1 -1
  179. package/dist/src/utils/local-marketplace.d.ts +1 -0
  180. package/dist/src/utils/local-marketplace.js +69 -73
  181. package/dist/src/utils/local-marketplace.js.map +1 -1
  182. package/dist/src/utils/multi-source-catalog.js +1 -0
  183. package/dist/src/utils/multi-source-catalog.js.map +1 -1
  184. package/dist/src/utils/native-cli.d.ts +9 -4
  185. package/dist/src/utils/native-cli.js +280 -34
  186. package/dist/src/utils/native-cli.js.map +1 -1
  187. package/dist/src/utils/path-catalog-provider.js +2 -0
  188. package/dist/src/utils/path-catalog-provider.js.map +1 -1
  189. package/dist/src/utils/plugin-config-write.d.ts +15 -13
  190. package/dist/src/utils/plugin-config-write.js +418 -198
  191. package/dist/src/utils/plugin-config-write.js.map +1 -1
  192. package/dist/src/utils/plugin-config.d.ts +48 -4
  193. package/dist/src/utils/plugin-config.js +289 -38
  194. package/dist/src/utils/plugin-config.js.map +1 -1
  195. package/dist/src/utils/plugin-scripts.d.ts +20 -12
  196. package/dist/src/utils/plugin-scripts.js +376 -109
  197. package/dist/src/utils/plugin-scripts.js.map +1 -1
  198. package/dist/src/utils/process-tree.d.ts +2 -2
  199. package/dist/src/utils/process-tree.js +1 -1
  200. package/dist/src/utils/process-tree.js.map +1 -1
  201. package/dist/src/utils/project-boundary.d.ts +11 -1
  202. package/dist/src/utils/project-boundary.js +17 -8
  203. package/dist/src/utils/project-boundary.js.map +1 -1
  204. package/dist/src/utils/safe-operation-diagnostic.d.ts +15 -0
  205. package/dist/src/utils/safe-operation-diagnostic.js +33 -0
  206. package/dist/src/utils/safe-operation-diagnostic.js.map +1 -0
  207. package/dist/src/utils/sensitive-content.d.ts +1 -0
  208. package/dist/src/utils/sensitive-content.js +15 -0
  209. package/dist/src/utils/sensitive-content.js.map +1 -0
  210. package/dist/src/utils/windows-claude-shim.d.ts +9 -0
  211. package/dist/src/utils/windows-claude-shim.js +141 -0
  212. package/dist/src/utils/windows-claude-shim.js.map +1 -0
  213. package/dist/vitest.config.js +91 -10
  214. package/dist/vitest.config.js.map +1 -1
  215. package/package.json +9 -9
  216. package/dist/authoring/templates/aw-config-runtime.d.mts +0 -2
  217. package/dist/authoring/templates/aw-config-runtime.mjs +0 -407
  218. 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,31 +2824,21 @@ 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 {
@@ -2379,7 +2867,7 @@ export function ConsoleAppV4(props) {
2379
2867
  go({ name: 'dependencyPlacement', pluginId: plugin.pluginId });
2380
2868
  },
2381
2869
  }] : []),
2382
- { label: '技术详情', description: '显示 canonical pluginId 和安全错误代码', run: () => setShowTechnical((value) => !value) },
2870
+ { label: '技术详情', description: '显示 canonical pluginId 和安全错误代码', run: () => toggleTechnical() },
2383
2871
  ];
2384
2872
  const rootStatus = plugin.aggregateStatus === 'installed' ? 'success'
2385
2873
  : plugin.aggregateStatus === 'needsAttention' ? 'warning'
@@ -2396,16 +2884,6 @@ export function ConsoleAppV4(props) {
2396
2884
  ];
2397
2885
  const technicalLineBudget = showTechnical ? 4 : 0;
2398
2886
  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
2887
  const prioritizedRelations = (entries) => [...entries].sort((left, right) => {
2410
2888
  const priority = (entry) => {
2411
2889
  const status = dependencyStatus(entry).status;
@@ -2413,20 +2891,28 @@ export function ConsoleAppV4(props) {
2413
2891
  };
2414
2892
  return priority(left) - priority(right);
2415
2893
  });
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) => {
2894
+ const allRelationRows = [
2895
+ ...(overview?.blockers ?? []).map((blocker) => _jsx(StaticRow, { title: `阻塞 · ${blocker.title}`, description: blocker.explanation, status: "error", compact: true }, `blocker:${blocker.code}`)),
2896
+ ...prioritizedRelations(overview?.dependencies ?? []).map((entry) => {
2420
2897
  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] });
2427
- }
2428
- }
2429
- else if (route.name === 'installPlacement' && plugin) {
2898
+ return _jsx(StaticRow, { title: `必需依赖 · ${entry.displayName}`, description: dependencyRelationDescription(entry), meta: dependencyPlacementLabel(entry), status: state.status, compact: true }, `dependency:${entry.pluginId}`);
2899
+ }),
2900
+ ...prioritizedRelations(visibleDependents).map((entry) => {
2901
+ const state = dependencyStatus(entry);
2902
+ return _jsx(StaticRow, { title: `被依赖 · ${entry.displayName}`, description: dependencyRelationDescription(entry), meta: dependentTargetLabel(entry), status: state.status, compact: true }, `dependent:${entry.pluginId}`);
2903
+ }),
2904
+ ];
2905
+ const relationLimit = Math.max(1, Math.min(relationItemBudget, showTechnical ? responsiveBudget(1, 6) : responsiveBudget(3, 7)));
2906
+ const relationOffset = Math.min(batchTargetOffset, Math.max(0, allRelationRows.length - 1));
2907
+ const visibleRelationRows = allRelationRows.slice(relationOffset, relationOffset + relationLimit);
2908
+ const hiddenRelationRows = Math.max(0, allRelationRows.length - relationOffset - visibleRelationRows.length);
2909
+ if (useBudgetedRelations && allRelationRows.length > relationLimit) {
2910
+ help = 'PgUp/PgDn 查看全部依赖关系 · ↑↓ 移动 · Enter 选择 · Esc 返回';
2911
+ }
2912
+ 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] });
2913
+ }
2914
+ }
2915
+ else if (route.name === 'installPlacement' && plugin) {
2430
2916
  breadcrumb = `${plugin.displayName} / 添加`;
2431
2917
  context = '第 1 步,共 2 步';
2432
2918
  title = `你希望在哪里使用 ${plugin.displayName}?`;
@@ -2474,7 +2960,7 @@ export function ConsoleAppV4(props) {
2474
2960
  const hasDependencies = (plugin.dependencySummary?.totalCount ?? 0) > 0;
2475
2961
  description = hasDependencies
2476
2962
  ? '根插件必须支持所选位置;每个依赖必须至少有一个合法位置。'
2477
- : '目标平台按所选位置验证;仅含 Codex native 的插件可同时启用两个位置。';
2963
+ : '目标平台按所选位置验证;仅含平台共享内容的插件可同时启用两个位置。';
2478
2964
  const choices = props.controller.listInstallTargets(plugin.pluginId, scope);
2479
2965
  const validSelectedTargets = selectedTargets.filter((platform) => choices.some((choice) => choice.platform === platform && choice.compatible));
2480
2966
  const allUnavailable = choices.length > 0 && choices.every((choice) => !choice.compatible);
@@ -2543,7 +3029,7 @@ export function ConsoleAppV4(props) {
2543
3029
  const dependencyActions = (draft?.targets ?? []).map((target) => ({
2544
3030
  label: `${target.displayName} · ${platformLabel(target.platform)}`,
2545
3031
  description: `${target.relation === 'direct' ? '直接依赖' : '间接依赖'} · ${target.explanation}`,
2546
- meta: `${scopeLabel(target.scope)}${target.assignmentSource === 'consumerPromotion' ? ' · fixed-point 提升' : ''}`,
3032
+ meta: `${scopeLabel(target.scope)}${target.assignmentSource === 'consumerPromotion' ? ' · 因依赖自动调整' : ''}`,
2547
3033
  selected: true,
2548
3034
  disabled: target.locked ? '位置由平台或上游约束固定' : undefined,
2549
3035
  run: () => go({
@@ -2563,7 +3049,7 @@ export function ConsoleAppV4(props) {
2563
3049
  },
2564
3050
  {
2565
3051
  label: '恢复推荐位置',
2566
- description: '恢复默认、健康 user 复用和 fixed-point 计算结果',
3052
+ description: '恢复依赖自动安排的建议位置',
2567
3053
  run: () => props.controller.resetDependencyScopes(),
2568
3054
  },
2569
3055
  { label: '返回目标平台', run: back },
@@ -2576,7 +3062,7 @@ export function ConsoleAppV4(props) {
2576
3062
  const reasonSummary = [
2577
3063
  ...(defaultCount > 0 ? [`默认 project ${defaultCount} 项`] : []),
2578
3064
  ...(reusedCount > 0 ? [`复用健康的所有项目安装 ${reusedCount} 项`] : []),
2579
- ...(promotedCount > 0 ? [`fixed-point 提升 ${promotedCount} 项`] : []),
3065
+ ...(promotedCount > 0 ? [`因依赖自动调整 ${promotedCount} 项`] : []),
2580
3066
  ].join(';');
2581
3067
  content = _jsxs(Box, { flexDirection: "column", children: [policy.mode === 'compact'
2582
3068
  ? _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 +3082,7 @@ export function ConsoleAppV4(props) {
2596
3082
  selected: target?.scope === candidateScope,
2597
3083
  description: candidateScope === 'user'
2598
3084
  ? '更宽:对其他项目可见,并会改变你的用户环境'
2599
- : '仅供当前项目使用;适合项目共享 intent',
3085
+ : '仅供当前项目使用;适合随项目共享',
2600
3086
  disabled: unavailable ? '这个依赖或上游约束不允许此位置' : undefined,
2601
3087
  run: async () => {
2602
3088
  await props.controller.setDependencyScope(route.dependencyPluginId, route.platform, candidateScope);
@@ -2605,7 +3091,7 @@ export function ConsoleAppV4(props) {
2605
3091
  };
2606
3092
  });
2607
3093
  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)}。` })
3094
+ ? _jsx(Callout, { status: "warning", title: "\u4F9D\u8D56\u9700\u8981\u66F4\u5927\u4F7F\u7528\u8303\u56F4", description: `上游依赖需要至少使用${scopeLabel(target.minimumScope)}。` })
2609
3095
  : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "choice", compact: policy.mode === 'compact' })] });
2610
3096
  help = '↑↓ 移动 · Enter 选择 · Esc 返回';
2611
3097
  }
@@ -2616,7 +3102,30 @@ export function ConsoleAppV4(props) {
2616
3102
  description = plugin.description;
2617
3103
  const dependencySummary = plugin.dependencySummary;
2618
3104
  const detailMenu = dynamicMenu;
2619
- actions = detailMenu.map((action) => {
3105
+ actions = plugin.configOnly ? detailMenu.map((action) => {
3106
+ if (action.id === 'settings')
3107
+ return {
3108
+ label: '查看已保存设置',
3109
+ description: '来源恢复前可检查问题,并只对记录中的设置层执行重置。',
3110
+ run: () => {
3111
+ props.controller.beginSavedConfigRecovery(plugin.pluginId);
3112
+ setFocus(0);
3113
+ setScrollOffset(0);
3114
+ },
3115
+ };
3116
+ if (action.id === 'sourceProblem')
3117
+ return {
3118
+ label: '查看来源问题',
3119
+ description: '恢复来源后可重新读取字段定义;不会把此记录当作可安装插件。',
3120
+ status: 'warning',
3121
+ run: () => go({ name: 'sourceDetail', sourceId: plugin.sourceId }),
3122
+ };
3123
+ return {
3124
+ label: '关于与技术详情',
3125
+ description: '查看保存设置和来源的安全摘要',
3126
+ run: () => go({ name: 'pluginAbout', pluginId: plugin.pluginId }),
3127
+ };
3128
+ }) : detailMenu.map((action) => {
2620
3129
  if (action.id === 'resolveScopeConflict')
2621
3130
  return {
2622
3131
  label: `保留${scopeLabel(action.keepScope)},移除${scopeLabel(action.scope)} root…`,
@@ -2657,14 +3166,14 @@ export function ConsoleAppV4(props) {
2657
3166
  if (action.id === 'upgradeAwCli')
2658
3167
  return {
2659
3168
  label: '升级 AW CLI',
2660
- description: plugin.capabilityRecovery ?? '升级到支持所需 capability 的 AW CLI 版本后重试',
3169
+ description: '升级到支持此插件的 AW CLI 版本后重试',
2661
3170
  status: 'warning',
2662
3171
  run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
2663
3172
  };
2664
3173
  if (action.id === 'capabilityTechnical')
2665
3174
  return {
2666
- label: '查看 capability 技术详情',
2667
- description: '查看缺少的 capability 和安全恢复方向',
3175
+ label: '查看功能支持详情',
3176
+ description: '查看缺少的功能和安全恢复方向',
2668
3177
  run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
2669
3178
  };
2670
3179
  if (action.id === 'sourceProblem')
@@ -2696,14 +3205,14 @@ export function ConsoleAppV4(props) {
2696
3205
  if (action.id === 'upgradeAwCli')
2697
3206
  return {
2698
3207
  label: '升级 AW CLI',
2699
- description: plugin.capabilityRecovery ?? '升级到支持所需 capability 的 AW CLI 版本后重试',
3208
+ description: '升级到支持此插件的 AW CLI 版本后重试',
2700
3209
  status: 'warning',
2701
3210
  run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
2702
3211
  };
2703
3212
  if (action.id === 'capabilityTechnical')
2704
3213
  return {
2705
- label: '查看 capability 技术详情',
2706
- description: '查看缺少的 capability 和安全恢复方向',
3214
+ label: '查看功能支持详情',
3215
+ description: '查看缺少的功能和安全恢复方向',
2707
3216
  run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
2708
3217
  };
2709
3218
  if (action.id === 'sourceProblem')
@@ -2725,11 +3234,11 @@ export function ConsoleAppV4(props) {
2725
3234
  if (action.id === 'addScope')
2726
3235
  return {
2727
3236
  label: `在${scopeLabel(action.scope)}中启用…`,
2728
- description: '只增加该 scope 的 activation;shared native 保持复用',
3237
+ description: '增加这个使用位置;现有共享内容保持不变',
2729
3238
  run: () => beginSecondScope(plugin, action.platform, action.scope),
2730
3239
  };
2731
3240
  if (action.id === 'about')
2732
- return { label: '查看支持信息', description: 'manifest 与平台能力', run: () => go({ name: 'pluginAbout', pluginId: plugin.pluginId }) };
3241
+ return { label: '查看支持信息', description: '插件定义与平台支持', run: () => go({ name: 'pluginAbout', pluginId: plugin.pluginId }) };
2733
3242
  if (action.id === 'source')
2734
3243
  return { label: '查看来源', run: () => go({ name: 'sourceDetail', sourceId: plugin.sourceId }) };
2735
3244
  if (action.id === 'outputCollision')
@@ -2751,10 +3260,10 @@ export function ConsoleAppV4(props) {
2751
3260
  run: () => beginScopeConflictRecovery(action),
2752
3261
  };
2753
3262
  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 }) };
3263
+ return { label: `刷新并同步到 ${action.availableVersion}…`, description: '先刷新相关来源,再确认新的目录内容', run: () => beginReview({ kind: 'pluginWrite', action: 'refreshAndSync', pluginId: plugin.pluginId, platform: action.platform, scope: action.scope }) };
2755
3264
  if (action.id === 'sync')
2756
3265
  return {
2757
- label: '同步 desired intent…',
3266
+ label: '同步已保存的使用位置…',
2758
3267
  description: placement?.state === 'installed'
2759
3268
  ? placement.recovery?.find((entry) => entry.command === 'sync')?.description
2760
3269
  ?? '进入现有 Review、进度和结果链修复 needsSync 或 drift'
@@ -2768,15 +3277,55 @@ export function ConsoleAppV4(props) {
2768
3277
  if (action.id === 'remove')
2769
3278
  return { label: `从${scopeLabel(action.scope)}移除…`, description: '保留插件设置', status: 'error', run: () => beginPluginRemoval(plugin, action.platform, action.scope) };
2770
3279
  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 }) };
3280
+ 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
3281
  if (action.id === 'technical')
2773
3282
  return typedIssue
2774
3283
  ? { label: '查看对应问题', description: typedIssue.explanation, run: () => go({ name: 'issueDetail', issueId: typedIssue.issueId }) }
2775
3284
  : { label: placement?.state === 'installed' && placement.health === 'error' ? '查看安全技术详情' : placement?.state === 'installed' && placement.health === 'incomplete' ? '查看影响' : '查看安装详情', description: '组件与诊断信息', run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }) };
2776
3285
  return { label: '返回插件', run: back };
2777
3286
  };
3287
+ const offlineCleanupAction = typedIssue?.actions.find((action) => action.id === 'cleanup');
3288
+ const offlineCleanup = typedIssue !== undefined
3289
+ && placement?.state === 'installed'
3290
+ && placement.blockingReason === undefined
3291
+ && (placement.actualState === 'orphaned' || placement.actualState === 'blocked')
3292
+ && offlineCleanupAction !== undefined
3293
+ && !platformMenu.some((action) => action.id === 'cleanup')
3294
+ && !platformMenu.some((action) => action.id === 'resolveScopeConflict')
3295
+ ? {
3296
+ label: `清理${scopeLabel(placement.scope)}中的遗留内容…`,
3297
+ description: '离线时也只清理这个插件目标中可证明属于 AW 的内容;完成后如需恢复,请单独确认重新安装。',
3298
+ status: 'error',
3299
+ run: () => {
3300
+ const intent = issueWriteIntent(typedIssue, offlineCleanupAction);
3301
+ if (intent)
3302
+ beginReview(intent);
3303
+ },
3304
+ }
3305
+ : undefined;
3306
+ const restrictedForceCleanup = placement?.state === 'installed'
3307
+ && placement.blockingReason === undefined
3308
+ && (placement.actualState === 'orphaned' || placement.actualState === 'blocked')
3309
+ && !platformMenu.some((action) => action.id === 'resolveScopeConflict')
3310
+ ? {
3311
+ label: '检查受限清理…',
3312
+ description: '只在当前单一目标有精确、专属的 AW 管理恢复证据时进入新的确认;不会清理共享或外来内容。',
3313
+ status: 'warning',
3314
+ run: () => beginRestrictedForceCleanup(plugin.pluginId, route.platform, placement.scope),
3315
+ }
3316
+ : undefined;
3317
+ const platformActions = () => [
3318
+ ...platformMenu.map(platformAction),
3319
+ ...(offlineCleanup ? [offlineCleanup] : []),
3320
+ ...(restrictedForceCleanup ? [restrictedForceCleanup] : []),
3321
+ ];
2778
3322
  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;
3323
+ 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' ? '可用'
3324
+ : placement.actualState === 'needsSync' ? '需要同步'
3325
+ : placement.actualState === 'blocked' ? '已阻断'
3326
+ : placement.health === 'incomplete' ? '需要处理' : '暂时不可用', status: placement.actualState === 'ready' ? 'success' : placement.actualState === 'blocked' ? 'error' : 'warning', compact: true }), placement.health !== 'ready'
3327
+ ? _jsx(StaticRow, { title: "\u539F\u56E0", meta: placement.blockingReason === 'artifactCollision' ? '输出位置存在冲突内容' : '当前安装与已保存设置不一致', status: "warning", compact: true })
3328
+ : null] }) : null;
2780
3329
  if (plugin.aggregateStatus === 'unavailable') {
2781
3330
  const sourceOverview = capabilityUnavailable ? undefined : props.controller.getSourceOverview(plugin.sourceId);
2782
3331
  const diagnostic = capabilityUnavailable
@@ -2786,7 +3335,7 @@ export function ConsoleAppV4(props) {
2786
3335
  ? `需要升级 AW CLI 才能管理 ${platformLabel(route.platform)}`
2787
3336
  : `暂时无法管理 ${platformLabel(route.platform)}`;
2788
3337
  description = '';
2789
- actions = platformMenu.map(platformAction);
3338
+ actions = platformActions();
2790
3339
  content = _jsxs(Box, { flexDirection: "column", children: [diagnostic
2791
3340
  ? _jsx(DiagnosticSummary, { diagnostic: diagnostic, compact: policy.mode === 'compact' })
2792
3341
  : null, _jsx(SemanticRows, { actions: actions, focus: effectiveFocus, kind: "action", compact: policy.mode === 'compact' })] });
@@ -2794,7 +3343,7 @@ export function ConsoleAppV4(props) {
2794
3343
  else if (!placement || placement.state === 'notInstalled') {
2795
3344
  title = `尚未添加到 ${platformLabel(route.platform)}`;
2796
3345
  description = '此插件支持所列使用位置。';
2797
- actions = platformMenu.map(platformAction);
3346
+ actions = platformActions();
2798
3347
  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
3348
  }
2800
3349
  else if (placement.state === 'blocked'
@@ -2803,13 +3352,13 @@ export function ConsoleAppV4(props) {
2803
3352
  description = placement.state === 'installed'
2804
3353
  ? '受管内容仍可安全移除;输出位置同时存在冲突内容。'
2805
3354
  : '输出位置已有不属于当前插件的内容。';
2806
- actions = platformMenu.map(platformAction);
3355
+ actions = platformActions();
2807
3356
  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
3357
  }
2809
3358
  else if (placement.state === 'scopeConflict') {
2810
3359
  title = '安装位置冲突';
2811
3360
  description = '正常状态只能保留一个位置。';
2812
- actions = platformMenu.map(platformAction);
3361
+ actions = platformActions();
2813
3362
  content = _jsx(SemanticRows, { actions: actions, focus: effectiveFocus, kind: "action", compact: policy.mode === 'compact' });
2814
3363
  }
2815
3364
  else if (placement.health === 'error') {
@@ -2834,13 +3383,13 @@ export function ConsoleAppV4(props) {
2834
3383
  };
2835
3384
  title = `${platformLabel(route.platform)} 暂时不可管理`;
2836
3385
  description = '';
2837
- actions = platformMenu.map(platformAction);
3386
+ actions = platformActions();
2838
3387
  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
3388
  }
2840
3389
  else if (placement.health === 'incomplete') {
2841
3390
  title = `${platformLabel(route.platform)} ${placement.health === 'incomplete' ? '安装不完整' : '暂时不可用'}`;
2842
3391
  description = '缺少一项必要内容,插件可能无法稳定启动。';
2843
- actions = platformMenu.map(platformAction);
3392
+ actions = platformActions();
2844
3393
  const actualTitle = placement.actualState === 'needsSync' ? '需要同步'
2845
3394
  : placement.actualState === 'blocked' ? '当前有安全阻断' : '需要处理';
2846
3395
  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 +3398,7 @@ export function ConsoleAppV4(props) {
2849
3398
  title = `已安装到${scopeLabel(placement.scope)}`;
2850
3399
  const installedVersion = placement.revision.installedVersion;
2851
3400
  description = `当前检查正常${installedVersion ? `;已安装版本 ${installedVersion}` : ''}。`;
2852
- actions = platformMenu.map(platformAction);
3401
+ actions = platformActions();
2853
3402
  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
3403
  }
2855
3404
  }
@@ -2886,7 +3435,7 @@ export function ConsoleAppV4(props) {
2886
3435
  ? '↑↓ 选择配置 · Enter 修改 · ←→ 分组 · Esc 返回'
2887
3436
  : '↑↓ 移动 · Enter 修改 · ←→ 分组 · s 保存 · u 恢复继承 · Esc 返回';
2888
3437
  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" }) });
3438
+ 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
3439
  }
2891
3440
  else {
2892
3441
  visibleConfigTabs = configTabs(currentConfigPage);
@@ -2900,7 +3449,13 @@ export function ConsoleAppV4(props) {
2900
3449
  ? (activeGroup?.fields ?? []).filter((field) => field.sharedNativeRender && !field.writable)
2901
3450
  : [];
2902
3451
  if (projectSharedNativeFields.length > 0) {
2903
- description += ' 等待设置由个人默认控制共享 native;当前项目不可改。更改影响所有项目。';
3452
+ description += ' 此设置由个人默认控制平台共享内容;当前项目不可改。更改影响所有项目。';
3453
+ }
3454
+ if (projectSharedNativeFields.some(configFieldCanUnset)) {
3455
+ description += ' 发现不再支持的 project override;实际使用 user/default;请删除旧值。';
3456
+ }
3457
+ if (!readonly && configDraftIssues.length > 0) {
3458
+ description += ' 字段已变化;失效草稿不会保存。';
2904
3459
  }
2905
3460
  const mutableLayer = {
2906
3461
  scope: route.layer.scope,
@@ -2919,7 +3474,7 @@ export function ConsoleAppV4(props) {
2919
3474
  })
2920
3475
  : () => applyConfigPreset(currentConfigPage, preset.id),
2921
3476
  }))
2922
- : (activeGroup?.fields ?? []).filter((field) => readonly || field.writable).map((field) => ({
3477
+ : (activeGroup?.fields ?? []).filter((field) => readonly || configFieldCanSet(field) || configFieldCanUnset(field)).map((field) => ({
2923
3478
  label: field.title,
2924
3479
  description: field.invalidOverrideIssue
2925
3480
  ? field.invalidOverrideOwnedByLayer
@@ -2927,7 +3482,9 @@ export function ConsoleAppV4(props) {
2927
3482
  : field.invalidOverrideIssue
2928
3483
  : readonly
2929
3484
  ? field.sourceLabel === '未设置' ? '没有任何位置提供值' : `来自${field.sourceLabel}`
2930
- : `${field.description ? `${field.description} · ` : ''}当前有效:${effectiveFieldDisplay(field)} · ${field.effectiveSourceLabel}`,
3485
+ : !configFieldCanSet(field) && configFieldCanUnset(field)
3486
+ ? '不再支持的 project override · 实际使用 user/default · 可删除旧值'
3487
+ : `${field.description ? `${field.description} · ` : ''}当前有效:${effectiveFieldDisplay(field)} · ${field.effectiveSourceLabel}`,
2931
3488
  meta: readonly ? effectiveFieldDisplay(field) : currentFieldDisplay(field),
2932
3489
  configKey: field.key,
2933
3490
  run: readonly
@@ -2937,7 +3494,7 @@ export function ConsoleAppV4(props) {
2937
3494
  fieldKey: field.key,
2938
3495
  groupId: activeGroup.id,
2939
3496
  })
2940
- : () => go(field.configured ? {
3497
+ : () => go(field.configured || !configFieldCanSet(field) ? {
2941
3498
  name: 'configFieldAction',
2942
3499
  pluginId: plugin.pluginId,
2943
3500
  layer: mutableLayer,
@@ -2952,17 +3509,21 @@ export function ConsoleAppV4(props) {
2952
3509
  }),
2953
3510
  }));
2954
3511
  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
- })),
3512
+ ...(configDraftIssues.length > 0 ? [{
3513
+ label: '放弃已变化字段的草稿',
3514
+ description: `${configDraftIssues.length} 项草稿的字段不存在、不可编辑或不再可删除;不会修改已保存设置。`,
3515
+ status: 'warning',
3516
+ run: discardInvalidConfigDraftChanges,
3517
+ }] : []),
3518
+ ...(dirty && configDraftIssues.length === 0 ? [{
3519
+ label: '查看保存汇总',
3520
+ description: '确认变化,再选择只保存或保存并应用',
3521
+ status: 'success',
3522
+ run: () => {
3523
+ setFocus(0);
3524
+ go({ name: 'configSave', pluginId: route.pluginId, layer: route.layer });
3525
+ },
3526
+ }] : []),
2966
3527
  ...(currentConfigPage.storedCount > 0 ? [{
2967
3528
  label: route.layer.scope === 'user'
2968
3529
  ? '清空个人默认…'
@@ -2993,29 +3554,47 @@ export function ConsoleAppV4(props) {
2993
3554
  ? activeGroup?.fields[effectiveConfigFocus]
2994
3555
  : undefined;
2995
3556
  if (!readonly) {
2996
- const canUnsetFocused = focusedConfigField?.configured === true
2997
- && (!focusedConfigField.invalidOverrideIssue
2998
- || focusedConfigField.invalidOverrideOwnedByLayer === true);
3557
+ const canUnsetFocused = focusedConfigField
3558
+ ? configFieldCanUnset(focusedConfigField)
3559
+ && (!focusedConfigField.invalidOverrideIssue
3560
+ || focusedConfigField.invalidOverrideOwnedByLayer === true)
3561
+ : false;
2999
3562
  help = canUnsetFocused
3000
- ? '↑↓ 移动 · Enter 修改 · ←→ 分组 · s 保存 · u 恢复继承 · Esc 返回'
3001
- : '↑↓ 移动 · Enter 修改 · ←→ 分组 · s 保存 · Esc 返回';
3563
+ ? '↑↓ 移动 · Enter 修改 · ←→ 分组 · s 查看汇总 · u 恢复继承 · Esc 返回'
3564
+ : '↑↓ 移动 · Enter 修改 · ←→ 分组 · s 查看汇总 · Esc 返回';
3002
3565
  }
3003
3566
  const fieldFocus = tabActions.length > 0
3004
3567
  ? effectiveConfigFocus < tabActions.length ? effectiveConfigFocus : Math.min(scrollOffset, tabActions.length - 1)
3005
3568
  : 0;
3006
3569
  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));
3570
+ const viewport = actionViewport(tabActions, policy.mode === 'compact', fieldFocus, scrollOffset, pageActionBudget(1 + operationRows + 3 + (configDraftIssues.length > 0 ? 3 : 0)));
3008
3571
  const totalFields = currentConfigPage.groups.reduce((count, group) => count + group.fields.length, 0);
3009
3572
  const countLabel = `${totalFields} 项${currentConfigPage.presets.length > 0 ? ` · ${currentConfigPage.presets.length} 个方案` : ''}`;
3010
3573
  context = readonly ? countLabel
3011
3574
  : 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
3575
+ 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
3576
  ? _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
3577
  : null, operationActions.length > 0
3015
3578
  ? _jsx(SemanticRows, { actions: actions, focus: effectiveConfigFocus, kind: "action", compact: policy.mode === 'compact', offset: tabActions.length, limit: operationActions.length })
3016
3579
  : null] });
3017
3580
  }
3018
3581
  }
3582
+ else if (route.name === 'configSave' && plugin && currentConfigPage) {
3583
+ const changes = configChanges();
3584
+ const sharedNative = route.layer.scope === 'user'
3585
+ && [...Object.keys(draftUpdates), ...draftRemoves].some((key) => currentConfigPage.groups.flatMap((group) => group.fields)
3586
+ .some((field) => field.key === key && field.sharedNativeRender));
3587
+ breadcrumb = `${plugin.displayName} / 设置 / 保存汇总`;
3588
+ context = `${changes.length} 项未保存`;
3589
+ title = `保存 ${changes.length} 项设置修改?`;
3590
+ description = '保存后可选择是否立即应用到当前可见安装。';
3591
+ actions = [
3592
+ { label: '保存并应用', description: sharedNative ? '保存并重新生成 Codex 平台共享 Skill;影响所有启用项目' : '保存设置并同步当前可见安装', status: 'success', run: () => beginConfigSave(true) },
3593
+ { label: '只保存', description: sharedNative ? '只保存个人默认;平台共享 Skill 将等待同步' : '只保存所选设置层,不应用当前安装', run: () => beginConfigSave(false) },
3594
+ { label: '返回修改', run: back },
3595
+ ];
3596
+ 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' })] });
3597
+ }
3019
3598
  else if (route.name === 'configFieldAction' && plugin && activeField) {
3020
3599
  const layer = configLayerLabel(route.layer);
3021
3600
  breadcrumb = `${plugin.displayName} / 设置 / ${layer} / ${activeField.title}`;
@@ -3026,23 +3605,25 @@ export function ConsoleAppV4(props) {
3026
3605
  ? '删除个人默认中的值'
3027
3606
  : '删除当前项目中的值';
3028
3607
  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)
3608
+ ...(configFieldCanSet(activeField) ? [{
3609
+ label: '修改值…',
3610
+ run: () => go({
3611
+ name: 'configFieldEditor',
3612
+ pluginId: plugin.pluginId,
3613
+ layer: route.layer,
3614
+ fieldKey: activeField.key,
3615
+ editor: fieldEditor(activeField),
3616
+ ...fieldEditorMode(activeField),
3617
+ }),
3618
+ }] : []),
3619
+ ...(configFieldCanUnset(activeField) && (!activeField.invalidOverrideIssue || activeField.invalidOverrideOwnedByLayer) ? [draftRemoves.includes(activeField.key)
3041
3620
  ? { label: '撤销删除', run: () => { cancelDraftRemoval(activeField.key); back(); } }
3042
3621
  : { label: deleteLabel, status: 'error', run: () => { removeDraft(activeField.key); back(); } }] : []),
3043
3622
  ];
3044
3623
  content = _jsxs(Box, { flexDirection: "column", children: [_jsxs(SectionCard, { children: [activeField.invalidOverrideIssue
3045
3624
  ? _jsx(StaticRow, { title: "\u5F53\u524D\u8986\u76D6\u65E0\u6548", meta: activeField.invalidOverrideIssue, status: "warning" })
3625
+ : null, !configFieldCanSet(activeField) && configFieldCanUnset(activeField)
3626
+ ? _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
3627
  : 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
3628
  ? _jsx(StaticRow, { title: "\u5220\u9664\u540E\u5C06\u6062\u590D", meta: `${activeField.fallbackDisplayValue ?? '未设置'} · 来自${activeField.fallbackSourceLabel ?? '未设置'}` })
3048
3629
  : null] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
@@ -3053,8 +3634,8 @@ export function ConsoleAppV4(props) {
3053
3634
  if (showChoices) {
3054
3635
  title = `选择${activeField.title}`;
3055
3636
  description = activeField.allowCustom
3056
- ? '先选择 manifest 方案,也可以继续自定义。'
3057
- : '此字段只允许使用 manifest 方案。';
3637
+ ? '先选择插件提供的方案,也可以继续自定义。'
3638
+ : '此字段只允许使用插件提供的方案。';
3058
3639
  help = '↑↓ 移动 · Enter 使用 · Esc 取消';
3059
3640
  const current = draftUpdates[activeField.key] ?? activeField.draftValue ?? activeField.value;
3060
3641
  const booleanCustomValues = activeField.type === 'boolean' && activeField.allowCustom
@@ -3164,7 +3745,7 @@ export function ConsoleAppV4(props) {
3164
3745
  }
3165
3746
  else {
3166
3747
  title = activeField.sensitive ? '输入敏感值' : `输入${activeField.title}`;
3167
- help = '输入 · Enter 使用 · Ctrl-U 删除本层值 · Esc 取消';
3748
+ help = '输入 · Enter 使用 · Ctrl-U 清空输入 · Esc 取消';
3168
3749
  description = route.editor === 'number'
3169
3750
  ? `允许范围:${activeField.min ?? '不限'}–${activeField.max ?? '不限'}${activeField.unit ? ` ${activeField.unit}` : ''}。`
3170
3751
  : activeField.sensitive ? '当前值不会回显;留空不会自动删除旧值。' : activeField.description ?? '';
@@ -3179,8 +3760,8 @@ export function ConsoleAppV4(props) {
3179
3760
  breadcrumb = `${plugin.displayName} / 设置 / ${activeField.title} / ${propertyTitle}`;
3180
3761
  if (route.stage === 'key') {
3181
3762
  title = '输入新键名';
3182
- description = inputError ?? '键名和属性数量会由 Core validator 检查。';
3183
- help = '输入 · Enter 继续 · Esc 取消';
3763
+ description = inputError ?? '键名和属性数量会按插件规则检查。';
3764
+ help = '输入 · Enter 继续 · Ctrl-U 清空输入 · Esc 取消';
3184
3765
  content = _jsx(InputCard, { focused: true, title: _jsx(InputDisplay, { value: inputValue, columns: Math.max(8, size.columns - 8) }), description: description, status: inputError ? 'error' : undefined });
3185
3766
  }
3186
3767
  else if (property !== undefined && definition && route.stage === 'action') {
@@ -3214,7 +3795,7 @@ export function ConsoleAppV4(props) {
3214
3795
  else if (property !== undefined && definition && route.stage === 'choices') {
3215
3796
  title = `选择${propertyTitle}`;
3216
3797
  description = definition.type !== 'boolean' && definition.choices?.allowCustom
3217
- ? '先选择 manifest 方案,也可以继续自定义。'
3798
+ ? '先选择插件提供的方案,也可以继续自定义。'
3218
3799
  : '只显示此属性允许的选择。';
3219
3800
  help = '↑↓ 移动 · Enter 使用 · Esc 取消';
3220
3801
  const current = activeObjectDraft[property];
@@ -3266,7 +3847,7 @@ export function ConsoleAppV4(props) {
3266
3847
  else if (property !== undefined && definition) {
3267
3848
  title = activePropertySensitive ? '输入敏感值' : `输入${propertyTitle}`;
3268
3849
  description = inputError ?? (activePropertySensitive ? '输入已隐藏。' : definition.description ?? '按 Enter 使用输入值。');
3269
- help = '输入 · Enter 使用 · Esc 取消';
3850
+ help = '输入 · Enter 使用 · Ctrl-U 清空输入 · Esc 取消';
3270
3851
  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
3852
  }
3272
3853
  }
@@ -3277,9 +3858,11 @@ export function ConsoleAppV4(props) {
3277
3858
  context = '暂时不可编辑';
3278
3859
  const diagnostic = configUnavailableDiagnostic(route);
3279
3860
  const invalidLayer = (route.invalidStores?.length ?? 0) > 0;
3861
+ const canRecoverSource = configUnavailableCanRecoverSource(route);
3280
3862
  title = diagnostic.title;
3281
3863
  description = '';
3282
3864
  const resetScopes = route.invalidStores?.map(({ scope }) => scope)
3865
+ ?? route.storedScopes
3283
3866
  ?? (route.layer ? [route.layer.scope] : []);
3284
3867
  actions = [
3285
3868
  ...resetScopes.flatMap((scope) => {
@@ -3298,19 +3881,23 @@ export function ConsoleAppV4(props) {
3298
3881
  }]
3299
3882
  : [];
3300
3883
  }),
3301
- ...(!invalidLayer ? [
3302
- { label: `在终端运行 aw catalog source sync ${unavailableSourceId}`, description: '同步成功后重新加载 TUI', run: back },
3303
- { label: showTechnical ? '返回问题摘要' : '技术详情', description: '查看脱敏错误代码和阶段', run: () => setShowTechnical((value) => !value) },
3884
+ ...(!invalidLayer && canRecoverSource ? [
3885
+ {
3886
+ label: '修复插件来源',
3887
+ description: `检查并恢复 ${unavailableSourceId};完成后返回当前设置位置。`,
3888
+ run: () => props.controller.beginConfigSourceRecovery(unavailableSourceId),
3889
+ },
3304
3890
  ] : []),
3891
+ { label: showTechnical ? '返回问题摘要' : '技术详情', description: '查看脱敏错误代码和阶段', run: () => toggleTechnical() },
3305
3892
  { label: '返回', run: back },
3306
3893
  ];
3307
3894
  const unsupportedScopes = resetScopes.filter((scope) => configResetAvailability[scope]?.resettable === false);
3308
3895
  const unsupportedRows = unsupportedScopes.map((scope) => _jsx(StaticRow, { title: `${scope === 'user' ? '个人默认' : '当前项目'}设置层不能自动重置:特殊节点,请手动移动`, status: "warning", compact: true }, `unsupported-reset:${scope}`));
3309
3896
  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 })] })
3897
+ ? _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) => [
3898
+ _jsx(StaticRow, { title: scopeLabel(store.scope), meta: store.code, compact: true }, store.scope),
3899
+ _jsx(StaticRow, { title: "path summary", meta: store.pathSummary, compact: true }, `${store.scope}:path`),
3900
+ ]), _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
3901
  : _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
3902
  }
3316
3903
  else if (route.name === 'sourceList') {
@@ -3340,9 +3927,14 @@ export function ConsoleAppV4(props) {
3340
3927
  };
3341
3928
  }),
3342
3929
  { label: '+ 添加来源', description: 'Git 仓库或本地目录', run: beginAddSource },
3930
+ {
3931
+ label: '检查全部来源',
3932
+ description: '并行检查各来源;结果不会自动启用待检查版本',
3933
+ run: () => props.controller.recheckAllCatalogSources(),
3934
+ },
3343
3935
  ];
3344
3936
  const viewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, responsiveBudget(8, 12));
3345
- const sourceCount = Math.max(0, actions.length - 1);
3937
+ const sourceCount = Math.max(0, actions.length - 2);
3346
3938
  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
3939
  }
3348
3940
  else if (route.name === 'sourceDetail') {
@@ -3356,92 +3948,103 @@ export function ConsoleAppV4(props) {
3356
3948
  const combinedRepairAndMismatch = generationState === 'repairRequired'
3357
3949
  && generation?.configMismatch === true;
3358
3950
  const logicalIdentityChanged = generation?.errorCode === 'catalogLogicalIdentityChanged';
3359
- const activationBlocked = props.controller.catalogActivationBlockReason();
3951
+ const activationBlocked = props.controller.catalogSourceRecoveryBlockReason();
3360
3952
  breadcrumb = `插件来源 / ${route.sourceId}`;
3361
3953
  context = detail?.type === 'path' ? '本地目录' : 'Git';
3362
3954
  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。可在这里获取并启用。';
3955
+ description = generationState === 'incompatible'
3956
+ ? `当前目录版本与 AW 不兼容;缺少 ${generation?.compatibility?.state === 'unsupported' ? generation.compatibility.missingCapabilities.length : 0} 项功能支持。`
3957
+ : freshness?.state === 'incompatible'
3958
+ ? generationState === 'repairRequired'
3959
+ ? `待检查版本需要升级 AW 或修改来源后才能使用;损坏的当前启用版本仍可按旧版本精确修复。`
3960
+ : activeVersion
3961
+ ? `当前 AW 缺少 ${freshness.missingCapabilities?.length ?? 0} 项待检查版本所需功能;当前目录版本 ${activeVersion} 仍可浏览和使用。`
3962
+ : `当前 AW 缺少 ${freshness.missingCapabilities?.length ?? 0} 项待检查版本所需功能;这个来源没有可用目录版本。`
3963
+ : freshness?.state === 'checkFailed' && activeVersion
3964
+ ? `更新检查失败,但当前目录版本 ${activeVersion} 仍可浏览和使用。`
3965
+ : freshness?.state === 'updateAvailable'
3966
+ ? `当前使用 ${activeVersion ?? '未启用'};待检查版本 ${candidateVersion ?? '未知'} 可更新。`
3967
+ : freshness?.state === 'localChangesAvailable'
3968
+ ? '本地插件目录内容已变化;确认前仍使用当前启用版本。'
3969
+ : activeVersion ? `当前使用目录版本 ${activeVersion}。`
3970
+ : generationState === 'stateInvalid' ? '目录版本状态无效。确认恢复后可以重新启用这个来源。'
3971
+ : generationState === 'repairRequired' ? combinedRepairAndMismatch
3972
+ ? '当前目录版本损坏,且来源配置已变化。验证待检查版本后可精确修复同一版本,或明确切换到新版本。'
3973
+ : '当前目录版本缺失或损坏。可在这里精确修复。'
3974
+ : generationState === 'configMismatch' ? '来源配置与当前启用版本不一致。可验证待检查版本并切换。'
3975
+ : logicalIdentityChanged ? '来源发布身份已变化。请编辑来源,或移除后重新添加。'
3976
+ : '来源没有可用的启用版本。可在这里取得并启用。';
3381
3977
  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
- }),
3978
+ const generationAction = logicalIdentityChanged ? []
3979
+ : generationState === 'incompatible' ? [{
3980
+ label: '查看不兼容详情',
3981
+ description: '显示所需和缺少的功能支持',
3982
+ status: 'error',
3983
+ run: () => go({ name: 'sourceTechnical', sourceId: route.sourceId }),
3402
3984
  }]
3403
- : generationState !== 'ready' ? [{
3404
- label: generationState === 'stateInvalid' ? '恢复状态并启用'
3405
- : generationState === 'configMismatch' ? '验证并切换' : '获取并启用',
3406
- description: '先准备只读候选 Review;确认后才切换 Active',
3407
- disabled: activationBlocked ? `请先处理当前草稿或操作:${activationBlocked}` : undefined,
3985
+ : generationState === 'repairRequired' ? [{
3986
+ label: '重新下载旧版本并精确修复',
3987
+ description: '使用当前启用版本的精确提交;成功后保持此版本',
3988
+ disabled: activationBlocked ? `请先处理:${sourceOperationBlockLabel(activationBlocked)}` : undefined,
3408
3989
  run: () => beginReview({
3409
3990
  kind: 'catalogGeneration',
3410
- action: generationState === 'stateInvalid' ? 'recoverState'
3411
- : generationState === 'configMismatch' ? 'update' : 'activate',
3991
+ action: 'repair',
3412
3992
  sourceId: route.sourceId,
3413
3993
  }),
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 }),
3994
+ }, ...(freshness?.state === 'incompatible' ? [] : [{
3995
+ label: '使用待检查版本替换损坏版本…',
3996
+ description: '旧版本无法取得时使用;进入独立影响确认',
3997
+ disabled: activationBlocked ? `请先处理:${sourceOperationBlockLabel(activationBlocked)}` : undefined,
3998
+ run: () => beginReview({
3999
+ kind: 'catalogGeneration',
4000
+ action: 'replaceDamaged',
4001
+ sourceId: route.sourceId,
4002
+ }),
4003
+ }])]
4004
+ : freshness?.state === 'incompatible' && generationState !== 'ready' ? []
4005
+ : generationState !== 'ready' ? [{
4006
+ label: generationState === 'stateInvalid' ? '恢复状态并启用'
4007
+ : generationState === 'configMismatch' ? '验证并切换' : '获取并启用',
4008
+ description: '先准备只读的待检查版本确认;确认后才切换当前版本',
4009
+ disabled: activationBlocked ? `请先处理:${sourceOperationBlockLabel(activationBlocked)}` : undefined,
4010
+ run: () => beginReview({
4011
+ kind: 'catalogGeneration',
4012
+ action: generationState === 'stateInvalid' ? 'recoverState'
4013
+ : generationState === 'configMismatch' ? 'update' : 'activate',
4014
+ sourceId: route.sourceId,
4015
+ }),
3422
4016
  }]
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
- }];
4017
+ : freshness?.state === 'updateAvailable' || freshness?.state === 'localChangesAvailable'
4018
+ ? [{
4019
+ label: freshness.state === 'localChangesAvailable'
4020
+ ? '应用本地内容变化…' : `更新到 ${candidateVersion ?? '待检查版本'}…`,
4021
+ description: '进入确认;确认前不会切换当前版本',
4022
+ disabled: activationBlocked ? `请先处理:${sourceOperationBlockLabel(activationBlocked)}` : undefined,
4023
+ run: () => beginReview({ kind: 'catalogGeneration', action: 'update', sourceId: route.sourceId }),
4024
+ }]
4025
+ : freshness?.state === 'versionConflict'
4026
+ || freshness?.state === 'rollbackDetected'
4027
+ || freshness?.state === 'incompatible'
4028
+ ? [{
4029
+ label: freshness.state === 'versionConflict' ? '查看版本冲突'
4030
+ : freshness.state === 'rollbackDetected' ? '查看回退详情' : '查看不兼容详情',
4031
+ description: '普通更新已阻止;当前启用版本保持不变',
4032
+ status: 'error',
4033
+ run: () => go({ name: 'sourceTechnical', sourceId: route.sourceId }),
4034
+ }]
4035
+ : [{
4036
+ label: freshness?.state === 'checking' ? '正在检查…' : '重新检查',
4037
+ description: '轻量检查这个来源;提交发生变化时才完整验证',
4038
+ disabled: freshness?.state === 'checking' ? '检查正在进行' : undefined,
4039
+ run: () => props.controller.recheckCatalogSource(route.sourceId),
4040
+ }];
3439
4041
  actions = [
3440
4042
  ...generationAction,
3441
- { label: '编辑来源…', description: '修改前先 Review;默认保存并启用', run: () => go({ name: 'sourceEdit', sourceId: route.sourceId }) },
3442
- ...(freshness?.state === 'incompatible' && generationState !== 'ready' ? [] : [{
4043
+ { label: '编辑来源…', description: '集中修改地址、引用、子目录和自动检查', run: () => openSourceEdit(route.sourceId) },
4044
+ ...(generationState === 'incompatible'
4045
+ || (freshness?.state === 'incompatible' && generationState !== 'ready') ? [] : [{
3443
4046
  label: generationState === 'ready' ? '设为主来源…' : '启用并设为主来源…',
3444
- description: generationState === 'ready' ? '只改变默认 marketplace' : '先启用确认的候选,再设为主来源',
4047
+ description: generationState === 'ready' ? '只改变默认插件来源' : '先启用确认的待检查版本,再设为主来源',
3445
4048
  disabled: detail?.primary ? '当前已经是主来源'
3446
4049
  : logicalIdentityChanged ? '请编辑来源,或移除后重新添加' : undefined,
3447
4050
  run: () => generationState === 'ready'
@@ -3456,7 +4059,7 @@ export function ConsoleAppV4(props) {
3456
4059
  }),
3457
4060
  }]),
3458
4061
  { 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 }) },
4062
+ { label: '技术详情', description: '查看脱敏错误代码和当前版本证据', run: () => go({ name: 'sourceTechnical', sourceId: route.sourceId }) },
3460
4063
  ...(generationState !== 'ready'
3461
4064
  || freshness?.state === 'updateAvailable'
3462
4065
  || freshness?.state === 'localChangesAvailable'
@@ -3464,7 +4067,7 @@ export function ConsoleAppV4(props) {
3464
4067
  || freshness?.state === 'rollbackDetected'
3465
4068
  || freshness?.state === 'incompatible' ? [{
3466
4069
  label: freshness?.state === 'checking' ? '正在检查…' : '重新检查',
3467
- description: '轻量检查这个来源;commit 变化时才完整验证',
4070
+ description: '轻量检查这个来源;提交发生变化时才完整验证',
3468
4071
  disabled: freshness?.state === 'checking' ? '检查正在进行' : undefined,
3469
4072
  run: () => props.controller.recheckCatalogSource(route.sourceId),
3470
4073
  }] : []),
@@ -3477,7 +4080,7 @@ export function ConsoleAppV4(props) {
3477
4080
  ];
3478
4081
  const sourceActionViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, responsiveBudget(2, 5));
3479
4082
  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 })] });
4083
+ 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
4084
  }
3482
4085
  else if (route.name === 'sourceTechnical') {
3483
4086
  const overview = props.controller.getSourceOverview(route.sourceId);
@@ -3501,9 +4104,9 @@ export function ConsoleAppV4(props) {
3501
4104
  context = '只读';
3502
4105
  title = '来源技术诊断';
3503
4106
  description = '';
3504
- help = 'Enter 返回 · Esc 返回';
4107
+ help = 'PgUp/PgDn 查看全部技术详情 · Enter 返回 · Esc 返回';
3505
4108
  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' })] });
4109
+ 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
4110
  }
3508
4111
  else if (route.name === 'sourceAdd' && sourceDraft?.owner === route.owner) {
3509
4112
  breadcrumb = sourceDraft.mode === 'add' ? '插件来源 / 添加' : `插件来源 / ${sourceDraft.sourceId} / 编辑`;
@@ -3516,6 +4119,7 @@ export function ConsoleAppV4(props) {
3516
4119
  description = '来源类型创建后不能修改。';
3517
4120
  const selectType = (type, autoCheck) => {
3518
4121
  setSourceDraft({ ...sourceDraft, type, autoCheck });
4122
+ setDirty(true);
3519
4123
  go({ name: 'sourceAdd', step: sourceDraft.sourceId ? 'location' : 'id', owner: route.owner });
3520
4124
  };
3521
4125
  actions = [
@@ -3532,8 +4136,9 @@ export function ConsoleAppV4(props) {
3532
4136
  const useRefPreset = async (refPolicy, ref) => {
3533
4137
  const next = { ...sourceDraft, refPolicy, ref };
3534
4138
  setSourceDraft(next);
3535
- if (next.mode === 'update')
3536
- await beginSourceUpdateReview(next);
4139
+ setDirty(true);
4140
+ if (route.returnToDraft)
4141
+ acceptSourceFieldAndBack();
3537
4142
  else
3538
4143
  go({ name: 'sourceAdd', step: 'subdir', owner: route.owner });
3539
4144
  };
@@ -3541,7 +4146,12 @@ export function ConsoleAppV4(props) {
3541
4146
  { 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
4147
  { 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
4148
  { 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 }); } },
4149
+ { label: '指定 Git ref…', description: 'branch、tag、完整 ref 或 commit', selected: sourceDraft.refPolicy === 'explicit' && sourceDraft.ref !== CATALOG_LATEST_REF && sourceDraft.ref !== CATALOG_BETA_REF, run: () => {
4150
+ if (route.returnToDraft)
4151
+ props.controller.replace({ name: 'sourceAdd', step: 'ref', owner: route.owner, returnToDraft: true });
4152
+ else
4153
+ go({ name: 'sourceAdd', step: 'ref', owner: route.owner });
4154
+ } },
3545
4155
  ];
3546
4156
  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
4157
  }
@@ -3558,60 +4168,115 @@ export function ConsoleAppV4(props) {
3558
4168
  }
3559
4169
  else if (route.name === 'sourceEdit') {
3560
4170
  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 });
4171
+ const activeDraft = route.owner && sourceDraft?.owner === route.owner ? sourceDraft : undefined;
4172
+ const startDraftField = (step) => {
4173
+ let draft = activeDraft;
4174
+ if (!draft) {
4175
+ const source = props.controller.getConfiguredSource(route.sourceId);
4176
+ if (!source)
4177
+ return;
4178
+ const owner = `source-update:${route.sourceId}:${Date.now()}`;
4179
+ draft = sourceDraftFromSource(source, owner);
4180
+ setSourceDraft(draft);
4181
+ props.controller.replace({ name: 'sourceEdit', sourceId: route.sourceId, owner });
4182
+ }
4183
+ go({ name: 'sourceAdd', step, owner: draft.owner, returnToDraft: true });
3571
4184
  };
3572
- const switchOfficialTransport = (location) => {
3573
- if (!detail || detail.type !== 'git'
3574
- || detail.locationSummary === projectCatalogSourceLocation(location))
4185
+ const draft = activeDraft;
4186
+ breadcrumb = `插件来源 / ${route.sourceId} / 编辑`;
4187
+ title = draft?.mode === 'add' ? `检查新来源 ${draft.sourceId}` : `编辑 ${route.sourceId}`;
4188
+ description = '地址、引用、子目录和自动检查组成一个草稿;保存前统一 Review。';
4189
+ const locationSummary = draft
4190
+ ? projectCatalogSourceLocation(draft.location)
4191
+ : detail?.locationSummary ?? '不可用';
4192
+ const setOfficialLocation = (location) => {
4193
+ const current = draft ?? (() => {
4194
+ const source = props.controller.getConfiguredSource(route.sourceId);
4195
+ if (!source)
4196
+ return undefined;
4197
+ return sourceDraftFromSource(source, `source-update:${route.sourceId}:${Date.now()}`);
4198
+ })();
4199
+ if (!current || current.type !== 'git' || current.location === location)
3575
4200
  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
- };
4201
+ const next = { ...current, location };
3589
4202
  setSourceDraft(next);
3590
4203
  setDirty(true);
3591
- void beginSourceUpdateReview(next);
4204
+ props.controller.replace({ name: 'sourceEdit', sourceId: route.sourceId, owner: next.owner });
3592
4205
  };
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 ? '关闭' : '开启' }] }) },
4206
+ actions = [
4207
+ ...(route.sourceId === 'official' && (draft?.type === 'git' || !draft && detail?.type === 'git') ? [
4208
+ { label: 'HTTPS', description: '使用系统 Git credential helper', selected: locationSummary === projectCatalogSourceLocation(OFFICIAL_CATALOG_SOURCE_URL), run: () => setOfficialLocation(OFFICIAL_CATALOG_SOURCE_URL) },
4209
+ { label: 'SSH', description: '使用本机 SSH key;Git 连接使用 BatchMode', selected: locationSummary === projectCatalogSourceLocation(OFFICIAL_CATALOG_SOURCE_SSH_URL), run: () => setOfficialLocation(OFFICIAL_CATALOG_SOURCE_SSH_URL) },
4210
+ ] : [{ label: draft?.type === 'path' || detail?.type === 'path' ? '本地目录' : 'Git 仓库地址', description: locationSummary, run: () => startDraftField('location') }]),
4211
+ ...(draft?.type === 'git' || !draft && detail?.type === 'git' ? [
4212
+ { label: '版本引用', description: draft ? draft.refPolicy === 'explicit' ? `固定:${draft.ref}` : '跟随 AW 版本通道' : detail?.refPolicy === 'explicit' ? `固定:${detail.explicitRef}` : '跟随 AW 版本通道', run: () => startDraftField('refPolicy') },
4213
+ { label: 'Catalog 子目录', description: draft?.subdir || detail?.subdir || '未设置', run: () => startDraftField('subdir') },
4214
+ ] : []),
4215
+ { label: '自动检查更新', description: (draft?.autoCheck ?? detail?.autoCheck) ? '已开启' : '已关闭', selected: draft?.autoCheck ?? detail?.autoCheck, run: () => {
4216
+ if (!draft) {
4217
+ const source = props.controller.getConfiguredSource(route.sourceId);
4218
+ if (!source)
4219
+ return;
4220
+ const owner = `source-update:${route.sourceId}:${Date.now()}`;
4221
+ const next = sourceDraftFromSource(source, owner);
4222
+ next.autoCheck = !next.autoCheck;
4223
+ setSourceDraft(next);
4224
+ setDirty(true);
4225
+ props.controller.replace({ name: 'sourceEdit', sourceId: route.sourceId, owner });
4226
+ return;
4227
+ }
4228
+ setSourceDraft({ ...draft, autoCheck: !draft.autoCheck });
4229
+ setDirty(true);
4230
+ } },
4231
+ { label: '查看修改汇总', description: draft && sourceDraftChanges(draft).length > 0 ? `${sourceDraftChanges(draft).length} 项未保存` : '尚无修改', disabled: draft && sourceDraftChanges(draft).length > 0 ? undefined : '请先修改来源设置', status: 'success', run: () => {
4232
+ if (!draft)
4233
+ return;
4234
+ setFocus(0);
4235
+ go({ name: 'sourceSave', owner: draft.owner });
4236
+ } },
3604
4237
  ];
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] });
4238
+ 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] });
4239
+ }
4240
+ else if (route.name === 'sourceSave' && sourceDraft?.owner === route.owner) {
4241
+ const changes = sourceDraftChanges(sourceDraft);
4242
+ breadcrumb = `插件来源 / ${sourceDraft.sourceId} / 修改汇总`;
4243
+ context = `${changes.length} 项未保存`;
4244
+ title = sourceDraft.mode === 'add' ? `保存新来源 ${sourceDraft.sourceId}?` : `保存 ${sourceDraft.sourceId} 的修改?`;
4245
+ description = '选择是否在保存后验证候选并启用。保存成功与启用成功会分别报告。';
4246
+ actions = [
4247
+ { label: '保存并启用', description: '保存来源配置,再验证并切换当前目录版本', status: 'success', run: () => beginSourceReview(sourceDraft, true) },
4248
+ { label: '只保存', description: '只保存来源配置;当前目录版本保持不变', run: () => beginSourceReview(sourceDraft, false) },
4249
+ { label: '返回修改', run: back },
4250
+ ];
4251
+ 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' })] });
4252
+ }
4253
+ else if (route.name === 'scriptTimeout' && pendingReviewIntent) {
4254
+ breadcrumb = '高级选项 / 安装前检查脚本';
4255
+ context = '毫秒';
4256
+ title = '设置脚本超时';
4257
+ description = `输入 ${SCRIPT_TIMEOUT_MIN_MS}–${SCRIPT_TIMEOUT_MAX_MS} 毫秒。默认 ${SCRIPT_TIMEOUT_DEFAULT_MS} 毫秒。`;
4258
+ help = '输入 · Enter 应用并重新准备 Review · Ctrl-U 清空 · Esc 取消';
4259
+ 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
4260
  }
3607
4261
  else if (route.name === 'sourceRemoveBlocked') {
3608
4262
  breadcrumb = `插件来源 / ${route.sourceId} / 移除`;
3609
4263
  context = '已阻止';
3610
4264
  title = `无法移除 ${route.sourceId}`;
3611
4265
  description = sourceReferenceSummary?.inventoryComplete === false ? '引用清单不完整,不能把未知引用当作零。' : '仍有内容依赖这个来源。';
3612
- actions = [{ label: sourceDependenciesExpanded ? '收起依赖内容' : '查看依赖内容', description: '只显示插件、来源、平台、位置和类别聚合', run: () => setSourceDependenciesExpanded((value) => !value) }, { label: '返回来源详情', run: back }];
4266
+ actions = [{ label: sourceDependenciesExpanded ? '收起依赖内容' : '查看依赖内容', description: '只显示插件、来源、平台、位置和类别聚合', run: () => {
4267
+ setSourceDependenciesExpanded((value) => !value);
4268
+ setBatchTargetOffset(0);
4269
+ } }, { label: '返回来源详情', run: back }];
3613
4270
  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' })] });
4271
+ const referenceLimit = Math.max(1, pageActionBudget(10));
4272
+ const references = sourceReferenceSummary?.dependencies ?? [];
4273
+ const referenceOffset = Math.min(batchTargetOffset, Math.max(0, references.length - 1));
4274
+ const visibleReferences = references.slice(referenceOffset, referenceOffset + referenceLimit);
4275
+ const hiddenReferences = Math.max(0, references.length - referenceOffset - visibleReferences.length);
4276
+ if (sourceDependenciesExpanded && references.length > referenceLimit) {
4277
+ help = 'PgUp/PgDn 查看全部引用 · ↑↓ 移动 · Enter 选择 · Esc 返回';
4278
+ }
4279
+ 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
4280
  }
3616
4281
  else if (route.name === 'issueList') {
3617
4282
  const issues = props.controller.listUserIssues();
@@ -3623,7 +4288,7 @@ export function ConsoleAppV4(props) {
3623
4288
  : '当前状态检查没有发现需要处理的安装、来源或设置问题。';
3624
4289
  if (issues.length === 0) {
3625
4290
  actions = [
3626
- { label: '返回首页', run: () => go({ name: 'home' }) },
4291
+ { label: '返回首页', run: () => requestSessionNavigation('home') },
3627
4292
  { label: '浏览插件', run: () => go({ name: 'pluginList', mode: 'browse' }) },
3628
4293
  ];
3629
4294
  }
@@ -3706,7 +4371,7 @@ export function ConsoleAppV4(props) {
3706
4371
  go({ name: 'pluginDependencies', pluginId: entry.pluginId });
3707
4372
  }
3708
4373
  else
3709
- setShowTechnical((value) => !value);
4374
+ toggleTechnical();
3710
4375
  };
3711
4376
  const issueHasTechnicalDetails = Boolean(issue.diagnostic.technical);
3712
4377
  actions = issue.actions.filter((entry) => entry.id !== 'viewTechnical' || issueHasTechnicalDetails).map((entry) => ({
@@ -3718,7 +4383,7 @@ export function ConsoleAppV4(props) {
3718
4383
  }));
3719
4384
  const issuePlugin = issue.pluginId ? props.controller.getPluginOverview(issue.pluginId) : undefined;
3720
4385
  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')
4386
+ 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
4387
  || issue.category === 'scopeConflict' });
3723
4388
  const issueActionViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(6));
3724
4389
  const compactTechnicalActionViewport = policy.mode === 'compact' && showTechnical
@@ -3736,7 +4401,7 @@ export function ConsoleAppV4(props) {
3736
4401
  actions = [
3737
4402
  { label: '查看完整依赖关系', run: () => go({ name: 'pluginDependencies', pluginId: issuePlugin.pluginId }) },
3738
4403
  { label: '返回插件详情', run: () => go({ name: 'pluginDetail', pluginId: issuePlugin.pluginId }) },
3739
- ...(issueHasTechnicalDetails ? [{ label: showTechnical ? '隐藏技术详情' : '技术详情', run: () => setShowTechnical((value) => !value) }] : []),
4404
+ ...(issueHasTechnicalDetails ? [{ label: showTechnical ? '隐藏技术详情' : '技术详情', run: () => toggleTechnical() }] : []),
3740
4405
  ];
3741
4406
  const dependencyBlockedViewport = policy.mode === 'compact' && showTechnical
3742
4407
  ? { offset: Math.min(Math.max(0, focus - 1), Math.max(0, actions.length - 2)), limit: 2 }
@@ -3778,7 +4443,7 @@ export function ConsoleAppV4(props) {
3778
4443
  go({ name: 'installTargets', pluginId: issuePlugin.pluginId });
3779
4444
  } },
3780
4445
  { label: '查看依赖关系', run: () => go({ name: 'pluginDependencies', pluginId: issuePlugin.pluginId }) },
3781
- ...(issueHasTechnicalDetails ? [{ label: showTechnical ? '隐藏技术详情' : '技术详情', run: () => setShowTechnical((value) => !value) }] : []),
4446
+ ...(issueHasTechnicalDetails ? [{ label: showTechnical ? '隐藏技术详情' : '技术详情', run: () => toggleTechnical() }] : []),
3782
4447
  ];
3783
4448
  const unsupportedTargetViewport = policy.mode === 'compact' && showTechnical
3784
4449
  ? { offset: Math.min(Math.max(0, focus - 1), Math.max(0, actions.length - 2)), limit: 2 }
@@ -3806,24 +4471,105 @@ export function ConsoleAppV4(props) {
3806
4471
  : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: issueActionViewport.offset, limit: issueActionViewport.limit })] });
3807
4472
  }
3808
4473
  }
4474
+ else if (route.name === 'operationHistory') {
4475
+ const records = snapshot.operationHistorySummaries;
4476
+ breadcrumb = '操作记录';
4477
+ context = `本次会话 · ${records.length} 项`;
4478
+ title = records.length === 0 ? '还没有操作记录' : '最近操作';
4479
+ description = records.length === 0
4480
+ ? '完成操作或遇到问题后,安全摘要会保留在这里。'
4481
+ : '记录只包含安全摘要,不携带可重放的确认票据。';
4482
+ actions = records.length === 0
4483
+ ? [{ label: '返回首页', run: () => requestSessionNavigation('home') }]
4484
+ : [
4485
+ ...records.map((record) => ({
4486
+ label: record.message,
4487
+ description: `${operationHistoryState(record).label} · ${record.diagnosticCount} 项安全诊断`,
4488
+ meta: operationHistoryMeta(record),
4489
+ status: operationHistoryState(record).status,
4490
+ run: () => go({ name: 'operationDiagnostic', recordId: record.recordId }),
4491
+ })),
4492
+ { label: '返回上一页', run: back },
4493
+ ];
4494
+ const viewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(4));
4495
+ 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 })] });
4496
+ }
4497
+ else if (route.name === 'operationDiagnostic') {
4498
+ const detail = props.controller.getOperationDiagnostic(route.recordId);
4499
+ breadcrumb = '操作记录 / 技术详情';
4500
+ context = '只读 · 已脱敏';
4501
+ title = detail ? '安全技术详情' : '操作记录已不可用';
4502
+ description = detail
4503
+ ? '可滚动查看所有目标、阶段、代码和已保留的安全原因。'
4504
+ : '这项会话记录已经不再可用。';
4505
+ const preview = detail ? props.controller.previewOperationReport(route.recordId) : undefined;
4506
+ actions = detail ? [
4507
+ preview
4508
+ ? { label: '预览安全报告', description: '确认内容后选择本地保存位置', run: () => go({ name: 'operationReportPreview', recordId: route.recordId }) }
4509
+ : { label: '报告当前不可用', disabled: '无法生成这项记录的报告' },
4510
+ { label: '返回操作记录', run: back },
4511
+ ] : [{ label: '返回操作记录', run: back }];
4512
+ const detailLineLimit = policy.mode === 'compact' ? 2 : Math.max(1, pageActionBudget(5));
4513
+ 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' })] });
4514
+ }
4515
+ else if (route.name === 'operationReportPreview') {
4516
+ const preview = props.controller.previewOperationReport(route.recordId);
4517
+ breadcrumb = '操作记录 / 报告预览';
4518
+ context = '只读预览';
4519
+ title = reportSaveMode ? '选择报告保存位置' : preview ? '安全报告预览' : '报告已不可用';
4520
+ description = reportSaveMode
4521
+ ? '输入一个新的本地文件路径,然后按 Enter 保存。'
4522
+ : preview
4523
+ ? '保存前请确认报告内容。不会自动上传或发送。'
4524
+ : '这项会话报告已经不再可用。';
4525
+ if (reportSaveMode && preview) {
4526
+ help = 'Enter 保存 · Ctrl-U 清空输入 · Esc 返回报告预览';
4527
+ 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 })] });
4528
+ }
4529
+ else if (preview) {
4530
+ actions = [
4531
+ { label: '选择本地保存位置…', description: '保存前输入明确的文件路径', run: () => {
4532
+ setReportSaveMode(true);
4533
+ setReportSaveError(undefined);
4534
+ setReportSaveResult(undefined);
4535
+ } },
4536
+ { label: '保存到临时目录', description: '使用唯一的临时文件名保存当前安全报告', run: () => {
4537
+ void props.controller.saveOperationReport(route.recordId).then(() => {
4538
+ setReportSaveResult('报告已保存到临时目录。');
4539
+ setReportSaveError(undefined);
4540
+ }).catch(() => {
4541
+ setReportSaveError('报告未保存。请检查保存位置后重试。');
4542
+ });
4543
+ } },
4544
+ { label: '返回技术详情', run: back },
4545
+ ];
4546
+ const reportLineLimit = policy.mode === 'compact' ? 2 : Math.max(1, pageActionBudget(5));
4547
+ const reportActionViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(5));
4548
+ 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 })] });
4549
+ }
4550
+ else {
4551
+ actions = [{ label: '返回技术详情', run: back }];
4552
+ 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' })] });
4553
+ }
4554
+ }
3809
4555
  else if (route.name === 'reviewPreparing' && snapshot.reviewPreparation) {
3810
4556
  const preparation = snapshot.reviewPreparation;
3811
- const labels = preparation.intent.kind === 'portableInstall'
4557
+ const labels = preparation.operationKind === 'portableInstall'
3812
4558
  ? ['通用 Skills / 安装', '正在准备通用 Skills 安装确认']
3813
- : preparation.intent.kind === 'configSave' || preparation.intent.kind === 'configReset'
4559
+ : preparation.operationKind === 'configSave' || preparation.operationKind === 'configReset'
3814
4560
  ? ['设置', '正在准备设置确认']
3815
- : preparation.intent.kind === 'sourceMutation' || preparation.intent.kind === 'catalogGeneration'
4561
+ : preparation.operationKind === 'sourceMutation' || preparation.operationKind === 'catalogGeneration'
3816
4562
  ? ['插件来源', '正在准备来源变更确认']
3817
4563
  : ['插件操作', '正在准备插件操作确认'];
3818
4564
  breadcrumb = `${labels[0]} / 准备确认`;
3819
4565
  context = '只读';
3820
4566
  title = preparation.state === 'failed' ? '无法准备确认信息' : labels[1];
3821
- const refreshPreparation = preparation.intent.kind === 'pluginWrite'
3822
- && preparation.intent.action === 'refreshAndSync';
4567
+ const refreshPreparation = preparation.operationKind === 'pluginWrite'
4568
+ && preparation.stages.some((stage) => stage.id === 'refresh');
3823
4569
  description = preparation.state === 'failed'
3824
4570
  ? preparation.message ?? '无法准备确认信息。请返回检查选择,或重试。'
3825
4571
  : refreshPreparation
3826
- ? '先刷新相关来源的弱 Active 状态,再读取并验证新的同步计划;不会修改插件安装或设置。'
4572
+ ? '先刷新相关来源的当前状态,再读取并验证新的同步计划;不会修改插件安装或设置。'
3827
4573
  : '这个阶段只读取和验证状态,不会修改任何安装或设置。';
3828
4574
  help = preparation.state === 'running'
3829
4575
  ? 'Esc 取消 · Ctrl-C 完成后退出'
@@ -3837,11 +4583,13 @@ export function ConsoleAppV4(props) {
3837
4583
  { label: '返回修改', run: back },
3838
4584
  ] : [];
3839
4585
  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] });
4586
+ 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
4587
+ ? _jsx(Callout, { status: "error", title: "\u786E\u8BA4\u4FE1\u606F\u672A\u51C6\u5907\u5B8C\u6210", description: preparation.message }) : null, showTechnical && preparation.code
4588
+ ? _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
4589
  }
3843
4590
  else if (route.name === 'review' && snapshot.activeReview) {
3844
4591
  const review = snapshot.activeReview;
4592
+ const batchReview = review.operationKind === 'pluginWriteBatch';
3845
4593
  const sharedImpactConfirmed = !review.requiresConfirmation
3846
4594
  || confirmedSharedReviewToken === review.reviewToken;
3847
4595
  breadcrumb = `${review.subject.name} / ${review.title}`;
@@ -3849,17 +4597,21 @@ export function ConsoleAppV4(props) {
3849
4597
  description = snapshot.result?.state === 'stale'
3850
4598
  ? '当前状态已变化。请检查新的变化后再次确认。'
3851
4599
  : review.requiresConfirmation ? '' : review.description;
4600
+ if (batchReview) {
4601
+ context = `${review.batch?.requestedPluginIds.length ?? 0} 个根插件 · ${review.batch?.groupCount ?? 0} 组`;
4602
+ help = 'PgUp/PgDn 查看全部根插件和依赖 · ↑↓ 移动 · Enter 选择 · Esc 返回';
4603
+ }
3852
4604
  actions = [
3853
4605
  ...(review.requiresConfirmation ? [{
3854
- label: sharedImpactConfirmed ? '已确认 shared impact' : '确认 shared impact',
3855
- description: review.sharedImpact.warning ?? '此操作会修改多个插件共享的 Codex native 内容',
4606
+ label: sharedImpactConfirmed ? '已确认共享影响' : '确认共享影响',
4607
+ description: review.sharedImpact.warning ?? '此操作会修改多个插件共同使用的 Codex 平台内容',
3856
4608
  selected: sharedImpactConfirmed,
3857
4609
  run: () => {
3858
4610
  setConfirmedSharedReviewToken(review.reviewToken);
3859
4611
  setFocus(1);
3860
4612
  },
3861
4613
  }] : []),
3862
- { label: review.primaryLabel, description: '执行前将再次检查当前状态', status: review.destructive ? 'error' : undefined, disabled: sharedImpactConfirmed ? undefined : '请先显式确认 shared impact', run: () => {
4614
+ { label: review.primaryLabel, description: '执行前将再次检查当前状态', status: review.destructive ? 'error' : undefined, disabled: sharedImpactConfirmed ? undefined : '请先确认共享影响', run: () => {
3863
4615
  if (operationBusy.current)
3864
4616
  return;
3865
4617
  operationBusy.current = true;
@@ -3870,6 +4622,23 @@ export function ConsoleAppV4(props) {
3870
4622
  operationBusy.current = false;
3871
4623
  });
3872
4624
  } },
4625
+ ...(sharedImpactConfirmed && (pendingReviewIntent?.kind === 'pluginWrite'
4626
+ && (pendingReviewIntent.action === 'install'
4627
+ || pendingReviewIntent.action === 'refreshAndSync'
4628
+ || pendingReviewIntent.action === 'reinstall'
4629
+ || pendingReviewIntent.action === 'sync')
4630
+ || pendingReviewIntent?.kind === 'pluginWriteBatch'
4631
+ && (pendingReviewIntent.action === 'install'
4632
+ || pendingReviewIntent.action === 'sync'
4633
+ || pendingReviewIntent.action === 'reinstall')
4634
+ || pendingReviewIntent?.kind === 'configSave' && pendingReviewIntent.apply) ? [{
4635
+ label: '高级选项:脚本超时',
4636
+ description: `${pendingReviewIntent.scriptTimeoutMs ?? SCRIPT_TIMEOUT_DEFAULT_MS} 毫秒`,
4637
+ run: () => {
4638
+ back();
4639
+ go({ name: 'scriptTimeout', owner: review.reviewToken });
4640
+ },
4641
+ }] : []),
3873
4642
  { label: '返回修改', run: review.operationKind === 'portableInstall' && snapshot.portableDraft
3874
4643
  ? async () => {
3875
4644
  if (await refreshPortableDraft(snapshot.portableDraft.pluginId))
@@ -3882,13 +4651,13 @@ export function ConsoleAppV4(props) {
3882
4651
  const groupTitle = {
3883
4652
  requested: review.operationKind === 'portableInstall' ? '已选择 Skills · 当前插件' : '你选择的插件',
3884
4653
  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',
4654
+ projectIntent: '当前项目使用设置',
4655
+ userIntent: '所有项目使用设置',
4656
+ sharedCodexNative: 'Codex 平台共享内容',
4657
+ userActivation: '所有项目启用位置',
4658
+ projectActivation: '当前项目启用位置',
4659
+ awExtensions: 'AW 管理附加能力',
4660
+ runtime: '执行设置',
3892
4661
  portableTarget: '安装目标',
3893
4662
  portableInputs: '准备内容',
3894
4663
  portableWarnings: '覆盖冲突',
@@ -3934,7 +4703,7 @@ export function ConsoleAppV4(props) {
3934
4703
  allocations.set(group.key, current + added);
3935
4704
  remainingRows -= added;
3936
4705
  }
3937
- const changeGroups = visibleSpecs.map((group) => {
4706
+ const standardChangeGroups = visibleSpecs.map((group) => {
3938
4707
  const visibleCount = allocations.get(group.key) ?? 1;
3939
4708
  const hiddenCount = group.changes.length - visibleCount;
3940
4709
  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 +4721,23 @@ export function ConsoleAppV4(props) {
3952
4721
  return _jsx(ReviewChangeRow, { title: `${category}${reviewSymbol(change)} ${change.title}${description ? ` · ${description}` : ''}`, meta: count, status: changeStatus(change), compact: true }, `${change.title}:${index}`);
3953
4722
  })] }, group.key);
3954
4723
  });
4724
+ const batchChangeLimit = Math.max(1, responsiveBudget(6, 10) - actions.length);
4725
+ const batchChanges = review.changes.slice(batchTargetOffset, batchTargetOffset + batchChangeLimit);
4726
+ const batchHiddenBefore = batchTargetOffset;
4727
+ const prioritizedReviewChanges = [
4728
+ ...review.changes.filter((change) => change.kind === 'warning'),
4729
+ ...review.changes.filter((change) => change.kind !== 'warning'),
4730
+ ];
4731
+ const pagedReviewChanges = prioritizedReviewChanges.slice(batchTargetOffset, batchTargetOffset + batchChangeLimit);
4732
+ const changeGroups = batchReview || review.changes.length > staticRowBudget
4733
+ ? _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] })
4734
+ : standardChangeGroups;
3955
4735
  const technicalDetails = review.technicalDetails ?? [];
3956
4736
  const technicalDetailLimit = responsiveBudget(1, 3);
3957
- const visibleTechnicalDetails = technicalDetails.slice(0, technicalDetailLimit);
3958
- const hiddenTechnicalDetails = technicalDetails.length - visibleTechnicalDetails.length;
4737
+ const visibleTechnicalDetails = technicalDetails.slice(batchTargetOffset, batchTargetOffset + technicalDetailLimit);
4738
+ const hiddenTechnicalDetails = Math.max(0, technicalDetails.length - batchTargetOffset - visibleTechnicalDetails.length);
3959
4739
  const technicalCardRows = 3 + visibleTechnicalDetails.length;
3960
- const technicalChangeBudget = Math.max(1, size.rows - 6 - 3 - (review.equivalentCommand ? 1 : 0) - actions.length - technicalCardRows);
4740
+ const technicalChangeBudget = Math.max(1, size.rows - 8 - 3 - (review.equivalentCommand ? 1 : 0) - actions.length - technicalCardRows);
3961
4741
  const prioritizedTechnicalChanges = [
3962
4742
  ...review.changes.filter((change) => change.kind === 'warning'),
3963
4743
  ...review.changes.filter((change) => change.kind !== 'warning'
@@ -3968,15 +4748,19 @@ export function ConsoleAppV4(props) {
3968
4748
  && change.group !== 'portableTarget'
3969
4749
  && (change.group === undefined || !technicalGroups.has(change.group))),
3970
4750
  ];
3971
- const visibleTechnicalChanges = prioritizedTechnicalChanges.slice(0, technicalChangeBudget);
3972
- const hiddenTechnicalChanges = review.changes.length - visibleTechnicalChanges.length;
4751
+ const visibleTechnicalChanges = prioritizedTechnicalChanges.slice(batchTargetOffset, batchTargetOffset + technicalChangeBudget);
4752
+ const hiddenTechnicalChanges = Math.max(0, review.changes.length - batchTargetOffset - visibleTechnicalChanges.length);
3973
4753
  const technicalChanges = visibleTechnicalChanges.map((change, index) => {
3974
4754
  const category = change.group ? groupTitle[change.group] : undefined;
3975
4755
  return _jsx(ReviewChangeRow, { title: `${category ? `${category} · ` : ''}${reviewSymbol(change)} ${change.title}${change.description ? ` · ${change.description}` : ''}`, meta: index === visibleTechnicalChanges.length - 1 && hiddenTechnicalChanges > 0
3976
4756
  ? `另有 ${hiddenTechnicalChanges} 项` : undefined, status: changeStatus(change), compact: true }, `${change.title}:${index}`);
3977
4757
  });
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] }) });
4758
+ const showEquivalentCommand = Boolean(review.equivalentCommand && showTechnical && !review.requiresConfirmation);
4759
+ if (review.changes.length > staticRowBudget || showTechnical
4760
+ && (review.changes.length > technicalChangeBudget || technicalDetails.length > technicalDetailLimit)) {
4761
+ help = 'PgUp/PgDn 查看全部确认信息 · ↑↓ 移动 · Enter 选择 · Esc 返回';
4762
+ }
4763
+ 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
4764
  }
3981
4765
  else if (route.name === 'progress') {
3982
4766
  const portableOperation = snapshot.activeReview?.operationKind === 'portableInstall';
@@ -3992,18 +4776,18 @@ export function ConsoleAppV4(props) {
3992
4776
  const catalogGenerationOperation = snapshot.activeReview?.operationKind === 'catalogGeneration';
3993
4777
  const catalogSourcesRecovery = snapshot.activeReview?.operationKind === 'catalogSourcesRecovery';
3994
4778
  title = portableOperation ? '正在安装通用 Skills' : issueOperation ? '正在处理问题'
3995
- : catalogGenerationOperation ? '正在验证并启用 Catalog'
3996
- : catalogSourcesRecovery ? '正在恢复 Catalog 来源配置'
4779
+ : catalogGenerationOperation ? '正在验证并启用目录版本'
4780
+ : catalogSourcesRecovery ? '正在恢复插件来源配置'
3997
4781
  : sourceOperation ? '正在保存来源配置'
3998
4782
  : configOperation ? configApply ? '正在保存并应用设置' : '正在保存设置'
3999
4783
  : '正在执行插件操作';
4000
- description = issueOperation ? '完成后会使用新的状态 generation 重新检查问题。'
4001
- : catalogGenerationOperation ? '正在验证并应用明确确认的 Catalog 操作。'
4784
+ description = issueOperation ? '完成后会重新读取状态并检查问题。'
4785
+ : catalogGenerationOperation ? '正在验证并应用已确认的目录版本操作。'
4002
4786
  : catalogSourcesRecovery ? '正在备份损坏配置并恢复官方默认来源。'
4003
4787
  : sourceOperation ? '只原子保存来源配置,不访问网络。'
4004
4788
  : configOperation ? configApply ? '先保存所选层,再重新应用当前可见安装位置。' : '只保存所选设置层。'
4005
4789
  : '请勿关闭当前终端。';
4006
- help = 'Ctrl-C 完成后退出';
4790
+ const batchProgress = snapshot.activeReview?.operationKind === 'pluginWriteBatch';
4007
4791
  const progress = snapshot.progress;
4008
4792
  const elapsedSeconds = Math.max(0, Math.floor(((progress.elapsedMs
4009
4793
  ?? (progress.startedAtMs === undefined ? 0 : Date.now() - progress.startedAtMs))) / 1_000));
@@ -4053,8 +4837,11 @@ export function ConsoleAppV4(props) {
4053
4837
  : 0;
4054
4838
  const progressTargetBudget = progressTargetAndMessageBudget - dependencyMessageLimit;
4055
4839
  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;
4840
+ help = prioritizedProgressTargets.length > progressTargetLimit
4841
+ ? 'PgUp/PgDn 查看全部目标 · Ctrl-C 完成后退出' : 'Ctrl-C 完成后退出';
4842
+ const progressTargetOffset = Math.min(batchTargetOffset, Math.max(0, prioritizedProgressTargets.length - 1));
4843
+ const visibleProgressTargets = prioritizedProgressTargets.slice(progressTargetOffset, progressTargetOffset + progressTargetLimit);
4844
+ const hiddenProgressTargets = Math.max(0, snapshot.progressTargets.length - progressTargetOffset - visibleProgressTargets.length);
4058
4845
  const targetStates = snapshot.progressTargets.map((entry) => entry.state);
4059
4846
  const executionComplete = targetStates.length > 0
4060
4847
  && targetStates.every((state) => state === 'completed' || state === 'kept');
@@ -4102,13 +4889,13 @@ export function ConsoleAppV4(props) {
4102
4889
  return step === 2 ? '正在安装' : step === 3 ? '正在验证' : step === 1 ? '正在生成' : '正在检查';
4103
4890
  };
4104
4891
  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) => {
4892
+ 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
4893
  const state = progressLabel(entry);
4107
4894
  const relation = entry.relation === 'requested' ? '你选择的插件' : entry.relation === 'direct' ? '直接依赖' : '间接依赖';
4108
4895
  const title = props.controller.getPluginOverview(entry.pluginId)?.displayName
4109
4896
  ?? entry.pluginId.split('/').at(-1);
4110
4897
  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
4898
+ }), 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
4899
  ? dependencyMessageLimit > 0 ? technicalMessages.slice(-dependencyMessageLimit) : []
4113
4900
  : 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
4901
  }
@@ -4122,6 +4909,7 @@ export function ConsoleAppV4(props) {
4122
4909
  : result.state === 'blocked' ? result.summary : undefined;
4123
4910
  const action = resultSummary?.action;
4124
4911
  const operationKind = 'operationKind' in result ? result.operationKind : undefined;
4912
+ const batchOperation = operationKind === 'pluginWriteBatch';
4125
4913
  const portableOperation = operationKind === 'portableInstall';
4126
4914
  const portableReceipt = result.state === 'succeeded' || result.state === 'partial'
4127
4915
  ? result.summary.portable : undefined;
@@ -4144,68 +4932,79 @@ export function ConsoleAppV4(props) {
4144
4932
  cleanup: '✓ 插件清理完成',
4145
4933
  sync: '✓ 插件同步完成',
4146
4934
  }[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
4935
+ title = batchOperation
4936
+ ? blocked ? '× 批量操作没有完成'
4937
+ : partial ? '! 批量操作只完成了一部分' : `✓ 批量${batchActionLabel(action)}完成`
4938
+ : portableOperation
4939
+ ? blocked ? '× 通用 Skills 安装被阻止'
4940
+ : partial ? '! 通用 Skills 结果未完全完成' : '✓ 通用 Skills 已安装'
4941
+ : blocked && result.writeMayHaveCompleted
4942
+ ? '× 写入结果未确认'
4943
+ : issueOperation
4944
+ ? blocked || partial ? '× 问题仍未解决' : '✓ 问题已处理'
4945
+ : sourceOperation
4946
+ ? blocked ? '× 来源操作未完成'
4947
+ : operationKind === 'catalogSourcesRecovery' ? '✓ Catalog 来源配置已恢复'
4948
+ : resultSummary?.catalogAction === 'update'
4949
+ ? resultSummary.catalogGeneration?.activeChanged
4950
+ && resultSummary.catalogGeneration.outcome === 'activated'
4951
+ ? '✓ Catalog 已更新' : '✓ Catalog 无需更新'
4952
+ : resultSummary?.catalogAction === 'repair' ? '✓ Catalog 已修复'
4953
+ : resultSummary?.catalogAction === 'replaceDamaged' ? '✓ 损坏的 Catalog 已替换'
4954
+ : resultSummary?.catalogAction === 'recoverState' ? '✓ Catalog 状态已恢复'
4955
+ : operationKind === 'catalogGeneration' ? '✓ Catalog 已启用'
4956
+ : '✓ 来源配置已保存'
4957
+ : configOperation
4958
+ ? blocked
4959
+ ? '× 设置未确认保存'
4960
+ : partial ? resultSummary?.configApplySkippedReason === 'invalidConfig'
4961
+ ? '! 设置已保存,但未应用'
4962
+ : '! 设置已保存,但部分应用失败'
4963
+ : resultSummary?.configApplied ? '✓ 设置已保存并应用' : '✓ 设置已保存'
4964
+ : partial ? '! 插件操作只完成了一部分'
4965
+ : blocked ? result.code === 'operationFailed' ? '× 插件操作没有完成' : '× 插件操作未完成'
4966
+ : completedTitle;
4967
+ description = batchOperation
4177
4968
  ? 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
4969
+ ? '批量写入被阻止。已完成和未完成目标按实际结果保留。'
4970
+ : partial
4971
+ ? (resultSummary?.batch?.retryTargets?.length ?? 0) > 0
4972
+ ? `${resultSummary?.batch?.retryTargets?.length ?? 0} 个根目标失败。成功目标不会重放。`
4973
+ : '失败目标无法精确定位。请查看本次记录后重新操作。'
4974
+ : `${resultSummary?.batch?.requestedPluginIds.length ?? 0} 个根插件已完成。`
4975
+ : portableOperation
4976
+ ? blocked
4977
+ ? result.code === 'operationReviewExpired' || result.code === 'reviewTicketExpired'
4978
+ ? 'Review 已过期,写入未开始。请返回修改并重新确认。'
4979
+ : result.code === 'operationReviewStale'
4980
+ ? '安装证据已变化,写入被阻止。请返回修改并重新确认。'
4981
+ : result.writeMayHaveCompleted
4982
+ ? '写入结果无法确认。请检查外部 Skills 状态后重试。'
4983
+ : '安装在写入前被阻止。请返回修改后重试。'
4984
+ : portableInstallFailed ? 'Skills CLI 安装失败,可能已有部分目标写入。可以修改 Agent 或使用原选择重试。' : partial ? '无法确认全部已选择 Skills。' : '所选 Skills 已通过安装验证。'
4985
+ : blocked && result.writeMayHaveCompleted
4986
+ ? result.message
4987
+ : issueOperation
4197
4988
  ? 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
- : '状态已刷新。';
4989
+ ? result.code === 'issueVerificationFailed'
4990
+ ? '写入可能已完成,但当前无法确认。请重新检查状态。'
4991
+ : result.code === 'issueStillPresent'
4992
+ ? '写入已完成,但重新检查后原问题仍然存在。'
4993
+ : '操作被安全阻止。可返回问题详情后重试。'
4994
+ : '状态已重新加载;原问题已确认消失。'
4995
+ : sourceOperation
4996
+ ? blocked
4997
+ ? result.writeMayHaveCompleted
4998
+ ? '来源配置可能已保存。请重新检查来源列表。'
4999
+ : '来源操作未执行。草稿已保留,可返回修改。'
5000
+ : partial ? resultSummary?.message ?? '来源配置已保存,但 Catalog 尚未启用。可立即重试。'
5001
+ : resultSummary?.message ?? '来源配置已保存。'
5002
+ : blocked ? result.message
5003
+ : configOperation
5004
+ ? partial
5005
+ ? resultSummary?.message ?? '设置层已经保存;平台应用失败不会回滚设置。'
5006
+ : '设置层已经更新。'
5007
+ : '状态已刷新。';
4209
5008
  const pluginId = resultSummary?.pluginId
4210
5009
  ?? ('pluginId' in result ? result.pluginId : undefined);
4211
5010
  const configResultPluginId = pluginId ?? ('returnRoute' in result
@@ -4254,7 +5053,8 @@ export function ConsoleAppV4(props) {
4254
5053
  : undefined;
4255
5054
  const retryAction = failedPluginId && failedTarget && failedTargetState
4256
5055
  ? selectLifecycleRetryAction({
4257
- operationKind: operationKind === 'catalogSourcesRecovery' ? undefined : operationKind,
5056
+ operationKind: operationKind === 'catalogSourcesRecovery' || operationKind === 'pluginWriteBatch'
5057
+ ? undefined : operationKind,
4258
5058
  originalAction: action,
4259
5059
  target: {
4260
5060
  pluginId: failedPluginId,
@@ -4274,131 +5074,146 @@ export function ConsoleAppV4(props) {
4274
5074
  && (candidate.platform ?? failedIssue.platform) === failedTarget?.platform
4275
5075
  && (candidate.scope ?? failedIssue.scope) === failedTarget?.scope)
4276
5076
  : 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) {
5077
+ actions = batchOperation
5078
+ ? [
5079
+ ...((partial || blocked) && (resultSummary?.batch?.retryTargets?.length ?? 0) > 0 ? [{
5080
+ label: '仅重试失败目标…',
5081
+ description: '重新读取失败根目标及其依赖,并进入新的确认页',
5082
+ run: () => {
5083
+ props.controller.retryFailedPluginWriteBatch();
5084
+ setFocus(0);
5085
+ setScrollOffset(0);
5086
+ },
5087
+ }] : []),
5088
+ { label: '返回我的插件', run: () => leaveResult() },
5089
+ { label: '查看本次记录', run: () => result.operationRecordId
5090
+ && go({ name: 'operationDiagnostic', recordId: result.operationRecordId }) },
5091
+ ]
5092
+ : portableOperation
5093
+ ? blocked || partial ? [
5094
+ { label: blocked || partial ? '返回修改' : portableOriginIsDetail ? '返回插件详情' : '返回插件预览', run: async () => {
5095
+ if (!pluginId) {
5096
+ leaveResult();
5097
+ return;
5098
+ }
5099
+ if (!blocked && !partial) {
5100
+ props.controller.discardPortableDraft();
5101
+ leaveResult();
5102
+ return;
5103
+ }
5104
+ if (!await refreshPortableDraft(pluginId))
5105
+ return;
5106
+ if (portableInstallFailed && portableReceipt?.agentMode === 'explicit') {
5107
+ props.controller.resetPortableFlowToPreview(pluginId, { name: 'portableAgentInput', pluginId }, true);
5108
+ return;
5109
+ }
5110
+ props.controller.resetPortableFlowToPreview(pluginId, { name: 'portableSkillSelect', pluginId }, true);
5111
+ } },
5112
+ { label: blocked || partial ? '重试' : '再安装其他 Skills…', run: async () => {
5113
+ if (!pluginId)
5114
+ return;
5115
+ if (blocked || partial) {
5116
+ props.controller.resetPortableFlowToPreview(pluginId, { name: 'portableSkillSelect', pluginId }, true);
5117
+ await beginPortableReview(pluginId, snapshot.portableDraft?.scope ?? 'project');
5118
+ return;
5119
+ }
5120
+ props.controller.resetPortableFlowToPreview(pluginId);
5121
+ await beginPortable(pluginId);
5122
+ } },
5123
+ { label: portableOriginIsDetail ? '返回插件详情' : '返回插件预览', run: () => { if (pluginId)
5124
+ props.controller.resetPortableFlowToPreview(pluginId); } },
5125
+ ] : [
5126
+ { label: portableOriginIsDetail ? '返回插件详情' : '返回插件预览', run: () => {
4285
5127
  props.controller.discardPortableDraft();
4286
5128
  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
5129
+ } },
5130
+ { label: '再安装其他 Skills…', run: async () => {
5131
+ if (!pluginId)
5132
+ return;
5133
+ props.controller.resetPortableFlowToPreview(pluginId);
5134
+ await beginPortable(pluginId);
5135
+ } },
5136
+ ]
5137
+ : issueOperation && !dependencyRecovery
5138
+ ? blocked || partial
4341
5139
  ? [
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' }) },
5140
+ { label: '返回问题详情', run: () => blocked
5141
+ ? leaveResult()
5142
+ : failedIssue
5143
+ ? leaveResult({ name: 'issueDetail', issueId: failedIssue.issueId })
5144
+ : pluginId && failedTarget
5145
+ ? leaveResult({ name: 'platform', pluginId, platform: failedTarget.platform })
5146
+ : leaveResult() },
5147
+ { label: '重新检查问题', run: () => leaveResult({ name: 'issueList' }) },
5148
+ ]
5149
+ : [
5150
+ { label: '查看问题列表', run: () => leaveResult({ name: 'issueList' }) },
5151
+ { label: '返回首页', run: () => requestSessionNavigation('home') },
4356
5152
  ]
4357
- : configOperation && blocked
5153
+ : sourceOperation
5154
+ ? [{ label: blocked ? '返回修改' : partial ? '现在启用 / 重试' : '查看来源', run: () => blocked ? props.controller.resumeResultDraft() : leaveResult() }, { label: '返回首页', run: () => requestSessionNavigation('home') }]
5155
+ : configOperation && partial
4358
5156
  ? [
4359
- { label: '返回设置', run: () => leaveResult() },
4360
- { label: '返回首页', run: () => leaveResult({ name: 'home' }) },
5157
+ ...(configRetryAvailable ? [{
5158
+ label: '重试应用',
5159
+ description: '先重新进入 Review,并重新采集当前证据',
5160
+ run: () => beginReview({
5161
+ kind: 'configSave',
5162
+ pluginId: configResultPluginId,
5163
+ scope: resultSummary.configScope,
5164
+ apply: true,
5165
+ retryApply: true,
5166
+ }),
5167
+ }] : []),
5168
+ { label: invalidConfigResult ? '返回设置' : '返回修改', description: '重新打开已保存的设置层', run: () => leaveResult() },
5169
+ { label: '返回设置删除字段或重置设置层', description: '显式删除顶层字段或整个设置层', run: () => leaveResult() },
5170
+ { label: '保留设置并退出', run: () => requestExit({ code: 1, reason: 'partial' }) },
4361
5171
  ]
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 });
5172
+ : configOperation && blocked
5173
+ ? [
5174
+ { label: '返回设置', run: () => props.controller.resumeResultDraft() },
5175
+ { label: '返回首页', run: () => requestSessionNavigation('home') },
5176
+ ]
5177
+ : [{ label: partial || blockedDependencyFailure ? '查看失败项' : successfulConfigOperation && configOrigin
5178
+ ? configOrigin?.name === 'pluginPreview' ? '返回插件预览' : '返回插件详情'
5179
+ : configOperation ? '查看插件'
5180
+ : action === 'uninstall' || action === 'cleanup' ? '返回插件详情' : '查看插件', run: () => {
5181
+ if (successfulConfigOperation && configOrigin) {
5182
+ leaveResult();
5183
+ return;
5184
+ }
5185
+ if ((partial || blockedDependencyFailure) && failedPluginId && failedTarget) {
5186
+ if (failedIssue)
5187
+ leaveResult({ name: 'issueDetail', issueId: failedIssue.issueId });
5188
+ else if (failedPluginPlatforms?.includes(failedTarget.platform)) {
5189
+ leaveResult({ name: 'platform', pluginId: failedPluginId, platform: failedTarget.platform });
5190
+ }
5191
+ else if (snapshot.viewModel?.plugins.some((entry) => entry.pluginId === failedPluginId)) {
5192
+ leaveResult({ name: 'pluginDetail', pluginId: failedPluginId });
5193
+ }
5194
+ else
5195
+ leaveResult({ name: 'pluginList', mode: 'installed' });
4375
5196
  }
4376
- else if (snapshot.viewModel?.plugins.some((entry) => entry.pluginId === failedPluginId)) {
4377
- leaveResult({ name: 'pluginDetail', pluginId: failedPluginId });
5197
+ else if (pluginId && snapshot.viewModel?.plugins.some((entry) => entry.pluginId === pluginId)) {
5198
+ leaveResult({ name: 'pluginDetail', pluginId });
4378
5199
  }
4379
5200
  else
4380
5201
  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' }) }];
5202
+ } }, ...((partial || blockedDependencyFailure) && failedPluginId && failedTarget && retryAction ? [{
5203
+ label: '重试失败部分…',
5204
+ run: () => {
5205
+ const issueIntent = failedIssue && retryIssueAction
5206
+ ? issueWriteIntent(failedIssue, retryIssueAction)
5207
+ : undefined;
5208
+ return beginReview(issueIntent ?? {
5209
+ kind: 'pluginWrite',
5210
+ action: retryAction,
5211
+ pluginId: failedPluginId,
5212
+ platform: failedTarget.platform,
5213
+ scope: failedTarget.scope,
5214
+ });
5215
+ },
5216
+ }] : []), { label: '返回首页', run: () => requestSessionNavigation('home') }];
4402
5217
  const visibleMessages = resultSummary?.messages?.slice(policy.mode === 'compact' ? -2 : -4) ?? [];
4403
5218
  const visibleChanges = resultSummary?.changes?.slice(0, policy.mode === 'compact' ? 2 : 4) ?? [];
4404
5219
  const resultTargetName = (target) => target.pluginId
@@ -4419,17 +5234,19 @@ export function ConsoleAppV4(props) {
4419
5234
  + visibleChanges.length
4420
5235
  + (partial && hasPreservedTarget && resultTargets.some((target) => target.state === 'skipped') ? 1 : 0);
4421
5236
  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;
5237
+ const resultTargetOffset = Math.min(batchTargetOffset, Math.max(0, prioritizedTargets.length - 1));
5238
+ const visibleResultTargets = prioritizedTargets.slice(resultTargetOffset, resultTargetOffset + resultTargetLimit);
5239
+ const hiddenResultTargets = Math.max(0, resultTargets.length - resultTargetOffset - visibleResultTargets.length);
4424
5240
  const portableMissingSkills = new Set(portableReceipt?.missing.map((entry) => entry.skill) ?? []);
4425
5241
  const portableSelectedSkills = portableReceipt?.selectedSkills
4426
- ?? snapshot.portableDraft?.candidates.filter((candidate) => candidate.selected).map((candidate) => candidate.name)
5242
+ ?? snapshot.portableResultSkills
4427
5243
  ?? [];
4428
5244
  const prioritizedPortableSkills = [...portableSelectedSkills].sort((left, right) => Number(portableMissingSkills.has(right)) - Number(portableMissingSkills.has(left)));
4429
5245
  const portableTechnicalSummary = showTechnical && (partial || blocked);
4430
5246
  const portableSkillLimit = showTechnical ? 1 : responsiveBudget(2, 6);
4431
- const visiblePortableSkills = prioritizedPortableSkills.slice(0, portableSkillLimit);
4432
- const hiddenPortableSkills = prioritizedPortableSkills.length - visiblePortableSkills.length;
5247
+ const portableSkillOffset = Math.min(batchTargetOffset, Math.max(0, prioritizedPortableSkills.length - 1));
5248
+ const visiblePortableSkills = prioritizedPortableSkills.slice(portableSkillOffset, portableSkillOffset + portableSkillLimit);
5249
+ const hiddenPortableSkills = Math.max(0, prioritizedPortableSkills.length - portableSkillOffset - visiblePortableSkills.length);
4433
5250
  const portableRows = visiblePortableSkills.map((skill) => {
4434
5251
  const missing = portableMissingSkills.has(skill);
4435
5252
  const status = blocked || portableReceipt?.verificationUnknown ? 'warning'
@@ -4457,11 +5274,15 @@ export function ConsoleAppV4(props) {
4457
5274
  : resultSummary?.resetDisposition === 'quarantinedDirectory'
4458
5275
  ? `设置路径是目录;已隔离为 ${resultSummary.quarantineBasename ?? '安全隔离目录'}`
4459
5276
  : 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'
5277
+ if (batchOperation || resultTargets.length > resultTargetLimit
5278
+ || prioritizedPortableSkills.length > portableSkillLimit) {
5279
+ help = 'PgUp/PgDn 查看全部项目 · ↑↓ 移动 · Enter 选择 · Esc 返回';
5280
+ }
5281
+ 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
5282
  ? _jsx(StaticRow, { title: "\u5B89\u5168\u6062\u590D", meta: portableReceipt.recovery, status: "warning", compact: true })
4462
5283
  : _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
5284
  ? _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 })] });
5285
+ : _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
5286
  }
4466
5287
  else if (route.name === 'pluginAbout' && plugin) {
4467
5288
  breadcrumb = `${plugin.displayName} / 关于`;
@@ -4487,17 +5308,89 @@ export function ConsoleAppV4(props) {
4487
5308
  const capabilityRowCount = plugin.capabilityUnsupported ? 4 : 0;
4488
5309
  const componentRowBudget = Math.max(1, detailRowBudget - 2 - plugin.installedPlacements.length - capabilityRowCount);
4489
5310
  const componentLimit = prioritizedComponents.length > componentRowBudget
4490
- ? Math.max(1, componentRowBudget - 1)
5311
+ ? Math.max(1, componentRowBudget - 2)
4491
5312
  : 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' })] });
5313
+ const componentOffset = Math.min(batchTargetOffset, Math.max(0, prioritizedComponents.length - 1));
5314
+ const visibleComponents = prioritizedComponents.slice(componentOffset, componentOffset + componentLimit);
5315
+ const hiddenComponents = Math.max(0, prioritizedComponents.length - componentOffset - visibleComponents.length);
5316
+ if (prioritizedComponents.length > componentLimit) {
5317
+ help = 'PgUp/PgDn 查看全部组件 · ↑↓ 移动 · Enter 选择 · Esc 返回';
5318
+ }
5319
+ 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
5320
  }
4496
5321
  else {
4497
- breadcrumb = '后续阶段';
4498
- title = '此业务页将在后续阶段接入';
4499
- description = '当前阶段只实现插件发现、安装和维护页面。';
4500
- actions = [{ label: '返回上一页', run: back }];
5322
+ const missingPlugin = definition.entity === 'plugin' && plugin === undefined;
5323
+ const missingIssue = definition.entity === 'issue' && issue === undefined;
5324
+ const missingConfigField = definition.rendererFamily === 'config'
5325
+ && configRoute !== undefined
5326
+ && configRoute.name !== 'configFields'
5327
+ && configRoute.name !== 'configSave'
5328
+ && currentConfigPage !== undefined
5329
+ && activeField === undefined;
5330
+ let unavailable;
5331
+ switch (route.name) {
5332
+ case 'pluginPreview':
5333
+ case 'pluginDependencies':
5334
+ case 'installPlacement':
5335
+ case 'installTargets':
5336
+ case 'dependencyPlacement':
5337
+ case 'dependencyPlacementEditor':
5338
+ case 'pluginDetail':
5339
+ case 'platform':
5340
+ case 'pluginAbout':
5341
+ case 'pluginTechnical':
5342
+ case 'configLayer':
5343
+ unavailable = missingPlugin ? 'plugin' : 'flow';
5344
+ break;
5345
+ case 'portableSkillSelect':
5346
+ case 'portableDependencies':
5347
+ case 'portableAgentMode':
5348
+ case 'portableAgentInput':
5349
+ case 'portableScope':
5350
+ case 'portableBlocker':
5351
+ unavailable = missingPlugin ? 'plugin' : 'flow';
5352
+ break;
5353
+ case 'configFields':
5354
+ case 'configSave':
5355
+ case 'configFieldAction':
5356
+ case 'configFieldEditor':
5357
+ case 'configObjectPropertyEditor':
5358
+ unavailable = missingPlugin ? 'plugin' : missingConfigField ? 'config' : 'flow';
5359
+ break;
5360
+ case 'issueDetail':
5361
+ unavailable = missingIssue ? 'issue' : 'flow';
5362
+ break;
5363
+ case 'sourceAdd':
5364
+ case 'sourceSave':
5365
+ case 'scriptTimeout':
5366
+ case 'reviewPreparing':
5367
+ case 'review':
5368
+ case 'result':
5369
+ unavailable = 'flow';
5370
+ break;
5371
+ case 'home':
5372
+ throw new Error(`TUI renderer invariant failed for ${route.name}`);
5373
+ default:
5374
+ unreachableTuiRoute(route);
5375
+ }
5376
+ breadcrumb = unavailable === 'plugin' ? '插件不可用'
5377
+ : unavailable === 'issue' ? '问题不可用'
5378
+ : unavailable === 'config' ? '设置已变化' : '操作已变化';
5379
+ title = unavailable === 'plugin' ? '这个插件已不在当前目录中'
5380
+ : unavailable === 'issue' ? '这个问题已经变化'
5381
+ : unavailable === 'config' ? '这个设置字段已不再可用' : '这个操作已结束或变化';
5382
+ description = '状态已变化。请返回列表并重新选择。';
5383
+ actions = [{
5384
+ label: unavailable === 'plugin' ? '返回插件列表'
5385
+ : unavailable === 'issue' ? '返回问题列表'
5386
+ : unavailable === 'config' ? '返回设置列表' : '返回首页',
5387
+ run: () => unavailable === 'flow'
5388
+ ? requestSessionNavigation('home')
5389
+ : go(unavailable === 'plugin'
5390
+ ? { name: 'pluginList', mode: 'installed' }
5391
+ : unavailable === 'issue' ? { name: 'issueList' }
5392
+ : { name: 'configFields', pluginId: configRoute.pluginId, layer: configRoute.layer }),
5393
+ }];
4501
5394
  content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' });
4502
5395
  }
4503
5396
  if (canOpenHelp && supportsTechnicalDetails) {
@@ -4508,6 +5401,11 @@ export function ConsoleAppV4(props) {
4508
5401
  else if (canOpenHelp && !help.includes('? 帮助')) {
4509
5402
  help = `? 帮助 · ${help}`;
4510
5403
  }
5404
+ if (props.onSessionNavigation && !isCharacterInputMode(route, searchFocus)) {
5405
+ help = busy
5406
+ ? `h/H 完成后导航 · Esc 撤销等待 · ${help}`
5407
+ : `h 首页 · H 重载 · ${help}`;
5408
+ }
4511
5409
  const targetActionSignature = route.name === 'installTargets' && route.view !== 'blocked'
4512
5410
  ? actions.map((action) => String(Boolean(action.disabled))).join(',')
4513
5411
  : '';
@@ -4544,13 +5442,75 @@ export function ConsoleAppV4(props) {
4544
5442
  void pending.catch((error) => {
4545
5443
  if (error instanceof Error && error.name === 'AbortError')
4546
5444
  return;
4547
- setActionError('操作未完成。请重新检查后重试。');
5445
+ showSafeUiFailure(error, 'tuiActionFailed', 'ui-action');
4548
5446
  });
4549
5447
  }
4550
- catch {
4551
- setActionError('操作未完成。请重新检查后重试。');
5448
+ catch (error) {
5449
+ showSafeUiFailure(error, 'tuiActionFailed', 'ui-action');
4552
5450
  }
4553
5451
  };
5452
+ function requestSessionNavigation(navigation, discardDrafts = false) {
5453
+ if (!props.onSessionNavigation) {
5454
+ if (navigation !== 'home')
5455
+ return;
5456
+ if (!props.controller.goHome({ discardDrafts })) {
5457
+ setDiscardDraftConfirmation(props.controller.externalDrafts);
5458
+ setDiscardForSessionNavigation(navigation);
5459
+ setOverlayFocus(0);
5460
+ setOverlay('sessionNavigation');
5461
+ return;
5462
+ }
5463
+ resetRootUiState();
5464
+ if (discardDrafts)
5465
+ clearExternalDraftValues();
5466
+ return;
5467
+ }
5468
+ const draftConfirmation = discardDrafts ? discardDraftConfirmation : undefined;
5469
+ void props.onSessionNavigation(navigation, {
5470
+ discardDrafts,
5471
+ ...(draftConfirmation ? { draftConfirmation } : {}),
5472
+ }).then((result) => {
5473
+ if (result === 'draftBlocked') {
5474
+ setDiscardDraftConfirmation(props.controller.externalDrafts);
5475
+ setDiscardForSessionNavigation(navigation);
5476
+ setOverlayFocus(0);
5477
+ setOverlay('sessionNavigation');
5478
+ return;
5479
+ }
5480
+ if (result === 'completed') {
5481
+ if (navigation === 'home')
5482
+ resetRootUiState();
5483
+ if (discardDrafts)
5484
+ clearExternalDraftValues();
5485
+ setDiscardDraftConfirmation(undefined);
5486
+ }
5487
+ }).catch((error) => showSafeUiFailure(error, 'tuiSessionNavigationFailed', 'session-navigation'));
5488
+ }
5489
+ function clearExternalDraftValues() {
5490
+ setDirty(false);
5491
+ setDraftUpdates({});
5492
+ setDraftRemoves([]);
5493
+ setObjectDrafts({});
5494
+ setChangedObjectDraftKeys([]);
5495
+ setObjectError(undefined);
5496
+ setSourceDraft(undefined);
5497
+ setBatchDraft(undefined);
5498
+ }
5499
+ function continueEditingFromSessionNavigation() {
5500
+ if (props.pendingSessionNavigation)
5501
+ props.onCancelPendingSessionNavigation?.();
5502
+ setOverlay(undefined);
5503
+ setDiscardForSessionNavigation(undefined);
5504
+ setDiscardDraftConfirmation(undefined);
5505
+ }
5506
+ useEffect(() => {
5507
+ if (!props.pendingSessionNavigation || !props.pendingSessionNavigationDrafts)
5508
+ return;
5509
+ setDiscardForSessionNavigation(props.pendingSessionNavigation);
5510
+ setDiscardDraftConfirmation(props.pendingSessionNavigationDrafts);
5511
+ setOverlayFocus(0);
5512
+ setOverlay('sessionNavigation');
5513
+ }, [props.pendingSessionNavigation, props.pendingSessionNavigationDrafts]);
4554
5514
  useInput((input, key) => {
4555
5515
  if (!overlay && route.name === 'reviewPreparing' && snapshot.reviewPreparation?.state === 'failed'
4556
5516
  && (key.escape || input.toLowerCase() === 'b')) {
@@ -4558,7 +5518,11 @@ export function ConsoleAppV4(props) {
4558
5518
  return;
4559
5519
  }
4560
5520
  if (preparationRunning) {
4561
- if (key.escape || input.toLowerCase() === 'b') {
5521
+ const sessionNavigation = resolveTuiSessionNavigationKey({ input, key }, { characterInput: false });
5522
+ if (sessionNavigation) {
5523
+ requestSessionNavigation(sessionNavigation);
5524
+ }
5525
+ else if (key.escape || input.toLowerCase() === 'b') {
4562
5526
  back();
4563
5527
  }
4564
5528
  else if (input.toLowerCase() === 'q' || (key.ctrl && input.toLowerCase() === 'c')) {
@@ -4567,7 +5531,13 @@ export function ConsoleAppV4(props) {
4567
5531
  return;
4568
5532
  }
4569
5533
  if (busy) {
4570
- if (overlay === 'help') {
5534
+ const sessionNavigation = resolveTuiSessionNavigationKey({ input, key }, { characterInput: false });
5535
+ if ((key.pageUp || key.pageDown) && route.name === 'progress') {
5536
+ const total = snapshot.progressTargets.length;
5537
+ const pageRows = 1;
5538
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - 1), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5539
+ }
5540
+ else if (overlay === 'help') {
4571
5541
  if (key.escape || key.return || input.toLowerCase() === 'b' || input === '?') {
4572
5542
  setOverlay(undefined);
4573
5543
  }
@@ -4575,6 +5545,12 @@ export function ConsoleAppV4(props) {
4575
5545
  setExitAfterBusy(true);
4576
5546
  }
4577
5547
  }
5548
+ else if (key.escape && props.pendingSessionNavigation) {
5549
+ props.onCancelPendingSessionNavigation?.();
5550
+ }
5551
+ else if (sessionNavigation) {
5552
+ requestSessionNavigation(sessionNavigation);
5553
+ }
4578
5554
  else if (input.toLowerCase() === 'q' || (key.ctrl && input.toLowerCase() === 'c')) {
4579
5555
  setExitAfterBusy(true);
4580
5556
  }
@@ -4583,7 +5559,7 @@ export function ConsoleAppV4(props) {
4583
5559
  setOverlay('help');
4584
5560
  }
4585
5561
  else if (!tooSmall && supportsTechnicalDetails && input.toLowerCase() === 'd') {
4586
- setShowTechnical((value) => !value);
5562
+ toggleTechnical();
4587
5563
  }
4588
5564
  return;
4589
5565
  }
@@ -4598,25 +5574,49 @@ export function ConsoleAppV4(props) {
4598
5574
  requestExit({ code: snapshot.phase === 'error' ? 1 : 0, reason: snapshot.phase === 'error' ? 'initialError' : 'user' });
4599
5575
  return;
4600
5576
  }
4601
- if (key.escape || input.toLowerCase() === 'b' || input === '?') {
5577
+ if (overlay === 'help' && input.toLowerCase() === 'o') {
4602
5578
  setOverlay(undefined);
5579
+ go({ name: 'operationHistory' });
5580
+ return;
5581
+ }
5582
+ if (key.escape || input.toLowerCase() === 'b' || input === '?') {
5583
+ if (overlay === 'sessionNavigation') {
5584
+ continueEditingFromSessionNavigation();
5585
+ }
5586
+ else
5587
+ setOverlay(undefined);
4603
5588
  return;
4604
5589
  }
4605
5590
  if (key.leftArrow || key.rightArrow || key.upArrow || key.downArrow) {
4606
- if (overlay !== 'help')
4607
- setOverlayFocus((value) => value === 0 ? 1 : 0);
5591
+ const count = overlay === 'help' && props.onSessionNavigation ? 3 : overlay === 'help' ? 1 : 2;
5592
+ const direction = key.leftArrow || key.upArrow ? -1 : 1;
5593
+ setOverlayFocus((value) => moveIndex(count, value, direction));
4608
5594
  return;
4609
5595
  }
4610
5596
  if (key.return) {
4611
- if (overlay === 'help' || overlayFocus === 0)
5597
+ if (overlay === 'help') {
4612
5598
  setOverlay(undefined);
5599
+ if (props.onSessionNavigation && overlayFocus === 1)
5600
+ requestSessionNavigation('home');
5601
+ else if (props.onSessionNavigation && overlayFocus === 2)
5602
+ requestSessionNavigation('reload');
5603
+ }
5604
+ else if (overlayFocus === 0) {
5605
+ if (overlay === 'sessionNavigation')
5606
+ continueEditingFromSessionNavigation();
5607
+ else
5608
+ setOverlay(undefined);
5609
+ }
4613
5610
  else if (overlay === 'discard') {
4614
5611
  setOverlay(undefined);
4615
5612
  setDirty(false);
4616
5613
  setDraftUpdates({});
4617
5614
  setDraftRemoves([]);
5615
+ setObjectDrafts({});
5616
+ setChangedObjectDraftKeys([]);
4618
5617
  const discardedSourceDraft = sourceDraft;
4619
5618
  setSourceDraft(undefined);
5619
+ setBatchDraft(undefined);
4620
5620
  if (discardForExit) {
4621
5621
  requestExit({ code: 0, reason: 'user' });
4622
5622
  }
@@ -4629,6 +5629,12 @@ export function ConsoleAppV4(props) {
4629
5629
  back();
4630
5630
  }
4631
5631
  }
5632
+ else if (overlay === 'sessionNavigation' && discardForSessionNavigation) {
5633
+ const navigation = discardForSessionNavigation;
5634
+ setOverlay(undefined);
5635
+ setDiscardForSessionNavigation(undefined);
5636
+ requestSessionNavigation(navigation, true);
5637
+ }
4632
5638
  else
4633
5639
  requestExit({ code: snapshot.phase === 'error' ? 1 : 0, reason: snapshot.phase === 'error' ? 'initialError' : 'user' });
4634
5640
  }
@@ -4638,6 +5644,111 @@ export function ConsoleAppV4(props) {
4638
5644
  requestExit({ code: snapshot.phase === 'error' ? 1 : 0, reason: snapshot.phase === 'error' ? 'initialError' : 'user' });
4639
5645
  return;
4640
5646
  }
5647
+ if (route.name === 'operationReportPreview' && reportSaveMode) {
5648
+ if (key.escape) {
5649
+ setReportSaveMode(false);
5650
+ setReportSaveError(undefined);
5651
+ return;
5652
+ }
5653
+ if (key.return) {
5654
+ const savePath = reportSavePath.trim();
5655
+ if (!savePath) {
5656
+ setReportSaveError('请输入要保存到的本地文件路径。');
5657
+ return;
5658
+ }
5659
+ void props.controller.saveOperationReport(route.recordId, savePath).then(() => {
5660
+ setReportSaveMode(false);
5661
+ setReportSaveResult('报告已保存到你选择的位置。');
5662
+ setReportSaveError(undefined);
5663
+ }).catch(() => {
5664
+ setReportSaveMode(false);
5665
+ setReportSaveError('报告未保存。请检查保存位置后重试。');
5666
+ });
5667
+ return;
5668
+ }
5669
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl)) {
5670
+ setReportSavePath('');
5671
+ setReportSaveError(undefined);
5672
+ return;
5673
+ }
5674
+ if (key.backspace || key.delete)
5675
+ setReportSavePath(removeLastGrapheme);
5676
+ else if (input && !key.ctrl && !key.meta && input >= ' ')
5677
+ setReportSavePath((value) => value + input);
5678
+ setReportSaveError(undefined);
5679
+ return;
5680
+ }
5681
+ if ((key.pageUp || key.pageDown)
5682
+ && route.name === 'review'
5683
+ && snapshot.activeReview) {
5684
+ const total = Math.max(snapshot.activeReview.changes.length, showTechnical ? snapshot.activeReview.technicalDetails?.length ?? 0 : 0);
5685
+ const pageRows = 1;
5686
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - 1), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5687
+ return;
5688
+ }
5689
+ if ((key.pageUp || key.pageDown)
5690
+ && route.name === 'result'
5691
+ && snapshot.result) {
5692
+ const total = 'summary' in snapshot.result
5693
+ ? Math.max(snapshot.result.summary?.targets?.length ?? 0, snapshot.result.operationKind === 'portableInstall'
5694
+ ? snapshot.portableResultSkills?.length ?? 0
5695
+ : 0)
5696
+ : 0;
5697
+ const pageRows = 1;
5698
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - 1), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5699
+ return;
5700
+ }
5701
+ if ((key.pageUp || key.pageDown) && route.name === 'pluginTechnical' && plugin) {
5702
+ const total = plugin.technicalComponents.length;
5703
+ const pageRows = 1;
5704
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - 1), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5705
+ return;
5706
+ }
5707
+ if ((key.pageUp || key.pageDown) && route.name === 'issueDetail' && issue && showTechnical) {
5708
+ const total = diagnosticDetailRowCount(issue.diagnostic);
5709
+ const pageRows = responsiveBudget(5, 10);
5710
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - pageRows), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5711
+ return;
5712
+ }
5713
+ if ((key.pageUp || key.pageDown) && route.name === 'sourceTechnical') {
5714
+ const overview = props.controller.getSourceOverview(route.sourceId);
5715
+ const diagnostic = sourceDiagnostic(route.sourceId, overview.health?.state ?? overview.source?.state, overview.detail, props.controller.getCatalogGeneration(route.sourceId), props.controller.getCatalogFreshness(route.sourceId));
5716
+ const total = diagnostic ? diagnosticDetailRowCount(diagnostic) : 0;
5717
+ const pageRows = responsiveBudget(5, 10);
5718
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - pageRows), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5719
+ return;
5720
+ }
5721
+ if ((key.pageUp || key.pageDown) && route.name === 'sourceRemoveBlocked'
5722
+ && sourceDependenciesExpanded) {
5723
+ const total = sourceReferenceSummary?.dependencies.length ?? 0;
5724
+ const pageRows = Math.max(1, pageActionBudget(10));
5725
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - pageRows), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5726
+ return;
5727
+ }
5728
+ if ((key.pageUp || key.pageDown) && route.name === 'pluginDependencies' && plugin) {
5729
+ const overview = dependencyOverview(plugin);
5730
+ const dependents = route.view === 'removeBlocked' && route.platform && route.scope
5731
+ ? props.controller.getBlockingDependents(plugin.pluginId, route.platform, route.scope)
5732
+ : overview?.dependents ?? [];
5733
+ const total = route.view === 'removeBlocked'
5734
+ ? dependents.length
5735
+ : (overview?.blockers.length ?? 0) + (overview?.dependencies.length ?? 0) + dependents.length;
5736
+ const pageRows = 1;
5737
+ setBatchTargetOffset((value) => Math.min(Math.max(0, total - pageRows), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5738
+ return;
5739
+ }
5740
+ if ((route.name === 'operationDiagnostic' || route.name === 'operationReportPreview')
5741
+ && (key.pageUp || key.pageDown)) {
5742
+ const detail = route.name === 'operationDiagnostic'
5743
+ ? props.controller.getOperationDiagnostic(route.recordId) : undefined;
5744
+ const preview = route.name === 'operationReportPreview'
5745
+ ? props.controller.previewOperationReport(route.recordId) : undefined;
5746
+ const total = detail ? operationDiagnosticLineCount(detail)
5747
+ : preview ? operationReportLineCount(preview) : 0;
5748
+ const pageRows = policy.mode === 'compact' ? 2 : Math.max(1, pageActionBudget(5));
5749
+ setScrollOffset((value) => Math.min(Math.max(0, total - pageRows), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
5750
+ return;
5751
+ }
4641
5752
  if (route.name === 'portableAgentInput') {
4642
5753
  if (key.escape) {
4643
5754
  back();
@@ -4656,6 +5767,11 @@ export function ConsoleAppV4(props) {
4656
5767
  }
4657
5768
  return;
4658
5769
  }
5770
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl)) {
5771
+ setPortableAgentInput('');
5772
+ setInputError(undefined);
5773
+ return;
5774
+ }
4659
5775
  if (key.backspace || key.delete)
4660
5776
  setPortableAgentInput(removeLastGrapheme);
4661
5777
  else if (input && !key.ctrl && !key.meta && input >= ' ')
@@ -4663,14 +5779,43 @@ export function ConsoleAppV4(props) {
4663
5779
  setInputError(undefined);
4664
5780
  return;
4665
5781
  }
5782
+ if (route.name === 'scriptTimeout') {
5783
+ if (key.escape) {
5784
+ const intent = pendingReviewIntentRef.current;
5785
+ back();
5786
+ if (intent)
5787
+ beginReview(intent);
5788
+ return;
5789
+ }
5790
+ const pendingIntent = pendingReviewIntentRef.current;
5791
+ if (key.return && pendingIntent) {
5792
+ try {
5793
+ const scriptTimeoutMs = parseScriptTimeoutMs(scriptTimeoutInput);
5794
+ const intent = { ...pendingIntent, scriptTimeoutMs };
5795
+ setScriptTimeoutError(undefined);
5796
+ back();
5797
+ beginReview(intent);
5798
+ }
5799
+ catch (error) {
5800
+ setScriptTimeoutError(error instanceof Error ? error.message : '脚本超时无效。');
5801
+ }
5802
+ return;
5803
+ }
5804
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl))
5805
+ setScriptTimeoutInput('');
5806
+ else if (key.backspace || key.delete)
5807
+ setScriptTimeoutInput(removeLastGrapheme);
5808
+ else if (input && !key.ctrl && !key.meta && input >= ' ')
5809
+ setScriptTimeoutInput((value) => value + input);
5810
+ setScriptTimeoutError(undefined);
5811
+ return;
5812
+ }
4666
5813
  if (route.name === 'sourceAdd'
4667
5814
  && route.step !== 'type'
4668
5815
  && route.step !== 'refPolicy'
4669
5816
  && sourceDraft?.owner === route.owner) {
4670
5817
  if (key.escape) {
4671
- setOverlayFocus(0);
4672
- setDiscardForExit(false);
4673
- setOverlay('discard');
5818
+ back();
4674
5819
  return;
4675
5820
  }
4676
5821
  if (key.return) {
@@ -4715,35 +5860,35 @@ export function ConsoleAppV4(props) {
4715
5860
  next.sourceId = sourceInput;
4716
5861
  else if (route.step === 'location')
4717
5862
  next.location = sourceInput;
4718
- else if (route.step === 'ref')
5863
+ else if (route.step === 'ref') {
5864
+ next.refPolicy = 'explicit';
4719
5865
  next.ref = sourceInput;
5866
+ }
4720
5867
  else
4721
5868
  next.subdir = sourceInput === '' ? '' : path.posix.normalize(sourceInput).replace(/^\.\//u, '').replace(/\/$/u, '');
4722
5869
  setSourceDraft(next);
5870
+ setDirty(true);
4723
5871
  setSourceInputError(undefined);
4724
- if (route.step === 'id')
5872
+ if (route.returnToDraft)
5873
+ acceptSourceFieldAndBack();
5874
+ else if (route.step === 'id')
4725
5875
  go({ name: 'sourceAdd', step: 'location', owner: route.owner });
4726
5876
  else if (route.step === 'location') {
4727
- if (next.mode === 'update')
4728
- void beginSourceUpdateReview(next).catch(() => setActionError('来源修改未通过验证。'));
4729
- else if (next.type === 'git')
5877
+ if (next.type === 'git')
4730
5878
  go({ name: 'sourceAdd', step: 'refPolicy', owner: route.owner });
4731
5879
  else
4732
- void beginSourceAddReviewWith(next).catch(() => setActionError('来源未通过验证。请检查输入后重试。'));
5880
+ go({ name: 'sourceEdit', sourceId: next.sourceId, owner: route.owner });
4733
5881
  }
4734
5882
  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 });
5883
+ go({ name: 'sourceAdd', step: 'subdir', owner: route.owner });
4739
5884
  }
4740
- else if (next.mode === 'update')
4741
- void beginSourceUpdateReview(next).catch(() => setActionError('来源修改未通过验证。'));
4742
5885
  else
4743
- void beginSourceAddReviewWith(next).catch(() => setActionError('来源未通过验证。请检查输入后重试。'));
5886
+ go({ name: 'sourceEdit', sourceId: next.sourceId, owner: route.owner });
4744
5887
  return;
4745
5888
  }
4746
- if (key.backspace || key.delete)
5889
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl))
5890
+ setSourceInput('');
5891
+ else if (key.backspace || key.delete)
4747
5892
  setSourceInput(removeLastGrapheme);
4748
5893
  else if (input && !key.ctrl && !key.meta && input >= ' ')
4749
5894
  setSourceInput((value) => value + input);
@@ -4758,8 +5903,8 @@ export function ConsoleAppV4(props) {
4758
5903
  return;
4759
5904
  }
4760
5905
  if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl)) {
4761
- removeDraft(route.fieldKey);
4762
- back();
5906
+ setInputValue('');
5907
+ setInputError(undefined);
4763
5908
  return;
4764
5909
  }
4765
5910
  if (key.return && activeField) {
@@ -4780,7 +5925,9 @@ export function ConsoleAppV4(props) {
4780
5925
  back();
4781
5926
  return;
4782
5927
  }
4783
- if (key.backspace || key.delete)
5928
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl))
5929
+ setInputValue('');
5930
+ else if (key.backspace || key.delete)
4784
5931
  setInputValue(removeLastGrapheme);
4785
5932
  else if (input && !key.ctrl && !key.meta && input >= ' ')
4786
5933
  setInputValue((value) => value + input);
@@ -4795,6 +5942,11 @@ export function ConsoleAppV4(props) {
4795
5942
  back();
4796
5943
  return;
4797
5944
  }
5945
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl)) {
5946
+ setInputValue('');
5947
+ setInputError(undefined);
5948
+ return;
5949
+ }
4798
5950
  if (key.return && route.stage === 'key') {
4799
5951
  const propertyKey = inputValue;
4800
5952
  if (Object.hasOwn(activeObjectDraft, propertyKey)
@@ -4880,7 +6032,9 @@ export function ConsoleAppV4(props) {
4880
6032
  setScrollOffset(0);
4881
6033
  return;
4882
6034
  }
4883
- if (key.backspace || key.delete)
6035
+ if (input === '\u0015' || (input.toLowerCase() === 'u' && key.ctrl))
6036
+ setQuery('');
6037
+ else if (key.backspace || key.delete)
4884
6038
  setQuery(removeLastGrapheme);
4885
6039
  else if (input && !key.ctrl && !key.meta && input >= ' ')
4886
6040
  setQuery((value) => value + input);
@@ -4894,26 +6048,50 @@ export function ConsoleAppV4(props) {
4894
6048
  || route.name === 'configObjectPropertyEditor')) {
4895
6049
  return;
4896
6050
  }
4897
- if (canOpenHelp && input === '?') {
6051
+ const sessionNavigation = resolveTuiSessionNavigationKey({ input, key }, { characterInput: isCharacterInputMode(route, searchFocus) });
6052
+ const globalAction = resolveTuiGlobalKeyAction({ input, key }, { mode: 'menu', allowBack: true });
6053
+ if (sessionNavigation) {
6054
+ requestSessionNavigation(sessionNavigation);
6055
+ }
6056
+ else if (route.name === 'sourceAdd' && key.escape && route.step !== 'type') {
6057
+ back();
6058
+ }
6059
+ else if (input.toLowerCase() === 'o'
6060
+ && (route.name === 'home'
6061
+ || route.name === 'issueList'
6062
+ || route.name === 'issueDetail'
6063
+ || route.name === 'result')) {
6064
+ go({ name: 'operationHistory' });
6065
+ }
6066
+ else if (canOpenHelp && globalAction === 'help') {
4898
6067
  setOverlayFocus(0);
4899
6068
  setOverlay('help');
4900
6069
  }
4901
- else if (input.toLowerCase() === 'q') {
6070
+ else if (globalAction === 'exit') {
4902
6071
  setOverlayFocus(0);
4903
6072
  setDiscardForExit(dirty);
4904
6073
  setOverlay(dirty ? 'discard' : 'exit');
4905
6074
  }
4906
6075
  else if (input.toLowerCase() === 'd' && supportsTechnicalDetails) {
4907
- setShowTechnical((value) => !value);
6076
+ toggleTechnical();
4908
6077
  }
4909
- else if ((key.escape || input.toLowerCase() === 'b')
6078
+ else if (globalAction === 'back'
4910
6079
  && (route.name === 'configFieldEditor'
4911
6080
  || route.name === 'configObjectPropertyEditor'
4912
6081
  || route.name === 'configFieldAction')) {
4913
6082
  back();
4914
6083
  }
4915
- else if (key.escape || input.toLowerCase() === 'b') {
4916
- if (dirty) {
6084
+ else if (globalAction === 'back' && route.name === 'batchPlacement') {
6085
+ back();
6086
+ }
6087
+ else if (globalAction === 'back'
6088
+ && (route.name === 'operationHistory'
6089
+ || route.name === 'operationDiagnostic'
6090
+ || route.name === 'operationReportPreview')) {
6091
+ back();
6092
+ }
6093
+ else if (globalAction === 'back') {
6094
+ if (dirty && route.name !== 'review') {
4917
6095
  setOverlayFocus(0);
4918
6096
  setDiscardForExit(false);
4919
6097
  setOverlay('discard');
@@ -4941,15 +6119,53 @@ export function ConsoleAppV4(props) {
4941
6119
  setFocus(retained >= 0 ? retained : 0);
4942
6120
  setScrollOffset(0);
4943
6121
  }
6122
+ else if (route.name === 'pluginList' && input.toLowerCase() === 'm') {
6123
+ openBatchSelection(route.mode === 'browse' ? 'install' : undefined);
6124
+ }
6125
+ else if (route.name === 'pluginList' && route.mode === 'installed' && input.toLowerCase() === 'a') {
6126
+ openBatchSelection();
6127
+ setBatchDraft({
6128
+ action: 'sync',
6129
+ selection: 'currentProjectIntentRoots',
6130
+ selectedPluginIds: [],
6131
+ platform: 'both',
6132
+ scope: 'project',
6133
+ });
6134
+ props.controller.push({ name: 'batchPlacement' });
6135
+ }
6136
+ else if (route.name === 'batchSelection' && batchDraft?.action && input.toLowerCase() === 'a') {
6137
+ const candidates = props.controller.listPlugins({
6138
+ mode: batchDraft.action === 'install' ? 'browse' : 'installed',
6139
+ }).filter((candidate) => !candidate.configOnly);
6140
+ setBatchDraft((current) => current ? {
6141
+ ...current,
6142
+ selectedPluginIds: current.selectedPluginIds.length === candidates.length
6143
+ ? [] : candidates.map((candidate) => candidate.pluginId),
6144
+ } : current);
6145
+ }
6146
+ else if (route.name === 'batchSelection' && batchDraft?.action && input.toLowerCase() === 'c') {
6147
+ if (batchDraft.selectedPluginIds.length > 0) {
6148
+ props.controller.push({ name: 'batchPlacement' });
6149
+ setFocus(0);
6150
+ setScrollOffset(0);
6151
+ }
6152
+ else
6153
+ setActionError('请至少选择一个插件。');
6154
+ }
6155
+ else if (route.name === 'batchSelection' && batchDraft?.action && input === ' ') {
6156
+ runAction(actions[effectiveFocus]);
6157
+ }
4944
6158
  else if (route.name === 'configFields' && route.layer.scope !== 'effective' && input.toLowerCase() === 's') {
4945
- if (dirty)
4946
- void beginConfigSave(false);
6159
+ if (dirty && configDraftIssues.length === 0) {
6160
+ setFocus(0);
6161
+ go({ name: 'configSave', pluginId: route.pluginId, layer: { scope: route.layer.scope } });
6162
+ }
4947
6163
  }
4948
6164
  else if (route.name === 'configFields' && route.layer.scope !== 'effective' && input.toLowerCase() === 'u') {
4949
6165
  const key = actions[focus]?.configKey;
4950
6166
  const field = currentConfigPage?.groups.flatMap((group) => group.fields)
4951
6167
  .find((candidate) => candidate.key === key);
4952
- if (field?.configured && (!field.invalidOverrideIssue || field.invalidOverrideOwnedByLayer)) {
6168
+ if (field && configFieldCanUnset(field) && (!field.invalidOverrideIssue || field.invalidOverrideOwnedByLayer)) {
4953
6169
  removeDraft(field.key);
4954
6170
  }
4955
6171
  }
@@ -4963,13 +6179,10 @@ export function ConsoleAppV4(props) {
4963
6179
  setScrollOffset(0);
4964
6180
  }
4965
6181
  else if (input.toLowerCase() === 'r' && route.name === 'sourceDetail') {
4966
- void props.controller.reloadCatalog({ sourceId: route.sourceId });
6182
+ void props.controller.recheckCatalogSource(route.sourceId);
4967
6183
  }
4968
6184
  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
- }
6185
+ void props.controller.recheckAllCatalogSources();
4973
6186
  }
4974
6187
  else if (route.name === 'pluginList' && (key.leftArrow || key.rightArrow || input === '[' || input === ']')) {
4975
6188
  const tabs = ['all', ...props.controller.listSources().map((source) => source.id)];
@@ -5011,7 +6224,7 @@ export function ConsoleAppV4(props) {
5011
6224
  const viewport = route.name === 'portableSkillSelect' && snapshot.portableDraft
5012
6225
  ? portableSkillCandidateViewport(actions.slice(0, snapshot.portableDraft.candidates.length), snapshot.portableDraft.candidates, policy.mode === 'compact', next, scrollOffset, pageActionBudget(actions.length - snapshot.portableDraft.candidates.length
5013
6226
  + (portableVariantCandidates.length > 0 ? 3 : 0)))
5014
- : actionViewport(actions, policy.mode === 'compact', next, scrollOffset, pageActionBudget(route.name === 'dependencyPlacement' ? dependencyPlacementStaticRows : 0));
6227
+ : actionViewport(actions, policy.mode === 'compact', next, scrollOffset, pageActionBudget(actionViewportStaticRows));
5015
6228
  setScrollOffset(viewport.offset);
5016
6229
  return next;
5017
6230
  });
@@ -5030,15 +6243,19 @@ export function ConsoleAppV4(props) {
5030
6243
  const viewport = route.name === 'portableSkillSelect' && snapshot.portableDraft
5031
6244
  ? portableSkillCandidateViewport(actions.slice(0, snapshot.portableDraft.candidates.length), snapshot.portableDraft.candidates, policy.mode === 'compact', next, scrollOffset, pageActionBudget(actions.length - snapshot.portableDraft.candidates.length
5032
6245
  + (portableVariantCandidates.length > 0 ? 3 : 0)))
5033
- : actionViewport(actions, policy.mode === 'compact', next, scrollOffset, pageActionBudget(route.name === 'dependencyPlacement' ? dependencyPlacementStaticRows : 0));
6246
+ : actionViewport(actions, policy.mode === 'compact', next, scrollOffset, pageActionBudget(actionViewportStaticRows));
5034
6247
  setScrollOffset(viewport.offset);
5035
6248
  return next;
5036
6249
  });
5037
6250
  }
5038
6251
  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 });
6252
+ const row = pluginRows[focus];
6253
+ if (row.menuAction)
6254
+ row.menuAction();
6255
+ else
6256
+ go(route.mode === 'browse'
6257
+ ? { name: 'pluginPreview', pluginId: row.pluginId }
6258
+ : { name: 'pluginDetail', pluginId: row.pluginId });
5042
6259
  }
5043
6260
  else if (key.return) {
5044
6261
  runAction(actions[effectiveFocus]);
@@ -5052,15 +6269,26 @@ export function ConsoleAppV4(props) {
5052
6269
  const base = (_jsx(ScreenFrame, { title: title, description: description, breadcrumb: breadcrumb, context: context, help: help, size: size, children: content }));
5053
6270
  if (!overlay)
5054
6271
  return base;
5055
- const helpRows = helpRowsForRoute(route, actions.length, supportsTechnicalDetails, busy);
6272
+ const helpRows = helpRowsForRoute(route, actions.length, supportsTechnicalDetails, busy, props.onSessionNavigation !== undefined);
5056
6273
  const overlayActions = overlay === 'help'
5057
- ? [{ label: '关闭' }]
6274
+ ? [
6275
+ { label: '关闭' },
6276
+ ...(props.onSessionNavigation ? [
6277
+ { label: '返回首页' },
6278
+ { label: '返回首页并重新加载' },
6279
+ ] : []),
6280
+ ]
5058
6281
  : [
5059
- { label: overlay === 'discard' ? '继续编辑' : '继续使用' },
5060
- { label: overlay === 'discard' ? '放弃修改' : '退出', status: 'error' },
6282
+ { label: overlay === 'discard' || overlay === 'sessionNavigation' ? '继续编辑' : '继续使用' },
6283
+ { label: overlay === 'discard' || overlay === 'sessionNavigation' ? '放弃修改' : '退出', status: 'error' },
5061
6284
  ];
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'
6285
+ 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' ? '当前页快捷键'
6286
+ : overlay === 'discard' || overlay === 'sessionNavigation' ? '放弃未保存修改?'
6287
+ : '退出控制台?', children: [overlay === 'help'
5063
6288
  ? _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 })] }) })] }));
6289
+ : _jsx(StaticRow, { title: overlay === 'discard' ? '放弃后将返回上一页。'
6290
+ : overlay === 'sessionNavigation'
6291
+ ? `放弃后将${discardForSessionNavigation === 'reload' ? '重新读取本地状态' : '返回首页'}。`
6292
+ : '进行中的写操作会先完成。', status: "muted" }), _jsx(SemanticRows, { actions: overlayActions, focus: overlayFocus, kind: "action", compact: true })] }) })] }));
5065
6293
  }
5066
6294
  //# sourceMappingURL=console-app-v4.js.map