@contentful/experience-design-system-cli 2.14.2 → 2.15.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 (193) hide show
  1. package/README.md +64 -2
  2. package/dist/package.json +3 -2
  3. package/dist/src/analyze/command.d.ts +11 -0
  4. package/dist/src/analyze/command.js +298 -3
  5. package/dist/src/analyze/composite-closure.d.ts +25 -0
  6. package/dist/src/analyze/composite-closure.js +0 -0
  7. package/dist/src/analyze/composition/agent-parser/author-prompt.d.ts +4 -0
  8. package/dist/src/analyze/composition/agent-parser/author-prompt.js +22 -0
  9. package/dist/src/analyze/composition/agent-parser/extract-parser.d.ts +8 -0
  10. package/dist/src/analyze/composition/agent-parser/extract-parser.js +44 -0
  11. package/dist/src/analyze/composition/agent-parser/load-prompt.d.ts +7 -0
  12. package/dist/src/analyze/composition/agent-parser/load-prompt.js +31 -0
  13. package/dist/src/analyze/composition/agent-parser/resolve-via-parser.d.ts +46 -0
  14. package/dist/src/analyze/composition/agent-parser/resolve-via-parser.js +88 -0
  15. package/dist/src/analyze/composition/agent-parser/sandbox.d.ts +45 -0
  16. package/dist/src/analyze/composition/agent-parser/sandbox.js +187 -0
  17. package/dist/src/analyze/composition/apply-mapping.d.ts +22 -0
  18. package/dist/src/analyze/composition/apply-mapping.js +67 -0
  19. package/dist/src/analyze/composition/candidate-critic-agent.d.ts +7 -0
  20. package/dist/src/analyze/composition/candidate-critic-agent.js +39 -0
  21. package/dist/src/analyze/composition/candidate-critic.d.ts +32 -0
  22. package/dist/src/analyze/composition/candidate-critic.js +63 -0
  23. package/dist/src/analyze/composition/candidate-files.d.ts +34 -0
  24. package/dist/src/analyze/composition/candidate-files.js +131 -0
  25. package/dist/src/analyze/composition/composition-cache-key.d.ts +19 -0
  26. package/dist/src/analyze/composition/composition-cache-key.js +19 -0
  27. package/dist/src/analyze/composition/interchange-schema.d.ts +37 -0
  28. package/dist/src/analyze/composition/interchange-schema.js +61 -0
  29. package/dist/src/analyze/composition/merge-edges.d.ts +19 -0
  30. package/dist/src/analyze/composition/merge-edges.js +50 -0
  31. package/dist/src/analyze/composition/parse-map-edges.d.ts +16 -0
  32. package/dist/src/analyze/composition/parse-map-edges.js +55 -0
  33. package/dist/src/analyze/composition/resolve-mapping-cli.d.ts +23 -0
  34. package/dist/src/analyze/composition/resolve-mapping-cli.js +29 -0
  35. package/dist/src/analyze/composition/resolve-mapping.d.ts +59 -0
  36. package/dist/src/analyze/composition/resolve-mapping.js +94 -0
  37. package/dist/src/analyze/cycle-detection.d.ts +27 -0
  38. package/dist/src/analyze/cycle-detection.js +234 -0
  39. package/dist/src/analyze/cycle-view.d.ts +7 -0
  40. package/dist/src/analyze/cycle-view.js +28 -0
  41. package/dist/src/analyze/fuzzy-search.d.ts +3 -0
  42. package/dist/src/analyze/fuzzy-search.js +55 -0
  43. package/dist/src/analyze/issue-inheritance.d.ts +12 -0
  44. package/dist/src/analyze/issue-inheritance.js +108 -0
  45. package/dist/src/analyze/lineage.d.ts +29 -0
  46. package/dist/src/analyze/lineage.js +0 -0
  47. package/dist/src/analyze/scope-gate-cascade.d.ts +11 -0
  48. package/dist/src/analyze/scope-gate-cascade.js +170 -0
  49. package/dist/src/analyze/search-neighborhood.d.ts +3 -0
  50. package/dist/src/analyze/search-neighborhood.js +56 -0
  51. package/dist/src/analyze/select/tui/App.js +5 -16
  52. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +0 -4
  53. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +3 -15
  54. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +29 -38
  55. package/dist/src/analyze/select/tui/components/FieldEditor.js +309 -185
  56. package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +13 -1
  57. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +23 -2
  58. package/dist/src/analyze/select/tui/components/GotoBanner.d.ts +31 -0
  59. package/dist/src/analyze/select/tui/components/GotoBanner.js +30 -0
  60. package/dist/src/analyze/select/tui/components/GroupedSidebar.d.ts +91 -0
  61. package/dist/src/analyze/select/tui/components/GroupedSidebar.js +497 -0
  62. package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +13 -1
  63. package/dist/src/analyze/select/tui/components/HelpOverlay.js +9 -3
  64. package/dist/src/analyze/select/tui/components/JsonEditor.js +3 -3
  65. package/dist/src/analyze/select/tui/components/LineagePanel.d.ts +14 -0
  66. package/dist/src/analyze/select/tui/components/LineagePanel.js +22 -0
  67. package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +0 -7
  68. package/dist/src/analyze/select/tui/components/RationalePanel.js +3 -15
  69. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +0 -6
  70. package/dist/src/analyze/select/tui/components/Sidebar.js +11 -27
  71. package/dist/src/analyze/select/tui/components/StatusBar.js +2 -1
  72. package/dist/src/analyze/select/tui/components/removed-components-text.d.ts +7 -0
  73. package/dist/src/analyze/select/tui/components/removed-components-text.js +11 -0
  74. package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +1 -5
  75. package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +8 -10
  76. package/dist/src/analyze/select/tui/theme.d.ts +15 -0
  77. package/dist/src/analyze/select/tui/theme.js +28 -0
  78. package/dist/src/analyze/selection-cascade.d.ts +3 -0
  79. package/dist/src/analyze/selection-cascade.js +19 -0
  80. package/dist/src/analyze/slot-graph.d.ts +11 -0
  81. package/dist/src/analyze/slot-graph.js +17 -0
  82. package/dist/src/apply/api-client.d.ts +2 -1
  83. package/dist/src/apply/api-client.js +40 -6
  84. package/dist/src/apply/command.d.ts +17 -1
  85. package/dist/src/apply/command.js +69 -11
  86. package/dist/src/apply/error-parser.d.ts +16 -0
  87. package/dist/src/apply/error-parser.js +117 -0
  88. package/dist/src/apply/tui/ServerPreviewView.js +3 -1
  89. package/dist/src/credentials-store.d.ts +6 -0
  90. package/dist/src/credentials-store.js +5 -1
  91. package/dist/src/generate/agent-runner.d.ts +6 -0
  92. package/dist/src/generate/agent-runner.js +24 -8
  93. package/dist/src/import/command.js +69 -31
  94. package/dist/src/import/cycle-auto-reject.d.ts +4 -0
  95. package/dist/src/import/cycle-auto-reject.js +14 -0
  96. package/dist/src/import/orchestrator.d.ts +20 -7
  97. package/dist/src/import/orchestrator.js +139 -35
  98. package/dist/src/import/strip-allowed-components.d.ts +18 -0
  99. package/dist/src/import/strip-allowed-components.js +22 -0
  100. package/dist/src/import/tui/CustomPromptBanner.d.ts +0 -5
  101. package/dist/src/import/tui/CustomPromptBanner.js +2 -1
  102. package/dist/src/import/tui/WizardApp.d.ts +10 -74
  103. package/dist/src/import/tui/WizardApp.js +234 -246
  104. package/dist/src/import/tui/ai-flag.d.ts +5 -0
  105. package/dist/src/import/tui/ai-flag.js +6 -0
  106. package/dist/src/import/tui/autocomplete.d.ts +5 -0
  107. package/dist/src/import/tui/autocomplete.js +32 -0
  108. package/dist/src/import/tui/components/AutoFilterBanner.d.ts +10 -0
  109. package/dist/src/import/tui/components/AutoFilterBanner.js +19 -0
  110. package/dist/src/import/tui/components/CounterStrip.d.ts +12 -0
  111. package/dist/src/import/tui/components/CounterStrip.js +12 -0
  112. package/dist/src/import/tui/components/LegendEntry.d.ts +2 -0
  113. package/dist/src/import/tui/components/LegendEntry.js +6 -0
  114. package/dist/src/import/tui/cycle-panel-scroll.d.ts +8 -0
  115. package/dist/src/import/tui/cycle-panel-scroll.js +33 -0
  116. package/dist/src/import/tui/final-review-host.d.ts +3 -7
  117. package/dist/src/import/tui/final-review-host.js +9 -4
  118. package/dist/src/import/tui/group-collapse.d.ts +2 -0
  119. package/dist/src/import/tui/group-collapse.js +11 -0
  120. package/dist/src/import/tui/history.d.ts +22 -0
  121. package/dist/src/import/tui/history.js +53 -0
  122. package/dist/src/import/tui/hooks/useLineage.d.ts +32 -0
  123. package/dist/src/import/tui/hooks/useLineage.js +50 -0
  124. package/dist/src/import/tui/hooks/useOverlayPanel.d.ts +13 -0
  125. package/dist/src/import/tui/hooks/useOverlayPanel.js +27 -0
  126. package/dist/src/import/tui/lineage-layout.d.ts +75 -0
  127. package/dist/src/import/tui/lineage-layout.js +82 -0
  128. package/dist/src/import/tui/runLivePreview.d.ts +11 -0
  129. package/dist/src/import/tui/runLivePreview.js +7 -1
  130. package/dist/src/import/tui/scope-gate-columns.d.ts +31 -0
  131. package/dist/src/import/tui/scope-gate-columns.js +94 -0
  132. package/dist/src/import/tui/scope-gate-host.d.ts +3 -1
  133. package/dist/src/import/tui/scope-gate-host.js +11 -3
  134. package/dist/src/import/tui/sidebar-width.d.ts +1 -0
  135. package/dist/src/import/tui/sidebar-width.js +3 -0
  136. package/dist/src/import/tui/step-filters.d.ts +19 -0
  137. package/dist/src/import/tui/step-filters.js +42 -0
  138. package/dist/src/import/tui/steps/AtomicGenerateReviewStep.d.ts +42 -0
  139. package/dist/src/import/tui/steps/AtomicGenerateReviewStep.js +726 -0
  140. package/dist/src/import/tui/steps/AtomicScopeGateStep.d.ts +3 -0
  141. package/dist/src/import/tui/steps/AtomicScopeGateStep.js +211 -0
  142. package/dist/src/import/tui/steps/CredentialsStep.js +5 -18
  143. package/dist/src/import/tui/steps/DoneStep.d.ts +0 -2
  144. package/dist/src/import/tui/steps/DoneStep.js +3 -2
  145. package/dist/src/import/tui/steps/ErrorStep.js +2 -1
  146. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +19 -25
  147. package/dist/src/import/tui/steps/GenerateReviewStep.js +1258 -229
  148. package/dist/src/import/tui/steps/PathValidationStep.js +3 -2
  149. package/dist/src/import/tui/steps/PreviewStep.js +2 -1
  150. package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +0 -7
  151. package/dist/src/import/tui/steps/PushDecisionGateStep.js +3 -6
  152. package/dist/src/import/tui/steps/PushingStep.js +2 -1
  153. package/dist/src/import/tui/steps/RunningStep.d.ts +4 -1
  154. package/dist/src/import/tui/steps/RunningStep.js +3 -2
  155. package/dist/src/import/tui/steps/ScopeGateStep.d.ts +24 -0
  156. package/dist/src/import/tui/steps/ScopeGateStep.js +850 -193
  157. package/dist/src/import/tui/steps/TokenInputStep.js +2 -1
  158. package/dist/src/import/tui/steps/WelcomeStep.js +2 -1
  159. package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +6 -0
  160. package/dist/src/import/tui/steps/WizardPreviewStep.js +95 -56
  161. package/dist/src/import/tui/steps/auto-reject-decision.d.ts +7 -0
  162. package/dist/src/import/tui/steps/auto-reject-decision.js +9 -0
  163. package/dist/src/import/tui/steps/breaking-change-format.d.ts +2 -0
  164. package/dist/src/import/tui/steps/breaking-change-format.js +24 -0
  165. package/dist/src/import/tui/steps/enumerate-cycle-breaks.d.ts +14 -0
  166. package/dist/src/import/tui/steps/enumerate-cycle-breaks.js +0 -0
  167. package/dist/src/import/tui/steps/preview-diff.js +48 -5
  168. package/dist/src/import/tui/useFinalizePreview.d.ts +34 -0
  169. package/dist/src/import/tui/useFinalizePreview.js +0 -0
  170. package/dist/src/import/tui/useLivePreview.d.ts +3 -0
  171. package/dist/src/import/tui/useLivePreview.js +1 -0
  172. package/dist/src/import/tui/wizard-state-transitions.d.ts +5 -59
  173. package/dist/src/import/tui/wizard-state-transitions.js +5 -60
  174. package/dist/src/lib/composition-mode.d.ts +28 -0
  175. package/dist/src/lib/composition-mode.js +37 -0
  176. package/dist/src/lib/prompt-overrides.d.ts +38 -0
  177. package/dist/src/lib/prompt-overrides.js +69 -0
  178. package/dist/src/print/command.js +19 -1
  179. package/dist/src/runs/modify-launcher.d.ts +3 -0
  180. package/dist/src/runs/modify-launcher.js +2 -0
  181. package/dist/src/runs/replay-helpers.js +1 -0
  182. package/dist/src/runs/run-picker.js +2 -18
  183. package/dist/src/runs/store.d.ts +4 -0
  184. package/dist/src/session/db.d.ts +23 -40
  185. package/dist/src/session/db.js +110 -138
  186. package/dist/src/setup/composition-mode-prompt.d.ts +14 -0
  187. package/dist/src/setup/composition-mode-prompt.js +37 -0
  188. package/package.json +5 -4
  189. package/prompts/composition-dir-critic.md +7 -0
  190. package/prompts/composition-edges.md +8 -0
  191. package/prompts/composition-parser-repair-empty.md +1 -0
  192. package/prompts/composition-parser-repair-error.md +2 -0
  193. package/prompts/composition-parser.md +42 -0
@@ -0,0 +1,726 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { Box, Text, useStdout } from 'ink';
4
+ import { Sidebar } from '../../../analyze/select/tui/components/Sidebar.js';
5
+ import { JsonPanel } from '../../../analyze/select/tui/components/JsonPanel.js';
6
+ import { FieldEditor } from '../../../analyze/select/tui/components/FieldEditor.js';
7
+ import { StatusBar } from '../../../analyze/select/tui/components/StatusBar.js';
8
+ import { FinalizeDialog } from '../../../analyze/select/tui/components/FinalizeDialog.js';
9
+ import { QuitDialog } from '../../../analyze/select/tui/components/QuitDialog.js';
10
+ import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
11
+ import { openPipelineDb, loadCDFComponents, storeCDFComponents, loadComponentReviewMetadata, loadComponentRationale, } from '../../../session/db.js';
12
+ import { RationalePanel } from '../../../analyze/select/tui/components/RationalePanel.js';
13
+ import { ComponentRationalePanel } from '../../../analyze/select/tui/components/ComponentRationalePanel.js';
14
+ import { applyPreviewAnnotations } from '../../../analyze/select/preview-annotations.js';
15
+ import { createHistoryStack } from '../history.js';
16
+ import { useLivePreview } from '../useLivePreview.js';
17
+ import { useFinalizePreview } from '../useFinalizePreview.js';
18
+ import { computeNextScrollOffset } from '../../../analyze/select/tui/hooks/scroll-offset.js';
19
+ import { PALETTE } from '../../../analyze/select/tui/theme.js';
20
+ /**
21
+ * Sort components for the final-review sidebar so the underlying data array
22
+ * matches the visual order. Empty components (zero classified $properties)
23
+ * surface at the top via the warning-tier path in Sidebar.tsx; we mirror that
24
+ * here so `selectedIdx` indexes into the same order the user sees. Without
25
+ * this, j/k navigation lands on different rows than the visually-selected
26
+ * one (INTEG-4259).
27
+ *
28
+ * Within each tier (empty / non-empty) we tie-break alphabetically by `key`.
29
+ */
30
+ export function sortComponentsForSidebar(components) {
31
+ return [...components].sort((a, b) => {
32
+ const aEmpty = Object.keys(a.entry.$properties ?? {}).length === 0;
33
+ const bEmpty = Object.keys(b.entry.$properties ?? {}).length === 0;
34
+ if (aEmpty !== bEmpty)
35
+ return aEmpty ? -1 : 1;
36
+ return a.key.localeCompare(b.key);
37
+ });
38
+ }
39
+ const VISIBLE_COUNT = 20;
40
+ const PANEL_HEIGHT = 22;
41
+ export function AtomicGenerateReviewStep({ extractSessionId, onFinalize, onQuit, livePreview = true, spaceId = '', environmentId = '', cmaToken = '', host = '', tokensPath = '', initialFinalizeError = null, }) {
42
+ const { stdout } = useStdout();
43
+ const terminalWidth = stdout?.columns ?? 80;
44
+ const [components, setComponents] = useState([]);
45
+ const [loading, setLoading] = useState(true);
46
+ const [loadError, setLoadError] = useState(null);
47
+ const [selectedIdx, setSelectedIdx] = useState(0);
48
+ const [sidebarScrollOffset, setSidebarScrollOffset] = useState(0);
49
+ const [jsonScrollOffset, setJsonScrollOffset] = useState(0);
50
+ const [sidebarFocused, setSidebarFocused] = useState(true);
51
+ const [showFinalize, setShowFinalize] = useState(false);
52
+ const [showQuit, setShowQuit] = useState(false);
53
+ // FieldEditor is the default editor. JSON view is an opt-in read-only toggle.
54
+ const [showJson, setShowJson] = useState(false);
55
+ const [draftValue, setDraftValue] = useState('');
56
+ const [saveError, setSaveError] = useState(null);
57
+ // INTEG-4411: inline banner shown when the operator tries to finalize
58
+ // with zero accepted components. Cleared on the next 'a' or 'A' press.
59
+ const [finalizeError, setFinalizeError] = useState(initialFinalizeError);
60
+ // Feature 1: per-component review metadata (rationale + source location)
61
+ // for the currently-selected component. Reloaded when selection changes.
62
+ const [reviewMetadata, setReviewMetadata] = useState(null);
63
+ // Feature 2: per-component preview annotations refreshed after every
64
+ // FieldEditor save via the useLivePreview hook below. Empty when live
65
+ // preview is disabled, when creds are missing, or before the first response.
66
+ const [previewAnnotations, setPreviewAnnotations] = useState(new Map());
67
+ // Pilot-2026-06-24: raw removed list for the `d` detail panel. The
68
+ // annotation map only carries kind, not the rich summaries we need to list
69
+ // names/ids when the operator asks "which ones?".
70
+ const [removedComponents, setRemovedComponents] = useState([]);
71
+ const [showRemovedPanel, setShowRemovedPanel] = useState(false);
72
+ // Lifted rationale + source panels (replaces FieldEditor's right pane).
73
+ // Mutually exclusive states.
74
+ const [panelOpen, setPanelOpen] = useState('none');
75
+ const [panelScrollOffset, setPanelScrollOffset] = useState(0);
76
+ const [textEntryActive, setTextEntryActive] = useState(false);
77
+ const [componentRationale, setComponentRationale] = useState(null);
78
+ // Tracks the first `g` of a potential `gg` double-tap (jumps to top in
79
+ // JSON-view + panel-focused state). Reset on any non-`g` key.
80
+ const pendingGRef = useRef(false);
81
+ const [showReloadDialog, setShowReloadDialog] = useState(false);
82
+ const historyRef = useRef(null);
83
+ const historySeededRef = useRef(false);
84
+ const handleLivePreviewResult = (response) => {
85
+ if (!response)
86
+ return;
87
+ setPreviewAnnotations(applyPreviewAnnotations(response, components.map((c) => c.key)));
88
+ setRemovedComponents(response.components.removed ?? []);
89
+ };
90
+ const livePreviewHook = useLivePreview({
91
+ enabled: livePreview,
92
+ sessionId: extractSessionId,
93
+ tokensPath,
94
+ spaceId,
95
+ environmentId,
96
+ cmaToken,
97
+ host,
98
+ onResult: handleLivePreviewResult,
99
+ });
100
+ // Manual spinner cycling (no extra dep) for the sidebar status-row
101
+ // indicator. Runs only while the live-preview hook reports `running`.
102
+ const SPINNER_FRAMES = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏';
103
+ const [spinnerTick, setSpinnerTick] = useState(0);
104
+ useEffect(() => {
105
+ if (livePreviewHook.status !== 'running')
106
+ return;
107
+ const id = setInterval(() => setSpinnerTick((t) => t + 1), 80);
108
+ return () => clearInterval(id);
109
+ }, [livePreviewHook.status]);
110
+ const livePreviewSpinner = SPINNER_FRAMES[spinnerTick % SPINNER_FRAMES.length];
111
+ const loadEntries = () => {
112
+ const db = openPipelineDb();
113
+ let cdfComponents;
114
+ try {
115
+ cdfComponents = loadCDFComponents(db, extractSessionId);
116
+ }
117
+ finally {
118
+ db.close();
119
+ }
120
+ if (cdfComponents.length === 0) {
121
+ return { entries: [], error: 'No generated definitions found for this session. Try re-running generate.' };
122
+ }
123
+ const reviewEntries = cdfComponents.map(({ key, entry }) => ({
124
+ key,
125
+ entry,
126
+ status: 'needs-review',
127
+ }));
128
+ return { entries: sortComponentsForSidebar(reviewEntries), error: null };
129
+ };
130
+ useEffect(() => {
131
+ try {
132
+ const { entries, error } = loadEntries();
133
+ if (error) {
134
+ setLoadError(error);
135
+ }
136
+ else {
137
+ setComponents(entries);
138
+ }
139
+ }
140
+ catch (e) {
141
+ setLoadError(String(e));
142
+ }
143
+ finally {
144
+ setLoading(false);
145
+ }
146
+ }, []);
147
+ // Seed the undo/redo history once components are loaded.
148
+ useEffect(() => {
149
+ if (loading)
150
+ return;
151
+ if (historySeededRef.current)
152
+ return;
153
+ historySeededRef.current = true;
154
+ historyRef.current = createHistoryStack({
155
+ components: components.map((c) => ({ key: c.key, entry: c.entry, status: c.status })),
156
+ autoRejected: [],
157
+ undoSnapshot: null,
158
+ });
159
+ }, [loading, components]);
160
+ const pushHistorySnapshot = (entries, label) => {
161
+ if (!historyRef.current)
162
+ return;
163
+ historyRef.current.push({
164
+ components: entries.map((c) => ({ key: c.key, entry: c.entry, status: c.status })),
165
+ autoRejected: [],
166
+ undoSnapshot: null,
167
+ }, label);
168
+ };
169
+ const applyHistorySnapshot = (snap) => {
170
+ setComponents(snap.components.map((c) => ({ key: c.key, entry: c.entry, status: c.status })));
171
+ };
172
+ const handleUndo = () => {
173
+ const snap = historyRef.current?.undo();
174
+ if (snap)
175
+ applyHistorySnapshot(snap);
176
+ };
177
+ const handleRedo = () => {
178
+ const snap = historyRef.current?.redo();
179
+ if (snap)
180
+ applyHistorySnapshot(snap);
181
+ };
182
+ const reloadFromSave = () => {
183
+ try {
184
+ const { entries, error } = loadEntries();
185
+ if (error) {
186
+ setLoadError(error);
187
+ return;
188
+ }
189
+ setComponents(entries);
190
+ historyRef.current?.reset({
191
+ components: entries.map((c) => ({ key: c.key, entry: c.entry, status: c.status })),
192
+ autoRejected: [],
193
+ undoSnapshot: null,
194
+ });
195
+ }
196
+ catch (e) {
197
+ setLoadError(String(e));
198
+ }
199
+ };
200
+ // Pilot-2026-06-23 R2: fire the live preview once on entry to final-review
201
+ // so diff badges populate before the operator's first save. We gate on the
202
+ // livePreview prop to honor --no-live-preview without depending on the
203
+ // hook's internal short-circuit. Cred-missing is still handled by the
204
+ // hook's own no-op path.
205
+ useEffect(() => {
206
+ if (loading)
207
+ return;
208
+ if (!livePreview)
209
+ return;
210
+ if (components.length === 0)
211
+ return;
212
+ livePreviewHook.trigger();
213
+ // Intentionally only on load completion — subsequent fires happen via
214
+ // handleEditSave. Adding livePreviewHook to deps would re-fire on every
215
+ // hook re-creation.
216
+ }, [loading]);
217
+ // Feature 1: load review metadata (rationale + source location) for the
218
+ // selected component when selection changes.
219
+ useEffect(() => {
220
+ const current = components[selectedIdx];
221
+ if (!current) {
222
+ setReviewMetadata(null);
223
+ return;
224
+ }
225
+ const db = openPipelineDb();
226
+ try {
227
+ setReviewMetadata(loadComponentReviewMetadata(db, extractSessionId, current.key));
228
+ }
229
+ catch {
230
+ setReviewMetadata(null);
231
+ }
232
+ finally {
233
+ db.close();
234
+ }
235
+ }, [selectedIdx, components, extractSessionId]);
236
+ // Load component-level rationale for the selected component (drives the
237
+ // `I` ComponentRationalePanel). Decoupled from review metadata so the data
238
+ // contracts can evolve independently.
239
+ useEffect(() => {
240
+ const current = components[selectedIdx];
241
+ if (!current) {
242
+ setComponentRationale(null);
243
+ return;
244
+ }
245
+ const db = openPipelineDb();
246
+ try {
247
+ setComponentRationale(loadComponentRationale(db, extractSessionId, current.key));
248
+ }
249
+ catch {
250
+ setComponentRationale(null);
251
+ }
252
+ finally {
253
+ db.close();
254
+ }
255
+ }, [selectedIdx, components, extractSessionId]);
256
+ const updateStatus = (idx, status) => {
257
+ setComponents((prev) => {
258
+ const next = prev.map((c, i) => (i === idx ? { ...c, status } : c));
259
+ pushHistorySnapshot(next, `status:${status}`);
260
+ return next;
261
+ });
262
+ };
263
+ const finalizePreview = useFinalizePreview({
264
+ open: showFinalize,
265
+ extractSessionId,
266
+ tokensPath,
267
+ spaceId,
268
+ environmentId,
269
+ cmaToken,
270
+ host,
271
+ acceptedKeys: new Set(components.filter((c) => c.status === 'accepted').map((c) => c.key)),
272
+ });
273
+ const handleFinalizeConfirm = () => {
274
+ // Strict opt-in: only EXPLICITLY ACCEPTED components ship. Anything left
275
+ // in 'needs-review' OR explicitly 'rejected' is downgraded to
276
+ // 'generate-rejected' so loadCDFComponents excludes it from the manifest.
277
+ // The operator told us they want accept-to-ship semantics — leaving a
278
+ // component unresolved should NOT silently push it (Pilot-2026-06-24 R2).
279
+ const acceptedCount = components.filter((c) => c.status === 'accepted').length;
280
+ // INTEG-4411 refined: DO NOT block on `acceptedCount === 0` up-front.
281
+ // A push with zero accepted but one or more rejections targeting a
282
+ // component that exists server-side still produces REMOVALS — a valid
283
+ // push, not a no-op. Same for token-only diffs. The load-bearing no-op
284
+ // check lives downstream in WizardApp.runPreview, which consults the
285
+ // preview response and only blocks when every diff bucket is empty.
286
+ // We keep the `finalizeError` state so the wizard can route back here
287
+ // with an inline banner when that downstream check fires.
288
+ const explicitlyRejected = components.filter((c) => c.status === 'rejected').map((c) => c.key);
289
+ const unresolved = components.filter((c) => c.status === 'needs-review').map((c) => c.key);
290
+ const toReject = [...explicitlyRejected, ...unresolved];
291
+ if (toReject.length > 0) {
292
+ const db = openPipelineDb();
293
+ try {
294
+ const stmt = db.prepare(`UPDATE raw_components SET status = 'generate-rejected' WHERE session_id = ? AND name = ?`);
295
+ db.exec('BEGIN');
296
+ try {
297
+ for (const name of toReject) {
298
+ stmt.run(extractSessionId, name);
299
+ }
300
+ db.exec('COMMIT');
301
+ }
302
+ catch (e) {
303
+ db.exec('ROLLBACK');
304
+ throw e;
305
+ }
306
+ }
307
+ finally {
308
+ db.close();
309
+ }
310
+ }
311
+ onFinalize(acceptedCount, explicitlyRejected.length, unresolved.length);
312
+ };
313
+ const handleEditSave = () => {
314
+ const current = components[selectedIdx];
315
+ if (!current)
316
+ return;
317
+ try {
318
+ const parsed = JSON.parse(draftValue);
319
+ // Accept both bare entry and wrapped { [key]: entry } forms
320
+ const keys = Object.keys(parsed);
321
+ const entry = keys.length === 1 && typeof parsed[keys[0]] === 'object' && parsed[keys[0]] !== null
322
+ ? parsed[keys[0]]
323
+ : parsed;
324
+ if (entry.$type !== 'component' || typeof entry.$properties !== 'object' || entry.$properties === null) {
325
+ setSaveError('Invalid CDF entry: must have $type: "component" and $properties object');
326
+ return;
327
+ }
328
+ setComponents((prev) => prev.map((c, i) => i === selectedIdx ? { ...c, entry, status: c.status === 'needs-review' ? 'accepted' : c.status } : c));
329
+ setDraftValue('');
330
+ setSaveError(null);
331
+ const db = openPipelineDb();
332
+ try {
333
+ storeCDFComponents(db, extractSessionId, [{ key: current.key, entry }]);
334
+ }
335
+ finally {
336
+ db.close();
337
+ }
338
+ // Feature 2: re-fire the live preview now that pipeline.db reflects
339
+ // the new state. The hook owns debounce + cred-missing short-circuit.
340
+ livePreviewHook.trigger();
341
+ }
342
+ catch (e) {
343
+ setSaveError(String(e));
344
+ }
345
+ };
346
+ const handleEditDiscard = () => {
347
+ setDraftValue('');
348
+ setSaveError(null);
349
+ };
350
+ const dialogOpen = showFinalize || showQuit;
351
+ useImmediateInput((input, key) => {
352
+ if (loading)
353
+ return;
354
+ // On a load error there's nothing to review — still let the operator quit
355
+ // (q / Esc / Enter) instead of trapping them on the error screen.
356
+ if (loadError) {
357
+ if (input === 'q' || key.escape || key.return)
358
+ onQuit();
359
+ return;
360
+ }
361
+ if (showFinalize) {
362
+ // The dialog owns y/n/Enter/Esc; here we own j/k scroll of its deletion list.
363
+ if (input === 'j' || key.downArrow) {
364
+ finalizePreview.scrollBy(1);
365
+ return;
366
+ }
367
+ if (input === 'k' || key.upArrow) {
368
+ finalizePreview.scrollBy(-1);
369
+ return;
370
+ }
371
+ return;
372
+ }
373
+ if (dialogOpen)
374
+ return;
375
+ if (showReloadDialog) {
376
+ if (key.return) {
377
+ reloadFromSave();
378
+ setShowReloadDialog(false);
379
+ return;
380
+ }
381
+ if (key.escape) {
382
+ setShowReloadDialog(false);
383
+ return;
384
+ }
385
+ return;
386
+ }
387
+ if (key.ctrl && input === 'z') {
388
+ handleUndo();
389
+ return;
390
+ }
391
+ if (key.ctrl && input === 'y') {
392
+ handleRedo();
393
+ return;
394
+ }
395
+ if (key.ctrl && input === 'r') {
396
+ setShowReloadDialog(true);
397
+ return;
398
+ }
399
+ // Pilot-2026-06-24: removed-detail panel. When open, only `d` (toggle)
400
+ // and Esc (close) respond — all other input is swallowed so j/k/Enter/
401
+ // Ctrl+S can't move state behind the modal. Mirrors the `?` overlay
402
+ // pattern from 8f0c62e in FieldEditor.
403
+ if (showRemovedPanel) {
404
+ if (input === 'd' || key.escape) {
405
+ setShowRemovedPanel(false);
406
+ }
407
+ return;
408
+ }
409
+ // `d` opens the panel only when live-preview is enabled and there is at
410
+ // least one removed component to display. Sidebar-focused only so it
411
+ // doesn't collide with FieldEditor input.
412
+ if (input === 'd' && sidebarFocused && livePreview && removedComponents.length > 0) {
413
+ setShowRemovedPanel(true);
414
+ return;
415
+ }
416
+ // Lifted rationale + source panels: i/I/s fire from anywhere (sidebar OR
417
+ // panel focus). Gated against text-entry surfaces inside FieldEditor
418
+ // (description editors, string-default editor, value-list text entry)
419
+ // via the `onTextEntryActiveChange` callback, plus the help/finalize/quit
420
+ // overlays and the JSON view.
421
+ if (panelOpen !== 'none') {
422
+ const PANEL_HEIGHT_LOCAL = 12;
423
+ const next = computeNextScrollOffset(panelScrollOffset, input, key, 9999, PANEL_HEIGHT_LOCAL);
424
+ if (next !== null) {
425
+ setPanelScrollOffset(() => next);
426
+ return;
427
+ }
428
+ if (key.escape) {
429
+ setPanelOpen('none');
430
+ setPanelScrollOffset(() => 0);
431
+ return;
432
+ }
433
+ // Guard against Ctrl-letter aliases (Tab is Ctrl+I in ASCII, Ctrl+S would
434
+ // collide with save in nested editors). Only react to bare keystrokes.
435
+ const togglable = !key.ctrl && !key.tab && !key.meta && !key.return;
436
+ if (togglable && input === 'i' && panelOpen === 'prop-rationale') {
437
+ setPanelOpen('none');
438
+ setPanelScrollOffset(() => 0);
439
+ return;
440
+ }
441
+ if (togglable && input === 'I' && panelOpen === 'component-rationale') {
442
+ setPanelOpen('none');
443
+ setPanelScrollOffset(() => 0);
444
+ return;
445
+ }
446
+ if (togglable && input === 's' && panelOpen === 'source') {
447
+ setPanelOpen('none');
448
+ setPanelScrollOffset(() => 0);
449
+ return;
450
+ }
451
+ // Cross-panel toggles while one is open.
452
+ if (togglable && input === 'i') {
453
+ setPanelOpen('prop-rationale');
454
+ setPanelScrollOffset(() => 0);
455
+ return;
456
+ }
457
+ if (togglable && input === 'I') {
458
+ setPanelOpen('component-rationale');
459
+ setPanelScrollOffset(() => 0);
460
+ return;
461
+ }
462
+ if (togglable && input === 's') {
463
+ setPanelOpen('source');
464
+ setPanelScrollOffset(() => 0);
465
+ return;
466
+ }
467
+ return;
468
+ }
469
+ const rationaleKeyOk = !textEntryActive && !showJson && !key.ctrl && !key.tab && !key.meta && !key.return;
470
+ if (rationaleKeyOk) {
471
+ if (input === 'i') {
472
+ setPanelOpen('prop-rationale');
473
+ setPanelScrollOffset(() => 0);
474
+ return;
475
+ }
476
+ if (input === 'I') {
477
+ setPanelOpen('component-rationale');
478
+ setPanelScrollOffset(() => 0);
479
+ return;
480
+ }
481
+ if (input === 's') {
482
+ setPanelOpen('source');
483
+ setPanelScrollOffset(() => 0);
484
+ return;
485
+ }
486
+ }
487
+ // Tab toggles focus bidirectionally between sidebar and panel. `e` is a
488
+ // sidebar-only alias for crossing INTO the panel — gating it to the
489
+ // sidebar-focused state prevents collision with FieldEditor's enum-values
490
+ // `e` binding (INTEG-4254) when the panel is focused. Crossing back from
491
+ // panel to sidebar is Tab-only.
492
+ if (key.tab) {
493
+ setSidebarFocused((prev) => !prev);
494
+ return;
495
+ }
496
+ if (input === 'e' && sidebarFocused) {
497
+ setSidebarFocused(false);
498
+ return;
499
+ }
500
+ // JSON view + panel focused: own j/k/arrows/PageUp/PageDown/Ctrl+u/d/gg/G
501
+ // for scrolling. Computed against the live `selectedJson` so the
502
+ // viewport math matches what JsonPanel renders.
503
+ if (!sidebarFocused && showJson) {
504
+ const current = components[selectedIdx];
505
+ const currentJson = current ? JSON.stringify({ [current.key]: current.entry }, null, 2) : '';
506
+ const totalLines = currentJson.split('\n').length;
507
+ const maxOffset = Math.max(0, totalLines - PANEL_HEIGHT);
508
+ // `gg` double-tap to jump to top; single `g` arms the pending flag.
509
+ if (input === 'g' && !key.ctrl) {
510
+ if (pendingGRef.current) {
511
+ pendingGRef.current = false;
512
+ setJsonScrollOffset(() => 0);
513
+ return;
514
+ }
515
+ pendingGRef.current = true;
516
+ return;
517
+ }
518
+ const next = computeNextScrollOffset(jsonScrollOffset, input, key, totalLines, PANEL_HEIGHT);
519
+ if (next !== null) {
520
+ pendingGRef.current = false;
521
+ // Functional setState mirrors the cursor-stutter fix (commit 5d11e60).
522
+ // Clamp against maxOffset re-computed at apply time in case totalLines
523
+ // shifted between events (defensive — helper already clamps).
524
+ const clamped = Math.min(maxOffset, Math.max(0, next));
525
+ setJsonScrollOffset(() => clamped);
526
+ return;
527
+ }
528
+ // Any other key in this slice resets the gg-pending flag, then falls
529
+ // through to the early-return below so the panel-focused state still
530
+ // swallows non-scroll input.
531
+ pendingGRef.current = false;
532
+ }
533
+ // When the panel is focused, FieldEditor (or JsonPanel) owns the keys.
534
+ // Only Tab (handled above) should escape from the panel-focused state.
535
+ if (!sidebarFocused)
536
+ return;
537
+ // Sidebar-focused keymap.
538
+ if (input === 'q') {
539
+ setShowQuit(true);
540
+ return;
541
+ }
542
+ if (input === 'F') {
543
+ setShowFinalize(true);
544
+ return;
545
+ }
546
+ if (input === 'a') {
547
+ updateStatus(selectedIdx, 'accepted');
548
+ setFinalizeError(null);
549
+ return;
550
+ }
551
+ if (input === 'r') {
552
+ updateStatus(selectedIdx, 'rejected');
553
+ return;
554
+ }
555
+ if (input === 'A') {
556
+ setComponents((prev) => {
557
+ const next = prev.map((c) => c.status === 'needs-review' ? { ...c, status: 'accepted' } : c);
558
+ pushHistorySnapshot(next, 'accept-all');
559
+ return next;
560
+ });
561
+ setFinalizeError(null);
562
+ return;
563
+ }
564
+ if (input === 'J') {
565
+ // Toggle read-only JSON view.
566
+ setShowJson((prev) => !prev);
567
+ setJsonScrollOffset(0);
568
+ pendingGRef.current = false;
569
+ return;
570
+ }
571
+ if (key.upArrow || input === 'k') {
572
+ // Pilot-2026-06-23 bug: rapid k/j bursts could lose cursor position
573
+ // because the previous implementation read `selectedIdx` from the
574
+ // handler's closure. Under high keyboard-repeat rate multiple key
575
+ // events fire between Ink render flushes, so every invocation saw the
576
+ // same stale value and recomputed the same `newIdx`. Using functional
577
+ // setState chains the updates correctly: each pending update sees the
578
+ // post-update value of the previous one. The viewport offset update is
579
+ // nested inside the cursor updater so it always reflects the same
580
+ // newIdx that selectedIdx is being set to.
581
+ setSelectedIdx((prev) => {
582
+ const newIdx = Math.max(0, prev - 1);
583
+ setSidebarScrollOffset((off) => Math.min(off, newIdx));
584
+ return newIdx;
585
+ });
586
+ setJsonScrollOffset(0);
587
+ setDraftValue('');
588
+ setSaveError(null);
589
+ }
590
+ else if (key.downArrow || input === 'j') {
591
+ setSelectedIdx((prev) => {
592
+ const newIdx = Math.min(components.length - 1, prev + 1);
593
+ setSidebarScrollOffset((off) => (newIdx >= off + VISIBLE_COUNT ? newIdx - VISIBLE_COUNT + 1 : off));
594
+ return newIdx;
595
+ });
596
+ setJsonScrollOffset(0);
597
+ setDraftValue('');
598
+ setSaveError(null);
599
+ }
600
+ });
601
+ if (loading) {
602
+ return (_jsx(Box, { paddingX: 2, paddingY: 1, children: _jsx(Text, { dimColor: true, children: "Loading generated definitions..." }) }));
603
+ }
604
+ if (loadError) {
605
+ 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" })] }));
606
+ }
607
+ const selected = components[selectedIdx] ?? null;
608
+ const selectedJson = selected ? JSON.stringify({ [selected.key]: selected.entry }, null, 2) : '';
609
+ // A component with zero classified $properties is a real defensibility issue —
610
+ // it can't be pushed to Contentful (no fields). Surface it in the sidebar via
611
+ // the existing warning-color path (yellow) and a "(empty)" suffix so the user
612
+ // can see what went wrong. They can manually add props in FieldEditor or
613
+ // explicitly reject the component.
614
+ const isEmpty = (c) => Object.keys(c.entry.$properties).length === 0;
615
+ const emptyCount = components.filter(isEmpty).length;
616
+ const sidebarItems = components.map((c) => ({
617
+ id: c.key,
618
+ name: isEmpty(c) ? `${c.key} (empty)` : c.key,
619
+ status: c.status,
620
+ previewAnnotation: previewAnnotations.get(c.key),
621
+ extractionConfidence: null,
622
+ needsReview: false,
623
+ validationErrorCount: 0,
624
+ validationWarningCount: isEmpty(c) ? 1 : 0,
625
+ }));
626
+ // Account for the "(empty)" suffix added to zero-prop component names so the
627
+ // sidebar doesn't truncate it.
628
+ const longestName = components.reduce((m, c) => Math.max(m, c.key.length + (isEmpty(c) ? ' (empty)'.length : 0)), 0);
629
+ // +5 = border (1) + status icon (1) + badge column (1) + space (1) + border (1).
630
+ // The badge column is reserved even when no annotation is present so the
631
+ // sidebar width doesn't jitter as live-preview annotations flip in/out.
632
+ const sidebarWidth = Math.min(Math.max(longestName + 5, 14), 30);
633
+ const panelWidth = Math.max(10, terminalWidth - sidebarWidth - 4);
634
+ const accepted = components.filter((c) => c.status === 'accepted').length;
635
+ const rejected = components.filter((c) => c.status === 'rejected').length;
636
+ const needsReview = components.filter((c) => c.status === 'needs-review').length;
637
+ const propCount = selected ? Object.keys(selected.entry.$properties).length : 0;
638
+ const slotCount = selected?.entry.$slots ? Object.keys(selected.entry.$slots).length : 0;
639
+ 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) }), 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' })] })), showRemovedPanel && !dialogOpen && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.info, paddingX: 1, children: [_jsx(Text, { bold: true, color: PALETTE.info, children: `Removed components (${removedComponents.length})` }), _jsx(Text, { dimColor: true, children: "these will be DELETED from the target space" }), _jsx(Text, { children: " " }), removedComponents.map((rc) => (_jsx(Text, { children: `- ${rc.name}${rc.id ? ` (${rc.id})` : ''}` }, rc.id))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "press d or Esc to close" })] })), !dialogOpen &&
640
+ livePreview &&
641
+ (() => {
642
+ // Pilot-2026-06-23 R2: at-a-glance diff summary at the top of the
643
+ // step. Mutually exclusive states:
644
+ // - hook running (and we don't yet have annotations) → spinner.
645
+ // - hook disabled (creds rejected) → static disabled hint.
646
+ // - annotations populated → counts.
647
+ // - idle, no annotations, not disabled → render nothing.
648
+ const counts = { new: 0, changed: 0, removed: 0, breaking: 0 };
649
+ for (const v of previewAnnotations.values()) {
650
+ counts[v] = (counts[v] ?? 0) + 1;
651
+ }
652
+ const hasCounts = counts.new + counts.changed + counts.removed + counts.breaking > 0;
653
+ if (livePreviewHook.disabled) {
654
+ return _jsx(Text, { dimColor: true, children: 'Preview: disabled (creds rejected)' });
655
+ }
656
+ if (livePreviewHook.status === 'running' && !hasCounts) {
657
+ return _jsx(Text, { dimColor: true, children: `Preview: ${livePreviewSpinner} running...` });
658
+ }
659
+ if (!hasCounts)
660
+ return null;
661
+ 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` }), removedComponents.length > 0 && _jsx(Text, { dimColor: true, children: ' ([d] removed list)' }), _jsx(Text, { children: ' · ' }), _jsx(Text, { color: PALETTE.error, bold: true, children: `${counts.breaking} breaking` })] }));
662
+ })(), !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: [_jsx(Sidebar, { components: sidebarItems, selectedId: selected?.key ?? null, focused: sidebarFocused, scrollOffset: sidebarScrollOffset, visibleCount: VISIBLE_COUNT, onSelect: (id) => {
663
+ const idx = components.findIndex((c) => c.key === id);
664
+ if (idx >= 0) {
665
+ setSelectedIdx(idx);
666
+ setJsonScrollOffset(0);
667
+ }
668
+ }, onScrollChange: setSidebarScrollOffset, width: sidebarWidth }), _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 ? '[e/Tab] focus panel' : '[Tab] focus list'] })] }), panelOpen === 'prop-rationale' ? ((() => {
669
+ const rows = [
670
+ ...(componentRationale?.props ?? []).map((p) => ({
671
+ name: p.name,
672
+ kind: 'prop',
673
+ rationale: p.rationale ?? '',
674
+ })),
675
+ ...(componentRationale?.slots ?? []).map((s) => ({
676
+ name: s.name,
677
+ kind: 'slot',
678
+ rationale: s.rationale ?? '',
679
+ })),
680
+ ];
681
+ return (_jsx(RationalePanel, { componentName: componentRationale?.name ?? selected.key, rows: rows, scrollOffset: panelScrollOffset, width: panelWidth, height: PANEL_HEIGHT, active: true }));
682
+ })()) : panelOpen === 'component-rationale' ? (_jsx(ComponentRationalePanel, { data: componentRationale ?? {
683
+ name: selected.key,
684
+ description: null,
685
+ descriptionRationale: null,
686
+ propsRationale: null,
687
+ slotsRationale: null,
688
+ props: [],
689
+ slots: [],
690
+ }, scrollOffset: panelScrollOffset, width: panelWidth, height: PANEL_HEIGHT, active: true })) : panelOpen === 'source' ? ((() => {
691
+ const path = reviewMetadata?.sourcePath ?? null;
692
+ const src = reviewMetadata?.componentSource ?? null;
693
+ const headerPath = path ?? '<unknown source path>';
694
+ const lines = src ? src.split('\n').slice(panelScrollOffset, panelScrollOffset + PANEL_HEIGHT) : [];
695
+ return (_jsxs(Box, { flexDirection: "column", width: panelWidth, borderStyle: "single", borderColor: PALETTE.border, 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' })] }));
696
+ })()) : 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
697
+ ? {
698
+ sourcePath: reviewMetadata.sourcePath,
699
+ componentSource: reviewMetadata.componentSource,
700
+ props: reviewMetadata.props,
701
+ }
702
+ : undefined, onTogglePropRationale: () => {
703
+ setPanelOpen('prop-rationale');
704
+ setPanelScrollOffset(() => 0);
705
+ }, onToggleComponentRationale: () => {
706
+ setPanelOpen('component-rationale');
707
+ setPanelScrollOffset(() => 0);
708
+ }, onToggleSourceExternal: () => {
709
+ setPanelOpen('source');
710
+ setPanelScrollOffset(() => 0);
711
+ }, onTextEntryActiveChange: setTextEntryActive }, selected.key)), saveError && _jsx(Text, { color: PALETTE.error, children: '✗ ' + saveError }), _jsxs(Text, { dimColor: true, children: [sidebarFocused
712
+ ? ' [a] accept [r] reject [A] accept all [i] prop rationale [I] component rationale [s] source [J] ' +
713
+ (showJson ? 'hide JSON' : 'show JSON') +
714
+ ' [^z] undo [^y] redo [^r] reload [F] finalize [e/Tab] focus panel' +
715
+ (livePreview && removedComponents.length > 0 ? ' [d] removed list' : '') +
716
+ ' [q] quit'
717
+ : showJson
718
+ ? ' [j/k] scroll [Ctrl+u/d] half-page [gg/G] top/bottom [Tab] focus list'
719
+ : ' [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" })) })] })), !dialogOpen && (_jsx(StatusBar, { accepted: accepted, rejected: rejected, reviewed: 0, needsReview: needsReview, onApproveAll: () => {
720
+ setComponents((prev) => {
721
+ const next = prev.map((c) => c.status === 'needs-review' ? { ...c, status: 'accepted' } : c);
722
+ pushHistorySnapshot(next, 'accept-all');
723
+ return next;
724
+ });
725
+ }, onFinalize: () => setShowFinalize(true) }))] }));
726
+ }