@lesliechan721/aw 0.1.0-beta.35 → 0.1.0-beta.36

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 (72) hide show
  1. package/dist/src/commands/operation-plan.d.ts +19 -0
  2. package/dist/src/commands/operation-plan.js.map +1 -1
  3. package/dist/src/commands/rebuild-command.d.ts +1 -0
  4. package/dist/src/commands/rebuild-command.js +890 -8
  5. package/dist/src/commands/rebuild-command.js.map +1 -1
  6. package/dist/src/commands/rebuild-integration.d.ts +12 -0
  7. package/dist/src/commands/rebuild-integration.js +88 -2
  8. package/dist/src/commands/rebuild-integration.js.map +1 -1
  9. package/dist/src/commands/status-v6.js +109 -19
  10. package/dist/src/commands/status-v6.js.map +1 -1
  11. package/dist/src/commands/wizard.d.ts +5 -1
  12. package/dist/src/commands/wizard.js +4 -2
  13. package/dist/src/commands/wizard.js.map +1 -1
  14. package/dist/src/commands/write-commands.js +3 -0
  15. package/dist/src/commands/write-commands.js.map +1 -1
  16. package/dist/src/index.js +57 -4
  17. package/dist/src/index.js.map +1 -1
  18. package/dist/src/tui/components.d.ts +20 -1
  19. package/dist/src/tui/components.js +41 -20
  20. package/dist/src/tui/components.js.map +1 -1
  21. package/dist/src/tui/console-app-v4.js +1323 -1165
  22. package/dist/src/tui/console-app-v4.js.map +1 -1
  23. package/dist/src/tui/content-item-card.d.ts +13 -0
  24. package/dist/src/tui/content-item-card.js +51 -0
  25. package/dist/src/tui/content-item-card.js.map +1 -0
  26. package/dist/src/tui/controller-runtime.js +18 -2
  27. package/dist/src/tui/controller-runtime.js.map +1 -1
  28. package/dist/src/tui/controller.d.ts +3 -0
  29. package/dist/src/tui/controller.js +57 -8
  30. package/dist/src/tui/controller.js.map +1 -1
  31. package/dist/src/tui/dependency-graph-view.d.ts +39 -0
  32. package/dist/src/tui/dependency-graph-view.js +121 -0
  33. package/dist/src/tui/dependency-graph-view.js.map +1 -0
  34. package/dist/src/tui/dependency-graph.js +16 -19
  35. package/dist/src/tui/dependency-graph.js.map +1 -1
  36. package/dist/src/tui/entity-cards.d.ts +55 -0
  37. package/dist/src/tui/entity-cards.js +220 -0
  38. package/dist/src/tui/entity-cards.js.map +1 -0
  39. package/dist/src/tui/input-display.js +10 -1
  40. package/dist/src/tui/input-display.js.map +1 -1
  41. package/dist/src/tui/model.d.ts +2 -0
  42. package/dist/src/tui/model.js +10 -2
  43. package/dist/src/tui/model.js.map +1 -1
  44. package/dist/src/tui/operation-diagnostic-screens.d.ts +2 -18
  45. package/dist/src/tui/operation-diagnostic-screens.js +16 -69
  46. package/dist/src/tui/operation-diagnostic-screens.js.map +1 -1
  47. package/dist/src/tui/operation-review-planner.js +44 -3
  48. package/dist/src/tui/operation-review-planner.js.map +1 -1
  49. package/dist/src/tui/plugin-content.d.ts +8 -0
  50. package/dist/src/tui/plugin-content.js +409 -65
  51. package/dist/src/tui/plugin-content.js.map +1 -1
  52. package/dist/src/tui/plugin-detail.d.ts +52 -0
  53. package/dist/src/tui/plugin-detail.js +165 -0
  54. package/dist/src/tui/plugin-detail.js.map +1 -0
  55. package/dist/src/tui/query-header.d.ts +30 -0
  56. package/dist/src/tui/query-header.js +40 -0
  57. package/dist/src/tui/query-header.js.map +1 -0
  58. package/dist/src/tui/reader.d.ts +37 -0
  59. package/dist/src/tui/reader.js +47 -0
  60. package/dist/src/tui/reader.js.map +1 -0
  61. package/dist/src/tui/router.d.ts +10 -0
  62. package/dist/src/tui/router.js.map +1 -1
  63. package/dist/src/tui/screen-registry.d.ts +2 -0
  64. package/dist/src/tui/source-detail-card.d.ts +29 -0
  65. package/dist/src/tui/source-detail-card.js +45 -0
  66. package/dist/src/tui/source-detail-card.js.map +1 -0
  67. package/dist/src/tui/wizard.js +26 -4
  68. package/dist/src/tui/wizard.js.map +1 -1
  69. package/dist/src/utils/local-marketplace.d.ts +1 -0
  70. package/dist/src/utils/local-marketplace.js +22 -0
  71. package/dist/src/utils/local-marketplace.js.map +1 -1
  72. package/package.json +4 -4
@@ -5,7 +5,6 @@ import { presentConfigValue, safeConfigText } from './config-value-presentation.
5
5
  import { Box, Text, useInput, useWindowSize } from 'ink';
6
6
  import { useEffect, useLayoutEffect, useRef, useState } from 'react';
7
7
  import path from 'node:path';
8
- import stringWidth from 'string-width';
9
8
  import { validatePluginConfigValue, } from '@lesliechan721/aw-plugin-core/manifest';
10
9
  import { CATALOG_BETA_REF, CATALOG_LATEST_REF, OFFICIAL_CATALOG_SOURCE_SSH_URL, OFFICIAL_CATALOG_SOURCE_URL, } from '../constants.js';
11
10
  import { CATALOG_SOURCE_ID_PATTERN } from '../utils/catalog-sources.js';
@@ -14,17 +13,23 @@ import { normalizePortableAgents, SKILLS_CLI_SUPPORTED_VERSION_LABEL, } from '..
14
13
  import { selectLifecycleRetryAction, } from '../commands/operation-planner.js';
15
14
  import { parseScriptTimeoutMs, SCRIPT_TIMEOUT_DEFAULT_MS, SCRIPT_TIMEOUT_MAX_MS, SCRIPT_TIMEOUT_MIN_MS, } from '../commands/options.js';
16
15
  import { projectCatalogSourceLocation } from '../utils/catalog-source-presentation.js';
17
- import { ActionRow, ConfigTile, configTileHeight, HomeNavTile, KeyCap, ObjectCardRow, objectCardRowLayout, pluginListRowLayout, StatusBadge, diagnosticDetailLines, diagnosticDetailText, Callout, ChoiceRow, ContentViewport, DiagnosticSummary, InputCard, PluginListRow, ProgressRow, ResultCard, ResultRow, ReviewCard, ReviewChangeRow, ScreenFrame, SectionCard, SectionRow, StaticRow, StatusCard, TaskRow, TechnicalDetails, ToggleRow, } from './components.js';
16
+ import { ActionRow, ConfigTile, configTileHeight, HomeNavTile, KeyCap, ObjectCardFrame, ObjectCardRow, objectCardRowLayout, pluginListRowLayout, StatusBadge, diagnosticDetailLines, diagnosticDetailText, Callout, ChoiceRow, ContentViewport, DiagnosticSummary, InputCard, PluginListRow, ProgressRow, ResultCard, ResultRow, ReviewCard, ReviewChangeRow, ScreenFrame, SectionCard, SectionRow, StaticRow, StatusCard, TaskRow, TechnicalDetails, ToggleRow, } from './components.js';
18
17
  import { safeDiagnosticCode, sanitizeDiagnosticText, } from './diagnostics.js';
19
18
  import { extractPortableError, TuiConfigInvalidStoreError, userIssueKey, } from './controller.js';
20
19
  import { pluginWriteTaskVersionEvidence } from './plugin-write-task.js';
21
20
  import { catalogSourceFreshnessLabel, catalogSourceListPresentation, } from './catalog-sources/screens.js';
22
- import { operationDiagnosticLineCount, operationDiagnosticText, operationHistoryMeta, operationHistoryState, operationReportLineCount, OperationDiagnosticScreen, OperationReportPreviewScreen, } from './operation-diagnostic-screens.js';
21
+ import { operationDiagnosticText, operationHistoryMeta, operationHistoryState, } from './operation-diagnostic-screens.js';
23
22
  import { copyToClipboard } from './clipboard.js';
24
23
  import { resolveTuiSizePolicy } from './layout.js';
25
24
  import { buildDependencyGraphProjection, buildUnifiedDependencyGraphProjection, } from './dependency-graph.js';
25
+ import { DependencyGraphView, projectDependencyGraphView } from './dependency-graph-view.js';
26
26
  import { platformLabel, scopeLabel } from './glossary.js';
27
27
  import { InputDisplay, removeLastGrapheme } from './input-display.js';
28
+ import { QueryHeader, querySearchWidth } from './query-header.js';
29
+ import { Pager, Reader, paginateWrappedRows, readerPage } from './reader.js';
30
+ import { PluginDetailCard, ProjectRootCard, EntityPlacementRow, entityPlacementRowHeight, entityCardWindowHeight, pluginDetailCardLayout, projectRootCardLayout } from './entity-cards.js';
31
+ import { ContentItemCard, contentItemCardLayout, contentItemCardWindowHeight, contentItemHasReader, contentTypeLabels } from './content-item-card.js';
32
+ import { SourceDetailCard, sourceDetailCardLayout } from './source-detail-card.js';
28
33
  import { catalogRefPolicyForPreset } from './source-ref-presets.js';
29
34
  import { buildPlatformActions, configObjectHasSensitiveChild, hasInstalledPlacement, configValuesEqual, } from './model.js';
30
35
  import { resolveTuiGlobalKeyAction, resolveTuiSessionNavigationKey } from './keys.js';
@@ -35,6 +40,17 @@ export const CONSOLE_V4_SCREEN_IDS = TUI_SCREEN_IDS;
35
40
  const PLUGIN_FILTERS = [
36
41
  'all', 'installed', 'uninstalled', 'updateAvailable', 'attention', 'project', 'user',
37
42
  ];
43
+ const PROJECT_STATUS_FILTERS = ['all', 'pending', 'blocked', 'unknown', 'ready'];
44
+ const PROJECT_STATUS_LABELS = {
45
+ all: '全部', pending: '待同步', blocked: '受阻', unknown: '未确认', ready: '就绪',
46
+ };
47
+ function queryMatches(query, ...values) {
48
+ const normalized = query.trim().toLocaleLowerCase();
49
+ return !normalized || values.some((value) => value?.toLocaleLowerCase().includes(normalized));
50
+ }
51
+ function contentMatches(query, item) {
52
+ return queryMatches(query, item.name, item.descriptionState === 'issue' ? undefined : item.description);
53
+ }
38
54
  function pluginFilterLabel(filter) {
39
55
  return {
40
56
  all: '全部',
@@ -65,13 +81,11 @@ function productResultMessage(message) {
65
81
  return message;
66
82
  }
67
83
  function pluginMenuActionId(action) {
68
- return action.id === 'platform' || action.id === 'scopeConflict'
69
- ? `${action.id}:${action.platform}`
70
- : action.id === 'addScope'
71
- ? `${action.id}:${action.platform}:${action.scope}`
72
- : action.id === 'resolveScopeConflict'
73
- ? `${action.id}:${action.pluginId}:${action.platform}:${action.scope}`
74
- : action.id;
84
+ return 'scope' in action && 'platform' in action
85
+ ? `${action.id}:${action.platform}:${action.scope}`
86
+ : action.id === 'platform' || action.id === 'scopeConflict'
87
+ ? `${action.id}:${action.platform}`
88
+ : action.id;
75
89
  }
76
90
  function batchTargetKey(target) {
77
91
  return [target.pluginId, target.platform, target.scope].join('\0');
@@ -85,58 +99,12 @@ function batchPluginVersionSummary(plugin) {
85
99
  ? changes[0]
86
100
  : `${updates.length} 个位置版本不同(Enter 查看)`;
87
101
  }
88
- function scopedPluginPlacementConclusion(placement, location) {
89
- if (placement.blockingReason === 'artifactCollision' || placement.actualState === 'blocked') {
90
- return { text: `${location}暂时不可用`, status: 'error' };
91
- }
92
- if (placement.actualState === 'needsSync' || placement.health === 'incomplete') {
93
- return { text: `${location}需要同步`, status: 'warning' };
94
- }
95
- if (placement.health === 'error')
96
- return { text: `${location}状态未知`, status: 'warning' };
97
- return { text: `${location}可用`, status: 'success' };
98
- }
99
- function pluginPlacementConclusions(placement) {
100
- if (placement.state === 'notInstalled')
101
- return [{ text: '未添加', status: 'muted' }];
102
- if (placement.state === 'blocked')
103
- return [{ text: '无法添加:输出位置有冲突', status: 'error' }];
104
- if (placement.state === 'scopeConflict')
105
- return [{ text: '使用位置冲突,需要选择一个位置', status: 'warning' }];
106
- if (!placement.scopedPlacements?.length) {
107
- const location = placement.desiredScopes?.map(scopeLabel).join('、') || scopeLabel(placement.scope);
108
- return [scopedPluginPlacementConclusion(placement, location)];
109
- }
110
- const grouped = new Map();
111
- for (const scoped of placement.scopedPlacements) {
112
- const conclusion = scopedPluginPlacementConclusion({
113
- platform: placement.platform,
114
- state: 'installed',
115
- scope: scoped.scope,
116
- health: scoped.health,
117
- revision: scoped.revision,
118
- desiredScopes: [scoped.scope],
119
- ...(scoped.actualState ? { actualState: scoped.actualState } : {}),
120
- ...(scoped.recovery ? { recovery: scoped.recovery } : {}),
121
- ...(scoped.blockingReason ? { blockingReason: scoped.blockingReason } : {}),
122
- }, '');
123
- const key = `${conclusion.status}\0${conclusion.text}`;
124
- const current = grouped.get(key);
125
- if (current)
126
- current.scopes.push(scoped.scope);
127
- else
128
- grouped.set(key, { scopes: [scoped.scope], status: conclusion.status, suffix: conclusion.text });
129
- }
130
- return [...grouped.values()].map((entry) => ({
131
- text: `${entry.scopes.map(scopeLabel).join('、')}${entry.suffix}`,
132
- status: entry.status,
133
- }));
134
- }
135
102
  function isCharacterInputMode(route, searchMode) {
103
+ if (searchMode)
104
+ return true;
136
105
  if (routeDefinition(route).keymap !== 'menuOrTextInput')
137
106
  return false;
138
- return searchMode
139
- || route.name === 'portableAgentInput'
107
+ return route.name === 'portableAgentInput'
140
108
  || route.name === 'configFieldEditor'
141
109
  && (route.editor === 'string' || route.editor === 'number')
142
110
  && route.mode !== 'choices'
@@ -154,56 +122,13 @@ function supportsScriptTimeout(intent) {
154
122
  || intent?.kind === 'pluginWriteTask'
155
123
  || (intent?.kind === 'configSave' || intent?.kind === 'configSaveBatch') && intent.apply;
156
124
  }
157
- function helpRowsForRoute(route, actionCount, supportsTechnicalDetails, busy, sessionNavigation) {
158
- const rows = [];
159
- if (!busy && actionCount > 1)
160
- rows.push(['↑/↓ · j/k', '移动当前唯一焦点']);
161
- if (!busy && actionCount > 0)
162
- rows.push(['Enter', '打开、选择或继续']);
163
- if (!busy && route.name !== 'home')
164
- rows.push(['Esc / b', '返回上一页']);
165
- if (route.name === 'pluginList') {
166
- rows.push(['←/→ · [ / ]', '切换来源']);
167
- rows.push(['/', '进入搜索']);
168
- rows.push(['f', '切换插件状态']);
169
- rows.push(['m', '原位选择多个插件']);
170
- }
171
- if (route.name === 'portableSkillSelect') {
172
- rows.push(['Space', '切换当前 Skill']);
173
- rows.push(['a', '定位下方操作 / 回到原项']);
174
- }
175
- if (route.name === 'batchSelection') {
176
- rows.push(['Space', '切换当前插件']);
177
- rows.push(['a', '定位下方操作 / 回到原项']);
178
- rows.push(['c', '继续选择任务或查看汇总']);
179
- }
180
- if (route.name === 'configFields' && route.layer.scope !== 'effective') {
181
- rows.push(['s', '查看保存汇总']);
182
- rows.push(['u', '恢复当前字段继承']);
183
- }
184
- if (route.name === 'sourceList' || route.name === 'sourceDetail' || route.name === 'sourceMore') {
185
- rows.push(['r', '检查来源']);
186
- }
187
- if (supportsTechnicalDetails) {
188
- rows.push(['d', route.name === 'progress' ? '打开技术进度' : '打开技术详情']);
189
- }
190
- if (sessionNavigation && !busy && !isCharacterInputMode(route, false)) {
191
- rows.push(['h / H', '返回首页 / 重新读取本地状态']);
192
- }
193
- if (sessionNavigation && busy) {
194
- rows.push(['h / H', '操作成功后返回首页或重新读取']);
195
- rows.push(['Esc', '撤销等待中的页面跳转']);
196
- }
197
- if (route.name === 'pluginList' || route.name === 'pluginDetail'
198
- || route.name === 'batchSelection' || route.name === 'installTargets') {
199
- rows.push(['状态', '✓已装 ○未装 !异常 ↑更新 ?未知']);
200
- }
201
- rows.push(['q / Ctrl-C', busy ? '操作完成后退出' : '退出控制台']);
202
- rows.push(['? / o', '关闭帮助 / 查看操作记录']);
203
- return rows;
125
+ function helpTextForPage(help, busy) {
126
+ return [...help.split(' · ').filter((part) => part !== '? 帮助' && !/^(q|Ctrl-C) /u.test(part)),
127
+ `q / Ctrl-C ${busy ? '操作完成后退出' : '退出控制台'}`,
128
+ ...(!busy ? ['o 查看操作记录'] : []), 'Esc / ? 关闭帮助'].join('\n');
204
129
  }
205
130
  function projectDependencyLines(root) {
206
- return root?.dependencies.flatMap((dependency) => dependency.targets.map((target) => `依赖 ${dependency.displayName} · ${dependency.pluginId} · ${platformLabel(target.platform)} · ${target.location === 'sharedUser' ? '共用个人安装' : '当前项目'} · 期望 ${target.expectedVersion ?? '未知'} · 实际 ${target.installedVersion ?? '未知'}${target.reasonCodes.length ? ` · ${target.reasonCodes.map((code) => safeDiagnosticCode(code, 'projectTargetUnknown')).join('、')}` : ''}`)) ?? [];
131
+ return root?.dependencies.flatMap((dependency) => dependency.targets.map((target) => `依赖 ${dependency.displayName} · ${dependency.pluginId} · ${platformLabel(target.platform)} · ${target.location === 'sharedUser' ? '共用个人安装' : '当前项目'} · 目录参考 ${target.expectedVersion ?? '未知'} · 实际 ${target.installedVersion ?? '未知'}${target.reasonCodes.length ? ` · ${target.reasonCodes.map((code) => safeDiagnosticCode(code, 'projectTargetUnknown')).join('、')}` : ''}`)) ?? [];
207
132
  }
208
133
  function configOwnerKey(route) {
209
134
  return route.pluginId;
@@ -253,14 +178,6 @@ function pluginWriteTaskTargetKey(candidate) {
253
178
  function dependencyGraphContextKey(pluginId, direction) {
254
179
  return `${pluginId}\0${direction}`;
255
180
  }
256
- function unifiedDependencyGraphActionIds(projection) {
257
- return [
258
- ...projection.rows.map((row) => `row:${row.branch}:${row.id}`),
259
- ...projection.dependents.scopeSummary.unknownConsumerPluginIds
260
- .map((pluginId) => `unverified:${pluginId}`),
261
- 'technical',
262
- ];
263
- }
264
181
  function statusText(plugin) {
265
182
  if (plugin.configOnly)
266
183
  return '已保存设置';
@@ -283,21 +200,6 @@ function statusText(plugin) {
283
200
  : '来源不可用',
284
201
  }[plugin.aggregateStatus];
285
202
  }
286
- function pluginUpdateDescription(plugin) {
287
- const targets = (plugin.update?.targets ?? []).filter((target) => target.availableVersion !== undefined);
288
- if (targets.length === 0) {
289
- const incompatibleCandidate = plugin.update?.targets.find((target) => target.candidateVersion !== undefined && (target.missingCapabilities?.length ?? 0) > 0);
290
- return incompatibleCandidate
291
- ? `候选版本缺少 ${incompatibleCandidate.missingCapabilities.length} 项 AW 功能支持;当前启用版本可继续使用。`
292
- : undefined;
293
- }
294
- if (targets.some((target) => target.requiresCatalogActivation)) {
295
- return plugin.update?.lastSuccessful
296
- ? `上次成功检查:${plugin.update.checkedAt ? new Date(plugin.update.checkedAt).toLocaleString('zh-CN', { hour12: false }) : '时间未知'};需先更新来源目录。`
297
- : '已验证待检查版本;需先更新来源目录。';
298
- }
299
- return '当前启用目录已验证;可直接进入更新确认。';
300
- }
301
203
  function pluginPlatformUnknown(plugin, platform) {
302
204
  const placements = plugin.installedPlacements.filter((entry) => entry.platform === platform);
303
205
  return plugin.sourceUnavailable || placements.length === 0
@@ -435,18 +337,6 @@ function relativeCheckTime(value, now = Date.now()) {
435
337
  function parseConfigNumberInput(input) {
436
338
  return input.trim().length === 0 ? undefined : Number(input);
437
339
  }
438
- function changeDetailLines(changes, columns) {
439
- const valueLines = (label, value) => [
440
- label,
441
- ...wrapConsoleText(value ?? '未设置', Math.max(1, columns - 4)).map((line) => ` ${line}`),
442
- ];
443
- return changes.flatMap((change) => [
444
- `修改项 · ${change.title}`,
445
- ...valueLines('修改前', change.before),
446
- ...valueLines('修改后', change.after),
447
- ...(change.children ?? []).flatMap((child) => [child.property, ...valueLines('修改前', child.before), ...valueLines('修改后', child.after)]),
448
- ]);
449
- }
450
340
  function changeDetailText(changes) {
451
341
  return changes.flatMap((change) => [
452
342
  `修改项 · ${change.title}`,
@@ -533,6 +423,15 @@ function sourceLocationSummary(type, value) {
533
423
  return value.startsWith('ssh://') || value.includes('@') ? 'SSH 远程来源' : '远程来源';
534
424
  }
535
425
  }
426
+ function matchingSources(controller, query, type) {
427
+ return controller.listSources().filter((source) => {
428
+ const configured = controller.getConfiguredSource(source.id);
429
+ const detail = controller.getSourceOverview(source.id).detail;
430
+ const sourceType = configured?.type ?? detail?.type;
431
+ return (type === 'all' || sourceType === type)
432
+ && queryMatches(query, source.id, detail?.locationSummary, configured?.type === 'git' ? configured.url : configured?.path);
433
+ });
434
+ }
536
435
  function portableBlockerDiagnostic(route) {
537
436
  const cliMissing = route.code === 'portableSkillsCliMissing';
538
437
  const cliIncompatible = route.code === 'portableSkillsCliIncompatible';
@@ -811,33 +710,6 @@ function sourceDiagnostic(sourceId, state, detail, generation, freshness) {
811
710
  },
812
711
  };
813
712
  }
814
- function capabilityDiagnostic(plugin) {
815
- const missingCapabilities = plugin.missingCapabilities?.join('、')
816
- ?? 'Status 未提供具体 capability';
817
- const recovery = '升级 AW CLI 到支持此插件的版本后重试。';
818
- return {
819
- severity: 'error',
820
- title: '当前 AW 版本不支持此插件',
821
- userMessage: '当前 AW CLI 不能安装或重新安装这个插件。来源保持可用。',
822
- impact: [
823
- { state: 'blocked', message: '安装和重新安装会保持阻断,直到 AW CLI 升级。' },
824
- { state: 'preserved', message: '已安装插件和已保存设置不会被删除。' },
825
- ],
826
- nextStep: recovery,
827
- technical: {
828
- code: 'awCapabilityUnsupported',
829
- stage: 'check-capability',
830
- context: [{ key: 'pluginId', value: plugin.pluginId }],
831
- checks: [
832
- `确认插件需要的 capability:${missingCapabilities}。`,
833
- '升级 AW CLI 后重新检查当前状态。',
834
- ],
835
- },
836
- };
837
- }
838
- function terminalTextWidth(value) {
839
- return [...value].reduce((width, character) => width + (character.codePointAt(0) > 0xff ? 2 : 1), 0);
840
- }
841
713
  function SemanticRows(props) {
842
714
  const offset = props.offset ?? 0;
843
715
  const actions = props.limit === undefined
@@ -847,9 +719,11 @@ function SemanticRows(props) {
847
719
  if (action.role === 'navigation')
848
720
  return null;
849
721
  const index = visibleIndex + offset;
722
+ // The legacy label suffix is display metadata, never an action or permission lookup.
723
+ const keyHint = action.label.match(/ \[([a-z/?])\]$/u);
850
724
  const rowProps = {
851
- title: _jsx(Text, { children: action.label.split(/(\[[a-z/?]\])/u).map((part, index) => /^\[[a-z/?]\]$/u.test(part)
852
- ? _jsx(KeyCap, { children: part.slice(1, -1) }, index) : part) }),
725
+ title: keyHint ? action.label.slice(0, -keyHint[0].length) : action.label,
726
+ keyHint: keyHint?.[1],
853
727
  description: action.description,
854
728
  meta: action.meta ?? (props.compact ? action.disabled : undefined),
855
729
  status: action.status,
@@ -879,7 +753,8 @@ function SequenceRows(props) {
879
753
  ];
880
754
  const viewport = rowViewport({
881
755
  rowHeights: props.actions.map((action, index) => action.role === 'navigation' ? 0 : index < props.itemCount
882
- ? action.configTile ? configTileHeight({ width: props.columns, ...action.configTile })
756
+ ? action.configTile ? configTileHeight({ width: props.columns, ...action.configTile,
757
+ focused: !props.inactive && index === props.focus, bodyRows: Math.max(1, props.rowBudget - 3) })
883
758
  : props.skillRows ? 2 + (action.exception ? 1 : 0)
884
759
  : props.wrapValues ? configLines(action).length : props.cards ? objectCardRowLayout({
885
760
  width: props.columns, description: action.disabled ?? action.description ?? '',
@@ -895,7 +770,7 @@ function SequenceRows(props) {
895
770
  const index = viewport.offset + visibleIndex;
896
771
  const focused = !props.inactive && index === props.focus;
897
772
  if (index < props.itemCount && action.configTile)
898
- return _jsx(ConfigTile, { width: props.columns, title: action.label, focused: focused, ...action.configTile }, action.configKey);
773
+ return _jsx(ConfigTile, { width: props.columns, title: action.label, focused: focused, ...action.configTile, bodyRows: Math.max(1, props.rowBudget - 3), page: focused ? props.configPage : 1 }, action.configKey);
899
774
  if (index < props.itemCount && props.skillRows)
900
775
  return _jsxs(Box, { flexDirection: "column", backgroundColor: focused ? tuiTheme.colors.selectionBackground : undefined, children: [_jsxs(Text, { bold: focused, color: tuiTheme.roles.primaryValue, wrap: "truncate-end", children: [focused ? '›' : ' ', " [", action.selected ? 'x' : ' ', "] ", action.label] }), _jsxs(Text, { color: tuiTheme.roles.secondary, wrap: "truncate-end", children: [" ", action.description || '暂无简介'] }), action.exception ? _jsxs(Text, { color: tuiTheme.roles.warning, wrap: "truncate-end", children: [" ! ", action.exception] }) : null] }, action.actionId ?? action.label);
901
776
  if (index < props.itemCount && props.wrapValues)
@@ -919,36 +794,6 @@ function IssueTechnicalDetails(props) {
919
794
  _jsx(StaticRow, { title: detail.message, meta: index === visible.length - 1 && hidden > 0 ? `另有 ${hidden} 项` : undefined, compact: true }, `${detail.code}:${index}:message`),
920
795
  ]) });
921
796
  }
922
- function SourceTabs(props) {
923
- const filter = { mode: props.mode, query: props.query, installFilter: props.installFilter };
924
- const tabs = [{ id: 'all', label: '全部', count: props.controller.listPlugins(filter).length },
925
- ...props.controller.listSources().map((source) => ({
926
- ...source,
927
- count: props.controller.listPlugins({ ...filter, sourceId: source.id }).length,
928
- }))];
929
- const index = Math.max(0, tabs.findIndex((tab) => tab.id === props.current));
930
- const badge = (state, count) => state === 'checking' ? ' …'
931
- : state === 'update' ? ' ↑'
932
- : state === 'recovery' ? ' ?'
933
- : state === 'error' ? ' !' : ` ${count ?? ''}`;
934
- const fullText = `来源 ${tabs.map((tab) => {
935
- const state = 'state' in tab ? tab.state : 'ready';
936
- return `${tab.id === props.current ? '[' : ''}${tab.label}${badge(state, tab.count)}${tab.id === props.current ? ']' : ''} `;
937
- }).join('')}`;
938
- if (props.compact || terminalTextWidth(`${fullText}←→ 切换`) > props.columns) {
939
- const tab = tabs[index];
940
- const neighbour = tabs[(index + 1) % tabs.length];
941
- return _jsxs(Text, { wrap: "truncate-end", children: ["\u6765\u6E90 ", _jsxs(Text, { color: tuiTheme.colors.accent, children: ["[", tab.label, "]"] }), " \u2190\u2192 \u5207\u6362 ", tabs.length > 1 ? neighbour?.label : ''] });
942
- }
943
- return (_jsxs(Text, { wrap: "truncate-end", children: ["\u6765\u6E90 ", tabs.map((tab) => {
944
- const active = tab.id === props.current;
945
- const state = 'state' in tab ? tab.state : 'ready';
946
- return (_jsxs(Text, { bold: active, color: active ? tuiTheme.colors.accent
947
- : state === 'error' ? tuiTheme.statusColors.error
948
- : state === 'update' || state === 'recovery' ? tuiTheme.statusColors.warning
949
- : tuiTheme.colors.muted, children: [active ? '[' : '', tab.label, badge(state, tab.count), active ? ']' : '', ' '] }, tab.id));
950
- }), "\u2190\u2192 \u5207\u6362"] }));
951
- }
952
797
  function configTabs(page) {
953
798
  return [
954
799
  ...page.groups.filter((group) => group.fields.length > 0).map((group) => ({
@@ -962,18 +807,6 @@ function configTabs(page) {
962
807
  : []),
963
808
  ];
964
809
  }
965
- function ConfigGroupTabs(props) {
966
- const index = Math.max(0, props.tabs.findIndex((tab) => tab.id === props.current));
967
- const fullText = `分组 ${props.tabs.map((tab) => `${tab.id === props.current ? '[' : ''}${tab.label} ${tab.count}${tab.id === props.current ? ']' : ''} `).join('')}`;
968
- if (props.compact || terminalTextWidth(fullText) > props.columns) {
969
- const tab = props.tabs[index];
970
- return _jsxs(Text, { wrap: "truncate-end", children: ["\u5206\u7EC4 ", _jsxs(Text, { color: tuiTheme.colors.accent, children: ["[", tab.label, " ", tab.count, "]"] }), " ", index + 1, " / ", props.tabs.length, " \u2190 \u2192"] });
971
- }
972
- return (_jsxs(Text, { wrap: "truncate-end", children: ["\u5206\u7EC4 ", props.tabs.map((tab) => {
973
- const active = tab.id === props.current;
974
- return _jsxs(Text, { bold: active, color: active ? tuiTheme.colors.accent : tuiTheme.colors.muted, children: [active ? '[' : '', tab.label, " ", tab.count, active ? ']' : '', ' '] }, tab.id);
975
- }), "\u2190 \u2192"] }));
976
- }
977
810
  function placementStatus(placement) {
978
811
  if (placement.state === 'notInstalled') {
979
812
  return { value: '未添加', status: 'muted', description: '选择使用位置后添加' };
@@ -1049,22 +882,41 @@ function dependencyRelationDescription(item) {
1049
882
  const requiredBy = item.requiredBy.map((entry) => entry.displayName).join('、');
1050
883
  return `${item.relation === 'direct' ? '直接依赖' : '间接依赖'}${requiredBy ? ` · ${requiredBy} 需要它` : ''}`;
1051
884
  }
1052
- function dependencyReferenceUsageLabel(reference, consumerName) {
1053
- const location = reference.consumer.scope === 'user'
1054
- ? '所有项目'
1055
- : reference.consumer.project === 'current' ? '当前项目' : '其他项目';
1056
- return `${consumerName} · ${location} · ${platformLabel(reference.consumer.platform)}`;
1057
- }
1058
- function dependencyGraphRowStatus(row) {
1059
- if (row.nodeState === 'ready')
1060
- return { value: '正常', status: 'success' };
1061
- if (row.nodeState === 'missing')
1062
- return { value: '缺失', status: 'error' };
1063
- if (row.nodeState === 'needsAttention')
1064
- return { value: '待恢复', status: 'warning' };
1065
- if (row.nodeState === 'unavailable')
1066
- return { value: '不可用', status: 'error' };
1067
- return { value: '未检查', status: 'muted' };
885
+ function dependencyEvidenceLines(row, subject) {
886
+ const references = row.edge?.availablePlacementReferences ?? [];
887
+ const placementLines = subject?.placements.map((placement) => `安装位置 · ${platformLabel(placement.platform)} · ${scopeLabel(placement.scope)} · 实际安装版本 ${placement.installedVersion ?? '未确认'} · 观察 ${placement.observation === 'unknown' ? '未确认' : '已检查'}`) ?? [];
888
+ const referenceLines = (reference) => [
889
+ `consumer · ${reference.consumer.pluginId}`,
890
+ `consumer · platform=${platformLabel(reference.consumer.platform)} · scope=${reference.consumer.scope}`,
891
+ `consumer · project=${reference.consumer.project ?? 'none'}`,
892
+ `dependency · ${reference.dependency.pluginId}`,
893
+ `dependency · platform=${platformLabel(reference.dependency.platform)} · scope=${reference.dependency.scope}`,
894
+ `dependency · project=${reference.dependency.project ?? 'none'}`,
895
+ ];
896
+ const availablePath = row.relationship === 'directAvailable'
897
+ || row.relationship === 'indirectAvailable';
898
+ return [
899
+ `pluginId · ${row.pluginId}`,
900
+ `目录版本 · ${subject?.catalogVersion ?? '未确认'}`,
901
+ ...(placementLines.length ? placementLines : ['安装位置 · 本次无位置事实']),
902
+ `relationship · ${row.relationship}`,
903
+ `图路径 · ${row.pathPluginIds.join(' → ')}`,
904
+ `直接边声明 · ${row.edge?.declared ? '已声明' : '未确认'}`,
905
+ `直接边已验证位置 · ${references.length} 个`,
906
+ ...references.flatMap((reference, index) => [
907
+ `直接边引用 ${index + 1}`,
908
+ ...referenceLines(reference),
909
+ ]),
910
+ ...(availablePath && row.pathReferences.length
911
+ ? row.pathReferences.flatMap((path, index) => [
912
+ `连续已验证路径 ${index + 1} · ${path.length} 段`,
913
+ ...path.flatMap((reference, hop) => [
914
+ `连续路径 ${index + 1} · 第 ${hop + 1} 段`,
915
+ ...referenceLines(reference),
916
+ ]),
917
+ ])
918
+ : ['连续路径 · 未验证']),
919
+ ];
1068
920
  }
1069
921
  function isCatalogSourcesBootstrapFlow(snapshot) {
1070
922
  const route = snapshot.router.current.route;
@@ -1127,6 +979,7 @@ export function ConsoleAppV4(props) {
1127
979
  const [snapshot, setSnapshot] = useState(() => props.controller.snapshot);
1128
980
  const [overlay, setOverlay] = useState();
1129
981
  const [overlayFocus, setOverlayFocus] = useState(0);
982
+ const [helpPage, setHelpPage] = useState(1);
1130
983
  const [operationConfirmation, setOperationConfirmation] = useState();
1131
984
  const [sourceTransportOverlay, setSourceTransportOverlay] = useState();
1132
985
  const [projectReminderOpen, setProjectReminderOpen] = useState(false);
@@ -1134,7 +987,10 @@ export function ConsoleAppV4(props) {
1134
987
  const [discardForSessionNavigation, setDiscardForSessionNavigation] = useState();
1135
988
  const [discardDraftConfirmation, setDiscardDraftConfirmation] = useState();
1136
989
  const [focus, setFocus] = useState(0);
990
+ const [graphEvidenceOpen, setGraphEvidenceOpen] = useState(false);
991
+ const [graphPortableSelectionOpen, setGraphPortableSelectionOpen] = useState(false);
1137
992
  const actionJumpAnchor = useRef(undefined);
993
+ const detailActionAnchor = useRef(undefined);
1138
994
  const [searchCursor, setSearchCursor] = useState(0);
1139
995
  const [scrollOffset, setScrollOffset] = useState(0);
1140
996
  const [searchFocus, setSearchFocus] = useState(false);
@@ -1173,6 +1029,8 @@ export function ConsoleAppV4(props) {
1173
1029
  const [batchDraft, setBatchDraft] = useState();
1174
1030
  const batchSelectionAnchor = useRef(undefined);
1175
1031
  const [batchTargetOffset, setBatchTargetOffset] = useState(0);
1032
+ const [configCardPage, setConfigCardPage] = useState();
1033
+ const [contentReader, setContentReader] = useState();
1176
1034
  const [portableResultDetailOpen, setPortableResultDetailOpen] = useState(false);
1177
1035
  const [portableResultDetailOffset, setPortableResultDetailOffset] = useState(0);
1178
1036
  const [portableRecheckBusy, setPortableRecheckBusy] = useState(false);
@@ -1204,7 +1062,6 @@ export function ConsoleAppV4(props) {
1204
1062
  const pluginListFocusId = useRef(undefined);
1205
1063
  const listFocusAnchor = useRef(undefined);
1206
1064
  const dependencyGraphContexts = useRef(new Map());
1207
- const selectedDependencyReferenceIndex = useRef(0);
1208
1065
  const externalDraftRevision = useRef({ config: 0, source: 0, batch: 0 });
1209
1066
  const previousExternalDraftValues = useRef({
1210
1067
  configDrafts, configCandidate, arrayDrafts, objectDrafts, changedObjectDraftKeys, sourceDraft, batchDraft,
@@ -1214,6 +1071,14 @@ export function ConsoleAppV4(props) {
1214
1071
  const exitSent = useRef(false);
1215
1072
  const operationBusy = useRef(false);
1216
1073
  const route = snapshot.router.current.route;
1074
+ const platformFacts = route.name === 'platform'
1075
+ ? props.controller.getPluginDetailSubject(route.pluginId)?.placements
1076
+ .filter((entry) => entry.platform === route.platform) ?? []
1077
+ : [];
1078
+ const platformContext = snapshot.router.current.context;
1079
+ const platformSelectedScope = route.name === 'platform'
1080
+ && platformFacts.some((entry) => entry.scope === platformContext.platformSelectedScope)
1081
+ ? platformContext.platformSelectedScope : undefined;
1217
1082
  if (route.name === 'configFields') {
1218
1083
  configOverviewAnchors.current.set(route.pluginId, {
1219
1084
  overviewFrameId: snapshot.router.currentFrameId,
@@ -1246,12 +1111,40 @@ export function ConsoleAppV4(props) {
1246
1111
  setBatchTargetOffset(0);
1247
1112
  };
1248
1113
  const plugin = currentPlugin(props.controller, route);
1114
+ const detailSubject = route.name === 'pluginDetail'
1115
+ ? props.controller.getPluginDetailSubject(route.pluginId) : undefined;
1249
1116
  const issue = route.name === 'issueDetail'
1250
1117
  ? props.controller.listUserIssues().find((entry) => entry.issueId === route.issueId)
1251
1118
  : undefined;
1252
1119
  const busy = route.name === 'progress';
1253
1120
  const preparationRunning = route.name === 'reviewPreparing'
1254
1121
  && snapshot.reviewPreparation?.state === 'running';
1122
+ useEffect(() => {
1123
+ if (route.name !== 'pluginDependencyNode' || route.view === 'references')
1124
+ return;
1125
+ props.controller.replace({ name: 'pluginDetail', pluginId: route.nodePluginId });
1126
+ }, [props.controller, route.name,
1127
+ route.name === 'pluginDependencyNode' ? route.nodePluginId : '',
1128
+ route.name === 'pluginDependencyNode' ? route.view : '']);
1129
+ useEffect(() => {
1130
+ if (route.name !== 'platform')
1131
+ return;
1132
+ const selected = snapshot.router.current.context.platformSelectedScope;
1133
+ if (!snapshot.router.current.context.platformSelectionInitialized) {
1134
+ props.controller.updatePageContext({
1135
+ platformSelectionInitialized: true,
1136
+ platformSelectedScope: platformFacts.length === 1 ? platformFacts[0].scope : undefined,
1137
+ });
1138
+ }
1139
+ else if (selected && !platformFacts.some((entry) => entry.scope === selected)) {
1140
+ props.controller.updatePageContext({ platformSelectedScope: undefined });
1141
+ setActionError('已选位置不再存在;请重新选择位置。');
1142
+ }
1143
+ }, [props.controller, route.name, route.name === 'platform' ? route.pluginId : '',
1144
+ route.name === 'platform' ? route.platform : '', snapshot.viewModel,
1145
+ snapshot.router.current.context.platformSelectionInitialized,
1146
+ snapshot.router.current.context.platformSelectedScope,
1147
+ platformFacts.map((entry) => entry.scope).join('\0')]);
1255
1148
  useEffect(() => {
1256
1149
  if (!busy)
1257
1150
  return;
@@ -1752,9 +1645,11 @@ export function ConsoleAppV4(props) {
1752
1645
  return () => { current = false; };
1753
1646
  }, [props.controller, route.name, snapshot.result, catalogStoreRefresh]);
1754
1647
  useEffect(() => {
1755
- if (route.name !== 'pluginDependencies')
1648
+ if (route.name !== 'pluginDependencies' && route.name !== 'portableDependencies')
1649
+ return;
1650
+ if (route.name === 'pluginDependencies' && route.view === 'removeBlocked')
1756
1651
  return;
1757
- const key = dependencyGraphContextKey(route.pluginId, 'dependencies');
1652
+ const key = dependencyGraphContextKey(route.pluginId, route.name === 'portableDependencies' ? 'portable' : 'dependencies');
1758
1653
  const stored = dependencyGraphContexts.current.get(key);
1759
1654
  const overviews = props.controller.listPluginDependencyOverviews();
1760
1655
  const knownPluginIds = new Set(overviews.flatMap((overview) => [
@@ -1770,32 +1665,31 @@ export function ConsoleAppV4(props) {
1770
1665
  rootPluginId: route.pluginId,
1771
1666
  ...(expandedPluginIds ? { expandedPluginIds } : {}),
1772
1667
  });
1773
- const actionIds = unifiedDependencyGraphActionIds(projection);
1668
+ const layout = projectDependencyGraphView(projection, { columns: size.columns - 2,
1669
+ rowBudget: Math.max(1, size.rows - 7),
1670
+ focusRowId: stored?.focusedRowId,
1671
+ scrollAnchorRowId: stored?.scrollAnchorRowId });
1774
1672
  const normalizedExpanded = expandedPluginIds ?? new Set([
1775
1673
  ...projection.dependencies.defaultExpandedPluginIds,
1776
1674
  ...projection.dependents.defaultExpandedPluginIds,
1777
1675
  ]);
1778
- const focusedIndex = stored?.focusedActionId
1779
- ? actionIds.indexOf(stored.focusedActionId)
1780
- : 0;
1781
- const nextFocus = focusedIndex >= 0 ? focusedIndex : 0;
1782
- const anchorIndex = stored?.scrollAnchorActionId
1783
- ? actionIds.indexOf(stored.scrollAnchorActionId)
1784
- : 0;
1785
- const nextScroll = Math.max(0, anchorIndex >= 0 ? Math.min(anchorIndex, nextFocus) : 0);
1676
+ const nextFocus = layout.focusIndex;
1677
+ const nextScroll = layout.viewport.offset;
1786
1678
  setExpandedDependencyPluginIds(normalizedExpanded);
1787
1679
  setFocus(nextFocus);
1788
1680
  setScrollOffset(nextScroll);
1789
1681
  dependencyGraphContexts.current.set(key, {
1790
1682
  expandedPluginIds: normalizedExpanded,
1791
- ...(actionIds[nextFocus] ? { focusedActionId: actionIds[nextFocus] } : {}),
1792
- ...(actionIds[nextScroll] ? { scrollAnchorActionId: actionIds[nextScroll] } : {}),
1683
+ focusedRowId: layout.focusRowId,
1684
+ scrollAnchorRowId: layout.scrollAnchorRowId,
1793
1685
  });
1794
1686
  }, [
1795
1687
  props.controller,
1796
1688
  route.name,
1797
- route.name === 'pluginDependencies' ? route.pluginId : '',
1689
+ route.name === 'pluginDependencies' || route.name === 'portableDependencies' ? route.pluginId : '',
1798
1690
  snapshot.viewModel,
1691
+ size.columns,
1692
+ size.rows,
1799
1693
  ]);
1800
1694
  useEffect(() => {
1801
1695
  if (route.name !== 'pluginList' || snapshot.phase !== 'ready'
@@ -1813,8 +1707,9 @@ export function ConsoleAppV4(props) {
1813
1707
  }, [props.controller, route.name, route.name === 'pluginList' ? route.mode : '',
1814
1708
  route.name === 'pluginList' ? route.sourceId : '', snapshot.phase]);
1815
1709
  useLayoutEffect(() => {
1816
- setBatchTargetOffset(0);
1817
- }, [route.name, route.name === 'review' || route.name === 'result' ? route.operationId : '']);
1710
+ setBatchTargetOffset(snapshot.router.current.context.entityLineOffset ?? 0);
1711
+ }, [snapshot.router.currentFrameId, route.name,
1712
+ route.name === 'review' || route.name === 'result' ? route.operationId : '']);
1818
1713
  const activeTopField = route.name === 'configFieldEditor'
1819
1714
  || route.name === 'configObjectPropertyEditor'
1820
1715
  || route.name === 'configFieldAction'
@@ -1980,29 +1875,30 @@ export function ConsoleAppV4(props) {
1980
1875
  : [];
1981
1876
  const portableVisibleCandidates = route.name === 'portableSkillSelect' && snapshot.portableDraft
1982
1877
  ? portableSelectionView === 'selected'
1983
- ? snapshot.portableDraft.candidates.filter((candidate) => candidate.selected)
1984
- : snapshot.portableDraft.candidates
1878
+ ? snapshot.portableDraft.candidates.filter((candidate) => candidate.selected
1879
+ && queryMatches(query, candidate.name, candidate.shortDescription
1880
+ ?? candidate.description.split(/[。!?\n]/u)[0]))
1881
+ : snapshot.portableDraft.candidates.filter((candidate) => queryMatches(query, candidate.name, candidate.shortDescription ?? candidate.description.split(/[。!?\n]/u)[0]))
1985
1882
  : [];
1986
- const supportsTechnicalDetails = snapshot.phase === 'error'
1987
- || Boolean(snapshot.projectIntentIssue)
1988
- || route.name === 'projectStatus'
1989
- || Boolean(route.name === 'reviewPreparing' && snapshot.reviewPreparation?.code)
1990
- || Boolean(route.name === 'progress' && snapshot.progress.messages.length > 0)
1991
- || Boolean(route.name === 'review' && snapshot.activeReview?.technicalDetails?.length)
1992
- || Boolean(route.name === 'issueDetail' && issue
1993
- && issue.diagnostic.technical)
1994
- || Boolean(route.name === 'pluginDependencies' && plugin)
1995
- || Boolean(route.name === 'pluginDependencyNode' && plugin)
1996
- || route.name === 'catalogStoreRules'
1997
- || portableVariantCandidates.length > 0
1998
- || Boolean(route.name === 'portableDependencies' && plugin
1999
- && props.controller.getPluginDependencyOverview(plugin.pluginId)?.dependencies.length)
2000
- || Boolean(route.name === 'portableBlocker' && plugin)
2001
- || Boolean(route.name === 'configUnavailable' && plugin)
2002
- || Boolean(route.name === 'result' && snapshot.result && 'operationKind' in snapshot.result
2003
- && snapshot.result.operationKind === 'portableInstall');
2004
- const canOpenHelp = !tooSmall
2005
- && !isCharacterInputMode(route, searchFocus);
1883
+ const supportsTechnicalDetails = !(route.name === 'pluginDependencies' && route.view !== 'removeBlocked'
1884
+ || route.name === 'pluginDependencyNode' || route.name === 'portableDependencies')
1885
+ && (snapshot.phase === 'error'
1886
+ || route.name === 'projectStatus'
1887
+ || Boolean(route.name === 'reviewPreparing' && snapshot.reviewPreparation?.code)
1888
+ || Boolean(route.name === 'progress' && snapshot.progress.messages.length > 0)
1889
+ || Boolean(route.name === 'review' && snapshot.activeReview?.technicalDetails?.length)
1890
+ || Boolean(route.name === 'issueDetail' && issue
1891
+ && issue.diagnostic.technical)
1892
+ || Boolean(route.name === 'pluginDependencies' && plugin)
1893
+ || route.name === 'catalogStoreRules'
1894
+ || portableVariantCandidates.length > 0
1895
+ || Boolean(route.name === 'portableBlocker' && plugin)
1896
+ || Boolean(route.name === 'configUnavailable' && plugin)
1897
+ || Boolean(route.name === 'result' && snapshot.result && 'operationKind' in snapshot.result
1898
+ && snapshot.result.operationKind === 'portableInstall'));
1899
+ const characterInput = isCharacterInputMode(route, searchFocus)
1900
+ || route.name === 'operationReportPreview' && reportSaveMode;
1901
+ const canOpenHelp = !tooSmall && !characterInput && !preparationRunning;
2006
1902
  useEffect(() => {
2007
1903
  props.onScreenChange?.(screenId);
2008
1904
  }, [props.onScreenChange, screenId]);
@@ -2017,6 +1913,9 @@ export function ConsoleAppV4(props) {
2017
1913
  let content;
2018
1914
  let actions = [];
2019
1915
  let readingText;
1916
+ let readerNavigation;
1917
+ const helpBodyRows = Math.max(1, size.rows - (props.onSessionNavigation ? 10 : 8));
1918
+ let configCardNavigation;
2020
1919
  let confirmationText;
2021
1920
  let referenceDetailPagination;
2022
1921
  let confirmationPagination;
@@ -2028,26 +1927,23 @@ export function ConsoleAppV4(props) {
2028
1927
  let movePluginListSource;
2029
1928
  let choosePluginListFilter;
2030
1929
  let choosePluginListSource;
1930
+ let toggleProjectStatusFilter;
1931
+ let moveProjectSection;
1932
+ let moveSourceType;
1933
+ let moveContentType;
1934
+ let entityCardNavigation;
1935
+ let graphLayout;
1936
+ let graphProjection;
1937
+ let graphRowBudget = 1;
2031
1938
  let returnToPreviousInstallPlatform;
2032
1939
  let visibleConfigTabs = [];
2033
1940
  let activeConfigTabId;
2034
1941
  const dynamicMenu = plugin && route.name === 'platform'
2035
- ? buildPlatformActions(plugin, route.platform)
1942
+ ? buildPlatformActions(plugin, route.platform).filter((action) => action.id === 'addScope' || !('scope' in action) || action.scope === platformSelectedScope)
2036
1943
  : undefined;
2037
- const dynamicActionIds = dynamicMenu?.map(pluginMenuActionId);
2038
- const dynamicActionSignature = dynamicActionIds?.join('\0');
2039
- const previousDynamicFocus = dynamicMenuFocus.current;
2040
- const retainedDynamicFocus = dynamicActionIds
2041
- && previousDynamicFocus?.routeKey === routeKey
2042
- && previousDynamicFocus.actionIds !== dynamicActionSignature
2043
- && previousDynamicFocus.focusedActionId !== undefined
2044
- ? dynamicActionIds.indexOf(previousDynamicFocus.focusedActionId)
2045
- : -1;
2046
- const effectiveFocus = dynamicActionIds
2047
- && previousDynamicFocus?.routeKey === routeKey
2048
- && previousDynamicFocus.actionIds !== dynamicActionSignature
2049
- ? retainedDynamicFocus >= 0 ? retainedDynamicFocus : 0
2050
- : focus;
1944
+ let dynamicActionIds;
1945
+ let dynamicActionSignature;
1946
+ let effectiveFocus = focus;
2051
1947
  const pageActionBudget = (staticRows = 0) => contentRowBudget({
2052
1948
  terminalRows: size.rows,
2053
1949
  description: Boolean(actionError ?? description),
@@ -2056,6 +1952,56 @@ export function ConsoleAppV4(props) {
2056
1952
  + Math.max(0, wrapConsoleText(actionError ?? description ?? '', size.columns - 2).length - 1)
2057
1953
  + Math.max(0, wrapConsoleText(title, size.columns - 2).length - 1),
2058
1954
  });
1955
+ const detailCardContent = (subject, completionLines = []) => {
1956
+ const cardWidth = size.columns - 2;
1957
+ const card = pluginDetailCardLayout(subject, cardWidth);
1958
+ const availableRows = pageActionBudget(completionLines.length);
1959
+ // Compact actions use one row each; paging adds one row inside the card.
1960
+ const actionRows = Math.min(3, actions.filter((action) => action.role !== 'navigation').length);
1961
+ const needsPaging = card.fullHeight > availableRows - actionRows;
1962
+ const lineLimit = needsPaging ? Math.max(1, availableRows - actionRows - 3) : undefined;
1963
+ const pageCount = lineLimit ? Math.max(1, Math.ceil(card.factRows / lineLimit)) : 1;
1964
+ const page = lineLimit ? Math.min(pageCount, Math.floor(batchTargetOffset / lineLimit) + 1) : 1;
1965
+ const lineOffset = lineLimit ? (page - 1) * lineLimit : 0;
1966
+ if (lineLimit)
1967
+ readerNavigation = { page, pageCount, move: (direction) => {
1968
+ setBatchTargetOffset((Math.min(pageCount, Math.max(1, page + direction)) - 1) * lineLimit);
1969
+ } };
1970
+ const cardHeight = entityCardWindowHeight(card, lineOffset, lineLimit);
1971
+ currentActionViewportLineBudget = Math.max(1, pageActionBudget(completionLines.length) - cardHeight);
1972
+ const viewport = actionViewport(actions, true, focus, scrollOffset, currentActionViewportLineBudget);
1973
+ help = [
1974
+ ...(actions.length > 0 ? ['↑↓ 选择动作'] : []),
1975
+ ...(needsPaging ? ['←→ 翻信息'] : []),
1976
+ ...(actions[focus]?.run && !actions[focus]?.disabled ? ['Enter 选择'] : []),
1977
+ 'Esc 返回',
1978
+ ].join(' · ');
1979
+ return _jsxs(Box, { flexDirection: "column", children: [completionLines.map((line, index) => _jsx(Text, { color: tuiTheme.statusColors.success, children: line }, `completion:${index}`)), _jsx(PluginDetailCard, { subject: subject, width: cardWidth, lineOffset: lineOffset, lineLimit: lineLimit }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: viewport.offset, limit: viewport.limit })] });
1980
+ };
1981
+ const readingPage = (text, columns, rows, offset = 'batch') => {
1982
+ const current = offset === 'scroll' ? scrollOffset
1983
+ : offset === 'portable' ? portableResultDetailOffset : batchTargetOffset;
1984
+ const page = readerPage({ text, columns, rows, page: current + 1 });
1985
+ readerNavigation = {
1986
+ page: page.page,
1987
+ pageCount: page.pageCount,
1988
+ move: (direction) => {
1989
+ const next = Math.min(page.pageCount, Math.max(1, page.page + direction)) - 1;
1990
+ if (offset === 'scroll')
1991
+ setScrollOffset(next);
1992
+ else if (offset === 'portable')
1993
+ setPortableResultDetailOffset(next);
1994
+ else
1995
+ setBatchTargetOffset(next);
1996
+ },
1997
+ };
1998
+ return _jsx(Reader, { text: text, columns: columns, rows: rows, page: page.page });
1999
+ };
2000
+ const pagedItems = (total, rows, offset = batchTargetOffset) => paginateWrappedRows({
2001
+ lines: Array.from({ length: total }, (_, index) => String(index)),
2002
+ rows,
2003
+ page: Math.floor(offset / Math.max(1, rows)) + 1,
2004
+ });
2059
2005
  const dependencyPlacementStaticRows = (policy.mode === 'compact' ? 3 : 10)
2060
2006
  + (snapshot.installPlacementDraft?.exactRootTargets?.length ?? 0);
2061
2007
  const actionViewportStaticRows = route.name === 'dependencyPlacement'
@@ -2064,17 +2010,11 @@ export function ConsoleAppV4(props) {
2064
2010
  : route.name === 'operationHistory' ? 4
2065
2011
  : route.name === 'operationReportPreview' ? 5 : 0;
2066
2012
  let currentActionViewportLineBudget = pageActionBudget(actionViewportStaticRows);
2067
- let portableResultDetailPagination;
2068
2013
  const responsiveBudget = (compactRows, standardRows) => {
2069
2014
  const baseline = policy.mode === 'compact' ? 18 : 24;
2070
2015
  const base = policy.mode === 'compact' ? compactRows : standardRows;
2071
2016
  return base + Math.max(0, size.rows - baseline);
2072
2017
  };
2073
- const operationDetailPageRows = responsiveBudget(3, 6);
2074
- const operationReportPageRows = responsiveBudget(3, 4);
2075
- const sourceTechnicalPageRows = Math.max(1, contentRowBudget({
2076
- terminalRows: size.rows, description: false, compact: policy.mode === 'compact', staticRows: 6,
2077
- }));
2078
2018
  const resolveSourceTechnicalDiagnostic = (sourceId) => {
2079
2019
  const overview = props.controller.getSourceOverview(sourceId);
2080
2020
  const freshness = props.controller.getCatalogFreshness(sourceId);
@@ -2122,8 +2062,10 @@ export function ConsoleAppV4(props) {
2122
2062
  };
2123
2063
  };
2124
2064
  const moveBatchTargetPage = (total, pageRows, direction) => {
2125
- const lastOffset = Math.max(0, total - 1);
2126
- setBatchTargetOffset((value) => Math.min(lastOffset, Math.max(0, value + direction * pageRows)));
2065
+ setBatchTargetOffset((value) => {
2066
+ const page = pagedItems(total, pageRows, value);
2067
+ return pagedItems(total, pageRows, (page.page + direction - 1) * pageRows).offset;
2068
+ });
2127
2069
  };
2128
2070
  const pluginListRowIds = (_mode, items, _queryValue = query, _sourceUnavailable = false) => {
2129
2071
  if (items.length === 0)
@@ -2132,19 +2074,31 @@ export function ConsoleAppV4(props) {
2132
2074
  };
2133
2075
  const go = (next) => {
2134
2076
  props.controller.updatePageContext({
2135
- focusIndex: focus,
2077
+ focusIndex: route.name === 'projectStatus' || route.name === 'platform' ? effectiveFocus : focus,
2136
2078
  scrollOffset,
2079
+ entityLineOffset: entityCardNavigation || route.name === 'pluginDetail' ? batchTargetOffset : undefined,
2137
2080
  query,
2138
2081
  installFilter,
2139
2082
  sourceTabId: route.name === 'pluginList' ? route.sourceId ?? 'all' : undefined,
2140
2083
  focusedEntityId: route.name === 'pluginList'
2141
2084
  ? pluginRows[focus]?.pluginId
2142
- : route.name === 'batchSelection' || route.name === 'projectStatus' ? actions[focus]?.pluginId
2143
- : route.name === 'sourceList' ? props.controller.listSources()[focus]?.id : undefined,
2085
+ : route.name === 'pluginContent' ? actions[focus]?.actionId?.replace(/^content:/u, '')
2086
+ : route.name === 'batchSelection' ? actions[focus]?.pluginId
2087
+ : route.name === 'projectStatus' ? actions[effectiveFocus]?.pluginId
2088
+ : route.name === 'sourceList' ? actions[focus]?.actionId?.replace(/^source:/u, '')
2089
+ : route.name === 'portableSkillSelect' ? actions[focus]?.actionId : undefined,
2090
+ ...(route.name === 'projectStatus' || route.name === 'platform'
2091
+ ? { focusedActionId: actions[effectiveFocus]?.actionId } : {}),
2144
2092
  });
2145
2093
  props.controller.push(next);
2094
+ if (route.name === 'pluginContent')
2095
+ setContentReader(undefined);
2146
2096
  setFocus(0);
2097
+ if (next.name === 'pluginDetail' || next.name === 'sourceDetail'
2098
+ || next.name === 'projectStatus')
2099
+ setBatchTargetOffset(0);
2147
2100
  setScrollOffset(0);
2101
+ setQuery('');
2148
2102
  setSearchFocus(false);
2149
2103
  if (next.name === 'pluginList') {
2150
2104
  setInstallFilter(next.mode === 'installed' ? 'installed' : 'all');
@@ -2170,6 +2124,13 @@ export function ConsoleAppV4(props) {
2170
2124
  setFocus(portableResultDetailReturnFocus.current);
2171
2125
  setPortableResultDetailOpen(false);
2172
2126
  setPortableResultDetailOffset(0);
2127
+ if (route.name === 'projectStatus' && dynamicActionIds && dynamicActionSignature !== undefined) {
2128
+ const recheck = actions.findIndex((action) => action.actionId === 'project-recheck');
2129
+ dynamicMenuFocus.current = { routeKey, actionIds: dynamicActionSignature,
2130
+ focusedActionId: 'project-recheck' };
2131
+ setFocus(Math.max(0, recheck));
2132
+ setScrollOffset(0);
2133
+ }
2173
2134
  setShowTechnical((value) => !value);
2174
2135
  if (!(route.name === 'result' && snapshot.result
2175
2136
  && 'operationKind' in snapshot.result
@@ -2198,9 +2159,25 @@ export function ConsoleAppV4(props) {
2198
2159
  : Math.min(restoredFocus, Math.max(0, rowIds.length - 1));
2199
2160
  }
2200
2161
  else if (restoredFrame.route.name === 'projectStatus') {
2201
- const roots = props.controller.listProjectInventoryRoots(restored.projectSection ?? 'declared').filter((root) => !restored.query || `${root.displayName} ${root.pluginId}`.toLocaleLowerCase().includes(restored.query.toLocaleLowerCase()));
2202
- const index = roots.findIndex((root) => root.pluginId === restored.focusedEntityId);
2203
- itemCount = roots.length + 2;
2162
+ const actionId = restored.focusedActionId
2163
+ ?? (restored.focusedEntityId ? `project-root:${restored.focusedEntityId}` : 'project-recheck');
2164
+ dynamicMenuFocus.current = {
2165
+ routeKey: JSON.stringify(restoredFrame.route), actionIds: '', focusedActionId: actionId,
2166
+ };
2167
+ }
2168
+ else if (restoredFrame.route.name === 'platform') {
2169
+ dynamicMenuFocus.current = {
2170
+ routeKey: JSON.stringify(restoredFrame.route), actionIds: '',
2171
+ focusedActionId: restored.focusedActionId,
2172
+ };
2173
+ }
2174
+ else if (restoredFrame.route.name === 'pluginContent') {
2175
+ const overview = props.controller.getPluginContentOverview(restoredFrame.route.pluginId);
2176
+ const items = (overview?.items ?? []).filter((item) => contentMatches(restored.query ?? '', item)
2177
+ && (restored.contentTypeTab === undefined || restored.contentTypeTab === 'all'
2178
+ || item.type === restored.contentTypeTab));
2179
+ const index = items.findIndex((item) => item.id === restored.focusedEntityId);
2180
+ itemCount = items.length + 2;
2204
2181
  restoredFocus = index >= 0 ? index : Math.min(restoredFocus, Math.max(0, itemCount - 1));
2205
2182
  }
2206
2183
  else if (restoredFrame.route.name === 'batchSelection' && batchDraft) {
@@ -2210,7 +2187,7 @@ export function ConsoleAppV4(props) {
2210
2187
  restoredFocus = entityIndex >= 0 ? entityIndex : Math.min(restoredFocus, Math.max(0, itemCount - 1));
2211
2188
  }
2212
2189
  else if (restoredFrame.route.name === 'sourceList') {
2213
- const sources = props.controller.listSources();
2190
+ const sources = matchingSources(props.controller, restored.query ?? '', restored.sourceTypeTab ?? 'all');
2214
2191
  itemCount = sources.length + 3;
2215
2192
  const entityIndex = restored.focusedEntityId
2216
2193
  ? sources.findIndex((source) => source.id === restored.focusedEntityId)
@@ -2219,13 +2196,25 @@ export function ConsoleAppV4(props) {
2219
2196
  ? entityIndex
2220
2197
  : Math.min(restoredFocus, Math.max(0, itemCount - 1));
2221
2198
  }
2222
- let restoredScroll = Math.min(restored.scrollOffset ?? 0, Math.max(0, (itemCount ?? 1) - 1));
2223
- if (restoredFocus < restoredScroll)
2224
- restoredScroll = restoredFocus;
2225
- else if (restoredFocus >= restoredScroll + 4)
2226
- restoredScroll = Math.max(0, restoredFocus - 3);
2199
+ else if (restoredFrame.route.name === 'portableSkillSelect' && snapshot.portableDraft) {
2200
+ const candidates = snapshot.portableDraft.candidates.filter((candidate) => queryMatches(restored.query ?? '', candidate.name, candidate.shortDescription ?? candidate.description.split(/[。!?\n]/u)[0]));
2201
+ const entityIndex = candidates.findIndex((candidate) => `skill:${candidate.providerPluginId}:${candidate.name}` === restored.focusedEntityId);
2202
+ itemCount = candidates.length + 2;
2203
+ restoredFocus = entityIndex >= 0 ? entityIndex : Math.min(restoredFocus, Math.max(0, itemCount - 1));
2204
+ }
2205
+ const dynamicMenuRoute = restoredFrame.route.name === 'projectStatus' || restoredFrame.route.name === 'platform';
2206
+ let restoredScroll = dynamicMenuRoute
2207
+ ? restored.scrollOffset ?? 0
2208
+ : Math.min(restored.scrollOffset ?? 0, Math.max(0, (itemCount ?? 1) - 1));
2209
+ if (!dynamicMenuRoute) {
2210
+ if (restoredFocus < restoredScroll)
2211
+ restoredScroll = restoredFocus;
2212
+ else if (restoredFocus >= restoredScroll + 4)
2213
+ restoredScroll = Math.max(0, restoredFocus - 3);
2214
+ }
2227
2215
  setFocus(restoredFocus);
2228
2216
  setScrollOffset(restoredScroll);
2217
+ setBatchTargetOffset(restored.entityLineOffset ?? 0);
2229
2218
  setQuery(restored.query ?? '');
2230
2219
  setInstallFilter(restored.installFilter ?? 'all');
2231
2220
  setSearchFocus(false);
@@ -2241,6 +2230,7 @@ export function ConsoleAppV4(props) {
2241
2230
  props.controller.beginConfigFlow(pluginId);
2242
2231
  setFocus(0);
2243
2232
  setScrollOffset(0);
2233
+ setQuery('');
2244
2234
  setSearchFocus(false);
2245
2235
  setShowTechnical(false);
2246
2236
  setActionError(undefined);
@@ -2260,6 +2250,8 @@ export function ConsoleAppV4(props) {
2260
2250
  setActionError(undefined);
2261
2251
  };
2262
2252
  const back = () => {
2253
+ if (route.name === 'pluginContent')
2254
+ setContentReader(undefined);
2263
2255
  if (route.name === 'sourceEdit'
2264
2256
  && sourceDraft
2265
2257
  && sourceDraft.owner === route.owner
@@ -2302,11 +2294,13 @@ export function ConsoleAppV4(props) {
2302
2294
  ? snapshot.result.returnRoute
2303
2295
  : undefined);
2304
2296
  props.controller.updatePageContext({
2305
- focusIndex: focus,
2297
+ focusIndex: route.name === 'projectStatus' || route.name === 'platform' ? effectiveFocus : focus,
2306
2298
  scrollOffset,
2307
2299
  query,
2308
2300
  installFilter,
2309
2301
  sourceTabId: route.name === 'pluginList' ? route.sourceId ?? 'all' : undefined,
2302
+ ...(route.name === 'projectStatus' || route.name === 'platform'
2303
+ ? { focusedActionId: actions[effectiveFocus]?.actionId } : {}),
2310
2304
  });
2311
2305
  props.controller.startReviewPreparation(intent, underlyingResultRoute);
2312
2306
  setFocus(0);
@@ -2404,9 +2398,10 @@ export function ConsoleAppV4(props) {
2404
2398
  : draft.action === undefined
2405
2399
  && !props.controller.pluginHasInstallableTarget(candidate.plugin.pluginId)
2406
2400
  && props.controller.listPluginWriteBatchTargets('uninstall', [candidate.plugin.pluginId]).length === 0
2401
+ && props.controller.listPluginWriteBatchTargets('cleanup', [candidate.plugin.pluginId]).length === 0
2407
2402
  && !batchTaskPluginIds.has(candidate.plugin.pluginId)
2408
2403
  ? '没有可用于批量处理的实际目标'
2409
- : draft.action === 'install' && candidate.plugin.configOnly
2404
+ : draft.action === 'install' && candidate.plugin.catalogEntryMissing
2410
2405
  ? '来源中没有可信插件定义'
2411
2406
  : draft.action === 'install' && candidate.plugin.sourceUnavailable
2412
2407
  ? '插件来源不可用'
@@ -2425,13 +2420,71 @@ export function ConsoleAppV4(props) {
2425
2420
  ...(byId.get(pluginId) ? { plugin: byId.get(pluginId) } : {}),
2426
2421
  }) !== undefined);
2427
2422
  };
2428
- const persistUnifiedDependencyGraphContext = (pluginId, projection, expandedPluginIds, focusIndex, offset) => {
2429
- const actionIds = unifiedDependencyGraphActionIds(projection);
2430
- dependencyGraphContexts.current.set(dependencyGraphContextKey(pluginId, 'dependencies'), {
2423
+ const persistUnifiedDependencyGraphContext = (pluginId, layout, expandedPluginIds, focusIndex, offset, portable = false) => {
2424
+ dependencyGraphContexts.current.set(dependencyGraphContextKey(pluginId, portable ? 'portable' : 'dependencies'), {
2431
2425
  expandedPluginIds: new Set(expandedPluginIds),
2432
- ...(actionIds[focusIndex] ? { focusedActionId: actionIds[focusIndex] } : {}),
2433
- ...(actionIds[offset] ? { scrollAnchorActionId: actionIds[offset] } : {}),
2426
+ ...(layout.rows[focusIndex] ? { focusedRowId: layout.rows[focusIndex].id } : {}),
2427
+ ...(layout.rows[offset] ? { scrollAnchorRowId: layout.rows[offset].id } : {}),
2428
+ });
2429
+ };
2430
+ const renderDependencyGraph = (pluginId, portable) => {
2431
+ graphProjection = buildUnifiedDependencyGraphProjection({
2432
+ overviews: props.controller.listPluginDependencyOverviews(),
2433
+ rootPluginId: pluginId,
2434
+ ...(expandedDependencyPluginIds ? { expandedPluginIds: expandedDependencyPluginIds } : {}),
2435
+ });
2436
+ graphRowBudget = pageActionBudget(3
2437
+ + Number(portable && snapshot.portableDraft !== undefined)
2438
+ + (graphProjection.status.state === 'invalid' ? 4 : 0));
2439
+ const ordered = projectDependencyGraphView(graphProjection, {
2440
+ columns: size.columns - 2, rowBudget: graphRowBudget,
2434
2441
  });
2442
+ const currentId = ordered.rows[focus]?.id;
2443
+ const anchorId = ordered.rows[scrollOffset]?.id;
2444
+ graphLayout = projectDependencyGraphView(graphProjection, {
2445
+ columns: size.columns - 2, rowBudget: graphRowBudget,
2446
+ focusRowId: currentId, scrollAnchorRowId: anchorId,
2447
+ });
2448
+ const layout = graphLayout;
2449
+ const projection = graphProjection;
2450
+ actions = layout.rows.map((row) => ({
2451
+ actionId: row.id,
2452
+ label: row.node.displayName,
2453
+ run: () => {
2454
+ persistUnifiedDependencyGraphContext(pluginId, layout, expandedDependencyPluginIds ?? new Set([
2455
+ ...projection.dependencies.defaultExpandedPluginIds,
2456
+ ...projection.dependents.defaultExpandedPluginIds,
2457
+ ]), focus, layout.viewport.offset, portable);
2458
+ setGraphEvidenceOpen(false);
2459
+ setGraphPortableSelectionOpen(false);
2460
+ go({ name: 'pluginDetail', pluginId: row.node.pluginId });
2461
+ },
2462
+ }));
2463
+ const selected = layout.rows[focus] ?? layout.rows[layout.focusIndex];
2464
+ if (portable && graphPortableSelectionOpen && snapshot.portableDraft) {
2465
+ readingText = [
2466
+ `本次选择 ${snapshot.portableDraft.selectedCount} 个 Skills`,
2467
+ ...snapshot.portableDraft.candidates.filter((candidate) => candidate.selected)
2468
+ .map((candidate) => `${candidate.name} · ${candidate.providerDisplayName} · ${candidate.providerPluginId}`),
2469
+ '安装位置在最终汇总中确认。',
2470
+ ].join('\n');
2471
+ content = _jsx(Box, { flexDirection: "column", children: readingPage(readingText, size.columns - 2, pageActionBudget(2)) });
2472
+ help = '←→/PgUp/PgDn 翻页 · y 复制全文 · Esc 返回关系图';
2473
+ }
2474
+ else if (graphEvidenceOpen && selected?.node.edge) {
2475
+ readingText = [
2476
+ `${selected.node.displayName} · ${selected.node.edge.declared ? '已声明' : '当前已验证'}`,
2477
+ ...dependencyEvidenceLines(selected.node, props.controller.getPluginDetailSubject(selected.node.pluginId)),
2478
+ ].join('\n');
2479
+ content = _jsx(Box, { flexDirection: "column", children: readingPage(readingText, size.columns - 2, pageActionBudget(2)) });
2480
+ help = '←→/PgUp/PgDn 翻页 · y 复制全文 · Esc 返回关系图';
2481
+ }
2482
+ else {
2483
+ const selectedHasEdge = selected?.node.edge !== undefined;
2484
+ help = `↑↓ 选择节点 · → 展开 · ← 收起 · Enter 插件详情${selectedHasEdge ? ' · r 引用证据' : ''}${portable ? ' · p 已选 Skills' : ''} · Esc 返回`;
2485
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(StaticRow, { title: "\u68C0\u67E5\u8303\u56F4", value: "\u5F53\u524D\u9879\u76EE\u548C\u4E2A\u4EBA\u4F4D\u7F6E\uFF1B\u5176\u4ED6\u9879\u76EE\u672A\u786E\u8BA4", status: "muted", compact: true }), projection.status.state === 'invalid'
2486
+ ? _jsx(Callout, { status: "error", title: "\u4F9D\u8D56\u5173\u7CFB\u65E0\u6548", description: projection.status.message }) : null, _jsx(DependencyGraphView, { projection: projection, columns: size.columns - 2, rowBudget: graphRowBudget, focusRowId: layout.focusRowId, scrollAnchorRowId: layout.scrollAnchorRowId }), selectedHasEdge ? _jsxs(Box, { width: size.columns - 2, justifyContent: "space-between", children: [_jsx(Text, { color: tuiTheme.colors.muted, children: "\u67E5\u770B\u5F53\u524D\u8FB9\u7684\u5B8C\u6574\u4F4D\u7F6E\u5F15\u7528" }), _jsx(KeyCap, { children: "r" })] }) : null, portable && snapshot.portableDraft ? _jsxs(Text, { color: tuiTheme.colors.muted, children: ["\u672C\u6B21\u9009\u62E9 ", snapshot.portableDraft.selectedCount, " \u4E2A Skills\uFF1B\u5B89\u88C5\u4F4D\u7F6E\u5728\u6700\u7EC8\u6C47\u603B\u786E\u8BA4\u3002"] }) : null] });
2487
+ }
2435
2488
  };
2436
2489
  const toggleBatchPlugin = (pluginId) => {
2437
2490
  setBatchDraft((current) => current ? {
@@ -2441,19 +2494,6 @@ export function ConsoleAppV4(props) {
2441
2494
  : [...current.selectedPluginIds, pluginId],
2442
2495
  } : current);
2443
2496
  };
2444
- const prepareCurrentProjectPlugins = () => {
2445
- if (props.controller.listCurrentProjectIntentRoots().length === 0) {
2446
- setActionError('当前项目没有已保存的插件清单。');
2447
- return;
2448
- }
2449
- beginReview({
2450
- kind: 'pluginWriteBatch',
2451
- action: 'sync',
2452
- selection: { kind: 'currentProjectIntentRoots' },
2453
- platform: 'both',
2454
- scope: 'project',
2455
- });
2456
- };
2457
2497
  const prepareCurrentProjectTargets = (targets) => {
2458
2498
  if (targets.length === 0) {
2459
2499
  setActionError('当前项目没有已确认可同步的目标。');
@@ -3427,7 +3467,7 @@ export function ConsoleAppV4(props) {
3427
3467
  const plugins = props.controller.listPlugins({ mode: 'browse' });
3428
3468
  const attention = new Set(plugins.filter((entry) => entry.primaryAttention).map((entry) => entry.pluginId)).size;
3429
3469
  const unknown = new Set(plugins.filter((entry) => entry.supportedPlatforms.some((platform) => pluginPlatformUnknown(entry, platform))).map((entry) => entry.pluginId)).size;
3430
- title = '';
3470
+ title = '管理 Agent Workflows 插件';
3431
3471
  description = undefined;
3432
3472
  context = undefined;
3433
3473
  const pluginState = [home.pluginUpdateCount ? `↑ ${home.pluginUpdateCount} 可更新` : '',
@@ -3438,13 +3478,15 @@ export function ConsoleAppV4(props) {
3438
3478
  : [inventory.counts.blocked ? `! ${inventory.counts.blocked} 受阻` : '',
3439
3479
  inventory.counts.pending ? `↻ ${inventory.counts.pending} 待同步` : '',
3440
3480
  inventory.counts.unknown ? `? ${inventory.counts.unknown} 未确认` : ''].filter(Boolean).join(' · ') || '✓ 全部就绪';
3481
+ const visibleProjectState = snapshot.projectInventoryRefresh.state === 'error'
3482
+ ? `! 项目检查失败,显示上次检查结果 · ${projectState}` : projectState;
3441
3483
  const sourceState = [home.catalogUpdateCount ? `↑ ${home.catalogUpdateCount} 有更新` : '',
3442
3484
  home.catalogRecoveryCount ? `! ${home.catalogRecoveryCount} 检查失败` : '',
3443
3485
  home.unavailableSourceCount ? `! ${home.unavailableSourceCount} 不可用` : '',
3444
3486
  home.catalogCheckingCount ? `${home.catalogCheckingCount} 检查中` : ''].filter(Boolean).join(' · ') || '✓ 当前可用';
3445
3487
  actions = [
3446
3488
  { label: '插件', description: pluginState, run: () => go({ name: 'pluginList', mode: 'browse' }) },
3447
- { label: '项目插件', description: projectState, run: () => go({ name: 'projectStatus' }) },
3489
+ { label: '项目插件', description: visibleProjectState, run: () => go({ name: 'projectStatus' }) },
3448
3490
  { label: '插件来源', description: sourceState, run: () => go({ name: 'sourceList' }) },
3449
3491
  ];
3450
3492
  content = _jsx(Box, { flexDirection: "column", children: actions.map((action, index) => _jsx(Box, { marginBottom: 1, children: _jsx(HomeNavTile, { width: size.columns - 2, title: action.label, focused: !projectReminderOpen && focus === index, children: _jsx(Text, { wrap: "wrap", children: action.description?.split(' · ').map((state, stateIndex) => _jsxs(Text, { color: state.startsWith('↑') ? tuiTheme.roles.change : state.startsWith('!') || state.startsWith('↻') ? tuiTheme.roles.warning
@@ -3453,54 +3495,123 @@ export function ConsoleAppV4(props) {
3453
3495
  else if (route.name === 'projectStatus') {
3454
3496
  const inventory = snapshot.projectInventory;
3455
3497
  const section = snapshot.router.current.context.projectSection ?? 'declared';
3498
+ const statusFilter = snapshot.router.current.context.projectStatusFilter ?? 'all';
3456
3499
  const stateLabel = { blocked: '受阻', pending: '待同步', unknown: '未确认', ready: '就绪' };
3457
- const roots = props.controller.listProjectInventoryRoots(section).filter((root) => !query || `${root.displayName} ${root.pluginId}`.toLocaleLowerCase().includes(query.toLocaleLowerCase()));
3500
+ const searchedRoots = props.controller.listProjectInventoryRoots(section).filter((root) => queryMatches(query, root.displayName, root.pluginId, root.sourceId));
3501
+ const roots = searchedRoots.filter((root) => statusFilter === 'all' || root.state === statusFilter);
3502
+ const statusCounts = Object.fromEntries(PROJECT_STATUS_FILTERS.map((filter) => [filter,
3503
+ filter === 'all' ? searchedRoots.length : searchedRoots.filter((root) => root.state === filter).length,
3504
+ ]));
3505
+ toggleProjectStatusFilter = () => {
3506
+ const available = PROJECT_STATUS_FILTERS.filter((filter) => filter === 'all' || filter === statusFilter || statusCounts[filter] > 0);
3507
+ const next = available[(available.indexOf(statusFilter) + 1) % available.length];
3508
+ props.controller.updatePageContext({ projectStatusFilter: next });
3509
+ dynamicMenuFocus.current = undefined;
3510
+ actionJumpAnchor.current = undefined;
3511
+ setFocus(0);
3512
+ setScrollOffset(0);
3513
+ };
3514
+ moveProjectSection = () => {
3515
+ const next = section === 'declared' ? 'extraInstalled' : 'declared';
3516
+ props.controller.updatePageContext({ projectSection: next });
3517
+ dynamicMenuFocus.current = undefined;
3518
+ actionJumpAnchor.current = undefined;
3519
+ setFocus(0);
3520
+ setScrollOffset(0);
3521
+ };
3458
3522
  breadcrumb = '项目插件';
3459
3523
  title = showTechnical ? '项目状态安全详情' : inventory?.projectName ?? '项目插件';
3460
3524
  context = undefined;
3461
- description = inventory?.declarationState === 'notProject' ? '当前不是项目;没有项目同步目标。'
3462
- : inventory?.declarationState === 'absent' ? '未配置项目插件。'
3463
- : snapshot.projectInventoryRefresh.state === 'error' ? '项目检查失败;可重新检查。'
3525
+ description = snapshot.projectInventoryRefresh.state === 'error'
3526
+ ? '项目检查失败;以下显示上次检查结果。可重新检查。'
3527
+ : inventory?.declarationState === 'notProject' ? '当前不是项目;没有项目同步目标。'
3528
+ : inventory?.declarationState === 'absent' ? '未配置项目插件。'
3464
3529
  : inventory?.declarationState === 'invalid' || inventory?.declarationState === 'unavailable' ? '项目声明需要检查。' : undefined;
3465
3530
  actions = roots.map((root) => ({
3466
3531
  actionId: `project-root:${root.pluginId}`, pluginId: root.pluginId,
3467
3532
  label: root.displayName,
3468
- description: [root.targets.map((target) => `${platformLabel(target.platform)} · ${target.location === 'sharedUser' ? '共用个人安装' : '当前项目'} · ${stateLabel[target.state]}`).join(';'),
3469
- ...(root.dependencies.some((dependency) => dependency.targets.some((target) => target.location === 'sharedUser'))
3470
- ? [`${root.dependencies.filter((dependency) => dependency.targets.some((target) => target.location === 'sharedUser')).length} 个依赖共用个人安装`] : [])].join('\n'),
3471
- maxDescriptionRows: 2,
3472
- sourceLabel: root.sourceId,
3473
3533
  run: () => go({ name: 'pluginDetail', pluginId: root.pluginId }),
3474
3534
  }));
3475
3535
  const syncTargets = inventory?.syncTargets ?? [];
3476
3536
  const syncCount = new Set(syncTargets.map((target) => target.pluginId)).size;
3537
+ const projectRestoreIds = [...new Set(props.controller.listPluginWriteTaskCandidates('restore')
3538
+ .filter((candidate) => candidate.target.scope === 'project')
3539
+ .map((candidate) => candidate.target.pluginId))];
3477
3540
  bottomActions = [
3478
3541
  ...(inventory?.projectRoot && syncCount > 0 ? [{ actionId: 'project-sync-actionable',
3479
- label: `同步待就绪插件(${syncCount}) [c]`, run: () => prepareCurrentProjectTargets(syncTargets) }] : []),
3480
- ...(inventory?.counts.blocked ? [{ actionId: 'project-open-sources', label: '处理插件来源', run: () => go({ name: 'sourceList' }) }] : []),
3481
- ...(!showTechnical && (inventory?.counts.blocked || inventory?.counts.unknown || snapshot.projectIntentIssue) ? [{ actionId: 'project-error-details', label: '错误详情', run: () => { setShowTechnical(true); setBatchTargetOffset(0); } }] : []),
3542
+ label: `同步全部可处理声明(${syncCount}) [c]`, status: 'info',
3543
+ run: () => prepareCurrentProjectTargets(syncTargets) }] : []),
3544
+ ...(projectRestoreIds.length > 0 ? [{ actionId: 'project-restore',
3545
+ label: `修复当前项目(${projectRestoreIds.length})`,
3546
+ run: () => openSelectedPluginTask('restore', projectRestoreIds, 'project') }] : []),
3547
+ ...(() => {
3548
+ const issues = props.controller.listUserIssues().filter((issue) => props.controller.listProjectInventoryRoots('declared').some((root) => issueMatchesTarget(issue, root.pluginId)));
3549
+ if (issues.length === 0)
3550
+ return [];
3551
+ const focusedRoot = roots[focus];
3552
+ const selectedIssue = focusedRoot
3553
+ ? issues.find((issue) => issueMatchesTarget(issue, focusedRoot.pluginId)) : undefined;
3554
+ const issue = selectedIssue ?? (issues.length === 1 ? issues[0] : undefined);
3555
+ return [{ actionId: 'project-open-issues',
3556
+ label: issue ? '查看对应问题' : '查看需要处理',
3557
+ run: () => go(issue
3558
+ ? { name: 'issueDetail', issueId: issue.issueId } : { name: 'issueList' }) }];
3559
+ })(),
3560
+ ...(!showTechnical && (inventory?.counts.blocked || inventory?.counts.unknown || snapshot.projectIntentIssue) ? [{ actionId: 'project-error-details', label: '错误详情', run: toggleTechnical }] : []),
3482
3561
  { actionId: 'project-recheck', label: snapshot.projectInventoryRefresh.state === 'checking' ? '正在检查项目' : '重新检查项目 [r]',
3483
3562
  disabled: snapshot.projectInventoryRefresh.state === 'checking' ? '检查中' : undefined,
3484
3563
  run: async () => { await props.controller.refreshProjectInventory(); } },
3485
3564
  ];
3486
3565
  const technicalText = [snapshot.projectIntentIssue ? `${safeDiagnosticCode(snapshot.projectIntentIssue.code, 'projectIntentUnknown')} · ${sanitizeDiagnosticText(snapshot.projectIntentIssue.message)}` : '',
3487
- ...roots.flatMap((root) => [...projectDependencyLines(root), ...root.targets.flatMap((target) => [
3566
+ ...props.controller.listProjectInventoryRoots(section).flatMap((root) => [...projectDependencyLines(root), ...root.targets.flatMap((target) => [
3488
3567
  `${root.pluginId} · ${platformLabel(target.platform)} · ${scopeLabel(target.scope)}`,
3489
- `期望 ${target.expectedVersion ?? '未知'} · 实际 ${target.installedVersion ?? '未知'} · ${stateLabel[target.state]}`,
3568
+ `目录参考 ${target.expectedVersion ?? '未知'} · 实际 ${target.installedVersion ?? '未知'} · ${stateLabel[target.state]}`,
3490
3569
  ...target.reasonCodes.map((code) => `原因 · ${safeDiagnosticCode(code, 'projectTargetUnknown')}`)
3491
3570
  ])])].filter(Boolean).join('\n');
3492
3571
  const itemCount = showTechnical ? 0 : actions.length;
3493
3572
  actions = [...(showTechnical ? [] : actions), ...bottomActions];
3573
+ dynamicActionIds = actions.map((action) => action.actionId);
3574
+ dynamicActionSignature = dynamicActionIds.join('\0');
3575
+ const previousProjectFocus = dynamicMenuFocus.current;
3576
+ if (previousProjectFocus?.routeKey === routeKey
3577
+ && previousProjectFocus.actionIds !== dynamicActionSignature) {
3578
+ const retained = previousProjectFocus.focusedActionId
3579
+ ? dynamicActionIds.indexOf(previousProjectFocus.focusedActionId) : -1;
3580
+ effectiveFocus = retained >= 0 ? retained
3581
+ : itemCount > 0 ? 0 : dynamicActionIds.indexOf('project-recheck');
3582
+ }
3494
3583
  help = '↑↓ 移动 · Enter 查看 · Esc 返回';
3495
3584
  if (showTechnical) {
3496
3585
  readingText = technicalText;
3497
- const lines = wrapConsoleText(technicalText, size.columns - 2);
3498
- const pageRows = pageActionBudget(3);
3499
- referenceDetailPagination = { total: lines.length, pageRows };
3500
- content = _jsxs(Box, { flexDirection: "column", children: [lines.slice(batchTargetOffset, batchTargetOffset + pageRows).map((line, index) => _jsx(Text, { children: line }, index)), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] });
3586
+ content = _jsxs(Box, { flexDirection: "column", children: [readingPage(technicalText, size.columns - 2, Math.max(1, pageActionBudget(4) - 1)), _jsx(SemanticRows, { actions: actions, focus: effectiveFocus, kind: "action", compact: true })] });
3587
+ }
3588
+ else {
3589
+ const cardWidth = size.columns - 2;
3590
+ const lineLimit = Math.max(1, Math.min(3, pageActionBudget(3) - 4));
3591
+ const cardSubjects = roots.map((root) => props.controller.getPluginDetailSubject(root.pluginId));
3592
+ const cardLayouts = roots.map((root, index) => projectRootCardLayout(root, cardSubjects[index], cardWidth));
3593
+ entityCardNavigation = { itemCount, factRows: cardLayouts.map((card) => card.factRows), lineLimit };
3594
+ const emptyHint = roots.length === 0 && inventory?.declarationState === 'ready'
3595
+ && Boolean(searchedRoots.length > 0 || query || statusFilter !== 'all');
3596
+ const projectViewport = rowViewport({
3597
+ rowHeights: [...cardLayouts.map((card, index) => entityCardWindowHeight(card, index === effectiveFocus ? batchTargetOffset : 0, lineLimit)), ...bottomActions.map(() => 1)],
3598
+ focus: effectiveFocus, requestedOffset: scrollOffset,
3599
+ rowBudget: pageActionBudget(3 + Number(emptyHint)),
3600
+ });
3601
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(QueryHeader, { width: size.columns - 2, tabs: { items: [
3602
+ { id: 'declared', label: '项目声明', count: props.controller.listProjectInventoryRoots('declared').filter((root) => queryMatches(query, root.displayName, root.pluginId, root.sourceId)).length },
3603
+ { id: 'extraInstalled', label: '额外安装', count: props.controller.listProjectInventoryRoots('extraInstalled').filter((root) => queryMatches(query, root.displayName, root.pluginId, root.sourceId)).length },
3604
+ ], currentId: section }, search: { value: query, placeholder: '名称或来源', input: searchFocus
3605
+ ? _jsx(InputDisplay, { value: query, cursor: searchCursor, columns: querySearchWidth(size.columns - 2) }) : undefined }, filter: { label: PROJECT_STATUS_LABELS[statusFilter], count: statusCounts[statusFilter],
3606
+ currentZero: statusCounts[statusFilter] === 0 && statusFilter !== 'all' } }), emptyHint
3607
+ ? _jsx(Text, { color: tuiTheme.roles.secondary, children: "\u5F53\u524D\u6761\u4EF6\u65E0\u5339\u914D\uFF1B\u641C\u7D22\u548C\u7B5B\u9009\u4FDD\u6301\u539F\u6837\u3002" }) : null, _jsx(ContentViewport, { children: actions.slice(projectViewport.offset, projectViewport.offset + projectViewport.limit)
3608
+ .map((action, visibleIndex) => {
3609
+ const index = projectViewport.offset + visibleIndex;
3610
+ if (index >= itemCount)
3611
+ return _jsx(SemanticRows, { actions: actions, focus: searchFocus ? -1 : effectiveFocus, kind: "action", compact: true, offset: index, limit: 1 }, action.actionId);
3612
+ return _jsx(ProjectRootCard, { root: roots[index], subject: cardSubjects[index], width: cardWidth, focused: !searchFocus && effectiveFocus === index, lineOffset: effectiveFocus === index ? batchTargetOffset : 0, lineLimit: lineLimit }, action.actionId);
3613
+ }) })] });
3501
3614
  }
3502
- else
3503
- content = _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(KeyCap, { children: "f" }), " ", section === 'declared' ? '项目声明' : '额外安装', " \u00B7 ", roots.length] }), _jsxs(Text, { children: ["\u641C\u7D22 ", _jsx(KeyCap, { children: "/" }), " ", searchFocus ? _jsx(InputDisplay, { value: query, cursor: searchCursor, columns: size.columns - 14 }) : query || '名称或来源'] }), roots.length === 0 ? _jsx(Text, { color: tuiTheme.roles.secondary, children: "\u672C\u6761\u4EF6\u65E0\u5339\u914D" }) : null, _jsx(SequenceRows, { actions: actions, itemCount: itemCount, focus: focus, scrollOffset: scrollOffset, columns: size.columns - 2, rowBudget: pageActionBudget(2 + (roots.length === 0 ? 1 : 0)), compact: policy.mode === 'compact', cards: true, inactive: searchFocus })] });
3504
3615
  }
3505
3616
  else if (route.name === 'pluginList') {
3506
3617
  const sourceId = route.sourceId ?? 'all';
@@ -3518,8 +3629,15 @@ export function ConsoleAppV4(props) {
3518
3629
  installFilter: filter,
3519
3630
  }).length,
3520
3631
  ]));
3632
+ const sourceTabs = [{ id: 'all', label: '全部', count: props.controller.listPlugins({
3633
+ mode: route.mode, ...(query ? { query } : {}), installFilter,
3634
+ }).length }, ...props.controller.listSources().map((source) => ({
3635
+ id: source.id, label: source.label,
3636
+ count: props.controller.listPlugins({ mode: route.mode, sourceId: source.id,
3637
+ ...(query ? { query } : {}), installFilter }).length,
3638
+ }))];
3521
3639
  breadcrumb = '插件';
3522
- title = `插件 · ${items.length} 个 [m] 多选`;
3640
+ title = `插件 · ${items.length} 个`;
3523
3641
  description = undefined;
3524
3642
  context = `${items.length} 个插件`;
3525
3643
  help = '↑↓ 移动 · Enter 查看 · Esc 返回';
@@ -3578,9 +3696,6 @@ export function ConsoleAppV4(props) {
3578
3696
  .filter((candidate) => candidate.eligible && listedPluginIds.has(candidate.target.pluginId));
3579
3697
  const recommendedUpdatePluginIds = [...new Set(recommendedUpdateCandidates
3580
3698
  .map((candidate) => candidate.target.pluginId))];
3581
- const projectRestoreCandidates = props.controller.listPluginWriteTaskCandidates('restore')
3582
- .filter((candidate) => candidate.target.scope === 'project');
3583
- const projectIntentRoots = props.controller.listCurrentProjectIntentRoots();
3584
3699
  const menuActions = [
3585
3700
  ...(props.controller.listUserIssues().length > 0 ? [{ actionId: 'plugin-list-issues',
3586
3701
  label: '需要处理', run: () => go({ name: 'issueList' }) }] : []),
@@ -3588,29 +3703,13 @@ export function ConsoleAppV4(props) {
3588
3703
  actionId: 'plugin-list-update',
3589
3704
  label: `更新 ${recommendedUpdatePluginIds.length} 个`,
3590
3705
  description: '使用已验证版本和现有安装位置,直接查看任务确认',
3591
- status: 'warning',
3592
- run: () => openRecommendedUpdateTask(recommendedUpdatePluginIds),
3593
- }] : []),
3594
- ...(projectIntentRoots.length > 0 ? [{
3595
- actionId: 'plugin-list-project',
3596
- label: '项目清单',
3597
- description: `${projectIntentRoots.length} 个已保存根插件;允许补齐当前项目缺少的安装`,
3598
3706
  status: 'info',
3599
- run: prepareCurrentProjectPlugins,
3600
- }] : []),
3601
- ...(projectRestoreCandidates.length > 0 ? [{
3602
- actionId: 'plugin-list-repair-project',
3603
- label: '修复项目',
3604
- description: '严格按已确认的现有版本修复异常,不改变插件版本',
3605
- status: 'warning',
3606
- run: () => openSelectedPluginTask('restore', [...new Set(projectRestoreCandidates
3607
- .map((candidate) => candidate.target.pluginId))], 'project'),
3707
+ run: () => openRecommendedUpdateTask(recommendedUpdatePluginIds),
3608
3708
  }] : []),
3609
3709
  {
3610
3710
  actionId: 'plugin-list-multi',
3611
3711
  label: '选择多个插件 [m]',
3612
3712
  description: '先选择插件,再选择安装、更新、恢复、卸载或维护任务',
3613
- status: 'info',
3614
3713
  run: () => openBatchSelection(),
3615
3714
  },
3616
3715
  ];
@@ -3712,9 +3811,9 @@ export function ConsoleAppV4(props) {
3712
3811
  }).height : 1),
3713
3812
  focus, requestedOffset: scrollOffset, rowBudget: pageActionBudget(3),
3714
3813
  });
3715
- content = (_jsxs(Box, { flexDirection: "column", children: [_jsx(SourceTabs, { controller: props.controller, current: sourceId, compact: policy.mode === 'compact', columns: size.columns - 2, mode: route.mode, query: query || undefined, installFilter: installFilter }), _jsxs(Text, { color: searchFocus ? tuiTheme.colors.accent : tuiTheme.colors.muted, children: ["\u641C\u7D22 ", _jsx(KeyCap, { children: "/" }), " ", searchFocus
3716
- ? _jsx(InputDisplay, { value: query, cursor: searchCursor, placeholder: "\u8F93\u5165\u641C\u7D22\u5185\u5BB9", columns: Math.max(8, size.columns - 14) })
3717
- : _jsxs(_Fragment, { children: [query || '输入名称或用途', query ? ' / 编辑' : ''] })] }), _jsxs(Text, { wrap: "truncate-end", color: tuiTheme.colors.muted, children: ["\u7B5B\u9009 ", _jsxs(Text, { color: tuiTheme.colors.accent, children: ["[", pluginFilterLabel(installFilter), " ", filterCounts[installFilter], "]"] }), " ", _jsx(KeyCap, { children: "f" }), " \u5207\u6362"] }), sourceError
3814
+ content = (_jsxs(Box, { flexDirection: "column", children: [_jsx(QueryHeader, { width: size.columns - 2, tabs: { label: '来源', items: sourceTabs, currentId: sourceId }, search: { value: query, placeholder: '名称或用途', input: searchFocus
3815
+ ? _jsx(InputDisplay, { value: query, cursor: searchCursor, placeholder: "\u8F93\u5165\u641C\u7D22\u5185\u5BB9", columns: querySearchWidth(size.columns - 2) }) : undefined }, filter: { label: pluginFilterLabel(installFilter), count: filterCounts[installFilter],
3816
+ currentZero: installFilter !== 'all' && filterCounts[installFilter] === 0 } }), sourceError
3718
3817
  ? _jsx(Callout, { status: "error", title: `${sourceId} 暂时无法提供插件`, description: "\u5F53\u524D\u4E0D\u80FD\u6D4F\u89C8\u8FD9\u4E2A\u6765\u6E90\u3002" })
3719
3818
  : route.mode === 'installed' && items.length === 0 && !query && sourceId === 'all'
3720
3819
  ? installFilter === 'updateAvailable'
@@ -3741,11 +3840,21 @@ export function ConsoleAppV4(props) {
3741
3840
  const selected = new Set(batchDraft.selectedPluginIds);
3742
3841
  const currentIds = new Set(candidates.map((candidate) => candidate.pluginId));
3743
3842
  const hiddenSelected = batchDraft.selectedPluginIds.filter((pluginId) => !currentIds.has(pluginId)).length;
3843
+ const batchSearchDraft = { ...batchDraft, showSelected: false };
3844
+ const batchFilterCounts = Object.fromEntries(PLUGIN_FILTERS.map((filter) => [filter,
3845
+ batchCandidates({ ...batchSearchDraft, installFilter: filter }).length,
3846
+ ]));
3847
+ const batchSourceTabs = [{ id: 'all', label: '全部', count: batchCandidates({ ...batchSearchDraft,
3848
+ sourceId: undefined }).length }, ...props.controller.listSources().map((source) => ({
3849
+ id: source.id, label: source.label,
3850
+ count: batchCandidates({ ...batchSearchDraft, sourceId: source.id }).length,
3851
+ }))];
3744
3852
  const candidateActions = candidates.map((candidate) => {
3745
3853
  const blockReason = batchCandidateBlockReason(batchDraft, candidate);
3746
3854
  const isSelected = selected.has(candidate.pluginId);
3747
3855
  const plugin = candidate.plugin;
3748
3856
  return {
3857
+ actionId: `batch:${candidate.pluginId}`,
3749
3858
  label: plugin
3750
3859
  ? plugin.displayName
3751
3860
  : `${candidate.pluginId}(已不可用,可取消选择)`,
@@ -3858,9 +3967,10 @@ export function ConsoleAppV4(props) {
3858
3967
  help = batchDraft.showSelectionTools
3859
3968
  ? '↑↓ 移动 · Enter 执行 · Esc 返回选择'
3860
3969
  : '↑↓ 移动 · Space 选择 · Enter 查看 · Esc 返回';
3861
- content = _jsxs(Box, { flexDirection: "column", children: [_jsx(SourceTabs, { controller: props.controller, current: batchDraft.showSelected ? 'all' : batchDraft.sourceId ?? 'all', compact: policy.mode === 'compact', columns: size.columns - 2, mode: batchDraft.mode, query: batchDraft.showSelected ? undefined : batchDraft.query || undefined, installFilter: batchDraft.showSelected ? 'all' : batchDraft.installFilter }), _jsxs(Text, { color: searchFocus ? tuiTheme.colors.accent : tuiTheme.colors.muted, children: ["\u641C\u7D22 [/] ", searchFocus
3862
- ? _jsx(InputDisplay, { value: batchDraft.query, cursor: searchCursor, placeholder: "\u8F93\u5165\u641C\u7D22\u5185\u5BB9", columns: Math.max(8, size.columns - 14) })
3863
- : _jsxs(_Fragment, { children: [batchDraft.query || '输入名称或用途', batchDraft.query ? ' / 编辑' : ''] })] }), _jsxs(Text, { color: tuiTheme.colors.muted, children: ["\u72B6\u6001 ", _jsxs(Text, { color: tuiTheme.colors.accent, children: ["[", pluginFilterLabel(batchDraft.installFilter), "]"] }), " [f] \u5207\u6362"] }), _jsxs(Text, { color: tuiTheme.colors.muted, children: ["\u5DF2\u9009 ", selected.size, " \u00B7 \u9690\u85CF\u5DF2\u9009 ", hiddenSelected, " [c] ", continueLabel] }), _jsx(SequenceRows, { actions: actions, itemCount: batchDraft.showSelectionTools ? 0 : candidateActions.length, focus: batchFocus, scrollOffset: scrollOffset, columns: size.columns - 2, rowBudget: pageActionBudget(4), compact: policy.mode === 'compact', cards: true, inactive: searchFocus })] });
3970
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(QueryHeader, { width: size.columns - 2, tabs: { label: '来源', items: batchSourceTabs,
3971
+ currentId: batchDraft.showSelected ? 'all' : batchDraft.sourceId ?? 'all' }, search: { value: batchDraft.query, placeholder: '名称或用途', input: searchFocus
3972
+ ? _jsx(InputDisplay, { value: batchDraft.query, cursor: searchCursor, placeholder: "\u8F93\u5165\u641C\u7D22\u5185\u5BB9", columns: querySearchWidth(size.columns - 2) }) : undefined }, filter: { label: pluginFilterLabel(batchDraft.installFilter), count: batchFilterCounts[batchDraft.installFilter],
3973
+ currentZero: batchDraft.installFilter !== 'all' && batchFilterCounts[batchDraft.installFilter] === 0 }, selection: { total: selected.size, hidden: hiddenSelected } }), _jsx(SequenceRows, { actions: actions, itemCount: batchDraft.showSelectionTools ? 0 : candidateActions.length, focus: batchFocus, scrollOffset: scrollOffset, columns: size.columns - 2, rowBudget: pageActionBudget(4), compact: policy.mode === 'compact', cards: true, inactive: searchFocus })] });
3864
3974
  }
3865
3975
  else {
3866
3976
  const selectedIds = batchDraft.selectedPluginIds;
@@ -3897,9 +4007,9 @@ export function ConsoleAppV4(props) {
3897
4007
  const uninstallTargets = props.controller.listPluginWriteBatchTargets('uninstall', selectedIds);
3898
4008
  if (batchDraft.stage === 'maintenance') {
3899
4009
  actions = [
3900
- { label: '同步已保存使用设置', description: `重新应用 ${syncTargets.length} 个已保存或现场目标`, run: () => startExistingBatch('sync') },
3901
- { label: '重新安装', description: `重新生成 ${props.controller.listPluginWriteBatchTargets('reinstall', selectedIds).length} 个已保存或现场目标`, run: () => startExistingBatch('reinstall') },
3902
- { label: '清理遗留内容', description: `${props.controller.listPluginWriteBatchTargets('cleanup', selectedIds).length} 个已核验位置;保留不属于插件或被共用的内容`, run: () => startExistingBatch('cleanup') },
4010
+ { label: '同步已保存使用设置', status: 'info', description: `重新应用 ${syncTargets.length} 个已保存或现场目标`, run: () => startExistingBatch('sync') },
4011
+ { label: '重新安装', status: 'info', description: `重新生成 ${props.controller.listPluginWriteBatchTargets('reinstall', selectedIds).length} 个已保存或现场目标`, run: () => startExistingBatch('reinstall') },
4012
+ { label: '清理遗留内容', status: 'error', description: `${props.controller.listPluginWriteBatchTargets('cleanup', selectedIds).length} 个已核验位置;保留不属于插件或被共用的内容`, run: () => startExistingBatch('cleanup') },
3903
4013
  {
3904
4014
  role: 'navigation', label: '返回任务选择',
3905
4015
  run: () => moveBatchStage('task'),
@@ -3917,7 +4027,7 @@ export function ConsoleAppV4(props) {
3917
4027
  ...(updates.length > 0 ? [{
3918
4028
  label: `更新所选中的 ${updateRoots} 个可更新插件`,
3919
4029
  description: '使用已验证版本和已经添加的位置',
3920
- status: 'warning',
4030
+ status: 'info',
3921
4031
  run: () => openSelectedPluginTask('update', selectedIds),
3922
4032
  }] : []),
3923
4033
  ...(restores.length > 0 ? [{
@@ -4245,8 +4355,10 @@ export function ConsoleAppV4(props) {
4245
4355
  title = '选择要安装的 Skills';
4246
4356
  description = undefined;
4247
4357
  const visibleCandidates = portableSelectionView === 'selected'
4248
- ? draft.candidates.filter((candidate) => candidate.selected)
4249
- : draft.candidates;
4358
+ ? portableVisibleCandidates.filter((candidate) => candidate.selected)
4359
+ : portableVisibleCandidates;
4360
+ const hiddenSelected = draft.candidates.filter((candidate) => candidate.selected
4361
+ && !visibleCandidates.includes(candidate)).length;
4250
4362
  const continuePortableSelection = () => {
4251
4363
  if (!draft.canContinue) {
4252
4364
  setPortableEmptySelectionOpen(true);
@@ -4255,6 +4367,7 @@ export function ConsoleAppV4(props) {
4255
4367
  go({ name: 'portableAgentMode', pluginId: plugin.pluginId });
4256
4368
  };
4257
4369
  const candidateActions = visibleCandidates.map((candidate) => ({
4370
+ actionId: `skill:${candidate.providerPluginId}:${candidate.name}`,
4258
4371
  label: draft.candidates.filter((entry) => entry.name === candidate.name).length > 1
4259
4372
  ? `${candidate.name} · ${candidate.providerDisplayName}` : candidate.name,
4260
4373
  selected: candidate.selected,
@@ -4307,16 +4420,18 @@ export function ConsoleAppV4(props) {
4307
4420
  const technicalVariant = focusedVariant ?? visibleVariant ?? variantCandidates[0];
4308
4421
  help = portableToolsOpen ? '↑↓ 移动 · Enter 选择 · Esc 返回'
4309
4422
  : '↑↓ 移动 · Space/Enter 切换 · Esc 返回';
4310
- content = _jsx(Box, { flexDirection: "column", children: _jsxs(ContentViewport, { children: [_jsx(SequenceRows, { actions: actions, itemCount: portableToolsOpen ? 0 : candidateActions.length, focus: focus, scrollOffset: scrollOffset, columns: size.columns - 2, rowBudget: pageActionBudget((variantCandidates.length > 0 ? 3 : 0) + (!draft.canContinue ? 1 : 0)
4311
- + (showTechnical && technicalVariant ? 5 : 0)), compact: policy.mode === 'compact', skillRows: true, inactive: portableEmptySelectionOpen }), !portableToolsOpen && variantCandidates.length > 0
4312
- ? showTechnical
4313
- ? _jsx(StaticRow, { title: `! ${variantCandidates.length} 个 Skill 存在范围变体`, status: "warning", compact: true })
4314
- : _jsx(Callout, { status: "warning", title: `! ${variantCandidates.length} 个 Skill 在不同安装范围会生成不同内容。选择范围后会重新确认最终内容。` })
4315
- : null, !portableToolsOpen && visibleCandidates.length === 0
4316
- ? _jsx(StaticRow, { title: "\u6CA1\u6709\u5DF2\u9009 Skill", description: "\u9009\u62E9\u201C\u67E5\u770B\u5168\u90E8\u5019\u9009\u201D\u540E\u53EF\u7EE7\u7EED\u52FE\u9009", status: "muted", compact: true })
4317
- : null, !portableToolsOpen && !draft.canContinue
4318
- ? _jsx(StaticRow, { title: "\u81F3\u5C11\u9009\u62E9\u4E00\u4E2A Skill", status: "warning", compact: true })
4319
- : null, !portableToolsOpen && showTechnical && technicalVariant ? _jsxs(TechnicalDetails, { title: "\u8303\u56F4\u53D8\u4F53", children: [_jsx(StaticRow, { title: `${technicalVariant.name} · ${portableScopeVariantLabel('project', technicalVariant.scopeDetails.project)}`, compact: true }, `${technicalVariant.name}:project`), _jsx(StaticRow, { title: `${technicalVariant.name} · ${portableScopeVariantLabel('user', technicalVariant.scopeDetails.user)}`, compact: true }, `${technicalVariant.name}:user`), variantCandidates.length > 1 ? _jsx(StaticRow, { title: `另有 ${variantCandidates.length - 1} 个 Skill 的范围变体已折叠`, status: "muted", compact: true }) : null] }) : null] }) });
4423
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(QueryHeader, { width: size.columns - 2, search: { value: query, placeholder: '名称或短描述', input: searchFocus
4424
+ ? _jsx(InputDisplay, { value: query, cursor: searchCursor, columns: querySearchWidth(size.columns - 2) }) : undefined }, selection: { total: draft.selectedCount, hidden: hiddenSelected } }), _jsxs(ContentViewport, { children: [_jsx(SequenceRows, { actions: actions, itemCount: portableToolsOpen ? 0 : candidateActions.length, focus: focus, scrollOffset: scrollOffset, columns: size.columns - 2, rowBudget: pageActionBudget(2 + (variantCandidates.length > 0 ? 3 : 0) + (!draft.canContinue ? 1 : 0)
4425
+ + (showTechnical && technicalVariant ? 5 : 0)), compact: policy.mode === 'compact', skillRows: true, inactive: portableEmptySelectionOpen }), !portableToolsOpen && variantCandidates.length > 0
4426
+ ? showTechnical
4427
+ ? _jsx(StaticRow, { title: `! ${variantCandidates.length} 个 Skill 存在范围变体`, status: "warning", compact: true })
4428
+ : _jsx(Callout, { status: "warning", title: `! ${variantCandidates.length} 个 Skill 在不同安装范围会生成不同内容。选择范围后会重新确认最终内容。` })
4429
+ : null, !portableToolsOpen && visibleCandidates.length === 0
4430
+ ? _jsx(StaticRow, { title: query ? '当前搜索没有匹配 Skill'
4431
+ : portableSelectionView === 'selected' ? '没有已选 Skill' : '当前没有可选 Skill', description: query ? '清除搜索后可查看其他候选' : '选择“查看全部候选”后可继续勾选', status: "muted", compact: true })
4432
+ : null, !portableToolsOpen && !draft.canContinue
4433
+ ? _jsx(StaticRow, { title: "\u81F3\u5C11\u9009\u62E9\u4E00\u4E2A Skill", status: "warning", compact: true })
4434
+ : null, !portableToolsOpen && showTechnical && technicalVariant ? _jsxs(TechnicalDetails, { title: "\u8303\u56F4\u53D8\u4F53", children: [_jsx(StaticRow, { title: `${technicalVariant.name} · ${portableScopeVariantLabel('project', technicalVariant.scopeDetails.project)}`, compact: true }, `${technicalVariant.name}:project`), _jsx(StaticRow, { title: `${technicalVariant.name} · ${portableScopeVariantLabel('user', technicalVariant.scopeDetails.user)}`, compact: true }, `${technicalVariant.name}:user`), variantCandidates.length > 1 ? _jsx(StaticRow, { title: `另有 ${variantCandidates.length - 1} 个 Skill 的范围变体已折叠`, status: "muted", compact: true }) : null] }) : null] })] });
4320
4435
  }
4321
4436
  else if (route.name === 'portablePreparing' && plugin) {
4322
4437
  breadcrumb = `${plugin.displayName} / 通用 Skills`;
@@ -4327,31 +4442,18 @@ export function ConsoleAppV4(props) {
4327
4442
  help = 'Esc 取消检查并返回目标平台';
4328
4443
  }
4329
4444
  else if (route.name === 'portableDependencies' && plugin && snapshot.portableDraft) {
4330
- const overview = dependencyOverview(plugin);
4331
- const dependencies = overview?.dependencies ?? [];
4332
4445
  breadcrumb = `${plugin.displayName} / 通用 Skills / 依赖`;
4333
- context = `${dependencies.length + 1} 个插件`;
4446
+ context = `${snapshot.portableDraft.selectedCount} 个 Skills`;
4334
4447
  title = '本次生成依赖';
4335
- description = '这些插件用于生成所选 Skills;安装位置在最终汇总中确认。';
4336
- const fields = [plugin.displayName, ...dependencies.flatMap((entry) => [
4337
- `${entry.displayName} · ${entry.pluginId}`,
4338
- `${entry.relation === 'direct' ? '直接依赖' : '间接依赖'} · ${entry.requiredBy.map((parent) => parent.displayName).join('、')} 需要它`,
4339
- `提供 ${snapshot.portableDraft.candidates.filter((candidate) => candidate.providerPluginId === entry.pluginId).length} 个 Skills`,
4340
- ])];
4341
- readingText = fields.join('\n');
4342
- const lines = fields.flatMap((field) => wrapConsoleText(field, size.columns - 2));
4343
- const pageRows = pageActionBudget(2);
4344
- referenceDetailPagination = { total: lines.length, pageRows };
4345
- const offset = Math.min(batchTargetOffset, Math.max(0, lines.length - 1));
4346
- actions = [{ label: '返回 Skills 选择', run: back }];
4347
- content = _jsxs(Box, { flexDirection: "column", children: [lines.slice(offset, offset + pageRows).map((line, index) => _jsx(Text, { children: line }, index)), _jsxs(Text, { color: tuiTheme.roles.secondary, children: [offset + 1, "\u2013", Math.min(lines.length, offset + pageRows), " / ", lines.length] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] });
4448
+ description = '关系图显示已知依赖;所选 Skills 和安装位置仍以最终汇总为准。';
4449
+ renderDependencyGraph(plugin.pluginId, true);
4348
4450
  }
4349
4451
  else if (route.name === 'portableAgentMode' && plugin && snapshot.portableDraft) {
4350
4452
  breadcrumb = `${plugin.displayName} / 通用 Skills / Agent`;
4351
4453
  title = '安装到哪些 Agent?';
4352
4454
  description = '可以自动选择检测到的 Agent,也可以限制到指定 Agent。';
4353
4455
  actions = [
4354
- { label: '使用 Skills CLI 默认选择', description: '继续 Skills CLI 默认选择', meta: '默认', selected: snapshot.portableDraft.agentMode === 'automatic', run: () => { props.controller.setPortableAgentMode('automatic'); go({ name: 'portableScope', pluginId: plugin.pluginId }); } },
4456
+ { label: '自动选择可用 Agent', description: '按当前环境确定可用 Agent,不承诺全部 Agent 可用', meta: '默认', selected: snapshot.portableDraft.agentMode === 'automatic', run: () => { props.controller.setPortableAgentMode('automatic'); go({ name: 'portableScope', pluginId: plugin.pluginId }); } },
4355
4457
  { label: '指定 Agent', description: '输入一个或多个 Agent ID', selected: snapshot.portableDraft.agentMode === 'explicit', run: () => go({ name: 'portableAgentInput', pluginId: plugin.pluginId }) },
4356
4458
  ];
4357
4459
  content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "choice", compact: policy.mode === 'compact' });
@@ -4430,21 +4532,29 @@ export function ConsoleAppV4(props) {
4430
4532
  const blockerActionViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, responsiveBudget(2, 4));
4431
4533
  content = _jsxs(Box, { flexDirection: "column", children: [showPortableRequirements ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: "\u5728 PATH \u4E2D\u63D0\u4F9B\u517C\u5BB9\u7684 skills \u53EF\u6267\u884C\u6587\u4EF6\u3002", compact: true }), _jsx(StaticRow, { title: `当前 AW adapter 支持:${SKILLS_CLI_SUPPORTED_VERSION_LABEL}`, compact: true }), _jsx(StaticRow, { title: "\u51C6\u5907\u5B8C\u6210\u540E\u8FD4\u56DE\u95EE\u9898\u6458\u8981\u5E76\u91CD\u65B0\u68C0\u67E5\u3002", compact: true }), _jsx(StaticRow, { title: "\u4E0D\u4F1A\u81EA\u52A8\u8FD0\u884C npx latest\u3001sudo \u6216\u5728\u7EBF\u5B89\u88C5\u811A\u672C\u3002", status: "warning", compact: true }), _jsx(StaticRow, { title: "\u9009\u5B9A\u63D2\u4EF6\u3001\u5B89\u88C5\u76EE\u6807\u548C Portable \u8349\u7A3F\u4FDD\u6301\u4E0D\u53D8\u3002", status: "success", compact: true }), _jsx(StaticRow, { title: "AW \u4E0D\u4F1A\u5728\u6B64\u9875\u5B89\u88C5\u5916\u90E8\u4F9D\u8D56\u3002", status: "warning", compact: true })] }) : _jsx(DiagnosticSummary, { diagnostic: diagnostic, technical: showTechnical, columns: size.columns, offset: showTechnical ? batchTargetOffset : undefined, limit: showTechnical ? technicalPageRows : undefined, compact: policy.mode === 'compact' }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: blockerActionViewport.offset, limit: blockerActionViewport.limit })] });
4432
4534
  }
4535
+ else if (route.name === 'pluginDependencies' && !plugin
4536
+ && route.view !== 'removeBlocked' && props.controller.getPluginDetailSubject(route.pluginId)) {
4537
+ const subject = props.controller.getPluginDetailSubject(route.pluginId);
4538
+ breadcrumb = `${subject.displayName} / 依赖关系`;
4539
+ title = '已知依赖关系';
4540
+ description = 'A → B 表示 A 需要 B;未验证关系不表示当前可用。';
4541
+ context = subject.relations.checkState === 'checked' ? '已检查' : '部分未确认';
4542
+ renderDependencyGraph(route.pluginId, false);
4543
+ }
4433
4544
  else if (route.name === 'pluginDependencies' && plugin) {
4434
4545
  const overview = dependencyOverview(plugin);
4435
4546
  const visibleDependents = route.view === 'removeBlocked' && route.platform && route.scope
4436
4547
  ? props.controller.getBlockingDependents(plugin.pluginId, route.platform, route.scope)
4437
4548
  : overview?.dependents ?? [];
4438
- const dependencyCount = overview?.dependencies.length ?? 0;
4439
4549
  const dependentCount = visibleDependents.length;
4440
4550
  const dependentLimit = responsiveBudget(1, 3);
4441
- const dependencyRows = (entries, limit, dependent = false, offset = 0) => {
4442
- const safeOffset = Math.min(offset, Math.max(0, entries.length - 1));
4443
- const visible = entries.slice(safeOffset, safeOffset + limit);
4444
- return _jsxs(_Fragment, { children: [safeOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${safeOffset} 项`, status: "muted", compact: true }) : null, visible.map((entry) => {
4551
+ const dependencyRows = (entries, limit, dependent = false) => {
4552
+ const page = pagedItems(entries.length, limit);
4553
+ const visible = entries.slice(page.offset, page.offset + limit);
4554
+ return _jsxs(_Fragment, { children: [visible.map((entry) => {
4445
4555
  const state = dependencyStatus(entry);
4446
4556
  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);
4447
- }), entries.length > safeOffset + visible.length ? _jsx(StaticRow, { title: `↓ 后面还有 ${entries.length - safeOffset - visible.length} 项`, status: "muted", compact: true }) : null] });
4557
+ }), page.pageCount > 1 ? _jsx(Pager, { page: page.page, pageCount: page.pageCount, columns: size.columns - 4 }) : null] });
4448
4558
  };
4449
4559
  if (route.view === 'removeBlocked') {
4450
4560
  if (!showTechnical)
@@ -4470,251 +4580,44 @@ export function ConsoleAppV4(props) {
4470
4580
  if (!showTechnical && visibleDependents.length > dependentLimit) {
4471
4581
  help = '←→ 查看全部被依赖关系 · ↑↓ 移动 · Enter 选择 · Esc 返回';
4472
4582
  }
4473
- content = _jsxs(Box, { flexDirection: "column", children: [_jsx(StaticRow, { title: `${dependentCount} 个已安装插件仍在使用`, status: "error", compact: true }), _jsx(StaticRow, { title: "\u6CA1\u6709\u4FEE\u6539\u4EFB\u4F55\u5B89\u88C5\u3002", status: "muted", compact: true }), showTechnical ? null : _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: "\u4F7F\u7528\u8FD9\u4E2A\u4F9D\u8D56\u7684\u63D2\u4EF6", meta: "\u53CD\u5411\u5F15\u7528", compact: true }), 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: [{
4583
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(StaticRow, { title: `${dependentCount} 个已安装插件仍在使用`, status: "error", compact: true }), _jsx(StaticRow, { title: "\u6CA1\u6709\u4FEE\u6539\u4EFB\u4F55\u5B89\u88C5\u3002", status: "muted", compact: true }), showTechnical ? null : _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: "\u4F7F\u7528\u8FD9\u4E2A\u4F9D\u8D56\u7684\u63D2\u4EF6", meta: "\u53CD\u5411\u5F15\u7528", compact: true }), 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: [{
4474
4584
  code: 'dependencyStillRequired',
4475
4585
  message: '已安装插件仍在使用这个依赖;请先处理依赖者。',
4476
4586
  }], compact: policy.mode === 'compact' }) : null] });
4477
4587
  }
4478
4588
  else {
4479
4589
  breadcrumb = `${plugin.displayName} / 依赖关系`;
4480
- const unified = buildUnifiedDependencyGraphProjection({
4481
- overviews: props.controller.listPluginDependencyOverviews(),
4482
- rootPluginId: plugin.pluginId,
4483
- ...(expandedDependencyPluginIds ? { expandedPluginIds: expandedDependencyPluginIds } : {}),
4484
- });
4485
- const visibleRows = unified.rows;
4486
4590
  context = '上下游';
4487
4591
  title = hasInstalledPlacement(plugin) ? '依赖与当前可用关系' : '安装所需插件';
4488
- description = 'A → B 表示 A 需要 B;当前插件只显示一次。';
4489
- const rowActions = visibleRows.map((row) => {
4490
- const state = row.depth === 0
4491
- ? { value: statusText(plugin), status: plugin.aggregateStatus === 'needsAttention'
4492
- ? 'warning'
4493
- : plugin.aggregateStatus === 'unavailable' ? 'error'
4494
- : plugin.aggregateStatus === 'available' ? 'muted' : 'success' }
4495
- : dependencyGraphRowStatus(row);
4496
- const branch = row.depth === 0 ? ''
4497
- : `${row.ancestorHasNextSibling.map((next) => next ? '│ ' : ' ').join('')}${row.lastChild ? '└─' : '├─'} `;
4498
- const compactBranch = policy.mode === 'compact' && row.depth > 2 ? '… └─ ' : branch;
4499
- return {
4500
- label: row.depth === 0
4501
- ? `${row.label}(当前插件)`
4502
- : `${row.branch === 'dependents' ? '↑ 使用者 ' : '↓ 所需 '}${compactBranch}${row.label}${row.sharedWithRowId ? '(共用,见上方)' : ''}`,
4503
- description: row.hiddenDescendantCount > 0
4504
- ? `另有 ${row.hiddenDescendantCount} 个${row.branch === 'dependencies' ? '所需插件' : '当前可用使用者'}`
4505
- : row.depth === 0 ? '当前插件 ★'
4506
- : row.branch === 'dependencies' ? '当前插件所需' : '需要当前插件',
4507
- meta: state.value,
4508
- status: state.status,
4509
- run: () => {
4510
- const direction = row.branch === 'dependents' ? 'dependents' : 'dependencies';
4511
- persistUnifiedDependencyGraphContext(plugin.pluginId, unified, expandedDependencyPluginIds ?? new Set([
4512
- ...unified.dependencies.defaultExpandedPluginIds,
4513
- ...unified.dependents.defaultExpandedPluginIds,
4514
- ]), focus, scrollOffset);
4515
- go({
4516
- name: 'pluginDependencyNode',
4517
- pluginId: plugin.pluginId,
4518
- nodePluginId: row.pluginId,
4519
- direction,
4520
- nodePathPluginIds: row.pathPluginIds,
4521
- });
4522
- },
4523
- };
4524
- });
4525
- const unverifiedConsumerActions = unified.dependents.scopeSummary.unknownConsumerPluginIds
4526
- .map((pluginId) => {
4527
- const declared = overview?.dependents.find((entry) => entry.pluginId === pluginId);
4528
- return {
4529
- label: declared?.displayName ?? pluginId,
4530
- description: '有依赖声明,但本次尚未确认依赖可用',
4531
- meta: declared?.aggregateStatus === 'notInstalled' ? '仅声明 · 未安装' : '仅声明 · 未验证',
4532
- status: 'muted',
4533
- run: () => {
4534
- persistUnifiedDependencyGraphContext(plugin.pluginId, unified, expandedDependencyPluginIds ?? new Set([
4535
- ...unified.dependencies.defaultExpandedPluginIds,
4536
- ...unified.dependents.defaultExpandedPluginIds,
4537
- ]), focus, scrollOffset);
4538
- go({
4539
- name: 'pluginDependencyNode',
4540
- pluginId: plugin.pluginId,
4541
- nodePluginId: pluginId,
4542
- direction: 'dependents',
4543
- });
4544
- },
4545
- };
4546
- });
4547
- actions = [
4548
- ...rowActions,
4549
- ...unverifiedConsumerActions,
4550
- { label: showTechnical ? '隐藏检查详情' : '查看检查详情', run: () => toggleTechnical() },
4551
- ];
4552
- const relationViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, showTechnical ? responsiveBudget(2, 5) : responsiveBudget(5, 9));
4553
- const focusedGraphRow = focus < rowActions.length ? visibleRows[focus] : undefined;
4554
- help = '↑↓ 选择 · → 展开 · ← 收起 · Enter 节点详情 · Esc 返回';
4555
- content = _jsxs(Box, { flexDirection: "column", children: [_jsx(StaticRow, { title: "\u68C0\u67E5\u8303\u56F4", value: "\u53EA\u68C0\u67E5\u5F53\u524D\u9879\u76EE\u548C\u4E2A\u4EBA\u4F4D\u7F6E\uFF1B\u5176\u4ED6\u9879\u76EE\u672A\u68C0\u67E5", status: "muted", compact: true }), unified.status.state === 'invalid'
4556
- ? _jsx(Callout, { status: "error", title: "\u4F9D\u8D56\u5173\u7CFB\u65E0\u6548", description: unified.status.message }) : null, unverifiedConsumerActions.length > 0
4557
- ? _jsx(StaticRow, { title: `未验证声明者 · ${unverifiedConsumerActions.length} 个`, description: "\u4EE5\u4E0B\u58F0\u660E\u8005\u4E0D\u8BA1\u5165\u5F53\u524D\u53EF\u7528\u5173\u7CFB", status: "muted", compact: true })
4558
- : null, focusedGraphRow ? _jsx(StaticRow, { title: "\u7528\u9014", value: props.controller
4559
- .getPluginOverview(focusedGraphRow.pluginId)?.description ?? '当前目录没有用途说明', compact: true }) : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: relationViewport.offset, limit: relationViewport.limit }), showTechnical ? _jsxs(TechnicalDetails, { children: [_jsx(StaticRow, { title: `${dependencyCount} 个依赖 · ${unified.dependents.scopeSummary.availableConsumerPluginIds.length} 个当前可用使用者`, meta: plugin.pluginId, compact: true }), _jsx(StaticRow, { title: `未验证声明者 · ${unified.dependents.scopeSummary.unknownConsumerPluginIds.length} 个`, compact: true })] }) : null] });
4592
+ description = 'A → B 表示 A 需要 B;实线已验证,虚线未验证。';
4593
+ renderDependencyGraph(plugin.pluginId, false);
4560
4594
  }
4561
4595
  }
4562
- else if (route.name === 'pluginDependencyNode' && plugin) {
4563
- const node = props.controller.getPluginOverview(route.nodePluginId);
4564
- const nodePathPluginIds = route.nodePathPluginIds ?? [plugin.pluginId, route.nodePluginId];
4596
+ else if (route.name === 'pluginDependencyNode' && route.view === 'references'
4597
+ && props.controller.getPluginDetailSubject(route.pluginId)) {
4598
+ const root = props.controller.getPluginDetailSubject(route.pluginId);
4599
+ const node = props.controller.getPluginDetailSubject(route.nodePluginId);
4565
4600
  const nodeProjection = buildDependencyGraphProjection({
4566
4601
  overviews: props.controller.listPluginDependencyOverviews(),
4567
- rootPluginId: plugin.pluginId,
4602
+ rootPluginId: route.pluginId,
4568
4603
  direction: route.direction,
4569
- expandedPluginIds: nodePathPluginIds,
4604
+ expandedPluginIds: route.nodePathPluginIds ?? [route.pluginId, route.nodePluginId],
4570
4605
  focusPluginId: route.nodePluginId,
4571
4606
  });
4572
- const selectedRow = nodeProjection.rows.find((row) => row.pathPluginIds.join('\0') === nodePathPluginIds.join('\0'))
4607
+ const selectedRow = nodeProjection.rows.find((row) => row.pathPluginIds.join('\0') === route.nodePathPluginIds?.join('\0'))
4573
4608
  ?? nodeProjection.rows.find((row) => row.pluginId === route.nodePluginId);
4574
- const item = route.nodePluginId === plugin.pluginId ? undefined
4575
- : (route.direction === 'dependencies'
4576
- ? props.controller.getPluginDependencyOverview(plugin.pluginId)?.dependencies
4577
- : props.controller.getPluginDependencyOverview(plugin.pluginId)?.dependents)
4578
- ?.find((entry) => entry.pluginId === route.nodePluginId);
4579
- const state = item ? dependencyStatus(item) : undefined;
4580
- const relationship = selectedRow?.relationship
4581
- ?? ((item?.availablePlacementReferences?.length ?? 0) > 0 ? 'unknown'
4582
- : item ? 'declared' : undefined);
4583
- breadcrumb = `${plugin.displayName} / 依赖关系 / ${node?.displayName ?? route.nodePluginId}`;
4584
- context = route.direction === 'dependencies' ? '它需要的' : '哪些插件依赖它';
4585
- title = node?.displayName ?? route.nodePluginId;
4586
- const pathPluginIds = selectedRow?.pathPluginIds ?? nodePathPluginIds;
4587
- const intermediateNames = pathPluginIds.slice(1, -1).reverse().map((pluginId) => props.controller.getPluginOverview(pluginId)?.displayName ?? pluginId);
4588
- const declaringPluginId = pathPluginIds.at(-2) ?? plugin.pluginId;
4589
- const declaringPluginName = props.controller.getPluginOverview(declaringPluginId)?.displayName
4590
- ?? declaringPluginId;
4591
- description = relationship === 'root'
4592
- ? '这是当前关系图的根插件。'
4593
- : relationship === 'directAvailable'
4594
- ? route.direction === 'dependencies'
4595
- ? `${plugin.displayName} 在本次检查的位置需要此插件,所需依赖当前可用。`
4596
- : `${node?.displayName ?? route.nodePluginId} 在本次检查的位置需要当前插件,所需依赖当前可用。`
4597
- : relationship === 'indirectAvailable'
4598
- ? route.direction === 'dependencies'
4599
- ? `${plugin.displayName} 通过 ${intermediateNames.join('、')} 需要 ${node?.displayName ?? route.nodePluginId},且各段依赖当前可用。`
4600
- : `${node?.displayName ?? route.nodePluginId} 通过 ${intermediateNames.join('、')} 需要 ${plugin.displayName},且各段依赖当前可用。`
4601
- : relationship === 'unknown'
4602
- ? '本次检查的位置不能组成连续路径,不能显示这项间接关系。'
4603
- : relationship === 'declared'
4604
- ? route.direction === 'dependencies'
4605
- ? `${declaringPluginName} 声明需要 ${node?.displayName ?? route.nodePluginId},但本次尚未确认依赖可用。`
4606
- : `${node?.displayName ?? route.nodePluginId} 声明需要当前插件,但本次尚未确认依赖可用。`
4607
- : '当前数据不足,不能判断这项依赖关系。';
4608
- const references = selectedRow?.edge?.availablePlacementReferences
4609
- ?? item?.availablePlacementReferences ?? [];
4610
- const referenceActions = references.map((reference) => {
4611
- const consumerName = props.controller.getPluginOverview(reference.consumer.pluginId)?.displayName
4612
- ?? reference.consumer.pluginId.split('/').at(-1);
4613
- return {
4614
- label: dependencyReferenceUsageLabel(reference, consumerName),
4615
- meta: '依赖当前可用',
4616
- };
4617
- });
4618
- if (route.view === 'references') {
4619
- breadcrumb = `${plugin.displayName} / 依赖关系 / ${node?.displayName ?? route.nodePluginId} / 使用位置`;
4620
- context = showTechnical ? '技术详情' : '使用位置';
4621
- title = `当前可用依赖位置 ${references.length} 个`;
4622
- description = showTechnical
4623
- ? '显示当前选择的完整引用字段。'
4624
- : '这些位置的所需依赖当前可用,且本次检查发现它们同时满足依赖声明和安装位置。';
4625
- if (!showTechnical && references[focus])
4626
- selectedDependencyReferenceIndex.current = focus;
4627
- const selectedReferenceIndex = Math.min(selectedDependencyReferenceIndex.current, Math.max(0, references.length - 1));
4628
- const selectedReference = references[selectedReferenceIndex];
4629
- const referenceIdLines = (label, pluginId) => {
4630
- const prefix = `${label} · `;
4631
- return wrapConsoleText(pluginId, Math.max(1, size.columns - 10 - stringWidth(prefix)))
4632
- .map((line) => `${prefix}${line}`);
4633
- };
4634
- const referenceDetailLines = selectedReference ? [
4635
- ...referenceIdLines('consumer', selectedReference.consumer.pluginId),
4636
- `consumer · platform=${platformLabel(selectedReference.consumer.platform)} · scope=${selectedReference.consumer.scope}`,
4637
- `consumer · project=${selectedReference.consumer.project ?? 'none'}`,
4638
- ...referenceIdLines('dependency', selectedReference.dependency.pluginId),
4639
- `dependency · platform=${platformLabel(selectedReference.dependency.platform)} · scope=${selectedReference.dependency.scope}`,
4640
- `dependency · project=${selectedReference.dependency.project ?? 'none'}`,
4641
- ] : [];
4642
- if (showTechnical && selectedReference)
4643
- readingText = [
4644
- `consumer · ${selectedReference.consumer.pluginId}`,
4645
- `consumer · platform=${platformLabel(selectedReference.consumer.platform)} · scope=${selectedReference.consumer.scope}`,
4646
- `consumer · project=${selectedReference.consumer.project ?? 'none'}`,
4647
- `dependency · ${selectedReference.dependency.pluginId}`,
4648
- `dependency · platform=${platformLabel(selectedReference.dependency.platform)} · scope=${selectedReference.dependency.scope}`,
4649
- `dependency · project=${selectedReference.dependency.project ?? 'none'}`,
4650
- ].join('\n');
4651
- const referenceDetailPageRows = referenceDetailLines.length <= 6
4652
- ? 6
4653
- : responsiveBudget(4, 8);
4654
- const referenceDetailOffset = Math.min(batchTargetOffset, Math.max(0, referenceDetailLines.length - 1));
4655
- const visibleReferenceDetailLines = referenceDetailLines.slice(referenceDetailOffset, referenceDetailOffset + referenceDetailPageRows);
4656
- referenceDetailPagination = showTechnical ? {
4657
- total: referenceDetailLines.length,
4658
- pageRows: referenceDetailPageRows,
4659
- } : undefined;
4660
- actions = [
4661
- ...(showTechnical && referenceActions[selectedReferenceIndex]
4662
- ? [referenceActions[selectedReferenceIndex]] : referenceActions),
4663
- { role: 'navigation', label: '返回节点详情', run: back },
4664
- ];
4665
- const referenceViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, responsiveBudget(3, 6));
4666
- help = showTechnical
4667
- ? '←→ 查看当前位置详情 · ↑↓ 移动 · Enter 选择 · d 隐藏技术详情 · Esc 返回节点详情'
4668
- : '↑↓ 查看全部位置 · d 技术详情 · Enter 返回节点详情 · Esc 返回节点详情';
4669
- content = _jsxs(Box, { flexDirection: "column", children: [showTechnical ? null
4670
- : _jsx(StaticRow, { title: "\u672C\u6B21\u68C0\u67E5", value: "\u53EA\u68C0\u67E5\u5F53\u524D\u9879\u76EE\u548C\u4E2A\u4EBA\u4F4D\u7F6E\uFF1B\u5176\u4ED6\u9879\u76EE\u672A\u68C0\u67E5", status: "muted", compact: true }), showTechnical && referenceDetailOffset > 0
4671
- ? _jsx(StaticRow, { title: `↑ 前面还有 ${referenceDetailOffset} 行`, status: "muted", compact: true }) : null, showTechnical ? visibleReferenceDetailLines.map((line, index) => _jsx(StaticRow, { title: line, compact: true }, `${referenceDetailOffset + index}:${line}`)) : null, showTechnical && referenceDetailOffset + visibleReferenceDetailLines.length < referenceDetailLines.length
4672
- ? _jsx(StaticRow, { title: `↓ 后面还有 ${referenceDetailLines.length - referenceDetailOffset - visibleReferenceDetailLines.length} 行`, status: "muted", compact: true }) : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: referenceViewport.offset, limit: referenceViewport.limit })] });
4673
- }
4674
- else {
4675
- actions = [
4676
- ...(references.length > 0 ? [{
4677
- label: `查看全部位置(${references.length})`,
4678
- description: '查看每个位置的当前可用依赖',
4679
- run: () => go({ ...route, view: 'references' }),
4680
- }] : []),
4681
- ...(route.nodePluginId !== plugin.pluginId ? [{
4682
- label: '以此节点查看关系',
4683
- description: '打开局部子树;面包屑保留来路',
4684
- run: () => go({
4685
- name: 'pluginDependencies',
4686
- pluginId: route.nodePluginId,
4687
- direction: route.direction,
4688
- }),
4689
- }] : []),
4690
- { role: 'navigation', label: '返回关系图', run: back },
4691
- ];
4692
- const reason = description;
4693
- description = undefined;
4694
- const summary = [
4695
- `用途:${node?.description ?? '当前目录没有用途说明'}`,
4696
- `为何需要:${reason}`,
4697
- `影响:${state?.value ?? (node ? statusText(node) : '尚未检查当前状态')}`,
4698
- ...(showTechnical ? [`pluginId · ${route.nodePluginId}`, `relationship · ${relationship ?? 'unknown'}`] : []),
4699
- ];
4700
- readingText = [title, ...summary].join('\n');
4701
- const summaryLines = summary.flatMap((line) => wrapConsoleText(line, size.columns - 2));
4702
- const actionCount = actions.filter((action) => action.role !== 'navigation').length;
4703
- const availableSummaryRows = pageActionBudget(Math.min(2, actionCount));
4704
- const pageRows = Math.max(1, availableSummaryRows - Number(summaryLines.length > availableSummaryRows));
4705
- const offset = Math.min(batchTargetOffset, Math.max(0, summaryLines.length - pageRows));
4706
- const visible = summaryLines.slice(offset, offset + pageRows);
4707
- const paginated = summaryLines.length > pageRows;
4708
- if (paginated) {
4709
- referenceDetailPagination = { total: summaryLines.length, pageRows };
4710
- context = `${offset + 1}–${offset + visible.length} / ${summaryLines.length}`;
4711
- help = '←→ 翻页 · ↑↓ 移动 · Enter 选择 · Esc 返回';
4712
- }
4713
- currentActionViewportLineBudget = pageActionBudget(visible.length + Number(paginated));
4714
- const nodeActionViewport = actionViewport(actions, true, focus, scrollOffset, currentActionViewportLineBudget);
4715
- content = _jsxs(Box, { flexDirection: "column", children: [visible.map((line, index) => _jsx(Text, { wrap: "truncate-end", children: line }, `${offset + index}`)), paginated ? _jsx(Text, { color: tuiTheme.colors.muted, children: offset + visible.length < summaryLines.length
4716
- ? `↓ 后面还有 ${summaryLines.length - offset - visible.length} 行` : '已到正文末尾' }) : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: nodeActionViewport.offset, limit: nodeActionViewport.limit })] });
4717
- }
4609
+ const references = selectedRow?.edge?.availablePlacementReferences ?? [];
4610
+ breadcrumb = `${root.displayName} / 依赖关系 / ${node?.displayName ?? route.nodePluginId} / 引用`;
4611
+ context = '完整引用';
4612
+ title = `当前可用依赖位置 ${references.length} 个`;
4613
+ description = references.length > 0
4614
+ ? '这些位置在本次检查中已验证;其他项目未检查。'
4615
+ : '本次没有可验证的位置引用;声明关系不表示当前可用。';
4616
+ readingText = selectedRow
4617
+ ? dependencyEvidenceLines(selectedRow, node).join('\n')
4618
+ : `pluginId · ${route.nodePluginId}\n目录版本 · ${node?.catalogVersion ?? '未确认'}\nrelationship · unknown\n连续路径 · 未验证`;
4619
+ content = _jsx(Box, { flexDirection: "column", children: readingPage(readingText, size.columns - 2, pageActionBudget(2)) });
4620
+ help = '←→/PgUp/PgDn 翻页 · y 复制全文 · Esc 返回';
4718
4621
  }
4719
4622
  else if (route.name === 'installTargets' && plugin) {
4720
4623
  breadcrumb = `${plugin.displayName} / 添加 / 目标平台`;
@@ -5123,7 +5026,7 @@ export function ConsoleAppV4(props) {
5123
5026
  return [[`update:${pluginWriteTaskTargetKey(candidate)}`, {
5124
5027
  label: `更新并恢复 ${targetLabel}`,
5125
5028
  description: '改用当前候选版本建立新任务;显示操作汇总前仍会重新检查状态',
5126
- status: 'warning',
5029
+ status: 'info',
5127
5030
  run: () => props.controller.beginNestedPluginWriteTask({
5128
5031
  name: 'pluginWriteTask',
5129
5032
  pluginId: target.pluginId,
@@ -5245,7 +5148,7 @@ export function ConsoleAppV4(props) {
5245
5148
  ...activationSources.map((sourceId) => ({
5246
5149
  label: `先更新来源目录 ${sourceId}`,
5247
5150
  description: '先单独确认目录变更;启用完成后重新准备插件更新',
5248
- status: 'warning',
5151
+ status: 'info',
5249
5152
  run: () => beginReview({ kind: 'catalogGeneration', action: 'update', sourceId }),
5250
5153
  })),
5251
5154
  ...recoveryActions,
@@ -5274,8 +5177,8 @@ export function ConsoleAppV4(props) {
5274
5177
  if (candidates.length > taskTargetLimit) {
5275
5178
  referenceDetailPagination = { total: candidates.length, pageRows: taskTargetLimit };
5276
5179
  }
5277
- const taskTargetOffset = Math.min(batchTargetOffset, Math.max(0, candidates.length - 1));
5278
- const visibleTaskCandidates = candidates.slice(taskTargetOffset, taskTargetOffset + taskTargetLimit);
5180
+ const taskPage = pagedItems(candidates.length, taskTargetLimit);
5181
+ const visibleTaskCandidates = candidates.slice(taskPage.offset, taskPage.offset + taskTargetLimit);
5279
5182
  const targetRows = visibleTaskCandidates.map((candidate) => {
5280
5183
  const target = candidate.target;
5281
5184
  const version = target.expectedUpdate
@@ -5286,43 +5189,41 @@ export function ConsoleAppV4(props) {
5286
5189
  ? pluginWriteTaskMethodLabel(candidate.recommendedMethod)
5287
5190
  : pluginWriteTaskBlockLabel(candidate.blockReason), meta: candidate.eligible ? version : '需要决定', status: candidate.eligible ? 'success' : 'warning', compact: policy.mode === 'compact' }, `${target.pluginId}:${target.platform}:${target.scope}`);
5288
5191
  });
5289
- const taskViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(Math.min(candidates.length, policy.mode === 'compact' ? 3 : 6) + 2));
5290
- content = _jsxs(Box, { flexDirection: "column", children: [_jsxs(SectionCard, { title: "\u5904\u7406\u8303\u56F4", meta: `${executable.length} 个可执行 · ${blockedCandidates.length} 个需决定`, compact: policy.mode === 'compact', children: [taskTargetOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${taskTargetOffset} 个位置`, status: "muted", compact: true }) : null, targetRows.length > 0 ? targetRows
5291
- : _jsx(StaticRow, { title: "\u5F53\u524D\u6CA1\u6709\u53EF\u6267\u884C\u76EE\u6807", status: "muted", compact: true }), candidates.length > taskTargetOffset + targetRows.length
5292
- ? _jsx(StaticRow, { title: `↓ 后面还有 ${candidates.length - taskTargetOffset - targetRows.length} 个位置`, status: "muted", compact: true }) : null] }), blockedCandidates.length > 0 ? _jsx(Callout, { status: "warning", title: `${blockedCandidates.length} 个位置需要先处理或决定`, description: executable.length > 0
5192
+ const taskViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(Math.min(candidates.length, policy.mode === 'compact' ? 3 : 6) + 3));
5193
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsxs(SectionCard, { title: "\u5904\u7406\u8303\u56F4", meta: `${executable.length} 个可执行 · ${blockedCandidates.length} 个需决定`, compact: policy.mode === 'compact', children: [targetRows.length > 0 ? targetRows
5194
+ : _jsx(StaticRow, { title: "\u5F53\u524D\u6CA1\u6709\u53EF\u6267\u884C\u76EE\u6807", status: "muted", compact: true }), taskPage.pageCount > 1
5195
+ ? _jsx(Pager, { page: taskPage.page, pageCount: taskPage.pageCount, columns: size.columns - 6 }) : null] }), blockedCandidates.length > 0 ? _jsx(Callout, { status: "warning", title: `${blockedCandidates.length} 个位置需要先处理或决定`, description: executable.length > 0
5293
5196
  ? '可以先处理可执行项,或进入下方对应入口处理受阻位置。'
5294
5197
  : '请进入下方对应入口处理;返回后会重新读取当前证据。', compact: policy.mode === 'compact' }) : null, missingTargetKeys.length > 0 ? _jsx(Callout, { status: "warning", title: `${missingTargetKeys.length} 个本次安装位置已失效或无需处理`, description: "\u4E0D\u4F1A\u628A\u8FD9\u4E9B\u4F4D\u7F6E\u5F53\u4F5C\u5DF2\u5B8C\u6210\uFF0C\u4E5F\u4E0D\u4F1A\u81EA\u52A8\u7F29\u5C0F\u672C\u6B21\u8303\u56F4\u3002\u8BF7\u8FD4\u56DE\u8C03\u6574\u9009\u62E9\u540E\u91CD\u65B0\u5F00\u59CB\u3002", compact: policy.mode === 'compact' }) : null, addedTargetKeys.length > 0 ? _jsx(Callout, { status: "warning", title: `${addedTargetKeys.length} 个新位置需要重新确认`, description: "\u4E0D\u4F1A\u628A\u5237\u65B0\u540E\u65B0\u589E\u7684\u4F4D\u7F6E\u81EA\u52A8\u52A0\u5165\u672C\u6B21\u64CD\u4F5C\u3002\u8BF7\u8FD4\u56DE\u8C03\u6574\u9009\u62E9\u540E\u91CD\u65B0\u5F00\u59CB\u3002", compact: policy.mode === 'compact' }) : null, versionEvidenceChanged ? _jsx(Callout, { status: "warning", title: "\u63D2\u4EF6\u76EE\u6807\u7248\u672C\u5DF2\u53D8\u5316", description: "\u4E0D\u4F1A\u6CBF\u7528\u6765\u6E90\u542F\u7528\u524D\u7684\u7248\u672C\u8BC1\u636E\uFF0C\u4E5F\u4E0D\u4F1A\u81EA\u52A8\u6267\u884C\u65B0\u7248\u672C\u3002", compact: policy.mode === 'compact' }) : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: taskViewport.offset, limit: taskViewport.limit })] });
5295
5198
  }
5296
- else if (route.name === 'pluginDetail' && !plugin && props.controller.getProjectInventoryRoot(route.pluginId)) {
5297
- const root = props.controller.getProjectInventoryRoot(route.pluginId);
5298
- breadcrumb = '项目插件';
5299
- title = root.displayName;
5300
- description = '来源当前没有此插件;项目声明仍保留。';
5301
- const fields = [root.pluginId, `来源:${root.sourceId}`, ...projectDependencyLines(root), ...root.targets.map((target) => `${platformLabel(target.platform)} · ${target.location === 'sharedUser' ? '共用个人安装' : '当前项目'} · 期望 ${target.expectedVersion ?? '未知'} · 实际 ${target.installedVersion ?? '未知'} · ${target.reasonCodes.join('、')}`)];
5302
- readingText = fields.join('\n');
5303
- const lines = fields.flatMap((field) => wrapConsoleText(field, size.columns - 2));
5304
- const pageRows = pageActionBudget(3);
5305
- referenceDetailPagination = { total: lines.length, pageRows };
5306
- actions = [{ label: '查看插件来源', run: () => go(props.controller.getSourceOverview(root.sourceId).source
5307
- ? { name: 'sourceDetail', sourceId: root.sourceId } : { name: 'sourceList' }) }];
5308
- content = _jsxs(Box, { flexDirection: "column", children: [lines.slice(batchTargetOffset, batchTargetOffset + pageRows).map((line, index) => _jsx(Text, { children: line }, index)), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] });
5199
+ else if (route.name === 'pluginDetail' && detailSubject && !plugin) {
5200
+ breadcrumb = `插件 / ${detailSubject.displayName}`;
5201
+ title = '';
5202
+ description = undefined;
5203
+ context = '插件详情';
5204
+ const issues = props.controller.listUserIssues().filter((entry) => issueMatchesTarget(entry, detailSubject.pluginId));
5205
+ const root = props.controller.getProjectInventoryRoot(detailSubject.pluginId);
5206
+ actions = [
5207
+ ...(issues.length > 0 ? [{ actionId: 'exceptional', label: '查看处理建议',
5208
+ status: 'warning',
5209
+ run: () => go(issues.length === 1
5210
+ ? { name: 'issueDetail', issueId: issues[0].issueId } : { name: 'issueList' }) }] : []),
5211
+ ...(detailSubject.savedConfigScopes.length > 0 ? [{ actionId: 'settings',
5212
+ label: '查看已保存设置', run: () => props.controller.beginSavedConfigRecovery(detailSubject.pluginId) }] : []),
5213
+ ...(detailSubject.relations.hasKnownRelations ? [{ actionId: 'dependencies',
5214
+ label: '依赖关系', run: () => go({ name: 'pluginDependencies', pluginId: detailSubject.pluginId }) }] : []),
5215
+ ...(props.controller.getSourceOverview(detailSubject.sourceId).source ? [{ actionId: 'source',
5216
+ label: '查看插件来源', run: () => go({ name: 'sourceDetail', sourceId: detailSubject.sourceId }) }] : []),
5217
+ ...(root && root.state !== 'ready' && issues.length === 0 ? [{ actionId: 'project-recheck',
5218
+ label: '重新检查项目', run: async () => { await props.controller.refreshProjectInventory(); } }] : []),
5219
+ ];
5220
+ content = detailCardContent(detailSubject);
5309
5221
  }
5310
- else if (route.name === 'pluginDetail' && plugin) {
5222
+ else if (route.name === 'pluginDetail' && plugin && detailSubject) {
5311
5223
  breadcrumb = `插件 / ${plugin.displayName}`;
5312
5224
  context = `${plugin.sourceLabel}${plugin.version ? ` · ${plugin.version}` : ''}`;
5313
5225
  title = plugin.displayName;
5314
5226
  description = '';
5315
- const updateDescription = pluginUpdateDescription(plugin);
5316
- const installedVersions = [...new Set((plugin.update?.targets ?? [])
5317
- .flatMap((target) => target.installedVersion ? [target.installedVersion] : []))];
5318
- const versionSummary = installedVersions.length > 1
5319
- ? `各平台版本不同:${installedVersions.join('、')}`
5320
- : installedVersions.length === 1
5321
- ? plugin.version && plugin.version !== installedVersions[0]
5322
- ? `${installedVersions[0]} → ${plugin.version}`
5323
- : `已安装 ${installedVersions[0]}`
5324
- : plugin.version ?? '待确认';
5325
- context = `${plugin.sourceLabel} · ${versionSummary}`;
5326
5227
  const updateTaskCandidates = plugin.configOnly ? []
5327
5228
  : props.controller.listPluginWriteTaskCandidates('update')
5328
5229
  .filter((candidate) => candidate.target.pluginId === plugin.pluginId);
@@ -5336,7 +5237,7 @@ export function ConsoleAppV4(props) {
5336
5237
  ? `更新 ${plugin.displayName}` : '准备插件更新',
5337
5238
  description: updateTaskCandidates.some((candidate) => candidate.blockReason === 'catalogActivationRequired')
5338
5239
  ? '需要先单独确认来源目录变更' : '使用精确版本和现有安装位置',
5339
- status: 'warning',
5240
+ status: 'info',
5340
5241
  run: () => go({
5341
5242
  name: 'pluginWriteTask', pluginId: plugin.pluginId, goal: 'update',
5342
5243
  targetKeys: updateTaskCandidates.map(pluginWriteTaskTargetKey),
@@ -5380,7 +5281,7 @@ export function ConsoleAppV4(props) {
5380
5281
  ? plugin.capabilityUnsupported && !plugin.sourceUnavailable ? {
5381
5282
  label: '需要升级 AW CLI',
5382
5283
  description: '查看缺少的功能和安全恢复方向',
5383
- status: 'warning',
5284
+ status: 'info',
5384
5285
  run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
5385
5286
  } : {
5386
5287
  label: '恢复插件来源',
@@ -5415,7 +5316,7 @@ export function ConsoleAppV4(props) {
5415
5316
  ? { name: 'installTargets', pluginId: plugin.pluginId }
5416
5317
  : { name: 'installTargets', pluginId: plugin.pluginId, view: 'blocked' });
5417
5318
  };
5418
- const installAction = !plugin.configOnly ? {
5319
+ const installAction = !plugin.catalogEntryMissing ? {
5419
5320
  actionId: 'install',
5420
5321
  label: installed ? '安装到其他位置' : '安装',
5421
5322
  description: plugin.sourceUnavailable
@@ -5431,7 +5332,8 @@ export function ConsoleAppV4(props) {
5431
5332
  : { run: beginInstall }),
5432
5333
  } : undefined;
5433
5334
  const contentOverview = props.controller.getPluginContentOverview(plugin.pluginId);
5434
- const contentAction = !plugin.configOnly ? {
5335
+ const contentAction = !plugin.catalogEntryMissing
5336
+ && detailSubject.definitionAvailable ? {
5435
5337
  actionId: 'content',
5436
5338
  label: '内容详情',
5437
5339
  description: contentOverview
@@ -5440,7 +5342,8 @@ export function ConsoleAppV4(props) {
5440
5342
  run: () => go({ name: 'pluginContent', pluginId: plugin.pluginId }),
5441
5343
  } : undefined;
5442
5344
  const dependencyOverviewForDetail = dependencyOverview(plugin);
5443
- const dependencyAction = !plugin.configOnly ? {
5345
+ const dependencyAction = !plugin.catalogEntryMissing
5346
+ && detailSubject.relations.hasKnownRelations ? {
5444
5347
  actionId: 'dependencies',
5445
5348
  label: '依赖关系',
5446
5349
  description: dependencyOverviewForDetail
@@ -5469,6 +5372,23 @@ export function ConsoleAppV4(props) {
5469
5372
  go({ name: 'uninstallTargets', pluginId: plugin.pluginId });
5470
5373
  },
5471
5374
  } : undefined;
5375
+ const cleanupAction = plugin.configOnly
5376
+ || plugin.removedPluginCleanupRequired ? {
5377
+ actionId: 'cleanup',
5378
+ label: plugin.removedPluginCleanupRequired ? '清理已删除插件' : '检查并清理插件残留',
5379
+ description: plugin.removedPluginCleanupRequired
5380
+ ? '预览两个平台、用户与当前项目中的残留和保存设置,再确认实际清理范围'
5381
+ : '先核验当前来源和现场,再显示可清理内容与实际范围',
5382
+ status: 'error',
5383
+ run: () => {
5384
+ const target = props.controller.listPluginWriteBatchTargets('cleanup', [plugin.pluginId])[0];
5385
+ beginReview({
5386
+ kind: 'pluginWrite', action: 'cleanup', pluginId: plugin.pluginId,
5387
+ platform: target?.platform ?? 'codex',
5388
+ scope: target?.scope ?? plugin.savedConfigScopes[0] ?? 'user',
5389
+ });
5390
+ },
5391
+ } : undefined;
5472
5392
  actions = dedupeActions([
5473
5393
  ...(exceptionalAction ? [{ ...exceptionalAction, actionId: 'exceptional' }] : []),
5474
5394
  ...(restoreAction ? [{ ...restoreAction, actionId: 'restore' }] : []),
@@ -5478,6 +5398,7 @@ export function ConsoleAppV4(props) {
5478
5398
  ...(contentAction ? [contentAction] : []),
5479
5399
  ...(dependencyAction ? [dependencyAction] : []),
5480
5400
  ...(uninstallAction ? [uninstallAction] : []),
5401
+ ...(cleanupAction ? [cleanupAction] : []),
5481
5402
  {
5482
5403
  actionId: 'more',
5483
5404
  label: '更多操作',
@@ -5485,92 +5406,64 @@ export function ConsoleAppV4(props) {
5485
5406
  run: () => go({ name: 'pluginMore', pluginId: plugin.pluginId }),
5486
5407
  },
5487
5408
  ]);
5488
- const dependencyDetailText = dependencyOverviewForDetail
5489
- && dependencyOverviewForDetail.summary.totalCount > 0
5490
- ? `需要 ${dependencyOverviewForDetail.summary.totalCount} 个插件;当前可用关系按当前范围检查,未显示的关系尚未检查通过。`
5491
- : undefined;
5492
- const detailPlatforms = [...new Set([
5493
- ...plugin.installedPlacements.map((placement) => placement.platform), ...plugin.supportedPlatforms,
5494
- ])];
5495
- const detailPositions = detailPlatforms.flatMap((platform) => {
5496
- const placements = plugin.installedPlacements.filter((placement) => placement.platform === platform && placement.state !== 'notInstalled');
5497
- return placements.length > 0
5498
- ? placements.map((placement) => `${platformLabel(platform)}:${pluginPlacementConclusions(placement).map((entry) => entry.text).join(' · ')}`)
5499
- : [`${platformLabel(platform)} · ${pluginPlatformUnknown(plugin, platform) ? '未知 · 尚未确认' : '尚未安装'}`];
5500
- });
5501
- const platformSummary = detailPositions.length > 0 ? detailPositions.join(';') : '当前没有可用平台';
5502
5409
  context = '插件详情';
5503
- const summary = [
5504
- plugin.description,
5505
- `版本 · ${versionSummary}`,
5506
- `来源 · ${plugin.sourceLabel}`,
5507
- ...(props.controller.getProjectInventoryRoot(plugin.pluginId)?.targets.map((target) => `${platformLabel(target.platform)} · ${target.location === 'sharedUser' ? '共用个人安装' : '项目声明'} · 期望 ${target.expectedVersion ?? '未知'} · 实际 ${target.installedVersion ?? '未知'}`) ?? []),
5508
- ...projectDependencyLines(props.controller.getProjectInventoryRoot(plugin.pluginId)),
5509
- ...(detailPositions.length > 0 ? detailPositions : [platformSummary]),
5510
- ...(plugin.portableSkills ? ['[Skills 可选]'] : []),
5511
- ...(plugin.configOnly ? ['[已保存设置]'] : []),
5512
- ...(updateDescription ? [`有可用更新 · ${updateDescription}`] : []),
5513
- ...(dependencyDetailText ? [dependencyDetailText] : []),
5514
- ];
5515
- readingText = [plugin.displayName, ...summary].join('\n');
5516
- const summaryLines = summary.flatMap((line) => wrapConsoleText(line, size.columns - 2));
5410
+ title = '';
5411
+ description = undefined;
5517
5412
  const completion = snapshot.completionNotice?.pluginId === plugin.pluginId
5518
5413
  ? snapshot.completionNotice : undefined;
5519
5414
  const completionLines = completion
5520
5415
  ? wrapConsoleText(`${completion.title} · ${completion.description}`, size.columns - 2) : [];
5521
- const availableSummaryRows = pageActionBudget(Math.min(3, actions.length) + completionLines.length);
5522
- const summaryPageRows = Math.max(1, availableSummaryRows - Number(summaryLines.length > availableSummaryRows));
5523
- const summaryOffset = Math.min(batchTargetOffset, Math.max(0, summaryLines.length - summaryPageRows));
5524
- const visibleSummary = summaryLines.slice(summaryOffset, summaryOffset + summaryPageRows);
5525
- const summaryPaginated = summaryLines.length > summaryPageRows;
5526
- if (summaryPaginated) {
5527
- referenceDetailPagination = { total: summaryLines.length, pageRows: summaryPageRows };
5528
- context = `${summaryOffset + 1}–${summaryOffset + visibleSummary.length} / ${summaryLines.length}`;
5529
- help = '←→ 翻页 · ↑↓ 移动 · Enter 选择 · Esc 返回';
5530
- }
5531
- currentActionViewportLineBudget = pageActionBudget(visibleSummary.length
5532
- + Number(summaryPaginated) + completionLines.length);
5533
- const detailViewport = actionViewport(actions, true, focus, scrollOffset, currentActionViewportLineBudget);
5534
- content = _jsxs(Box, { flexDirection: "column", children: [completionLines.map((line, index) => _jsx(Text, { color: tuiTheme.statusColors.success, children: line }, `completion:${index}`)), visibleSummary.map((line, index) => _jsx(Text, { wrap: "truncate-end", children: line }, `${summaryOffset + index}`)), summaryPaginated ? _jsx(Text, { color: tuiTheme.colors.muted, children: summaryOffset + visibleSummary.length < summaryLines.length
5535
- ? `↓ 后面还有 ${summaryLines.length - summaryOffset - visibleSummary.length} 行`
5536
- : '已到正文末尾' }) : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: detailViewport.offset, limit: detailViewport.limit })] });
5416
+ content = detailCardContent(detailSubject, completionLines);
5537
5417
  }
5538
5418
  else if (route.name === 'pluginMore' && plugin) {
5539
5419
  breadcrumb = `插件 / ${plugin.displayName} / 更多操作`;
5540
5420
  context = plugin.sourceLabel;
5541
5421
  title = '更多操作';
5542
5422
  description = '选择平台可使用同步、重新安装、移除和受限清理。';
5423
+ const detail = props.controller.getPluginDetailSubject(plugin.pluginId);
5424
+ const platforms = plugin.installedPlacements.filter((placement) => placement.state !== 'notInstalled');
5425
+ const entryFacts = platforms.map((placement) => detail?.placements
5426
+ .filter((entry) => entry.platform === placement.platform) ?? []);
5543
5427
  actions = [
5544
- ...plugin.installedPlacements
5545
- .filter((placement) => placement.state !== 'notInstalled')
5428
+ ...platforms
5546
5429
  .map((placement) => {
5547
- const projected = placementStatus(placement);
5548
5430
  return {
5431
+ actionId: `platform:${placement.platform}`,
5549
5432
  label: `${platformLabel(placement.platform)} 平台维护`,
5550
- description: '同步、重新安装和受限清理',
5551
- meta: projected.value,
5552
- status: projected.status,
5553
5433
  run: () => go({ name: 'platform', pluginId: plugin.pluginId, platform: placement.platform }),
5554
5434
  };
5555
5435
  }),
5556
5436
  { label: '查看插件来源', run: () => go({ name: 'sourceDetail', sourceId: plugin.sourceId }) },
5557
5437
  { label: '诊断与技术详情', description: '查看插件身份、组件状态和安全错误代码', run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }) },
5558
5438
  ];
5559
- const moreViewport = actionViewport(actions, policy.mode === 'compact', focus, scrollOffset, pageActionBudget(1));
5560
- content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: moreViewport.offset, limit: moreViewport.limit });
5439
+ const cardWidth = size.columns - 2;
5440
+ const moreViewport = rowViewport({ rowHeights: actions.map((_, index) => index < platforms.length
5441
+ ? 2 + (entryFacts[index].length ? entryFacts[index].reduce((height, fact) => height
5442
+ + entityPlacementRowHeight({ ...fact, shared: fact.sharedWithCurrentProject }, cardWidth), 0) : 1)
5443
+ : 1), focus, requestedOffset: scrollOffset, rowBudget: pageActionBudget(0) });
5444
+ content = _jsx(ContentViewport, { children: actions.slice(moreViewport.offset, moreViewport.offset + moreViewport.limit)
5445
+ .map((action, visibleIndex) => {
5446
+ const index = moreViewport.offset + visibleIndex;
5447
+ return index < platforms.length ? _jsx(ObjectCardFrame, { width: cardWidth, title: action.label, focused: focus === index, children: entryFacts[index].length ? entryFacts[index].map((fact) => _jsx(EntityPlacementRow, { placement: { ...fact, shared: fact.sharedWithCurrentProject }, width: cardWidth }, fact.scope)) : _jsx(Text, { color: tuiTheme.colors.muted, children: "\u4F4D\u7F6E\u72B6\u6001\u672A\u786E\u8BA4" }) }, action.actionId) : _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: index, limit: 1 }, index);
5448
+ }) });
5561
5449
  }
5562
5450
  else if (route.name === 'platform' && plugin) {
5563
- const placement = props.controller.getPlatformPlacement(plugin.pluginId, route.platform);
5451
+ const aggregatePlacement = props.controller.getPlatformPlacement(plugin.pluginId, route.platform);
5452
+ const placement = aggregatePlacement?.state === 'installed' && platformSelectedScope
5453
+ ? { ...aggregatePlacement,
5454
+ ...(aggregatePlacement.scopedPlacements?.find((entry) => entry.scope === platformSelectedScope) ?? {}) }
5455
+ : aggregatePlacement;
5564
5456
  const platformMenu = dynamicMenu;
5565
- const typedIssue = placement?.state === 'installed' && placement.health === 'error'
5457
+ const typedIssue = platformSelectedScope
5566
5458
  ? props.controller.listUserIssues().find((candidate) => candidate.pluginId === plugin.pluginId
5567
5459
  && candidate.platform === route.platform
5568
- && candidate.scope === placement.scope)
5460
+ && candidate.scope === platformSelectedScope)
5569
5461
  : undefined;
5570
5462
  const collisionIssue = placement?.state === 'blocked'
5571
5463
  || placement?.state === 'installed' && placement.blockingReason === 'artifactCollision'
5572
5464
  ? props.controller.listUserIssues().find((candidate) => candidate.pluginId === plugin.pluginId
5573
5465
  && candidate.platform === route.platform
5466
+ && (!platformSelectedScope || candidate.scope === platformSelectedScope)
5574
5467
  && candidate.category === 'artifactCollision')
5575
5468
  : undefined;
5576
5469
  const capabilityUnavailable = plugin.capabilityUnsupported && !plugin.sourceUnavailable;
@@ -5582,14 +5475,16 @@ export function ConsoleAppV4(props) {
5582
5475
  return {
5583
5476
  label: '升级 AW CLI',
5584
5477
  description: '升级到支持此插件的 AW CLI 版本后重试',
5585
- status: 'warning',
5586
- run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
5478
+ status: 'info',
5479
+ run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId,
5480
+ platform: route.platform, scope: platformSelectedScope }),
5587
5481
  };
5588
5482
  if (action.id === 'capabilityTechnical')
5589
5483
  return {
5590
5484
  label: '查看功能支持详情',
5591
5485
  description: '查看缺少的功能和安全恢复方向',
5592
- run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
5486
+ run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId,
5487
+ platform: route.platform, scope: platformSelectedScope }),
5593
5488
  };
5594
5489
  if (action.id === 'sourceProblem')
5595
5490
  return { label: '查看来源问题', description: `${plugin.sourceId} 当前不可用`, run: () => go({ name: 'issueDetail', issueId: `source:${plugin.sourceId}:unavailable` }) };
@@ -5609,11 +5504,11 @@ export function ConsoleAppV4(props) {
5609
5504
  if (action.id === 'addScope')
5610
5505
  return {
5611
5506
  label: `在${scopeLabel(action.scope)}中启用`,
5612
- description: '增加这个使用位置;已被其它项目共用的内容保持不变',
5507
+ description: `补充安装目标:${platformLabel(action.platform)} · ${scopeLabel(action.scope)};在操作汇总中确认。`,
5613
5508
  run: () => beginSecondScope(plugin, action.platform, action.scope),
5614
5509
  };
5615
5510
  if (action.id === 'about')
5616
- return { label: '查看支持信息', description: '插件定义与平台支持', run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }) };
5511
+ return { label: '查看支持信息', description: '插件定义与平台支持', run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId, platform: route.platform, scope: platformSelectedScope }) };
5617
5512
  if (action.id === 'source')
5618
5513
  return { label: '查看来源', run: () => go({ name: 'sourceDetail', sourceId: plugin.sourceId }) };
5619
5514
  if (action.id === 'outputCollision')
@@ -5623,7 +5518,8 @@ export function ConsoleAppV4(props) {
5623
5518
  status: 'error',
5624
5519
  run: () => collisionIssue
5625
5520
  ? go({ name: 'issueDetail', issueId: collisionIssue.issueId })
5626
- : go({ name: 'pluginTechnical', pluginId: plugin.pluginId }),
5521
+ : go({ name: 'pluginTechnical', pluginId: plugin.pluginId,
5522
+ platform: route.platform, scope: platformSelectedScope }),
5627
5523
  };
5628
5524
  if (action.id === 'scopeConflict')
5629
5525
  return { label: '查看位置冲突问题', run: () => go({ name: 'issueDetail', issueId: `scope:${plugin.pluginId}:${route.platform}` }) };
@@ -5637,6 +5533,7 @@ export function ConsoleAppV4(props) {
5637
5533
  if (action.id === 'refreshAndSync')
5638
5534
  return {
5639
5535
  label: `更新到 ${action.availableVersion}`,
5536
+ status: 'info',
5640
5537
  description: '使用当前启用目录中的已确认版本',
5641
5538
  run: () => beginReview({
5642
5539
  kind: 'pluginWrite', action: 'sync', pluginId: plugin.pluginId,
@@ -5652,6 +5549,7 @@ export function ConsoleAppV4(props) {
5652
5549
  if (action.id === 'sync')
5653
5550
  return {
5654
5551
  label: `同步${scopeLabel(action.scope)}的使用位置`,
5552
+ status: 'info',
5655
5553
  description: placement?.state === 'installed'
5656
5554
  ? (placement.scopedPlacements?.find((entry) => entry.scope === action.scope)?.recovery
5657
5555
  ?? placement.recovery)?.find((entry) => entry.command === 'sync')?.description
@@ -5662,7 +5560,7 @@ export function ConsoleAppV4(props) {
5662
5560
  : undefined,
5663
5561
  };
5664
5562
  if (action.id === 'reinstall')
5665
- return { label: `重新安装${scopeLabel(action.scope)}位置`, description: '保留现有设置并重新生成受管内容', run: () => beginReview({ kind: 'pluginWrite', action: 'reinstall', pluginId: plugin.pluginId, platform: action.platform, scope: action.scope }) };
5563
+ return { label: `重新安装${scopeLabel(action.scope)}位置`, status: 'info', description: '保留现有设置并重新生成受管内容', run: () => beginReview({ kind: 'pluginWrite', action: 'reinstall', pluginId: plugin.pluginId, platform: action.platform, scope: action.scope }) };
5666
5564
  if (action.id === 'remove')
5667
5565
  return { label: `从${scopeLabel(action.scope)}移除`, description: '保留插件设置', status: 'error', run: () => beginPluginRemoval(plugin, action.platform, action.scope) };
5668
5566
  if (action.id === 'cleanup')
@@ -5670,7 +5568,7 @@ export function ConsoleAppV4(props) {
5670
5568
  if (action.id === 'technical')
5671
5569
  return typedIssue
5672
5570
  ? { label: '查看对应问题', description: typedIssue.explanation, run: () => go({ name: 'issueDetail', issueId: typedIssue.issueId }) }
5673
- : { label: placement?.state === 'installed' && placement.health === 'error' ? '查看安全技术详情' : placement?.state === 'installed' && placement.health === 'incomplete' ? '查看影响' : '查看安装详情', description: '组件与诊断信息', run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }) };
5571
+ : { label: placement?.state === 'installed' && placement.health === 'error' ? '查看安全技术详情' : placement?.state === 'installed' && placement.health === 'incomplete' ? '查看影响' : '查看安装详情', description: '组件与诊断信息', run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId, platform: route.platform, scope: platformSelectedScope }) };
5674
5572
  return { role: 'navigation', label: '返回插件', run: back };
5675
5573
  };
5676
5574
  const offlineCleanupAction = typedIssue?.actions.find((action) => action.id === 'cleanup');
@@ -5678,6 +5576,7 @@ export function ConsoleAppV4(props) {
5678
5576
  && placement?.state === 'installed'
5679
5577
  && placement.blockingReason === undefined
5680
5578
  && (placement.actualState === 'orphaned' || placement.actualState === 'blocked')
5579
+ && platformSelectedScope === placement.scope
5681
5580
  && offlineCleanupAction !== undefined
5682
5581
  && !platformMenu.some((action) => action.id === 'cleanup')
5683
5582
  && !platformMenu.some((action) => action.id === 'resolveScopeConflict')
@@ -5695,6 +5594,7 @@ export function ConsoleAppV4(props) {
5695
5594
  const restrictedForceCleanup = placement?.state === 'installed'
5696
5595
  && placement.blockingReason === undefined
5697
5596
  && (placement.actualState === 'orphaned' || placement.actualState === 'blocked')
5597
+ && platformSelectedScope === placement.scope
5698
5598
  && !platformMenu.some((action) => action.id === 'resolveScopeConflict')
5699
5599
  ? {
5700
5600
  label: '检查受限清理',
@@ -5704,102 +5604,84 @@ export function ConsoleAppV4(props) {
5704
5604
  }
5705
5605
  : undefined;
5706
5606
  const platformActions = () => [
5707
- ...platformMenu.map(platformAction),
5708
- ...(offlineCleanup ? [offlineCleanup] : []),
5709
- ...(restrictedForceCleanup ? [restrictedForceCleanup] : []),
5607
+ ...platformMenu.filter((action) => action.id === 'addScope' || !('scope' in action) || action.scope === platformSelectedScope)
5608
+ .filter((action) => platformSelectedScope !== undefined
5609
+ || !('scope' in action))
5610
+ .filter((action) => platformSelectedScope !== undefined
5611
+ || platformFacts.length === 0
5612
+ || action.id !== 'technical' && action.id !== 'outputCollision')
5613
+ .filter((action) => {
5614
+ const selectedFact = platformFacts.find((entry) => entry.scope === platformSelectedScope);
5615
+ return selectedFact?.observation !== 'unknown'
5616
+ || !['refreshAndSync', 'sync', 'reinstall', 'remove', 'cleanup'].includes(action.id);
5617
+ }).map((action) => ({ ...platformAction(action),
5618
+ actionId: pluginMenuActionId(action) })),
5619
+ ...(offlineCleanup ? [{ ...offlineCleanup,
5620
+ actionId: `offlineCleanup:${route.platform}:${platformSelectedScope}` }] : []),
5621
+ ...(restrictedForceCleanup ? [{ ...restrictedForceCleanup,
5622
+ actionId: `forceCleanup:${route.platform}:${platformSelectedScope}` }] : []),
5710
5623
  ];
5711
5624
  breadcrumb = `${plugin.displayName} / ${platformLabel(route.platform)}`;
5712
- const installedPlacementRows = placement?.state === 'installed' ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: "\u5DF2\u4FDD\u5B58\u4F4D\u7F6E", meta: placement.desiredScopes?.map(scopeLabel).join('、') || '无', compact: true }), _jsx(StaticRow, { title: "\u5F53\u524D\u4F4D\u7F6E", meta: placement.actualScopes?.map(scopeLabel).join('、') || '未启用', compact: true }), _jsx(StaticRow, { title: "\u72B6\u6001", meta: placement.actualState === 'ready' ? '可用'
5713
- : placement.actualState === 'needsSync'
5714
- ? placement.sharedNative?.reason === 'outdated'
5715
- ? placement.revision.state === 'upgradeAvailable' ? '有更新' : '版本待同步'
5716
- : '需要同步'
5717
- : placement.actualState === 'blocked' ? '已阻断'
5718
- : placement.health === 'incomplete' ? '需要处理' : '暂时不可用', status: placement.actualState === 'ready' ? 'success' : placement.actualState === 'blocked' ? 'error' : 'warning', compact: true }), placement.health !== 'ready'
5719
- ? _jsx(StaticRow, { title: "\u539F\u56E0", meta: placement.blockingReason === 'artifactCollision'
5720
- ? '输出位置存在冲突内容'
5721
- : placement.sharedNative?.reason === 'outdated'
5722
- ? '已安装版本与当前 Catalog 版本不一致'
5723
- : typedIssue?.explanation ?? '安装状态需要同步,请查看问题详情', status: "warning", compact: true })
5724
- : null] }) : null;
5725
- if (plugin.aggregateStatus === 'unavailable') {
5726
- const sourceOverview = capabilityUnavailable ? undefined : props.controller.getSourceOverview(plugin.sourceId);
5727
- const diagnostic = capabilityUnavailable
5728
- ? capabilityDiagnostic(plugin)
5729
- : sourceIssue?.diagnostic ?? sourceDiagnostic(plugin.sourceId, sourceOverview?.health?.state ?? sourceOverview?.source?.state, sourceOverview?.detail);
5730
- title = capabilityUnavailable
5731
- ? `需要升级 AW CLI 才能管理 ${platformLabel(route.platform)}`
5732
- : `暂时无法管理 ${platformLabel(route.platform)}`;
5733
- description = '';
5734
- actions = platformActions();
5735
- content = _jsxs(Box, { flexDirection: "column", children: [diagnostic
5736
- ? _jsx(DiagnosticSummary, { diagnostic: diagnostic, compact: policy.mode === 'compact' })
5737
- : null, _jsx(SemanticRows, { actions: actions, focus: effectiveFocus, kind: "action", compact: policy.mode === 'compact' })] });
5738
- }
5739
- else if (!placement || placement.state === 'notInstalled') {
5740
- title = `尚未添加到 ${platformLabel(route.platform)}`;
5741
- description = '此插件支持所列使用位置。';
5742
- actions = platformActions();
5743
- 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' })] });
5744
- }
5745
- else if (placement.state === 'blocked'
5746
- || placement.state === 'installed' && placement.blockingReason === 'artifactCollision') {
5747
- title = `${platformLabel(route.platform)} 无法添加`;
5748
- description = placement.state === 'installed'
5749
- ? '受管内容仍可安全移除;输出位置同时存在冲突内容。'
5750
- : '输出位置已有不属于当前插件的内容。';
5751
- actions = platformActions();
5752
- 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' })] });
5753
- }
5754
- else if (placement.state === 'scopeConflict') {
5755
- title = '安装位置冲突';
5756
- description = '正常状态只能保留一个位置。';
5757
- actions = platformActions();
5758
- content = _jsx(SemanticRows, { actions: actions, focus: effectiveFocus, kind: "action", compact: policy.mode === 'compact' });
5759
- }
5760
- else if (placement.health === 'error') {
5761
- const diagnostic = typedIssue?.diagnostic ?? {
5762
- severity: 'error',
5763
- title: '状态检查失败',
5764
- userMessage: '平台状态检查失败,当前不能安全选择写操作。',
5765
- impact: [
5766
- { state: 'blocked', message: '当前只提供只读操作。' },
5767
- { state: 'preserved', message: '现有安装和已保存设置不会因本页自动改变。' },
5768
- ],
5769
- nextStep: '查看安全技术详情,然后重新检查当前状态。',
5770
- technical: {
5771
- code: 'targetStateError',
5772
- stage: 'read-platform-state',
5773
- context: [
5774
- { key: 'pluginId', value: plugin.pluginId },
5775
- { key: 'platform', value: route.platform },
5776
- { key: 'scope', value: placement.scope },
5777
- ],
5778
- },
5779
- };
5780
- title = `${platformLabel(route.platform)} 暂时不可管理`;
5781
- description = '';
5782
- actions = platformActions();
5783
- 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' })] });
5784
- }
5785
- else if (placement.health === 'incomplete') {
5786
- const issue = props.controller.listUserIssues().find((candidate) => candidate.pluginId === plugin.pluginId
5787
- && candidate.platform === route.platform
5788
- && candidate.scope === placement.scope);
5789
- title = issue?.title ?? `${platformLabel(route.platform)} 状态待同步`;
5790
- description = issue?.explanation ?? '现场状态与期望状态不一致,需要重新检查并同步。';
5791
- actions = platformActions();
5792
- const actualTitle = placement.actualState === 'needsSync' ? '需要同步'
5793
- : placement.actualState === 'blocked' ? '当前有安全阻断' : '需要处理';
5794
- 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' })] });
5795
- }
5796
- else {
5797
- title = `已安装到${scopeLabel(placement.scope)}`;
5798
- const installedVersion = placement.revision.installedVersion;
5799
- description = `当前检查正常${installedVersion ? `;已安装版本 ${installedVersion}` : ''}。`;
5800
- actions = platformActions();
5801
- 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' })] });
5802
- }
5625
+ const selectedFact = platformFacts.find((entry) => entry.scope === platformSelectedScope);
5626
+ const selectedIssue = platformSelectedScope ? props.controller.listUserIssues().find((candidate) => candidate.pluginId === plugin.pluginId && candidate.platform === route.platform
5627
+ && candidate.scope === platformSelectedScope) : undefined;
5628
+ const factActions = platformFacts.map((entry) => ({
5629
+ actionId: `placement:${entry.platform}:${entry.scope}`,
5630
+ label: `${platformLabel(entry.platform)} · ${scopeLabel(entry.scope)}`,
5631
+ selected: entry.scope === platformSelectedScope,
5632
+ run: () => {
5633
+ props.controller.updatePageContext({
5634
+ platformSelectionInitialized: true,
5635
+ platformSelectedScope: entry.scope,
5636
+ });
5637
+ dynamicMenuFocus.current = undefined;
5638
+ setActionError(undefined);
5639
+ },
5640
+ }));
5641
+ actions = [...factActions, ...platformActions()];
5642
+ dynamicActionIds = actions.map((action) => action.actionId);
5643
+ dynamicActionSignature = dynamicActionIds.join('\0');
5644
+ const previousDynamicFocus = dynamicMenuFocus.current;
5645
+ if (previousDynamicFocus?.routeKey === routeKey
5646
+ && previousDynamicFocus.actionIds !== dynamicActionSignature) {
5647
+ const retained = previousDynamicFocus.focusedActionId
5648
+ ? dynamicActionIds.indexOf(previousDynamicFocus.focusedActionId) : -1;
5649
+ const selectedCard = platformSelectedScope
5650
+ ? dynamicActionIds.indexOf(`placement:${route.platform}:${platformSelectedScope}`) : -1;
5651
+ effectiveFocus = retained >= 0 ? retained : Math.max(0, selectedCard);
5652
+ }
5653
+ const cardWidth = size.columns - 2;
5654
+ const rowHeights = actions.map((action, index) => index < platformFacts.length
5655
+ ? entityPlacementRowHeight({ ...platformFacts[index], shared: platformFacts[index].sharedWithCurrentProject }, cardWidth) + 2
5656
+ : action.role === 'navigation' ? 0 : 1);
5657
+ title = `${platformLabel(route.platform)} 维护`;
5658
+ description = selectedFact
5659
+ ? `${scopeLabel(selectedFact.scope)} · ${selectedFact.observation === 'unknown'
5660
+ ? '位置状态未确认;普通写操作不可用。'
5661
+ : selectedIssue?.explanation ?? (placement?.state === 'installed'
5662
+ && placement.blockingReason === 'artifactCollision'
5663
+ ? '输出位置存在冲突内容;不会覆盖或删除占用内容。'
5664
+ : placement?.state === 'installed' && placement.sharedNative?.reason === 'outdated'
5665
+ ? '已安装版本与当前 Catalog 版本不一致。'
5666
+ : '仅显示此位置获许可的动作。')}`
5667
+ : platformFacts.length > 0 ? '先按 Enter 选择一个位置,再执行该位置的操作。'
5668
+ : plugin.aggregateStatus === 'unavailable'
5669
+ ? capabilityUnavailable ? '当前 AW CLI 不支持此插件所需功能。'
5670
+ : sourceIssue?.explanation ?? '来源当前不可用;请检查来源。'
5671
+ : placement?.state === 'blocked' ? '输出位置已有冲突内容;不会覆盖或删除。'
5672
+ : placement?.state === 'scopeConflict' ? '安装位置冲突;请先查看问题。'
5673
+ : '当前没有已确认的安装位置。';
5674
+ const viewport = rowViewport({ rowHeights, focus: effectiveFocus,
5675
+ requestedOffset: scrollOffset, rowBudget: pageActionBudget(1) });
5676
+ help = '↑↓ 移动焦点 · Enter 选择位置或动作 · Esc 返回';
5677
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(StaticRow, { title: "\u4F7F\u7528\u4F4D\u7F6E", meta: `${platformFacts.length} 个`, compact: true }), _jsx(ContentViewport, { children: actions.slice(viewport.offset, viewport.offset + viewport.limit)
5678
+ .map((action, visibleIndex) => {
5679
+ const index = viewport.offset + visibleIndex;
5680
+ if (index >= platformFacts.length)
5681
+ return _jsx(SemanticRows, { actions: actions, focus: effectiveFocus, kind: "action", compact: true, offset: index, limit: 1 }, action.actionId ?? index);
5682
+ const fact = platformFacts[index];
5683
+ return _jsx(ObjectCardFrame, { width: cardWidth, focused: effectiveFocus === index, title: `${scopeLabel(fact.scope)} ${fact.scope === platformSelectedScope ? '[选定]' : '[选择]'}`, children: _jsx(EntityPlacementRow, { placement: { ...fact, shared: fact.sharedWithCurrentProject }, width: cardWidth }) }, action.actionId);
5684
+ }) })] });
5803
5685
  }
5804
5686
  else if (route.name === 'configLayer' && plugin) {
5805
5687
  breadcrumb = `${plugin.displayName} / 设置`;
@@ -5869,7 +5751,7 @@ export function ConsoleAppV4(props) {
5869
5751
  description = undefined;
5870
5752
  if (configActionError)
5871
5753
  description = configActionError;
5872
- help = '↑↓ 移动 · Enter 修改· ←→ 分组 · s 查看保存汇总 · u 恢复继承 · Esc 返回';
5754
+ help = 'Esc 返回';
5873
5755
  if (!currentConfigPage) {
5874
5756
  content = _jsx(StatusCard, { status: "info", title: "\u6B63\u5728\u8BFB\u53D6\u63D2\u4EF6\u8BBE\u7F6E", children: _jsx(ProgressRow, { title: "\u8BFB\u53D6\u8BBE\u7F6E\u5B57\u6BB5", state: "active" }) });
5875
5757
  }
@@ -5905,6 +5787,8 @@ export function ConsoleAppV4(props) {
5905
5787
  return {
5906
5788
  value: pending ?? effectiveFieldDisplay(field),
5907
5789
  source: field.effectiveSourceLabel,
5790
+ description: field.description?.split('\n').map(safeConfigText).join('\n'),
5791
+ issue: field.invalidOverrideIssue?.split('\n').map(safeConfigText).join('\n'),
5908
5792
  invalid: Boolean(field.invalidOverrideIssue),
5909
5793
  ...(targets.length ? { pending: covered
5910
5794
  ? '待保存到个人;个人修改不会影响本项目'
@@ -5973,17 +5857,25 @@ export function ConsoleAppV4(props) {
5973
5857
  } }, ...operationActions];
5974
5858
  }
5975
5859
  const effectiveConfigFocus = Math.min(focus, Math.max(0, actions.length - 1));
5976
- const focusedConfigField = effectiveConfigFocus < (activeGroup?.fields.length ?? 0)
5977
- ? activeGroup?.fields[effectiveConfigFocus]
5978
- : undefined;
5860
+ const focusedConfigField = activeGroup?.fields.find((field) => field.key === actions[effectiveConfigFocus]?.configKey);
5861
+ const configRowBudget = pageActionBudget(Number(visibleConfigTabs.length > 1)
5862
+ + (configDraftIssues.length > 0 ? 3 : 0));
5863
+ const focusedTile = actions[effectiveConfigFocus]?.configTile;
5864
+ if (focusedTile && route.view !== 'options' && route.view !== 'values') {
5865
+ const key = `${routeKey}:${focusedConfigField?.key}`;
5866
+ const pageCount = Math.max(1, Math.ceil((configTileHeight({ width: size.columns - 2,
5867
+ ...focusedTile, focused: true }) - 2) / Math.max(1, configRowBudget - 3)));
5868
+ configCardNavigation = { key, pageCount,
5869
+ page: Math.min(pageCount, configCardPage?.key === key ? configCardPage.page : 1) };
5870
+ }
5979
5871
  if (!readonly) {
5980
5872
  const canUnsetFocused = focusedConfigField
5981
5873
  ? ['project', 'user'].some((scope) => configLayerPages[scope]?.groups.flatMap((group) => group.fields)
5982
5874
  .some((field) => field.key === focusedConfigField.key && configFieldCanUnset(field)))
5983
5875
  : false;
5984
- help = canUnsetFocused
5985
- ? '↑↓ 移动 · Enter 修改 · ←→ 分组· s 查看汇总 · u 恢复继承 · Esc 返回'
5986
- : '↑↓ 移动 · Enter 修改 · ←→ 分组· s 查看汇总 · Esc 返回';
5876
+ help = `↑↓ 选择 · Enter ${focusedConfigField ? '修改' : '选择'}${!route.view && visibleConfigTabs.length > 1
5877
+ ? ' · ←→ 分组' : ''}${(configCardNavigation?.pageCount ?? 1) > 1 ? ' · PgUp/PgDn 卡片全文' : ''}`
5878
+ + `${configDraftCount > 0 ? ' · s 查看汇总' : ''}${canUnsetFocused ? ' · u 恢复继承' : ''} · Esc 返回`;
5987
5879
  }
5988
5880
  const totalFields = currentConfigPage.groups.reduce((count, group) => count + group.fields.length, 0);
5989
5881
  const countLabel = `${totalFields} 项${currentConfigPage.presets.length > 0 ? ` · ${currentConfigPage.presets.length} 个方案` : ''}`;
@@ -5992,19 +5884,15 @@ export function ConsoleAppV4(props) {
5992
5884
  : route.view === 'values' ? (() => {
5993
5885
  title = '完整设置值与来源';
5994
5886
  description = '显示各保存位置的完整安全值;敏感值不回显。';
5995
- const lines = ['effective', 'project', 'user'].flatMap((scope) => ((scope === 'effective' ? currentConfigPage : configLayerPages[scope])?.groups.flatMap((group) => group.fields) ?? []).flatMap((field) => wrapConsoleText(`${field.title} · ${configLayerLabel({ scope })}:${scope === 'effective'
5996
- ? field.invalidOverrideIssue ? '需修正' : fieldValueText(field, field.draftValue ?? field.effectiveValue, true, field.effectiveSourceLabel !== '未设置')
5997
- : fieldValueText(field, field.draftValue ?? field.value, true, field.configured)}`, size.columns - 4)));
5998
5887
  readingText = ['effective', 'project', 'user'].flatMap((scope) => ((scope === 'effective' ? currentConfigPage : configLayerPages[scope])?.groups.flatMap((group) => group.fields) ?? []).map((field) => `${field.title} · ${configLayerLabel({ scope })}:${scope === 'effective'
5999
5888
  ? field.invalidOverrideIssue ? '需修正' : fieldValueText(field, field.draftValue ?? field.effectiveValue, true, field.effectiveSourceLabel !== '未设置')
6000
5889
  : fieldValueText(field, field.draftValue ?? field.value, true, field.configured)}`)).join('\n');
6001
- const pageRows = Math.max(1, pageActionBudget(4));
6002
- const offset = Math.min(batchTargetOffset, Math.max(0, lines.length - 1));
6003
- referenceDetailPagination = { total: lines.length, pageRows };
6004
5890
  actions = [{ label: '返回设置', run: back }];
6005
5891
  help = '←→ 或 PgUp/PgDn 阅读 · Esc 返回';
6006
- return _jsxs(Box, { flexDirection: "column", children: [lines.slice(offset, offset + pageRows).map((line, index) => _jsx(Text, { children: line }, index)), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] });
6007
- })() : _jsxs(Box, { flexDirection: "column", children: [!readonly && configDraftIssues.length > 0 ? _jsx(Callout, { status: "warning", title: "\u6709\u672A\u4FDD\u5B58\u4FEE\u6539\u65E0\u6CD5\u7EE7\u7EED\u4F7F\u7528", description: "\u63D2\u4EF6\u6765\u6E90\u6062\u590D\u540E\uFF0C\u8BBE\u7F6E\u5185\u5BB9\u6216\u53EF\u7528\u4F4D\u7F6E\u5DF2\u53D8\u5316\u3002\u8BF7\u653E\u5F03\u8FD9\u4E9B\u4FEE\u6539\uFF0C\u6216\u8FD4\u56DE\u68C0\u67E5\u63D2\u4EF6\u6765\u6E90\uFF1B\u7CFB\u7EDF\u4E0D\u4F1A\u81EA\u52A8\u4FDD\u5B58\u3002" }) : null, activeTab ? _jsx(ConfigGroupTabs, { tabs: visibleConfigTabs, current: activeTab.id, compact: policy.mode === 'compact', columns: size.columns - 2 }) : null, _jsx(SequenceRows, { actions: actions, itemCount: tabActions.length, focus: effectiveConfigFocus, scrollOffset: scrollOffset, columns: size.columns - 2, rowBudget: pageActionBudget(2 + (configDraftIssues.length > 0 ? 3 : 0)), compact: policy.mode === 'compact' }), focusedConfigField && (focusedConfigField.invalidOverrideIssue || focusedConfigField.description) ? _jsx(Text, { color: focusedConfigField.invalidOverrideIssue ? tuiTheme.roles.error : tuiTheme.roles.secondary, wrap: "truncate-end", children: focusedConfigField.invalidOverrideIssue ?? focusedConfigField.description }) : null] });
5892
+ return _jsxs(Box, { flexDirection: "column", children: [readingPage(readingText, size.columns - 2, Math.max(1, pageActionBudget(4) - 1)), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] });
5893
+ })() : _jsxs(Box, { flexDirection: "column", children: [!readonly && configDraftIssues.length > 0 ? _jsx(Callout, { status: "warning", title: "\u6709\u672A\u4FDD\u5B58\u4FEE\u6539\u65E0\u6CD5\u7EE7\u7EED\u4F7F\u7528", description: "\u63D2\u4EF6\u6765\u6E90\u6062\u590D\u540E\uFF0C\u8BBE\u7F6E\u5185\u5BB9\u6216\u53EF\u7528\u4F4D\u7F6E\u5DF2\u53D8\u5316\u3002\u8BF7\u653E\u5F03\u8FD9\u4E9B\u4FEE\u6539\uFF0C\u6216\u8FD4\u56DE\u68C0\u67E5\u63D2\u4EF6\u6765\u6E90\uFF1B\u7CFB\u7EDF\u4E0D\u4F1A\u81EA\u52A8\u4FDD\u5B58\u3002" }) : null, activeTab && visibleConfigTabs.length > 1 ? _jsx(QueryHeader, { width: size.columns - 2, tabs: { label: '分组', items: visibleConfigTabs.map((tab) => ({
5894
+ id: tab.id, label: tab.label, count: tab.count,
5895
+ })), currentId: activeTab.id } }) : null, _jsx(SequenceRows, { actions: actions, itemCount: tabActions.length, focus: effectiveConfigFocus, scrollOffset: scrollOffset, columns: size.columns - 2, rowBudget: configRowBudget, configPage: configCardNavigation?.page, compact: policy.mode === 'compact' })] });
6008
5896
  }
6009
5897
  }
6010
5898
  else if (route.name === 'configSave' && plugin) {
@@ -6018,22 +5906,16 @@ export function ConsoleAppV4(props) {
6018
5906
  title: `${scopeLabel(group.scope)} · ${change.title}`,
6019
5907
  })));
6020
5908
  if (route.view === 'changes') {
6021
- const lines = changeDetailLines(changes, size.columns - 4);
6022
5909
  readingText = changeDetailText(changes);
6023
- const pageRows = Math.max(1, Math.min(responsiveBudget(5, 10), pageActionBudget(5)));
6024
- const detailOffset = Math.min(batchTargetOffset, Math.max(0, lines.length - 1));
6025
- const visibleLines = lines.slice(detailOffset, detailOffset + pageRows);
6026
- referenceDetailPagination = { total: lines.length, pageRows };
5910
+ const pageRows = Math.max(1, Math.min(responsiveBudget(5, 10), pageActionBudget(5) - 1));
6027
5911
  breadcrumb = `${plugin.displayName} / 设置 / 完整变化`;
6028
- context = `${Math.min(detailOffset + 1, lines.length)}–${Math.min(detailOffset + visibleLines.length, lines.length)} / ${lines.length}`;
6029
5912
  title = '完整设置变化';
6030
5913
  description = '逐项显示修改前和修改后的完整值。';
6031
5914
  help = '←→ 或 PgUp/PgDn 阅读 · ↑↓ 移动 · Enter 选择 · Esc 返回保存汇总';
6032
5915
  actions = [
6033
5916
  { label: '返回保存汇总', run: back },
6034
5917
  ];
6035
- content = _jsxs(Box, { flexDirection: "column", children: [detailOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${detailOffset} 行`, status: "muted", compact: true }) : null, visibleLines.map((line, index) => _jsx(StaticRow, { title: line, compact: true }, `${detailOffset + index}:${line}`)), detailOffset + visibleLines.length < lines.length
6036
- ? _jsx(StaticRow, { title: `↓ 后面还有 ${lines.length - detailOffset - visibleLines.length} 行`, status: "muted", compact: true }) : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] });
5918
+ content = _jsxs(Box, { flexDirection: "column", children: [readingPage(readingText, size.columns - 2, pageRows), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] });
6037
5919
  }
6038
5920
  else {
6039
5921
  const visibleChanges = changes.slice(0, 2);
@@ -6408,7 +6290,24 @@ export function ConsoleAppV4(props) {
6408
6290
  description = '单个来源失败不会阻止其他来源继续工作。';
6409
6291
  context = `${props.controller.listSources().length} 个来源`;
6410
6292
  help = '↑↓ 移动 · Enter 打开 · Esc 返回';
6411
- actions = props.controller.listSources().map((source) => {
6293
+ const allSources = props.controller.listSources();
6294
+ const sourceKinds = new Set(allSources.map((source) => props.controller.getConfiguredSource(source.id)?.type
6295
+ ?? props.controller.getSourceOverview(source.id).detail?.type));
6296
+ const hasTypeTabs = sourceKinds.has('git') && sourceKinds.has('path');
6297
+ const sourceType = hasTypeTabs ? snapshot.router.current.context.sourceTypeTab ?? 'all' : 'all';
6298
+ const visibleSources = matchingSources(props.controller, query, sourceType);
6299
+ moveSourceType = hasTypeTabs ? (direction) => {
6300
+ const tabs = ['all', 'git', 'path'];
6301
+ const index = tabs.indexOf(sourceType);
6302
+ const next = tabs[(index + direction + tabs.length) % tabs.length];
6303
+ const nextSources = matchingSources(props.controller, query, next);
6304
+ const focusedId = actions[focus]?.actionId;
6305
+ const retained = nextSources.findIndex((source) => `source:${source.id}` === focusedId);
6306
+ props.controller.updatePageContext({ sourceTypeTab: next });
6307
+ setFocus(retained >= 0 ? retained : Math.min(focus, Math.max(0, nextSources.length - 1)));
6308
+ setScrollOffset(0);
6309
+ } : undefined;
6310
+ actions = visibleSources.map((source) => {
6412
6311
  const overview = props.controller.getSourceOverview(source.id);
6413
6312
  const generation = props.controller.getCatalogGeneration(source.id);
6414
6313
  const freshness = props.controller.getCatalogFreshness(source.id);
@@ -6435,6 +6334,7 @@ export function ConsoleAppV4(props) {
6435
6334
  const available = generation?.state === 'ready' || health === 'ready';
6436
6335
  const unavailable = health === 'unavailable' || health === 'error' || health === 'recovery';
6437
6336
  return {
6337
+ actionId: `source:${source.id}`,
6438
6338
  label: source.id,
6439
6339
  description: sourceLocationSummary(overview.detail?.type ?? 'git', overview.detail?.locationSummary),
6440
6340
  sourceLabel: policy,
@@ -6459,7 +6359,13 @@ export function ConsoleAppV4(props) {
6459
6359
  help = '↑↓ 移动 · Enter 打开 · Esc 返回';
6460
6360
  const sourceCount = actions.length;
6461
6361
  actions = [...actions, ...bottomActions];
6462
- content = _jsx(SequenceRows, { actions: actions, itemCount: sourceCount, focus: focus, scrollOffset: scrollOffset, columns: size.columns - 2, rowBudget: pageActionBudget(), compact: policy.mode === 'compact', cards: true });
6362
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(QueryHeader, { width: size.columns - 2, tabs: hasTypeTabs ? { label: '类型', currentId: sourceType, items: [
6363
+ { id: 'all', label: '全部', count: matchingSources(props.controller, query, 'all').length },
6364
+ { id: 'git', label: 'Git', count: matchingSources(props.controller, query, 'git').length },
6365
+ { id: 'path', label: '本地', count: matchingSources(props.controller, query, 'path').length },
6366
+ ] } : undefined, search: { value: query, placeholder: '名称或地址', input: searchFocus
6367
+ ? _jsx(InputDisplay, { value: query, cursor: searchCursor, columns: querySearchWidth(size.columns - 2) }) : undefined } }), sourceCount === 0 && allSources.length > 0
6368
+ ? _jsx(Text, { color: tuiTheme.roles.secondary, children: "\u5F53\u524D\u6761\u4EF6\u65E0\u5339\u914D\uFF1B\u641C\u7D22\u548C\u7C7B\u578B\u4FDD\u6301\u539F\u6837\u3002" }) : null, _jsx(SequenceRows, { actions: actions, itemCount: sourceCount, focus: focus, scrollOffset: scrollOffset, columns: size.columns - 2, rowBudget: pageActionBudget(1 + Number(hasTypeTabs) + Number(sourceCount === 0 && allSources.length > 0)), compact: policy.mode === 'compact', cards: true, inactive: searchFocus })] });
6463
6369
  }
6464
6370
  else if (route.name === 'catalogStoreOverview') {
6465
6371
  breadcrumb = '插件来源 / 本地存储';
@@ -6498,7 +6404,16 @@ export function ConsoleAppV4(props) {
6498
6404
  { actionId: 'catalog-store-rules-technical', label: showTechnical ? '隐藏技术详情' : '技术详情', run: () => toggleTechnical() },
6499
6405
  { role: 'navigation', actionId: 'catalog-store-rules-back', label: '返回存储概览', run: back },
6500
6406
  ];
6501
- content = _jsxs(Box, { flexDirection: "column", children: [!showTechnical ? _jsxs(_Fragment, { children: [_jsx(SectionCard, { title: "\u59CB\u7EC8\u4FDD\u7559 \u00B7 \u5F53\u524D\u4F7F\u7528\u7684\u76EE\u5F55\u4E0D\u4F1A\u5220\u9664", children: _jsx(StaticRow, { title: "\u4E0D\u5C5E\u4E8E AW\u3001\u65E0\u6CD5\u786E\u8BA4\u5B89\u5168\u6216\u4ECD\u5728\u4F7F\u7528\u7684\u5185\u5BB9\u4F1A\u4FDD\u7559", status: "muted", compact: true }) }), _jsxs(SectionCard, { title: "\u6EE1\u8DB3\u65F6\u95F4\u548C\u5B8C\u6574\u9A8C\u8BC1\u540E\u53EF\u56DE\u6536", children: [_jsx(StaticRow, { title: "\u65E7\u7248\u672C\u6EE1\u8DB3\u4FDD\u7559\u671F\u9650\u540E\u624D\u6E05\u7406", compact: true }), _jsx(StaticRow, { title: "\u672A\u5B8C\u6210\u7684\u4E0B\u8F7D\u6216\u66FF\u6362\u5185\u5BB9\u81F3\u5C11\u4FDD\u7559 24 \u5C0F\u65F6", compact: true }), _jsx(StaticRow, { title: "\u9694\u79BB\u5185\u5BB9\u81F3\u5C11\u4FDD\u7559 24 \u5C0F\u65F6", compact: true }), _jsx(StaticRow, { title: "\u5BF9\u5E94\u6B63\u5F0F\u7248\u672C\u5B8C\u6574\u53EF\u7528\u540E\u624D\u6E05\u7406", compact: true })] })] }) : null, showTechnical ? _jsxs(TechnicalDetails, { title: "\u6E05\u7406\u5224\u5B9A\u8BC1\u636E", children: [_jsx(StaticRow, { title: "\u9501", meta: "AW_HOME \u5199\u9501\u5185\u91CD\u65B0\u68C0\u67E5", compact: true }), _jsx(StaticRow, { title: "\u6B63\u5F0F\u76EE\u5F55\u7248\u672C", meta: "\u975E Active \u9879\u4FDD\u7559 30 \u5929", compact: true }), _jsx(StaticRow, { title: "staging\u3001replacement\u3001delete claim", meta: "\u4FDD\u7559 24 \u5C0F\u65F6", compact: true }), _jsx(StaticRow, { title: "quarantine", meta: "\u4FDD\u7559 24 \u5C0F\u65F6\uFF1B\u5BF9\u5E94\u6B63\u5F0F\u76EE\u5F55\u7248\u672C\u5FC5\u987B\u5B8C\u6574\u5065\u5EB7", compact: true }), _jsx(StaticRow, { title: "\u4FDD\u7559\u8282\u70B9", meta: "foreign\u3001\u7B26\u53F7\u94FE\u63A5\u3001unsafe\u3001\u635F\u574F\u6216\u672A\u77E5\u9879\u76EE", compact: true })] }) : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
6407
+ if (showTechnical)
6408
+ readingText = [
6409
+ '锁 · AW_HOME 写锁内重新检查',
6410
+ '正式目录版本 · 非 Active 项保留 30 天',
6411
+ 'staging、replacement、delete claim · 保留 24 小时',
6412
+ 'quarantine · 保留 24 小时;对应正式目录版本必须完整健康',
6413
+ '保留节点 · foreign、符号链接、unsafe、损坏或未知项目',
6414
+ ].join('\n');
6415
+ content = _jsxs(Box, { flexDirection: "column", children: [!showTechnical ? _jsxs(_Fragment, { children: [_jsx(SectionCard, { title: "\u59CB\u7EC8\u4FDD\u7559 \u00B7 \u5F53\u524D\u4F7F\u7528\u7684\u76EE\u5F55\u4E0D\u4F1A\u5220\u9664", children: _jsx(StaticRow, { title: "\u4E0D\u5C5E\u4E8E AW\u3001\u65E0\u6CD5\u786E\u8BA4\u5B89\u5168\u6216\u4ECD\u5728\u4F7F\u7528\u7684\u5185\u5BB9\u4F1A\u4FDD\u7559", status: "muted", compact: true }) }), _jsxs(SectionCard, { title: "\u6EE1\u8DB3\u65F6\u95F4\u548C\u5B8C\u6574\u9A8C\u8BC1\u540E\u53EF\u56DE\u6536", children: [_jsx(StaticRow, { title: "\u65E7\u7248\u672C\u6EE1\u8DB3\u4FDD\u7559\u671F\u9650\u540E\u624D\u6E05\u7406", compact: true }), _jsx(StaticRow, { title: "\u672A\u5B8C\u6210\u7684\u4E0B\u8F7D\u6216\u66FF\u6362\u5185\u5BB9\u81F3\u5C11\u4FDD\u7559 24 \u5C0F\u65F6", compact: true }), _jsx(StaticRow, { title: "\u9694\u79BB\u5185\u5BB9\u81F3\u5C11\u4FDD\u7559 24 \u5C0F\u65F6", compact: true }), _jsx(StaticRow, { title: "\u5BF9\u5E94\u6B63\u5F0F\u7248\u672C\u5B8C\u6574\u53EF\u7528\u540E\u624D\u6E05\u7406", compact: true })] })] }) : null, readingText !== undefined
6416
+ ? readingPage(readingText, size.columns - 2, Math.max(1, pageActionBudget(5) - 1)) : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
6502
6417
  }
6503
6418
  else if (route.name === 'sourceDetail') {
6504
6419
  const overview = props.controller.getSourceOverview(route.sourceId);
@@ -6595,6 +6510,7 @@ export function ConsoleAppV4(props) {
6595
6510
  : freshness?.state === 'updateAvailable' || freshness?.state === 'localChangesAvailable'
6596
6511
  ? [{
6597
6512
  actionId: 'source-update',
6513
+ status: 'info',
6598
6514
  label: freshness.state === 'localChangesAvailable'
6599
6515
  ? '应用本地内容变化' : `更新到 ${candidateVersion ?? '待检查版本'}`,
6600
6516
  description: '进入确认;确认前不会切换当前版本',
@@ -6720,26 +6636,36 @@ export function ConsoleAppV4(props) {
6720
6636
  : generationState === 'configMismatch' ? '来源设置与当前目录不一致'
6721
6637
  : generationState === 'stateInvalid' ? '来源状态需要恢复'
6722
6638
  : '来源尚未启用';
6723
- const sourceSummary = [
6724
- `${transport} · ${sourceLocationSummary(detail?.type ?? 'git', detail?.locationSummary)}`,
6725
- `当前 ${activeVersion ?? '未启用'}${candidateVersion ? ` → ${candidateVersion}` : ''} · ${sourceCount === undefined ? '插件数未知' : `${sourceCount} 个插件`}`,
6726
- `版本策略:${versionPolicy}`,
6727
- `检查:${freshnessLabel} · ${freshness?.state === 'checking'
6728
- ? '正在检查这个来源;必要时验证内容'
6729
- : relativeCheckTime(freshness?.checkedAt)}`,
6730
- ].join('\n');
6731
- const sourceCardHeight = objectCardRowLayout({
6732
- description: sourceSummary, width: size.columns - 2, hasTags: true,
6733
- }).height;
6639
+ const sourceFacts = {
6640
+ sourceId: route.sourceId, primary: detail?.primary ?? false,
6641
+ address: sourceLocationSummary(detail?.type ?? 'git', detail?.locationSummary),
6642
+ transport, versionPolicy, currentVersion: activeVersion, candidateVersion,
6643
+ pluginCount: sourceCount,
6644
+ state: { label: sourceStateLabel, status: generationState === 'ready' ? 'success' : 'warning' },
6645
+ check: { label: freshnessLabel, time: relativeCheckTime(freshness?.checkedAt) },
6646
+ };
6647
+ title = '';
6648
+ context = '来源详情';
6649
+ if (generationState === 'ready' && freshness?.state !== 'checkFailed'
6650
+ && freshness?.state !== 'incompatible')
6651
+ description = undefined;
6652
+ actions.unshift({ actionId: 'source-information', label: '来源信息', run: () => setFocus(1) });
6734
6653
  const focusedSourceDescription = actions[focus]?.disabled ?? actions[focus]?.description;
6735
6654
  const sourceDescriptionLines = focusedSourceDescription
6736
6655
  ? wrapConsoleText(focusedSourceDescription, size.columns - 2) : [];
6737
- const sourceActionDescriptionLines = sourceDescriptionLines.length <= pageActionBudget(sourceCardHeight + 1)
6738
- ? sourceDescriptionLines : [];
6739
- const detailMenuViewport = actionViewport(actions, true, focus, scrollOffset, pageActionBudget(sourceCardHeight + sourceActionDescriptionLines.length));
6740
- content = _jsxs(Box, { flexDirection: "column", children: [_jsx(ObjectCardRow, { width: size.columns - 2, title: route.sourceId, description: sourceSummary, tags: _jsxs(Box, { children: [detail?.primary ? _jsx(Text, { color: tuiTheme.colors.accent, children: "[\u4E3B\u6765\u6E90] " }) : null, _jsx(StatusBadge, { items: [{ key: 'source-state', label: sourceStateLabel,
6741
- symbol: generationState === 'ready' ? '✓' : '!',
6742
- status: generationState === 'ready' ? 'success' : 'warning' }] })] }) }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: detailMenuViewport.offset, limit: detailMenuViewport.limit }), sourceActionDescriptionLines.map((line, index) => _jsx(Text, { color: tuiTheme.colors.muted, children: line }, index))] });
6656
+ const sourceActionDescriptionLines = sourceDescriptionLines;
6657
+ const cardWidth = size.columns - 2;
6658
+ const sourceLayout = sourceDetailCardLayout(sourceFacts, cardWidth);
6659
+ const lineLimit = Math.max(1, pageActionBudget(sourceActionDescriptionLines.length + 2) - 4);
6660
+ const lineOffset = Math.min(batchTargetOffset, Math.max(0, sourceLayout.factRows - lineLimit));
6661
+ entityCardNavigation = { itemCount: 1, factRows: [sourceLayout.factRows], lineLimit };
6662
+ const sourceCardHeight = entityCardWindowHeight(sourceLayout, lineOffset, lineLimit);
6663
+ currentActionViewportLineBudget = Math.max(1, pageActionBudget(sourceCardHeight + sourceActionDescriptionLines.length));
6664
+ const detailMenuViewport = actionViewport(actions, true, focus, scrollOffset, currentActionViewportLineBudget);
6665
+ help = focus === 0
6666
+ ? '↑↓ 浏览信息 · PgUp/PgDn 翻信息 · a 选择动作 · Enter 选择动作 · Esc 返回'
6667
+ : '↑↓ 选择动作 · a 查看信息 · Enter 选择 · Esc 返回';
6668
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(SourceDetailCard, { facts: sourceFacts, width: cardWidth, focused: focus === 0, lineOffset: lineOffset, lineLimit: lineLimit }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: Math.max(1, detailMenuViewport.offset), limit: Math.max(0, detailMenuViewport.limit - Number(detailMenuViewport.offset === 0)) }), sourceActionDescriptionLines.map((line, index) => _jsx(Text, { color: tuiTheme.colors.muted, children: line }, index))] });
6743
6669
  }
6744
6670
  else if (route.name === 'sourceMore') {
6745
6671
  const overview = props.controller.getSourceOverview(route.sourceId);
@@ -6830,16 +6756,13 @@ export function ConsoleAppV4(props) {
6830
6756
  const diagnostic = resolveSourceTechnicalDiagnostic(route.sourceId);
6831
6757
  readingText = diagnosticDetailText(diagnostic);
6832
6758
  breadcrumb = `插件来源 / ${route.sourceId} / 技术详情`;
6833
- const lines = diagnosticDetailLines(diagnostic, size.columns - 6);
6834
- const offset = Math.min(batchTargetOffset, Math.max(0, lines.length - sourceTechnicalPageRows));
6835
- context = `${offset + 1}–${Math.min(lines.length, offset + sourceTechnicalPageRows)} / ${lines.length} 行`;
6836
6759
  title = '来源技术诊断';
6837
6760
  description = '';
6838
6761
  help = '←→ 或 PgUp/PgDn 翻页 · Esc 返回';
6839
6762
  actions = [
6840
6763
  { label: '返回来源详情', run: back },
6841
6764
  ];
6842
- content = _jsxs(Box, { flexDirection: "column", children: [_jsx(SectionCard, { title: "\u5B8C\u6574\u5B89\u5168\u5B57\u6BB5", compact: true, children: lines.slice(offset, offset + sourceTechnicalPageRows).map((line, index) => _jsx(Text, { children: line }, offset + index)) }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] });
6765
+ content = _jsxs(Box, { flexDirection: "column", children: [readingPage(readingText, size.columns - 2, Math.max(1, pageActionBudget(4) - 1)), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] });
6843
6766
  }
6844
6767
  else if (route.name === 'sourceAdd' && sourceDraft?.owner === route.owner) {
6845
6768
  breadcrumb = sourceDraft.mode === 'add' ? '插件来源 / 添加' : `插件来源 / ${sourceDraft.sourceId} / 编辑`;
@@ -6976,22 +6899,16 @@ export function ConsoleAppV4(props) {
6976
6899
  else if (route.name === 'sourceSave' && sourceDraft?.owner === route.owner) {
6977
6900
  const changes = sourceDraftChanges(sourceDraft);
6978
6901
  if (route.view === 'changes') {
6979
- const lines = changeDetailLines(changes, size.columns - 4);
6980
6902
  readingText = changeDetailText(changes);
6981
- const pageRows = Math.max(1, Math.min(responsiveBudget(5, 10), pageActionBudget(5)));
6982
- const detailOffset = Math.min(batchTargetOffset, Math.max(0, lines.length - 1));
6983
- const visibleLines = lines.slice(detailOffset, detailOffset + pageRows);
6984
- referenceDetailPagination = { total: lines.length, pageRows };
6903
+ const pageRows = Math.max(1, Math.min(responsiveBudget(5, 10), pageActionBudget(5) - 1));
6985
6904
  breadcrumb = `插件来源 / ${sourceDraft.sourceId} / 完整变化`;
6986
- context = `${Math.min(detailOffset + 1, lines.length)}–${Math.min(detailOffset + visibleLines.length, lines.length)} / ${lines.length}`;
6987
6905
  title = '完整来源变化';
6988
6906
  description = '逐项显示修改前和修改后的完整值。';
6989
6907
  help = '←→ 或 PgUp/PgDn 阅读 · ↑↓ 移动 · Enter 选择 · Esc 返回修改汇总';
6990
6908
  actions = [
6991
6909
  { label: '返回修改汇总', run: back },
6992
6910
  ];
6993
- content = _jsxs(Box, { flexDirection: "column", children: [detailOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${detailOffset} 行`, status: "muted", compact: true }) : null, visibleLines.map((line, index) => _jsx(StaticRow, { title: line, compact: true }, `${detailOffset + index}:${line}`)), detailOffset + visibleLines.length < lines.length
6994
- ? _jsx(StaticRow, { title: `↓ 后面还有 ${lines.length - detailOffset - visibleLines.length} 行`, status: "muted", compact: true }) : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] });
6911
+ content = _jsxs(Box, { flexDirection: "column", children: [readingPage(readingText, size.columns - 2, pageRows), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] });
6995
6912
  }
6996
6913
  else {
6997
6914
  const savedSource = props.controller.getConfiguredSource(sourceDraft.sourceId);
@@ -7046,16 +6963,14 @@ export function ConsoleAppV4(props) {
7046
6963
  const references = sourceReferenceSummary?.dependencies ?? [];
7047
6964
  if (sourceDependenciesExpanded)
7048
6965
  readingText = references.map((dependency) => `${dependency.category} · ${dependency.pluginId ?? dependency.sourceId ?? '身份未知'} · ${dependency.platform ? platformLabel(dependency.platform) : ''}${dependency.scope ? ` · ${scopeLabel(dependency.scope)}` : ''}`).join('\n');
7049
- const referenceOffset = Math.min(batchTargetOffset, Math.max(0, references.length - 1));
7050
- const visibleReferences = references.slice(referenceOffset, referenceOffset + referenceLimit);
7051
- const hiddenReferences = Math.max(0, references.length - referenceOffset - visibleReferences.length);
6966
+ const referencePage = pagedItems(references.length, referenceLimit);
6967
+ const visibleReferences = references.slice(referencePage.offset, referencePage.offset + referenceLimit);
7052
6968
  if (sourceDependenciesExpanded && references.length > referenceLimit) {
7053
6969
  referenceDetailPagination = { total: references.length, pageRows: referenceLimit };
7054
6970
  help = '←→ 查看全部引用 · ↑↓ 移动 · Enter 选择 · Esc 返回';
7055
6971
  }
7056
- content = _jsxs(Box, { flexDirection: "column", children: [_jsx(Callout, { status: "error", title: sourceReferenceSummary?.inventoryComplete === false ? '引用清单不完整' : '发现活动引用' }), _jsxs(SectionCard, { title: sourceDependenciesExpanded
7057
- ? referenceOffset > 0 ? `引用内容 · ↑ 前面还有 ${referenceOffset} 项` : '引用内容'
7058
- : '引用汇总', children: [!sourceDependenciesExpanded ? _jsxs(_Fragment, { 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, '个') })] }) : 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' })] });
6972
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(Callout, { status: "error", title: sourceReferenceSummary?.inventoryComplete === false ? '引用清单不完整' : '发现活动引用' }), _jsxs(SectionCard, { title: sourceDependenciesExpanded ? '引用内容' : '引用汇总', children: [!sourceDependenciesExpanded ? _jsxs(_Fragment, { 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, '个') })] }) : 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 ?? referencePage.offset + index}`)) : null, sourceDependenciesExpanded && referencePage.pageCount > 1
6973
+ ? _jsx(Pager, { page: referencePage.page, pageCount: referencePage.pageCount, columns: size.columns - 6 }) : null] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' })] });
7059
6974
  }
7060
6975
  else if (route.name === 'issueList') {
7061
6976
  const issues = props.controller.listUserIssues();
@@ -7293,14 +7208,12 @@ export function ConsoleAppV4(props) {
7293
7208
  context = '安全详情';
7294
7209
  title = detail ? '安全技术详情' : '操作记录已不可用';
7295
7210
  description = detail
7296
- ? '可滚动查看所有目标、阶段、代码和已保留的安全原因。'
7211
+ ? '可翻页查看所有目标、阶段、代码和已保留的安全原因。'
7297
7212
  : '这项会话记录已经不再可用。';
7298
7213
  const preview = detail ? props.controller.previewOperationReport(route.recordId) : undefined;
7299
- const detailLineLimit = operationDetailPageRows;
7214
+ const detailLineLimit = Math.max(1, pageActionBudget(policy.mode === 'compact' ? 1 : 5) - 1);
7300
7215
  if (detail) {
7301
7216
  readingText = operationDiagnosticText(detail);
7302
- const total = operationDiagnosticLineCount(detail, size.columns);
7303
- context = `${Math.min(total, scrollOffset + 1)}–${Math.min(total, scrollOffset + detailLineLimit)} / ${total} 行`;
7304
7217
  }
7305
7218
  actions = detail ? [
7306
7219
  preview
@@ -7309,7 +7222,7 @@ export function ConsoleAppV4(props) {
7309
7222
  { role: 'navigation', label: '返回操作记录', run: back },
7310
7223
  ] : [{ role: 'navigation', label: '返回操作记录', run: back }];
7311
7224
  const diagnosticActionViewport = actionViewport(actions, policy.mode === 'compact', focus, 0, policy.mode === 'compact' ? 2 : 3);
7312
- content = detail ? _jsxs(Box, { flexDirection: "column", children: [_jsx(ContentViewport, { children: _jsx(OperationDiagnosticScreen, { detail: detail, compact: policy.mode === 'compact', columns: size.columns, offset: scrollOffset, limit: detailLineLimit }) }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: diagnosticActionViewport.offset, limit: diagnosticActionViewport.limit })] }) : _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' })] });
7225
+ content = detail && readingText !== undefined ? _jsxs(Box, { flexDirection: "column", children: [policy.mode === 'compact' ? null : _jsx(ResultCard, { status: operationHistoryState(detail.summary).status, title: detail.summary.message, meta: operationHistoryState(detail.summary).label, compact: false }), _jsx(ContentViewport, { children: readingPage(readingText, size.columns - 2, detailLineLimit, 'scroll') }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact', offset: diagnosticActionViewport.offset, limit: diagnosticActionViewport.limit })] }) : _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' })] });
7313
7226
  }
7314
7227
  else if (route.name === 'operationReportPreview') {
7315
7228
  const preview = props.controller.previewOperationReport(route.recordId);
@@ -7327,9 +7240,9 @@ export function ConsoleAppV4(props) {
7327
7240
  }
7328
7241
  else if (preview) {
7329
7242
  readingText = preview.content;
7330
- const reportLineLimit = operationReportPageRows;
7331
- const total = operationReportLineCount(preview, size.columns);
7332
- context = `${Math.min(total, scrollOffset + 1)}–${Math.min(total, scrollOffset + reportLineLimit)} / ${total} 行`;
7243
+ const reportFeedbackVisible = Boolean(reportSaveResult || reportSaveError);
7244
+ const reportLineLimit = Math.max(1, pageActionBudget((policy.mode === 'compact' ? 2 : 8)
7245
+ + (reportFeedbackVisible ? 4 : 0)) - 1);
7333
7246
  actions = [
7334
7247
  { label: '选择本地保存位置', description: '保存前输入明确的文件路径', run: () => {
7335
7248
  setReportSaveMode(true);
@@ -7346,11 +7259,10 @@ export function ConsoleAppV4(props) {
7346
7259
  } },
7347
7260
  { role: 'navigation', label: '返回技术详情', run: back },
7348
7261
  ];
7349
- const reportFeedbackVisible = Boolean(reportSaveResult || reportSaveError);
7350
7262
  const compactReportActions = policy.mode === 'compact' || reportFeedbackVisible;
7351
7263
  const reportActionLimit = policy.mode === 'compact' || reportFeedbackVisible ? 2 : 3;
7352
7264
  const reportActionOffset = Math.min(Math.max(0, focus - 1), Math.max(0, actions.length - reportActionLimit));
7353
- content = _jsxs(Box, { flexDirection: "column", children: [_jsx(ContentViewport, { children: _jsx(OperationReportPreviewScreen, { preview: preview, compact: policy.mode === 'compact', columns: size.columns, 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: compactReportActions, offset: reportActionOffset, limit: reportActionLimit })] });
7265
+ content = _jsxs(Box, { flexDirection: "column", children: [policy.mode === 'compact' ? null : _jsx(ResultCard, { status: preview.omitted ? 'warning' : 'info', title: "\u62A5\u544A\u9884\u89C8", description: preview.omitted ? '报告只包含已保留的安全内容。' : '报告只包含本页安全内容。', meta: preview.fileName, compact: false }), _jsx(ContentViewport, { children: readingPage(readingText, size.columns - 2, reportLineLimit, 'scroll') }), 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: compactReportActions, offset: reportActionOffset, limit: reportActionLimit })] });
7354
7266
  }
7355
7267
  else {
7356
7268
  actions = [{ role: 'navigation', label: '返回技术详情', run: back }];
@@ -7648,13 +7560,6 @@ export function ConsoleAppV4(props) {
7648
7560
  } : group).filter((group) => group.changes.length > 0);
7649
7561
  const headingLines = compressedReview ? 0 : visibleSpecs.filter((group) => group.label).length;
7650
7562
  const staticRowBudget = Math.max(visibleSpecs.length, responsiveBudget(7, 12) - headingLines);
7651
- const reviewPaginationTotal = showTechnical
7652
- ? Math.max(review.changes.length, technicalDetails.length)
7653
- : userReviewChanges.length;
7654
- const reviewPageRows = Math.max(1, responsiveBudget(6, 10) - actions.length - 2);
7655
- if (reviewPaginationTotal > reviewPageRows) {
7656
- referenceDetailPagination = { total: reviewPaginationTotal, pageRows: 1 };
7657
- }
7658
7563
  const reviewActionViewport = actionViewport(actions, true, focus, scrollOffset, responsiveBudget(2, 6));
7659
7564
  const allocations = new Map(visibleSpecs.map((group) => [group.key, 1]));
7660
7565
  const targetGroup = visibleSpecs.find((group) => group.key === 'portableTarget');
@@ -7692,8 +7597,11 @@ export function ConsoleAppV4(props) {
7692
7597
  const batchChangeLimit = portableReview
7693
7598
  ? responsiveBudget(3, 5)
7694
7599
  : policy.mode === 'compact' ? 1 : Math.max(1, responsiveBudget(6, 10) - reviewActionViewport.limit);
7695
- const batchChanges = userReviewChanges.slice(batchTargetOffset, batchTargetOffset + batchChangeLimit);
7696
- const batchHiddenBefore = batchTargetOffset;
7600
+ const batchChangePage = paginateWrappedRows({
7601
+ lines: userReviewChanges.map((_, index) => String(index)), rows: batchChangeLimit,
7602
+ page: batchTargetOffset + 1,
7603
+ });
7604
+ const batchChanges = userReviewChanges.slice(batchChangePage.offset, batchChangePage.offset + batchChangeLimit);
7697
7605
  const prioritizedReviewChanges = portableReview ? [
7698
7606
  ...userReviewChanges.filter((change) => change.kind === 'warning'),
7699
7607
  ...userReviewChanges.filter((change) => change.kind !== 'warning'
@@ -7704,21 +7612,19 @@ export function ConsoleAppV4(props) {
7704
7612
  ...userReviewChanges.filter((change) => change.kind === 'warning'),
7705
7613
  ...userReviewChanges.filter((change) => change.kind !== 'warning'),
7706
7614
  ];
7707
- const pagedReviewChanges = prioritizedReviewChanges.slice(batchTargetOffset, batchTargetOffset + batchChangeLimit);
7615
+ const pagedReviewChanges = prioritizedReviewChanges.slice(batchChangePage.offset, batchChangePage.offset + batchChangeLimit);
7708
7616
  const changeGroups = portableReview || batchReview || userReviewChanges.length > staticRowBudget
7709
- ? _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: `${[
7710
- portableReview && change.group ? groupTitle[change.group] : undefined,
7711
- change.sourceId,
7712
- change.relation === 'requested' ? '根插件'
7713
- : change.relation === 'direct' ? '直接依赖'
7714
- : change.relation === 'transitive' ? '间接依赖' : undefined,
7715
- ].filter(Boolean).join(' · ')}${portableReview && change.group || change.sourceId || change.relation ? ' · ' : ''}${reviewSymbol(change)} ${change.title}`, description: (portableReview && change.kind !== 'warning') || exactProjectSyncReview
7716
- ? change.description : undefined, meta: portableReview || exactProjectSyncReview ? undefined : change.description, status: changeStatus(change), compact: !portableReview && !exactProjectSyncReview }, `${change.batchGroupId ?? ''}:${change.pluginId ?? change.title}:${index}`)), Math.max(0, userReviewChanges.length - batchTargetOffset - (batchReview ? batchChanges : pagedReviewChanges).length) > 0 ? _jsx(StaticRow, { title: `↓ 后面还有 ${Math.max(0, userReviewChanges.length - batchTargetOffset - (batchReview ? batchChanges : pagedReviewChanges).length)} 项`, status: "muted", compact: true }) : null] })
7617
+ ? _jsx(Box, { flexDirection: "column", children: (batchReview ? batchChanges : pagedReviewChanges).map((change, index) => _jsx(ReviewChangeRow, { title: `${[
7618
+ portableReview && change.group ? groupTitle[change.group] : undefined,
7619
+ change.sourceId,
7620
+ change.relation === 'requested' ? '根插件'
7621
+ : change.relation === 'direct' ? '直接依赖'
7622
+ : change.relation === 'transitive' ? '间接依赖' : undefined,
7623
+ ].filter(Boolean).join(' · ')}${portableReview && change.group || change.sourceId || change.relation ? ' · ' : ''}${reviewSymbol(change)} ${change.title}`, description: (portableReview && change.kind !== 'warning') || exactProjectSyncReview
7624
+ ? change.description : undefined, meta: portableReview || exactProjectSyncReview ? undefined : change.description, status: changeStatus(change), compact: !portableReview && !exactProjectSyncReview }, `${change.batchGroupId ?? ''}:${change.pluginId ?? change.title}:${index}`)) })
7717
7625
  : standardChangeGroups;
7718
7626
  const technicalDetailLimit = responsiveBudget(1, 1);
7719
- const visibleTechnicalDetails = technicalDetails.slice(batchTargetOffset, batchTargetOffset + technicalDetailLimit);
7720
- const hiddenTechnicalDetails = Math.max(0, technicalDetails.length - batchTargetOffset - visibleTechnicalDetails.length);
7721
- const technicalCardRows = 3 + visibleTechnicalDetails.length;
7627
+ const technicalCardRows = 3 + Math.min(technicalDetails.length, technicalDetailLimit);
7722
7628
  const technicalChangeBudget = Math.max(1, Math.floor((size.rows - 8 - 3 - (review.equivalentCommand ? 1 : 0)
7723
7629
  - reviewActionViewport.limit - technicalCardRows) / 2));
7724
7630
  const prioritizedTechnicalChanges = [
@@ -7731,8 +7637,27 @@ export function ConsoleAppV4(props) {
7731
7637
  && change.group !== 'portableTarget'
7732
7638
  && (change.group === undefined || !technicalGroups.has(change.group))),
7733
7639
  ];
7734
- const visibleTechnicalChanges = prioritizedTechnicalChanges.slice(batchTargetOffset, batchTargetOffset + technicalChangeBudget);
7735
- const hiddenTechnicalChanges = Math.max(0, review.changes.length - batchTargetOffset - visibleTechnicalChanges.length);
7640
+ const technicalChangePageCount = Math.ceil(prioritizedTechnicalChanges.length / technicalChangeBudget);
7641
+ const technicalDetailPageCount = Math.ceil(technicalDetails.length / technicalDetailLimit);
7642
+ const reviewPageCount = showTechnical
7643
+ ? Math.max(1, technicalChangePageCount + technicalDetailPageCount)
7644
+ : portableReview || batchReview || userReviewChanges.length > staticRowBudget
7645
+ ? batchChangePage.pageCount : 1;
7646
+ const reviewCurrentPage = Math.min(reviewPageCount, batchTargetOffset + 1);
7647
+ const technicalChangePage = paginateWrappedRows({
7648
+ lines: prioritizedTechnicalChanges.map((_, index) => String(index)),
7649
+ rows: technicalChangeBudget, page: reviewCurrentPage,
7650
+ });
7651
+ const technicalDetailPage = paginateWrappedRows({
7652
+ lines: technicalDetails, rows: technicalDetailLimit,
7653
+ page: Math.max(1, reviewCurrentPage - technicalChangePageCount),
7654
+ });
7655
+ const visibleTechnicalDetails = reviewCurrentPage > technicalChangePageCount
7656
+ ? technicalDetailPage.lines : [];
7657
+ const hiddenTechnicalDetails = Math.max(0, technicalDetails.length - technicalDetailPage.offset - visibleTechnicalDetails.length);
7658
+ const visibleTechnicalChanges = reviewCurrentPage <= technicalChangePageCount
7659
+ ? prioritizedTechnicalChanges.slice(technicalChangePage.offset, technicalChangePage.offset + technicalChangeBudget) : [];
7660
+ const hiddenTechnicalChanges = Math.max(0, review.changes.length - technicalChangePage.offset - visibleTechnicalChanges.length);
7736
7661
  const technicalChanges = visibleTechnicalChanges.map((change, index) => {
7737
7662
  const category = change.group ? groupTitle[change.group] : undefined;
7738
7663
  return _jsx(ReviewChangeRow, { title: category
@@ -7764,11 +7689,15 @@ export function ConsoleAppV4(props) {
7764
7689
  const widenedDependencyCount = new Set(installDependencyTargets
7765
7690
  .filter((target) => target.affectsOtherProjects && target.action !== 'keep')
7766
7691
  .map((target) => target.pluginId)).size;
7767
- if (userReviewChanges.length > staticRowBudget || showTechnical
7768
- && (review.changes.length > technicalChangeBudget || technicalDetails.length > technicalDetailLimit)) {
7692
+ if (reviewPageCount > 1) {
7693
+ readerNavigation = {
7694
+ page: reviewCurrentPage,
7695
+ pageCount: reviewPageCount,
7696
+ move: (direction) => setBatchTargetOffset(Math.min(reviewPageCount, Math.max(1, reviewCurrentPage + direction)) - 1),
7697
+ };
7769
7698
  help = 'PgUp/PgDn 查看全部确认信息 · ↑↓ 移动 · Enter 选择 · Esc 返回';
7770
7699
  }
7771
- content = _jsx(Box, { flexDirection: "column", children: _jsxs(ReviewCard, { title: reviewRootSummary ? `本次插件 · ${reviewRootSummary}` : review.subject.name, meta: reviewRootSummary ? `版本 ${reviewVersionSummary} · 范围 ${reviewRangeSummary || '未知'}` : `${review.subject.sourceLabel ?? ''} ${review.subject.version ?? ''}`, description: showEquivalentCommand ? review.description : undefined, children: [showEquivalentCommand ? _jsxs(Text, { children: ["\u7B49\u4EF7\u547D\u4EE4\uFF1A", review.equivalentCommand] }) : null, portableReview ? _jsx(SemanticRows, { actions: actions, focus: operationConfirmation ? -1 : focus, kind: "action", compact: true, offset: reviewActionViewport.offset, limit: reviewActionViewport.limit }) : null, showTechnical && review.catalogBinding ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: `Catalog:${review.catalogBinding.sourceId} ${review.catalogBinding.catalogVersion} · 当前使用`, meta: `generation ${review.catalogBinding.generationDigestShort}`, status: "success", compact: true }), _jsx(StaticRow, { title: "\u64CD\u4F5C\u671F\u95F4\u4E0D\u68C0\u67E5\u8FDC\u7AEF Catalog", status: "success", compact: true })] }) : null, installDependencyPluginIds.size > 0 ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: `需要 ${installDependencyPluginIds.size} 个额外插件;${reusedDependencyPluginIds.size} 个复用、${newDependencyCount} 个新增`, status: "info", compact: true }), _jsx(StaticRow, { title: widenedDependencyCount > 0 ? `${widenedDependencyCount} 个依赖会扩大到所有项目` : '没有依赖会扩大到所有项目', status: widenedDependencyCount > 0 ? 'warning' : 'success', compact: true })] }) : null, review.requiresConfirmation ? _jsx(StaticRow, { title: review.operationKind === 'catalogStorePrune' ? '! 需要确认删除本次预览的可回收项目' : projectPreApplyConfirmation ? '! 需要确认当前项目同步脚本' : '! 需要确认会影响其他项目的内容', meta: review.sharedImpact.pluginIds.length > 0 ? `${review.sharedImpact.pluginIds.length} 个插件` : undefined, description: review.sharedImpact.warning, status: "warning", compact: true }) : null, showTechnical ? technicalChanges : changeGroups, portableReview ? null : _jsx(SemanticRows, { actions: actions, focus: operationConfirmation ? -1 : focus, kind: "action", compact: true, offset: reviewActionViewport.offset, limit: reviewActionViewport.limit }), 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: catalogReferenceDetailOffset >= 0 && batchTargetOffset + index >= catalogReferenceDetailOffset ? undefined : index === visibleTechnicalDetails.length - 1 && hiddenTechnicalDetails > 0 ? `另有 ${hiddenTechnicalDetails} 项` : undefined, compact: true }, `${batchTargetOffset + index}:${detail}`)) }) : null] }) });
7700
+ content = _jsx(Box, { flexDirection: "column", children: _jsxs(ReviewCard, { title: reviewRootSummary ? `本次插件 · ${reviewRootSummary}` : review.subject.name, meta: reviewRootSummary ? `版本 ${reviewVersionSummary} · 范围 ${reviewRangeSummary || '未知'}` : `${review.subject.sourceLabel ?? ''} ${review.subject.version ?? ''}`, description: showEquivalentCommand ? review.description : undefined, children: [showEquivalentCommand ? _jsxs(Text, { children: ["\u7B49\u4EF7\u547D\u4EE4\uFF1A", review.equivalentCommand] }) : null, portableReview ? _jsx(SemanticRows, { actions: actions, focus: operationConfirmation ? -1 : focus, kind: "action", compact: true, offset: reviewActionViewport.offset, limit: reviewActionViewport.limit }) : null, showTechnical && review.catalogBinding ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: `Catalog:${review.catalogBinding.sourceId} ${review.catalogBinding.catalogVersion} · 当前使用`, meta: `generation ${review.catalogBinding.generationDigestShort}`, status: "success", compact: true }), _jsx(StaticRow, { title: "\u64CD\u4F5C\u671F\u95F4\u4E0D\u68C0\u67E5\u8FDC\u7AEF Catalog", status: "success", compact: true })] }) : null, installDependencyPluginIds.size > 0 ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: `需要 ${installDependencyPluginIds.size} 个额外插件;${reusedDependencyPluginIds.size} 个复用、${newDependencyCount} 个新增`, status: "info", compact: true }), _jsx(StaticRow, { title: widenedDependencyCount > 0 ? `${widenedDependencyCount} 个依赖会扩大到所有项目` : '没有依赖会扩大到所有项目', status: widenedDependencyCount > 0 ? 'warning' : 'success', compact: true })] }) : null, review.requiresConfirmation ? _jsx(StaticRow, { title: review.operationKind === 'catalogStorePrune' ? '! 需要确认删除本次预览的可回收项目' : projectPreApplyConfirmation ? '! 需要确认当前项目同步脚本' : '! 需要确认会影响其他项目的内容', meta: review.sharedImpact.pluginIds.length > 0 ? `${review.sharedImpact.pluginIds.length} 个插件` : undefined, description: review.sharedImpact.warning, status: "warning", compact: true }) : null, showTechnical ? technicalChanges : changeGroups, showTechnical && visibleTechnicalDetails.length > 0 ? _jsx(TechnicalDetails, { children: visibleTechnicalDetails.map((detail, index) => _jsx(StaticRow, { title: detail, meta: catalogReferenceDetailOffset >= 0 && technicalDetailPage.offset + index >= catalogReferenceDetailOffset ? undefined : index === visibleTechnicalDetails.length - 1 && hiddenTechnicalDetails > 0 ? `另有 ${hiddenTechnicalDetails} 项` : undefined, compact: true }, `${technicalDetailPage.offset + index}:${detail}`)) }) : null, reviewPageCount > 1 ? _jsx(Pager, { page: reviewCurrentPage, pageCount: reviewPageCount, columns: size.columns - 6 }) : null, portableReview ? null : _jsx(SemanticRows, { actions: actions, focus: operationConfirmation ? -1 : focus, kind: "action", compact: true, offset: reviewActionViewport.offset, limit: reviewActionViewport.limit }), compressedReview || showTechnical ? null : _jsx(StaticRow, { title: "\u6280\u672F\u8BE6\u60C5\uFF08d\uFF09", status: "muted" })] }) });
7772
7701
  if (pendingReviewIntent?.kind === 'configSaveBatch' || sourceDraft && (pendingReviewIntent?.kind === 'sourceMutation' || pendingReviewIntent?.kind === 'catalogGeneration') && pendingReviewIntent.sourceId === sourceDraft.sourceId) {
7773
7702
  const changesText = pendingReviewIntent.kind === 'configSaveBatch' ? pendingReviewIntent.groups.map((group) => `${configLayerLabel({ scope: group.scope })}\n${(group.changes ?? []).flatMap((change) => [
7774
7703
  `${change.title} · ${change.before ?? '未设置'} → ${change.after}${change.children?.length ? ` · ${change.children.length} 个子属性变化` : ''}`,
@@ -7782,11 +7711,8 @@ export function ConsoleAppV4(props) {
7782
7711
  : `${changesText}\n\n${readingText}`;
7783
7712
  if (pendingReviewIntent.kind === 'configSaveBatch')
7784
7713
  description = pendingReviewIntent.apply ? '保存并生效;每层独立提交,应用结果单独报告。' : '只保存;每层独立提交,稍后应用。';
7785
- const lines = wrapConsoleText(readingText, size.columns - 2);
7786
- const pageRows = Math.max(1, pageActionBudget(actions.length + 1));
7787
- referenceDetailPagination = { total: lines.length, pageRows };
7788
- const offset = Math.min(batchTargetOffset, Math.max(0, lines.length - 1));
7789
- content = _jsxs(Box, { flexDirection: "column", children: [lines.slice(offset, offset + pageRows).map((line, index) => _jsx(Text, { children: line }, index)), _jsxs(Text, { color: tuiTheme.roles.secondary, children: [offset + 1, "\u2013", Math.min(lines.length, offset + pageRows), " / ", lines.length] }), _jsx(SemanticRows, { actions: actions, focus: operationConfirmation ? -1 : focus, kind: "action", compact: true })] });
7714
+ const pageRows = Math.max(1, pageActionBudget(actions.length + 1) - 1);
7715
+ content = _jsxs(Box, { flexDirection: "column", children: [readingPage(readingText, size.columns - 2, pageRows), _jsx(SemanticRows, { actions: actions, focus: operationConfirmation ? -1 : focus, kind: "action", compact: true })] });
7790
7716
  }
7791
7717
  }
7792
7718
  else if (route.name === 'progress') {
@@ -7942,7 +7868,7 @@ export function ConsoleAppV4(props) {
7942
7868
  return step === 2 ? '正在安装' : step === 3 ? '正在验证' : step === 1 ? '正在生成' : '正在检查';
7943
7869
  };
7944
7870
  const compactPortableProgress = policy.mode === 'compact' && showTechnical;
7945
- 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: `${progressAction}所选插件和所需插件`, 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: storeOperation ? '复核精确清理目标' : sourceOperation ? '检查来源操作输入' : configOperation ? '验证设置值' : '检查兼容性', state: !currentPhase ? 'pending' : preflightActive ? 'active' : 'complete' }), _jsx(ProgressRow, { title: progress.current?.phase || (storeOperation ? '安全删除已确认项目' : 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) => {
7871
+ 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('、') : '自动选择可用 Agent'}`, 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: `${progressAction}所选插件和所需插件`, 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: storeOperation ? '复核精确清理目标' : sourceOperation ? '检查来源操作输入' : configOperation ? '验证设置值' : '检查兼容性', state: !currentPhase ? 'pending' : preflightActive ? 'active' : 'complete' }), _jsx(ProgressRow, { title: progress.current?.phase || (storeOperation ? '安全删除已确认项目' : 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) => {
7946
7872
  const state = progressLabel(entry);
7947
7873
  const relation = entry.relation === 'requested' ? '你选择的插件' : entry.relation === 'direct' ? '直接依赖' : '间接依赖';
7948
7874
  const title = props.controller.getPluginOverview(entry.pluginId)?.displayName
@@ -8511,8 +8437,8 @@ export function ConsoleAppV4(props) {
8511
8437
  const resultPaginationTotal = result.operationKind === 'portableInstall'
8512
8438
  ? prioritizedPortablePairs.length
8513
8439
  : baseResultPaginationTotal;
8514
- const resultPageRows = result.operationKind === 'portableInstall' ? portableSkillLimit : 1;
8515
- const portableSkillOffset = Math.min(batchTargetOffset, Math.max(0, prioritizedPortablePairs.length - 1));
8440
+ const portableSkillPage = pagedItems(prioritizedPortablePairs.length, portableSkillLimit);
8441
+ const portableSkillOffset = portableSkillPage.offset;
8516
8442
  const visiblePortablePairs = prioritizedPortablePairs.slice(portableSkillOffset, portableSkillOffset + portableSkillLimit);
8517
8443
  const portableResultRange = prioritizedPortablePairs.length > 0
8518
8444
  ? `第 ${portableSkillOffset + 1}–${portableSkillOffset + visiblePortablePairs.length} 项,共 ${prioritizedPortablePairs.length} 项`
@@ -8523,25 +8449,6 @@ export function ConsoleAppV4(props) {
8523
8449
  const selectedPortablePair = portableResultDetailOpen
8524
8450
  ? prioritizedPortablePairs.find((pair) => portablePairKey(pair) === portableResultDetailPairKey.current)
8525
8451
  : visiblePortablePairs[0];
8526
- const wrapPortableField = (label, value) => {
8527
- const chunks = wrapConsoleText(value, Math.max(12, size.columns - stringWidth(label) - 14));
8528
- return chunks.map((line, index) => `${label} ${index + 1}/${chunks.length} · ${line}`);
8529
- };
8530
- const portableResultDetailLines = selectedPortablePair ? [
8531
- ...wrapPortableField('Skill', selectedPortablePair.skill),
8532
- ...wrapPortableField('Agent', selectedPortablePair.agent ?? '无法确认'),
8533
- `状态 · ${portablePairStateLabel(selectedPortablePair)}`,
8534
- ...(portableResultScope ? [`范围 · ${scopeLabel(portableResultScope)}`] : []),
8535
- ...(portableReceipt?.recovery
8536
- ? wrapPortableField('安全恢复', portableReceipt.recovery) : []),
8537
- ...(portableReceipt?.failureCode
8538
- ? wrapPortableField('failure code', portableReceipt.failureCode) : []),
8539
- ...(portableReceipt ? [`verification missing · ${portableReceipt.missing.length}`] : []),
8540
- ...(portableReceipt ? wrapPortableField('Skills CLI version', portableReceipt.skillsCliVersion) : []),
8541
- ...(portableReceipt
8542
- ? wrapPortableField('materialization digest', portableReceipt.materializationDigest) : []),
8543
- ...(resultSummary?.elapsedMs !== undefined ? [`elapsed · ${resultSummary.elapsedMs} ms`] : []),
8544
- ] : ['当前结果已变化。请返回结果摘要。'];
8545
8452
  readingText = portableResultDetailOpen ? selectedPortablePair ? [
8546
8453
  `Skill · ${selectedPortablePair.skill}`,
8547
8454
  `Agent · ${selectedPortablePair.agent ?? '无法确认'}`,
@@ -8565,8 +8472,8 @@ export function ConsoleAppV4(props) {
8565
8472
  ...(resultSummary?.batch?.rootResults ?? []).map((root) => `${root.pluginId} · ${root.state}`),
8566
8473
  ].join('\n');
8567
8474
  const portableResultDetailPageRows = responsiveBudget(5, 10);
8568
- const safePortableResultDetailOffset = Math.min(portableResultDetailOffset, Math.max(0, portableResultDetailLines.length - 1));
8569
- const visiblePortableResultDetailLines = portableResultDetailLines.slice(safePortableResultDetailOffset, safePortableResultDetailOffset + portableResultDetailPageRows);
8475
+ const portableDetailPage = readerPage({ text: readingText, columns: size.columns - 2,
8476
+ rows: portableResultDetailPageRows, page: portableResultDetailOffset + 1 });
8570
8477
  if (portableOperation && portableResultDetailOpen) {
8571
8478
  title = '当前结果完整字段';
8572
8479
  description = '按行显示当前 Skill、Agent、状态和技术证据。';
@@ -8579,10 +8486,6 @@ export function ConsoleAppV4(props) {
8579
8486
  setScrollOffset(0);
8580
8487
  } },
8581
8488
  ];
8582
- portableResultDetailPagination = {
8583
- total: portableResultDetailLines.length,
8584
- pageRows: portableResultDetailPageRows,
8585
- };
8586
8489
  }
8587
8490
  else if (portableOperation) {
8588
8491
  const detailAction = selectedPortablePair ? {
@@ -8602,38 +8505,29 @@ export function ConsoleAppV4(props) {
8602
8505
  ...(detailAction ? [detailAction] : []),
8603
8506
  ...actions.slice(1),
8604
8507
  ];
8605
- if (resultPaginationTotal > resultPageRows) {
8606
- referenceDetailPagination = { total: resultPaginationTotal, pageRows: resultPageRows };
8607
- }
8608
- }
8609
- else if (resultPaginationTotal > 1) {
8610
- referenceDetailPagination = { total: resultPaginationTotal, pageRows: resultPageRows };
8611
8508
  }
8612
8509
  const portableTechnicalFieldCount = portableReceipt
8613
8510
  ? 3 + (portableReceipt.failureCode ? 1 : 0)
8614
8511
  + (policy.mode === 'compact' ? 0 : resultSummary?.elapsedMs !== undefined ? 1 : 0)
8615
8512
  : 1;
8616
8513
  const portableFixedRows = (portableRecheckBusy ? 1 : 0) + (showTechnical
8617
- ? 1 + portableSkillLimit + 1
8618
- + (portableResultAgentMode === 'automatic' ? 1 : 0)
8514
+ ? 1 + portableSkillLimit + (portableResultScope && policy.mode !== 'compact' ? 1 : 0)
8515
+ + (portableResultAgentMode === 'automatic' && policy.mode !== 'compact' ? 1 : 0)
8619
8516
  + (portableReceipt?.recovery ? 1 : 0)
8620
- + 1 + portableTechnicalFieldCount
8517
+ + (policy.mode === 'compact' ? 0 : 1) + portableTechnicalFieldCount
8621
8518
  : 3 + portableSkillLimit + 1
8622
8519
  + (portableResultAgentMode === 'automatic' ? 1 : 0)
8623
8520
  + (portableReceipt?.recovery ? 1 : 0));
8624
8521
  currentActionViewportLineBudget = pageActionBudget(portableOperation
8625
8522
  ? portableResultDetailOpen
8626
- ? visiblePortableResultDetailLines.length
8627
- + (safePortableResultDetailOffset > 0 ? 1 : 0)
8628
- + (safePortableResultDetailOffset + visiblePortableResultDetailLines.length
8629
- < portableResultDetailLines.length ? 1 : 0)
8523
+ ? portableDetailPage.lines.length + 1
8630
8524
  : portableFixedRows
8631
8525
  : resultPaginationTotal > 1 ? 7 : 8);
8632
8526
  const resultActionViewport = actionViewport(actions, true, focus, scrollOffset, currentActionViewportLineBudget);
8633
8527
  const resultTargetLimit = Math.max(1, prefixRowLimit(prioritizedTargets.map(() => policy.mode === 'compact' ? 1 : 2), pageActionBudget() - 3 - resultActionViewport.limit - resultExtraRows));
8634
- const resultTargetOffset = Math.min(batchTargetOffset, Math.max(0, prioritizedTargets.length - 1));
8528
+ const resultTargetPage = pagedItems(prioritizedTargets.length, resultTargetLimit);
8529
+ const resultTargetOffset = resultTargetPage.offset;
8635
8530
  const visibleResultTargets = prioritizedTargets.slice(resultTargetOffset, resultTargetOffset + resultTargetLimit);
8636
- const hiddenResultTargets = Math.max(0, resultTargets.length - resultTargetOffset - visibleResultTargets.length);
8637
8531
  const portableTechnicalSummary = showTechnical;
8638
8532
  const portableRows = visiblePortablePairs.map((pair) => {
8639
8533
  const status = blocked || pair.state !== 'verified' ? 'warning' : 'success';
@@ -8752,7 +8646,15 @@ export function ConsoleAppV4(props) {
8752
8646
  const pluginResultRows = taskOperation ? taskSelectedPluginRows : batchPluginResultRows;
8753
8647
  const locationResultRows = taskOperation ? taskLocationRows : batchLocationResultRows;
8754
8648
  const combinedResultRows = [...pluginResultRows, ...locationResultRows];
8755
- const taskResultOffset = Math.min(batchTargetOffset, Math.max(0, combinedResultRows.length - 1));
8649
+ const actualResultRows = taskOperation || batchOperation ? combinedResultRows.length
8650
+ : portableOperation ? prioritizedPortablePairs.length : prioritizedTargets.length;
8651
+ const actualResultPageRows = taskOperation || batchOperation ? taskResultLimit
8652
+ : portableOperation ? portableSkillLimit : resultTargetLimit;
8653
+ if (!portableResultDetailOpen && actualResultRows > actualResultPageRows) {
8654
+ referenceDetailPagination = { total: actualResultRows, pageRows: actualResultPageRows };
8655
+ }
8656
+ const taskResultPage = pagedItems(combinedResultRows.length, taskResultLimit);
8657
+ const taskResultOffset = taskResultPage.offset;
8756
8658
  const visiblePluginResultRows = taskResultOffset < pluginResultRows.length
8757
8659
  ? pluginResultRows.slice(taskResultOffset, taskResultOffset + taskResultLimit) : [];
8758
8660
  const visibleLocationStart = Math.max(0, taskResultOffset - pluginResultRows.length);
@@ -8762,19 +8664,17 @@ export function ConsoleAppV4(props) {
8762
8664
  || prioritizedPortablePairs.length > portableSkillLimit) {
8763
8665
  help = '←→ 查看全部项目 · ↑↓ 移动 · Enter 选择 · Esc 返回';
8764
8666
  }
8765
- content = taskOperation || batchOperation ? _jsxs(Box, { flexDirection: "column", children: [taskResultOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${taskResultOffset} 项`, status: "muted", compact: true }) : null, visiblePluginResultRows.length > 0 ? _jsx(ResultCard, { status: resultStatus, title: "\u63D2\u4EF6\u7ED3\u679C", meta: `${pluginResultRows.length} 个插件`, children: visiblePluginResultRows }) : null, visibleLocationResultRows.length > 0 ? _jsx(ResultCard, { status: resultStatus, title: "\u4F7F\u7528\u4F4D\u7F6E\u4E0E\u6240\u9700\u63D2\u4EF6", meta: `${locationResultRows.length} 项`, children: visibleLocationResultRows }) : null, combinedResultRows.length > taskResultOffset + visiblePluginResultRows.length + visibleLocationResultRows.length
8766
- ? _jsx(StaticRow, { title: "\u2193 \u540E\u9762\u8FD8\u6709", meta: `${combinedResultRows.length - taskResultOffset - visiblePluginResultRows.length - visibleLocationResultRows.length} 项`, status: "muted", compact: true }) : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: resultActionViewport.offset, limit: resultActionViewport.limit })] }) : portableOperation ? _jsxs(Box, { flexDirection: "column", children: [portableRecheckBusy
8667
+ content = taskOperation || batchOperation ? _jsxs(Box, { flexDirection: "column", children: [visiblePluginResultRows.length > 0 ? _jsx(ResultCard, { status: resultStatus, title: "\u63D2\u4EF6\u7ED3\u679C", meta: `${pluginResultRows.length} 个插件`, children: visiblePluginResultRows }) : null, visibleLocationResultRows.length > 0 ? _jsx(ResultCard, { status: resultStatus, title: "\u4F7F\u7528\u4F4D\u7F6E\u4E0E\u6240\u9700\u63D2\u4EF6", meta: `${locationResultRows.length} 项`, children: visibleLocationResultRows }) : null, taskResultPage.pageCount > 1
8668
+ ? _jsx(Pager, { page: taskResultPage.page, pageCount: taskResultPage.pageCount, columns: size.columns - 2 }) : null, _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true, offset: resultActionViewport.offset, limit: resultActionViewport.limit })] }) : portableOperation ? _jsxs(Box, { flexDirection: "column", children: [portableRecheckBusy
8767
8669
  ? _jsx(ProgressRow, { title: "\u6B63\u5728\u91CD\u65B0\u68C0\u67E5\u7ED3\u679C", description: "\u53EA\u8BFB\u53D6 Skills CLI \u5F53\u524D\u72B6\u6001\uFF0C\u4E0D\u6267\u884C\u5B89\u88C5", state: "active", compact: policy.mode === 'compact' })
8768
- : null, portableResultDetailOpen ? _jsxs(_Fragment, { children: [safePortableResultDetailOffset > 0
8769
- ? _jsx(StaticRow, { title: `↑ 前面还有 ${safePortableResultDetailOffset} 行`, status: "muted", compact: true }) : null, visiblePortableResultDetailLines.map((line, index) => _jsx(StaticRow, { title: line, compact: true }, `${safePortableResultDetailOffset + index}:${line}`)), safePortableResultDetailOffset + visiblePortableResultDetailLines.length
8770
- < portableResultDetailLines.length
8771
- ? _jsx(StaticRow, { title: `↓ 后面还有 ${portableResultDetailLines.length
8772
- - safePortableResultDetailOffset - visiblePortableResultDetailLines.length} 行`, status: "muted", compact: true })
8773
- : null] }) : portableTechnicalSummary ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: blocked ? '安装未开始或结果未确认' : partial ? '部分 Skills 未确认' : '通用 Skills 安装结果', meta: portableResultRange, status: resultStatus, compact: true }), portableRows, portableResultScope ? _jsx(StaticRow, { title: "\u8303\u56F4", meta: scopeLabel(portableResultScope), compact: true }) : null, portableResultAgentMode === 'automatic'
8774
- ? _jsx(StaticRow, { title: "Agent", meta: "Skills CLI \u9ED8\u8BA4\u9009\u62E9", compact: true }) : null, portableReceipt?.recovery ? _jsx(StaticRow, { title: `安全恢复:${portableReceipt.recovery}`, status: "warning", compact: true }) : null, _jsx(StaticRow, { title: "\u6280\u672F\u8BE6\u60C5", status: "muted", compact: true }), portableTechnicalRows] }) : _jsx(_Fragment, { children: _jsxs(ResultCard, { status: resultStatus, title: blocked ? '安装未开始或结果未确认' : partial ? '部分 Skills 未确认' : '通用 Skills 安装结果', meta: portableResultRange, children: [portableRows, portableResultScope ? _jsx(StaticRow, { title: "\u8303\u56F4", meta: scopeLabel(portableResultScope), compact: true }) : null, portableResultAgentMode === 'automatic'
8775
- ? _jsx(StaticRow, { title: "Agent", meta: "Skills CLI \u9ED8\u8BA4\u9009\u62E9", compact: true }) : null, portableReceipt?.recovery
8670
+ : null, portableResultDetailOpen ? _jsx(_Fragment, { children: readingPage(readingText, size.columns - 2, portableResultDetailPageRows, 'portable') }) : portableTechnicalSummary ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: blocked ? '安装未开始或结果未确认' : partial ? '部分 Skills 未确认' : '通用 Skills 安装结果', meta: portableResultScope && policy.mode === 'compact'
8671
+ ? `${portableResultRange ?? ''} · ${scopeLabel(portableResultScope)}` : portableResultRange, status: resultStatus, compact: true }), portableRows, portableResultScope && policy.mode !== 'compact'
8672
+ ? _jsx(StaticRow, { title: "\u8303\u56F4", meta: scopeLabel(portableResultScope), compact: true }) : null, portableResultAgentMode === 'automatic' && policy.mode !== 'compact'
8673
+ ? _jsx(StaticRow, { title: "Agent", meta: "\u81EA\u52A8\u9009\u62E9\u53EF\u7528 Agent", compact: true }) : null, portableReceipt?.recovery ? _jsx(StaticRow, { title: `安全恢复:${portableReceipt.recovery}`, status: "warning", compact: true }) : null, policy.mode === 'compact' ? null : _jsx(StaticRow, { title: "\u6280\u672F\u8BE6\u60C5", status: "muted", compact: true }), portableTechnicalRows] }) : _jsx(_Fragment, { children: _jsxs(ResultCard, { status: resultStatus, title: blocked ? '安装未开始或结果未确认' : partial ? '部分 Skills 未确认' : '通用 Skills 安装结果', meta: portableResultRange, children: [portableRows, portableResultScope ? _jsx(StaticRow, { title: "\u8303\u56F4", meta: scopeLabel(portableResultScope), compact: true }) : null, portableResultAgentMode === 'automatic'
8674
+ ? _jsx(StaticRow, { title: "Agent", meta: "\u81EA\u52A8\u9009\u62E9\u53EF\u7528 Agent", compact: true }) : null, portableReceipt?.recovery
8776
8675
  ? _jsx(StaticRow, { title: `安全恢复:${portableReceipt.recovery}`, status: "warning", compact: true })
8777
- : 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 ?? '状态已变化,请重新确认', meta: resultTargets.length > 0 ? `${resultTargets.length} 个目标` : undefined, children: [resetDispositionText ? _jsx(StaticRow, { title: resetDispositionText, status: "success", compact: true }) : null, configGroupRows, 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: "\u5DF2\u4FDD\u5B58\u7684\u8BBE\u7F6E\u5C42\u4E0D\u4F1A\u91CD\u5199\u3002\u5931\u8D25\u7684\u4FDD\u5B58\u5C42\u4FDD\u7559\u8349\u7A3F\uFF1B\u5E94\u7528\u5931\u8D25\u65F6\u53EA\u91CD\u8BD5\u5E94\u7528\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 })] });
8676
+ : null] }) }), !portableResultDetailOpen && portableSkillPage.pageCount > 1
8677
+ ? _jsx(Pager, { page: portableSkillPage.page, pageCount: portableSkillPage.pageCount, columns: size.columns - 2 }) : 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 ?? '状态已变化,请重新确认', meta: resultTargets.length > 0 ? `${resultTargets.length} 个目标` : undefined, children: [resetDispositionText ? _jsx(StaticRow, { title: resetDispositionText, status: "success", compact: true }) : null, configGroupRows, 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}`); }), resultTargetPage.pageCount > 1 ? _jsx(Pager, { page: resultTargetPage.page, pageCount: resultTargetPage.pageCount, columns: size.columns - 6 }) : 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: "\u5DF2\u4FDD\u5B58\u7684\u8BBE\u7F6E\u5C42\u4E0D\u4F1A\u91CD\u5199\u3002\u5931\u8D25\u7684\u4FDD\u5B58\u5C42\u4FDD\u7559\u8349\u7A3F\uFF1B\u5E94\u7528\u5931\u8D25\u65F6\u53EA\u91CD\u8BD5\u5E94\u7528\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 })] });
8778
8678
  }
8779
8679
  else if (route.name === 'pluginContent' && plugin) {
8780
8680
  const overview = props.controller.getPluginContentOverview(plugin.pluginId);
@@ -8782,46 +8682,104 @@ export function ConsoleAppV4(props) {
8782
8682
  const contentAvailable = overview?.evidence === 'catalogBundle';
8783
8683
  breadcrumb = `${plugin.displayName} / 内容详情`;
8784
8684
  context = contentState === 'loading' ? '正在读取'
8785
- : contentAvailable ? `目录版本 ${overview.pluginVersion}` : '内容不可用';
8685
+ : contentAvailable ? undefined : '内容不可用';
8786
8686
  title = '内容详情';
8787
- description = contentState === 'loading'
8788
- ? '正在读取这个插件当前目录版本的内容。'
8789
- : contentAvailable
8790
- ? '这里显示已验证目录包声明的组成,不读取或展示完整操作指令。'
8791
- : '当前没有可安全读取的目录内容。';
8792
- const contentNavigationActions = [
8793
- { label: '查看技术详情', run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }) },
8794
- ];
8795
- const contentTypeLabels = {
8796
- skill: 'Skill', agent: 'Agent', role: '角色', instruction: '指令', permission: '权限',
8797
- hook: 'Hook', command: '命令', mcpServer: 'MCP Server', app: 'App',
8798
- };
8687
+ description = undefined;
8688
+ const reader = contentReader?.pluginId === plugin.pluginId ? contentReader : undefined;
8689
+ const activeType = snapshot.router.current.context.contentTypeTab ?? 'all';
8799
8690
  const installedVersions = [...new Set((plugin.update?.targets ?? [])
8800
8691
  .flatMap((target) => target.installedVersion ? [target.installedVersion] : []))];
8801
8692
  const versionDiffers = contentAvailable
8802
8693
  && installedVersions.some((version) => version !== overview.pluginVersion);
8803
- const items = overview?.items ?? [];
8694
+ const allItems = overview?.items ?? [];
8695
+ const visibleTypes = [...new Set(allItems.map((item) => item.type))];
8696
+ const typeTabs = ['all', ...visibleTypes];
8697
+ const selectedType = activeType === 'all' || visibleTypes.includes(activeType) ? activeType : 'all';
8698
+ const searchedItems = allItems.filter((item) => contentMatches(query, item));
8699
+ const items = searchedItems.filter((item) => selectedType === 'all' || item.type === selectedType);
8700
+ moveContentType = (direction) => {
8701
+ const next = typeTabs[(typeTabs.indexOf(selectedType) + direction + typeTabs.length) % typeTabs.length];
8702
+ const selectedId = items[focus]?.id;
8703
+ const nextItems = searchedItems.filter((item) => next === 'all' || item.type === next);
8704
+ const retained = nextItems.findIndex((item) => item.id === selectedId);
8705
+ props.controller.updatePageContext({ contentTypeTab: next });
8706
+ setFocus(retained >= 0 ? retained : 0);
8707
+ setScrollOffset(0);
8708
+ setBatchTargetOffset(0);
8709
+ };
8804
8710
  const itemActions = items.map((item) => ({
8805
8711
  actionId: `content:${item.id}`,
8806
8712
  label: item.name,
8807
- description: `${contentTypeLabels[item.type]} · ${item.tools.join('、') || '适用工具待确认'} · ${item.description ?? '目录没有提供简短描述'}`,
8808
- status: item.description ? undefined : 'warning',
8713
+ ...(item.issue ? { status: 'warning' } : {}),
8714
+ run: () => {
8715
+ if (!contentItemHasReader(item, size.columns - 2))
8716
+ return;
8717
+ setContentReader({ pluginId: plugin.pluginId, itemId: item.id,
8718
+ returnLineOffset: batchTargetOffset });
8719
+ setBatchTargetOffset(0);
8720
+ },
8809
8721
  }));
8810
- actions = [...itemActions, ...contentNavigationActions];
8811
- const fixedContentRows = contentAvailable ? 4 : 3;
8812
- currentActionViewportLineBudget = pageActionBudget(fixedContentRows + (overview?.notices.length ?? 0));
8813
- const contentViewport = actionViewport(actions, false, focus, scrollOffset, currentActionViewportLineBudget);
8814
- if (items.length > contentViewport.limit) {
8815
- context = `${items.length} 项内容 · 上下键滚动`;
8816
- help = '↑↓ 阅读全部内容 · Enter 选择 · Esc 返回';
8817
- }
8818
- content = _jsxs(Box, { flexDirection: "column", children: [contentAvailable ? _jsxs(_Fragment, { children: [_jsx(StaticRow, { title: "\u76EE\u5F55\u7248\u672C", meta: `${overview.pluginVersion} · Catalog ${overview.catalogVersion}`, compact: true }), _jsx(StaticRow, { title: installedVersions.length > 0
8819
- ? `安装版本 ${installedVersions.join('、')}`
8820
- : '安装版本尚未确认', description: versionDiffers
8821
- ? '以下清单属于目录版本,不代表旧安装版本的内容。'
8822
- : '以下清单来自当前已验证的目录包。', status: versionDiffers ? 'warning' : 'info', compact: true })] }) : contentState === 'loading'
8823
- ? _jsx(Callout, { status: "info", title: "\u6B63\u5728\u8BFB\u53D6\u5185\u5BB9", description: "\u5B8C\u6210\u540E\u5C06\u5728\u5F53\u524D\u9875\u9762\u663E\u793A\u3002" })
8824
- : _jsx(Callout, { status: "warning", title: "\u5185\u5BB9\u6682\u4E0D\u53EF\u7528", description: "\u53EF\u4EE5\u67E5\u770B\u6280\u672F\u8BE6\u60C5\u6216\u6062\u590D\u63D2\u4EF6\u6765\u6E90\u540E\u91CD\u8BD5\u3002" }), overview?.notices.map((notice, index) => _jsx(StaticRow, { title: notice, status: "warning", compact: true }, `${index}:${notice}`)), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: false, offset: contentViewport.offset, limit: contentViewport.limit })] });
8722
+ bottomActions = [
8723
+ ...(overview?.coverageIssue ? [{ actionId: 'content-coverage-issue',
8724
+ label: '查看清单读取问题', status: 'warning',
8725
+ run: () => {
8726
+ setContentReader({ pluginId: plugin.pluginId,
8727
+ returnLineOffset: batchTargetOffset });
8728
+ setBatchTargetOffset(0);
8729
+ } }] : []),
8730
+ { actionId: 'content-technical', label: '查看技术详情',
8731
+ run: () => go({ name: 'pluginTechnical', pluginId: plugin.pluginId }) },
8732
+ ];
8733
+ actions = [...itemActions, ...bottomActions];
8734
+ if (reader) {
8735
+ const item = allItems.find((candidate) => candidate.id === reader.itemId);
8736
+ readingText = item ? [
8737
+ `名称 ${item.name}`, `类型 ${contentTypeLabels[item.type]}`,
8738
+ `适用工具 ${item.tools.length ? item.tools.join('、') : '未确认'}`,
8739
+ ...(item.descriptionState !== 'issue' && item.description ? [`简介 ${item.description}`] : []),
8740
+ ...(item.issue ? [`读取问题 ${item.issue.code}`, item.issue.message] : []),
8741
+ ].join('\n') : overview?.coverageIssue
8742
+ ? [`清单覆盖 ${overview.coverage ?? '未确认'}`,
8743
+ `读取问题 ${overview.coverageIssue.code}`, overview.coverageIssue.message].join('\n') : '';
8744
+ title = item ? item.name : '清单读取问题';
8745
+ description = undefined;
8746
+ help = '←→ 或 PgUp/PgDn 阅读 · y 复制全文 · Esc 返回清单';
8747
+ content = readingPage(readingText, size.columns - 2, pageActionBudget(1));
8748
+ }
8749
+ else {
8750
+ const cardWidth = size.columns - 2;
8751
+ const lineLimit = 3;
8752
+ const layouts = items.map((item) => contentItemCardLayout(item, cardWidth));
8753
+ entityCardNavigation = { itemCount: items.length, factRows: layouts.map((card) => card.factRows), lineLimit };
8754
+ const coverageState = overview?.coverage ?? (contentAvailable ? 'complete' : 'unavailable');
8755
+ const showCoverage = contentAvailable && coverageState === 'partial';
8756
+ const showUnavailable = contentState !== 'loading' && coverageState === 'unavailable';
8757
+ const emptyHint = contentAvailable && coverageState !== 'unavailable' && items.length === 0
8758
+ && (query || selectedType !== 'all');
8759
+ const viewport = rowViewport({
8760
+ rowHeights: [...layouts.map((card, index) => contentItemCardWindowHeight(card, index === focus ? batchTargetOffset : 0, lineLimit)), ...bottomActions.map(() => 1)],
8761
+ focus, requestedOffset: scrollOffset,
8762
+ rowBudget: pageActionBudget(1 + Number(visibleTypes.length > 0) + 1
8763
+ + Number(showCoverage) + Number(showUnavailable && contentAvailable) + Number(emptyHint)),
8764
+ });
8765
+ help = `↑↓ 浏览内容 · ←→ 切换类型 · / 搜索 · a 下方操作${items[focus]
8766
+ && contentItemHasReader(items[focus], cardWidth) ? ' · Enter 阅读' : ''} · Esc 返回`;
8767
+ content = _jsxs(Box, { flexDirection: "column", children: [_jsx(QueryHeader, { width: cardWidth, tabs: visibleTypes.length > 0 ? { label: '类型', items: typeTabs.map((type) => ({ id: type,
8768
+ label: type === 'all' ? '全部' : contentTypeLabels[type],
8769
+ count: type === 'all' ? searchedItems.length
8770
+ : searchedItems.filter((item) => item.type === type).length })), currentId: selectedType } : undefined, search: { value: query, placeholder: '名称或可信简介', input: searchFocus
8771
+ ? _jsx(InputDisplay, { value: query, cursor: searchCursor, columns: querySearchWidth(size.columns - 2) }) : undefined } }), contentAvailable ? _jsxs(Box, { width: cardWidth, justifyContent: "space-between", height: 1, overflow: "hidden", children: [_jsxs(Text, { color: tuiTheme.colors.muted, children: ["\u5185\u5BB9\u7248\u672C ", _jsx(Text, { color: tuiTheme.colors.text, children: overview.pluginVersion })] }), _jsxs(Text, { color: versionDiffers ? tuiTheme.roles.change : tuiTheme.colors.muted, wrap: "truncate-end", children: ["\u5B89\u88C5 ", installedVersions.length ? installedVersions.join('、') : '未确认'] })] }) : contentState === 'loading'
8772
+ ? _jsx(StaticRow, { title: "\u6B63\u5728\u8BFB\u53D6\u5185\u5BB9", status: "muted", compact: true })
8773
+ : _jsx(StaticRow, { title: "\u5185\u5BB9\u6682\u4E0D\u53EF\u7528", status: "warning", compact: true }), showCoverage ? _jsx(StaticRow, { title: "\u6E05\u5355\u8986\u76D6\u4E0D\u5B8C\u6574\uFF1B\u6570\u91CF\u4EC5\u542B\u5DF2\u786E\u8BA4\u6761\u76EE", status: "warning", compact: true }) : null, showUnavailable && contentAvailable
8774
+ ? _jsx(StaticRow, { title: "\u6E05\u5355\u65E0\u6CD5\u786E\u8BA4", status: "warning", compact: true }) : null, emptyHint ? _jsx(Text, { color: tuiTheme.colors.muted, children: "\u5F53\u524D\u6761\u4EF6\u65E0\u5339\u914D\uFF1B\u67E5\u8BE2\u4FDD\u6301\u539F\u6837\u3002" }) : null, contentAvailable && allItems.length === 0 && coverageState === 'complete'
8775
+ ? _jsx(Text, { color: tuiTheme.colors.muted, children: "\u5F53\u524D\u76EE\u5F55\u6CA1\u6709\u5185\u5BB9\u6761\u76EE\u3002" }) : null, _jsx(ContentViewport, { children: actions.slice(viewport.offset, viewport.offset + viewport.limit)
8776
+ .map((action, visibleIndex) => {
8777
+ const index = viewport.offset + visibleIndex;
8778
+ if (index >= items.length)
8779
+ return _jsx(SemanticRows, { actions: actions, focus: searchFocus ? -1 : focus, kind: "action", compact: true, offset: index, limit: 1 }, action.actionId);
8780
+ return _jsx(ContentItemCard, { item: items[index], width: cardWidth, focused: !searchFocus && focus === index, lineOffset: focus === index ? batchTargetOffset : 0, lineLimit: lineLimit }, action.actionId);
8781
+ }) })] });
8782
+ }
8825
8783
  }
8826
8784
  else if (route.name === 'pluginTechnical' && plugin) {
8827
8785
  breadcrumb = `${plugin.displayName} / 技术详情`;
@@ -8831,7 +8789,9 @@ export function ConsoleAppV4(props) {
8831
8789
  ? '当前 AW CLI 不支持此插件所需 capability。'
8832
8790
  : '默认页面不会展示这些内部信息。';
8833
8791
  actions = [{ role: 'navigation', label: '返回插件', run: back }];
8834
- const prioritizedComponents = [...plugin.technicalComponents].sort((left, right) => {
8792
+ const prioritizedComponents = plugin.technicalComponents.filter((component) => !route.platform || component.platform === route.platform
8793
+ && (!route.scope || component.scope === route.scope || component.scope === undefined))
8794
+ .sort((left, right) => {
8835
8795
  const priority = (state) => state === 'error' ? 0
8836
8796
  : state === 'partial' ? 1 : state === 'absent' ? 2 : 3;
8837
8797
  return priority(left.state) - priority(right.state);
@@ -8839,6 +8799,8 @@ export function ConsoleAppV4(props) {
8839
8799
  const technicalFields = [
8840
8800
  `canonical pluginId · ${plugin.pluginId}`,
8841
8801
  `来源 · ${plugin.sourceId}`,
8802
+ ...(route.platform ? [`目标平台 · ${platformLabel(route.platform)}`] : []),
8803
+ ...(route.scope ? [`目标位置 · ${scopeLabel(route.scope)}`] : []),
8842
8804
  ...prioritizedComponents.map((component) => `组件 · ${component.state === 'absent' ? '缺失' : component.state}${component.errorCode ? ` · ${component.errorCode}` : ''} · ${component.key} · ${platformLabel(component.platform)} · ${component.shared ? '平台共享' : scopeLabel(component.scope)}`),
8843
8805
  ...(plugin.capabilityUnsupported ? [
8844
8806
  'failure code · awCapabilityUnsupported',
@@ -8846,19 +8808,23 @@ export function ConsoleAppV4(props) {
8846
8808
  `缺少 capability · ${plugin.missingCapabilities?.join('、') ?? 'Status 未提供具体 capability'}`,
8847
8809
  `恢复方向 · ${plugin.capabilityRecovery ?? '升级 AW CLI 到支持所需 capability 的版本后重试。'}`,
8848
8810
  ] : []),
8849
- ...plugin.installedPlacements.map((placement) => `${platformLabel(placement.platform)} target · ${placementStatus(placement).value}`),
8811
+ ...plugin.installedPlacements.filter((placement) => !route.platform || placement.platform === route.platform).flatMap((placement) => {
8812
+ if (placement.state === 'installed' && route.scope) {
8813
+ const scoped = placement.scopedPlacements?.find((entry) => entry.scope === route.scope);
8814
+ if (placement.scope !== route.scope && !scoped)
8815
+ return [];
8816
+ const target = scoped ? { ...placement, ...scoped, scopedPlacements: undefined } : placement;
8817
+ return [`${platformLabel(placement.platform)} · ${scopeLabel(route.scope)} target · ${placementStatus(target).value}`];
8818
+ }
8819
+ return [`${platformLabel(placement.platform)} target · ${placementStatus(placement).value}`];
8820
+ }),
8821
+ ...(route.platform && route.scope ? (props.controller.getPluginDetailSubject(plugin.pluginId)?.placements
8822
+ .filter((entry) => entry.platform === route.platform && entry.scope === route.scope)
8823
+ .map((entry) => `观察 · ${entry.observation === 'unknown' ? '未确认' : '已检查'} · 安装版本 ${entry.installedVersion ?? '未确认'}`)
8824
+ ?? []) : []),
8850
8825
  ];
8851
8826
  readingText = technicalFields.join('\n');
8852
- const technicalLines = technicalFields.flatMap((field) => wrapDisplayText(field, size.columns - 6));
8853
- const technicalPageRows = Math.max(1, pageActionBudget(7));
8854
- const technicalOffset = Math.min(batchTargetOffset, Math.max(0, technicalLines.length - 1));
8855
- const visibleTechnicalLines = technicalLines.slice(technicalOffset, technicalOffset + technicalPageRows);
8856
- if (technicalLines.length > technicalPageRows) {
8857
- referenceDetailPagination = { total: technicalLines.length, pageRows: technicalPageRows };
8858
- help = '←→ 或 PgUp/PgDn 阅读技术字段 · ↑↓ 移动 · Enter 选择 · Esc 返回';
8859
- }
8860
- content = _jsxs(Box, { flexDirection: "column", children: [_jsxs(TechnicalDetails, { children: [technicalOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${technicalOffset} 行`, status: "muted", compact: true }) : null, visibleTechnicalLines.map((line, index) => _jsx(StaticRow, { title: line, compact: true }, `${technicalOffset + index}:${line}`)), technicalOffset + visibleTechnicalLines.length < technicalLines.length
8861
- ? _jsx(StaticRow, { title: `↓ 后面还有 ${technicalLines.length - technicalOffset - visibleTechnicalLines.length} 行`, status: "muted", compact: true }) : null] }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] });
8827
+ content = _jsxs(Box, { flexDirection: "column", children: [readingPage(readingText, size.columns - 2, Math.max(1, pageActionBudget(4) - 1)), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] });
8862
8828
  }
8863
8829
  else {
8864
8830
  const missingPlugin = definition.entity === 'plugin' && plugin === undefined;
@@ -8941,16 +8907,54 @@ export function ConsoleAppV4(props) {
8941
8907
  }];
8942
8908
  content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: policy.mode === 'compact' });
8943
8909
  }
8910
+ if (snapshot.phase === 'ready' && !busy && !characterInput) {
8911
+ const hints = [];
8912
+ if (route.name === 'pluginList')
8913
+ hints.push('/ 搜索', 'f 筛选', '←→ 来源', 'm 多选');
8914
+ if (route.name === 'projectStatus' && !showTechnical)
8915
+ hints.push('/ 搜索', '←→ 分区', 'f 筛选');
8916
+ if (route.name === 'sourceList') {
8917
+ hints.push('/ 搜索');
8918
+ if (moveSourceType)
8919
+ hints.push('←→ 类型');
8920
+ }
8921
+ if (route.name === 'batchSelection' && batchDraft?.stage === 'select') {
8922
+ hints.push('/ 搜索', '←→ 来源', 'f 筛选');
8923
+ if (bottomActions.some((action) => action.actionId === 'batch-selection-continue' && !action.disabled))
8924
+ hints.push('c 继续');
8925
+ }
8926
+ if (route.name === 'portableSkillSelect' && !portableToolsOpen)
8927
+ hints.push('/ 搜索', 'c 继续');
8928
+ if (bottomActions.some((action) => action.actionId === 'project-recheck' && !action.disabled))
8929
+ hints.push('r 检查项目');
8930
+ if (bottomActions.some((action) => action.actionId === 'project-sync-actionable' && !action.disabled))
8931
+ hints.push('c 同步声明');
8932
+ if (bottomActions.some((action) => action.actionId === 'source-check-all' && !action.disabled))
8933
+ hints.push('r 检查全部');
8934
+ if (route.name === 'sourceDetail' || route.name === 'sourceMore')
8935
+ hints.push('r 检查来源');
8936
+ if (bottomActions.length > 0 && actions.length > bottomActions.length
8937
+ && bottomActions.some((action) => actions.includes(action)))
8938
+ hints.push('a 下方操作');
8939
+ if (entityCardNavigation && focus < entityCardNavigation.itemCount
8940
+ && (entityCardNavigation.factRows[focus] ?? 0) > entityCardNavigation.lineLimit)
8941
+ hints.push('PgUp/PgDn 翻信息');
8942
+ for (const hint of hints) {
8943
+ const key = hint.slice(0, hint.indexOf(' '));
8944
+ if (!help.split(' · ').some((part) => part.startsWith(`${key} `)))
8945
+ help += ` · ${hint}`;
8946
+ }
8947
+ }
8944
8948
  if (canOpenHelp && supportsTechnicalDetails) {
8945
- const technicalHelp = route.name === 'progress' ? 'd 技术进度' : 'd 技术详情';
8949
+ const technicalHelp = showTechnical ? 'd 返回摘要' : route.name === 'progress' ? 'd 技术进度' : 'd 技术详情';
8946
8950
  const remainingHelp = help.split(' · ')
8947
- .filter((item) => item !== '? 帮助' && item !== technicalHelp).join(' · ');
8951
+ .filter((item) => item !== '? 帮助' && !item.startsWith('d ')).join(' · ');
8948
8952
  help = `${technicalHelp} · ? 帮助 · ${remainingHelp}`;
8949
8953
  }
8950
8954
  else if (canOpenHelp && !help.includes('? 帮助')) {
8951
8955
  help = `? 帮助 · ${help}`;
8952
8956
  }
8953
- if (props.onSessionNavigation && !isCharacterInputMode(route, searchFocus)) {
8957
+ if (props.onSessionNavigation && !characterInput) {
8954
8958
  help = busy
8955
8959
  ? `${props.pendingSessionNavigation === 'reload' ? '已安排完成后重载'
8956
8960
  : props.pendingSessionNavigation === 'home' ? '已安排完成后回首页'
@@ -8958,12 +8962,10 @@ export function ConsoleAppV4(props) {
8958
8962
  : `h 首页 · H 重载 · ${help}`;
8959
8963
  }
8960
8964
  const helpParts = help.split(' · ');
8961
- const isRoutineKey = (part) => /^(↑|Enter|Space|Esc|Ctrl-C|←→)/u.test(part);
8965
+ const isRoutineKey = (part) => /^(↑|Enter|Space|Esc|Ctrl-C|←→|PgUp)/u.test(part);
8962
8966
  help = [...helpParts.filter(isRoutineKey), ...helpParts.filter((part) => !isRoutineKey(part))].join(' · ');
8963
8967
  if (searchFocus)
8964
8968
  help = '←→ 光标 · ↑↓ 移动结果 · Enter/Esc 完成输入';
8965
- else if (bottomActions.length > 0 && actions.length > 8)
8966
- help += ' · a 下方操作';
8967
8969
  useEffect(() => {
8968
8970
  if ((bottomActions.length > 0 || route.name === 'issueList' || route.name === 'operationHistory')
8969
8971
  && focus >= actions.length && actions.length > 0)
@@ -8971,10 +8973,31 @@ export function ConsoleAppV4(props) {
8971
8973
  else if (actions[focus]?.role === 'navigation')
8972
8974
  setFocus(moveFocus(actions, focus, 1));
8973
8975
  }, [routeKey, focus, actions.length, actions.map((action) => action.role ?? '').join(',')]);
8974
- const listCandidates = route.name === 'pluginList' || route.name === 'projectStatus'
8976
+ const listCandidates = route.name === 'pluginList'
8975
8977
  || route.name === 'batchSelection' && batchDraft?.stage === 'select'
8976
8978
  ? actions.filter((action) => action.pluginId !== undefined) : [];
8977
8979
  const listCandidateSignature = listCandidates.map((action) => action.pluginId).join('\0');
8980
+ const detailActionSignature = route.name === 'pluginDetail' || route.name === 'sourceDetail'
8981
+ ? actions.map((action) => action.actionId ?? action.label).join('\0') : undefined;
8982
+ useEffect(() => {
8983
+ if (detailActionSignature === undefined) {
8984
+ detailActionAnchor.current = undefined;
8985
+ return;
8986
+ }
8987
+ const previous = detailActionAnchor.current;
8988
+ if (previous?.routeKey === routeKey && previous.signature !== detailActionSignature
8989
+ && previous.actionId) {
8990
+ const index = actions.findIndex((action) => action.actionId === previous.actionId);
8991
+ if (index >= 0) {
8992
+ detailActionAnchor.current = { routeKey, signature: detailActionSignature,
8993
+ actionId: previous.actionId };
8994
+ setFocus(index);
8995
+ return;
8996
+ }
8997
+ }
8998
+ detailActionAnchor.current = { routeKey, signature: detailActionSignature,
8999
+ actionId: actions[focus]?.actionId };
9000
+ }, [routeKey, detailActionSignature, focus]);
8978
9001
  useEffect(() => {
8979
9002
  const pluginId = listCandidates[focus]?.pluginId;
8980
9003
  if (route.name === 'batchSelection' && batchDraft?.stage === 'select'
@@ -8986,7 +9009,7 @@ export function ConsoleAppV4(props) {
8986
9009
  }, [snapshot.router.currentFrameId, route.name, batchDraft?.stage,
8987
9010
  batchDraft?.showSelectionTools, listCandidateSignature, focus, scrollOffset]);
8988
9011
  useEffect(() => {
8989
- if (route.name !== 'pluginList' && route.name !== 'projectStatus' && !(route.name === 'batchSelection' && batchDraft?.stage === 'select')) {
9012
+ if (route.name !== 'pluginList' && !(route.name === 'batchSelection' && batchDraft?.stage === 'select')) {
8990
9013
  listFocusAnchor.current = undefined;
8991
9014
  return;
8992
9015
  }
@@ -9055,14 +9078,13 @@ export function ConsoleAppV4(props) {
9055
9078
  }
9056
9079
  if (effectiveFocus !== focus) {
9057
9080
  setFocus(effectiveFocus);
9058
- setScrollOffset(0);
9059
9081
  }
9060
9082
  dynamicMenuFocus.current = {
9061
9083
  routeKey,
9062
9084
  actionIds: dynamicActionSignature,
9063
9085
  focusedActionId: dynamicActionIds[effectiveFocus],
9064
9086
  };
9065
- }, [dynamicActionSignature, routeKey]);
9087
+ }, [dynamicActionSignature, routeKey, focus]);
9066
9088
  const runAction = (action) => {
9067
9089
  if (!action || action.disabled || action.role === 'navigation')
9068
9090
  return;
@@ -9177,6 +9199,12 @@ export function ConsoleAppV4(props) {
9177
9199
  }
9178
9200
  return;
9179
9201
  }
9202
+ if (overlay === 'help' && (key.leftArrow || key.rightArrow || key.pageUp || key.pageDown)) {
9203
+ const current = readerPage({ text: helpTextForPage(help, busy), columns: size.columns - 6,
9204
+ rows: helpBodyRows, page: helpPage });
9205
+ setHelpPage(Math.min(current.pageCount, Math.max(1, current.page + (key.leftArrow || key.pageUp ? -1 : 1))));
9206
+ return;
9207
+ }
9180
9208
  if (busy) {
9181
9209
  const sessionNavigation = resolveTuiSessionNavigationKey({ input, key }, { characterInput: false });
9182
9210
  if ((key.pageUp || key.pageDown || key.leftArrow || key.rightArrow)
@@ -9194,9 +9222,19 @@ export function ConsoleAppV4(props) {
9194
9222
  });
9195
9223
  }
9196
9224
  else if (overlay === 'help') {
9197
- if (key.escape || key.return || input.toLowerCase() === 'b' || input === '?') {
9225
+ if (key.escape || input.toLowerCase() === 'b' || input === '?') {
9198
9226
  setOverlay(undefined);
9199
9227
  }
9228
+ else if (key.upArrow || key.downArrow) {
9229
+ setOverlayFocus((value) => moveIndex(props.onSessionNavigation ? 3 : 1, value, key.upArrow ? -1 : 1));
9230
+ }
9231
+ else if (key.return) {
9232
+ setOverlay(undefined);
9233
+ if (props.onSessionNavigation && overlayFocus === 1)
9234
+ requestSessionNavigation('home');
9235
+ else if (props.onSessionNavigation && overlayFocus === 2)
9236
+ requestSessionNavigation('reload');
9237
+ }
9200
9238
  else if (input.toLowerCase() === 'q' || (key.ctrl && input.toLowerCase() === 'c')) {
9201
9239
  setExitAfterBusy(true);
9202
9240
  }
@@ -9216,6 +9254,7 @@ export function ConsoleAppV4(props) {
9216
9254
  }
9217
9255
  else if (canOpenHelp && input === '?') {
9218
9256
  setOverlayFocus(0);
9257
+ setHelpPage(1);
9219
9258
  setOverlay('help');
9220
9259
  }
9221
9260
  else if (!tooSmall && supportsTechnicalDetails && input.toLowerCase() === 'd') {
@@ -9326,8 +9365,7 @@ export function ConsoleAppV4(props) {
9326
9365
  const warningLimit = confirmationPagination.pageRows;
9327
9366
  setOperationConfirmation((current) => current ? {
9328
9367
  ...current,
9329
- warningOffset: Math.min(Math.max(0, warningCount - 1), Math.max(0, current.warningOffset
9330
- + (key.pageUp || key.leftArrow ? -warningLimit : warningLimit))),
9368
+ warningOffset: pagedItems(warningCount, warningLimit, current.warningOffset + (key.pageUp || key.leftArrow ? -warningLimit : warningLimit)).offset,
9331
9369
  } : current);
9332
9370
  }
9333
9371
  else if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) {
@@ -9516,12 +9554,113 @@ export function ConsoleAppV4(props) {
9516
9554
  });
9517
9555
  return;
9518
9556
  }
9519
- if ((key.pageUp || key.pageDown || key.leftArrow || key.rightArrow)
9520
- && route.name === 'result' && portableResultDetailOpen
9521
- && portableResultDetailPagination) {
9522
- const { total, pageRows } = portableResultDetailPagination;
9523
- const direction = key.pageUp || key.leftArrow ? -1 : 1;
9524
- setPortableResultDetailOffset((value) => Math.min(Math.max(0, total - 1), Math.max(0, value + direction * pageRows)));
9557
+ if (readerNavigation && (key.pageUp || key.pageDown || key.leftArrow || key.rightArrow)) {
9558
+ readerNavigation.move(key.pageUp || key.leftArrow ? -1 : 1);
9559
+ return;
9560
+ }
9561
+ if (configCardNavigation && (key.pageUp || key.pageDown)) {
9562
+ setConfigCardPage({ key: configCardNavigation.key,
9563
+ page: Math.min(configCardNavigation.pageCount, Math.max(1, configCardNavigation.page + (key.pageUp ? -1 : 1))) });
9564
+ return;
9565
+ }
9566
+ if (route.name === 'platform' && (key.upArrow || key.downArrow
9567
+ || input.toLowerCase() === 'j' || input.toLowerCase() === 'k')) {
9568
+ const next = moveFocus(actions, effectiveFocus, key.upArrow || input.toLowerCase() === 'k' ? -1 : 1);
9569
+ const cardWidth = size.columns - 2;
9570
+ const viewport = rowViewport({
9571
+ rowHeights: actions.map((action, index) => index < platformFacts.length
9572
+ ? entityPlacementRowHeight({ ...platformFacts[index],
9573
+ shared: platformFacts[index].sharedWithCurrentProject }, cardWidth) + 1
9574
+ : action.role === 'navigation' ? 0 : 1),
9575
+ focus: next, requestedOffset: scrollOffset, rowBudget: pageActionBudget(3),
9576
+ });
9577
+ dynamicMenuFocus.current = dynamicActionIds && dynamicActionSignature
9578
+ ? { routeKey, actionIds: dynamicActionSignature,
9579
+ focusedActionId: actions[next]?.actionId } : undefined;
9580
+ setFocus(next);
9581
+ setScrollOffset(viewport.offset);
9582
+ return;
9583
+ }
9584
+ if (graphLayout && graphProjection) {
9585
+ const portable = route.name === 'portableDependencies';
9586
+ const graphPluginId = route.name === 'portableDependencies'
9587
+ || route.name === 'pluginDependencies' ? route.pluginId : '';
9588
+ if (graphEvidenceOpen || graphPortableSelectionOpen) {
9589
+ if (key.escape || input.toLowerCase() === 'b') {
9590
+ setGraphEvidenceOpen(false);
9591
+ setGraphPortableSelectionOpen(false);
9592
+ return;
9593
+ }
9594
+ if (input !== 'h' && input !== 'H' && input !== '?'
9595
+ && input.toLowerCase() !== 'q' && !(key.ctrl && input.toLowerCase() === 'c'))
9596
+ return;
9597
+ }
9598
+ const row = graphLayout.rows[focus] ?? graphLayout.rows[graphLayout.focusIndex];
9599
+ if (input.toLowerCase() === 'r' && row?.node.edge) {
9600
+ setGraphEvidenceOpen(true);
9601
+ setBatchTargetOffset(0);
9602
+ return;
9603
+ }
9604
+ if (portable && input.toLowerCase() === 'p') {
9605
+ setGraphPortableSelectionOpen(true);
9606
+ setBatchTargetOffset(0);
9607
+ return;
9608
+ }
9609
+ if (key.upArrow || key.downArrow || input.toLowerCase() === 'j'
9610
+ || input.toLowerCase() === 'k') {
9611
+ const next = moveIndex(graphLayout.rows.length, focus, key.upArrow || input.toLowerCase() === 'k' ? -1 : 1);
9612
+ const nextLayout = projectDependencyGraphView(graphProjection, {
9613
+ columns: size.columns - 2, rowBudget: graphRowBudget,
9614
+ focusRowId: graphLayout.rows[next]?.id,
9615
+ scrollAnchorRowId: graphLayout.rows[scrollOffset]?.id,
9616
+ });
9617
+ setFocus(nextLayout.focusIndex);
9618
+ setScrollOffset(nextLayout.viewport.offset);
9619
+ persistUnifiedDependencyGraphContext(graphPluginId, nextLayout, expandedDependencyPluginIds ?? new Set([
9620
+ ...graphProjection.dependencies.defaultExpandedPluginIds,
9621
+ ...graphProjection.dependents.defaultExpandedPluginIds,
9622
+ ]), nextLayout.focusIndex, nextLayout.viewport.offset, portable);
9623
+ return;
9624
+ }
9625
+ if ((key.leftArrow || key.rightArrow) && row) {
9626
+ const nextExpanded = new Set(expandedDependencyPluginIds ?? [
9627
+ ...graphProjection.dependencies.defaultExpandedPluginIds,
9628
+ ...graphProjection.dependents.defaultExpandedPluginIds,
9629
+ ]);
9630
+ if (key.rightArrow && row.node.canExpand)
9631
+ nextExpanded.add(row.node.pluginId);
9632
+ if (key.leftArrow && row.node.canExpand)
9633
+ nextExpanded.delete(row.node.pluginId);
9634
+ const nextProjection = buildUnifiedDependencyGraphProjection({
9635
+ overviews: props.controller.listPluginDependencyOverviews(),
9636
+ rootPluginId: graphPluginId, expandedPluginIds: nextExpanded,
9637
+ });
9638
+ const nextLayout = projectDependencyGraphView(nextProjection, {
9639
+ columns: size.columns - 2, rowBudget: graphRowBudget,
9640
+ focusRowId: row.id,
9641
+ scrollAnchorRowId: graphLayout.rows[scrollOffset]?.id,
9642
+ });
9643
+ setExpandedDependencyPluginIds(nextExpanded);
9644
+ setFocus(nextLayout.focusIndex);
9645
+ setScrollOffset(nextLayout.viewport.offset);
9646
+ persistUnifiedDependencyGraphContext(graphPluginId, nextLayout, nextExpanded, nextLayout.focusIndex, nextLayout.viewport.offset, portable);
9647
+ return;
9648
+ }
9649
+ if (key.return) {
9650
+ runAction(actions[focus]);
9651
+ return;
9652
+ }
9653
+ }
9654
+ if (route.name === 'pluginContent' && contentReader
9655
+ && !key.escape && input !== 'h' && input !== 'H' && input !== '?'
9656
+ && input.toLowerCase() !== 'q' && !(key.ctrl && input.toLowerCase() === 'c'))
9657
+ return;
9658
+ if (entityCardNavigation && focus < entityCardNavigation.itemCount
9659
+ && (key.pageUp || key.pageDown)) {
9660
+ const maxOffset = Math.max(0, (entityCardNavigation.factRows[focus] ?? 0) - entityCardNavigation.lineLimit);
9661
+ const step = entityCardNavigation.lineLimit;
9662
+ setBatchTargetOffset((value) => Math.min(maxOffset, Math.max(0, value + (key.pageUp || key.leftArrow
9663
+ ? -step : step))));
9525
9664
  return;
9526
9665
  }
9527
9666
  if ((key.pageUp || key.pageDown || key.leftArrow || key.rightArrow)
@@ -9539,27 +9678,13 @@ export function ConsoleAppV4(props) {
9539
9678
  || route.name === 'configFields' && route.view === 'values'
9540
9679
  || route.name === 'sourceSave' && route.view === 'changes'
9541
9680
  || route.name === 'review'
9681
+ || route.name === 'pluginWriteTask'
9542
9682
  || route.name === 'result')
9543
9683
  && referenceDetailPagination) {
9544
9684
  const { total, pageRows } = referenceDetailPagination;
9545
9685
  moveBatchTargetPage(total, pageRows, key.pageUp || key.leftArrow ? -1 : 1);
9546
9686
  return;
9547
9687
  }
9548
- if ((key.pageUp || key.pageDown || key.leftArrow || key.rightArrow)
9549
- && route.name === 'pluginWriteTask') {
9550
- const total = props.controller.listPluginWriteTaskCandidates(route.goal)
9551
- .filter((candidate) => candidate.target.pluginId === route.pluginId
9552
- && (!route.targetKeys || route.targetKeys.includes(pluginWriteTaskTargetKey(candidate)))).length;
9553
- setBatchTargetOffset((value) => Math.min(Math.max(0, total - 1), Math.max(0, value + (key.pageUp || key.leftArrow ? -1 : 1))));
9554
- return;
9555
- }
9556
- if ((key.pageUp || key.pageDown || key.leftArrow || key.rightArrow)
9557
- && route.name === 'sourceTechnical') {
9558
- const total = diagnosticDetailLines(resolveSourceTechnicalDiagnostic(route.sourceId), size.columns - 6).length;
9559
- setBatchTargetOffset((value) => Math.min(Math.max(0, total - sourceTechnicalPageRows), Math.max(0, value + (key.pageUp || key.leftArrow
9560
- ? -sourceTechnicalPageRows : sourceTechnicalPageRows))));
9561
- return;
9562
- }
9563
9688
  if ((key.pageUp || key.pageDown) && route.name === 'sourceRemoveBlocked'
9564
9689
  && sourceDependenciesExpanded) {
9565
9690
  const total = sourceReferenceSummary?.dependencies.length ?? 0;
@@ -9579,19 +9704,6 @@ export function ConsoleAppV4(props) {
9579
9704
  setBatchTargetOffset((value) => Math.min(Math.max(0, total - pageRows), Math.max(0, value + (key.pageUp ? -pageRows : pageRows))));
9580
9705
  return;
9581
9706
  }
9582
- if ((route.name === 'operationDiagnostic' || route.name === 'operationReportPreview')
9583
- && (key.pageUp || key.pageDown || key.leftArrow || key.rightArrow)) {
9584
- const detail = route.name === 'operationDiagnostic'
9585
- ? props.controller.getOperationDiagnostic(route.recordId) : undefined;
9586
- const preview = route.name === 'operationReportPreview'
9587
- ? props.controller.previewOperationReport(route.recordId) : undefined;
9588
- const total = detail ? operationDiagnosticLineCount(detail, size.columns)
9589
- : preview ? operationReportLineCount(preview, size.columns) : 0;
9590
- const pageRows = route.name === 'operationReportPreview'
9591
- ? operationReportPageRows : operationDetailPageRows;
9592
- setScrollOffset((value) => Math.min(Math.max(0, total - pageRows), Math.max(0, value + (key.pageUp || key.leftArrow ? -pageRows : pageRows))));
9593
- return;
9594
- }
9595
9707
  if (route.name === 'portableAgentInput') {
9596
9708
  if (key.escape) {
9597
9709
  back();
@@ -9869,7 +9981,10 @@ export function ConsoleAppV4(props) {
9869
9981
  setInputError(undefined);
9870
9982
  return;
9871
9983
  }
9872
- if (searchFocus && (route.name === 'pluginList' || route.name === 'projectStatus' || route.name === 'batchSelection')) {
9984
+ if (searchFocus && (route.name === 'pluginList' || route.name === 'projectStatus'
9985
+ || route.name === 'pluginContent'
9986
+ || route.name === 'batchSelection' || route.name === 'sourceList'
9987
+ || route.name === 'portableSkillSelect')) {
9873
9988
  const currentQuery = route.name !== 'batchSelection' ? query : batchDraft?.query ?? '';
9874
9989
  const parts = [...new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(currentQuery)]
9875
9990
  .map((part) => part.segment);
@@ -9881,7 +9996,10 @@ export function ConsoleAppV4(props) {
9881
9996
  if (key.upArrow || key.downArrow) {
9882
9997
  setSearchFocus(false);
9883
9998
  const candidateCount = route.name === 'batchSelection' && batchDraft
9884
- ? batchCandidates(batchDraft).length : route.name === 'projectStatus' ? actions.filter((action) => action.pluginId).length : pluginRows.length;
9999
+ ? batchCandidates(batchDraft).length
10000
+ : route.name === 'projectStatus' || route.name === 'pluginContent'
10001
+ || route.name === 'portableSkillSelect'
10002
+ || route.name === 'sourceList' ? actions.length - bottomActions.length : pluginRows.length;
9885
10003
  setFocus((value) => moveIndex(candidateCount, Math.min(value, Math.max(0, candidateCount - 1)), key.upArrow ? -1 : 1));
9886
10004
  return;
9887
10005
  }
@@ -9906,15 +10024,56 @@ export function ConsoleAppV4(props) {
9906
10024
  }
9907
10025
  const nextQuery = parts.join('');
9908
10026
  setSearchCursor(nextCursor);
10027
+ const focusedId = actions[focus]?.actionId;
10028
+ let retained = -1;
10029
+ if (focusedId) {
10030
+ if (route.name === 'pluginList') {
10031
+ const ids = props.controller.listPlugins({ mode: route.mode,
10032
+ ...(route.sourceId ? { sourceId: route.sourceId } : {}),
10033
+ ...(nextQuery ? { query: nextQuery } : {}), installFilter }).map((item) => `plugin:${item.pluginId}`);
10034
+ retained = ids.indexOf(focusedId);
10035
+ }
10036
+ else if (route.name === 'projectStatus') {
10037
+ const section = snapshot.router.current.context.projectSection ?? 'declared';
10038
+ const filter = snapshot.router.current.context.projectStatusFilter ?? 'all';
10039
+ retained = props.controller.listProjectInventoryRoots(section).filter((root) => queryMatches(nextQuery, root.displayName, root.pluginId, root.sourceId)
10040
+ && (filter === 'all' || root.state === filter))
10041
+ .findIndex((root) => `project-root:${root.pluginId}` === focusedId);
10042
+ }
10043
+ else if (route.name === 'pluginContent') {
10044
+ const type = snapshot.router.current.context.contentTypeTab ?? 'all';
10045
+ retained = (props.controller.getPluginContentOverview(route.pluginId)?.items ?? [])
10046
+ .filter((item) => contentMatches(nextQuery, item)
10047
+ && (type === 'all' || item.type === type))
10048
+ .findIndex((item) => `content:${item.id}` === focusedId);
10049
+ }
10050
+ else if (route.name === 'sourceList') {
10051
+ retained = matchingSources(props.controller, nextQuery, snapshot.router.current.context.sourceTypeTab ?? 'all')
10052
+ .findIndex((source) => `source:${source.id}` === focusedId);
10053
+ }
10054
+ else if (route.name === 'portableSkillSelect') {
10055
+ retained = (snapshot.portableDraft?.candidates ?? []).filter((candidate) => (portableSelectionView === 'all' || candidate.selected)
10056
+ && queryMatches(nextQuery, candidate.name, candidate.shortDescription ?? candidate.description.split(/[。!?\n]/u)[0]))
10057
+ .findIndex((candidate) => `skill:${candidate.providerPluginId}:${candidate.name}` === focusedId);
10058
+ }
10059
+ else if (batchDraft) {
10060
+ retained = batchCandidates({ ...batchDraft, query: nextQuery, showSelected: false })
10061
+ .findIndex((candidate) => candidate.pluginId === actions[focus]?.pluginId);
10062
+ }
10063
+ }
9909
10064
  if (route.name !== 'batchSelection') {
9910
10065
  setQuery(nextQuery);
9911
10066
  props.controller.updatePageContext({ query: nextQuery });
10067
+ if (route.name === 'projectStatus') {
10068
+ dynamicMenuFocus.current = undefined;
10069
+ actionJumpAnchor.current = undefined;
10070
+ }
9912
10071
  }
9913
10072
  else {
9914
10073
  setBatchDraft((draft) => draft ? { ...draft, query: nextQuery, showSelected: false } : draft);
9915
10074
  }
9916
- setFocus(0);
9917
- setScrollOffset(0);
10075
+ setFocus(retained >= 0 ? retained : 0);
10076
+ setScrollOffset(retained >= 0 ? Math.min(scrollOffset, retained) : 0);
9918
10077
  return;
9919
10078
  }
9920
10079
  // Paste and composed text are data, never multiple background commands.
@@ -9922,8 +10081,11 @@ export function ConsoleAppV4(props) {
9922
10081
  return;
9923
10082
  if (key.tab)
9924
10083
  return;
9925
- if (route.name === 'projectStatus' && input === '/' && !showTechnical) {
9926
- setSearchCursor([...query].length);
10084
+ if ((route.name === 'projectStatus' && !showTechnical
10085
+ || route.name === 'pluginContent' && !contentReader
10086
+ || route.name === 'sourceList' || route.name === 'portableSkillSelect' && !portableToolsOpen) && input === '/') {
10087
+ setSearchCursor([...new Intl.Segmenter(undefined, { granularity: 'grapheme' })
10088
+ .segment(query)].length);
9927
10089
  setSearchFocus(true);
9928
10090
  return;
9929
10091
  }
@@ -9935,6 +10097,12 @@ export function ConsoleAppV4(props) {
9935
10097
  runAction(bottomActions.find((action) => action.actionId === 'plugin-list-multi'));
9936
10098
  return;
9937
10099
  }
10100
+ if (input.toLowerCase() === 'a' && route.name === 'sourceDetail'
10101
+ && entityCardNavigation) {
10102
+ setFocus(focus === 0 ? Math.min(1, actions.length - 1) : 0);
10103
+ setScrollOffset(0);
10104
+ return;
10105
+ }
9938
10106
  if (input.toLowerCase() === 'a' && bottomActions.length > 0) {
9939
10107
  const firstAction = actions.findIndex((action) => bottomActions.includes(action));
9940
10108
  if (firstAction >= 0) {
@@ -9942,11 +10110,20 @@ export function ConsoleAppV4(props) {
9942
10110
  if (anchor?.routeKey === routeKey && focus >= firstAction) {
9943
10111
  const entityIndex = anchor.entityId
9944
10112
  ? actions.findIndex((action) => action.pluginId === anchor.entityId) : -1;
9945
- setFocus(entityIndex >= 0 ? entityIndex : Math.min(anchor.index, Math.max(0, firstAction - 1)));
10113
+ const next = entityIndex >= 0 ? entityIndex : Math.min(anchor.index, Math.max(0, firstAction - 1));
10114
+ if (route.name === 'projectStatus' && dynamicActionIds && dynamicActionSignature) {
10115
+ dynamicMenuFocus.current = { routeKey, actionIds: dynamicActionSignature,
10116
+ focusedActionId: dynamicActionIds[next] };
10117
+ }
10118
+ setFocus(next);
9946
10119
  actionJumpAnchor.current = undefined;
9947
10120
  }
9948
10121
  else {
9949
10122
  actionJumpAnchor.current = { routeKey, index: focus, entityId: actions[focus]?.pluginId };
10123
+ if (route.name === 'projectStatus' && dynamicActionIds && dynamicActionSignature) {
10124
+ dynamicMenuFocus.current = { routeKey, actionIds: dynamicActionSignature,
10125
+ focusedActionId: dynamicActionIds[firstAction] };
10126
+ }
9950
10127
  setFocus(firstAction);
9951
10128
  }
9952
10129
  setScrollOffset(0);
@@ -9959,37 +10136,7 @@ export function ConsoleAppV4(props) {
9959
10136
  || route.name === 'configObjectPropertyEditor')) {
9960
10137
  return;
9961
10138
  }
9962
- if (route.name === 'pluginDependencies' && route.view !== 'removeBlocked'
9963
- && (key.leftArrow || key.rightArrow)) {
9964
- const unified = buildUnifiedDependencyGraphProjection({
9965
- overviews: props.controller.listPluginDependencyOverviews(),
9966
- rootPluginId: route.pluginId,
9967
- ...(expandedDependencyPluginIds ? { expandedPluginIds: expandedDependencyPluginIds } : {}),
9968
- });
9969
- const row = unified.rows[focus];
9970
- if (!row)
9971
- return;
9972
- const next = new Set(expandedDependencyPluginIds ?? [
9973
- ...unified.dependencies.defaultExpandedPluginIds,
9974
- ...unified.dependents.defaultExpandedPluginIds,
9975
- ]);
9976
- if (key.rightArrow && row.canExpand)
9977
- next.add(row.pluginId);
9978
- if (key.leftArrow && row.canExpand)
9979
- next.delete(row.pluginId);
9980
- const nextUnified = buildUnifiedDependencyGraphProjection({
9981
- overviews: props.controller.listPluginDependencyOverviews(),
9982
- rootPluginId: route.pluginId,
9983
- expandedPluginIds: next,
9984
- });
9985
- const nextFocus = Math.max(0, nextUnified.rows.findIndex((candidate) => candidate.id === row.id && candidate.branch === row.branch));
9986
- const nextScroll = Math.min(scrollOffset, nextFocus);
9987
- setExpandedDependencyPluginIds(next);
9988
- setFocus(nextFocus);
9989
- setScrollOffset(nextScroll);
9990
- return;
9991
- }
9992
- const sessionNavigation = resolveTuiSessionNavigationKey({ input, key }, { characterInput: isCharacterInputMode(route, searchFocus) });
10139
+ const sessionNavigation = resolveTuiSessionNavigationKey({ input, key }, { characterInput });
9993
10140
  const globalAction = resolveTuiGlobalKeyAction({ input, key }, { mode: 'menu', allowBack: true });
9994
10141
  if (sessionNavigation) {
9995
10142
  requestSessionNavigation(sessionNavigation);
@@ -10006,6 +10153,7 @@ export function ConsoleAppV4(props) {
10006
10153
  }
10007
10154
  else if (canOpenHelp && globalAction === 'help') {
10008
10155
  setOverlayFocus(0);
10156
+ setHelpPage(1);
10009
10157
  setOverlay('help');
10010
10158
  }
10011
10159
  else if (globalAction === 'exit') {
@@ -10014,6 +10162,10 @@ export function ConsoleAppV4(props) {
10014
10162
  else if (input.toLowerCase() === 'd' && supportsTechnicalDetails) {
10015
10163
  toggleTechnical();
10016
10164
  }
10165
+ else if (globalAction === 'back' && route.name === 'pluginContent' && contentReader) {
10166
+ setContentReader(undefined);
10167
+ setBatchTargetOffset(contentReader.returnLineOffset);
10168
+ }
10017
10169
  else if (globalAction === 'back' && route.name === 'result' && portableResultDetailOpen) {
10018
10170
  setPortableResultDetailOpen(false);
10019
10171
  setPortableResultDetailOffset(0);
@@ -10114,9 +10266,7 @@ export function ConsoleAppV4(props) {
10114
10266
  setScrollOffset(0);
10115
10267
  }
10116
10268
  else if (route.name === 'projectStatus' && input.toLowerCase() === 'f' && !showTechnical) {
10117
- props.controller.updatePageContext({ projectSection: snapshot.router.current.context.projectSection === 'extraInstalled' ? 'declared' : 'extraInstalled' });
10118
- setFocus(0);
10119
- setScrollOffset(0);
10269
+ toggleProjectStatusFilter?.();
10120
10270
  }
10121
10271
  else if (route.name === 'projectStatus' && input.toLowerCase() === 'r') {
10122
10272
  runAction(bottomActions.find((action) => action.actionId === 'project-recheck'));
@@ -10193,7 +10343,7 @@ export function ConsoleAppV4(props) {
10193
10343
  removeDraft(field.key);
10194
10344
  }
10195
10345
  }
10196
- else if (route.name === 'configFields' && visibleConfigTabs.length > 1
10346
+ else if (route.name === 'configFields' && !route.view && visibleConfigTabs.length > 1
10197
10347
  && (key.leftArrow || key.rightArrow)) {
10198
10348
  const current = Math.max(0, visibleConfigTabs.findIndex((tab) => tab.id === activeConfigTabId));
10199
10349
  const direction = key.leftArrow ? -1 : 1;
@@ -10202,6 +10352,17 @@ export function ConsoleAppV4(props) {
10202
10352
  setFocus(0);
10203
10353
  setScrollOffset(0);
10204
10354
  }
10355
+ else if (route.name === 'projectStatus' && !showTechnical
10356
+ && (key.leftArrow || key.rightArrow)) {
10357
+ moveProjectSection?.(key.leftArrow ? -1 : 1);
10358
+ }
10359
+ else if (route.name === 'pluginContent' && !contentReader
10360
+ && (key.leftArrow || key.rightArrow)) {
10361
+ moveContentType?.(key.leftArrow ? -1 : 1);
10362
+ }
10363
+ else if (route.name === 'sourceList' && (key.leftArrow || key.rightArrow)) {
10364
+ moveSourceType?.(key.leftArrow ? -1 : 1);
10365
+ }
10205
10366
  else if ((input.toLowerCase() === 'r' || input.toLowerCase() === 'c')
10206
10367
  && (route.name === 'sourceDetail' || route.name === 'sourceMore')) {
10207
10368
  void props.controller.recheckCatalogSource(route.sourceId);
@@ -10223,7 +10384,18 @@ export function ConsoleAppV4(props) {
10223
10384
  if (candidate)
10224
10385
  props.controller.togglePortableSkill(candidate.name);
10225
10386
  }
10387
+ else if (entityCardNavigation && focus < entityCardNavigation.itemCount
10388
+ && (key.upArrow || input.toLowerCase() === 'k') && batchTargetOffset > 0) {
10389
+ setBatchTargetOffset((value) => value - 1);
10390
+ }
10391
+ else if (entityCardNavigation && focus < entityCardNavigation.itemCount
10392
+ && (key.downArrow || input.toLowerCase() === 'j')
10393
+ && batchTargetOffset < Math.max(0, (entityCardNavigation.factRows[focus] ?? 0) - entityCardNavigation.lineLimit)) {
10394
+ setBatchTargetOffset((value) => value + 1);
10395
+ }
10226
10396
  else if (key.upArrow || input.toLowerCase() === 'k') {
10397
+ if (entityCardNavigation)
10398
+ setBatchTargetOffset(0);
10227
10399
  if (route.name === 'pluginDependencyNode' && route.view === 'references' && !showTechnical) {
10228
10400
  setBatchTargetOffset(0);
10229
10401
  }
@@ -10240,23 +10412,14 @@ export function ConsoleAppV4(props) {
10240
10412
  };
10241
10413
  }
10242
10414
  const viewport = actionViewport(actions, policy.mode === 'compact', next, scrollOffset, currentActionViewportLineBudget);
10243
- if (route.name === 'pluginDependencies' && route.view !== 'removeBlocked') {
10244
- const projection = buildUnifiedDependencyGraphProjection({
10245
- overviews: props.controller.listPluginDependencyOverviews(),
10246
- rootPluginId: route.pluginId,
10247
- ...(expandedDependencyPluginIds ? { expandedPluginIds: expandedDependencyPluginIds } : {}),
10248
- });
10249
- persistUnifiedDependencyGraphContext(route.pluginId, projection, expandedDependencyPluginIds ?? new Set([
10250
- ...projection.dependencies.defaultExpandedPluginIds,
10251
- ...projection.dependents.defaultExpandedPluginIds,
10252
- ]), next, viewport.offset);
10253
- }
10254
10415
  if (bottomActions.length === 0)
10255
10416
  setScrollOffset(viewport.offset);
10256
10417
  return next;
10257
10418
  });
10258
10419
  }
10259
10420
  else if (key.downArrow || input.toLowerCase() === 'j') {
10421
+ if (entityCardNavigation)
10422
+ setBatchTargetOffset(0);
10260
10423
  if (route.name === 'pluginDependencyNode' && route.view === 'references' && !showTechnical) {
10261
10424
  setBatchTargetOffset(0);
10262
10425
  }
@@ -10273,17 +10436,6 @@ export function ConsoleAppV4(props) {
10273
10436
  };
10274
10437
  }
10275
10438
  const viewport = actionViewport(actions, policy.mode === 'compact', next, scrollOffset, currentActionViewportLineBudget);
10276
- if (route.name === 'pluginDependencies' && route.view !== 'removeBlocked') {
10277
- const projection = buildUnifiedDependencyGraphProjection({
10278
- overviews: props.controller.listPluginDependencyOverviews(),
10279
- rootPluginId: route.pluginId,
10280
- ...(expandedDependencyPluginIds ? { expandedPluginIds: expandedDependencyPluginIds } : {}),
10281
- });
10282
- persistUnifiedDependencyGraphContext(route.pluginId, projection, expandedDependencyPluginIds ?? new Set([
10283
- ...projection.dependencies.defaultExpandedPluginIds,
10284
- ...projection.dependents.defaultExpandedPluginIds,
10285
- ]), next, viewport.offset);
10286
- }
10287
10439
  if (bottomActions.length === 0)
10288
10440
  setScrollOffset(viewport.offset);
10289
10441
  return next;
@@ -10297,21 +10449,26 @@ export function ConsoleAppV4(props) {
10297
10449
  return (_jsx(ScreenFrame, { title: "\u7EC8\u7AEF\u7A7A\u95F4\u4E0D\u8DB3", breadcrumb: "\u7EC8\u7AEF\u7A7A\u95F4\u4E0D\u8DB3", context: `${size.columns} × ${size.rows}`, help: "q \u9000\u51FA", size: size, children: _jsx(Callout, { status: "warning", title: "\u2194 \u8BF7\u5C06\u7EC8\u7AEF\u8C03\u6574\u5230\u81F3\u5C11 60 \u00D7 18", description: "\u8C03\u6574\u540E\u4F1A\u8FD4\u56DE\u539F\u9875\u9762\uFF0C\u4E0D\u4F1A\u4E22\u5931\u5F53\u524D\u9009\u62E9\u3002" }) }));
10298
10450
  }
10299
10451
  if (readingText !== undefined && route.name !== 'progress') {
10300
- const paginated = referenceDetailPagination !== undefined
10301
- || portableResultDetailPagination !== undefined
10452
+ const paginated = readerNavigation !== undefined || referenceDetailPagination !== undefined
10302
10453
  || route.name === 'sourceTechnical'
10303
10454
  || route.name === 'operationDiagnostic'
10304
10455
  || route.name === 'operationReportPreview'
10305
10456
  || route.name === 'portableDependencies';
10306
- help = `${paginated ? '←→ 翻页 · ' : ''}[y] 复制全文 · Esc 返回${supportsTechnicalDetails
10307
- ? ' · d 技术详情' : ''} · ${size.columns < 80
10308
- ? '↑↓/Enter 菜单' : '↑↓ 移动 · Enter 选择'}`;
10457
+ help = graphEvidenceOpen || graphPortableSelectionOpen
10458
+ ? '←→/PgUp/PgDn 翻页 · [y] 复制全文 · Esc 返回关系图'
10459
+ : route.name === 'pluginContent' && contentReader
10460
+ ? `${paginated ? '←→/PgUp/PgDn 翻页 · ' : ''}[y] 复制全文 · Esc 返回清单`
10461
+ : `${paginated ? '←→ 翻页 · ' : ''}[y] 复制全文 · Esc 返回${supportsTechnicalDetails
10462
+ ? showTechnical ? ' · d 返回摘要' : ' · d 技术详情' : ''}${actions.length > 0
10463
+ ? ` · ${size.columns < 80 ? '↑↓/Enter 菜单' : '↑↓ 移动 · Enter 选择'}` : ''}`;
10464
+ if (canOpenHelp)
10465
+ help += ' · ? 帮助';
10309
10466
  if (copyFeedback?.routeKey === routeKey)
10310
10467
  description = copyFeedback.message;
10311
10468
  }
10312
10469
  if (actionError)
10313
10470
  description = actionError;
10314
- const base = (_jsx(ScreenFrame, { title: projectReminderOpen ? '' : title, description: projectReminderOpen ? undefined : description, breadcrumb: breadcrumb, context: context, help: portableEmptySelectionOpen ? 'Enter 返回选择 · Esc 返回' : help, size: size, children: projectReminderOpen || sourceTransportOverlay ? null : content }));
10471
+ const base = (_jsx(ScreenFrame, { title: projectReminderOpen ? '' : title, description: projectReminderOpen ? undefined : description, breadcrumb: breadcrumb, context: context, headerHint: !projectReminderOpen && route.name === 'pluginList' ? { key: 'm', label: '多选' } : undefined, help: portableEmptySelectionOpen ? 'Enter 返回选择 · Esc 返回' : help, size: size, children: projectReminderOpen || sourceTransportOverlay ? null : content }));
10315
10472
  if (portableEmptySelectionOpen) {
10316
10473
  const modalWidth = Math.max(40, Math.floor(size.columns * 0.6));
10317
10474
  return _jsxs(Box, { position: "relative", width: size.columns, height: size.rows, children: [_jsx(Box, { children: base }), _jsx(Box, { position: "absolute", marginTop: 7, marginLeft: Math.max(2, Math.floor((size.columns - modalWidth) / 2)), width: modalWidth, flexDirection: "column", backgroundColor: tuiTheme.colors.panelBackground, children: _jsx(ReviewCard, { title: "\u8FD8\u6CA1\u6709\u9009\u62E9 Skill", description: "\u7528 Space \u52FE\u9009\uFF1B\u4E5F\u53EF\u4EE5\u6253\u5F00\u9009\u62E9\u5DE5\u5177\u3002", children: _jsx(SemanticRows, { actions: [{ label: '返回选择' }], focus: 0, kind: "action", compact: true }) }) })] });
@@ -10367,9 +10524,10 @@ export function ConsoleAppV4(props) {
10367
10524
  ? modalWarningLines
10368
10525
  : confirmationRiskLines(Math.max(12, size.columns - 8));
10369
10526
  const warningLimit = Math.max(1, policy.mode === 'compact' ? 1 : 7);
10370
- const warningOffset = Math.min(operationConfirmation.warningOffset, Math.max(0, warningLines.length - 1));
10527
+ const warningPage = paginateWrappedRows({ lines: warningLines, rows: warningLimit,
10528
+ page: Math.floor(operationConfirmation.warningOffset / warningLimit) + 1 });
10371
10529
  const visibleWarnings = shortConfirmation
10372
- ? warningLines : warningLines.slice(warningOffset, warningOffset + warningLimit);
10530
+ ? warningLines : warningPage.lines;
10373
10531
  const warningPaginated = !shortConfirmation && warningLines.length > warningLimit;
10374
10532
  confirmationText = portableUserConfirmation
10375
10533
  ? '! 本次在所有项目范围安装选定 Skills。\n具体对象和范围以刚才的汇总为准。'
@@ -10389,10 +10547,10 @@ export function ConsoleAppV4(props) {
10389
10547
  },
10390
10548
  ];
10391
10549
  const confirmationCard = (_jsxs(ReviewCard, { title: portableUserConfirmation ? '确认本次影响' : '请确认本次风险', description: `${copyFeedback?.routeKey === `${routeKey}:confirmation`
10392
- ? `${copyFeedback.message} · ` : ''}确认只对当前操作汇总有效;[y] 复制全文。`, children: [!shortConfirmation && warningOffset > 0 ? _jsx(StaticRow, { title: `↑ 前面还有 ${warningOffset} 行`, status: "muted", compact: true }) : null, visibleWarnings.length > 0
10393
- ? visibleWarnings.map((warning, index) => _jsx(StaticRow, { title: warning, status: warning.startsWith('! ') ? 'warning' : 'info', compact: true }, `${warningOffset + index}:${warning}`))
10394
- : _jsx(StaticRow, { title: "! \u6B64\u64CD\u4F5C\u9700\u8981\u660E\u786E\u6388\u6743", status: "warning", compact: true }), !shortConfirmation && warningLines.length - warningOffset - visibleWarnings.length > 0
10395
- ? _jsx(StaticRow, { title: `↓ 后面还有 ${warningLines.length - warningOffset - visibleWarnings.length} 行`, status: "muted", compact: true }) : null, _jsx(SemanticRows, { actions: confirmationActions, focus: overlayFocus, kind: "action", compact: true })] }));
10550
+ ? `${copyFeedback.message} · ` : ''}确认只对当前操作汇总有效;[y] 复制全文。`, children: [visibleWarnings.length > 0
10551
+ ? visibleWarnings.map((warning, index) => _jsx(StaticRow, { title: warning, status: warning.startsWith('! ') ? 'warning' : 'info', compact: true }, `${warningPage.offset + index}:${warning}`))
10552
+ : _jsx(StaticRow, { title: "! \u6B64\u64CD\u4F5C\u9700\u8981\u660E\u786E\u6388\u6743", status: "warning", compact: true }), warningPaginated
10553
+ ? _jsx(Pager, { page: warningPage.page, pageCount: warningPage.pageCount, columns: modalWidth - 6 }) : null, _jsx(SemanticRows, { actions: confirmationActions, focus: overlayFocus, kind: "action", compact: true })] }));
10396
10554
  if (shortConfirmation) {
10397
10555
  return _jsxs(Box, { position: "relative", width: size.columns, height: size.rows, children: [_jsx(Box, { children: base }), _jsx(Box, { position: "absolute", marginTop: 5, marginLeft: modalMarginLeft, width: modalWidth, flexDirection: "column", children: confirmationCard })] });
10398
10556
  }
@@ -10418,13 +10576,13 @@ export function ConsoleAppV4(props) {
10418
10576
  }
10419
10577
  if (!overlay)
10420
10578
  return base;
10421
- const helpRows = helpRowsForRoute(route, actions.length, supportsTechnicalDetails, busy, props.onSessionNavigation !== undefined);
10579
+ const helpText = helpTextForPage(help, busy);
10422
10580
  const overlayActions = overlay === 'help'
10423
10581
  ? [
10424
10582
  { label: '关闭' },
10425
10583
  ...(props.onSessionNavigation ? [
10426
- { label: '返回首页' },
10427
- { label: '返回首页并重新加载' },
10584
+ { label: busy ? '操作成功后返回首页' : '返回首页' },
10585
+ { label: busy ? '操作成功后重载' : '返回首页并重新加载' },
10428
10586
  ] : []),
10429
10587
  ]
10430
10588
  : [
@@ -10434,11 +10592,11 @@ export function ConsoleAppV4(props) {
10434
10592
  return (_jsxs(Box, { position: "relative", width: size.columns, height: size.rows, children: [_jsx(Box, { children: overlay === 'help' ? null : base }), _jsx(Box, { position: "absolute", marginTop: overlay === 'help' ? 0 : 5, marginLeft: overlay === 'help' ? 1 : Math.max(2, Math.floor(size.columns / 5)), width: overlay === 'help' ? size.columns - 2 : Math.max(40, Math.floor(size.columns * 0.6)), flexDirection: "column", children: _jsxs(ReviewCard, { title: overlay === 'help' ? '当前页快捷键'
10435
10593
  : overlay === 'discard' || overlay === 'sessionNavigation' ? '放弃未保存修改?'
10436
10594
  : '退出控制台?', children: [overlay === 'help'
10437
- ? _jsx(_Fragment, { children: helpRows.map(([key, value]) => _jsx(StaticRow, { title: `${key} ${value}`, compact: true }, key)) })
10595
+ ? _jsx(Reader, { text: helpText, columns: size.columns - 6, rows: helpBodyRows, page: helpPage })
10438
10596
  : _jsx(StaticRow, { title: overlay === 'discard'
10439
10597
  ? discardForExit ? '放弃后将退出控制台。' : '放弃后将返回上一页。'
10440
10598
  : overlay === 'sessionNavigation'
10441
10599
  ? `放弃后将${discardForSessionNavigation === 'reload' ? '重新读取本地状态' : '返回首页'}。`
10442
- : '进行中的写操作会先完成。', status: "muted" }), _jsx(SemanticRows, { actions: overlayActions, focus: overlayFocus, kind: "action", compact: true })] }) })] }));
10600
+ : '进行中的写操作会先完成。', status: "muted" }), _jsx(SemanticRows, { actions: overlayActions, focus: overlayFocus, kind: "action", compact: true }), overlay === 'help' ? _jsx(Text, { color: tuiTheme.colors.muted, children: "\u2190\u2192 \u7FFB\u5E2E\u52A9 \u00B7 \u2191\u2193 \u9009\u62E9\u64CD\u4F5C \u00B7 Enter \u786E\u8BA4 \u00B7 Esc \u5173\u95ED" }) : null] }) })] }));
10443
10601
  }
10444
10602
  //# sourceMappingURL=console-app-v4.js.map