@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.
- package/dist/package.json +1 -1
- package/dist/src/analyze/select/tui/App.js +441 -54
- package/dist/src/analyze/select/tui/components/ComponentDetail.d.ts +4 -3
- package/dist/src/analyze/select/tui/components/ComponentDetail.js +2 -2
- package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +1 -1
- package/dist/src/analyze/select/tui/components/FinalizeDialog.js +10 -2
- package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +1 -1
- package/dist/src/analyze/select/tui/components/HelpOverlay.js +7 -2
- package/dist/src/analyze/select/tui/components/JsonEditor.d.ts +4 -7
- package/dist/src/analyze/select/tui/components/JsonEditor.js +125 -17
- package/dist/src/analyze/select/tui/components/QuitDialog.d.ts +1 -1
- package/dist/src/analyze/select/tui/components/QuitDialog.js +10 -2
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +2 -0
- package/dist/src/analyze/select/tui/components/StatusBar.d.ts +2 -0
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +0 -5
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +4 -7
- package/dist/src/analyze/select/tui/hooks/useKeymap.d.ts +23 -0
- package/dist/src/analyze/select/tui/hooks/useKeymap.js +67 -1
- package/dist/src/analyze/select/tui/hooks/useUndo.js +21 -15
- package/dist/src/analyze/tui/AnalyzeView.js +0 -2
- package/dist/src/import/tui/WizardApp.js +0 -2
- package/dist/src/import/tui/steps/GenerateReviewStep.js +11 -4
- package/dist/src/print/validate/tui/ValidateView.js +0 -2
- package/package.json +2 -2
- package/dist/src/analyze/select/tui/hooks/useRawMode.d.ts +0 -6
- package/dist/src/analyze/select/tui/hooks/useRawMode.js +0 -16
- package/dist/src/analyze/select/tui/hooks/useSideEffects.d.ts +0 -17
- package/dist/src/analyze/select/tui/hooks/useSideEffects.js +0 -226
- package/dist/src/analyze/select/tui/inputToAction.d.ts +0 -13
- package/dist/src/analyze/select/tui/inputToAction.js +0 -68
- package/dist/src/analyze/select/tui/state.d.ts +0 -136
- package/dist/src/analyze/select/tui/state.js +0 -385
- package/dist/src/analyze/select/tui/utils.d.ts +0 -5
- package/dist/src/analyze/select/tui/utils.js +0 -11
package/dist/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect,
|
|
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 {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
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 [
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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 && !
|
|
46
|
-
|
|
143
|
+
if (loadedSession && !session) {
|
|
144
|
+
setSession(loadedSession);
|
|
145
|
+
setSelectedId(loadedSession.components[0]?.id ?? null);
|
|
47
146
|
}
|
|
48
147
|
}, [loadedSession]);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
337
|
+
}
|
|
338
|
+
if (sessionError) {
|
|
55
339
|
return (_jsxs(Text, { color: "red", children: [sessionError, '\nPress q to exit.'] }));
|
|
56
|
-
|
|
340
|
+
}
|
|
341
|
+
if (!session || !paths) {
|
|
57
342
|
return _jsx(Text, { color: "red", children: "Session unavailable." });
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
return _jsx(FinalizedScreen, { result:
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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: () =>
|
|
94
|
-
|
|
95
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
4
|
-
export function FinalizeDialog({ accepted, rejected, needsReview, onConfirm
|
|
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
|
|
6
|
+
export declare function HelpOverlay({ mode, onClose }: HelpOverlayProps): React.ReactElement;
|
|
7
7
|
export {};
|