@contentful/experience-design-system-cli 2.26.1-dev-build-a852362.0 → 2.26.1-dev-build-f03f014.0

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 (204) hide show
  1. package/dist/package.json +14 -10
  2. package/dist/src/index.js +34937 -9
  3. package/dist/src/lib/cli-path.d.ts +4 -0
  4. package/dist/src/runs/export-helpers.d.ts +1 -12
  5. package/dist/src/runs/push-helpers.d.ts +1 -6
  6. package/package.json +12 -7
  7. package/skills/generate-components.md +427 -0
  8. package/skills/generate-tokens.md +194 -0
  9. package/skills/select-components.md +224 -0
  10. package/dist/src/analytics/apply.js +0 -30
  11. package/dist/src/analytics/client.js +0 -81
  12. package/dist/src/analytics/constants.js +0 -4
  13. package/dist/src/analytics/env.js +0 -14
  14. package/dist/src/analytics/exit.js +0 -24
  15. package/dist/src/analytics/index.js +0 -9
  16. package/dist/src/analytics/normalize.js +0 -18
  17. package/dist/src/analytics/os.js +0 -13
  18. package/dist/src/analytics/session.js +0 -15
  19. package/dist/src/analytics/tracker.js +0 -126
  20. package/dist/src/analytics/types.js +0 -1
  21. package/dist/src/analyze/build-analyze-view-rows.js +0 -63
  22. package/dist/src/analyze/command.js +0 -599
  23. package/dist/src/analyze/composite-closure.js +0 -0
  24. package/dist/src/analyze/composition/agent-parser/author-prompt.js +0 -22
  25. package/dist/src/analyze/composition/agent-parser/extract-parser.js +0 -44
  26. package/dist/src/analyze/composition/agent-parser/load-prompt.js +0 -31
  27. package/dist/src/analyze/composition/agent-parser/resolve-via-parser.js +0 -88
  28. package/dist/src/analyze/composition/agent-parser/sandbox.js +0 -187
  29. package/dist/src/analyze/composition/apply-mapping.js +0 -67
  30. package/dist/src/analyze/composition/candidate-critic-agent.js +0 -39
  31. package/dist/src/analyze/composition/candidate-critic.js +0 -63
  32. package/dist/src/analyze/composition/candidate-files.js +0 -131
  33. package/dist/src/analyze/composition/composition-cache-key.js +0 -19
  34. package/dist/src/analyze/composition/interchange-schema.js +0 -61
  35. package/dist/src/analyze/composition/manifest-doc-evidence.js +0 -136
  36. package/dist/src/analyze/composition/merge-edges.js +0 -70
  37. package/dist/src/analyze/composition/parse-map-edges.js +0 -55
  38. package/dist/src/analyze/composition/resolve-mapping-cli.js +0 -29
  39. package/dist/src/analyze/composition/resolve-mapping.js +0 -111
  40. package/dist/src/analyze/cycle-detection.js +0 -234
  41. package/dist/src/analyze/cycle-view.js +0 -28
  42. package/dist/src/analyze/fuzzy-search.js +0 -55
  43. package/dist/src/analyze/issue-inheritance.js +0 -108
  44. package/dist/src/analyze/lineage.js +0 -0
  45. package/dist/src/analyze/scope-gate-cascade.js +0 -170
  46. package/dist/src/analyze/search-neighborhood.js +0 -56
  47. package/dist/src/analyze/select/command.js +0 -484
  48. package/dist/src/analyze/select/index.js +0 -5
  49. package/dist/src/analyze/select/parser.js +0 -53
  50. package/dist/src/analyze/select/persistence.js +0 -80
  51. package/dist/src/analyze/select/preview-annotations.js +0 -62
  52. package/dist/src/analyze/select/stdout.js +0 -3
  53. package/dist/src/analyze/select/tui/App.js +0 -483
  54. package/dist/src/analyze/select/tui/components/ComponentDetail.js +0 -48
  55. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +0 -125
  56. package/dist/src/analyze/select/tui/components/FieldEditor.js +0 -1131
  57. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +0 -36
  58. package/dist/src/analyze/select/tui/components/GotoBanner.js +0 -30
  59. package/dist/src/analyze/select/tui/components/GroupedSidebar.js +0 -517
  60. package/dist/src/analyze/select/tui/components/HelpOverlay.js +0 -17
  61. package/dist/src/analyze/select/tui/components/JsonEditor.js +0 -131
  62. package/dist/src/analyze/select/tui/components/JsonPanel.js +0 -66
  63. package/dist/src/analyze/select/tui/components/LineagePanel.js +0 -22
  64. package/dist/src/analyze/select/tui/components/PreviewSummaryBar.js +0 -29
  65. package/dist/src/analyze/select/tui/components/QuitDialog.js +0 -14
  66. package/dist/src/analyze/select/tui/components/RationalePanel.js +0 -80
  67. package/dist/src/analyze/select/tui/components/Sidebar.js +0 -92
  68. package/dist/src/analyze/select/tui/components/SourcePanel.js +0 -52
  69. package/dist/src/analyze/select/tui/components/StatusBar.js +0 -7
  70. package/dist/src/analyze/select/tui/components/TopBar.js +0 -10
  71. package/dist/src/analyze/select/tui/components/removed-components-text.js +0 -11
  72. package/dist/src/analyze/select/tui/hooks/scroll-offset.js +0 -26
  73. package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +0 -66
  74. package/dist/src/analyze/select/tui/hooks/useKeymap.js +0 -67
  75. package/dist/src/analyze/select/tui/hooks/useSession.js +0 -52
  76. package/dist/src/analyze/select/tui/hooks/useUndo.js +0 -26
  77. package/dist/src/analyze/select/tui/theme.js +0 -28
  78. package/dist/src/analyze/select/types.js +0 -39
  79. package/dist/src/analyze/select-agent/command.js +0 -558
  80. package/dist/src/analyze/select-agent/context-builder.js +0 -197
  81. package/dist/src/analyze/select-agent/show-rationale.js +0 -92
  82. package/dist/src/analyze/selection-cascade.js +0 -19
  83. package/dist/src/analyze/slot-graph.js +0 -17
  84. package/dist/src/analyze/tui/AnalyzeView.js +0 -64
  85. package/dist/src/apply/api-client.js +0 -440
  86. package/dist/src/apply/command.js +0 -845
  87. package/dist/src/apply/manifest.js +0 -1
  88. package/dist/src/apply/preview-utils.js +0 -26
  89. package/dist/src/apply/tui/SelectView.js +0 -34
  90. package/dist/src/apply/tui/ServerApplyView.js +0 -49
  91. package/dist/src/apply/tui/ServerPreviewView.js +0 -23
  92. package/dist/src/credentials-store.js +0 -74
  93. package/dist/src/generate/command.js +0 -597
  94. package/dist/src/generate/edit/command.js +0 -127
  95. package/dist/src/generate/tui/GenerateView.js +0 -10
  96. package/dist/src/host-utils.js +0 -26
  97. package/dist/src/import/agent-model-resolve.js +0 -35
  98. package/dist/src/import/auto-filter-resolve.js +0 -16
  99. package/dist/src/import/command.js +0 -380
  100. package/dist/src/import/cycle-auto-reject.js +0 -14
  101. package/dist/src/import/orchestrator.js +0 -603
  102. package/dist/src/import/path-utils.js +0 -30
  103. package/dist/src/import/picker-dispatch.js +0 -44
  104. package/dist/src/import/print-prompt.js +0 -31
  105. package/dist/src/import/strip-allowed-components.js +0 -22
  106. package/dist/src/import/tui/CustomPromptBanner.js +0 -8
  107. package/dist/src/import/tui/WizardApp.js +0 -1670
  108. package/dist/src/import/tui/ai-flag.js +0 -6
  109. package/dist/src/import/tui/auto-filter-error.js +0 -29
  110. package/dist/src/import/tui/autocomplete.js +0 -32
  111. package/dist/src/import/tui/components/AutoFilterBanner.js +0 -19
  112. package/dist/src/import/tui/components/CounterStrip.js +0 -12
  113. package/dist/src/import/tui/components/LegendEntry.js +0 -6
  114. package/dist/src/import/tui/cycle-panel-scroll.js +0 -33
  115. package/dist/src/import/tui/final-review-host.js +0 -25
  116. package/dist/src/import/tui/group-collapse.js +0 -11
  117. package/dist/src/import/tui/history.js +0 -53
  118. package/dist/src/import/tui/hooks/useLineage.js +0 -50
  119. package/dist/src/import/tui/hooks/useOverlayPanel.js +0 -27
  120. package/dist/src/import/tui/lineage-layout.js +0 -82
  121. package/dist/src/import/tui/merge-ai-decisions.js +0 -22
  122. package/dist/src/import/tui/push-decision-gate-helpers.js +0 -8
  123. package/dist/src/import/tui/push-progress.js +0 -1
  124. package/dist/src/import/tui/run-print-files-helpers.js +0 -8
  125. package/dist/src/import/tui/run-teaser.js +0 -9
  126. package/dist/src/import/tui/runLivePreview.js +0 -82
  127. package/dist/src/import/tui/runScopeGate.js +0 -28
  128. package/dist/src/import/tui/scope-gate-columns.js +0 -94
  129. package/dist/src/import/tui/scope-gate-host.js +0 -28
  130. package/dist/src/import/tui/sidebar-width.js +0 -3
  131. package/dist/src/import/tui/spawn-generate.js +0 -53
  132. package/dist/src/import/tui/step-filters.js +0 -42
  133. package/dist/src/import/tui/steps/AtomicGenerateReviewStep.js +0 -726
  134. package/dist/src/import/tui/steps/AtomicScopeGateStep.js +0 -210
  135. package/dist/src/import/tui/steps/CredentialsStep.js +0 -108
  136. package/dist/src/import/tui/steps/DoneStep.js +0 -24
  137. package/dist/src/import/tui/steps/ErrorStep.js +0 -16
  138. package/dist/src/import/tui/steps/GateStep.js +0 -22
  139. package/dist/src/import/tui/steps/GenerateReviewStep.js +0 -1618
  140. package/dist/src/import/tui/steps/PathValidationStep.js +0 -152
  141. package/dist/src/import/tui/steps/PreviewStep.js +0 -37
  142. package/dist/src/import/tui/steps/PreviewValidationErrorStep.js +0 -23
  143. package/dist/src/import/tui/steps/PushDecisionGateStep.js +0 -73
  144. package/dist/src/import/tui/steps/PushingStep.js +0 -24
  145. package/dist/src/import/tui/steps/RunningStep.js +0 -21
  146. package/dist/src/import/tui/steps/ScopeGateStep.js +0 -907
  147. package/dist/src/import/tui/steps/TokenInputStep.js +0 -88
  148. package/dist/src/import/tui/steps/WelcomeStep.js +0 -34
  149. package/dist/src/import/tui/steps/WizardPreviewStep.js +0 -169
  150. package/dist/src/import/tui/steps/auto-reject-decision.js +0 -9
  151. package/dist/src/import/tui/steps/breaking-change-format.js +0 -24
  152. package/dist/src/import/tui/steps/enumerate-cycle-breaks.js +0 -0
  153. package/dist/src/import/tui/steps/preview-diff.js +0 -175
  154. package/dist/src/import/tui/useFinalizePreview.js +0 -0
  155. package/dist/src/import/tui/useLivePreview.js +0 -122
  156. package/dist/src/import/tui/wizard-422-helpers.js +0 -60
  157. package/dist/src/import/tui/wizard-generate-progress.js +0 -25
  158. package/dist/src/import/tui/wizard-save-flow.js +0 -20
  159. package/dist/src/import/tui/wizard-state-transitions.js +0 -32
  160. package/dist/src/lib/agent-model-options.js +0 -15
  161. package/dist/src/lib/command-options.js +0 -30
  162. package/dist/src/lib/composition-mode.js +0 -39
  163. package/dist/src/lib/contentful-urls.js +0 -27
  164. package/dist/src/lib/debug-logger.js +0 -229
  165. package/dist/src/lib/debug-preamble.js +0 -33
  166. package/dist/src/lib/error-parser.js +0 -269
  167. package/dist/src/lib/prompt-overrides.js +0 -69
  168. package/dist/src/lib/terminal-capabilities.js +0 -51
  169. package/dist/src/lib/user-agent.js +0 -29
  170. package/dist/src/output/format.js +0 -110
  171. package/dist/src/print/command.js +0 -221
  172. package/dist/src/print/validate/tui/ValidateView.js +0 -37
  173. package/dist/src/print/validate/validators/cdf-validator.js +0 -104
  174. package/dist/src/print/validate/validators/dtcg-validator.js +0 -110
  175. package/dist/src/print/validate/validators/format-errors.js +0 -18
  176. package/dist/src/program.js +0 -105
  177. package/dist/src/runs/export-helpers.js +0 -59
  178. package/dist/src/runs/fingerprint.js +0 -75
  179. package/dist/src/runs/ls-command.js +0 -137
  180. package/dist/src/runs/modify-launcher.js +0 -42
  181. package/dist/src/runs/path-prompt.js +0 -72
  182. package/dist/src/runs/push-creds-prompt.js +0 -39
  183. package/dist/src/runs/push-helpers.js +0 -72
  184. package/dist/src/runs/push-launcher.js +0 -77
  185. package/dist/src/runs/replay-helpers.js +0 -164
  186. package/dist/src/runs/resolve-run-target.js +0 -45
  187. package/dist/src/runs/run-picker-mount.js +0 -72
  188. package/dist/src/runs/run-picker.js +0 -129
  189. package/dist/src/runs/save-conflict.js +0 -56
  190. package/dist/src/runs/save-path-resolver.js +0 -75
  191. package/dist/src/runs/staleness.js +0 -140
  192. package/dist/src/runs/store.js +0 -136
  193. package/dist/src/session/cache-keys.js +0 -44
  194. package/dist/src/session/command.js +0 -261
  195. package/dist/src/session/db.js +0 -1538
  196. package/dist/src/session/migration.js +0 -117
  197. package/dist/src/session/session-id.js +0 -212
  198. package/dist/src/session/stats.js +0 -89
  199. package/dist/src/setup/analytics-prompt.js +0 -24
  200. package/dist/src/setup/auto-filter-prompt.js +0 -24
  201. package/dist/src/setup/command.js +0 -966
  202. package/dist/src/setup/composition-mode-prompt.js +0 -37
  203. package/dist/src/setup/debug-mode-prompt.js +0 -22
  204. package/dist/src/types.js +0 -1
@@ -1,1618 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useRef, useState } from 'react';
3
- import { PALETTE } from '../../../analyze/select/tui/theme.js';
4
- import { Box, Text, useStdout } from 'ink';
5
- import { GroupedSidebar, buildVisibleRows, } from '../../../analyze/select/tui/components/GroupedSidebar.js';
6
- import { computeAllClosures } from '../../../analyze/composite-closure.js';
7
- import { buildComponentGraph } from '../../../analyze/slot-graph.js';
8
- import { computeCycleView } from '../../../analyze/cycle-view.js';
9
- import { computeRenderStatuses, pickDrillTarget } from '../../../analyze/issue-inheritance.js';
10
- import { JsonPanel } from '../../../analyze/select/tui/components/JsonPanel.js';
11
- import { FieldEditor } from '../../../analyze/select/tui/components/FieldEditor.js';
12
- import { StatusBar } from '../../../analyze/select/tui/components/StatusBar.js';
13
- import { FinalizeDialog } from '../../../analyze/select/tui/components/FinalizeDialog.js';
14
- import { removedComponentsHeader, removedComponentLine, } from '../../../analyze/select/tui/components/removed-components-text.js';
15
- import { QuitDialog } from '../../../analyze/select/tui/components/QuitDialog.js';
16
- import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
17
- import { openPipelineDb, loadCDFComponents, storeCDFComponents, loadComponentReviewMetadata, loadComponentRationale, loadSlotCycles, storeSlotCycles, } from '../../../session/db.js';
18
- import { formatCyclePathSegments, findSlotCycles, suggestCycleBreakEdge } from '../../../analyze/cycle-detection.js';
19
- import { followCycleScroll } from '../cycle-panel-scroll.js';
20
- import { RationalePanel } from '../../../analyze/select/tui/components/RationalePanel.js';
21
- import { ComponentRationalePanel } from '../../../analyze/select/tui/components/ComponentRationalePanel.js';
22
- import { applyPreviewAnnotations } from '../../../analyze/select/preview-annotations.js';
23
- import { useLivePreview } from '../useLivePreview.js';
24
- import { useFinalizePreview } from '../useFinalizePreview.js';
25
- import { computeNextScrollOffset } from '../../../analyze/select/tui/hooks/scroll-offset.js';
26
- import { fuzzyMatches } from '../../../analyze/fuzzy-search.js';
27
- import { computeDirectNeighborhood, findAllAncestors as findAllAncestorsInclusive, } from '../../../analyze/search-neighborhood.js';
28
- import { computeSidebarWidth } from '../sidebar-width.js';
29
- import { computeAcceptCascade, computeRejectCascade } from '../../../analyze/selection-cascade.js';
30
- import { useLineage } from '../hooks/useLineage.js';
31
- import { computeCycleAutoRejectTargets } from '../../cycle-auto-reject.js';
32
- import { useOverlayPanel } from '../hooks/useOverlayPanel.js';
33
- import { LineagePanel } from '../../../analyze/select/tui/components/LineagePanel.js';
34
- import { GotoBanner } from '../../../analyze/select/tui/components/GotoBanner.js';
35
- import { computeSidebarBudget, FALLBACK_ROWS } from '../lineage-layout.js';
36
- import { HelpOverlay } from '../../../analyze/select/tui/components/HelpOverlay.js';
37
- import { legendEntry } from '../components/LegendEntry.js';
38
- import { computeAutoRejectDecision } from './auto-reject-decision.js';
39
- import { formatBreakingChange } from './breaking-change-format.js';
40
- import { enumerateCycleBreaks, shouldBreakOverlayGoFullScreen } from './enumerate-cycle-breaks.js';
41
- import { createHistoryStack } from '../history.js';
42
- import { computeAutocomplete } from '../autocomplete.js';
43
- import { resolveGroupRoot } from '../group-collapse.js';
44
- import { buildFlatDimPredicate, computeFilterKeys, intersectFilterKeys } from '../step-filters.js';
45
- export function sortComponentsForSidebar(components, cycleParticipants) {
46
- const isEmpty = (entry) => Object.keys(entry.$properties ?? {}).length === 0 && Object.keys(entry.$slots ?? {}).length === 0;
47
- const tier = (c) => {
48
- if (cycleParticipants?.has(c.key))
49
- return 0;
50
- if (isEmpty(c.entry))
51
- return 1;
52
- return 2;
53
- };
54
- return [...components].sort((a, b) => {
55
- const at = tier(a);
56
- const bt = tier(b);
57
- if (at !== bt)
58
- return at - bt;
59
- return a.key.localeCompare(b.key);
60
- });
61
- }
62
- const PANEL_HEIGHT = 22;
63
- const HELP_SECTIONS = [
64
- {
65
- title: 'Navigation',
66
- entries: [
67
- { keys: 'j / k / ↑ / ↓', label: 'Move cursor' },
68
- { keys: 'Tab', label: 'Toggle sidebar/panel' },
69
- { keys: 'Enter', label: 'Drill to source' },
70
- ],
71
- },
72
- {
73
- title: 'Selection',
74
- entries: [
75
- { keys: 'a', label: 'Accept' },
76
- { keys: 'r', label: 'Reject' },
77
- { keys: 'A', label: 'Accept all' },
78
- { keys: 'F', label: 'Finalize' },
79
- ],
80
- },
81
- {
82
- title: 'Sidebar views',
83
- entries: [
84
- { keys: 'L', label: 'Flat view' },
85
- { keys: 'l', label: 'Lineage' },
86
- { keys: 'i', label: 'Focus lineage' },
87
- { keys: 'w', label: 'Only breaking' },
88
- { keys: 'o', label: 'Only cycles' },
89
- { keys: 'space', label: 'Expand/collapse group' },
90
- { keys: 'E / C', label: 'Expand/collapse all' },
91
- ],
92
- },
93
- {
94
- title: 'Panels',
95
- entries: [
96
- { keys: 'c', label: 'Cycle list' },
97
- { keys: 'p', label: 'Prop rationale' },
98
- { keys: 'P', label: 'Component rationale' },
99
- { keys: 's', label: 'Source' },
100
- { keys: 'J', label: 'Toggle JSON' },
101
- ],
102
- },
103
- {
104
- title: 'Resolving cycles',
105
- entries: [
106
- { keys: 'r', label: 'Reject a cycle member (drops it from the push), or' },
107
- { keys: '', label: "break the cycle by removing a slot's" },
108
- { keys: '', label: '$allowedComponents edge (see [c] suggested fix).' },
109
- { keys: 'x', label: 'Break cycle (from [c]): delete a slot edge.' },
110
- ],
111
- },
112
- {
113
- title: 'Search',
114
- entries: [{ keys: '/', label: 'Search' }],
115
- },
116
- {
117
- title: 'History',
118
- entries: [
119
- { keys: 'Ctrl+Z', label: 'Undo' },
120
- { keys: 'Ctrl+Y', label: 'Redo' },
121
- { keys: 'Ctrl+R', label: 'Reload from save' },
122
- ],
123
- },
124
- {
125
- title: 'General',
126
- entries: [
127
- { keys: '?', label: 'Close help' },
128
- { keys: 'q', label: 'Quit' },
129
- ],
130
- },
131
- ];
132
- export { computeCycleAutoRejectTargets } from '../../cycle-auto-reject.js';
133
- export function buildBreakingRows(breakingChanges) {
134
- const out = [];
135
- for (const b of breakingChanges) {
136
- if (b.changes.length === 0) {
137
- out.push({ label: `${b.componentName} — breaking`, componentName: b.componentName });
138
- continue;
139
- }
140
- for (const change of b.changes) {
141
- const label = formatBreakingChange(change, b.current);
142
- if ('slotId' in change) {
143
- out.push({
144
- label,
145
- componentName: b.componentName,
146
- focusTarget: { kind: 'slot', name: change.slotId },
147
- });
148
- }
149
- else {
150
- out.push({
151
- label,
152
- componentName: b.componentName,
153
- focusTarget: { kind: 'prop', name: change.propertyId },
154
- });
155
- }
156
- }
157
- }
158
- return out;
159
- }
160
- export function deriveBreakingChanges(response) {
161
- const out = [];
162
- for (const item of response.components.changed ?? []) {
163
- if (item.changeClassification?.classification !== 'breaking')
164
- continue;
165
- const componentName = item.current?.name;
166
- if (typeof componentName !== 'string')
167
- continue;
168
- out.push({
169
- componentName,
170
- changes: item.changeClassification.breakingChanges ?? [],
171
- impact: item.impact,
172
- current: item.current,
173
- });
174
- }
175
- return out;
176
- }
177
- export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, livePreview = true, spaceId = '', environmentId = '', cmaToken = '', host = '', tokensPath = '', initialFinalizeError = null, allowDeletions = false, }) {
178
- const { stdout } = useStdout();
179
- const terminalWidth = stdout?.columns ?? 80;
180
- const [components, setComponents] = useState([]);
181
- const [loading, setLoading] = useState(true);
182
- const [loadError, setLoadError] = useState(null);
183
- const [nav, setNav] = useState({
184
- cursorRowIdx: 0,
185
- sidebarScrollOffset: 0,
186
- });
187
- const cursorRowIdx = nav.cursorRowIdx;
188
- const sidebarScrollOffset = nav.sidebarScrollOffset;
189
- const [jsonScrollOffset, setJsonScrollOffset] = useState(0);
190
- const [sidebarFocused, setSidebarFocused] = useState(true);
191
- const [showFinalize, setShowFinalize] = useState(false);
192
- const [showQuit, setShowQuit] = useState(false);
193
- const [showJson, setShowJson] = useState(false);
194
- const [draftValue, setDraftValue] = useState('');
195
- const [saveError, setSaveError] = useState(null);
196
- const [finalizeError, setFinalizeError] = useState(initialFinalizeError);
197
- const [reviewMetadata, setReviewMetadata] = useState(null);
198
- const [previewAnnotations, setPreviewAnnotations] = useState(new Map());
199
- const [removedComponents, setRemovedComponents] = useState([]);
200
- const [removedBannerCollapsed, setRemovedBannerCollapsed] = useState(false);
201
- const removedBannerDefaultedRef = useRef(false);
202
- const [panelOpen, setPanelOpen] = useState('none');
203
- const [panelScrollOffset, setPanelScrollOffset] = useState(0);
204
- const [textEntryActive, setTextEntryActive] = useState(false);
205
- const [componentRationale, setComponentRationale] = useState(null);
206
- const pendingGRef = useRef(false);
207
- const [slotCycles, setSlotCycles] = useState([]);
208
- const [cyclePanelScroll, setCyclePanelScroll] = useState(0);
209
- const [cyclesCursor, setCyclesCursor] = useState(0);
210
- const cyclePanel = useOverlayPanel({
211
- toggleKey: 'c',
212
- onClose: () => {
213
- setCyclePanelScroll(0);
214
- setCyclesCursor(0);
215
- },
216
- });
217
- const breakPanel = useOverlayPanel({
218
- toggleKey: 'x',
219
- onClose: () => {
220
- setBreakCursor(0);
221
- setBreakConfirm(false);
222
- },
223
- });
224
- const [breakCursor, setBreakCursor] = useState(0);
225
- const [breakConfirm, setBreakConfirm] = useState(false);
226
- const [expandedGroups, setExpandedGroups] = useState(new Set());
227
- const seededGroupsRef = useRef(false);
228
- const [searchOpen, setSearchOpen] = useState(false);
229
- const [searchQuery, setSearchQuery] = useState('');
230
- const [autocompleteCandidates, setAutocompleteCandidates] = useState([]);
231
- const [jumpFilterTarget, setJumpFilterTarget] = useState(null);
232
- const lineagePanel = useOverlayPanel({ toggleKey: 'l' });
233
- const [lineageCursor, setLineageCursor] = useState(0);
234
- const breakingPanel = useOverlayPanel({ toggleKey: 'b', onClose: () => setBreakingDetailOpen(false) });
235
- const [breakingChanges, setBreakingChanges] = useState([]);
236
- const [breakingCursor, setBreakingCursor] = useState(0);
237
- const [breakingDetailOpen, setBreakingDetailOpen] = useState(false);
238
- const [pendingEditorFocus, setPendingEditorFocus] = useState(null);
239
- const [columnOneView, setColumnOneView] = useState('grouped');
240
- const [activeFilters, setActiveFilters] = useState(new Set());
241
- const [autoRejected, setAutoRejected] = useState([]);
242
- const [undoSnapshot, setUndoSnapshot] = useState(null);
243
- const autoRejectFiredRef = useRef(false);
244
- const [editorDirty, setEditorDirty] = useState(false);
245
- const [showUnsavedWarning, setShowUnsavedWarning] = useState(false);
246
- const [pendingFocusAway, setPendingFocusAway] = useState(null);
247
- const [discardTrigger, setDiscardTrigger] = useState(0);
248
- const historyRef = useRef(null);
249
- const historySeededRef = useRef(false);
250
- const [showReloadDialog, setShowReloadDialog] = useState(false);
251
- const [showHelp, setShowHelp] = useState(false);
252
- const handleLivePreviewResult = (response) => {
253
- if (!response)
254
- return;
255
- setPreviewAnnotations(applyPreviewAnnotations(response, components.map((c) => c.key)));
256
- const nextRemoved = response.components.removed ?? [];
257
- setRemovedComponents(nextRemoved);
258
- if (!removedBannerDefaultedRef.current && nextRemoved.length > 0) {
259
- removedBannerDefaultedRef.current = true;
260
- setRemovedBannerCollapsed(nextRemoved.length > 5);
261
- }
262
- setBreakingChanges(deriveBreakingChanges(response));
263
- };
264
- const acceptedCountForPreview = components.filter((c) => c.status === 'accepted').length;
265
- const livePreviewHook = useLivePreview({
266
- enabled: livePreview,
267
- sessionId: extractSessionId,
268
- tokensPath,
269
- spaceId,
270
- environmentId,
271
- cmaToken,
272
- host,
273
- onResult: handleLivePreviewResult,
274
- deleteAllComponents: acceptedCountForPreview === 0,
275
- allowDeletions,
276
- });
277
- const SPINNER_FRAMES = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏';
278
- const [spinnerTick, setSpinnerTick] = useState(0);
279
- useEffect(() => {
280
- if (livePreviewHook.status !== 'running')
281
- return;
282
- const id = setInterval(() => setSpinnerTick((t) => t + 1), 80);
283
- return () => clearInterval(id);
284
- }, [livePreviewHook.status]);
285
- const livePreviewSpinner = SPINNER_FRAMES[spinnerTick % SPINNER_FRAMES.length];
286
- const loadSessionState = () => {
287
- const db = openPipelineDb();
288
- let cdfComponents = [];
289
- let cycles = [];
290
- try {
291
- cdfComponents = loadCDFComponents(db, extractSessionId);
292
- cycles = loadSlotCycles(db, extractSessionId);
293
- }
294
- finally {
295
- db.close();
296
- }
297
- if (cdfComponents.length === 0) {
298
- return {
299
- entries: [],
300
- cycles: [],
301
- error: 'No generated definitions found for this session. Try re-running generate.',
302
- };
303
- }
304
- const cycleParticipants = new Set();
305
- for (const c of cycles)
306
- for (const p of c.path)
307
- cycleParticipants.add(p);
308
- const reviewEntries = cdfComponents.map(({ key, entry }) => ({
309
- key,
310
- entry,
311
- status: 'needs-review',
312
- }));
313
- return {
314
- entries: sortComponentsForSidebar(reviewEntries, cycleParticipants),
315
- cycles,
316
- error: null,
317
- };
318
- };
319
- useEffect(() => {
320
- try {
321
- const { entries, cycles, error } = loadSessionState();
322
- if (error) {
323
- setLoadError(error);
324
- setLoading(false);
325
- return;
326
- }
327
- setSlotCycles(cycles);
328
- setComponents(entries);
329
- setLoading(false);
330
- }
331
- catch (e) {
332
- setLoadError(String(e));
333
- setLoading(false);
334
- }
335
- }, []);
336
- useEffect(() => {
337
- if (loading)
338
- return;
339
- if (!livePreview)
340
- return;
341
- if (components.length === 0)
342
- return;
343
- livePreviewHook.trigger();
344
- }, [loading]);
345
- const finalizePreview = useFinalizePreview({
346
- open: showFinalize,
347
- extractSessionId,
348
- tokensPath,
349
- spaceId,
350
- environmentId,
351
- cmaToken,
352
- host,
353
- acceptedKeys: new Set(components.filter((c) => c.status === 'accepted').map((c) => c.key)),
354
- allowDeletions,
355
- });
356
- const handleFinalizeConfirm = () => {
357
- const acceptedCount = components.filter((c) => c.status === 'accepted').length;
358
- const explicitlyRejected = components.filter((c) => c.status === 'rejected').map((c) => c.key);
359
- const unresolved = components.filter((c) => c.status === 'needs-review').map((c) => c.key);
360
- const toReject = [...explicitlyRejected, ...unresolved];
361
- if (toReject.length > 0) {
362
- const db = openPipelineDb();
363
- try {
364
- const stmt = db.prepare(`UPDATE raw_components SET status = 'generate-rejected' WHERE session_id = ? AND name = ?`);
365
- db.exec('BEGIN');
366
- try {
367
- for (const name of toReject) {
368
- stmt.run(extractSessionId, name);
369
- }
370
- db.exec('COMMIT');
371
- }
372
- catch (e) {
373
- db.exec('ROLLBACK');
374
- throw e;
375
- }
376
- }
377
- finally {
378
- db.close();
379
- }
380
- }
381
- onFinalize(acceptedCount, explicitlyRejected.length, unresolved.length);
382
- };
383
- const recomputeCycles = (currentComponents) => {
384
- try {
385
- const view = computeCycleView(currentComponents);
386
- const rawCycles = view.pushBlocking;
387
- const next = rawCycles.map((cycle) => ({
388
- path: cycle.path,
389
- edges: cycle.edges,
390
- suggestedBreak: cycle.edges.length > 0 ? suggestCycleBreakEdge(cycle, rawCycles) : null,
391
- }));
392
- const prevSerialized = JSON.stringify(slotCycles);
393
- const nextSerialized = JSON.stringify(next);
394
- if (prevSerialized === nextSerialized)
395
- return;
396
- setSlotCycles(next);
397
- const db = openPipelineDb();
398
- try {
399
- storeSlotCycles(db, extractSessionId, next);
400
- }
401
- finally {
402
- db.close();
403
- }
404
- if (next.length === 0)
405
- setFinalizeError(null);
406
- }
407
- catch {
408
- // Defensive: swallow — never let cycle detection crash the review UI.
409
- }
410
- };
411
- const componentGraph = useMemo(() => buildComponentGraph(components), [components]);
412
- const sidebarGraph = useMemo(() => buildComponentGraph(components, { stripRejectedEdges: true }), [components]);
413
- const closures = useMemo(() => computeAllClosures(componentGraph), [componentGraph]);
414
- useEffect(() => {
415
- if (seededGroupsRef.current)
416
- return;
417
- if (closures.size === 0 && slotCycles.length === 0)
418
- return;
419
- seededGroupsRef.current = true;
420
- const seed = new Set(closures.keys());
421
- for (const cyc of slotCycles)
422
- for (const p of cyc.path)
423
- seed.add(p);
424
- setExpandedGroups(seed);
425
- }, [closures, slotCycles]);
426
- const directIssues = useMemo(() => {
427
- const m = new Map();
428
- for (const c of components) {
429
- if (c.status === 'rejected')
430
- m.set(c.key, 'error');
431
- }
432
- return m;
433
- }, [components]);
434
- const cycleView = useMemo(() => computeCycleView(components), [components]);
435
- useEffect(() => {
436
- const decision = computeAutoRejectDecision({
437
- loading,
438
- autoRejectFired: autoRejectFiredRef.current,
439
- hasCycle: cycleView.structural.size > 0,
440
- });
441
- if (decision === 'skip')
442
- return;
443
- autoRejectFiredRef.current = true;
444
- const targets = computeCycleAutoRejectTargets(slotCycles, componentGraph);
445
- if (targets.size === 0)
446
- return;
447
- const snapshot = new Map();
448
- const flipped = [];
449
- for (const c of components) {
450
- if (!targets.has(c.key))
451
- continue;
452
- snapshot.set(c.key, c.status);
453
- if (c.status !== 'rejected')
454
- flipped.push(c.key);
455
- }
456
- setComponents((prev) => prev.map((c) => (targets.has(c.key) ? { ...c, status: 'rejected' } : c)));
457
- setAutoRejected(flipped.length > 0 ? flipped : [...targets].sort());
458
- setUndoSnapshot(flipped.length > 0 ? snapshot : null);
459
- }, [loading, cycleView, componentGraph, slotCycles]);
460
- useEffect(() => {
461
- if (loading)
462
- return;
463
- if (historySeededRef.current)
464
- return;
465
- historySeededRef.current = true;
466
- historyRef.current = createHistoryStack({
467
- components: components.map((c) => ({ key: c.key, entry: c.entry, status: c.status })),
468
- autoRejected: [],
469
- undoSnapshot: null,
470
- });
471
- }, [loading, components]);
472
- const pushHistorySnapshot = (entries, autoRej, undoSnap, label) => {
473
- if (!historyRef.current)
474
- return;
475
- historyRef.current.push({
476
- components: entries.map((c) => ({ key: c.key, entry: c.entry, status: c.status })),
477
- autoRejected: [...autoRej],
478
- undoSnapshot: undoSnap === null ? null : new Map(undoSnap),
479
- }, label);
480
- };
481
- const autoRejectPushedRef = useRef(false);
482
- useEffect(() => {
483
- if (loading)
484
- return;
485
- if (!historySeededRef.current)
486
- return;
487
- if (!autoRejectFiredRef.current)
488
- return;
489
- if (autoRejectPushedRef.current)
490
- return;
491
- autoRejectPushedRef.current = true;
492
- pushHistorySnapshot(components, autoRejected, undoSnapshot, 'mount-auto-reject');
493
- }, [autoRejected]);
494
- const applyHistorySnapshot = (snap) => {
495
- const restored = snap.components.map((c) => ({
496
- key: c.key,
497
- entry: c.entry,
498
- status: c.status,
499
- }));
500
- setComponents(restored);
501
- setAutoRejected(snap.autoRejected);
502
- setUndoSnapshot(snap.undoSnapshot);
503
- recomputeCycles(restored);
504
- };
505
- const handleUndo = () => {
506
- const snap = historyRef.current?.undo();
507
- if (!snap)
508
- return;
509
- applyHistorySnapshot(snap);
510
- };
511
- const handleRedo = () => {
512
- const snap = historyRef.current?.redo();
513
- if (!snap)
514
- return;
515
- applyHistorySnapshot(snap);
516
- };
517
- const reloadFromSave = () => {
518
- try {
519
- const { entries, cycles, error } = loadSessionState();
520
- if (error) {
521
- setLoadError(error);
522
- return;
523
- }
524
- setSlotCycles(cycles);
525
- setComponents(entries);
526
- const reloadGraph = buildComponentGraph(entries);
527
- const reloadClosures = computeAllClosures(reloadGraph);
528
- const reloadCycleView = computeCycleView(entries);
529
- const reloadSeed = new Set();
530
- for (const [name, closure] of reloadClosures.entries()) {
531
- if (closure.nodes.length > 1)
532
- reloadSeed.add(name);
533
- }
534
- for (const name of reloadCycleView.structural)
535
- reloadSeed.add(name);
536
- setExpandedGroups(reloadSeed);
537
- seededGroupsRef.current = true;
538
- setAutoRejected([]);
539
- setUndoSnapshot(null);
540
- autoRejectFiredRef.current = false;
541
- autoRejectPushedRef.current = false;
542
- historyRef.current?.reset({
543
- components: entries.map((c) => ({ key: c.key, entry: c.entry, status: c.status })),
544
- autoRejected: [],
545
- undoSnapshot: null,
546
- });
547
- setNav({ cursorRowIdx: 0, sidebarScrollOffset: 0 });
548
- setSaveError(null);
549
- setFinalizeError(null);
550
- }
551
- catch (e) {
552
- setLoadError(String(e));
553
- }
554
- };
555
- const groupedItemsMemo = useMemo(() => components.map((c) => ({
556
- key: c.key,
557
- entry: c.entry,
558
- status: (directIssues.get(c.key) ?? 'ok'),
559
- })), [components, directIssues]);
560
- const brokenKeys = useMemo(() => new Set(breakingChanges.map((b) => b.componentName)), [breakingChanges]);
561
- const breakingRows = useMemo(() => buildBreakingRows(breakingChanges), [breakingChanges]);
562
- const filterVisibleKeys = useMemo(() => {
563
- if (jumpFilterTarget) {
564
- return findAllAncestorsInclusive(jumpFilterTarget, sidebarGraph);
565
- }
566
- const categoryKeys = computeFilterKeys({
567
- filters: activeFilters,
568
- data: { cycles: cycleView.structural, broken: brokenKeys },
569
- });
570
- const searchKeys = (() => {
571
- if (!searchQuery)
572
- return undefined;
573
- const matches = groupedItemsMemo.map((it) => it.key).filter((k) => fuzzyMatches(searchQuery, k));
574
- if (matches.length === 0)
575
- return undefined;
576
- return computeDirectNeighborhood(matches, sidebarGraph);
577
- })();
578
- return intersectFilterKeys(categoryKeys, searchKeys);
579
- }, [jumpFilterTarget, activeFilters, cycleView, brokenKeys, searchQuery, groupedItemsMemo, sidebarGraph]);
580
- const visibleRowsMemo = useMemo(() => buildVisibleRows({
581
- items: groupedItemsMemo,
582
- cycleParticipants: cycleView.structural,
583
- expandedGroups,
584
- viewMode: columnOneView,
585
- graph: sidebarGraph,
586
- filterVisibleKeys,
587
- }), [groupedItemsMemo, cycleView, expandedGroups, columnOneView, sidebarGraph, filterVisibleKeys]);
588
- const selectableRowPositions = useMemo(() => {
589
- const out = [];
590
- for (let i = 0; i < visibleRowsMemo.length; i++) {
591
- if (visibleRowsMemo[i].itemIdx >= 0)
592
- out.push(i);
593
- }
594
- return out;
595
- }, [visibleRowsMemo]);
596
- const selectedIdx = visibleRowsMemo[cursorRowIdx]?.itemIdx ?? -1;
597
- const focusedComponentKey = components[selectedIdx]?.key ?? null;
598
- const { entries: lineageEntries, jumpables: lineageJumpables } = useLineage(focusedComponentKey, componentGraph);
599
- const { sidebarVisibleCount: visibleCount, panelMaxRows } = computeSidebarBudget({
600
- rows: stdout?.rows ?? FALLBACK_ROWS,
601
- panelOpen: lineagePanel.isOpen,
602
- entryCount: lineageEntries.length,
603
- });
604
- useEffect(() => {
605
- if (selectableRowPositions.length === 0)
606
- return;
607
- const cursorInRange = selectableRowPositions.includes(cursorRowIdx);
608
- const maxScroll = Math.max(0, visibleRowsMemo.length - visibleCount);
609
- const scrollNeedsClamp = sidebarScrollOffset > maxScroll;
610
- if (cursorInRange && !scrollNeedsClamp)
611
- return;
612
- const nextCursor = cursorInRange ? cursorRowIdx : selectableRowPositions[0];
613
- setNav(() => ({
614
- cursorRowIdx: nextCursor,
615
- sidebarScrollOffset: Math.min(sidebarScrollOffset, maxScroll),
616
- }));
617
- }, [selectableRowPositions, cursorRowIdx, sidebarScrollOffset, visibleRowsMemo.length, visibleCount]);
618
- useEffect(() => {
619
- const current = components[selectedIdx];
620
- if (!current) {
621
- setReviewMetadata(null);
622
- return;
623
- }
624
- const db = openPipelineDb();
625
- try {
626
- setReviewMetadata(loadComponentReviewMetadata(db, extractSessionId, current.key));
627
- }
628
- catch {
629
- setReviewMetadata(null);
630
- }
631
- finally {
632
- db.close();
633
- }
634
- }, [selectedIdx, components, extractSessionId]);
635
- useEffect(() => {
636
- const current = components[selectedIdx];
637
- if (!current) {
638
- setComponentRationale(null);
639
- return;
640
- }
641
- const db = openPipelineDb();
642
- try {
643
- setComponentRationale(loadComponentRationale(db, extractSessionId, current.key));
644
- }
645
- catch {
646
- setComponentRationale(null);
647
- }
648
- finally {
649
- db.close();
650
- }
651
- }, [selectedIdx, components, extractSessionId]);
652
- const renderStatusByKey = useMemo(() => {
653
- const merged = new Map();
654
- for (const closure of closures.values()) {
655
- const per = computeRenderStatuses(closure, directIssues);
656
- for (const [name, rs] of per.entries()) {
657
- const existing = merged.get(name);
658
- if (!existing || (!existing.isOwn && rs.isOwn)) {
659
- merged.set(name, rs);
660
- }
661
- }
662
- }
663
- return merged;
664
- }, [closures, directIssues]);
665
- const selectionStateByKey = useMemo(() => {
666
- const map = new Map();
667
- for (const c of components) {
668
- if (c.status === 'accepted')
669
- map.set(c.key, 'accepted');
670
- else if (c.status === 'rejected')
671
- map.set(c.key, 'rejected');
672
- else
673
- map.set(c.key, 'undecided');
674
- }
675
- return map;
676
- }, [components]);
677
- const searchMatches = useMemo(() => {
678
- if (!searchQuery)
679
- return [];
680
- const out = [];
681
- for (const pos of selectableRowPositions) {
682
- const row = visibleRowsMemo[pos];
683
- const key = row ? components[row.itemIdx]?.key : undefined;
684
- if (key && fuzzyMatches(searchQuery, key))
685
- out.push(pos);
686
- }
687
- return out;
688
- }, [searchQuery, selectableRowPositions, visibleRowsMemo, components]);
689
- const searchMatchCount = useMemo(() => {
690
- if (searchMatches.length === 0)
691
- return 0;
692
- const seen = new Set();
693
- for (const pos of searchMatches) {
694
- const itemIdx = visibleRowsMemo[pos]?.itemIdx;
695
- if (itemIdx != null && itemIdx >= 0)
696
- seen.add(itemIdx);
697
- }
698
- return seen.size;
699
- }, [searchMatches, visibleRowsMemo]);
700
- const dimPredicate = useMemo(() => buildFlatDimPredicate({
701
- viewMode: columnOneView,
702
- searchQuery,
703
- filterVisibleKeys,
704
- }), [columnOneView, searchQuery, filterVisibleKeys]);
705
- const jumpCursorToRow = (rowIdx) => {
706
- if (rowIdx < 0 || rowIdx >= visibleRowsMemo.length)
707
- return;
708
- setNav(({ sidebarScrollOffset: prev }) => {
709
- let nextOff = prev;
710
- if (rowIdx < prev)
711
- nextOff = rowIdx;
712
- else if (rowIdx >= prev + visibleCount)
713
- nextOff = rowIdx - visibleCount + 1;
714
- return { cursorRowIdx: rowIdx, sidebarScrollOffset: nextOff };
715
- });
716
- setJsonScrollOffset(0);
717
- setDraftValue('');
718
- setSaveError(null);
719
- setPendingEditorFocus(null);
720
- };
721
- const jumpCursorToName = (name) => {
722
- for (let i = 0; i < visibleRowsMemo.length; i++) {
723
- const row = visibleRowsMemo[i];
724
- if (row.itemIdx < 0)
725
- continue;
726
- if (components[row.itemIdx]?.key === name) {
727
- jumpCursorToRow(i);
728
- return;
729
- }
730
- }
731
- };
732
- const handleEditSave = () => {
733
- const current = components[selectedIdx];
734
- if (!current)
735
- return;
736
- try {
737
- const parsed = JSON.parse(draftValue);
738
- const keys = Object.keys(parsed);
739
- const entry = keys.length === 1 && typeof parsed[keys[0]] === 'object' && parsed[keys[0]] !== null
740
- ? parsed[keys[0]]
741
- : parsed;
742
- if (entry.$type !== 'component' || typeof entry.$properties !== 'object' || entry.$properties === null) {
743
- setSaveError('Invalid CDF entry: must have $type: "component" and $properties object');
744
- return;
745
- }
746
- let updatedComponents = [];
747
- setComponents((prev) => {
748
- updatedComponents = prev.map((c, i) => i === selectedIdx ? { ...c, entry, status: c.status === 'needs-review' ? 'accepted' : c.status } : c);
749
- return updatedComponents;
750
- });
751
- setDraftValue('');
752
- setSaveError(null);
753
- const db = openPipelineDb();
754
- try {
755
- storeCDFComponents(db, extractSessionId, [{ key: current.key, entry }]);
756
- }
757
- finally {
758
- db.close();
759
- }
760
- recomputeCycles(updatedComponents);
761
- livePreviewHook.trigger();
762
- pushHistorySnapshot(updatedComponents, autoRejected, undoSnapshot, 'edit-save');
763
- }
764
- catch (e) {
765
- setSaveError(String(e));
766
- }
767
- };
768
- const handleEditDiscard = () => {
769
- setDraftValue('');
770
- setSaveError(null);
771
- };
772
- const breakEdges = useMemo(() => {
773
- const cycle = slotCycles[cyclesCursor];
774
- if (!cycle)
775
- return [];
776
- return enumerateCycleBreaks(cycle, components);
777
- }, [slotCycles, cyclesCursor, components]);
778
- const handleBreakEdge = (edge) => {
779
- const idx = components.findIndex((c) => c.key === edge.fromComponent);
780
- if (idx < 0)
781
- return;
782
- const target = components[idx];
783
- const slot = target.entry.$slots?.[edge.slotName];
784
- if (!slot || !Array.isArray(slot.$allowedComponents))
785
- return;
786
- const nextAllowed = slot.$allowedComponents.filter((v) => v !== edge.toComponent);
787
- const nextEntry = {
788
- ...target.entry,
789
- $slots: {
790
- ...target.entry.$slots,
791
- [edge.slotName]: { ...slot, $allowedComponents: nextAllowed },
792
- },
793
- };
794
- const next = components.map((c, i) => (i === idx ? { ...c, entry: nextEntry } : c));
795
- setComponents(next);
796
- const db = openPipelineDb();
797
- try {
798
- storeCDFComponents(db, extractSessionId, [{ key: target.key, entry: nextEntry }]);
799
- }
800
- finally {
801
- db.close();
802
- }
803
- recomputeCycles(next);
804
- livePreviewHook.trigger();
805
- pushHistorySnapshot(next, autoRejected, undoSnapshot, 'break-cycle-edge');
806
- };
807
- const handleRejectComponent = (key) => {
808
- const rejectCascade = computeRejectCascade(key, componentGraph);
809
- const acceptCascade = computeAcceptCascade(key, componentGraph);
810
- const next = components.map((c) => {
811
- if (rejectCascade.has(c.key)) {
812
- return { ...c, status: 'rejected' };
813
- }
814
- if (acceptCascade.has(c.key) && c.key !== key) {
815
- return { ...c, status: 'needs-review' };
816
- }
817
- return c;
818
- });
819
- setComponents(next);
820
- recomputeCycles(next);
821
- pushHistorySnapshot(next, autoRejected, undoSnapshot, 'reject-cascade');
822
- };
823
- const dialogOpen = showFinalize || showQuit;
824
- useImmediateInput((input, key) => {
825
- if (loading)
826
- return;
827
- // On a load error there's nothing to review — still let the operator quit
828
- // (q / Esc / Enter) instead of trapping them on the error screen.
829
- if (loadError) {
830
- if (input === 'q' || key.escape || key.return)
831
- onQuit();
832
- return;
833
- }
834
- if (showFinalize) {
835
- // The dialog owns y/n/Enter/Esc; here we own j/k scroll of its deletion list.
836
- if (input === 'j' || key.downArrow) {
837
- finalizePreview.scrollBy(1);
838
- return;
839
- }
840
- if (input === 'k' || key.upArrow) {
841
- finalizePreview.scrollBy(-1);
842
- return;
843
- }
844
- return;
845
- }
846
- if (dialogOpen)
847
- return;
848
- if (showHelp)
849
- return;
850
- if (showReloadDialog) {
851
- if (key.return) {
852
- reloadFromSave();
853
- setShowReloadDialog(false);
854
- return;
855
- }
856
- if (key.escape) {
857
- setShowReloadDialog(false);
858
- return;
859
- }
860
- return;
861
- }
862
- if (key.ctrl && input === 'z') {
863
- handleUndo();
864
- return;
865
- }
866
- if (key.ctrl && input === 'y') {
867
- handleRedo();
868
- return;
869
- }
870
- if (key.ctrl && input === 'r') {
871
- setShowReloadDialog(true);
872
- return;
873
- }
874
- if (showUnsavedWarning) {
875
- if (key.return) {
876
- handleEditSave();
877
- setShowUnsavedWarning(false);
878
- if (pendingFocusAway === 'tab-to-sidebar')
879
- setSidebarFocused(true);
880
- setPendingFocusAway(null);
881
- return;
882
- }
883
- if (key.escape) {
884
- setDiscardTrigger((n) => n + 1);
885
- setShowUnsavedWarning(false);
886
- if (pendingFocusAway === 'tab-to-sidebar')
887
- setSidebarFocused(true);
888
- setPendingFocusAway(null);
889
- return;
890
- }
891
- setShowUnsavedWarning(false);
892
- setPendingFocusAway(null);
893
- return;
894
- }
895
- if (searchOpen) {
896
- if (key.escape) {
897
- setSearchOpen(false);
898
- setSearchQuery('');
899
- setAutocompleteCandidates([]);
900
- return;
901
- }
902
- if (key.return) {
903
- setAutocompleteCandidates([]);
904
- if (!searchQuery || searchMatches.length === 0) {
905
- setSearchOpen(false);
906
- setSearchQuery('');
907
- return;
908
- }
909
- let jumped = false;
910
- for (let i = cursorRowIdx + 1; i < visibleRowsMemo.length; i++) {
911
- const row = visibleRowsMemo[i];
912
- if (row.itemIdx < 0)
913
- continue;
914
- const key2 = components[row.itemIdx]?.key;
915
- if (key2 && fuzzyMatches(searchQuery, key2)) {
916
- jumpCursorToRow(i);
917
- jumped = true;
918
- break;
919
- }
920
- }
921
- if (!jumped) {
922
- jumpCursorToRow(searchMatches[0]);
923
- }
924
- setSearchOpen(false);
925
- return;
926
- }
927
- if (key.tab) {
928
- const { completion, candidates } = computeAutocomplete(searchQuery, components.map((c) => c.key));
929
- setSearchQuery(completion);
930
- setAutocompleteCandidates(candidates);
931
- return;
932
- }
933
- if (key.backspace) {
934
- setAutocompleteCandidates([]);
935
- setSearchQuery((q) => q.slice(0, -1));
936
- return;
937
- }
938
- if (input && input.length === 1 && input >= ' ' && input !== '\r' && input !== '\n') {
939
- setAutocompleteCandidates([]);
940
- setSearchQuery((q) => q + input);
941
- return;
942
- }
943
- return;
944
- }
945
- if (breakingPanel.isOpen) {
946
- if (breakingPanel.handleInput(input, key))
947
- return;
948
- if (input === 'D') {
949
- setBreakingDetailOpen((prev) => !prev);
950
- return;
951
- }
952
- if (key.upArrow || input === 'k') {
953
- setBreakingDetailOpen(false);
954
- setBreakingCursor((c) => Math.max(0, c - 1));
955
- return;
956
- }
957
- if (key.downArrow || input === 'j') {
958
- setBreakingDetailOpen(false);
959
- setBreakingCursor((c) => Math.min(Math.max(0, breakingRows.length - 1), c + 1));
960
- return;
961
- }
962
- if (key.return) {
963
- const row = breakingRows[breakingCursor];
964
- if (row) {
965
- jumpCursorToName(row.componentName);
966
- if (row.focusTarget) {
967
- setPendingEditorFocus({ componentName: row.componentName, target: row.focusTarget });
968
- setSidebarFocused(false);
969
- }
970
- }
971
- breakingPanel.close();
972
- return;
973
- }
974
- return;
975
- }
976
- if (lineagePanel.isOpen) {
977
- if (lineagePanel.handleInput(input, key))
978
- return;
979
- if (key.upArrow || input === 'k') {
980
- setLineageCursor((c) => Math.max(0, c - 1));
981
- return;
982
- }
983
- if (key.downArrow || input === 'j') {
984
- setLineageCursor((c) => Math.min(Math.max(0, lineageJumpables.length - 1), c + 1));
985
- return;
986
- }
987
- if (key.tab) {
988
- setLineageCursor((c) => (lineageJumpables.length === 0 ? 0 : (c + 1) % lineageJumpables.length));
989
- return;
990
- }
991
- if (key.return) {
992
- const target = lineageJumpables[lineageCursor];
993
- if (target && (target.entry.kind === 'ancestor' || target.entry.kind === 'descendant')) {
994
- jumpCursorToName(target.entry.jumpTarget);
995
- }
996
- lineagePanel.close();
997
- return;
998
- }
999
- return;
1000
- }
1001
- if (breakPanel.isOpen) {
1002
- if (breakConfirm) {
1003
- if (input === 'y') {
1004
- const edge = breakEdges[breakCursor];
1005
- if (edge)
1006
- handleBreakEdge(edge);
1007
- setBreakConfirm(false);
1008
- breakPanel.close();
1009
- return;
1010
- }
1011
- if (input === 'n' || key.escape) {
1012
- setBreakConfirm(false);
1013
- return;
1014
- }
1015
- return;
1016
- }
1017
- if (breakPanel.handleInput(input, key))
1018
- return;
1019
- if (key.upArrow || input === 'k') {
1020
- setBreakCursor((c) => Math.max(0, c - 1));
1021
- return;
1022
- }
1023
- if (key.downArrow || input === 'j') {
1024
- setBreakCursor((c) => Math.min(Math.max(0, breakEdges.length - 1), c + 1));
1025
- return;
1026
- }
1027
- if (key.return) {
1028
- if (breakEdges[breakCursor])
1029
- setBreakConfirm(true);
1030
- return;
1031
- }
1032
- return;
1033
- }
1034
- if (cyclePanel.isOpen) {
1035
- if (input === 'x' && breakEdges.length > 0) {
1036
- breakPanel.open();
1037
- setBreakCursor(0);
1038
- setBreakConfirm(false);
1039
- return;
1040
- }
1041
- if (cyclePanel.handleInput(input, key))
1042
- return;
1043
- if (input === 'q') {
1044
- cyclePanel.close();
1045
- return;
1046
- }
1047
- if (key.upArrow || input === 'k') {
1048
- setCyclesCursor((c) => {
1049
- const next = Math.max(0, c - 1);
1050
- setCyclePanelScroll((scroll) => followCycleScroll(scroll, next, slotCycles, 20));
1051
- return next;
1052
- });
1053
- return;
1054
- }
1055
- if (key.downArrow || input === 'j') {
1056
- setCyclesCursor((c) => {
1057
- const next = Math.min(Math.max(0, slotCycles.length - 1), c + 1);
1058
- setCyclePanelScroll((scroll) => followCycleScroll(scroll, next, slotCycles, 20));
1059
- return next;
1060
- });
1061
- return;
1062
- }
1063
- if (key.return) {
1064
- const target = slotCycles[cyclesCursor];
1065
- if (target && target.path.length > 0)
1066
- jumpCursorToName(target.path[0]);
1067
- cyclePanel.close();
1068
- return;
1069
- }
1070
- return;
1071
- }
1072
- if (input === 'c' && sidebarFocused && slotCycles.length > 0) {
1073
- cyclePanel.open();
1074
- setCyclePanelScroll(0);
1075
- setCyclesCursor(0);
1076
- return;
1077
- }
1078
- if (input === 'l' && sidebarFocused && focusedComponentKey) {
1079
- lineagePanel.open();
1080
- setLineageCursor(0);
1081
- return;
1082
- }
1083
- if (input === 'b' && sidebarFocused && breakingChanges.length > 0) {
1084
- breakingPanel.open();
1085
- setBreakingCursor(0);
1086
- return;
1087
- }
1088
- if (input === 'd' && sidebarFocused && removedComponents.length > 0) {
1089
- setRemovedBannerCollapsed((prev) => !prev);
1090
- return;
1091
- }
1092
- if (sidebarFocused && (input === 'o' || input === 'w')) {
1093
- const category = input === 'o' ? 'cycles' : 'broken';
1094
- setActiveFilters((prev) => {
1095
- const next = new Set(prev);
1096
- if (next.has(category))
1097
- next.delete(category);
1098
- else
1099
- next.add(category);
1100
- return next;
1101
- });
1102
- return;
1103
- }
1104
- if (input === 'i' && sidebarFocused && !key.tab && !key.ctrl && !key.meta && focusedComponentKey) {
1105
- const t = focusedComponentKey;
1106
- setJumpFilterTarget((prev) => (prev === t ? null : t));
1107
- return;
1108
- }
1109
- if (panelOpen !== 'none') {
1110
- const PANEL_HEIGHT_LOCAL = 12;
1111
- const next = computeNextScrollOffset(panelScrollOffset, input, key, 9999, PANEL_HEIGHT_LOCAL);
1112
- if (next !== null) {
1113
- setPanelScrollOffset(() => next);
1114
- return;
1115
- }
1116
- if (key.escape) {
1117
- setPanelOpen('none');
1118
- setPanelScrollOffset(() => 0);
1119
- return;
1120
- }
1121
- const togglable = !key.ctrl && !key.tab && !key.meta && !key.return;
1122
- if (togglable && input === 'p' && panelOpen === 'prop-rationale') {
1123
- setPanelOpen('none');
1124
- setPanelScrollOffset(() => 0);
1125
- return;
1126
- }
1127
- if (togglable && input === 'P' && panelOpen === 'component-rationale') {
1128
- setPanelOpen('none');
1129
- setPanelScrollOffset(() => 0);
1130
- return;
1131
- }
1132
- if (togglable && input === 's' && panelOpen === 'source') {
1133
- setPanelOpen('none');
1134
- setPanelScrollOffset(() => 0);
1135
- return;
1136
- }
1137
- if (togglable && input === 'p') {
1138
- setPanelOpen('prop-rationale');
1139
- setPanelScrollOffset(() => 0);
1140
- return;
1141
- }
1142
- if (togglable && input === 'P') {
1143
- setPanelOpen('component-rationale');
1144
- setPanelScrollOffset(() => 0);
1145
- return;
1146
- }
1147
- if (togglable && input === 's') {
1148
- setPanelOpen('source');
1149
- setPanelScrollOffset(() => 0);
1150
- return;
1151
- }
1152
- return;
1153
- }
1154
- const rationaleKeyOk = !textEntryActive && !showJson && !key.ctrl && !key.tab && !key.meta && !key.return;
1155
- if (rationaleKeyOk) {
1156
- if (input === 'p') {
1157
- setPanelOpen('prop-rationale');
1158
- setPanelScrollOffset(() => 0);
1159
- return;
1160
- }
1161
- if (input === 'P') {
1162
- setPanelOpen('component-rationale');
1163
- setPanelScrollOffset(() => 0);
1164
- return;
1165
- }
1166
- if (input === 's') {
1167
- setPanelOpen('source');
1168
- setPanelScrollOffset(() => 0);
1169
- return;
1170
- }
1171
- }
1172
- if (key.tab) {
1173
- if (!sidebarFocused && editorDirty) {
1174
- setPendingFocusAway('tab-to-sidebar');
1175
- setShowUnsavedWarning(true);
1176
- return;
1177
- }
1178
- setSidebarFocused((prev) => !prev);
1179
- return;
1180
- }
1181
- if (input === ' ' && sidebarFocused && !showJson) {
1182
- const current = components[selectedIdx];
1183
- if (!current)
1184
- return;
1185
- const rootName = resolveGroupRoot(current.key, closures, cycleView.structural);
1186
- if (!rootName)
1187
- return;
1188
- setExpandedGroups((prev) => {
1189
- const next = new Set(prev);
1190
- if (next.has(rootName))
1191
- next.delete(rootName);
1192
- else
1193
- next.add(rootName);
1194
- return next;
1195
- });
1196
- return;
1197
- }
1198
- if (!sidebarFocused && showJson) {
1199
- const current = components[selectedIdx];
1200
- const currentJson = current ? JSON.stringify({ [current.key]: current.entry }, null, 2) : '';
1201
- const totalLines = currentJson.split('\n').length;
1202
- const maxOffset = Math.max(0, totalLines - PANEL_HEIGHT);
1203
- if (input === 'g' && !key.ctrl) {
1204
- if (pendingGRef.current) {
1205
- pendingGRef.current = false;
1206
- setJsonScrollOffset(() => 0);
1207
- return;
1208
- }
1209
- pendingGRef.current = true;
1210
- return;
1211
- }
1212
- const next = computeNextScrollOffset(jsonScrollOffset, input, key, totalLines, PANEL_HEIGHT);
1213
- if (next !== null) {
1214
- pendingGRef.current = false;
1215
- const clamped = Math.min(maxOffset, Math.max(0, next));
1216
- setJsonScrollOffset(() => clamped);
1217
- return;
1218
- }
1219
- pendingGRef.current = false;
1220
- }
1221
- if (!sidebarFocused)
1222
- return;
1223
- if (input === '/') {
1224
- setSearchOpen(true);
1225
- return;
1226
- }
1227
- if (key.escape && jumpFilterTarget) {
1228
- setJumpFilterTarget(null);
1229
- return;
1230
- }
1231
- if (key.escape && searchQuery) {
1232
- setSearchQuery('');
1233
- setAutocompleteCandidates([]);
1234
- return;
1235
- }
1236
- if (input === '?') {
1237
- setShowHelp(true);
1238
- return;
1239
- }
1240
- if (input === 'q') {
1241
- setShowQuit(true);
1242
- return;
1243
- }
1244
- if (input === 'F' || input === 'f') {
1245
- const acceptedNames = new Set(components.filter((c) => c.status === 'accepted').map((c) => c.key));
1246
- const acceptedSubgraph = componentGraph.filter((n) => acceptedNames.has(n.name));
1247
- const acceptedCycles = (() => {
1248
- try {
1249
- return findSlotCycles(acceptedSubgraph);
1250
- }
1251
- catch {
1252
- return [];
1253
- }
1254
- })();
1255
- if (acceptedCycles.length > 0) {
1256
- const participants = new Set();
1257
- for (const c of acceptedCycles)
1258
- for (const p of c.path)
1259
- participants.add(p);
1260
- setFinalizeError(`Cannot finalize — accepted set still contains a cycle (${[...participants].sort().join(', ')}). Reject a cycle member to break it.`);
1261
- return;
1262
- }
1263
- setShowFinalize(true);
1264
- return;
1265
- }
1266
- if (input === 'L') {
1267
- const currentKey = cursorRowIdx >= 0 && cursorRowIdx < visibleRowsMemo.length
1268
- ? (components[visibleRowsMemo[cursorRowIdx]?.itemIdx ?? -1]?.key ?? null)
1269
- : null;
1270
- const nextView = columnOneView === 'grouped' ? 'flat' : 'grouped';
1271
- const nextRows = buildVisibleRows({
1272
- items: groupedItemsMemo,
1273
- cycleParticipants: cycleView.structural,
1274
- expandedGroups,
1275
- viewMode: nextView,
1276
- graph: sidebarGraph,
1277
- });
1278
- let nextCursor = 0;
1279
- if (currentKey) {
1280
- for (let i = 0; i < nextRows.length; i++) {
1281
- const r = nextRows[i];
1282
- if (r.itemIdx < 0)
1283
- continue;
1284
- if (components[r.itemIdx]?.key === currentKey) {
1285
- nextCursor = i;
1286
- break;
1287
- }
1288
- }
1289
- }
1290
- const nextScroll = nextCursor < sidebarScrollOffset
1291
- ? nextCursor
1292
- : nextCursor >= sidebarScrollOffset + visibleCount
1293
- ? nextCursor - visibleCount + 1
1294
- : sidebarScrollOffset;
1295
- setColumnOneView(nextView);
1296
- setNav({ cursorRowIdx: nextCursor, sidebarScrollOffset: nextScroll });
1297
- return;
1298
- }
1299
- if (input === 'a') {
1300
- const current = components[selectedIdx];
1301
- if (!current)
1302
- return;
1303
- const cascade = computeAcceptCascade(current.key, componentGraph);
1304
- const next = components.map((c) => cascade.has(c.key) ? { ...c, status: 'accepted' } : c);
1305
- setComponents(next);
1306
- setFinalizeError(null);
1307
- recomputeCycles(next);
1308
- pushHistorySnapshot(next, autoRejected, undoSnapshot, 'accept-cascade');
1309
- return;
1310
- }
1311
- if (input === 'r') {
1312
- const current = components[selectedIdx];
1313
- if (!current)
1314
- return;
1315
- handleRejectComponent(current.key);
1316
- return;
1317
- }
1318
- if (input === 'A') {
1319
- const next = components.map((c) => c.status === 'needs-review' ? { ...c, status: 'accepted' } : c);
1320
- setComponents(next);
1321
- setFinalizeError(null);
1322
- pushHistorySnapshot(next, autoRejected, undoSnapshot, 'bulk-accept');
1323
- return;
1324
- }
1325
- if (input === 'E') {
1326
- const roots = new Set();
1327
- for (const [name, closure] of closures.entries()) {
1328
- if (closure.nodes.length > 1)
1329
- roots.add(name);
1330
- }
1331
- for (const name of cycleView.structural)
1332
- roots.add(name);
1333
- setExpandedGroups(roots);
1334
- return;
1335
- }
1336
- if (input === 'C') {
1337
- setExpandedGroups(new Set());
1338
- return;
1339
- }
1340
- if (input === 'J') {
1341
- setShowJson((prev) => !prev);
1342
- setJsonScrollOffset(0);
1343
- pendingGRef.current = false;
1344
- return;
1345
- }
1346
- if (key.return) {
1347
- const current = components[selectedIdx];
1348
- if (!current)
1349
- return;
1350
- const rs = renderStatusByKey.get(current.key);
1351
- if (!rs || rs.isOwn)
1352
- return;
1353
- for (const closure of closures.values()) {
1354
- if (!closure.nodes.some((n) => n.name === current.key))
1355
- continue;
1356
- const target = pickDrillTarget(current.key, closure, directIssues);
1357
- if (target && target !== current.key) {
1358
- jumpCursorToName(target);
1359
- }
1360
- break;
1361
- }
1362
- return;
1363
- }
1364
- if (key.upArrow || input === 'k') {
1365
- setNav(({ cursorRowIdx: prev, sidebarScrollOffset: off }) => {
1366
- const positions = selectableRowPositions;
1367
- if (positions.length === 0)
1368
- return { cursorRowIdx: prev, sidebarScrollOffset: off };
1369
- const pos = positions.indexOf(prev);
1370
- const currentSelectableIdx = pos >= 0
1371
- ? pos
1372
- : Math.max(0, positions.reduce((acc, p, i) => (p <= prev ? i : acc), 0));
1373
- const nextSelectableIdx = Math.max(0, currentSelectableIdx - 1);
1374
- const newRow = positions[nextSelectableIdx] ?? prev;
1375
- return { cursorRowIdx: newRow, sidebarScrollOffset: Math.min(off, newRow) };
1376
- });
1377
- setJsonScrollOffset(0);
1378
- setDraftValue('');
1379
- setSaveError(null);
1380
- setPendingEditorFocus(null);
1381
- }
1382
- else if (key.downArrow || input === 'j') {
1383
- setNav(({ cursorRowIdx: prev, sidebarScrollOffset: off }) => {
1384
- const positions = selectableRowPositions;
1385
- if (positions.length === 0)
1386
- return { cursorRowIdx: prev, sidebarScrollOffset: off };
1387
- const pos = positions.indexOf(prev);
1388
- const currentSelectableIdx = pos >= 0
1389
- ? pos
1390
- : Math.max(0, positions.reduce((acc, p, i) => (p <= prev ? i : acc), 0));
1391
- const nextSelectableIdx = Math.min(positions.length - 1, currentSelectableIdx + 1);
1392
- const newRow = positions[nextSelectableIdx] ?? prev;
1393
- const nextOff = newRow >= off + visibleCount ? newRow - visibleCount + 1 : off;
1394
- return { cursorRowIdx: newRow, sidebarScrollOffset: nextOff };
1395
- });
1396
- setJsonScrollOffset(0);
1397
- setDraftValue('');
1398
- setSaveError(null);
1399
- setPendingEditorFocus(null);
1400
- }
1401
- });
1402
- if (loading) {
1403
- return (_jsx(Box, { paddingX: 2, paddingY: 1, children: _jsx(Text, { dimColor: true, children: "Loading generated definitions..." }) }));
1404
- }
1405
- if (loadError) {
1406
- return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: PALETTE.error, children: loadError }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "[q / Enter / Esc] Quit" })] }));
1407
- }
1408
- if (showHelp) {
1409
- return _jsx(HelpOverlay, { sections: HELP_SECTIONS, onClose: () => setShowHelp(false) });
1410
- }
1411
- const renderBreakOverlay = (width) => {
1412
- const highlightedCycle = slotCycles[cyclesCursor];
1413
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.warning, paddingX: 1, width: width, children: [_jsx(Text, { bold: true, color: PALETTE.warning, children: `BREAK CYCLE ${cyclesCursor + 1} — remove a slot edge or reject a member` }), highlightedCycle &&
1414
- (() => {
1415
- const segs = formatCyclePathSegments(highlightedCycle);
1416
- return (_jsxs(Text, { children: [' ', segs.map((seg, si) => seg.kind === 'slot' ? (_jsx(Text, { color: PALETTE.info, children: seg.text }, si)) : seg.kind === 'arrow' ? (_jsx(Text, { dimColor: true, children: seg.text }, si)) : (_jsx(Text, { color: PALETTE.warning, children: seg.text }, si)))] }));
1417
- })(), _jsx(Text, { dimColor: true, children: highlightedCycle
1418
- ? 'Deleting an edge removes it from $allowedComponents (undo with Ctrl+Z).'
1419
- : 'No cycle highlighted.' }), _jsx(Text, { children: " " }), breakEdges.length > 0 && _jsx(Text, { dimColor: true, children: 'remove slot edge:' }), breakEdges.map((edge, idx) => {
1420
- const isCursor = idx === breakCursor;
1421
- return (_jsx(Text, { inverse: isCursor, children: `${isCursor ? '▶' : ' '} remove '${edge.toComponent}' from ${edge.fromComponent}.$slots.${edge.slotName}.$allowedComponents` }, `break-${idx}`));
1422
- }), breakConfirm ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: PALETTE.warning, children: 'Delete this slot edge? [y] confirm [n] cancel' })] })) : (_jsx(Text, { dimColor: true, children: '[↑↓/j/k] move [Enter] delete [x/Esc] close' }))] }));
1423
- };
1424
- const breakOverlayFullScreen = breakPanel.isOpen &&
1425
- shouldBreakOverlayGoFullScreen({
1426
- rows: stdout?.rows ?? FALLBACK_ROWS,
1427
- edgeCount: breakEdges.length,
1428
- });
1429
- if (breakOverlayFullScreen) {
1430
- return renderBreakOverlay();
1431
- }
1432
- const selected = components[selectedIdx] ?? null;
1433
- const selectedJson = selected ? JSON.stringify({ [selected.key]: selected.entry }, null, 2) : '';
1434
- const isEmpty = (c) => Object.keys(c.entry.$properties).length === 0 && Object.keys(c.entry.$slots ?? {}).length === 0;
1435
- const emptyCount = components.filter(isEmpty).length;
1436
- const cycleParticipantSet = cycleView.structural;
1437
- const sidebarSuffix = (c) => {
1438
- if (cycleParticipantSet.has(c.key))
1439
- return ' (cycle)';
1440
- if (isEmpty(c))
1441
- return ' (empty)';
1442
- return '';
1443
- };
1444
- const groupedItems = groupedItemsMemo;
1445
- const previewAnnotationByKey = previewAnnotations;
1446
- const longestName = components.reduce((m, c) => {
1447
- const suffixLen = sidebarSuffix(c).length;
1448
- const groupOverhead = 12; // "▸ (99 deps) ✗"
1449
- return Math.max(m, c.key.length + Math.max(suffixLen, groupOverhead));
1450
- }, 0);
1451
- const sidebarWidthCap = computeSidebarWidth(terminalWidth);
1452
- const sidebarWidth = Math.min(Math.max(longestName + 9, 18), sidebarWidthCap);
1453
- const panelWidth = Math.max(10, terminalWidth - sidebarWidth - 4);
1454
- const projectSlotGraph = components.map((c) => ({
1455
- name: c.key,
1456
- slots: Object.entries(c.entry.$slots ?? {}).map(([slotName, slotDef]) => ({
1457
- name: slotName,
1458
- allowedComponents: Array.isArray(slotDef?.$allowedComponents)
1459
- ? slotDef.$allowedComponents.filter((v) => typeof v === 'string')
1460
- : [],
1461
- })),
1462
- }));
1463
- const hasGroupRoots = (() => {
1464
- for (const c of closures.values())
1465
- if (c.nodes.length > 1)
1466
- return true;
1467
- return false;
1468
- })();
1469
- const accepted = components.filter((c) => c.status === 'accepted').length;
1470
- const rejected = components.filter((c) => c.status === 'rejected').length;
1471
- const needsReview = components.filter((c) => c.status === 'needs-review').length;
1472
- const propCount = selected ? Object.keys(selected.entry.$properties).length : 0;
1473
- const slotCount = selected?.entry.$slots ? Object.keys(selected.entry.$slots).length : 0;
1474
- return (_jsxs(Box, { flexDirection: "column", children: [showFinalize && (_jsx(FinalizeDialog, { accepted: accepted, rejected: rejected, needsReview: needsReview, removed: finalizePreview.removed, previewStatus: finalizePreview.status, removedScrollOffset: finalizePreview.scrollOffset, onConfirm: handleFinalizeConfirm, onCancel: () => setShowFinalize(false) })), showQuit && _jsx(QuitDialog, { hasUnsavedDrafts: false, onConfirm: onQuit, onCancel: () => setShowQuit(false) }), showUnsavedWarning && !dialogOpen && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.warning, paddingX: 1, children: [_jsx(Text, { bold: true, color: PALETTE.warning, children: "Unsaved changes" }), _jsx(Text, { children: "You have unsaved edits in the current field editor." }), _jsx(Text, { children: " " }), _jsx(Text, { children: ' [Enter] Save and continue' }), _jsx(Text, { children: ' [Esc] Discard changes and continue' }), _jsx(Text, { children: ' [Tab] Stay in the panel' })] })), showReloadDialog && !dialogOpen && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.warning, paddingX: 1, children: [_jsx(Text, { bold: true, color: PALETTE.warning, children: "Reload from saved state?" }), _jsx(Text, { children: "Unsaved in-memory changes will be lost." }), _jsx(Text, { children: " " }), _jsx(Text, { children: ' [Enter] Confirm' }), _jsx(Text, { children: ' [Esc] Cancel' })] })), removedComponents.length > 0 && !dialogOpen && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.error, paddingX: 1, children: [_jsx(Text, { bold: true, color: PALETTE.error, children: removedComponentsHeader(removedComponents.length, true) }), !removedBannerCollapsed && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), removedComponents.map((rc) => (_jsx(Text, { children: removedComponentLine(rc) }, rc.id)))] }))] })), breakingChanges.length > 0 && !dialogOpen && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: PALETTE.warning, children: `[b] ${breakingChanges.length} breaking change${breakingChanges.length === 1 ? '' : 's'}` }) })), breakingPanel.isOpen &&
1475
- breakingDetailOpen &&
1476
- !dialogOpen &&
1477
- (() => {
1478
- const row = breakingRows[breakingCursor];
1479
- const comp = row ? breakingChanges.find((b) => b.componentName === row.componentName) : undefined;
1480
- if (!comp)
1481
- return null;
1482
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.warning, paddingX: 1, children: [_jsx(Text, { bold: true, color: PALETTE.warning, children: `Breaking changes — ${comp.componentName}` }), comp.impact && (_jsx(Text, { dimColor: true, children: ` affects ${comp.impact.affectedExperiences} experience${comp.impact.affectedExperiences === 1 ? '' : 's'}, ${comp.impact.affectedFragments} fragment${comp.impact.affectedFragments === 1 ? '' : 's'}` })), comp.changes.length === 0 ? (_jsx(Text, { dimColor: true, children: ' (no enumerated changes)' })) : (comp.changes.map((change, ci) => (_jsx(Text, { children: ` • ${formatBreakingChange(change, comp.current)}` }, `bd-detail-${ci}`)))), _jsx(Text, { dimColor: true, children: '[D/Esc] close detail' })] }));
1483
- })(), cyclePanel.isOpen &&
1484
- !dialogOpen &&
1485
- (() => {
1486
- const PANEL_H = 20;
1487
- const lines = [];
1488
- lines.push(_jsx(Text, { bold: true, color: PALETTE.warning, children: `SLOT DEPENDENCY CYCLES (${slotCycles.length})` }, "cyc-title"));
1489
- lines.push(_jsx(Text, { dimColor: true, children: 'push will fail until these are resolved' }, "cyc-sub"));
1490
- lines.push(_jsx(Text, { dimColor: true, children: 'To fix: reject a cycle member, or break the cycle by removing a slot edge.' }, "cyc-guide"));
1491
- lines.push(_jsx(Text, { children: " " }, "cyc-space"));
1492
- slotCycles.forEach((cycle, idx) => {
1493
- const nodeCount = new Set(cycle.path).size;
1494
- const isCursor = idx === cyclesCursor;
1495
- lines.push(_jsx(Text, { bold: true, inverse: isCursor, children: `${isCursor ? '▶' : ' '} Cycle ${idx + 1} (${nodeCount} component${nodeCount === 1 ? '' : 's'}):` }, `cyc-h-${idx}`));
1496
- const segs = formatCyclePathSegments(cycle, 16);
1497
- lines.push(_jsxs(Text, { children: [' ', segs.map((seg, si) => seg.kind === 'slot' ? (_jsx(Text, { color: PALETTE.info, children: seg.text }, si)) : seg.kind === 'arrow' ? (_jsx(Text, { dimColor: true, children: seg.text }, si)) : (_jsx(Text, { children: seg.text }, si)))] }, `cyc-p-${idx}`));
1498
- if (cycle.suggestedBreak) {
1499
- const b = cycle.suggestedBreak;
1500
- lines.push(_jsx(Text, { dimColor: true, children: ` Suggested fix: remove '${b.toComponent}' from ${b.fromComponent}.$slots.${b.slotName}.$allowedComponents` }, `cyc-f-${idx}`));
1501
- }
1502
- lines.push(_jsx(Text, { children: " " }, `cyc-s-${idx}`));
1503
- });
1504
- const visible = lines.slice(cyclePanelScroll, cyclePanelScroll + PANEL_H);
1505
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.warning, paddingX: 1, children: [visible, _jsx(Text, { dimColor: true, children: '[↑↓/j/k] move [Enter] jump [x] break cycle [c/q/Esc] close' })] }));
1506
- })(), !dialogOpen &&
1507
- livePreview &&
1508
- (() => {
1509
- const counts = { new: 0, changed: 0, removed: 0, breaking: 0 };
1510
- for (const v of previewAnnotations.values()) {
1511
- counts[v] = (counts[v] ?? 0) + 1;
1512
- }
1513
- const hasCounts = counts.new + counts.changed + counts.removed + counts.breaking > 0;
1514
- if (livePreviewHook.disabled) {
1515
- return _jsx(Text, { dimColor: true, children: 'Preview: disabled (creds rejected)' });
1516
- }
1517
- if (livePreviewHook.status === 'running' && !hasCounts) {
1518
- return _jsx(Text, { dimColor: true, children: `Preview: ${livePreviewSpinner} running...` });
1519
- }
1520
- if (!hasCounts)
1521
- return null;
1522
- return (_jsxs(Box, { children: [_jsx(Text, { children: 'Preview: ' }), _jsx(Text, { color: PALETTE.success, children: `${counts.new} new` }), _jsx(Text, { children: ' · ' }), _jsx(Text, { color: PALETTE.warning, children: `${counts.changed} changed` }), _jsx(Text, { children: ' · ' }), _jsx(Text, { dimColor: true, children: `${counts.removed} removed` }), _jsx(Text, { children: ' · ' }), _jsx(Text, { color: PALETTE.error, bold: true, children: `${counts.breaking} breaking` })] }));
1523
- })(), !dialogOpen &&
1524
- autoRejected.length > 0 &&
1525
- (() => {
1526
- const stillRejected = autoRejected.filter((name) => {
1527
- const c = components.find((x) => x.key === name);
1528
- return c?.status === 'rejected';
1529
- });
1530
- if (stillRejected.length === 0)
1531
- return null;
1532
- const participantSet = cycleView.structural;
1533
- const members = stillRejected.filter((n) => participantSet.has(n)).sort();
1534
- const ancestors = stillRejected.filter((n) => !participantSet.has(n)).sort();
1535
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: PALETTE.error, paddingX: 1, children: [_jsx(Text, { color: PALETTE.error, bold: true, children: `Cyclic manifest — auto-rejected ${stillRejected.length} component${stillRejected.length === 1 ? '' : 's'}:` }), members.length > 0 && _jsx(Text, { color: PALETTE.error, children: ` Cycle members: ${members.join(', ')}` }), ancestors.length > 0 && _jsx(Text, { color: PALETTE.error, children: ` Ancestors: ${ancestors.join(', ')}` }), _jsx(Text, { dimColor: true, children: undoSnapshot
1536
- ? ' [Ctrl+Z] undo · [r]/[a] manually toggle · [F] continue'
1537
- : ' [r]/[a] manually toggle · [F] continue' })] }));
1538
- })(), !dialogOpen && emptyCount > 0 && (_jsx(Text, { color: PALETTE.warning, children: `⚠ ${emptyCount} component${emptyCount === 1 ? '' : 's'} had no classifiable props — review with care` })), !dialogOpen && finalizeError && _jsx(Text, { color: PALETTE.error, children: `⚠ ${finalizeError}` }), !dialogOpen && (_jsxs(Box, { children: [breakingPanel.isOpen ? (_jsx(GotoBanner, { title: "Breaking changes", rows: breakingRows.map((r) => ({
1539
- label: r.label,
1540
- jumpTarget: r.componentName,
1541
- })), cursor: breakingCursor, maxRows: panelMaxRows, width: sidebarWidth, footerHint: "[\u2191/\u2193] move \u00B7 [Enter] jump \u00B7 [D] detail \u00B7 [Esc] close" })) : lineagePanel.isOpen && focusedComponentKey ? (_jsx(LineagePanel, { focusedComponentKey: focusedComponentKey, entries: lineageEntries, cursor: lineageCursor, jumpables: lineageJumpables, maxRows: panelMaxRows, width: sidebarWidth })) : (_jsx(GroupedSidebar, { items: groupedItems, cycleParticipants: cycleParticipantSet, selectedIdx: selectedIdx, selectedRowIdx: cursorRowIdx, onSelect: (idx) => {
1542
- for (let i = 0; i < visibleRowsMemo.length; i++) {
1543
- if (visibleRowsMemo[i].itemIdx === idx) {
1544
- jumpCursorToRow(i);
1545
- return;
1546
- }
1547
- }
1548
- setJsonScrollOffset(0);
1549
- }, expandedGroups: expandedGroups, onToggleExpanded: (rootName) => {
1550
- setExpandedGroups((prev) => {
1551
- const next = new Set(prev);
1552
- if (next.has(rootName))
1553
- next.delete(rootName);
1554
- else
1555
- next.add(rootName);
1556
- return next;
1557
- });
1558
- }, width: sidebarWidth, focused: sidebarFocused, renderStatusByKey: renderStatusByKey, previewAnnotationByKey: previewAnnotationByKey, selectionStateByKey: selectionStateByKey, scrollOffset: sidebarScrollOffset, visibleCount: visibleCount, dimPredicate: dimPredicate, visibleRows: visibleRowsMemo, viewMode: columnOneView, graph: sidebarGraph })), _jsx(Box, { flexGrow: 1, paddingLeft: 1, flexDirection: "column", children: selected ? (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: selected.key }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { dimColor: true, children: [propCount, " prop", propCount !== 1 ? 's' : '', slotCount > 0 ? ` · ${slotCount} slot${slotCount !== 1 ? 's' : ''}` : '', ' ', sidebarFocused ? '[Tab] focus panel' : '[Tab] focus list'] })] }), panelOpen === 'prop-rationale' ? ((() => {
1559
- const rows = [
1560
- ...(componentRationale?.props ?? []).map((p) => ({
1561
- name: p.name,
1562
- kind: 'prop',
1563
- rationale: p.rationale ?? '',
1564
- })),
1565
- ...(componentRationale?.slots ?? []).map((s) => ({
1566
- name: s.name,
1567
- kind: 'slot',
1568
- rationale: s.rationale ?? '',
1569
- })),
1570
- ];
1571
- return (_jsx(RationalePanel, { componentName: componentRationale?.name ?? selected.key, rows: rows, scrollOffset: panelScrollOffset, width: panelWidth, height: PANEL_HEIGHT, active: true }));
1572
- })()) : panelOpen === 'component-rationale' ? (_jsx(ComponentRationalePanel, { data: componentRationale ?? {
1573
- name: selected.key,
1574
- description: null,
1575
- descriptionRationale: null,
1576
- propsRationale: null,
1577
- slotsRationale: null,
1578
- props: [],
1579
- slots: [],
1580
- }, scrollOffset: panelScrollOffset, width: panelWidth, height: PANEL_HEIGHT, active: true })) : panelOpen === 'source' ? ((() => {
1581
- const path = reviewMetadata?.sourcePath ?? null;
1582
- const src = reviewMetadata?.componentSource ?? null;
1583
- const headerPath = path ?? '<unknown source path>';
1584
- const lines = src ? src.split('\n').slice(panelScrollOffset, panelScrollOffset + PANEL_HEIGHT) : [];
1585
- return (_jsxs(Box, { flexDirection: "column", width: panelWidth, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: `source: ${headerPath}` }), src ? (lines.map((ln, i) => (_jsx(Text, { dimColor: true, children: ln }, `source-line-${i}`)))) : (_jsx(Text, { dimColor: true, children: '(no source captured)' })), _jsx(Text, { dimColor: true, children: '[s/Esc] close' })] }));
1586
- })()) : showJson ? (_jsx(JsonPanel, { label: "GENERATED DEFINITION (read-only)", value: selectedJson, scrollOffset: jsonScrollOffset, width: panelWidth, height: PANEL_HEIGHT, active: !sidebarFocused })) : (_jsx(FieldEditor, { value: draftValue || selectedJson, width: panelWidth, height: PANEL_HEIGHT, active: !sidebarFocused, onChange: setDraftValue, onSave: handleEditSave, onDiscard: handleEditDiscard, onExit: () => setSidebarFocused(true), metadata: reviewMetadata
1587
- ? {
1588
- sourcePath: reviewMetadata.sourcePath,
1589
- componentSource: reviewMetadata.componentSource,
1590
- props: reviewMetadata.props,
1591
- }
1592
- : undefined, onTogglePropRationale: () => {
1593
- setPanelOpen('prop-rationale');
1594
- setPanelScrollOffset(() => 0);
1595
- }, propRationaleKey: "p", componentRationaleKey: "P", onToggleComponentRationale: () => {
1596
- setPanelOpen('component-rationale');
1597
- setPanelScrollOffset(() => 0);
1598
- }, onToggleSourceExternal: () => {
1599
- setPanelOpen('source');
1600
- setPanelScrollOffset(() => 0);
1601
- }, onTextEntryActiveChange: setTextEntryActive, projectSlotGraph: projectSlotGraph, currentComponentName: selected.key, onDirtyChange: setEditorDirty, discardTrigger: discardTrigger, initialFocusTarget: pendingEditorFocus && pendingEditorFocus.componentName === selected.key
1602
- ? pendingEditorFocus.target
1603
- : undefined }, pendingEditorFocus && pendingEditorFocus.componentName === selected.key
1604
- ? `${selected.key}::${pendingEditorFocus.target.kind}:${pendingEditorFocus.target.name}`
1605
- : selected.key)), saveError && _jsx(Text, { color: PALETTE.error, children: '✗ ' + saveError }), _jsxs(Text, { dimColor: true, children: [sidebarFocused
1606
- ? hasGroupRoots
1607
- ? ' [Space] expand/collapse group [E/C] expand/collapse all'
1608
- : ''
1609
- : showJson
1610
- ? ' [j/k] scroll [Ctrl+u/d] half-page [gg/G] top/bottom [Tab] focus list'
1611
- : ' [Tab] focus list (edit fields)', livePreviewHook.status === 'running' && _jsx(Text, { children: ` ${livePreviewSpinner} live preview` }), livePreviewHook.disabled && _jsx(Text, { children: ' · live preview disabled' })] })] })) : (_jsx(Text, { dimColor: true, children: "No component selected" })) })] })), breakPanel.isOpen && !breakOverlayFullScreen && !dialogOpen && renderBreakOverlay(), !dialogOpen && slotCycles.length > 0 && !cyclePanel.isOpen && !breakPanel.isOpen && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: PALETTE.warning, children: `⚠ ${slotCycles.length} slot dependency cycle${slotCycles.length === 1 ? '' : 's'} detected — push will fail` }), slotCycles.slice(0, 3).map((cycle, idx) => {
1612
- const segs = formatCyclePathSegments(cycle);
1613
- return (_jsxs(Text, { color: PALETTE.warning, children: [' Cycle: ', segs.map((seg, si) => seg.kind === 'slot' ? (_jsx(Text, { color: PALETTE.info, children: seg.text }, si)) : seg.kind === 'arrow' ? (_jsx(Text, { dimColor: true, children: seg.text }, si)) : (_jsx(Text, { color: PALETTE.warning, children: seg.text }, si)))] }, `cyc-banner-${idx}`));
1614
- }), slotCycles.length > 3 && _jsx(Text, { color: PALETTE.warning, children: ` …${slotCycles.length - 3} more` }), _jsx(Text, { dimColor: true, children: ' press [c] for detail' })] })), !dialogOpen && searchOpen && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [`/${searchQuery}`, _jsx(Text, { color: PALETTE.info, children: '▎' }), searchQuery && _jsx(Text, { dimColor: true, children: ` (${searchMatchCount}/${components.length} matches)` })] }), autocompleteCandidates.length > 1 && (_jsx(Text, { dimColor: true, children: ` possibilities: ${autocompleteCandidates.join(' · ').slice(0, 120)}` }))] })), !dialogOpen && !searchOpen && searchQuery && (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: `/${searchQuery} (${searchMatchCount}/${components.length} matches) · [Esc] clear` }) })), !dialogOpen && sidebarFocused && (_jsxs(Box, { columnGap: 2, flexWrap: "wrap", children: [legendEntry('[j/k]', 'move'), legendEntry('[a]', 'accept'), legendEntry('[r]', 'reject'), legendEntry('[A]', 'accept all'), legendEntry('[F]', 'finalize'), legendEntry('[L]', 'flat', columnOneView === 'flat'), legendEntry('[l]', 'lineage', lineagePanel.isOpen), legendEntry('[i]', 'focus lineage', jumpFilterTarget !== null), legendEntry('[w]', 'only breaking', activeFilters.has('broken')), slotCycles.length > 0 && legendEntry('[o]', 'only cycles', activeFilters.has('cycles')), slotCycles.length > 0 && legendEntry('[c]', 'cycle list', cyclePanel.isOpen), legendEntry('[p]', 'prop rationale', panelOpen === 'prop-rationale'), legendEntry('[P]', 'component rationale', panelOpen === 'component-rationale'), legendEntry('[s]', 'source', panelOpen === 'source'), legendEntry('[J]', showJson ? 'hide JSON' : 'show JSON', showJson), breakingChanges.length > 0 && legendEntry('[b]', 'see breaking changes', breakingPanel.isOpen), removedComponents.length > 0 &&
1615
- legendEntry('[d]', removedBannerCollapsed ? 'show removed' : 'hide removed', !removedBannerCollapsed), legendEntry('[/]', 'search', searchOpen || searchQuery.length > 0), legendEntry('[Tab]', 'focus panel'), legendEntry('[Ctrl+Z]', 'undo'), legendEntry('[Ctrl+Y]', 'redo'), legendEntry('[Ctrl+R]', 'reload'), legendEntry('[?]', 'help'), legendEntry('[q]', 'quit')] })), !dialogOpen && (_jsx(StatusBar, { accepted: accepted, rejected: rejected, reviewed: 0, needsReview: needsReview, onApproveAll: () => {
1616
- setComponents((prev) => prev.map((c) => (c.status === 'needs-review' ? { ...c, status: 'accepted' } : c)));
1617
- }, onFinalize: () => setShowFinalize(true) }))] }));
1618
- }