@contentful/experience-design-system-cli 2.7.0 → 2.7.1-dev-build-02830eb.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 +54 -475
  3. package/dist/src/analyze/select/tui/components/ComponentDetail.d.ts +3 -5
  4. package/dist/src/analyze/select/tui/components/ComponentDetail.js +3 -3
  5. package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +1 -1
  6. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +2 -10
  7. package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +1 -1
  8. package/dist/src/analyze/select/tui/components/HelpOverlay.js +2 -7
  9. package/dist/src/analyze/select/tui/components/JsonEditor.d.ts +7 -5
  10. package/dist/src/analyze/select/tui/components/JsonEditor.js +15 -146
  11. package/dist/src/analyze/select/tui/components/QuitDialog.d.ts +1 -1
  12. package/dist/src/analyze/select/tui/components/QuitDialog.js +2 -10
  13. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +0 -2
  14. package/dist/src/analyze/select/tui/components/StatusBar.d.ts +0 -2
  15. package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +5 -0
  16. package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +7 -4
  17. package/dist/src/analyze/select/tui/hooks/useKeymap.d.ts +0 -23
  18. package/dist/src/analyze/select/tui/hooks/useKeymap.js +1 -67
  19. package/dist/src/analyze/select/tui/hooks/useRawMode.d.ts +6 -0
  20. package/dist/src/analyze/select/tui/hooks/useRawMode.js +16 -0
  21. package/dist/src/analyze/select/tui/hooks/useSideEffects.d.ts +17 -0
  22. package/dist/src/analyze/select/tui/hooks/useSideEffects.js +226 -0
  23. package/dist/src/analyze/select/tui/hooks/useUndo.js +15 -21
  24. package/dist/src/analyze/select/tui/inputToAction.d.ts +13 -0
  25. package/dist/src/analyze/select/tui/inputToAction.js +68 -0
  26. package/dist/src/analyze/select/tui/state.d.ts +136 -0
  27. package/dist/src/analyze/select/tui/state.js +385 -0
  28. package/dist/src/analyze/select/tui/utils.d.ts +5 -0
  29. package/dist/src/analyze/select/tui/utils.js +11 -0
  30. package/dist/src/analyze/tui/AnalyzeView.js +2 -0
  31. package/dist/src/import/tui/WizardApp.js +2 -0
  32. package/dist/src/import/tui/steps/GenerateReviewStep.js +4 -11
  33. package/dist/src/print/validate/tui/ValidateView.js +2 -0
  34. package/package.json +2 -2
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.7.0",
3
+ "version": "2.7.1-dev-build-02830eb.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,9 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { useEffect, useReducer, useRef } from 'react';
3
3
  import { Box, Text, useStdout } from 'ink';
4
- import { readFile } from 'node:fs/promises';
5
- import { createReviewSessionDetail } from '../types.js';
6
- import { stripScoringFields } from '../../../types.js';
7
4
  import { TopBar } from './components/TopBar.js';
8
5
  import { Sidebar } from './components/Sidebar.js';
9
6
  import { ComponentDetail } from './components/ComponentDetail.js';
@@ -12,367 +9,69 @@ import { HelpOverlay } from './components/HelpOverlay.js';
12
9
  import { FinalizeDialog } from './components/FinalizeDialog.js';
13
10
  import { QuitDialog } from './components/QuitDialog.js';
14
11
  import { PreviewSummaryBar } from './components/PreviewSummaryBar.js';
15
- import { useKeymap } from './hooks/useKeymap.js';
16
12
  import { useImmediateInput } from './hooks/useImmediateInput.js';
13
+ import { useRawMode } from './hooks/useRawMode.js';
17
14
  import { useSession } from './hooks/useSession.js';
18
- import { openPipelineDb, storeRawComponents, loadCDFComponents } from '../../../session/db.js';
19
- import { ImportApiClient } from '../../../apply/api-client.js';
20
- import { readTokensFromPath } from '../../../apply/manifest.js';
21
- import { buildManifest } from '@contentful/experience-design-system-types';
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';
22
19
  export function App({ sessionId, artifactsRoot, reviewRoot }) {
23
20
  const { stdout } = useStdout();
24
21
  const terminalWidth = stdout?.columns ?? 80;
22
+ const visibleCount = Math.max(1, (stdout?.rows ?? 24) - 5);
23
+ useRawMode();
25
24
  const { session: loadedSession, paths, loading, error: sessionError, saveState, appendEvent, } = useSession({
26
25
  sessionId,
27
26
  artifactsRoot,
28
27
  reviewRoot,
29
28
  });
30
- const [session, setSession] = useState(null);
31
- const [selectedId, setSelectedId] = useState(null);
32
- const [draftsByComponentId, setDraftsByComponentId] = useState({});
33
- const [sidebarFocused, setSidebarFocused] = useState(true);
34
- const [editMode, setEditMode] = useState(false);
35
- const [sourceVisible, setSourceVisible] = useState(false);
36
- const [showHelp, setShowHelp] = useState(false);
37
- const [showFinalizeDialog, setShowFinalizeDialog] = useState(false);
38
- const [showQuitDialog, setShowQuitDialog] = useState(false);
39
- const [isSaving] = useState(false);
40
- const [saveError, setSaveError] = useState(null);
41
- const [finalizedResult, setFinalizedResult] = useState(null);
42
- const [sidebarScrollOffset, setSidebarScrollOffset] = useState(0);
43
- const [jsonScrollOffset, setJsonScrollOffset] = useState(0);
44
- // Keeps the visual sort order in sync for use inside keymap handlers (which close over a stale render)
45
- const sortedIdsRef = useRef([]);
46
- // Source code kept separate from session state so lazy loading never mutates
47
- // session.components — that would invalidate useMemo(sessionSummary) and flash the sidebar
48
- const [sourceCodeById, setSourceCodeById] = useState({});
49
- const [previewAnnotations, setPreviewAnnotations] = useState(() => {
50
- const raw = process.env['EDS_PREVIEW_ANNOTATIONS'];
51
- if (!raw)
52
- return {};
53
- try {
54
- return JSON.parse(raw);
55
- }
56
- catch {
57
- return {};
58
- }
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);
59
42
  });
60
- const [previewLoading, setPreviewLoading] = useState(false);
61
- const [previewError, setPreviewError] = useState(null);
62
- const [previewResponse, setPreviewResponse] = useState(null);
63
- const refreshPreview = useCallback(async () => {
64
- const cmaToken = process.env['EDS_CMA_TOKEN'];
65
- const spaceId = process.env['EDS_SPACE_ID'];
66
- const environmentId = process.env['EDS_ENVIRONMENT_ID'];
67
- const tokensPath = process.env['EDS_TOKENS_PATH'];
68
- if (!cmaToken || !spaceId || !environmentId)
69
- return;
70
- setPreviewLoading(true);
71
- try {
72
- const db = openPipelineDb();
73
- let components = [];
74
- try {
75
- components = loadCDFComponents(db, sessionId);
76
- }
77
- finally {
78
- db.close();
79
- }
80
- let tokens = [];
81
- if (tokensPath)
82
- tokens = await readTokensFromPath('tokens', tokensPath);
83
- const manifest = buildManifest(components, tokens);
84
- if (!manifest.componentsManifest)
85
- manifest.componentsManifest = {};
86
- const client = new ImportApiClient({ cmaToken, spaceId, environmentId });
87
- const preview = await client.previewImport(manifest);
88
- const annotations = {};
89
- for (const item of preview.components.new) {
90
- const name = item.name ?? '';
91
- if (name)
92
- annotations[name] = 'new';
93
- }
94
- for (const item of preview.components.removed) {
95
- annotations[item.name] = 'removed';
96
- }
97
- for (const item of preview.components.changed) {
98
- if (item.changeClassification?.classification === 'breaking') {
99
- annotations[item.current.name] = 'breaking';
100
- }
101
- else {
102
- annotations[item.current.name] = 'changed';
103
- }
104
- }
105
- setPreviewAnnotations(annotations);
106
- setPreviewResponse(preview);
107
- setPreviewError(null);
108
- }
109
- catch (err) {
110
- const msg = err instanceof Error ? err.message : String(err);
111
- setPreviewError(msg.includes('token') ? 'Preview request failed' : msg);
112
- }
113
- finally {
114
- setPreviewLoading(false);
115
- }
116
- }, [sessionId]);
117
- const syncSessionToDb = useCallback((currentSession) => {
118
- const db = openPipelineDb();
119
- try {
120
- db.prepare(`UPDATE raw_components SET status = 'extracted' WHERE session_id = ?`).run(sessionId);
121
- const acceptedNames = currentSession.components
122
- .filter((c) => c.status === 'accepted' || c.status === 'reviewed')
123
- .map((c) => c.name);
124
- if (acceptedNames.length > 0) {
125
- db.prepare(`UPDATE raw_components SET status = 'generated' WHERE session_id = ? AND name IN (${acceptedNames.map(() => '?').join(',')})`).run(sessionId, ...acceptedNames);
126
- }
127
- }
128
- finally {
129
- db.close();
130
- }
131
- }, [sessionId]);
132
- const previewDebounceRef = useRef(null);
133
- const debouncedRefreshPreview = useCallback((currentSession) => {
134
- if (previewDebounceRef.current)
135
- clearTimeout(previewDebounceRef.current);
136
- previewDebounceRef.current = setTimeout(() => {
137
- syncSessionToDb(currentSession);
138
- void refreshPreview();
139
- }, 500);
140
- }, [syncSessionToDb, refreshPreview]);
43
+ // ── Load session ──────────────────────────────────────────────────────────
141
44
  useEffect(() => {
142
- return () => {
143
- if (previewDebounceRef.current)
144
- clearTimeout(previewDebounceRef.current);
145
- };
146
- }, []);
147
- // Sync loaded session into local state; selectedId is set to null here
148
- // and will be corrected to the first sorted item on first render via the effect below
149
- useEffect(() => {
150
- if (loadedSession && !session) {
151
- setSession(loadedSession);
152
- setSelectedId(null);
45
+ if (loadedSession && !state.session) {
46
+ dispatch({ type: 'SESSION_LOADED', session: loadedSession, paths });
153
47
  }
154
48
  }, [loadedSession]);
155
- // Once the sort order is known (after first render), select the first sorted item.
156
- // Runs only when session transitions from null → loaded.
157
- const sessionLoaded = session !== null;
158
- useEffect(() => {
159
- if (sessionLoaded && selectedId === null && sortedIdsRef.current.length > 0) {
160
- setSelectedId(sortedIdsRef.current[0]);
161
- }
162
- }, [sessionLoaded]);
163
- useEffect(() => {
164
- if (session && !previewResponse) {
165
- const raw = process.env['EDS_PREVIEW_COUNTS'];
166
- if (raw) {
167
- try {
168
- const counts = JSON.parse(raw);
169
- setPreviewResponse({
170
- components: {
171
- new: Array(counts.compNew).fill({}),
172
- changed: Array(counts.compChanged).fill({}),
173
- removed: Array(counts.compRemoved).fill({}),
174
- unchanged: Array(counts.compUnchanged).fill({}),
175
- },
176
- tokens: {
177
- new: Array(counts.tokNew).fill({}),
178
- changed: Array(counts.tokChanged).fill({}),
179
- removed: Array(counts.tokRemoved).fill({}),
180
- unchanged: Array(counts.tokUnchanged).fill({}),
181
- },
182
- });
183
- }
184
- catch (err) {
185
- process.stderr.write(`[eds] failed to parse EDS_PREVIEW_COUNTS: ${err instanceof Error ? err.message : String(err)}\n`);
186
- }
187
- }
188
- }
189
- }, [session]);
190
- // Lazy source code loading — writes to sourceCodeById, NOT session.components,
191
- // so it doesn't invalidate useMemo(sessionSummary) and cause a sidebar flash
192
- useEffect(() => {
193
- if (!session || !selectedId)
194
- return;
195
- if (sourceCodeById[selectedId] !== undefined)
196
- return; // already loaded
197
- const selectedComponent = session.components.find((c) => c.id === selectedId);
198
- if (!selectedComponent)
199
- return;
200
- readFile(selectedComponent.resolvedSourcePath, 'utf8')
201
- .then((code) => {
202
- setSourceCodeById((prev) => ({ ...prev, [selectedId]: code }));
203
- })
204
- .catch(() => {
205
- setSourceCodeById((prev) => ({ ...prev, [selectedId]: '' }));
206
- });
207
- }, [selectedId]);
208
- // SIGINT handler
209
- useEffect(() => {
210
- const handler = () => {
211
- if (Object.keys(draftsByComponentId).length > 0) {
212
- setShowQuitDialog(true);
213
- }
214
- else {
215
- process.exit(1);
216
- }
217
- };
218
- process.on('SIGINT', handler);
219
- return () => {
220
- process.off('SIGINT', handler);
221
- };
222
- }, [draftsByComponentId]);
223
- const updateStatus = async (componentId, newStatus) => {
224
- if (!session || !paths)
225
- return;
226
- const updatedSession = {
227
- ...session,
228
- components: session.components.map((c) => (c.id === componentId ? { ...c, status: newStatus } : c)),
229
- };
230
- setSession(updatedSession);
231
- await saveState(updatedSession);
232
- const component = session.components.find((c) => c.id === componentId);
233
- await appendEvent({
234
- type: 'status_changed',
235
- payload: { componentId, from: component?.status, to: newStatus },
236
- });
237
- debouncedRefreshPreview(updatedSession);
238
- };
239
- const dialogOpen = showHelp || showFinalizeDialog || showQuitDialog;
240
- useKeymap({
241
- sidebarFocused,
242
- editMode,
243
- dialogOpen,
244
- disabled: isSaving,
245
- }, {
246
- onSidebarUp: () => {
247
- if (!session)
248
- return;
249
- const ids = sortedIdsRef.current;
250
- const idx = ids.indexOf(selectedId ?? '');
251
- if (idx > 0) {
252
- setSelectedId(ids[idx - 1]);
253
- setJsonScrollOffset(0);
254
- setSidebarScrollOffset((prev) => Math.min(prev, idx - 1));
255
- }
256
- },
257
- onSidebarDown: () => {
258
- if (!session)
259
- return;
260
- const ids = sortedIdsRef.current;
261
- const idx = ids.indexOf(selectedId ?? '');
262
- if (idx < ids.length - 1) {
263
- setSelectedId(ids[idx + 1]);
264
- setJsonScrollOffset(0);
265
- setSidebarScrollOffset((prev) => {
266
- const newIdx = idx + 1;
267
- return newIdx >= prev + visibleCount ? newIdx - visibleCount + 1 : prev;
268
- });
269
- }
270
- },
271
- onSidebarSelect: () => { },
272
- onAccept: () => {
273
- if (selectedId)
274
- void updateStatus(selectedId, 'accepted');
275
- },
276
- onReject: () => {
277
- if (selectedId)
278
- void updateStatus(selectedId, 'rejected');
279
- },
280
- onEnterEditMode: () => {
281
- if (!session || !selectedId)
282
- return;
283
- const component = session.components.find((c) => c.id === selectedId);
284
- if (!component)
285
- return;
286
- setEditMode(true);
287
- setDraftsByComponentId((prev) => ({
288
- ...prev,
289
- [selectedId]: prev[selectedId] ?? JSON.stringify(stripScoringFields(component.editedProposal), null, 2),
290
- }));
291
- },
292
- onToggleSource: () => {
293
- if (terminalWidth < 120) {
294
- setSaveError('Terminal too narrow for source panel (need 120+ cols)');
295
- setTimeout(() => setSaveError(null), 3000);
296
- return;
297
- }
298
- setSourceVisible((prev) => !prev);
299
- },
300
- onScrollUp: () => {
301
- setJsonScrollOffset((prev) => Math.max(0, prev - 1));
302
- },
303
- onScrollDown: () => {
304
- setJsonScrollOffset((prev) => prev + 1);
305
- },
306
- onToggleFocus: () => setSidebarFocused((prev) => !prev),
307
- onApproveAll: async () => {
308
- if (!session || !paths)
309
- return;
310
- const updatedComponents = session.components.map((c) => c.status === 'needs-review' ? { ...c, status: 'accepted' } : c);
311
- const affected = updatedComponents.filter((c) => c.status === 'accepted').length -
312
- session.components.filter((c) => c.status === 'accepted').length;
313
- const updatedSession = { ...session, components: updatedComponents };
314
- setSession(updatedSession);
315
- await saveState(updatedSession);
316
- await appendEvent({ type: 'approve_all', payload: { affected } });
317
- syncSessionToDb(updatedSession);
318
- void refreshPreview();
319
- },
320
- onFinalize: () => setShowFinalizeDialog(true),
321
- onQuit: () => {
322
- if (Object.keys(draftsByComponentId).length > 0) {
323
- setShowQuitDialog(true);
324
- }
325
- else {
326
- process.exit(1);
327
- }
328
- },
329
- onToggleHelp: () => setShowHelp((prev) => !prev),
330
- });
331
- // Sorted order: flagged+unresolved first, then ascending confidence.
332
- // Memoised above early returns (React rules of hooks) so the array reference
333
- // is stable between renders — prevents Sidebar repainting on scroll/select.
334
- const sessionSummary = useMemo(() => (session?.components ?? [])
335
- .map((c) => ({
336
- id: c.id,
337
- name: c.name,
338
- status: c.status,
339
- previewAnnotation: previewAnnotations[c.name],
340
- extractionConfidence: c.originalProposal.extractionConfidence ?? null,
341
- needsReview: c.originalProposal.needsReview ?? false,
342
- }))
343
- .sort((a, b) => {
344
- const aFlagged = a.needsReview && a.status === 'needs-review' ? 0 : 1;
345
- const bFlagged = b.needsReview && b.status === 'needs-review' ? 0 : 1;
346
- if (aFlagged !== bFlagged)
347
- return aFlagged - bFlagged;
348
- // null (unscored) sorts last; lower numeric score sorts first (most concerning)
349
- const aConf = a.extractionConfidence ?? 6;
350
- const bConf = b.extractionConfidence ?? 6;
351
- return aConf - bConf;
352
- }), [session?.components, previewAnnotations]);
353
- // Stable ID order — kept in a ref for use inside keymap handlers
354
- const sortedIds = useMemo(() => sessionSummary.map((c) => c.id), [sessionSummary]);
355
- sortedIdsRef.current = sortedIds;
356
- if (loading) {
49
+ // ── All side effects in one hook ──────────────────────────────────────────
50
+ useSideEffects(state, dispatch, { sessionId, saveState, appendEvent });
51
+ // ── Render ────────────────────────────────────────────────────────────────
52
+ if (loading)
357
53
  return _jsx(Text, { children: "Loading session..." });
358
- }
359
- if (sessionError) {
54
+ if (sessionError)
360
55
  return (_jsxs(Text, { color: "red", children: [sessionError, '\nPress q to exit.'] }));
361
- }
362
- if (!session || !paths) {
56
+ if (!state.session)
363
57
  return _jsx(Text, { color: "red", children: "Session unavailable." });
364
- }
365
- if (finalizedResult) {
366
- return _jsx(FinalizedScreen, { result: finalizedResult });
367
- }
58
+ const { mode, session, selectedId, sidebarScrollOffset, jsonScrollOffset, editor } = state;
59
+ if (mode.type === 'finalized')
60
+ return _jsx(FinalizedScreen, { result: mode });
368
61
  const selectedRecord = session.components.find((c) => c.id === selectedId) ?? null;
369
62
  const sessionDetail = selectedRecord ? createReviewSessionDetail({ ...session, components: [selectedRecord] }) : null;
370
63
  const selectedDetail = sessionDetail?.components[0] ?? null;
371
- const acceptedCount = session.components.filter((c) => c.status === 'accepted').length;
372
- const rejectedCount = session.components.filter((c) => c.status === 'rejected').length;
373
- const reviewedCount = session.components.filter((c) => c.status === 'reviewed').length;
374
- const needsReviewCount = session.components.filter((c) => c.status === 'needs-review').length;
375
- const hints = editMode
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
376
75
  ? [
377
76
  { key: 'Ctrl+S', label: 'save' },
378
77
  { key: 'Esc', label: 'discard' },
@@ -384,141 +83,21 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
384
83
  { key: 'q', label: 'quit' },
385
84
  ];
386
85
  const collapsed = terminalWidth < 80;
387
- // TopBar(1) + statusbar(1) + footer(1) + border padding(2) = ~5 chrome rows
388
- const CHROME_ROWS = 5;
389
- const terminalRows = stdout?.rows ?? 24;
390
- const visibleCount = Math.max(1, terminalRows - CHROME_ROWS);
391
- const longestName = session.components.reduce((max, c) => Math.max(max, c.name.length), 0);
392
- // icon + space + name + 2 border chars; min 14, max 22
86
+ const longestName = session.components.reduce((m, c) => Math.max(m, c.name.length), 0);
393
87
  const sidebarWidth = collapsed ? 3 : Math.min(Math.max(longestName + 4, 14), 22);
394
- const handleDraftSave = async () => {
395
- if (!selectedId || !session || !paths)
396
- return;
397
- const draft = draftsByComponentId[selectedId];
398
- if (!draft)
399
- return;
400
- try {
401
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
402
- const parsed = JSON.parse(draft);
403
- const currentStatus = session.components.find((c) => c.id === selectedId)?.status;
404
- const newStatus = currentStatus === 'needs-review' ? 'reviewed' : currentStatus;
405
- const updatedSession = {
406
- ...session,
407
- components: session.components.map((c) => c.id === selectedId
408
- ? {
409
- ...c,
410
- editedProposal: parsed,
411
- status: newStatus,
412
- }
413
- : c),
414
- };
415
- const { [selectedId]: _removed, ...remainingDrafts } = draftsByComponentId;
416
- setSession(updatedSession);
417
- setDraftsByComponentId(remainingDrafts);
418
- setEditMode(false);
419
- await saveState(updatedSession);
420
- await appendEvent({
421
- type: 'draft_saved',
422
- payload: { componentId: selectedId },
423
- });
424
- // Sync all edited proposals to pipeline DB (keep 'generated' for preview)
425
- const allEdited = updatedSession.components.map((c) => c.editedProposal);
426
- const syncDb = openPipelineDb();
427
- try {
428
- storeRawComponents(syncDb, sessionId, allEdited, {
429
- status: 'generated',
430
- preserveCDF: true,
431
- });
432
- }
433
- finally {
434
- syncDb.close();
435
- }
436
- void refreshPreview();
437
- }
438
- catch {
439
- // JSON parse error — JsonEditor already shows the error inline
440
- }
441
- };
442
- const handleDraftDiscard = () => {
443
- if (!selectedId)
444
- return;
445
- const { [selectedId]: _removed, ...remainingDrafts } = draftsByComponentId;
446
- setDraftsByComponentId(remainingDrafts);
447
- setEditMode(false);
448
- };
449
- const handleFinalize = async () => {
450
- if (!session || !paths)
451
- return;
452
- try {
453
- await appendEvent({
454
- type: 'finalized',
455
- payload: {
456
- accepted: acceptedCount,
457
- rejected: rejectedCount,
458
- excluded: needsReviewCount,
459
- },
460
- });
461
- // Write all components back to DB, marking accepted as 'generated'
462
- // so loadCDFComponents (push/preview) only picks up accepted ones,
463
- // but loadRawComponents (editor re-entry) still finds all of them
464
- const acceptedNames = new Set(session.components.filter((c) => c.status === 'accepted').map((c) => c.name));
465
- const db = openPipelineDb();
466
- try {
467
- storeRawComponents(db, sessionId, session.components.map((c) => c.editedProposal), { status: 'extracted', preserveCDF: true });
468
- if (acceptedNames.size > 0) {
469
- db.prepare(`UPDATE raw_components SET status = 'generated' WHERE session_id = ? AND name IN (${[...acceptedNames].map(() => '?').join(',')})`).run(sessionId, ...acceptedNames);
470
- }
471
- }
472
- finally {
473
- db.close();
474
- }
475
- const output = JSON.stringify({
476
- status: 'finalized',
477
- sessionDir: paths.sessionDir,
478
- accepted: acceptedCount,
479
- rejected: rejectedCount,
480
- excluded: needsReviewCount,
481
- }, null, 2) + '\n';
482
- process.stdout.write(output);
483
- setFinalizedResult({
484
- accepted: acceptedCount,
485
- rejected: rejectedCount,
486
- excluded: needsReviewCount,
487
- });
488
- }
489
- catch (err) {
490
- setSaveError(String(err));
491
- }
492
- };
493
- 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: () => {
494
- void handleFinalize();
495
- }, onCancel: () => setShowFinalizeDialog(false) })), showQuitDialog && (_jsx(QuitDialog, { hasUnsavedDrafts: Object.keys(draftsByComponentId).length > 0, onConfirm: async () => {
496
- if (paths) {
497
- await appendEvent({
498
- type: 'session_quit',
499
- payload: { reason: 'user_quit' },
500
- });
501
- }
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' } });
502
92
  process.exit(1);
503
- }, 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) => {
504
- setSelectedId(id);
505
- setJsonScrollOffset(0);
506
- }, onScrollChange: setSidebarScrollOffset, collapsed: collapsed, width: sidebarWidth }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: selectedDetail ? (_jsx(ComponentDetail, { component: selectedDetail, sourceCode: selectedId ? (sourceCodeById[selectedId] ?? null) : null, draftValue: selectedId ? (draftsByComponentId[selectedId] ?? '') : '', editMode: editMode, sourceVisible: sourceVisible, jsonScrollOffset: jsonScrollOffset, sourceScrollX: 0, sourceScrollY: 0, terminalWidth: terminalWidth, previewAnnotation: selectedRecord ? previewAnnotations[selectedRecord.name] : undefined, onDraftChange: (value) => {
507
- if (!selectedId)
508
- return;
509
- setDraftsByComponentId((prev) => ({
510
- ...prev,
511
- [selectedId]: value,
512
- }));
513
- }, onSaveDraft: () => {
514
- void handleDraftSave();
515
- }, 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) }))] }));
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 }))] }));
516
96
  }
517
97
  function FinalizedScreen({ result, }) {
518
98
  useImmediateInput((_input, key) => {
519
- if (key.return || _input === 'q' || key.escape) {
99
+ if (key.return || _input === 'q' || key.escape)
520
100
  process.exit(0);
521
- }
522
101
  });
523
- 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" }) })] }));
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" }) })] }));
524
103
  }
@@ -1,20 +1,18 @@
1
1
  import React from 'react';
2
2
  import type { PreviewAnnotation, ReviewComponentDetail } from '../../types.js';
3
+ import type { EditorState } from '../state.js';
3
4
  type ComponentDetailProps = {
4
5
  component: ReviewComponentDetail;
5
6
  sourceCode: string | null;
6
7
  draftValue: string;
7
8
  editMode: boolean;
9
+ editorState: EditorState | null;
8
10
  sourceVisible: boolean;
9
11
  jsonScrollOffset: number;
10
12
  sourceScrollX: number;
11
13
  sourceScrollY: number;
12
14
  terminalWidth: number;
13
15
  previewAnnotation?: PreviewAnnotation;
14
- onDraftChange: (value: string) => void;
15
- onSaveDraft: () => void;
16
- onDiscardDraft: () => void;
17
- onScrollChange: (offset: number) => void;
18
16
  };
19
- export declare function ComponentDetail({ component, sourceCode, draftValue, editMode, sourceVisible, jsonScrollOffset, sourceScrollX, sourceScrollY, terminalWidth, previewAnnotation, onDraftChange, onSaveDraft, onDiscardDraft, }: ComponentDetailProps): React.ReactElement;
17
+ export declare function ComponentDetail({ component, sourceCode, draftValue, editMode, editorState, sourceVisible, jsonScrollOffset, sourceScrollX, sourceScrollY, terminalWidth, previewAnnotation, }: ComponentDetailProps): React.ReactElement;
20
18
  export {};
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { Box, Text } from 'ink';
3
3
  import { stripScoringFields } from '../../../../types.js';
4
4
  import { JsonPanel } from './JsonPanel.js';
5
- import { FieldEditor } from './FieldEditor.js';
5
+ import { JsonEditor } from './JsonEditor.js';
6
6
  import { SourcePanel } from './SourcePanel.js';
7
7
  function annotationLabel(annotation) {
8
8
  switch (annotation) {
@@ -18,7 +18,7 @@ function annotationLabel(annotation) {
18
18
  return null;
19
19
  }
20
20
  }
21
- export function ComponentDetail({ component, sourceCode, draftValue, editMode, sourceVisible, jsonScrollOffset, sourceScrollX, sourceScrollY, terminalWidth, previewAnnotation, onDraftChange, onSaveDraft, onDiscardDraft, }) {
21
+ export function ComponentDetail({ component, sourceCode, draftValue, editMode, editorState, sourceVisible, jsonScrollOffset, sourceScrollX, sourceScrollY, terminalWidth, previewAnnotation, }) {
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, s
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 ? (_jsx(FieldEditor, { value: draftValue || editedJson, width: editWidth, height: panelHeight, onChange: onDraftChange, 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' })] }));
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' })] }));
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, onCancel, }: FinalizeDialogProps): React.ReactElement;
9
+ export declare function FinalizeDialog({ accepted, rejected, needsReview, onConfirm: _onConfirm, onCancel: _onCancel, }: FinalizeDialogProps): React.ReactElement;
10
10
  export {};