@contentful/experience-design-system-cli 2.11.4-dev-build-d47ab65.0 → 2.12.1-dev-build-589b615.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 +214 -357
- package/dist/package.json +1 -1
- package/dist/src/analyze/extract/astro.js +9 -0
- package/dist/src/analyze/extract/react.js +23 -0
- package/dist/src/analyze/extract/stencil.js +3 -0
- package/dist/src/analyze/extract/vue.js +6 -0
- package/dist/src/analyze/extract/web-components.js +10 -0
- package/dist/src/analyze/pre-classify.d.ts +1 -1
- package/dist/src/analyze/pre-classify.js +3 -3
- package/dist/src/analyze/select/persistence.d.ts +13 -0
- package/dist/src/analyze/select/persistence.js +38 -0
- package/dist/src/analyze/select/preview-annotations.d.ts +26 -0
- package/dist/src/analyze/select/preview-annotations.js +62 -0
- package/dist/src/analyze/select/tui/App.js +5 -2
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +30 -0
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +137 -0
- package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +58 -1
- package/dist/src/analyze/select/tui/components/FieldEditor.js +637 -161
- package/dist/src/analyze/select/tui/components/FinalizeDialog.js +1 -1
- package/dist/src/analyze/select/tui/components/JsonPanel.js +5 -1
- package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +34 -0
- package/dist/src/analyze/select/tui/components/RationalePanel.js +92 -0
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +13 -1
- package/dist/src/analyze/select/tui/components/Sidebar.js +27 -3
- package/dist/src/analyze/select/tui/hooks/scroll-offset.d.ts +29 -0
- package/dist/src/analyze/select/tui/hooks/scroll-offset.js +26 -0
- package/dist/src/analyze/select-agent/command.d.ts +2 -0
- package/dist/src/analyze/select-agent/command.js +259 -45
- package/dist/src/analyze/select-agent/show-rationale.d.ts +35 -0
- package/dist/src/analyze/select-agent/show-rationale.js +92 -0
- package/dist/src/apply/command.js +7 -3
- package/dist/src/apply/tui/ServerApplyView.d.ts +3 -1
- package/dist/src/apply/tui/ServerApplyView.js +3 -2
- package/dist/src/credentials-store.d.ts +5 -0
- package/dist/src/credentials-store.js +7 -1
- package/dist/src/generate/agent-runner.d.ts +14 -0
- package/dist/src/generate/agent-runner.js +16 -0
- package/dist/src/generate/command.d.ts +6 -0
- package/dist/src/generate/command.js +45 -10
- package/dist/src/generate/progress.d.ts +9 -0
- package/dist/src/generate/progress.js +11 -0
- package/dist/src/generate/prompt-builder.d.ts +8 -0
- package/dist/src/generate/prompt-builder.js +22 -11
- package/dist/src/import/agent-model-resolve.d.ts +23 -0
- package/dist/src/import/agent-model-resolve.js +35 -0
- package/dist/src/import/auto-filter-resolve.d.ts +12 -0
- package/dist/src/import/auto-filter-resolve.js +16 -0
- package/dist/src/import/command.js +248 -8
- package/dist/src/import/orchestrator.d.ts +2 -0
- package/dist/src/import/orchestrator.js +11 -3
- package/dist/src/import/print-prompt.d.ts +35 -0
- package/dist/src/import/print-prompt.js +31 -0
- package/dist/src/import/tui/CustomPromptBanner.d.ts +11 -0
- package/dist/src/import/tui/CustomPromptBanner.js +7 -0
- package/dist/src/import/tui/WizardApp.d.ts +112 -8
- package/dist/src/import/tui/WizardApp.js +814 -333
- package/dist/src/import/tui/auto-filter-error.d.ts +2 -0
- package/dist/src/import/tui/auto-filter-error.js +29 -0
- package/dist/src/import/tui/final-review-host.d.ts +15 -0
- package/dist/src/import/tui/final-review-host.js +20 -0
- package/dist/src/import/tui/push-decision-gate-helpers.d.ts +8 -0
- package/dist/src/import/tui/push-decision-gate-helpers.js +8 -0
- package/dist/src/import/tui/push-progress.d.ts +23 -0
- package/dist/src/import/tui/push-progress.js +14 -0
- package/dist/src/import/tui/run-print-files-helpers.d.ts +26 -0
- package/dist/src/import/tui/run-print-files-helpers.js +8 -0
- package/dist/src/import/tui/run-teaser.d.ts +5 -0
- package/dist/src/import/tui/run-teaser.js +9 -0
- package/dist/src/import/tui/runLivePreview.d.ts +35 -0
- package/dist/src/import/tui/runLivePreview.js +73 -0
- package/dist/src/import/tui/runScopeGate.d.ts +22 -0
- package/dist/src/import/tui/runScopeGate.js +28 -0
- package/dist/src/import/tui/scope-gate-host.d.ts +21 -0
- package/dist/src/import/tui/scope-gate-host.js +20 -0
- package/dist/src/import/tui/spawn-generate.d.ts +33 -0
- package/dist/src/import/tui/spawn-generate.js +53 -0
- package/dist/src/import/tui/steps/CredentialsStep.d.ts +25 -1
- package/dist/src/import/tui/steps/CredentialsStep.js +31 -3
- package/dist/src/import/tui/steps/DoneStep.d.ts +6 -1
- package/dist/src/import/tui/steps/DoneStep.js +10 -4
- package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +29 -2
- package/dist/src/import/tui/steps/GenerateReviewStep.js +412 -48
- package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +19 -0
- package/dist/src/import/tui/steps/PushDecisionGateStep.js +76 -0
- package/dist/src/import/tui/steps/PushingStep.d.ts +10 -0
- package/dist/src/import/tui/steps/PushingStep.js +40 -0
- package/dist/src/import/tui/steps/ScopeGateStep.d.ts +23 -0
- package/dist/src/import/tui/steps/ScopeGateStep.js +243 -0
- package/dist/src/import/tui/steps/TokenInputStep.js +20 -3
- package/dist/src/import/tui/useLivePreview.d.ts +35 -0
- package/dist/src/import/tui/useLivePreview.js +120 -0
- package/dist/src/import/tui/wizard-generate-progress.d.ts +21 -0
- package/dist/src/import/tui/wizard-generate-progress.js +25 -0
- package/dist/src/import/tui/wizard-save-flow.d.ts +37 -0
- package/dist/src/import/tui/wizard-save-flow.js +20 -0
- package/dist/src/import/tui/wizard-state-transitions.d.ts +80 -0
- package/dist/src/import/tui/wizard-state-transitions.js +79 -0
- package/dist/src/lib/contentful-urls.d.ts +34 -0
- package/dist/src/lib/contentful-urls.js +41 -0
- package/dist/src/program.js +2 -0
- package/dist/src/runs/export-helpers.d.ts +30 -0
- package/dist/src/runs/export-helpers.js +59 -0
- package/dist/src/runs/fingerprint.d.ts +47 -0
- package/dist/src/runs/fingerprint.js +75 -0
- package/dist/src/runs/ls-command.d.ts +12 -0
- package/dist/src/runs/ls-command.js +137 -0
- package/dist/src/runs/modify-launcher.d.ts +24 -0
- package/dist/src/runs/modify-launcher.js +36 -0
- package/dist/src/runs/path-prompt.d.ts +8 -0
- package/dist/src/runs/path-prompt.js +72 -0
- package/dist/src/runs/push-creds-prompt.d.ts +25 -0
- package/dist/src/runs/push-creds-prompt.js +39 -0
- package/dist/src/runs/push-helpers.d.ts +29 -0
- package/dist/src/runs/push-helpers.js +69 -0
- package/dist/src/runs/replay-helpers.d.ts +67 -0
- package/dist/src/runs/replay-helpers.js +148 -0
- package/dist/src/runs/resolve-run-target.d.ts +12 -0
- package/dist/src/runs/resolve-run-target.js +45 -0
- package/dist/src/runs/run-picker-mount.d.ts +46 -0
- package/dist/src/runs/run-picker-mount.js +72 -0
- package/dist/src/runs/run-picker.d.ts +17 -0
- package/dist/src/runs/run-picker.js +145 -0
- package/dist/src/runs/save-conflict.d.ts +8 -0
- package/dist/src/runs/save-conflict.js +56 -0
- package/dist/src/runs/save-path-resolver.d.ts +55 -0
- package/dist/src/runs/save-path-resolver.js +71 -0
- package/dist/src/runs/staleness.d.ts +38 -0
- package/dist/src/runs/staleness.js +140 -0
- package/dist/src/runs/store.d.ts +78 -0
- package/dist/src/runs/store.js +136 -0
- package/dist/src/session/cache-keys.d.ts +15 -0
- package/dist/src/session/cache-keys.js +38 -0
- package/dist/src/session/db.d.ts +89 -3
- package/dist/src/session/db.js +447 -104
- package/dist/src/setup/auto-filter-prompt.d.ts +13 -0
- package/dist/src/setup/auto-filter-prompt.js +24 -0
- package/dist/src/setup/command.d.ts +7 -0
- package/dist/src/setup/command.js +58 -2
- package/dist/src/types.d.ts +6 -0
- package/package.json +2 -2
- package/skills/generate-components.md +54 -6
- package/skills/select-components.md +13 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { Box, Text, useStdout } from 'ink';
|
|
4
4
|
import { Sidebar } from '../../../analyze/select/tui/components/Sidebar.js';
|
|
5
5
|
import { JsonPanel } from '../../../analyze/select/tui/components/JsonPanel.js';
|
|
@@ -8,10 +8,34 @@ import { StatusBar } from '../../../analyze/select/tui/components/StatusBar.js';
|
|
|
8
8
|
import { FinalizeDialog } from '../../../analyze/select/tui/components/FinalizeDialog.js';
|
|
9
9
|
import { QuitDialog } from '../../../analyze/select/tui/components/QuitDialog.js';
|
|
10
10
|
import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
|
|
11
|
-
import { openPipelineDb, loadCDFComponents, storeCDFComponents } from '../../../session/db.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 { useLivePreview } from '../useLivePreview.js';
|
|
16
|
+
import { computeNextScrollOffset } from '../../../analyze/select/tui/hooks/scroll-offset.js';
|
|
17
|
+
/**
|
|
18
|
+
* Sort components for the final-review sidebar so the underlying data array
|
|
19
|
+
* matches the visual order. Empty components (zero classified $properties)
|
|
20
|
+
* surface at the top via the warning-tier path in Sidebar.tsx; we mirror that
|
|
21
|
+
* here so `selectedIdx` indexes into the same order the user sees. Without
|
|
22
|
+
* this, j/k navigation lands on different rows than the visually-selected
|
|
23
|
+
* one (INTEG-4259).
|
|
24
|
+
*
|
|
25
|
+
* Within each tier (empty / non-empty) we tie-break alphabetically by `key`.
|
|
26
|
+
*/
|
|
27
|
+
export function sortComponentsForSidebar(components) {
|
|
28
|
+
return [...components].sort((a, b) => {
|
|
29
|
+
const aEmpty = Object.keys(a.entry.$properties ?? {}).length === 0;
|
|
30
|
+
const bEmpty = Object.keys(b.entry.$properties ?? {}).length === 0;
|
|
31
|
+
if (aEmpty !== bEmpty)
|
|
32
|
+
return aEmpty ? -1 : 1;
|
|
33
|
+
return a.key.localeCompare(b.key);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
12
36
|
const VISIBLE_COUNT = 20;
|
|
13
37
|
const PANEL_HEIGHT = 22;
|
|
14
|
-
export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, }) {
|
|
38
|
+
export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, livePreview = true, spaceId = '', environmentId = '', cmaToken = '', host = '', tokensPath = '', }) {
|
|
15
39
|
const { stdout } = useStdout();
|
|
16
40
|
const terminalWidth = stdout?.columns ?? 80;
|
|
17
41
|
const [components, setComponents] = useState([]);
|
|
@@ -23,9 +47,58 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, }) {
|
|
|
23
47
|
const [sidebarFocused, setSidebarFocused] = useState(true);
|
|
24
48
|
const [showFinalize, setShowFinalize] = useState(false);
|
|
25
49
|
const [showQuit, setShowQuit] = useState(false);
|
|
26
|
-
|
|
50
|
+
// FieldEditor is the default editor. JSON view is an opt-in read-only toggle.
|
|
51
|
+
const [showJson, setShowJson] = useState(false);
|
|
27
52
|
const [draftValue, setDraftValue] = useState('');
|
|
28
53
|
const [saveError, setSaveError] = useState(null);
|
|
54
|
+
// Feature 1: per-component review metadata (rationale + source location)
|
|
55
|
+
// for the currently-selected component. Reloaded when selection changes.
|
|
56
|
+
const [reviewMetadata, setReviewMetadata] = useState(null);
|
|
57
|
+
// Feature 2: per-component preview annotations refreshed after every
|
|
58
|
+
// FieldEditor save via the useLivePreview hook below. Empty when live
|
|
59
|
+
// preview is disabled, when creds are missing, or before the first response.
|
|
60
|
+
const [previewAnnotations, setPreviewAnnotations] = useState(new Map());
|
|
61
|
+
// Pilot-2026-06-24: raw removed list for the `d` detail panel. The
|
|
62
|
+
// annotation map only carries kind, not the rich summaries we need to list
|
|
63
|
+
// names/ids when the operator asks "which ones?".
|
|
64
|
+
const [removedComponents, setRemovedComponents] = useState([]);
|
|
65
|
+
const [showRemovedPanel, setShowRemovedPanel] = useState(false);
|
|
66
|
+
// Lifted rationale + source panels (replaces FieldEditor's right pane).
|
|
67
|
+
// Mutually exclusive states.
|
|
68
|
+
const [panelOpen, setPanelOpen] = useState('none');
|
|
69
|
+
const [panelScrollOffset, setPanelScrollOffset] = useState(0);
|
|
70
|
+
const [textEntryActive, setTextEntryActive] = useState(false);
|
|
71
|
+
const [componentRationale, setComponentRationale] = useState(null);
|
|
72
|
+
// Tracks the first `g` of a potential `gg` double-tap (jumps to top in
|
|
73
|
+
// JSON-view + panel-focused state). Reset on any non-`g` key.
|
|
74
|
+
const pendingGRef = useRef(false);
|
|
75
|
+
const handleLivePreviewResult = (response) => {
|
|
76
|
+
if (!response)
|
|
77
|
+
return;
|
|
78
|
+
setPreviewAnnotations(applyPreviewAnnotations(response, components.map((c) => c.key)));
|
|
79
|
+
setRemovedComponents(response.components.removed ?? []);
|
|
80
|
+
};
|
|
81
|
+
const livePreviewHook = useLivePreview({
|
|
82
|
+
enabled: livePreview,
|
|
83
|
+
sessionId: extractSessionId,
|
|
84
|
+
tokensPath,
|
|
85
|
+
spaceId,
|
|
86
|
+
environmentId,
|
|
87
|
+
cmaToken,
|
|
88
|
+
host,
|
|
89
|
+
onResult: handleLivePreviewResult,
|
|
90
|
+
});
|
|
91
|
+
// Manual spinner cycling (no extra dep) for the sidebar status-row
|
|
92
|
+
// indicator. Runs only while the live-preview hook reports `running`.
|
|
93
|
+
const SPINNER_FRAMES = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏';
|
|
94
|
+
const [spinnerTick, setSpinnerTick] = useState(0);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (livePreviewHook.status !== 'running')
|
|
97
|
+
return;
|
|
98
|
+
const id = setInterval(() => setSpinnerTick((t) => t + 1), 80);
|
|
99
|
+
return () => clearInterval(id);
|
|
100
|
+
}, [livePreviewHook.status]);
|
|
101
|
+
const livePreviewSpinner = SPINNER_FRAMES[spinnerTick % SPINNER_FRAMES.length];
|
|
29
102
|
useEffect(() => {
|
|
30
103
|
async function load() {
|
|
31
104
|
const db = openPipelineDb();
|
|
@@ -41,7 +114,12 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, }) {
|
|
|
41
114
|
setLoading(false);
|
|
42
115
|
return;
|
|
43
116
|
}
|
|
44
|
-
|
|
117
|
+
const reviewEntries = cdfComponents.map(({ key, entry }) => ({
|
|
118
|
+
key,
|
|
119
|
+
entry,
|
|
120
|
+
status: 'needs-review',
|
|
121
|
+
}));
|
|
122
|
+
setComponents(sortComponentsForSidebar(reviewEntries));
|
|
45
123
|
setLoading(false);
|
|
46
124
|
}
|
|
47
125
|
load().catch((e) => {
|
|
@@ -49,18 +127,81 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, }) {
|
|
|
49
127
|
setLoading(false);
|
|
50
128
|
});
|
|
51
129
|
}, []);
|
|
130
|
+
// Pilot-2026-06-23 R2: fire the live preview once on entry to final-review
|
|
131
|
+
// so diff badges populate before the operator's first save. We gate on the
|
|
132
|
+
// livePreview prop to honor --no-live-preview without depending on the
|
|
133
|
+
// hook's internal short-circuit. Cred-missing is still handled by the
|
|
134
|
+
// hook's own no-op path.
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (loading)
|
|
137
|
+
return;
|
|
138
|
+
if (!livePreview)
|
|
139
|
+
return;
|
|
140
|
+
if (components.length === 0)
|
|
141
|
+
return;
|
|
142
|
+
livePreviewHook.trigger();
|
|
143
|
+
// Intentionally only on load completion — subsequent fires happen via
|
|
144
|
+
// handleEditSave. Adding livePreviewHook to deps would re-fire on every
|
|
145
|
+
// hook re-creation.
|
|
146
|
+
}, [loading]);
|
|
147
|
+
// Feature 1: load review metadata (rationale + source location) for the
|
|
148
|
+
// selected component when selection changes.
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
const current = components[selectedIdx];
|
|
151
|
+
if (!current) {
|
|
152
|
+
setReviewMetadata(null);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const db = openPipelineDb();
|
|
156
|
+
try {
|
|
157
|
+
setReviewMetadata(loadComponentReviewMetadata(db, extractSessionId, current.key));
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
setReviewMetadata(null);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
db.close();
|
|
164
|
+
}
|
|
165
|
+
}, [selectedIdx, components, extractSessionId]);
|
|
166
|
+
// Load component-level rationale for the selected component (drives the
|
|
167
|
+
// `I` ComponentRationalePanel). Decoupled from review metadata so the data
|
|
168
|
+
// contracts can evolve independently.
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
const current = components[selectedIdx];
|
|
171
|
+
if (!current) {
|
|
172
|
+
setComponentRationale(null);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const db = openPipelineDb();
|
|
176
|
+
try {
|
|
177
|
+
setComponentRationale(loadComponentRationale(db, extractSessionId, current.key));
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
setComponentRationale(null);
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
db.close();
|
|
184
|
+
}
|
|
185
|
+
}, [selectedIdx, components, extractSessionId]);
|
|
52
186
|
const updateStatus = (idx, status) => {
|
|
53
187
|
setComponents((prev) => prev.map((c, i) => (i === idx ? { ...c, status } : c)));
|
|
54
188
|
};
|
|
55
189
|
const handleFinalizeConfirm = () => {
|
|
56
|
-
|
|
57
|
-
|
|
190
|
+
// Strict opt-in: only EXPLICITLY ACCEPTED components ship. Anything left
|
|
191
|
+
// in 'needs-review' OR explicitly 'rejected' is downgraded to
|
|
192
|
+
// 'generate-rejected' so loadCDFComponents excludes it from the manifest.
|
|
193
|
+
// The operator told us they want accept-to-ship semantics — leaving a
|
|
194
|
+
// component unresolved should NOT silently push it (Pilot-2026-06-24 R2).
|
|
195
|
+
const explicitlyRejected = components.filter((c) => c.status === 'rejected').map((c) => c.key);
|
|
196
|
+
const unresolved = components.filter((c) => c.status === 'needs-review').map((c) => c.key);
|
|
197
|
+
const toReject = [...explicitlyRejected, ...unresolved];
|
|
198
|
+
if (toReject.length > 0) {
|
|
58
199
|
const db = openPipelineDb();
|
|
59
200
|
try {
|
|
60
201
|
const stmt = db.prepare(`UPDATE raw_components SET status = 'generate-rejected' WHERE session_id = ? AND name = ?`);
|
|
61
202
|
db.exec('BEGIN');
|
|
62
203
|
try {
|
|
63
|
-
for (const name of
|
|
204
|
+
for (const name of toReject) {
|
|
64
205
|
stmt.run(extractSessionId, name);
|
|
65
206
|
}
|
|
66
207
|
db.exec('COMMIT');
|
|
@@ -74,8 +215,8 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, }) {
|
|
|
74
215
|
db.close();
|
|
75
216
|
}
|
|
76
217
|
}
|
|
77
|
-
const
|
|
78
|
-
onFinalize(
|
|
218
|
+
const acceptedCount = components.filter((c) => c.status === 'accepted').length;
|
|
219
|
+
onFinalize(acceptedCount, explicitlyRejected.length, unresolved.length);
|
|
79
220
|
};
|
|
80
221
|
const handleEditSave = () => {
|
|
81
222
|
const current = components[selectedIdx];
|
|
@@ -93,7 +234,6 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, }) {
|
|
|
93
234
|
return;
|
|
94
235
|
}
|
|
95
236
|
setComponents((prev) => prev.map((c, i) => i === selectedIdx ? { ...c, entry, status: c.status === 'needs-review' ? 'accepted' : c.status } : c));
|
|
96
|
-
setEditMode(false);
|
|
97
237
|
setDraftValue('');
|
|
98
238
|
setSaveError(null);
|
|
99
239
|
const db = openPipelineDb();
|
|
@@ -103,13 +243,15 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, }) {
|
|
|
103
243
|
finally {
|
|
104
244
|
db.close();
|
|
105
245
|
}
|
|
246
|
+
// Feature 2: re-fire the live preview now that pipeline.db reflects
|
|
247
|
+
// the new state. The hook owns debounce + cred-missing short-circuit.
|
|
248
|
+
livePreviewHook.trigger();
|
|
106
249
|
}
|
|
107
250
|
catch (e) {
|
|
108
251
|
setSaveError(String(e));
|
|
109
252
|
}
|
|
110
253
|
};
|
|
111
254
|
const handleEditDiscard = () => {
|
|
112
|
-
setEditMode(false);
|
|
113
255
|
setDraftValue('');
|
|
114
256
|
setSaveError(null);
|
|
115
257
|
};
|
|
@@ -119,20 +261,148 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, }) {
|
|
|
119
261
|
return;
|
|
120
262
|
if (dialogOpen)
|
|
121
263
|
return;
|
|
122
|
-
|
|
264
|
+
// Pilot-2026-06-24: removed-detail panel. When open, only `d` (toggle)
|
|
265
|
+
// and Esc (close) respond — all other input is swallowed so j/k/Enter/
|
|
266
|
+
// Ctrl+S can't move state behind the modal. Mirrors the `?` overlay
|
|
267
|
+
// pattern from 8f0c62e in FieldEditor.
|
|
268
|
+
if (showRemovedPanel) {
|
|
269
|
+
if (input === 'd' || key.escape) {
|
|
270
|
+
setShowRemovedPanel(false);
|
|
271
|
+
}
|
|
123
272
|
return;
|
|
124
|
-
|
|
125
|
-
|
|
273
|
+
}
|
|
274
|
+
// `d` opens the panel only when live-preview is enabled and there is at
|
|
275
|
+
// least one removed component to display. Sidebar-focused only so it
|
|
276
|
+
// doesn't collide with FieldEditor input.
|
|
277
|
+
if (input === 'd' && sidebarFocused && livePreview && removedComponents.length > 0) {
|
|
278
|
+
setShowRemovedPanel(true);
|
|
126
279
|
return;
|
|
127
280
|
}
|
|
128
|
-
|
|
129
|
-
|
|
281
|
+
// Lifted rationale + source panels: i/I/s fire from anywhere (sidebar OR
|
|
282
|
+
// panel focus). Gated against text-entry surfaces inside FieldEditor
|
|
283
|
+
// (description editors, string-default editor, value-list text entry)
|
|
284
|
+
// via the `onTextEntryActiveChange` callback, plus the help/finalize/quit
|
|
285
|
+
// overlays and the JSON view.
|
|
286
|
+
if (panelOpen !== 'none') {
|
|
287
|
+
const PANEL_HEIGHT_LOCAL = 12;
|
|
288
|
+
const next = computeNextScrollOffset(panelScrollOffset, input, key, 9999, PANEL_HEIGHT_LOCAL);
|
|
289
|
+
if (next !== null) {
|
|
290
|
+
setPanelScrollOffset(() => next);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (key.escape) {
|
|
294
|
+
setPanelOpen('none');
|
|
295
|
+
setPanelScrollOffset(() => 0);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// Guard against Ctrl-letter aliases (Tab is Ctrl+I in ASCII, Ctrl+S would
|
|
299
|
+
// collide with save in nested editors). Only react to bare keystrokes.
|
|
300
|
+
const togglable = !key.ctrl && !key.tab && !key.meta && !key.return;
|
|
301
|
+
if (togglable && input === 'i' && panelOpen === 'prop-rationale') {
|
|
302
|
+
setPanelOpen('none');
|
|
303
|
+
setPanelScrollOffset(() => 0);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (togglable && input === 'I' && panelOpen === 'component-rationale') {
|
|
307
|
+
setPanelOpen('none');
|
|
308
|
+
setPanelScrollOffset(() => 0);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (togglable && input === 's' && panelOpen === 'source') {
|
|
312
|
+
setPanelOpen('none');
|
|
313
|
+
setPanelScrollOffset(() => 0);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
// Cross-panel toggles while one is open.
|
|
317
|
+
if (togglable && input === 'i') {
|
|
318
|
+
setPanelOpen('prop-rationale');
|
|
319
|
+
setPanelScrollOffset(() => 0);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (togglable && input === 'I') {
|
|
323
|
+
setPanelOpen('component-rationale');
|
|
324
|
+
setPanelScrollOffset(() => 0);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (togglable && input === 's') {
|
|
328
|
+
setPanelOpen('source');
|
|
329
|
+
setPanelScrollOffset(() => 0);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
130
332
|
return;
|
|
131
333
|
}
|
|
334
|
+
const rationaleKeyOk = !textEntryActive && !showJson && !key.ctrl && !key.tab && !key.meta && !key.return;
|
|
335
|
+
if (rationaleKeyOk) {
|
|
336
|
+
if (input === 'i') {
|
|
337
|
+
setPanelOpen('prop-rationale');
|
|
338
|
+
setPanelScrollOffset(() => 0);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (input === 'I') {
|
|
342
|
+
setPanelOpen('component-rationale');
|
|
343
|
+
setPanelScrollOffset(() => 0);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Tab toggles focus bidirectionally between sidebar and panel. `e` is a
|
|
348
|
+
// sidebar-only alias for crossing INTO the panel — gating it to the
|
|
349
|
+
// sidebar-focused state prevents collision with FieldEditor's enum-values
|
|
350
|
+
// `e` binding (INTEG-4254) when the panel is focused. Crossing back from
|
|
351
|
+
// panel to sidebar is Tab-only.
|
|
132
352
|
if (key.tab) {
|
|
133
353
|
setSidebarFocused((prev) => !prev);
|
|
134
354
|
return;
|
|
135
355
|
}
|
|
356
|
+
if (input === 'e' && sidebarFocused) {
|
|
357
|
+
setSidebarFocused(false);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// JSON view + panel focused: own j/k/arrows/PageUp/PageDown/Ctrl+u/d/gg/G
|
|
361
|
+
// for scrolling. Computed against the live `selectedJson` so the
|
|
362
|
+
// viewport math matches what JsonPanel renders.
|
|
363
|
+
if (!sidebarFocused && showJson) {
|
|
364
|
+
const current = components[selectedIdx];
|
|
365
|
+
const currentJson = current ? JSON.stringify({ [current.key]: current.entry }, null, 2) : '';
|
|
366
|
+
const totalLines = currentJson.split('\n').length;
|
|
367
|
+
const maxOffset = Math.max(0, totalLines - PANEL_HEIGHT);
|
|
368
|
+
// `gg` double-tap to jump to top; single `g` arms the pending flag.
|
|
369
|
+
if (input === 'g' && !key.ctrl) {
|
|
370
|
+
if (pendingGRef.current) {
|
|
371
|
+
pendingGRef.current = false;
|
|
372
|
+
setJsonScrollOffset(() => 0);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
pendingGRef.current = true;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const next = computeNextScrollOffset(jsonScrollOffset, input, key, totalLines, PANEL_HEIGHT);
|
|
379
|
+
if (next !== null) {
|
|
380
|
+
pendingGRef.current = false;
|
|
381
|
+
// Functional setState mirrors the cursor-stutter fix (commit 5d11e60).
|
|
382
|
+
// Clamp against maxOffset re-computed at apply time in case totalLines
|
|
383
|
+
// shifted between events (defensive — helper already clamps).
|
|
384
|
+
const clamped = Math.min(maxOffset, Math.max(0, next));
|
|
385
|
+
setJsonScrollOffset(() => clamped);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
// Any other key in this slice resets the gg-pending flag, then falls
|
|
389
|
+
// through to the early-return below so the panel-focused state still
|
|
390
|
+
// swallows non-scroll input.
|
|
391
|
+
pendingGRef.current = false;
|
|
392
|
+
}
|
|
393
|
+
// When the panel is focused, FieldEditor (or JsonPanel) owns the keys.
|
|
394
|
+
// Only Tab (handled above) should escape from the panel-focused state.
|
|
395
|
+
if (!sidebarFocused)
|
|
396
|
+
return;
|
|
397
|
+
// Sidebar-focused keymap.
|
|
398
|
+
if (input === 'q') {
|
|
399
|
+
setShowQuit(true);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (input === 'F') {
|
|
403
|
+
setShowFinalize(true);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
136
406
|
if (input === 'a') {
|
|
137
407
|
updateStatus(selectedIdx, 'accepted');
|
|
138
408
|
return;
|
|
@@ -145,32 +415,41 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, }) {
|
|
|
145
415
|
setComponents((prev) => prev.map((c) => (c.status === 'needs-review' ? { ...c, status: 'accepted' } : c)));
|
|
146
416
|
return;
|
|
147
417
|
}
|
|
148
|
-
if (input === '
|
|
149
|
-
|
|
150
|
-
|
|
418
|
+
if (input === 'J') {
|
|
419
|
+
// Toggle read-only JSON view.
|
|
420
|
+
setShowJson((prev) => !prev);
|
|
421
|
+
setJsonScrollOffset(0);
|
|
422
|
+
pendingGRef.current = false;
|
|
151
423
|
return;
|
|
152
424
|
}
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
425
|
+
if (key.upArrow || input === 'k') {
|
|
426
|
+
// Pilot-2026-06-23 bug: rapid k/j bursts could lose cursor position
|
|
427
|
+
// because the previous implementation read `selectedIdx` from the
|
|
428
|
+
// handler's closure. Under high keyboard-repeat rate multiple key
|
|
429
|
+
// events fire between Ink render flushes, so every invocation saw the
|
|
430
|
+
// same stale value and recomputed the same `newIdx`. Using functional
|
|
431
|
+
// setState chains the updates correctly: each pending update sees the
|
|
432
|
+
// post-update value of the previous one. The viewport offset update is
|
|
433
|
+
// nested inside the cursor updater so it always reflects the same
|
|
434
|
+
// newIdx that selectedIdx is being set to.
|
|
435
|
+
setSelectedIdx((prev) => {
|
|
436
|
+
const newIdx = Math.max(0, prev - 1);
|
|
437
|
+
setSidebarScrollOffset((off) => Math.min(off, newIdx));
|
|
438
|
+
return newIdx;
|
|
439
|
+
});
|
|
440
|
+
setJsonScrollOffset(0);
|
|
441
|
+
setDraftValue('');
|
|
442
|
+
setSaveError(null);
|
|
166
443
|
}
|
|
167
|
-
else {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
444
|
+
else if (key.downArrow || input === 'j') {
|
|
445
|
+
setSelectedIdx((prev) => {
|
|
446
|
+
const newIdx = Math.min(components.length - 1, prev + 1);
|
|
447
|
+
setSidebarScrollOffset((off) => (newIdx >= off + VISIBLE_COUNT ? newIdx - VISIBLE_COUNT + 1 : off));
|
|
448
|
+
return newIdx;
|
|
449
|
+
});
|
|
450
|
+
setJsonScrollOffset(0);
|
|
451
|
+
setDraftValue('');
|
|
452
|
+
setSaveError(null);
|
|
174
453
|
}
|
|
175
454
|
});
|
|
176
455
|
if (loading) {
|
|
@@ -181,32 +460,117 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, }) {
|
|
|
181
460
|
}
|
|
182
461
|
const selected = components[selectedIdx] ?? null;
|
|
183
462
|
const selectedJson = selected ? JSON.stringify({ [selected.key]: selected.entry }, null, 2) : '';
|
|
463
|
+
// A component with zero classified $properties is a real defensibility issue —
|
|
464
|
+
// it can't be pushed to Contentful (no fields). Surface it in the sidebar via
|
|
465
|
+
// the existing warning-color path (yellow) and a "(empty)" suffix so the user
|
|
466
|
+
// can see what went wrong. They can manually add props in FieldEditor or
|
|
467
|
+
// explicitly reject the component.
|
|
468
|
+
const isEmpty = (c) => Object.keys(c.entry.$properties).length === 0;
|
|
469
|
+
const emptyCount = components.filter(isEmpty).length;
|
|
184
470
|
const sidebarItems = components.map((c) => ({
|
|
185
471
|
id: c.key,
|
|
186
|
-
name: c.key,
|
|
472
|
+
name: isEmpty(c) ? `${c.key} (empty)` : c.key,
|
|
187
473
|
status: c.status,
|
|
474
|
+
previewAnnotation: previewAnnotations.get(c.key),
|
|
188
475
|
extractionConfidence: null,
|
|
189
476
|
needsReview: false,
|
|
190
477
|
validationErrorCount: 0,
|
|
191
|
-
validationWarningCount: 0,
|
|
478
|
+
validationWarningCount: isEmpty(c) ? 1 : 0,
|
|
192
479
|
}));
|
|
193
|
-
|
|
194
|
-
|
|
480
|
+
// Account for the "(empty)" suffix added to zero-prop component names so the
|
|
481
|
+
// sidebar doesn't truncate it.
|
|
482
|
+
const longestName = components.reduce((m, c) => Math.max(m, c.key.length + (isEmpty(c) ? ' (empty)'.length : 0)), 0);
|
|
483
|
+
// +5 = border (1) + status icon (1) + badge column (1) + space (1) + border (1).
|
|
484
|
+
// The badge column is reserved even when no annotation is present so the
|
|
485
|
+
// sidebar width doesn't jitter as live-preview annotations flip in/out.
|
|
486
|
+
const sidebarWidth = Math.min(Math.max(longestName + 5, 14), 30);
|
|
195
487
|
const panelWidth = Math.max(10, terminalWidth - sidebarWidth - 4);
|
|
196
488
|
const accepted = components.filter((c) => c.status === 'accepted').length;
|
|
197
489
|
const rejected = components.filter((c) => c.status === 'rejected').length;
|
|
198
490
|
const needsReview = components.filter((c) => c.status === 'needs-review').length;
|
|
199
491
|
const propCount = selected ? Object.keys(selected.entry.$properties).length : 0;
|
|
200
492
|
const slotCount = selected?.entry.$slots ? Object.keys(selected.entry.$slots).length : 0;
|
|
201
|
-
return (_jsxs(Box, { flexDirection: "column", children: [showFinalize && (_jsx(FinalizeDialog, { accepted: accepted, rejected: rejected, needsReview: needsReview, onConfirm: handleFinalizeConfirm, onCancel: () => setShowFinalize(false) })), showQuit && _jsx(QuitDialog, { hasUnsavedDrafts: false, onConfirm: onQuit, onCancel: () => setShowQuit(false) }), !dialogOpen && (_jsxs(Box, { children: [_jsx(
|
|
493
|
+
return (_jsxs(Box, { flexDirection: "column", children: [showFinalize && (_jsx(FinalizeDialog, { accepted: accepted, rejected: rejected, needsReview: needsReview, onConfirm: handleFinalizeConfirm, onCancel: () => setShowFinalize(false) })), showQuit && _jsx(QuitDialog, { hasUnsavedDrafts: false, onConfirm: onQuit, onCancel: () => setShowQuit(false) }), showRemovedPanel && !dialogOpen && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", 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 &&
|
|
494
|
+
livePreview &&
|
|
495
|
+
(() => {
|
|
496
|
+
// Pilot-2026-06-23 R2: at-a-glance diff summary at the top of the
|
|
497
|
+
// step. Mutually exclusive states:
|
|
498
|
+
// - hook running (and we don't yet have annotations) → spinner.
|
|
499
|
+
// - hook disabled (creds rejected) → static disabled hint.
|
|
500
|
+
// - annotations populated → counts.
|
|
501
|
+
// - idle, no annotations, not disabled → render nothing.
|
|
502
|
+
const counts = { new: 0, changed: 0, removed: 0, breaking: 0 };
|
|
503
|
+
for (const v of previewAnnotations.values()) {
|
|
504
|
+
counts[v] = (counts[v] ?? 0) + 1;
|
|
505
|
+
}
|
|
506
|
+
const hasCounts = counts.new + counts.changed + counts.removed + counts.breaking > 0;
|
|
507
|
+
if (livePreviewHook.disabled) {
|
|
508
|
+
return _jsx(Text, { dimColor: true, children: 'Preview: disabled (creds rejected)' });
|
|
509
|
+
}
|
|
510
|
+
if (livePreviewHook.status === 'running' && !hasCounts) {
|
|
511
|
+
return _jsx(Text, { dimColor: true, children: `Preview: ${livePreviewSpinner} running...` });
|
|
512
|
+
}
|
|
513
|
+
if (!hasCounts)
|
|
514
|
+
return null;
|
|
515
|
+
return (_jsxs(Box, { children: [_jsx(Text, { children: 'Preview: ' }), _jsx(Text, { color: "green", children: `${counts.new} new` }), _jsx(Text, { children: ' · ' }), _jsx(Text, { color: "yellow", 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: "red", bold: true, children: `${counts.breaking} breaking` })] }));
|
|
516
|
+
})(), !dialogOpen && emptyCount > 0 && (_jsx(Text, { color: "yellow", children: `⚠ ${emptyCount} component${emptyCount === 1 ? '' : 's'} had no classifiable props — review with care` })), !dialogOpen && (_jsxs(Box, { children: [_jsx(Sidebar, { components: sidebarItems, selectedId: selected?.key ?? null, focused: sidebarFocused, scrollOffset: sidebarScrollOffset, visibleCount: VISIBLE_COUNT, onSelect: (id) => {
|
|
202
517
|
const idx = components.findIndex((c) => c.key === id);
|
|
203
518
|
if (idx >= 0) {
|
|
204
519
|
setSelectedIdx(idx);
|
|
205
520
|
setJsonScrollOffset(0);
|
|
206
521
|
}
|
|
207
|
-
}, 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 ? '[Tab] focus panel' : '[Tab] focus list'] })] }),
|
|
208
|
-
|
|
209
|
-
|
|
522
|
+
}, 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' ? ((() => {
|
|
523
|
+
const rows = [
|
|
524
|
+
...(componentRationale?.props ?? []).map((p) => ({
|
|
525
|
+
name: p.name,
|
|
526
|
+
kind: 'prop',
|
|
527
|
+
rationale: p.rationale ?? '',
|
|
528
|
+
})),
|
|
529
|
+
...(componentRationale?.slots ?? []).map((s) => ({
|
|
530
|
+
name: s.name,
|
|
531
|
+
kind: 'slot',
|
|
532
|
+
rationale: s.rationale ?? '',
|
|
533
|
+
})),
|
|
534
|
+
];
|
|
535
|
+
return (_jsx(RationalePanel, { componentName: componentRationale?.name ?? selected.key, rows: rows, scrollOffset: panelScrollOffset, width: panelWidth, height: PANEL_HEIGHT, active: true }));
|
|
536
|
+
})()) : panelOpen === 'component-rationale' ? (_jsx(ComponentRationalePanel, { data: componentRationale ?? {
|
|
537
|
+
name: selected.key,
|
|
538
|
+
description: null,
|
|
539
|
+
descriptionRationale: null,
|
|
540
|
+
propsRationale: null,
|
|
541
|
+
slotsRationale: null,
|
|
542
|
+
props: [],
|
|
543
|
+
slots: [],
|
|
544
|
+
}, scrollOffset: panelScrollOffset, width: panelWidth, height: PANEL_HEIGHT, active: true })) : panelOpen === 'source' ? ((() => {
|
|
545
|
+
const path = reviewMetadata?.sourcePath ?? null;
|
|
546
|
+
const src = reviewMetadata?.componentSource ?? null;
|
|
547
|
+
const headerPath = path ?? '<unknown source path>';
|
|
548
|
+
const lines = src ? src.split('\n').slice(panelScrollOffset, panelScrollOffset + PANEL_HEIGHT) : [];
|
|
549
|
+
return (_jsxs(Box, { flexDirection: "column", width: panelWidth, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: `source: ${headerPath}` }), src ? (lines.map((ln, i) => (_jsx(Text, { dimColor: true, children: ln }, `source-line-${i}`)))) : (_jsx(Text, { dimColor: true, children: '(no source captured)' })), _jsx(Text, { dimColor: true, children: '[s/Esc] close' })] }));
|
|
550
|
+
})()) : 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
|
|
551
|
+
? {
|
|
552
|
+
sourcePath: reviewMetadata.sourcePath,
|
|
553
|
+
componentSource: reviewMetadata.componentSource,
|
|
554
|
+
props: reviewMetadata.props,
|
|
555
|
+
}
|
|
556
|
+
: undefined, onTogglePropRationale: () => {
|
|
557
|
+
setPanelOpen('prop-rationale');
|
|
558
|
+
setPanelScrollOffset(() => 0);
|
|
559
|
+
}, onToggleComponentRationale: () => {
|
|
560
|
+
setPanelOpen('component-rationale');
|
|
561
|
+
setPanelScrollOffset(() => 0);
|
|
562
|
+
}, onToggleSourceExternal: () => {
|
|
563
|
+
setPanelOpen('source');
|
|
564
|
+
setPanelScrollOffset(() => 0);
|
|
565
|
+
}, onTextEntryActiveChange: setTextEntryActive }, selected.key)), saveError && _jsx(Text, { color: "red", children: '✗ ' + saveError }), _jsxs(Text, { dimColor: true, children: [sidebarFocused
|
|
566
|
+
? ' [a] accept [r] reject [A] accept all [J] ' +
|
|
567
|
+
(showJson ? 'hide JSON' : 'show JSON') +
|
|
568
|
+
' [F] finalize [e/Tab] focus panel' +
|
|
569
|
+
(livePreview && removedComponents.length > 0 ? ' [d] removed list' : '') +
|
|
570
|
+
' [q] quit'
|
|
571
|
+
: showJson
|
|
572
|
+
? ' [j/k] scroll [Ctrl+u/d] half-page [gg/G] top/bottom [Tab] focus list'
|
|
573
|
+
: ' [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: () => {
|
|
210
574
|
setComponents((prev) => prev.map((c) => (c.status === 'needs-review' ? { ...c, status: 'accepted' } : c)));
|
|
211
575
|
}, onFinalize: () => setShowFinalize(true) }))] }));
|
|
212
576
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export type PushDecisionChoice = 'both' | 'push-only' | 'save-only';
|
|
3
|
+
type PushDecisionGateStepProps = {
|
|
4
|
+
summary: string;
|
|
5
|
+
context: string;
|
|
6
|
+
fileList: string;
|
|
7
|
+
onChoice: (choice: PushDecisionChoice) => void;
|
|
8
|
+
onQuit: () => void;
|
|
9
|
+
/**
|
|
10
|
+
* Skip-credentials spec — Task 3. When the operator advanced past the
|
|
11
|
+
* credentials screen via the skip path, push options are not actually
|
|
12
|
+
* usable (we never validated a token). Render all three rows for visual
|
|
13
|
+
* continuity but disable "Save AND push" and "Push only", with the
|
|
14
|
+
* `(unavailable — credentials skipped)` suffix.
|
|
15
|
+
*/
|
|
16
|
+
pushDisabled?: boolean;
|
|
17
|
+
};
|
|
18
|
+
export declare function PushDecisionGateStep({ summary, context, onChoice, onQuit, pushDisabled, }: PushDecisionGateStepProps): React.ReactElement;
|
|
19
|
+
export {};
|