@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.
- package/README.md +64 -2
- package/dist/package.json +3 -2
- package/dist/src/analyze/command.d.ts +11 -0
- package/dist/src/analyze/command.js +298 -3
- package/dist/src/analyze/composite-closure.d.ts +25 -0
- package/dist/src/analyze/composite-closure.js +0 -0
- package/dist/src/analyze/composition/agent-parser/author-prompt.d.ts +4 -0
- package/dist/src/analyze/composition/agent-parser/author-prompt.js +22 -0
- package/dist/src/analyze/composition/agent-parser/extract-parser.d.ts +8 -0
- package/dist/src/analyze/composition/agent-parser/extract-parser.js +44 -0
- package/dist/src/analyze/composition/agent-parser/load-prompt.d.ts +7 -0
- package/dist/src/analyze/composition/agent-parser/load-prompt.js +31 -0
- package/dist/src/analyze/composition/agent-parser/resolve-via-parser.d.ts +46 -0
- package/dist/src/analyze/composition/agent-parser/resolve-via-parser.js +88 -0
- package/dist/src/analyze/composition/agent-parser/sandbox.d.ts +45 -0
- package/dist/src/analyze/composition/agent-parser/sandbox.js +187 -0
- package/dist/src/analyze/composition/apply-mapping.d.ts +22 -0
- package/dist/src/analyze/composition/apply-mapping.js +67 -0
- package/dist/src/analyze/composition/candidate-critic-agent.d.ts +7 -0
- package/dist/src/analyze/composition/candidate-critic-agent.js +39 -0
- package/dist/src/analyze/composition/candidate-critic.d.ts +32 -0
- package/dist/src/analyze/composition/candidate-critic.js +63 -0
- package/dist/src/analyze/composition/candidate-files.d.ts +34 -0
- package/dist/src/analyze/composition/candidate-files.js +131 -0
- package/dist/src/analyze/composition/composition-cache-key.d.ts +19 -0
- package/dist/src/analyze/composition/composition-cache-key.js +19 -0
- package/dist/src/analyze/composition/interchange-schema.d.ts +37 -0
- package/dist/src/analyze/composition/interchange-schema.js +61 -0
- package/dist/src/analyze/composition/merge-edges.d.ts +19 -0
- package/dist/src/analyze/composition/merge-edges.js +50 -0
- package/dist/src/analyze/composition/parse-map-edges.d.ts +16 -0
- package/dist/src/analyze/composition/parse-map-edges.js +55 -0
- package/dist/src/analyze/composition/resolve-mapping-cli.d.ts +23 -0
- package/dist/src/analyze/composition/resolve-mapping-cli.js +29 -0
- package/dist/src/analyze/composition/resolve-mapping.d.ts +59 -0
- package/dist/src/analyze/composition/resolve-mapping.js +94 -0
- package/dist/src/analyze/cycle-detection.d.ts +27 -0
- package/dist/src/analyze/cycle-detection.js +234 -0
- package/dist/src/analyze/cycle-view.d.ts +7 -0
- package/dist/src/analyze/cycle-view.js +28 -0
- package/dist/src/analyze/fuzzy-search.d.ts +3 -0
- package/dist/src/analyze/fuzzy-search.js +55 -0
- package/dist/src/analyze/issue-inheritance.d.ts +12 -0
- package/dist/src/analyze/issue-inheritance.js +108 -0
- package/dist/src/analyze/lineage.d.ts +29 -0
- package/dist/src/analyze/lineage.js +0 -0
- package/dist/src/analyze/scope-gate-cascade.d.ts +11 -0
- package/dist/src/analyze/scope-gate-cascade.js +170 -0
- package/dist/src/analyze/search-neighborhood.d.ts +3 -0
- package/dist/src/analyze/search-neighborhood.js +56 -0
- package/dist/src/analyze/select/tui/App.js +5 -16
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +0 -4
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +3 -15
- package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +29 -38
- package/dist/src/analyze/select/tui/components/FieldEditor.js +309 -185
- package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +13 -1
- package/dist/src/analyze/select/tui/components/FinalizeDialog.js +23 -2
- package/dist/src/analyze/select/tui/components/GotoBanner.d.ts +31 -0
- package/dist/src/analyze/select/tui/components/GotoBanner.js +30 -0
- package/dist/src/analyze/select/tui/components/GroupedSidebar.d.ts +91 -0
- package/dist/src/analyze/select/tui/components/GroupedSidebar.js +497 -0
- package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +13 -1
- package/dist/src/analyze/select/tui/components/HelpOverlay.js +9 -3
- package/dist/src/analyze/select/tui/components/JsonEditor.js +3 -3
- package/dist/src/analyze/select/tui/components/LineagePanel.d.ts +14 -0
- package/dist/src/analyze/select/tui/components/LineagePanel.js +22 -0
- package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +0 -7
- package/dist/src/analyze/select/tui/components/RationalePanel.js +3 -15
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +0 -6
- package/dist/src/analyze/select/tui/components/Sidebar.js +11 -27
- package/dist/src/analyze/select/tui/components/StatusBar.js +2 -1
- package/dist/src/analyze/select/tui/components/removed-components-text.d.ts +7 -0
- package/dist/src/analyze/select/tui/components/removed-components-text.js +11 -0
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +1 -5
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +8 -10
- package/dist/src/analyze/select/tui/theme.d.ts +15 -0
- package/dist/src/analyze/select/tui/theme.js +28 -0
- package/dist/src/analyze/selection-cascade.d.ts +3 -0
- package/dist/src/analyze/selection-cascade.js +19 -0
- package/dist/src/analyze/slot-graph.d.ts +11 -0
- package/dist/src/analyze/slot-graph.js +17 -0
- package/dist/src/apply/api-client.d.ts +2 -1
- package/dist/src/apply/api-client.js +40 -6
- package/dist/src/apply/command.d.ts +17 -1
- package/dist/src/apply/command.js +69 -11
- package/dist/src/apply/error-parser.d.ts +16 -0
- package/dist/src/apply/error-parser.js +117 -0
- package/dist/src/apply/tui/ServerPreviewView.js +3 -1
- package/dist/src/credentials-store.d.ts +6 -0
- package/dist/src/credentials-store.js +5 -1
- package/dist/src/generate/agent-runner.d.ts +6 -0
- package/dist/src/generate/agent-runner.js +24 -8
- package/dist/src/import/command.js +69 -31
- package/dist/src/import/cycle-auto-reject.d.ts +4 -0
- package/dist/src/import/cycle-auto-reject.js +14 -0
- package/dist/src/import/orchestrator.d.ts +20 -7
- package/dist/src/import/orchestrator.js +139 -35
- package/dist/src/import/strip-allowed-components.d.ts +18 -0
- package/dist/src/import/strip-allowed-components.js +22 -0
- package/dist/src/import/tui/CustomPromptBanner.d.ts +0 -5
- package/dist/src/import/tui/CustomPromptBanner.js +2 -1
- package/dist/src/import/tui/WizardApp.d.ts +10 -74
- package/dist/src/import/tui/WizardApp.js +234 -246
- package/dist/src/import/tui/ai-flag.d.ts +5 -0
- package/dist/src/import/tui/ai-flag.js +6 -0
- package/dist/src/import/tui/autocomplete.d.ts +5 -0
- package/dist/src/import/tui/autocomplete.js +32 -0
- package/dist/src/import/tui/components/AutoFilterBanner.d.ts +10 -0
- package/dist/src/import/tui/components/AutoFilterBanner.js +19 -0
- package/dist/src/import/tui/components/CounterStrip.d.ts +12 -0
- package/dist/src/import/tui/components/CounterStrip.js +12 -0
- package/dist/src/import/tui/components/LegendEntry.d.ts +2 -0
- package/dist/src/import/tui/components/LegendEntry.js +6 -0
- package/dist/src/import/tui/cycle-panel-scroll.d.ts +8 -0
- package/dist/src/import/tui/cycle-panel-scroll.js +33 -0
- package/dist/src/import/tui/final-review-host.d.ts +3 -7
- package/dist/src/import/tui/final-review-host.js +9 -4
- package/dist/src/import/tui/group-collapse.d.ts +2 -0
- package/dist/src/import/tui/group-collapse.js +11 -0
- package/dist/src/import/tui/history.d.ts +22 -0
- package/dist/src/import/tui/history.js +53 -0
- package/dist/src/import/tui/hooks/useLineage.d.ts +32 -0
- package/dist/src/import/tui/hooks/useLineage.js +50 -0
- package/dist/src/import/tui/hooks/useOverlayPanel.d.ts +13 -0
- package/dist/src/import/tui/hooks/useOverlayPanel.js +27 -0
- package/dist/src/import/tui/lineage-layout.d.ts +75 -0
- package/dist/src/import/tui/lineage-layout.js +82 -0
- package/dist/src/import/tui/runLivePreview.d.ts +11 -0
- package/dist/src/import/tui/runLivePreview.js +7 -1
- package/dist/src/import/tui/scope-gate-columns.d.ts +31 -0
- package/dist/src/import/tui/scope-gate-columns.js +94 -0
- package/dist/src/import/tui/scope-gate-host.d.ts +3 -1
- package/dist/src/import/tui/scope-gate-host.js +11 -3
- package/dist/src/import/tui/sidebar-width.d.ts +1 -0
- package/dist/src/import/tui/sidebar-width.js +3 -0
- package/dist/src/import/tui/step-filters.d.ts +19 -0
- package/dist/src/import/tui/step-filters.js +42 -0
- package/dist/src/import/tui/steps/AtomicGenerateReviewStep.d.ts +42 -0
- package/dist/src/import/tui/steps/AtomicGenerateReviewStep.js +726 -0
- package/dist/src/import/tui/steps/AtomicScopeGateStep.d.ts +3 -0
- package/dist/src/import/tui/steps/AtomicScopeGateStep.js +211 -0
- package/dist/src/import/tui/steps/CredentialsStep.js +5 -18
- package/dist/src/import/tui/steps/DoneStep.d.ts +0 -2
- package/dist/src/import/tui/steps/DoneStep.js +3 -2
- package/dist/src/import/tui/steps/ErrorStep.js +2 -1
- package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +19 -25
- package/dist/src/import/tui/steps/GenerateReviewStep.js +1258 -229
- package/dist/src/import/tui/steps/PathValidationStep.js +3 -2
- package/dist/src/import/tui/steps/PreviewStep.js +2 -1
- package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +0 -7
- package/dist/src/import/tui/steps/PushDecisionGateStep.js +3 -6
- package/dist/src/import/tui/steps/PushingStep.js +2 -1
- package/dist/src/import/tui/steps/RunningStep.d.ts +4 -1
- package/dist/src/import/tui/steps/RunningStep.js +3 -2
- package/dist/src/import/tui/steps/ScopeGateStep.d.ts +24 -0
- package/dist/src/import/tui/steps/ScopeGateStep.js +850 -193
- package/dist/src/import/tui/steps/TokenInputStep.js +2 -1
- package/dist/src/import/tui/steps/WelcomeStep.js +2 -1
- package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +6 -0
- package/dist/src/import/tui/steps/WizardPreviewStep.js +95 -56
- package/dist/src/import/tui/steps/auto-reject-decision.d.ts +7 -0
- package/dist/src/import/tui/steps/auto-reject-decision.js +9 -0
- package/dist/src/import/tui/steps/breaking-change-format.d.ts +2 -0
- package/dist/src/import/tui/steps/breaking-change-format.js +24 -0
- package/dist/src/import/tui/steps/enumerate-cycle-breaks.d.ts +14 -0
- package/dist/src/import/tui/steps/enumerate-cycle-breaks.js +0 -0
- package/dist/src/import/tui/steps/preview-diff.js +48 -5
- package/dist/src/import/tui/useFinalizePreview.d.ts +34 -0
- package/dist/src/import/tui/useFinalizePreview.js +0 -0
- package/dist/src/import/tui/useLivePreview.d.ts +3 -0
- package/dist/src/import/tui/useLivePreview.js +1 -0
- package/dist/src/import/tui/wizard-state-transitions.d.ts +5 -59
- package/dist/src/import/tui/wizard-state-transitions.js +5 -60
- package/dist/src/lib/composition-mode.d.ts +28 -0
- package/dist/src/lib/composition-mode.js +37 -0
- package/dist/src/lib/prompt-overrides.d.ts +38 -0
- package/dist/src/lib/prompt-overrides.js +69 -0
- package/dist/src/print/command.js +19 -1
- package/dist/src/runs/modify-launcher.d.ts +3 -0
- package/dist/src/runs/modify-launcher.js +2 -0
- package/dist/src/runs/replay-helpers.js +1 -0
- package/dist/src/runs/run-picker.js +2 -18
- package/dist/src/runs/store.d.ts +4 -0
- package/dist/src/session/db.d.ts +23 -40
- package/dist/src/session/db.js +110 -138
- package/dist/src/setup/composition-mode-prompt.d.ts +14 -0
- package/dist/src/setup/composition-mode-prompt.js +37 -0
- package/package.json +5 -4
- package/prompts/composition-dir-critic.md +7 -0
- package/prompts/composition-edges.md +8 -0
- package/prompts/composition-parser-repair-empty.md +1 -0
- package/prompts/composition-parser-repair-error.md +2 -0
- 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
|
+
}
|