@contentful/experience-design-system-cli 2.7.1-dev-build-02830eb.0 → 2.7.2-dev-build-bc65c2b.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/analyze/select/tui/App.js +441 -54
  3. package/dist/src/analyze/select/tui/components/ComponentDetail.d.ts +4 -3
  4. package/dist/src/analyze/select/tui/components/ComponentDetail.js +2 -2
  5. package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +1 -1
  6. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +10 -2
  7. package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +1 -1
  8. package/dist/src/analyze/select/tui/components/HelpOverlay.js +7 -2
  9. package/dist/src/analyze/select/tui/components/JsonEditor.d.ts +4 -7
  10. package/dist/src/analyze/select/tui/components/JsonEditor.js +125 -17
  11. package/dist/src/analyze/select/tui/components/QuitDialog.d.ts +1 -1
  12. package/dist/src/analyze/select/tui/components/QuitDialog.js +10 -2
  13. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +2 -0
  14. package/dist/src/analyze/select/tui/components/StatusBar.d.ts +2 -0
  15. package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +0 -5
  16. package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +4 -7
  17. package/dist/src/analyze/select/tui/hooks/useKeymap.d.ts +23 -0
  18. package/dist/src/analyze/select/tui/hooks/useKeymap.js +67 -1
  19. package/dist/src/analyze/select/tui/hooks/useUndo.js +21 -15
  20. package/dist/src/analyze/tui/AnalyzeView.js +0 -2
  21. package/dist/src/import/tui/WizardApp.js +0 -2
  22. package/dist/src/import/tui/steps/GenerateReviewStep.js +11 -4
  23. package/dist/src/print/validate/tui/ValidateView.js +0 -2
  24. package/package.json +2 -2
  25. package/dist/src/analyze/select/tui/hooks/useRawMode.d.ts +0 -6
  26. package/dist/src/analyze/select/tui/hooks/useRawMode.js +0 -16
  27. package/dist/src/analyze/select/tui/hooks/useSideEffects.d.ts +0 -17
  28. package/dist/src/analyze/select/tui/hooks/useSideEffects.js +0 -226
  29. package/dist/src/analyze/select/tui/inputToAction.d.ts +0 -13
  30. package/dist/src/analyze/select/tui/inputToAction.js +0 -68
  31. package/dist/src/analyze/select/tui/state.d.ts +0 -136
  32. package/dist/src/analyze/select/tui/state.js +0 -385
  33. package/dist/src/analyze/select/tui/utils.d.ts +0 -5
  34. package/dist/src/analyze/select/tui/utils.js +0 -11
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.7.1-dev-build-02830eb.0",
3
+ "version": "2.7.2-dev-build-bc65c2b.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useReducer, useRef } from 'react';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { Box, Text, useStdout } from 'ink';
4
+ import { readFile } from 'node:fs/promises';
5
+ import { createReviewSessionDetail } from '../types.js';
4
6
  import { TopBar } from './components/TopBar.js';
5
7
  import { Sidebar } from './components/Sidebar.js';
6
8
  import { ComponentDetail } from './components/ComponentDetail.js';
@@ -9,69 +11,347 @@ import { HelpOverlay } from './components/HelpOverlay.js';
9
11
  import { FinalizeDialog } from './components/FinalizeDialog.js';
10
12
  import { QuitDialog } from './components/QuitDialog.js';
11
13
  import { PreviewSummaryBar } from './components/PreviewSummaryBar.js';
14
+ import { useKeymap } from './hooks/useKeymap.js';
12
15
  import { useImmediateInput } from './hooks/useImmediateInput.js';
13
- import { useRawMode } from './hooks/useRawMode.js';
14
16
  import { useSession } from './hooks/useSession.js';
15
- import { useSideEffects } from './hooks/useSideEffects.js';
16
- import { reducer, initialState } from './state.js';
17
- import { inputToAction } from './inputToAction.js';
18
- import { createReviewSessionDetail } from '../types.js';
17
+ import { openPipelineDb, storeRawComponents, loadCDFComponents } from '../../../session/db.js';
18
+ import { ImportApiClient } from '../../../apply/api-client.js';
19
+ import { readTokensFromPath } from '../../../apply/manifest.js';
20
+ import { buildManifest } from '@contentful/experience-design-system-types';
19
21
  export function App({ sessionId, artifactsRoot, reviewRoot }) {
20
22
  const { stdout } = useStdout();
21
23
  const terminalWidth = stdout?.columns ?? 80;
22
- const visibleCount = Math.max(1, (stdout?.rows ?? 24) - 5);
23
- useRawMode();
24
24
  const { session: loadedSession, paths, loading, error: sessionError, saveState, appendEvent, } = useSession({
25
25
  sessionId,
26
26
  artifactsRoot,
27
27
  reviewRoot,
28
28
  });
29
- const [state, dispatch] = useReducer(reducer, initialState);
30
- // Refs so the single stdin listener always reads the latest committed values
31
- const stateRef = useRef(state);
32
- const visibleCountRef = useRef(visibleCount);
33
- const terminalWidthRef = useRef(terminalWidth);
34
- stateRef.current = state;
35
- visibleCountRef.current = visibleCount;
36
- terminalWidthRef.current = terminalWidth;
37
- // ── Single stdin listener ─────────────────────────────────────────────────
38
- useImmediateInput((input, key) => {
39
- const action = inputToAction(input, key, stateRef.current, visibleCountRef.current, terminalWidthRef.current);
40
- if (action)
41
- dispatch(action);
29
+ const [session, setSession] = useState(null);
30
+ const [selectedId, setSelectedId] = useState(null);
31
+ const [draftsByComponentId, setDraftsByComponentId] = useState({});
32
+ const [sidebarFocused, setSidebarFocused] = useState(true);
33
+ const [editMode, setEditMode] = useState(false);
34
+ const [sourceVisible, setSourceVisible] = useState(false);
35
+ const [showHelp, setShowHelp] = useState(false);
36
+ const [showFinalizeDialog, setShowFinalizeDialog] = useState(false);
37
+ const [showQuitDialog, setShowQuitDialog] = useState(false);
38
+ const [isSaving] = useState(false);
39
+ const [saveError, setSaveError] = useState(null);
40
+ const [finalizedResult, setFinalizedResult] = useState(null);
41
+ const [sidebarScrollOffset, setSidebarScrollOffset] = useState(0);
42
+ const [jsonScrollOffset, setJsonScrollOffset] = useState(0);
43
+ const [previewAnnotations, setPreviewAnnotations] = useState(() => {
44
+ const raw = process.env['EDS_PREVIEW_ANNOTATIONS'];
45
+ if (!raw)
46
+ return {};
47
+ try {
48
+ return JSON.parse(raw);
49
+ }
50
+ catch {
51
+ return {};
52
+ }
42
53
  });
43
- // ── Load session ──────────────────────────────────────────────────────────
54
+ const [previewLoading, setPreviewLoading] = useState(false);
55
+ const [previewError, setPreviewError] = useState(null);
56
+ const [previewResponse, setPreviewResponse] = useState(null);
57
+ const refreshPreview = useCallback(async () => {
58
+ const cmaToken = process.env['EDS_CMA_TOKEN'];
59
+ const spaceId = process.env['EDS_SPACE_ID'];
60
+ const environmentId = process.env['EDS_ENVIRONMENT_ID'];
61
+ const tokensPath = process.env['EDS_TOKENS_PATH'];
62
+ if (!cmaToken || !spaceId || !environmentId)
63
+ return;
64
+ setPreviewLoading(true);
65
+ try {
66
+ const db = openPipelineDb();
67
+ let components = [];
68
+ try {
69
+ components = loadCDFComponents(db, sessionId);
70
+ }
71
+ finally {
72
+ db.close();
73
+ }
74
+ let tokens = [];
75
+ if (tokensPath)
76
+ tokens = await readTokensFromPath('tokens', tokensPath);
77
+ const manifest = buildManifest(components, tokens);
78
+ if (!manifest.componentsManifest)
79
+ manifest.componentsManifest = {};
80
+ const client = new ImportApiClient({ cmaToken, spaceId, environmentId });
81
+ const preview = await client.previewImport(manifest);
82
+ const annotations = {};
83
+ for (const item of preview.components.new) {
84
+ const name = item.name ?? '';
85
+ if (name)
86
+ annotations[name] = 'new';
87
+ }
88
+ for (const item of preview.components.removed) {
89
+ annotations[item.name] = 'removed';
90
+ }
91
+ for (const item of preview.components.changed) {
92
+ if (item.changeClassification?.classification === 'breaking') {
93
+ annotations[item.current.name] = 'breaking';
94
+ }
95
+ else {
96
+ annotations[item.current.name] = 'changed';
97
+ }
98
+ }
99
+ setPreviewAnnotations(annotations);
100
+ setPreviewResponse(preview);
101
+ setPreviewError(null);
102
+ }
103
+ catch (err) {
104
+ const msg = err instanceof Error ? err.message : String(err);
105
+ setPreviewError(msg.includes('token') ? 'Preview request failed' : msg);
106
+ }
107
+ finally {
108
+ setPreviewLoading(false);
109
+ }
110
+ }, [sessionId]);
111
+ const syncSessionToDb = useCallback((currentSession) => {
112
+ const db = openPipelineDb();
113
+ try {
114
+ db.prepare(`UPDATE raw_components SET status = 'extracted' WHERE session_id = ?`).run(sessionId);
115
+ const acceptedNames = currentSession.components
116
+ .filter((c) => c.status === 'accepted' || c.status === 'reviewed')
117
+ .map((c) => c.name);
118
+ if (acceptedNames.length > 0) {
119
+ db.prepare(`UPDATE raw_components SET status = 'generated' WHERE session_id = ? AND name IN (${acceptedNames.map(() => '?').join(',')})`).run(sessionId, ...acceptedNames);
120
+ }
121
+ }
122
+ finally {
123
+ db.close();
124
+ }
125
+ }, [sessionId]);
126
+ const previewDebounceRef = useRef(null);
127
+ const debouncedRefreshPreview = useCallback((currentSession) => {
128
+ if (previewDebounceRef.current)
129
+ clearTimeout(previewDebounceRef.current);
130
+ previewDebounceRef.current = setTimeout(() => {
131
+ syncSessionToDb(currentSession);
132
+ void refreshPreview();
133
+ }, 500);
134
+ }, [syncSessionToDb, refreshPreview]);
135
+ useEffect(() => {
136
+ return () => {
137
+ if (previewDebounceRef.current)
138
+ clearTimeout(previewDebounceRef.current);
139
+ };
140
+ }, []);
141
+ // Sync loaded session into local state
44
142
  useEffect(() => {
45
- if (loadedSession && !state.session) {
46
- dispatch({ type: 'SESSION_LOADED', session: loadedSession, paths });
143
+ if (loadedSession && !session) {
144
+ setSession(loadedSession);
145
+ setSelectedId(loadedSession.components[0]?.id ?? null);
47
146
  }
48
147
  }, [loadedSession]);
49
- // ── All side effects in one hook ──────────────────────────────────────────
50
- useSideEffects(state, dispatch, { sessionId, saveState, appendEvent });
51
- // ── Render ────────────────────────────────────────────────────────────────
52
- if (loading)
148
+ useEffect(() => {
149
+ if (session && !previewResponse) {
150
+ const raw = process.env['EDS_PREVIEW_COUNTS'];
151
+ if (raw) {
152
+ try {
153
+ const counts = JSON.parse(raw);
154
+ setPreviewResponse({
155
+ components: {
156
+ new: Array(counts.compNew).fill({}),
157
+ changed: Array(counts.compChanged).fill({}),
158
+ removed: Array(counts.compRemoved).fill({}),
159
+ unchanged: Array(counts.compUnchanged).fill({}),
160
+ },
161
+ tokens: {
162
+ new: Array(counts.tokNew).fill({}),
163
+ changed: Array(counts.tokChanged).fill({}),
164
+ removed: Array(counts.tokRemoved).fill({}),
165
+ unchanged: Array(counts.tokUnchanged).fill({}),
166
+ },
167
+ });
168
+ }
169
+ catch (err) {
170
+ process.stderr.write(`[eds] failed to parse EDS_PREVIEW_COUNTS: ${err instanceof Error ? err.message : String(err)}\n`);
171
+ }
172
+ }
173
+ }
174
+ }, [session]);
175
+ // Lazy source code loading
176
+ useEffect(() => {
177
+ if (!session || !selectedId)
178
+ return;
179
+ const selectedComponent = session.components.find((c) => c.id === selectedId);
180
+ if (!selectedComponent || selectedComponent.sourceCode !== null)
181
+ return;
182
+ readFile(selectedComponent.resolvedSourcePath, 'utf8')
183
+ .then((code) => {
184
+ setSession((prev) => {
185
+ if (!prev)
186
+ return prev;
187
+ return {
188
+ ...prev,
189
+ components: prev.components.map((c) => (c.id === selectedComponent.id ? { ...c, sourceCode: code } : c)),
190
+ };
191
+ });
192
+ })
193
+ .catch(() => {
194
+ // Leave as null; SourcePanel shows [No source available]
195
+ });
196
+ }, [selectedId]);
197
+ // SIGINT handler
198
+ useEffect(() => {
199
+ const handler = () => {
200
+ if (Object.keys(draftsByComponentId).length > 0) {
201
+ setShowQuitDialog(true);
202
+ }
203
+ else {
204
+ process.exit(1);
205
+ }
206
+ };
207
+ process.on('SIGINT', handler);
208
+ return () => {
209
+ process.off('SIGINT', handler);
210
+ };
211
+ }, [draftsByComponentId]);
212
+ const updateStatus = async (componentId, newStatus) => {
213
+ if (!session || !paths)
214
+ return;
215
+ const updatedSession = {
216
+ ...session,
217
+ components: session.components.map((c) => (c.id === componentId ? { ...c, status: newStatus } : c)),
218
+ };
219
+ setSession(updatedSession);
220
+ await saveState(updatedSession);
221
+ const component = session.components.find((c) => c.id === componentId);
222
+ await appendEvent({
223
+ type: 'status_changed',
224
+ payload: { componentId, from: component?.status, to: newStatus },
225
+ });
226
+ debouncedRefreshPreview(updatedSession);
227
+ };
228
+ const dialogOpen = showHelp || showFinalizeDialog || showQuitDialog;
229
+ useKeymap({
230
+ sidebarFocused,
231
+ editMode,
232
+ dialogOpen,
233
+ disabled: isSaving,
234
+ }, {
235
+ onSidebarUp: () => {
236
+ if (!session)
237
+ return;
238
+ const idx = session.components.findIndex((c) => c.id === selectedId);
239
+ if (idx > 0) {
240
+ setSelectedId(session.components[idx - 1].id);
241
+ setJsonScrollOffset(0);
242
+ setSidebarScrollOffset((prev) => Math.min(prev, idx - 1));
243
+ }
244
+ },
245
+ onSidebarDown: () => {
246
+ if (!session)
247
+ return;
248
+ const idx = session.components.findIndex((c) => c.id === selectedId);
249
+ if (idx < session.components.length - 1) {
250
+ setSelectedId(session.components[idx + 1].id);
251
+ setJsonScrollOffset(0);
252
+ setSidebarScrollOffset((prev) => {
253
+ const newIdx = idx + 1;
254
+ return newIdx >= prev + visibleCount ? newIdx - visibleCount + 1 : prev;
255
+ });
256
+ }
257
+ },
258
+ onSidebarSelect: () => { },
259
+ onAccept: () => {
260
+ if (selectedId)
261
+ void updateStatus(selectedId, 'accepted');
262
+ },
263
+ onReject: () => {
264
+ if (selectedId)
265
+ void updateStatus(selectedId, 'rejected');
266
+ },
267
+ onEnterEditMode: () => {
268
+ if (!session || !selectedId)
269
+ return;
270
+ const component = session.components.find((c) => c.id === selectedId);
271
+ if (!component)
272
+ return;
273
+ setEditMode(true);
274
+ setDraftsByComponentId((prev) => ({
275
+ ...prev,
276
+ [selectedId]: prev[selectedId] ?? JSON.stringify(component.editedProposal, null, 2),
277
+ }));
278
+ },
279
+ onToggleSource: () => {
280
+ if (terminalWidth < 120) {
281
+ setSaveError('Terminal too narrow for source panel (need 120+ cols)');
282
+ setTimeout(() => setSaveError(null), 3000);
283
+ return;
284
+ }
285
+ setSourceVisible((prev) => !prev);
286
+ },
287
+ onScrollUp: () => {
288
+ setJsonScrollOffset((prev) => Math.max(0, prev - 1));
289
+ },
290
+ onScrollDown: () => {
291
+ setJsonScrollOffset((prev) => prev + 1);
292
+ },
293
+ onToggleFocus: () => setSidebarFocused((prev) => !prev),
294
+ onApproveAll: async () => {
295
+ if (!session || !paths)
296
+ return;
297
+ const updatedComponents = session.components.map((c) => c.status === 'needs-review' ? { ...c, status: 'accepted' } : c);
298
+ const affected = updatedComponents.filter((c) => c.status === 'accepted').length -
299
+ session.components.filter((c) => c.status === 'accepted').length;
300
+ const updatedSession = { ...session, components: updatedComponents };
301
+ setSession(updatedSession);
302
+ await saveState(updatedSession);
303
+ await appendEvent({ type: 'approve_all', payload: { affected } });
304
+ syncSessionToDb(updatedSession);
305
+ void refreshPreview();
306
+ },
307
+ onFinalize: () => setShowFinalizeDialog(true),
308
+ onQuit: () => {
309
+ if (Object.keys(draftsByComponentId).length > 0) {
310
+ setShowQuitDialog(true);
311
+ }
312
+ else {
313
+ process.exit(1);
314
+ }
315
+ },
316
+ onToggleHelp: () => setShowHelp((prev) => !prev),
317
+ });
318
+ // Must be before early returns — Rules of Hooks
319
+ const sessionSummary = useMemo(() => (session?.components ?? [])
320
+ .map((c) => ({
321
+ id: c.id,
322
+ name: c.name,
323
+ status: c.status,
324
+ previewAnnotation: previewAnnotations[c.name],
325
+ extractionConfidence: c.originalProposal.extractionConfidence ?? null,
326
+ needsReview: c.originalProposal.needsReview ?? false,
327
+ }))
328
+ .sort((a, b) => {
329
+ const aF = a.needsReview && a.status === 'needs-review' ? 0 : 1;
330
+ const bF = b.needsReview && b.status === 'needs-review' ? 0 : 1;
331
+ if (aF !== bF)
332
+ return aF - bF;
333
+ return (a.extractionConfidence ?? 6) - (b.extractionConfidence ?? 6);
334
+ }), [session?.components, previewAnnotations]);
335
+ if (loading) {
53
336
  return _jsx(Text, { children: "Loading session..." });
54
- if (sessionError)
337
+ }
338
+ if (sessionError) {
55
339
  return (_jsxs(Text, { color: "red", children: [sessionError, '\nPress q to exit.'] }));
56
- if (!state.session)
340
+ }
341
+ if (!session || !paths) {
57
342
  return _jsx(Text, { color: "red", children: "Session unavailable." });
58
- const { mode, session, selectedId, sidebarScrollOffset, jsonScrollOffset, editor } = state;
59
- if (mode.type === 'finalized')
60
- return _jsx(FinalizedScreen, { result: mode });
343
+ }
344
+ if (finalizedResult) {
345
+ return _jsx(FinalizedScreen, { result: finalizedResult });
346
+ }
61
347
  const selectedRecord = session.components.find((c) => c.id === selectedId) ?? null;
62
348
  const sessionDetail = selectedRecord ? createReviewSessionDetail({ ...session, components: [selectedRecord] }) : null;
63
349
  const selectedDetail = sessionDetail?.components[0] ?? null;
64
- const counts = {
65
- accepted: session.components.filter((c) => c.status === 'accepted').length,
66
- rejected: session.components.filter((c) => c.status === 'rejected').length,
67
- reviewed: session.components.filter((c) => c.status === 'reviewed').length,
68
- needsReview: session.components.filter((c) => c.status === 'needs-review').length,
69
- };
70
- const dialogOpen = mode.type === 'dialog';
71
- const isEditing = mode.type === 'editing';
72
- const sourceVisible = mode.type === 'browsing' && mode.sourceVisible;
73
- const sidebarFocused = mode.type === 'browsing' && mode.sidebarFocused;
74
- const hints = isEditing
350
+ const acceptedCount = session.components.filter((c) => c.status === 'accepted').length;
351
+ const rejectedCount = session.components.filter((c) => c.status === 'rejected').length;
352
+ const reviewedCount = session.components.filter((c) => c.status === 'reviewed').length;
353
+ const needsReviewCount = session.components.filter((c) => c.status === 'needs-review').length;
354
+ const hints = editMode
75
355
  ? [
76
356
  { key: 'Ctrl+S', label: 'save' },
77
357
  { key: 'Esc', label: 'discard' },
@@ -83,21 +363,128 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
83
363
  { key: 'q', label: 'quit' },
84
364
  ];
85
365
  const collapsed = terminalWidth < 80;
86
- const longestName = session.components.reduce((m, c) => Math.max(m, c.name.length), 0);
366
+ const visibleCount = 20;
367
+ const longestName = session.components.reduce((max, c) => Math.max(max, c.name.length), 0);
368
+ // icon + space + name + 2 border chars; min 14, max 22
87
369
  const sidebarWidth = collapsed ? 3 : Math.min(Math.max(longestName + 4, 14), 22);
88
- const { sessionSummary } = state; // stable reference — only changes when data changes, never on scroll/select
89
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TopBar, { subcommand: "analyze select", hints: hints }), mode.type === 'dialog' && mode.which === 'help' && (_jsx(HelpOverlay, { mode: "review", onClose: () => dispatch({ type: 'CLOSE_DIALOG' }) })), mode.type === 'dialog' && mode.which === 'finalize' && (_jsx(FinalizeDialog, { accepted: counts.accepted, rejected: counts.rejected, needsReview: counts.needsReview, onConfirm: () => dispatch({ type: 'FINALIZE_CONFIRM' }), onCancel: () => dispatch({ type: 'CLOSE_DIALOG' }) })), mode.type === 'dialog' && mode.which === 'quit' && (_jsx(QuitDialog, { hasUnsavedDrafts: Object.keys(state.draftsByComponentId).length > 0, onConfirm: async () => {
90
- if (paths)
91
- await appendEvent({ type: 'session_quit', payload: { reason: 'user_quit' } });
370
+ const handleDraftSave = async (draft) => {
371
+ if (!selectedId || !session || !paths)
372
+ return;
373
+ if (!draft)
374
+ return;
375
+ try {
376
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
377
+ const parsed = JSON.parse(draft);
378
+ const currentStatus = session.components.find((c) => c.id === selectedId)?.status;
379
+ const newStatus = currentStatus === 'needs-review' ? 'reviewed' : currentStatus;
380
+ const updatedSession = {
381
+ ...session,
382
+ components: session.components.map((c) => c.id === selectedId
383
+ ? {
384
+ ...c,
385
+ editedProposal: parsed,
386
+ status: newStatus,
387
+ }
388
+ : c),
389
+ };
390
+ const { [selectedId]: _removed, ...remainingDrafts } = draftsByComponentId;
391
+ setSession(updatedSession);
392
+ setDraftsByComponentId(remainingDrafts);
393
+ setEditMode(false);
394
+ await saveState(updatedSession);
395
+ await appendEvent({
396
+ type: 'draft_saved',
397
+ payload: { componentId: selectedId },
398
+ });
399
+ // Sync all edited proposals to pipeline DB (keep 'generated' for preview)
400
+ const allEdited = updatedSession.components.map((c) => c.editedProposal);
401
+ const syncDb = openPipelineDb();
402
+ try {
403
+ storeRawComponents(syncDb, sessionId, allEdited, {
404
+ status: 'generated',
405
+ preserveCDF: true,
406
+ });
407
+ }
408
+ finally {
409
+ syncDb.close();
410
+ }
411
+ void refreshPreview();
412
+ }
413
+ catch {
414
+ // JSON parse error — JsonEditor already shows the error inline
415
+ }
416
+ };
417
+ const handleDraftDiscard = () => {
418
+ if (!selectedId)
419
+ return;
420
+ const { [selectedId]: _removed, ...remainingDrafts } = draftsByComponentId;
421
+ setDraftsByComponentId(remainingDrafts);
422
+ setEditMode(false);
423
+ };
424
+ const handleFinalize = async () => {
425
+ if (!session || !paths)
426
+ return;
427
+ try {
428
+ await appendEvent({
429
+ type: 'finalized',
430
+ payload: {
431
+ accepted: acceptedCount,
432
+ rejected: rejectedCount,
433
+ excluded: needsReviewCount,
434
+ },
435
+ });
436
+ // Write all components back to DB, marking accepted as 'generated'
437
+ // so loadCDFComponents (push/preview) only picks up accepted ones,
438
+ // but loadRawComponents (editor re-entry) still finds all of them
439
+ const acceptedNames = new Set(session.components.filter((c) => c.status === 'accepted').map((c) => c.name));
440
+ const db = openPipelineDb();
441
+ try {
442
+ storeRawComponents(db, sessionId, session.components.map((c) => c.editedProposal), { status: 'extracted', preserveCDF: true });
443
+ if (acceptedNames.size > 0) {
444
+ db.prepare(`UPDATE raw_components SET status = 'generated' WHERE session_id = ? AND name IN (${[...acceptedNames].map(() => '?').join(',')})`).run(sessionId, ...acceptedNames);
445
+ }
446
+ }
447
+ finally {
448
+ db.close();
449
+ }
450
+ const output = JSON.stringify({
451
+ status: 'finalized',
452
+ sessionDir: paths.sessionDir,
453
+ accepted: acceptedCount,
454
+ rejected: rejectedCount,
455
+ excluded: needsReviewCount,
456
+ }, null, 2) + '\n';
457
+ process.stdout.write(output);
458
+ setFinalizedResult({
459
+ accepted: acceptedCount,
460
+ rejected: rejectedCount,
461
+ excluded: needsReviewCount,
462
+ });
463
+ }
464
+ catch (err) {
465
+ setSaveError(String(err));
466
+ }
467
+ };
468
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TopBar, { subcommand: "analyze select", hints: hints }), showHelp && _jsx(HelpOverlay, { mode: "review", onClose: () => setShowHelp(false) }), showFinalizeDialog && (_jsx(FinalizeDialog, { accepted: acceptedCount, rejected: rejectedCount, needsReview: needsReviewCount, onConfirm: () => {
469
+ void handleFinalize();
470
+ }, onCancel: () => setShowFinalizeDialog(false) })), showQuitDialog && (_jsx(QuitDialog, { hasUnsavedDrafts: Object.keys(draftsByComponentId).length > 0, onConfirm: async () => {
471
+ if (paths) {
472
+ await appendEvent({
473
+ type: 'session_quit',
474
+ payload: { reason: 'user_quit' },
475
+ });
476
+ }
92
477
  process.exit(1);
93
- }, onCancel: () => dispatch({ type: 'CLOSE_DIALOG' }) })), !dialogOpen && (_jsxs(Box, { flexGrow: 1, children: [_jsx(Sidebar, { components: sessionSummary, selectedId: selectedId, focused: sidebarFocused, scrollOffset: sidebarScrollOffset, visibleCount: visibleCount, collapsed: collapsed, width: sidebarWidth }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: selectedDetail ? (_jsx(ComponentDetail, { component: selectedDetail, sourceCode: selectedId ? (state.sourceCodeById[selectedId] ?? null) : null, draftValue: selectedId ? (state.draftsByComponentId[selectedId] ?? '') : '', editMode: isEditing, editorState: isEditing ? editor : null, sourceVisible: sourceVisible, jsonScrollOffset: jsonScrollOffset, sourceScrollX: 0, sourceScrollY: 0, terminalWidth: terminalWidth, previewAnnotation: selectedRecord
94
- ? state.previewAnnotations[selectedRecord.name]
95
- : undefined })) : (_jsx(Text, { dimColor: true, children: "No component selected" })) })] })), _jsx(PreviewSummaryBar, { preview: state.previewResponse, loading: state.previewLoading }), state.previewError && _jsx(Text, { color: "yellow", children: '⚠ Preview: ' + state.previewError }), state.saveError && _jsx(Text, { color: "red", children: '⚠ ' + state.saveError }), !dialogOpen && (_jsx(StatusBar, { accepted: counts.accepted, rejected: counts.rejected, reviewed: counts.reviewed, needsReview: counts.needsReview }))] }));
478
+ }, onCancel: () => setShowQuitDialog(false) })), !showHelp && !showFinalizeDialog && !showQuitDialog && (_jsxs(Box, { flexGrow: 1, children: [_jsx(Sidebar, { components: sessionSummary, selectedId: selectedId, focused: sidebarFocused, scrollOffset: sidebarScrollOffset, visibleCount: visibleCount, onSelect: (id) => {
479
+ setSelectedId(id);
480
+ setJsonScrollOffset(0);
481
+ }, onScrollChange: setSidebarScrollOffset, collapsed: collapsed, width: sidebarWidth }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: selectedDetail ? (_jsx(ComponentDetail, { component: selectedDetail, sourceCode: selectedRecord?.sourceCode ?? null, draftValue: selectedId ? (draftsByComponentId[selectedId] ?? '') : '', editMode: editMode, sourceVisible: sourceVisible, jsonScrollOffset: jsonScrollOffset, sourceScrollX: 0, sourceScrollY: 0, terminalWidth: terminalWidth, previewAnnotation: selectedRecord ? previewAnnotations[selectedRecord.name] : undefined, onSaveDraft: (value) => void handleDraftSave(value), onDiscardDraft: handleDraftDiscard, onScrollChange: setJsonScrollOffset })) : (_jsx(Text, { dimColor: true, children: "No component selected" })) })] })), _jsx(PreviewSummaryBar, { preview: previewResponse, loading: previewLoading }), previewError && _jsx(Text, { color: "yellow", children: '⚠ Preview: ' + previewError }), saveError && _jsx(Text, { color: "red", children: '⚠ ' + saveError }), !dialogOpen && (_jsx(StatusBar, { accepted: acceptedCount, rejected: rejectedCount, reviewed: reviewedCount, needsReview: needsReviewCount, onApproveAll: () => { }, onFinalize: () => setShowFinalizeDialog(true) }))] }));
96
482
  }
97
483
  function FinalizedScreen({ result, }) {
98
484
  useImmediateInput((_input, key) => {
99
- if (key.return || _input === 'q' || key.escape)
485
+ if (key.return || _input === 'q' || key.escape) {
100
486
  process.exit(0);
487
+ }
101
488
  });
102
- return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, gap: 1, children: [_jsx(Text, { bold: true, color: "green", children: "\u2713 Finalized" }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "green", children: "\u2713" }), _jsxs(Text, { children: [result.accepted, " accepted"] })] }), result.rejected > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "red", children: "\u2717" }), _jsxs(Text, { children: [result.rejected, " rejected"] })] })), result.excluded > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "\u00B7" }), _jsxs(Text, { dimColor: true, children: [result.excluded, " excluded"] })] }))] }), _jsx(Text, { dimColor: true, children: "Decisions saved. Ready for the next step." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[Enter / q] Exit" }) })] }));
489
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, gap: 1, children: [_jsx(Text, { bold: true, color: "green", children: "\u2713 Finalized" }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "green", children: "\u2713" }), _jsxs(Text, { children: [result.accepted, " accepted"] })] }), result.rejected > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "red", children: "\u2717" }), _jsxs(Text, { children: [result.rejected, " rejected"] })] })), result.excluded > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "\u00B7" }), _jsxs(Text, { dimColor: true, children: [result.excluded, " excluded (unresolved)"] })] }))] }), _jsx(Text, { dimColor: true, children: "Decisions saved. Ready for the next step." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[Enter / q] Exit" }) })] }));
103
490
  }
@@ -1,18 +1,19 @@
1
1
  import React from 'react';
2
2
  import type { PreviewAnnotation, ReviewComponentDetail } from '../../types.js';
3
- import type { EditorState } from '../state.js';
4
3
  type ComponentDetailProps = {
5
4
  component: ReviewComponentDetail;
6
5
  sourceCode: string | null;
7
6
  draftValue: string;
8
7
  editMode: boolean;
9
- editorState: EditorState | null;
10
8
  sourceVisible: boolean;
11
9
  jsonScrollOffset: number;
12
10
  sourceScrollX: number;
13
11
  sourceScrollY: number;
14
12
  terminalWidth: number;
15
13
  previewAnnotation?: PreviewAnnotation;
14
+ onSaveDraft: (value: string) => void;
15
+ onDiscardDraft: () => void;
16
+ onScrollChange: (offset: number) => void;
16
17
  };
17
- export declare function ComponentDetail({ component, sourceCode, draftValue, editMode, editorState, sourceVisible, jsonScrollOffset, sourceScrollX, sourceScrollY, terminalWidth, previewAnnotation, }: ComponentDetailProps): React.ReactElement;
18
+ export declare function ComponentDetail({ component, sourceCode, draftValue, editMode, sourceVisible, jsonScrollOffset, sourceScrollX, sourceScrollY, terminalWidth, previewAnnotation, onSaveDraft, onDiscardDraft, }: ComponentDetailProps): React.ReactElement;
18
19
  export {};
@@ -18,7 +18,7 @@ function annotationLabel(annotation) {
18
18
  return null;
19
19
  }
20
20
  }
21
- export function ComponentDetail({ component, sourceCode, draftValue, editMode, editorState, sourceVisible, jsonScrollOffset, sourceScrollX, sourceScrollY, terminalWidth, previewAnnotation, }) {
21
+ export function ComponentDetail({ component, sourceCode, draftValue, editMode, sourceVisible, jsonScrollOffset, sourceScrollX, sourceScrollY, terminalWidth, previewAnnotation, onSaveDraft, onDiscardDraft, }) {
22
22
  const sidebarWidth = terminalWidth < 80 ? 5 : 20;
23
23
  const availableWidth = terminalWidth - sidebarWidth - 2;
24
24
  const panelHeight = 20;
@@ -44,5 +44,5 @@ export function ComponentDetail({ component, sourceCode, draftValue, editMode, e
44
44
  return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: component.name }), (() => {
45
45
  const ann = annotationLabel(previewAnnotation);
46
46
  return ann ? _jsx(Text, { color: ann.color, children: ann.text }) : null;
47
- })(), _jsx(Text, { color: confColor, children: ' — ' + confLabel }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { dimColor: true, children: [sourceVisible ? '[s] hide src' : '[s] src', editMode ? '' : ' [e] edit'] })] }), _jsxs(Box, { children: [_jsx(JsonPanel, { label: "ORIGINAL (read-only)", value: originalJson, scrollOffset: jsonScrollOffset, width: originalWidth, height: panelHeight, active: false }), _jsx(Text, { children: " " }), editMode && editorState ? (_jsx(JsonEditor, { editorState: editorState, width: editWidth, height: panelHeight })) : (_jsx(JsonPanel, { label: "EDIT (draft)", value: draftValue || editedJson, scrollOffset: jsonScrollOffset, width: editWidth, height: panelHeight, active: true })), sourceVisible && sourceWidth > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(SourcePanel, { sourceCode: sourceCode, filePath: component.originalProposal.source, width: sourceWidth, height: panelHeight, scrollX: sourceScrollX, scrollY: sourceScrollY })] }))] }), !editMode && _jsx(Text, { dimColor: true, children: ' [a] accept [r] reject [e] edit [A] accept all [↑↓] scroll' })] }));
47
+ })(), _jsx(Text, { color: confColor, children: ' — ' + confLabel }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { dimColor: true, children: [sourceVisible ? '[s] hide src' : '[s] src', editMode ? '' : ' [e] edit'] })] }), _jsxs(Box, { children: [_jsx(JsonPanel, { label: "ORIGINAL (read-only)", value: originalJson, scrollOffset: jsonScrollOffset, width: originalWidth, height: panelHeight, active: false }), _jsx(Text, { children: " " }), editMode ? (_jsx(JsonEditor, { value: draftValue || editedJson, width: editWidth, height: panelHeight, onSave: onSaveDraft, onDiscard: onDiscardDraft })) : (_jsx(JsonPanel, { label: "EDIT (draft)", value: draftValue || editedJson, scrollOffset: jsonScrollOffset, width: editWidth, height: panelHeight, active: true })), sourceVisible && sourceWidth > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(SourcePanel, { sourceCode: sourceCode, filePath: component.originalProposal.source, width: sourceWidth, height: panelHeight, scrollX: sourceScrollX, scrollY: sourceScrollY })] }))] }), !editMode && _jsx(Text, { dimColor: true, children: ' [a] accept [r] reject [e] edit [A] accept all [↑↓] scroll' })] }));
48
48
  }
@@ -6,5 +6,5 @@ type FinalizeDialogProps = {
6
6
  onConfirm: () => void;
7
7
  onCancel: () => void;
8
8
  };
9
- export declare function FinalizeDialog({ accepted, rejected, needsReview, onConfirm: _onConfirm, onCancel: _onCancel, }: FinalizeDialogProps): React.ReactElement;
9
+ export declare function FinalizeDialog({ accepted, rejected, needsReview, onConfirm, onCancel, }: FinalizeDialogProps): React.ReactElement;
10
10
  export {};
@@ -1,7 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
- // Pure render input handled by inputToAction in App.
4
- export function FinalizeDialog({ accepted, rejected, needsReview, onConfirm: _onConfirm, onCancel: _onCancel, }) {
3
+ import { useImmediateInput } from '../hooks/useImmediateInput.js';
4
+ export function FinalizeDialog({ accepted, rejected, needsReview, onConfirm, onCancel, }) {
5
+ useImmediateInput((input, key) => {
6
+ if (input === 'y' || key.return) {
7
+ onConfirm();
8
+ }
9
+ else if (input === 'n' || key.escape) {
10
+ onCancel();
11
+ }
12
+ });
5
13
  const allResolved = needsReview === 0;
6
14
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", padding: 1, width: 58, children: [_jsx(Text, { bold: true, children: '─'.repeat(17) + ' Finalize ' + '─'.repeat(17) }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsxs(Text, { color: "green", children: [accepted, " accepted"] }), _jsx(Text, { children: " \u00B7 " }), _jsxs(Text, { color: "red", children: [rejected, " rejected"] }), _jsx(Text, { children: " \u00B7 " }), _jsxs(Text, { dimColor: true, children: [needsReview, " unresolved"] })] }), _jsx(Text, { children: " " }), !allResolved && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: '⚠ ' + needsReview + ' component' + (needsReview === 1 ? ' is' : 's are') + ' unresolved and will be' }), _jsx(Text, { color: "yellow", children: ' excluded from the output.' }), _jsx(Text, { children: " " })] })), _jsx(Text, { children: allResolved ? 'Save decisions and exit? All components resolved.' : 'Save decisions and exit?' }), _jsx(Text, { children: " " }), _jsx(Text, { children: ' [y / Enter] Confirm [n / Esc] Cancel' })] }));
7
15
  }
@@ -3,5 +3,5 @@ type HelpOverlayProps = {
3
3
  mode: 'analyze' | 'validate' | 'review';
4
4
  onClose: () => void;
5
5
  };
6
- export declare function HelpOverlay({ mode, onClose: _onClose }: HelpOverlayProps): React.ReactElement;
6
+ export declare function HelpOverlay({ mode, onClose }: HelpOverlayProps): React.ReactElement;
7
7
  export {};