@contentful/experience-design-system-cli 2.14.2 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -2
- package/dist/package.json +3 -2
- package/dist/src/analyze/command.d.ts +11 -0
- package/dist/src/analyze/command.js +298 -3
- package/dist/src/analyze/composite-closure.d.ts +25 -0
- package/dist/src/analyze/composite-closure.js +0 -0
- package/dist/src/analyze/composition/agent-parser/author-prompt.d.ts +4 -0
- package/dist/src/analyze/composition/agent-parser/author-prompt.js +22 -0
- package/dist/src/analyze/composition/agent-parser/extract-parser.d.ts +8 -0
- package/dist/src/analyze/composition/agent-parser/extract-parser.js +44 -0
- package/dist/src/analyze/composition/agent-parser/load-prompt.d.ts +7 -0
- package/dist/src/analyze/composition/agent-parser/load-prompt.js +31 -0
- package/dist/src/analyze/composition/agent-parser/resolve-via-parser.d.ts +46 -0
- package/dist/src/analyze/composition/agent-parser/resolve-via-parser.js +88 -0
- package/dist/src/analyze/composition/agent-parser/sandbox.d.ts +45 -0
- package/dist/src/analyze/composition/agent-parser/sandbox.js +187 -0
- package/dist/src/analyze/composition/apply-mapping.d.ts +22 -0
- package/dist/src/analyze/composition/apply-mapping.js +67 -0
- package/dist/src/analyze/composition/candidate-critic-agent.d.ts +7 -0
- package/dist/src/analyze/composition/candidate-critic-agent.js +39 -0
- package/dist/src/analyze/composition/candidate-critic.d.ts +32 -0
- package/dist/src/analyze/composition/candidate-critic.js +63 -0
- package/dist/src/analyze/composition/candidate-files.d.ts +34 -0
- package/dist/src/analyze/composition/candidate-files.js +131 -0
- package/dist/src/analyze/composition/composition-cache-key.d.ts +19 -0
- package/dist/src/analyze/composition/composition-cache-key.js +19 -0
- package/dist/src/analyze/composition/interchange-schema.d.ts +37 -0
- package/dist/src/analyze/composition/interchange-schema.js +61 -0
- package/dist/src/analyze/composition/merge-edges.d.ts +19 -0
- package/dist/src/analyze/composition/merge-edges.js +50 -0
- package/dist/src/analyze/composition/parse-map-edges.d.ts +16 -0
- package/dist/src/analyze/composition/parse-map-edges.js +55 -0
- package/dist/src/analyze/composition/resolve-mapping-cli.d.ts +23 -0
- package/dist/src/analyze/composition/resolve-mapping-cli.js +29 -0
- package/dist/src/analyze/composition/resolve-mapping.d.ts +59 -0
- package/dist/src/analyze/composition/resolve-mapping.js +94 -0
- package/dist/src/analyze/cycle-detection.d.ts +27 -0
- package/dist/src/analyze/cycle-detection.js +234 -0
- package/dist/src/analyze/cycle-view.d.ts +7 -0
- package/dist/src/analyze/cycle-view.js +28 -0
- package/dist/src/analyze/fuzzy-search.d.ts +3 -0
- package/dist/src/analyze/fuzzy-search.js +55 -0
- package/dist/src/analyze/issue-inheritance.d.ts +12 -0
- package/dist/src/analyze/issue-inheritance.js +108 -0
- package/dist/src/analyze/lineage.d.ts +29 -0
- package/dist/src/analyze/lineage.js +0 -0
- package/dist/src/analyze/scope-gate-cascade.d.ts +11 -0
- package/dist/src/analyze/scope-gate-cascade.js +170 -0
- package/dist/src/analyze/search-neighborhood.d.ts +3 -0
- package/dist/src/analyze/search-neighborhood.js +56 -0
- package/dist/src/analyze/select/tui/App.js +5 -16
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +0 -4
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +3 -15
- package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +29 -38
- package/dist/src/analyze/select/tui/components/FieldEditor.js +309 -185
- package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +13 -1
- package/dist/src/analyze/select/tui/components/FinalizeDialog.js +23 -2
- package/dist/src/analyze/select/tui/components/GotoBanner.d.ts +31 -0
- package/dist/src/analyze/select/tui/components/GotoBanner.js +30 -0
- package/dist/src/analyze/select/tui/components/GroupedSidebar.d.ts +91 -0
- package/dist/src/analyze/select/tui/components/GroupedSidebar.js +497 -0
- package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +13 -1
- package/dist/src/analyze/select/tui/components/HelpOverlay.js +9 -3
- package/dist/src/analyze/select/tui/components/JsonEditor.js +3 -3
- package/dist/src/analyze/select/tui/components/LineagePanel.d.ts +14 -0
- package/dist/src/analyze/select/tui/components/LineagePanel.js +22 -0
- package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +0 -7
- package/dist/src/analyze/select/tui/components/RationalePanel.js +3 -15
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +0 -6
- package/dist/src/analyze/select/tui/components/Sidebar.js +11 -27
- package/dist/src/analyze/select/tui/components/StatusBar.js +2 -1
- package/dist/src/analyze/select/tui/components/removed-components-text.d.ts +7 -0
- package/dist/src/analyze/select/tui/components/removed-components-text.js +11 -0
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +1 -5
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +8 -10
- package/dist/src/analyze/select/tui/theme.d.ts +15 -0
- package/dist/src/analyze/select/tui/theme.js +28 -0
- package/dist/src/analyze/selection-cascade.d.ts +3 -0
- package/dist/src/analyze/selection-cascade.js +19 -0
- package/dist/src/analyze/slot-graph.d.ts +11 -0
- package/dist/src/analyze/slot-graph.js +17 -0
- package/dist/src/apply/api-client.d.ts +2 -1
- package/dist/src/apply/api-client.js +40 -6
- package/dist/src/apply/command.d.ts +17 -1
- package/dist/src/apply/command.js +69 -11
- package/dist/src/apply/error-parser.d.ts +16 -0
- package/dist/src/apply/error-parser.js +117 -0
- package/dist/src/apply/tui/ServerPreviewView.js +3 -1
- package/dist/src/credentials-store.d.ts +6 -0
- package/dist/src/credentials-store.js +5 -1
- package/dist/src/generate/agent-runner.d.ts +6 -0
- package/dist/src/generate/agent-runner.js +24 -8
- package/dist/src/import/command.js +69 -31
- package/dist/src/import/cycle-auto-reject.d.ts +4 -0
- package/dist/src/import/cycle-auto-reject.js +14 -0
- package/dist/src/import/orchestrator.d.ts +20 -7
- package/dist/src/import/orchestrator.js +139 -35
- package/dist/src/import/strip-allowed-components.d.ts +18 -0
- package/dist/src/import/strip-allowed-components.js +22 -0
- package/dist/src/import/tui/CustomPromptBanner.d.ts +0 -5
- package/dist/src/import/tui/CustomPromptBanner.js +2 -1
- package/dist/src/import/tui/WizardApp.d.ts +10 -74
- package/dist/src/import/tui/WizardApp.js +234 -246
- package/dist/src/import/tui/ai-flag.d.ts +5 -0
- package/dist/src/import/tui/ai-flag.js +6 -0
- package/dist/src/import/tui/autocomplete.d.ts +5 -0
- package/dist/src/import/tui/autocomplete.js +32 -0
- package/dist/src/import/tui/components/AutoFilterBanner.d.ts +10 -0
- package/dist/src/import/tui/components/AutoFilterBanner.js +19 -0
- package/dist/src/import/tui/components/CounterStrip.d.ts +12 -0
- package/dist/src/import/tui/components/CounterStrip.js +12 -0
- package/dist/src/import/tui/components/LegendEntry.d.ts +2 -0
- package/dist/src/import/tui/components/LegendEntry.js +6 -0
- package/dist/src/import/tui/cycle-panel-scroll.d.ts +8 -0
- package/dist/src/import/tui/cycle-panel-scroll.js +33 -0
- package/dist/src/import/tui/final-review-host.d.ts +3 -7
- package/dist/src/import/tui/final-review-host.js +9 -4
- package/dist/src/import/tui/group-collapse.d.ts +2 -0
- package/dist/src/import/tui/group-collapse.js +11 -0
- package/dist/src/import/tui/history.d.ts +22 -0
- package/dist/src/import/tui/history.js +53 -0
- package/dist/src/import/tui/hooks/useLineage.d.ts +32 -0
- package/dist/src/import/tui/hooks/useLineage.js +50 -0
- package/dist/src/import/tui/hooks/useOverlayPanel.d.ts +13 -0
- package/dist/src/import/tui/hooks/useOverlayPanel.js +27 -0
- package/dist/src/import/tui/lineage-layout.d.ts +75 -0
- package/dist/src/import/tui/lineage-layout.js +82 -0
- package/dist/src/import/tui/runLivePreview.d.ts +11 -0
- package/dist/src/import/tui/runLivePreview.js +7 -1
- package/dist/src/import/tui/scope-gate-columns.d.ts +31 -0
- package/dist/src/import/tui/scope-gate-columns.js +94 -0
- package/dist/src/import/tui/scope-gate-host.d.ts +3 -1
- package/dist/src/import/tui/scope-gate-host.js +11 -3
- package/dist/src/import/tui/sidebar-width.d.ts +1 -0
- package/dist/src/import/tui/sidebar-width.js +3 -0
- package/dist/src/import/tui/step-filters.d.ts +19 -0
- package/dist/src/import/tui/step-filters.js +42 -0
- package/dist/src/import/tui/steps/AtomicGenerateReviewStep.d.ts +42 -0
- package/dist/src/import/tui/steps/AtomicGenerateReviewStep.js +726 -0
- package/dist/src/import/tui/steps/AtomicScopeGateStep.d.ts +3 -0
- package/dist/src/import/tui/steps/AtomicScopeGateStep.js +211 -0
- package/dist/src/import/tui/steps/CredentialsStep.js +5 -18
- package/dist/src/import/tui/steps/DoneStep.d.ts +0 -2
- package/dist/src/import/tui/steps/DoneStep.js +3 -2
- package/dist/src/import/tui/steps/ErrorStep.js +2 -1
- package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +19 -25
- package/dist/src/import/tui/steps/GenerateReviewStep.js +1258 -229
- package/dist/src/import/tui/steps/PathValidationStep.js +3 -2
- package/dist/src/import/tui/steps/PreviewStep.js +2 -1
- package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +0 -7
- package/dist/src/import/tui/steps/PushDecisionGateStep.js +3 -6
- package/dist/src/import/tui/steps/PushingStep.js +2 -1
- package/dist/src/import/tui/steps/RunningStep.d.ts +4 -1
- package/dist/src/import/tui/steps/RunningStep.js +3 -2
- package/dist/src/import/tui/steps/ScopeGateStep.d.ts +24 -0
- package/dist/src/import/tui/steps/ScopeGateStep.js +850 -193
- package/dist/src/import/tui/steps/TokenInputStep.js +2 -1
- package/dist/src/import/tui/steps/WelcomeStep.js +2 -1
- package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +6 -0
- package/dist/src/import/tui/steps/WizardPreviewStep.js +95 -56
- package/dist/src/import/tui/steps/auto-reject-decision.d.ts +7 -0
- package/dist/src/import/tui/steps/auto-reject-decision.js +9 -0
- package/dist/src/import/tui/steps/breaking-change-format.d.ts +2 -0
- package/dist/src/import/tui/steps/breaking-change-format.js +24 -0
- package/dist/src/import/tui/steps/enumerate-cycle-breaks.d.ts +14 -0
- package/dist/src/import/tui/steps/enumerate-cycle-breaks.js +0 -0
- package/dist/src/import/tui/steps/preview-diff.js +48 -5
- package/dist/src/import/tui/useFinalizePreview.d.ts +34 -0
- package/dist/src/import/tui/useFinalizePreview.js +0 -0
- package/dist/src/import/tui/useLivePreview.d.ts +3 -0
- package/dist/src/import/tui/useLivePreview.js +1 -0
- package/dist/src/import/tui/wizard-state-transitions.d.ts +5 -59
- package/dist/src/import/tui/wizard-state-transitions.js +5 -60
- package/dist/src/lib/composition-mode.d.ts +28 -0
- package/dist/src/lib/composition-mode.js +37 -0
- package/dist/src/lib/prompt-overrides.d.ts +38 -0
- package/dist/src/lib/prompt-overrides.js +69 -0
- package/dist/src/print/command.js +19 -1
- package/dist/src/runs/modify-launcher.d.ts +3 -0
- package/dist/src/runs/modify-launcher.js +2 -0
- package/dist/src/runs/replay-helpers.js +1 -0
- package/dist/src/runs/run-picker.js +2 -18
- package/dist/src/runs/store.d.ts +4 -0
- package/dist/src/session/db.d.ts +23 -40
- package/dist/src/session/db.js +110 -138
- package/dist/src/setup/composition-mode-prompt.d.ts +14 -0
- package/dist/src/setup/composition-mode-prompt.js +37 -0
- package/package.json +5 -4
- package/prompts/composition-dir-critic.md +7 -0
- package/prompts/composition-edges.md +8 -0
- package/prompts/composition-parser-repair-empty.md +1 -0
- package/prompts/composition-parser-repair-error.md +2 -0
- package/prompts/composition-parser.md +42 -0
|
@@ -1,86 +1,267 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { PALETTE } from '../../../analyze/select/tui/theme.js';
|
|
3
4
|
import { Box, Text, useStdout } from 'ink';
|
|
4
|
-
import {
|
|
5
|
+
import { GroupedSidebar, buildVisibleRows, } from '../../../analyze/select/tui/components/GroupedSidebar.js';
|
|
6
|
+
import { computeAllClosures } from '../../../analyze/composite-closure.js';
|
|
7
|
+
import { buildComponentGraph } from '../../../analyze/slot-graph.js';
|
|
8
|
+
import { computeCycleView } from '../../../analyze/cycle-view.js';
|
|
9
|
+
import { computeRenderStatuses, pickDrillTarget } from '../../../analyze/issue-inheritance.js';
|
|
5
10
|
import { JsonPanel } from '../../../analyze/select/tui/components/JsonPanel.js';
|
|
6
11
|
import { FieldEditor } from '../../../analyze/select/tui/components/FieldEditor.js';
|
|
7
12
|
import { StatusBar } from '../../../analyze/select/tui/components/StatusBar.js';
|
|
8
13
|
import { FinalizeDialog } from '../../../analyze/select/tui/components/FinalizeDialog.js';
|
|
14
|
+
import { removedComponentsHeader, removedComponentLine, } from '../../../analyze/select/tui/components/removed-components-text.js';
|
|
9
15
|
import { QuitDialog } from '../../../analyze/select/tui/components/QuitDialog.js';
|
|
10
16
|
import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
|
|
11
|
-
import { openPipelineDb, loadCDFComponents, storeCDFComponents, loadComponentReviewMetadata, loadComponentRationale, } from '../../../session/db.js';
|
|
17
|
+
import { openPipelineDb, loadCDFComponents, storeCDFComponents, loadComponentReviewMetadata, loadComponentRationale, loadSlotCycles, storeSlotCycles, } from '../../../session/db.js';
|
|
18
|
+
import { formatCyclePathSegments, findSlotCycles, suggestCycleBreakEdge } from '../../../analyze/cycle-detection.js';
|
|
19
|
+
import { followCycleScroll } from '../cycle-panel-scroll.js';
|
|
12
20
|
import { RationalePanel } from '../../../analyze/select/tui/components/RationalePanel.js';
|
|
13
21
|
import { ComponentRationalePanel } from '../../../analyze/select/tui/components/ComponentRationalePanel.js';
|
|
14
22
|
import { applyPreviewAnnotations } from '../../../analyze/select/preview-annotations.js';
|
|
15
23
|
import { useLivePreview } from '../useLivePreview.js';
|
|
24
|
+
import { useFinalizePreview } from '../useFinalizePreview.js';
|
|
16
25
|
import { computeNextScrollOffset } from '../../../analyze/select/tui/hooks/scroll-offset.js';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
import { fuzzyMatches } from '../../../analyze/fuzzy-search.js';
|
|
27
|
+
import { computeDirectNeighborhood, findAllAncestors as findAllAncestorsInclusive, } from '../../../analyze/search-neighborhood.js';
|
|
28
|
+
import { computeSidebarWidth } from '../sidebar-width.js';
|
|
29
|
+
import { computeAcceptCascade, computeRejectCascade } from '../../../analyze/selection-cascade.js';
|
|
30
|
+
import { useLineage } from '../hooks/useLineage.js';
|
|
31
|
+
import { computeCycleAutoRejectTargets } from '../../cycle-auto-reject.js';
|
|
32
|
+
import { useOverlayPanel } from '../hooks/useOverlayPanel.js';
|
|
33
|
+
import { LineagePanel } from '../../../analyze/select/tui/components/LineagePanel.js';
|
|
34
|
+
import { GotoBanner } from '../../../analyze/select/tui/components/GotoBanner.js';
|
|
35
|
+
import { computeSidebarBudget, FALLBACK_ROWS } from '../lineage-layout.js';
|
|
36
|
+
import { HelpOverlay } from '../../../analyze/select/tui/components/HelpOverlay.js';
|
|
37
|
+
import { legendEntry } from '../components/LegendEntry.js';
|
|
38
|
+
import { computeAutoRejectDecision } from './auto-reject-decision.js';
|
|
39
|
+
import { formatBreakingChange } from './breaking-change-format.js';
|
|
40
|
+
import { enumerateCycleBreaks, shouldBreakOverlayGoFullScreen } from './enumerate-cycle-breaks.js';
|
|
41
|
+
import { createHistoryStack } from '../history.js';
|
|
42
|
+
import { computeAutocomplete } from '../autocomplete.js';
|
|
43
|
+
import { resolveGroupRoot } from '../group-collapse.js';
|
|
44
|
+
import { buildFlatDimPredicate, computeFilterKeys, intersectFilterKeys } from '../step-filters.js';
|
|
45
|
+
export function sortComponentsForSidebar(components, cycleParticipants) {
|
|
46
|
+
const isEmpty = (entry) => Object.keys(entry.$properties ?? {}).length === 0 && Object.keys(entry.$slots ?? {}).length === 0;
|
|
47
|
+
const tier = (c) => {
|
|
48
|
+
if (cycleParticipants?.has(c.key))
|
|
49
|
+
return 0;
|
|
50
|
+
if (isEmpty(c.entry))
|
|
51
|
+
return 1;
|
|
52
|
+
return 2;
|
|
53
|
+
};
|
|
28
54
|
return [...components].sort((a, b) => {
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
32
|
-
return
|
|
55
|
+
const at = tier(a);
|
|
56
|
+
const bt = tier(b);
|
|
57
|
+
if (at !== bt)
|
|
58
|
+
return at - bt;
|
|
33
59
|
return a.key.localeCompare(b.key);
|
|
34
60
|
});
|
|
35
61
|
}
|
|
36
|
-
const VISIBLE_COUNT = 20;
|
|
37
62
|
const PANEL_HEIGHT = 22;
|
|
63
|
+
const HELP_SECTIONS = [
|
|
64
|
+
{
|
|
65
|
+
title: 'Navigation',
|
|
66
|
+
entries: [
|
|
67
|
+
{ keys: 'j / k / ↑ / ↓', label: 'Move cursor' },
|
|
68
|
+
{ keys: 'Tab', label: 'Toggle sidebar/panel' },
|
|
69
|
+
{ keys: 'Enter', label: 'Drill to source' },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
title: 'Selection',
|
|
74
|
+
entries: [
|
|
75
|
+
{ keys: 'a', label: 'Accept' },
|
|
76
|
+
{ keys: 'r', label: 'Reject' },
|
|
77
|
+
{ keys: 'A', label: 'Accept all' },
|
|
78
|
+
{ keys: 'F', label: 'Finalize' },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
title: 'Sidebar views',
|
|
83
|
+
entries: [
|
|
84
|
+
{ keys: 'L', label: 'Flat view' },
|
|
85
|
+
{ keys: 'l', label: 'Lineage' },
|
|
86
|
+
{ keys: 'i', label: 'Focus lineage' },
|
|
87
|
+
{ keys: 'w', label: 'Only breaking' },
|
|
88
|
+
{ keys: 'o', label: 'Only cycles' },
|
|
89
|
+
{ keys: 'space', label: 'Expand/collapse group' },
|
|
90
|
+
{ keys: 'E / C', label: 'Expand/collapse all' },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
title: 'Panels',
|
|
95
|
+
entries: [
|
|
96
|
+
{ keys: 'c', label: 'Cycle list' },
|
|
97
|
+
{ keys: 'p', label: 'Prop rationale' },
|
|
98
|
+
{ keys: 'P', label: 'Component rationale' },
|
|
99
|
+
{ keys: 's', label: 'Source' },
|
|
100
|
+
{ keys: 'J', label: 'Toggle JSON' },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
title: 'Resolving cycles',
|
|
105
|
+
entries: [
|
|
106
|
+
{ keys: 'r', label: 'Reject a cycle member (drops it from the push), or' },
|
|
107
|
+
{ keys: '', label: "break the cycle by removing a slot's" },
|
|
108
|
+
{ keys: '', label: '$allowedComponents edge (see [c] suggested fix).' },
|
|
109
|
+
{ keys: 'x', label: 'Break cycle (from [c]): delete a slot edge.' },
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
title: 'Search',
|
|
114
|
+
entries: [{ keys: '/', label: 'Search' }],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
title: 'History',
|
|
118
|
+
entries: [
|
|
119
|
+
{ keys: 'Ctrl+Z', label: 'Undo' },
|
|
120
|
+
{ keys: 'Ctrl+Y', label: 'Redo' },
|
|
121
|
+
{ keys: 'Ctrl+R', label: 'Reload from save' },
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
title: 'General',
|
|
126
|
+
entries: [
|
|
127
|
+
{ keys: '?', label: 'Close help' },
|
|
128
|
+
{ keys: 'q', label: 'Quit' },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
export { computeCycleAutoRejectTargets } from '../../cycle-auto-reject.js';
|
|
133
|
+
export function buildBreakingRows(breakingChanges) {
|
|
134
|
+
const out = [];
|
|
135
|
+
for (const b of breakingChanges) {
|
|
136
|
+
if (b.changes.length === 0) {
|
|
137
|
+
out.push({ label: `${b.componentName} — breaking`, componentName: b.componentName });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
for (const change of b.changes) {
|
|
141
|
+
const label = formatBreakingChange(change, b.current);
|
|
142
|
+
if ('slotId' in change) {
|
|
143
|
+
out.push({
|
|
144
|
+
label,
|
|
145
|
+
componentName: b.componentName,
|
|
146
|
+
focusTarget: { kind: 'slot', name: change.slotId },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
out.push({
|
|
151
|
+
label,
|
|
152
|
+
componentName: b.componentName,
|
|
153
|
+
focusTarget: { kind: 'prop', name: change.propertyId },
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
export function deriveBreakingChanges(response) {
|
|
161
|
+
const out = [];
|
|
162
|
+
for (const item of response.components.changed ?? []) {
|
|
163
|
+
if (item.changeClassification?.classification !== 'breaking')
|
|
164
|
+
continue;
|
|
165
|
+
const componentName = item.current?.name;
|
|
166
|
+
if (typeof componentName !== 'string')
|
|
167
|
+
continue;
|
|
168
|
+
out.push({
|
|
169
|
+
componentName,
|
|
170
|
+
changes: item.changeClassification.breakingChanges ?? [],
|
|
171
|
+
impact: item.impact,
|
|
172
|
+
current: item.current,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
38
177
|
export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, livePreview = true, spaceId = '', environmentId = '', cmaToken = '', host = '', tokensPath = '', initialFinalizeError = null, }) {
|
|
39
178
|
const { stdout } = useStdout();
|
|
40
179
|
const terminalWidth = stdout?.columns ?? 80;
|
|
41
180
|
const [components, setComponents] = useState([]);
|
|
42
181
|
const [loading, setLoading] = useState(true);
|
|
43
182
|
const [loadError, setLoadError] = useState(null);
|
|
44
|
-
const [
|
|
45
|
-
|
|
183
|
+
const [nav, setNav] = useState({
|
|
184
|
+
cursorRowIdx: 0,
|
|
185
|
+
sidebarScrollOffset: 0,
|
|
186
|
+
});
|
|
187
|
+
const cursorRowIdx = nav.cursorRowIdx;
|
|
188
|
+
const sidebarScrollOffset = nav.sidebarScrollOffset;
|
|
46
189
|
const [jsonScrollOffset, setJsonScrollOffset] = useState(0);
|
|
47
190
|
const [sidebarFocused, setSidebarFocused] = useState(true);
|
|
48
191
|
const [showFinalize, setShowFinalize] = useState(false);
|
|
49
192
|
const [showQuit, setShowQuit] = useState(false);
|
|
50
|
-
// FieldEditor is the default editor. JSON view is an opt-in read-only toggle.
|
|
51
193
|
const [showJson, setShowJson] = useState(false);
|
|
52
194
|
const [draftValue, setDraftValue] = useState('');
|
|
53
195
|
const [saveError, setSaveError] = useState(null);
|
|
54
|
-
// INTEG-4411: inline banner shown when the operator tries to finalize
|
|
55
|
-
// with zero accepted components. Cleared on the next 'a' or 'A' press.
|
|
56
196
|
const [finalizeError, setFinalizeError] = useState(initialFinalizeError);
|
|
57
|
-
// Feature 1: per-component review metadata (rationale + source location)
|
|
58
|
-
// for the currently-selected component. Reloaded when selection changes.
|
|
59
197
|
const [reviewMetadata, setReviewMetadata] = useState(null);
|
|
60
|
-
// Feature 2: per-component preview annotations refreshed after every
|
|
61
|
-
// FieldEditor save via the useLivePreview hook below. Empty when live
|
|
62
|
-
// preview is disabled, when creds are missing, or before the first response.
|
|
63
198
|
const [previewAnnotations, setPreviewAnnotations] = useState(new Map());
|
|
64
|
-
// Pilot-2026-06-24: raw removed list for the `d` detail panel. The
|
|
65
|
-
// annotation map only carries kind, not the rich summaries we need to list
|
|
66
|
-
// names/ids when the operator asks "which ones?".
|
|
67
199
|
const [removedComponents, setRemovedComponents] = useState([]);
|
|
68
|
-
const [
|
|
69
|
-
|
|
70
|
-
// Mutually exclusive states.
|
|
200
|
+
const [removedBannerCollapsed, setRemovedBannerCollapsed] = useState(false);
|
|
201
|
+
const removedBannerDefaultedRef = useRef(false);
|
|
71
202
|
const [panelOpen, setPanelOpen] = useState('none');
|
|
72
203
|
const [panelScrollOffset, setPanelScrollOffset] = useState(0);
|
|
73
204
|
const [textEntryActive, setTextEntryActive] = useState(false);
|
|
74
205
|
const [componentRationale, setComponentRationale] = useState(null);
|
|
75
|
-
// Tracks the first `g` of a potential `gg` double-tap (jumps to top in
|
|
76
|
-
// JSON-view + panel-focused state). Reset on any non-`g` key.
|
|
77
206
|
const pendingGRef = useRef(false);
|
|
207
|
+
const [slotCycles, setSlotCycles] = useState([]);
|
|
208
|
+
const [cyclePanelScroll, setCyclePanelScroll] = useState(0);
|
|
209
|
+
const [cyclesCursor, setCyclesCursor] = useState(0);
|
|
210
|
+
const cyclePanel = useOverlayPanel({
|
|
211
|
+
toggleKey: 'c',
|
|
212
|
+
onClose: () => {
|
|
213
|
+
setCyclePanelScroll(0);
|
|
214
|
+
setCyclesCursor(0);
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
const breakPanel = useOverlayPanel({
|
|
218
|
+
toggleKey: 'x',
|
|
219
|
+
onClose: () => {
|
|
220
|
+
setBreakCursor(0);
|
|
221
|
+
setBreakConfirm(false);
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
const [breakCursor, setBreakCursor] = useState(0);
|
|
225
|
+
const [breakConfirm, setBreakConfirm] = useState(false);
|
|
226
|
+
const [expandedGroups, setExpandedGroups] = useState(new Set());
|
|
227
|
+
const seededGroupsRef = useRef(false);
|
|
228
|
+
const [searchOpen, setSearchOpen] = useState(false);
|
|
229
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
230
|
+
const [autocompleteCandidates, setAutocompleteCandidates] = useState([]);
|
|
231
|
+
const [jumpFilterTarget, setJumpFilterTarget] = useState(null);
|
|
232
|
+
const lineagePanel = useOverlayPanel({ toggleKey: 'l' });
|
|
233
|
+
const [lineageCursor, setLineageCursor] = useState(0);
|
|
234
|
+
const breakingPanel = useOverlayPanel({ toggleKey: 'b', onClose: () => setBreakingDetailOpen(false) });
|
|
235
|
+
const [breakingChanges, setBreakingChanges] = useState([]);
|
|
236
|
+
const [breakingCursor, setBreakingCursor] = useState(0);
|
|
237
|
+
const [breakingDetailOpen, setBreakingDetailOpen] = useState(false);
|
|
238
|
+
const [pendingEditorFocus, setPendingEditorFocus] = useState(null);
|
|
239
|
+
const [columnOneView, setColumnOneView] = useState('grouped');
|
|
240
|
+
const [activeFilters, setActiveFilters] = useState(new Set());
|
|
241
|
+
const [autoRejected, setAutoRejected] = useState([]);
|
|
242
|
+
const [undoSnapshot, setUndoSnapshot] = useState(null);
|
|
243
|
+
const autoRejectFiredRef = useRef(false);
|
|
244
|
+
const [editorDirty, setEditorDirty] = useState(false);
|
|
245
|
+
const [showUnsavedWarning, setShowUnsavedWarning] = useState(false);
|
|
246
|
+
const [pendingFocusAway, setPendingFocusAway] = useState(null);
|
|
247
|
+
const [discardTrigger, setDiscardTrigger] = useState(0);
|
|
248
|
+
const historyRef = useRef(null);
|
|
249
|
+
const historySeededRef = useRef(false);
|
|
250
|
+
const [showReloadDialog, setShowReloadDialog] = useState(false);
|
|
251
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
78
252
|
const handleLivePreviewResult = (response) => {
|
|
79
253
|
if (!response)
|
|
80
254
|
return;
|
|
81
255
|
setPreviewAnnotations(applyPreviewAnnotations(response, components.map((c) => c.key)));
|
|
82
|
-
|
|
256
|
+
const nextRemoved = response.components.removed ?? [];
|
|
257
|
+
setRemovedComponents(nextRemoved);
|
|
258
|
+
if (!removedBannerDefaultedRef.current && nextRemoved.length > 0) {
|
|
259
|
+
removedBannerDefaultedRef.current = true;
|
|
260
|
+
setRemovedBannerCollapsed(nextRemoved.length > 5);
|
|
261
|
+
}
|
|
262
|
+
setBreakingChanges(deriveBreakingChanges(response));
|
|
83
263
|
};
|
|
264
|
+
const acceptedCountForPreview = components.filter((c) => c.status === 'accepted').length;
|
|
84
265
|
const livePreviewHook = useLivePreview({
|
|
85
266
|
enabled: livePreview,
|
|
86
267
|
sessionId: extractSessionId,
|
|
@@ -90,9 +271,10 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
90
271
|
cmaToken,
|
|
91
272
|
host,
|
|
92
273
|
onResult: handleLivePreviewResult,
|
|
274
|
+
// With nothing accepted, preview the delete-all diff so the review UI shows
|
|
275
|
+
// which existing components a push would remove (instead of an empty preview).
|
|
276
|
+
deleteAllComponents: acceptedCountForPreview === 0,
|
|
93
277
|
});
|
|
94
|
-
// Manual spinner cycling (no extra dep) for the sidebar status-row
|
|
95
|
-
// indicator. Runs only while the live-preview hook reports `running`.
|
|
96
278
|
const SPINNER_FRAMES = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏';
|
|
97
279
|
const [spinnerTick, setSpinnerTick] = useState(0);
|
|
98
280
|
useEffect(() => {
|
|
@@ -102,39 +284,56 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
102
284
|
return () => clearInterval(id);
|
|
103
285
|
}, [livePreviewHook.status]);
|
|
104
286
|
const livePreviewSpinner = SPINNER_FRAMES[spinnerTick % SPINNER_FRAMES.length];
|
|
287
|
+
const loadSessionState = () => {
|
|
288
|
+
const db = openPipelineDb();
|
|
289
|
+
let cdfComponents = [];
|
|
290
|
+
let cycles = [];
|
|
291
|
+
try {
|
|
292
|
+
cdfComponents = loadCDFComponents(db, extractSessionId);
|
|
293
|
+
cycles = loadSlotCycles(db, extractSessionId);
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
db.close();
|
|
297
|
+
}
|
|
298
|
+
if (cdfComponents.length === 0) {
|
|
299
|
+
return {
|
|
300
|
+
entries: [],
|
|
301
|
+
cycles: [],
|
|
302
|
+
error: 'No generated definitions found for this session. Try re-running generate.',
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const cycleParticipants = new Set();
|
|
306
|
+
for (const c of cycles)
|
|
307
|
+
for (const p of c.path)
|
|
308
|
+
cycleParticipants.add(p);
|
|
309
|
+
const reviewEntries = cdfComponents.map(({ key, entry }) => ({
|
|
310
|
+
key,
|
|
311
|
+
entry,
|
|
312
|
+
status: 'needs-review',
|
|
313
|
+
}));
|
|
314
|
+
return {
|
|
315
|
+
entries: sortComponentsForSidebar(reviewEntries, cycleParticipants),
|
|
316
|
+
cycles,
|
|
317
|
+
error: null,
|
|
318
|
+
};
|
|
319
|
+
};
|
|
105
320
|
useEffect(() => {
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
cdfComponents = loadCDFComponents(db, extractSessionId);
|
|
111
|
-
}
|
|
112
|
-
finally {
|
|
113
|
-
db.close();
|
|
114
|
-
}
|
|
115
|
-
if (cdfComponents.length === 0) {
|
|
116
|
-
setLoadError('No generated definitions found for this session. Try re-running generate.');
|
|
321
|
+
try {
|
|
322
|
+
const { entries, cycles, error } = loadSessionState();
|
|
323
|
+
if (error) {
|
|
324
|
+
setLoadError(error);
|
|
117
325
|
setLoading(false);
|
|
118
326
|
return;
|
|
119
327
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
entry,
|
|
123
|
-
status: 'needs-review',
|
|
124
|
-
}));
|
|
125
|
-
setComponents(sortComponentsForSidebar(reviewEntries));
|
|
328
|
+
setSlotCycles(cycles);
|
|
329
|
+
setComponents(entries);
|
|
126
330
|
setLoading(false);
|
|
127
331
|
}
|
|
128
|
-
|
|
332
|
+
catch (e) {
|
|
129
333
|
setLoadError(String(e));
|
|
130
334
|
setLoading(false);
|
|
131
|
-
}
|
|
335
|
+
}
|
|
132
336
|
}, []);
|
|
133
|
-
// Pilot-2026-06-23 R2: fire the live preview once on entry to final-review
|
|
134
|
-
// so diff badges populate before the operator's first save. We gate on the
|
|
135
|
-
// livePreview prop to honor --no-live-preview without depending on the
|
|
136
|
-
// hook's internal short-circuit. Cred-missing is still handled by the
|
|
137
|
-
// hook's own no-op path.
|
|
138
337
|
useEffect(() => {
|
|
139
338
|
if (loading)
|
|
140
339
|
return;
|
|
@@ -143,12 +342,279 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
143
342
|
if (components.length === 0)
|
|
144
343
|
return;
|
|
145
344
|
livePreviewHook.trigger();
|
|
146
|
-
// Intentionally only on load completion — subsequent fires happen via
|
|
147
|
-
// handleEditSave. Adding livePreviewHook to deps would re-fire on every
|
|
148
|
-
// hook re-creation.
|
|
149
345
|
}, [loading]);
|
|
150
|
-
|
|
151
|
-
|
|
346
|
+
const finalizePreview = useFinalizePreview({
|
|
347
|
+
open: showFinalize,
|
|
348
|
+
extractSessionId,
|
|
349
|
+
tokensPath,
|
|
350
|
+
spaceId,
|
|
351
|
+
environmentId,
|
|
352
|
+
cmaToken,
|
|
353
|
+
host,
|
|
354
|
+
acceptedKeys: new Set(components.filter((c) => c.status === 'accepted').map((c) => c.key)),
|
|
355
|
+
});
|
|
356
|
+
const handleFinalizeConfirm = () => {
|
|
357
|
+
const acceptedCount = components.filter((c) => c.status === 'accepted').length;
|
|
358
|
+
const explicitlyRejected = components.filter((c) => c.status === 'rejected').map((c) => c.key);
|
|
359
|
+
const unresolved = components.filter((c) => c.status === 'needs-review').map((c) => c.key);
|
|
360
|
+
const toReject = [...explicitlyRejected, ...unresolved];
|
|
361
|
+
if (toReject.length > 0) {
|
|
362
|
+
const db = openPipelineDb();
|
|
363
|
+
try {
|
|
364
|
+
const stmt = db.prepare(`UPDATE raw_components SET status = 'generate-rejected' WHERE session_id = ? AND name = ?`);
|
|
365
|
+
db.exec('BEGIN');
|
|
366
|
+
try {
|
|
367
|
+
for (const name of toReject) {
|
|
368
|
+
stmt.run(extractSessionId, name);
|
|
369
|
+
}
|
|
370
|
+
db.exec('COMMIT');
|
|
371
|
+
}
|
|
372
|
+
catch (e) {
|
|
373
|
+
db.exec('ROLLBACK');
|
|
374
|
+
throw e;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
db.close();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
onFinalize(acceptedCount, explicitlyRejected.length, unresolved.length);
|
|
382
|
+
};
|
|
383
|
+
const recomputeCycles = (currentComponents) => {
|
|
384
|
+
try {
|
|
385
|
+
const view = computeCycleView(currentComponents);
|
|
386
|
+
const rawCycles = view.pushBlocking;
|
|
387
|
+
const next = rawCycles.map((cycle) => ({
|
|
388
|
+
path: cycle.path,
|
|
389
|
+
edges: cycle.edges,
|
|
390
|
+
suggestedBreak: cycle.edges.length > 0 ? suggestCycleBreakEdge(cycle, rawCycles) : null,
|
|
391
|
+
}));
|
|
392
|
+
const prevSerialized = JSON.stringify(slotCycles);
|
|
393
|
+
const nextSerialized = JSON.stringify(next);
|
|
394
|
+
if (prevSerialized === nextSerialized)
|
|
395
|
+
return;
|
|
396
|
+
setSlotCycles(next);
|
|
397
|
+
const db = openPipelineDb();
|
|
398
|
+
try {
|
|
399
|
+
storeSlotCycles(db, extractSessionId, next);
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
402
|
+
db.close();
|
|
403
|
+
}
|
|
404
|
+
if (next.length === 0)
|
|
405
|
+
setFinalizeError(null);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
// Defensive: swallow — never let cycle detection crash the review UI.
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
const componentGraph = useMemo(() => buildComponentGraph(components), [components]);
|
|
412
|
+
const sidebarGraph = useMemo(() => buildComponentGraph(components, { stripRejectedEdges: true }), [components]);
|
|
413
|
+
const closures = useMemo(() => computeAllClosures(componentGraph), [componentGraph]);
|
|
414
|
+
useEffect(() => {
|
|
415
|
+
if (seededGroupsRef.current)
|
|
416
|
+
return;
|
|
417
|
+
if (closures.size === 0 && slotCycles.length === 0)
|
|
418
|
+
return;
|
|
419
|
+
seededGroupsRef.current = true;
|
|
420
|
+
const seed = new Set(closures.keys());
|
|
421
|
+
for (const cyc of slotCycles)
|
|
422
|
+
for (const p of cyc.path)
|
|
423
|
+
seed.add(p);
|
|
424
|
+
setExpandedGroups(seed);
|
|
425
|
+
}, [closures, slotCycles]);
|
|
426
|
+
const directIssues = useMemo(() => {
|
|
427
|
+
const m = new Map();
|
|
428
|
+
for (const c of components) {
|
|
429
|
+
if (c.status === 'rejected')
|
|
430
|
+
m.set(c.key, 'error');
|
|
431
|
+
}
|
|
432
|
+
return m;
|
|
433
|
+
}, [components]);
|
|
434
|
+
const cycleView = useMemo(() => computeCycleView(components), [components]);
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
const decision = computeAutoRejectDecision({
|
|
437
|
+
loading,
|
|
438
|
+
autoRejectFired: autoRejectFiredRef.current,
|
|
439
|
+
hasCycle: cycleView.structural.size > 0,
|
|
440
|
+
});
|
|
441
|
+
if (decision === 'skip')
|
|
442
|
+
return;
|
|
443
|
+
autoRejectFiredRef.current = true;
|
|
444
|
+
const targets = computeCycleAutoRejectTargets(slotCycles, componentGraph);
|
|
445
|
+
if (targets.size === 0)
|
|
446
|
+
return;
|
|
447
|
+
const snapshot = new Map();
|
|
448
|
+
const flipped = [];
|
|
449
|
+
for (const c of components) {
|
|
450
|
+
if (!targets.has(c.key))
|
|
451
|
+
continue;
|
|
452
|
+
snapshot.set(c.key, c.status);
|
|
453
|
+
if (c.status !== 'rejected')
|
|
454
|
+
flipped.push(c.key);
|
|
455
|
+
}
|
|
456
|
+
setComponents((prev) => prev.map((c) => (targets.has(c.key) ? { ...c, status: 'rejected' } : c)));
|
|
457
|
+
setAutoRejected(flipped.length > 0 ? flipped : [...targets].sort());
|
|
458
|
+
setUndoSnapshot(flipped.length > 0 ? snapshot : null);
|
|
459
|
+
}, [loading, cycleView, componentGraph, slotCycles]);
|
|
460
|
+
useEffect(() => {
|
|
461
|
+
if (loading)
|
|
462
|
+
return;
|
|
463
|
+
if (historySeededRef.current)
|
|
464
|
+
return;
|
|
465
|
+
historySeededRef.current = true;
|
|
466
|
+
historyRef.current = createHistoryStack({
|
|
467
|
+
components: components.map((c) => ({ key: c.key, entry: c.entry, status: c.status })),
|
|
468
|
+
autoRejected: [],
|
|
469
|
+
undoSnapshot: null,
|
|
470
|
+
});
|
|
471
|
+
}, [loading, components]);
|
|
472
|
+
const pushHistorySnapshot = (entries, autoRej, undoSnap, label) => {
|
|
473
|
+
if (!historyRef.current)
|
|
474
|
+
return;
|
|
475
|
+
historyRef.current.push({
|
|
476
|
+
components: entries.map((c) => ({ key: c.key, entry: c.entry, status: c.status })),
|
|
477
|
+
autoRejected: [...autoRej],
|
|
478
|
+
undoSnapshot: undoSnap === null ? null : new Map(undoSnap),
|
|
479
|
+
}, label);
|
|
480
|
+
};
|
|
481
|
+
const autoRejectPushedRef = useRef(false);
|
|
482
|
+
useEffect(() => {
|
|
483
|
+
if (loading)
|
|
484
|
+
return;
|
|
485
|
+
if (!historySeededRef.current)
|
|
486
|
+
return;
|
|
487
|
+
if (!autoRejectFiredRef.current)
|
|
488
|
+
return;
|
|
489
|
+
if (autoRejectPushedRef.current)
|
|
490
|
+
return;
|
|
491
|
+
autoRejectPushedRef.current = true;
|
|
492
|
+
pushHistorySnapshot(components, autoRejected, undoSnapshot, 'mount-auto-reject');
|
|
493
|
+
}, [autoRejected]);
|
|
494
|
+
const applyHistorySnapshot = (snap) => {
|
|
495
|
+
const restored = snap.components.map((c) => ({
|
|
496
|
+
key: c.key,
|
|
497
|
+
entry: c.entry,
|
|
498
|
+
status: c.status,
|
|
499
|
+
}));
|
|
500
|
+
setComponents(restored);
|
|
501
|
+
setAutoRejected(snap.autoRejected);
|
|
502
|
+
setUndoSnapshot(snap.undoSnapshot);
|
|
503
|
+
recomputeCycles(restored);
|
|
504
|
+
};
|
|
505
|
+
const handleUndo = () => {
|
|
506
|
+
const snap = historyRef.current?.undo();
|
|
507
|
+
if (!snap)
|
|
508
|
+
return;
|
|
509
|
+
applyHistorySnapshot(snap);
|
|
510
|
+
};
|
|
511
|
+
const handleRedo = () => {
|
|
512
|
+
const snap = historyRef.current?.redo();
|
|
513
|
+
if (!snap)
|
|
514
|
+
return;
|
|
515
|
+
applyHistorySnapshot(snap);
|
|
516
|
+
};
|
|
517
|
+
const reloadFromSave = () => {
|
|
518
|
+
try {
|
|
519
|
+
const { entries, cycles, error } = loadSessionState();
|
|
520
|
+
if (error) {
|
|
521
|
+
setLoadError(error);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
setSlotCycles(cycles);
|
|
525
|
+
setComponents(entries);
|
|
526
|
+
const reloadGraph = buildComponentGraph(entries);
|
|
527
|
+
const reloadClosures = computeAllClosures(reloadGraph);
|
|
528
|
+
const reloadCycleView = computeCycleView(entries);
|
|
529
|
+
const reloadSeed = new Set();
|
|
530
|
+
for (const [name, closure] of reloadClosures.entries()) {
|
|
531
|
+
if (closure.nodes.length > 1)
|
|
532
|
+
reloadSeed.add(name);
|
|
533
|
+
}
|
|
534
|
+
for (const name of reloadCycleView.structural)
|
|
535
|
+
reloadSeed.add(name);
|
|
536
|
+
setExpandedGroups(reloadSeed);
|
|
537
|
+
seededGroupsRef.current = true;
|
|
538
|
+
setAutoRejected([]);
|
|
539
|
+
setUndoSnapshot(null);
|
|
540
|
+
autoRejectFiredRef.current = false;
|
|
541
|
+
autoRejectPushedRef.current = false;
|
|
542
|
+
historyRef.current?.reset({
|
|
543
|
+
components: entries.map((c) => ({ key: c.key, entry: c.entry, status: c.status })),
|
|
544
|
+
autoRejected: [],
|
|
545
|
+
undoSnapshot: null,
|
|
546
|
+
});
|
|
547
|
+
setNav({ cursorRowIdx: 0, sidebarScrollOffset: 0 });
|
|
548
|
+
setSaveError(null);
|
|
549
|
+
setFinalizeError(null);
|
|
550
|
+
}
|
|
551
|
+
catch (e) {
|
|
552
|
+
setLoadError(String(e));
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
const groupedItemsMemo = useMemo(() => components.map((c) => ({
|
|
556
|
+
key: c.key,
|
|
557
|
+
entry: c.entry,
|
|
558
|
+
status: (directIssues.get(c.key) ?? 'ok'),
|
|
559
|
+
})), [components, directIssues]);
|
|
560
|
+
const brokenKeys = useMemo(() => new Set(breakingChanges.map((b) => b.componentName)), [breakingChanges]);
|
|
561
|
+
const breakingRows = useMemo(() => buildBreakingRows(breakingChanges), [breakingChanges]);
|
|
562
|
+
const filterVisibleKeys = useMemo(() => {
|
|
563
|
+
if (jumpFilterTarget) {
|
|
564
|
+
return findAllAncestorsInclusive(jumpFilterTarget, sidebarGraph);
|
|
565
|
+
}
|
|
566
|
+
const categoryKeys = computeFilterKeys({
|
|
567
|
+
filters: activeFilters,
|
|
568
|
+
data: { cycles: cycleView.structural, broken: brokenKeys },
|
|
569
|
+
});
|
|
570
|
+
const searchKeys = (() => {
|
|
571
|
+
if (!searchQuery)
|
|
572
|
+
return undefined;
|
|
573
|
+
const matches = groupedItemsMemo.map((it) => it.key).filter((k) => fuzzyMatches(searchQuery, k));
|
|
574
|
+
if (matches.length === 0)
|
|
575
|
+
return undefined;
|
|
576
|
+
return computeDirectNeighborhood(matches, sidebarGraph);
|
|
577
|
+
})();
|
|
578
|
+
return intersectFilterKeys(categoryKeys, searchKeys);
|
|
579
|
+
}, [jumpFilterTarget, activeFilters, cycleView, brokenKeys, searchQuery, groupedItemsMemo, sidebarGraph]);
|
|
580
|
+
const visibleRowsMemo = useMemo(() => buildVisibleRows({
|
|
581
|
+
items: groupedItemsMemo,
|
|
582
|
+
cycleParticipants: cycleView.structural,
|
|
583
|
+
expandedGroups,
|
|
584
|
+
viewMode: columnOneView,
|
|
585
|
+
graph: sidebarGraph,
|
|
586
|
+
filterVisibleKeys,
|
|
587
|
+
}), [groupedItemsMemo, cycleView, expandedGroups, columnOneView, sidebarGraph, filterVisibleKeys]);
|
|
588
|
+
const selectableRowPositions = useMemo(() => {
|
|
589
|
+
const out = [];
|
|
590
|
+
for (let i = 0; i < visibleRowsMemo.length; i++) {
|
|
591
|
+
if (visibleRowsMemo[i].itemIdx >= 0)
|
|
592
|
+
out.push(i);
|
|
593
|
+
}
|
|
594
|
+
return out;
|
|
595
|
+
}, [visibleRowsMemo]);
|
|
596
|
+
const selectedIdx = visibleRowsMemo[cursorRowIdx]?.itemIdx ?? -1;
|
|
597
|
+
const focusedComponentKey = components[selectedIdx]?.key ?? null;
|
|
598
|
+
const { entries: lineageEntries, jumpables: lineageJumpables } = useLineage(focusedComponentKey, componentGraph);
|
|
599
|
+
const { sidebarVisibleCount: visibleCount, panelMaxRows } = computeSidebarBudget({
|
|
600
|
+
rows: stdout?.rows ?? FALLBACK_ROWS,
|
|
601
|
+
panelOpen: lineagePanel.isOpen,
|
|
602
|
+
entryCount: lineageEntries.length,
|
|
603
|
+
});
|
|
604
|
+
useEffect(() => {
|
|
605
|
+
if (selectableRowPositions.length === 0)
|
|
606
|
+
return;
|
|
607
|
+
const cursorInRange = selectableRowPositions.includes(cursorRowIdx);
|
|
608
|
+
const maxScroll = Math.max(0, visibleRowsMemo.length - visibleCount);
|
|
609
|
+
const scrollNeedsClamp = sidebarScrollOffset > maxScroll;
|
|
610
|
+
if (cursorInRange && !scrollNeedsClamp)
|
|
611
|
+
return;
|
|
612
|
+
const nextCursor = cursorInRange ? cursorRowIdx : selectableRowPositions[0];
|
|
613
|
+
setNav(() => ({
|
|
614
|
+
cursorRowIdx: nextCursor,
|
|
615
|
+
sidebarScrollOffset: Math.min(sidebarScrollOffset, maxScroll),
|
|
616
|
+
}));
|
|
617
|
+
}, [selectableRowPositions, cursorRowIdx, sidebarScrollOffset, visibleRowsMemo.length, visibleCount]);
|
|
152
618
|
useEffect(() => {
|
|
153
619
|
const current = components[selectedIdx];
|
|
154
620
|
if (!current) {
|
|
@@ -166,9 +632,6 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
166
632
|
db.close();
|
|
167
633
|
}
|
|
168
634
|
}, [selectedIdx, components, extractSessionId]);
|
|
169
|
-
// Load component-level rationale for the selected component (drives the
|
|
170
|
-
// `I` ComponentRationalePanel). Decoupled from review metadata so the data
|
|
171
|
-
// contracts can evolve independently.
|
|
172
635
|
useEffect(() => {
|
|
173
636
|
const current = components[selectedIdx];
|
|
174
637
|
if (!current) {
|
|
@@ -186,48 +649,85 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
186
649
|
db.close();
|
|
187
650
|
}
|
|
188
651
|
}, [selectedIdx, components, extractSessionId]);
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// component unresolved should NOT silently push it (Pilot-2026-06-24 R2).
|
|
198
|
-
const acceptedCount = components.filter((c) => c.status === 'accepted').length;
|
|
199
|
-
// INTEG-4411 refined: DO NOT block on `acceptedCount === 0` up-front.
|
|
200
|
-
// A push with zero accepted but one or more rejections targeting a
|
|
201
|
-
// component that exists server-side still produces REMOVALS — a valid
|
|
202
|
-
// push, not a no-op. Same for token-only diffs. The load-bearing no-op
|
|
203
|
-
// check lives downstream in WizardApp.runPreview, which consults the
|
|
204
|
-
// preview response and only blocks when every diff bucket is empty.
|
|
205
|
-
// We keep the `finalizeError` state so the wizard can route back here
|
|
206
|
-
// with an inline banner when that downstream check fires.
|
|
207
|
-
const explicitlyRejected = components.filter((c) => c.status === 'rejected').map((c) => c.key);
|
|
208
|
-
const unresolved = components.filter((c) => c.status === 'needs-review').map((c) => c.key);
|
|
209
|
-
const toReject = [...explicitlyRejected, ...unresolved];
|
|
210
|
-
if (toReject.length > 0) {
|
|
211
|
-
const db = openPipelineDb();
|
|
212
|
-
try {
|
|
213
|
-
const stmt = db.prepare(`UPDATE raw_components SET status = 'generate-rejected' WHERE session_id = ? AND name = ?`);
|
|
214
|
-
db.exec('BEGIN');
|
|
215
|
-
try {
|
|
216
|
-
for (const name of toReject) {
|
|
217
|
-
stmt.run(extractSessionId, name);
|
|
218
|
-
}
|
|
219
|
-
db.exec('COMMIT');
|
|
220
|
-
}
|
|
221
|
-
catch (e) {
|
|
222
|
-
db.exec('ROLLBACK');
|
|
223
|
-
throw e;
|
|
652
|
+
const renderStatusByKey = useMemo(() => {
|
|
653
|
+
const merged = new Map();
|
|
654
|
+
for (const closure of closures.values()) {
|
|
655
|
+
const per = computeRenderStatuses(closure, directIssues);
|
|
656
|
+
for (const [name, rs] of per.entries()) {
|
|
657
|
+
const existing = merged.get(name);
|
|
658
|
+
if (!existing || (!existing.isOwn && rs.isOwn)) {
|
|
659
|
+
merged.set(name, rs);
|
|
224
660
|
}
|
|
225
661
|
}
|
|
226
|
-
|
|
227
|
-
|
|
662
|
+
}
|
|
663
|
+
return merged;
|
|
664
|
+
}, [closures, directIssues]);
|
|
665
|
+
const selectionStateByKey = useMemo(() => {
|
|
666
|
+
const map = new Map();
|
|
667
|
+
for (const c of components) {
|
|
668
|
+
if (c.status === 'accepted')
|
|
669
|
+
map.set(c.key, 'accepted');
|
|
670
|
+
else if (c.status === 'rejected')
|
|
671
|
+
map.set(c.key, 'rejected');
|
|
672
|
+
else
|
|
673
|
+
map.set(c.key, 'undecided');
|
|
674
|
+
}
|
|
675
|
+
return map;
|
|
676
|
+
}, [components]);
|
|
677
|
+
const searchMatches = useMemo(() => {
|
|
678
|
+
if (!searchQuery)
|
|
679
|
+
return [];
|
|
680
|
+
const out = [];
|
|
681
|
+
for (const pos of selectableRowPositions) {
|
|
682
|
+
const row = visibleRowsMemo[pos];
|
|
683
|
+
const key = row ? components[row.itemIdx]?.key : undefined;
|
|
684
|
+
if (key && fuzzyMatches(searchQuery, key))
|
|
685
|
+
out.push(pos);
|
|
686
|
+
}
|
|
687
|
+
return out;
|
|
688
|
+
}, [searchQuery, selectableRowPositions, visibleRowsMemo, components]);
|
|
689
|
+
const searchMatchCount = useMemo(() => {
|
|
690
|
+
if (searchMatches.length === 0)
|
|
691
|
+
return 0;
|
|
692
|
+
const seen = new Set();
|
|
693
|
+
for (const pos of searchMatches) {
|
|
694
|
+
const itemIdx = visibleRowsMemo[pos]?.itemIdx;
|
|
695
|
+
if (itemIdx != null && itemIdx >= 0)
|
|
696
|
+
seen.add(itemIdx);
|
|
697
|
+
}
|
|
698
|
+
return seen.size;
|
|
699
|
+
}, [searchMatches, visibleRowsMemo]);
|
|
700
|
+
const dimPredicate = useMemo(() => buildFlatDimPredicate({
|
|
701
|
+
viewMode: columnOneView,
|
|
702
|
+
searchQuery,
|
|
703
|
+
filterVisibleKeys,
|
|
704
|
+
}), [columnOneView, searchQuery, filterVisibleKeys]);
|
|
705
|
+
const jumpCursorToRow = (rowIdx) => {
|
|
706
|
+
if (rowIdx < 0 || rowIdx >= visibleRowsMemo.length)
|
|
707
|
+
return;
|
|
708
|
+
setNav(({ sidebarScrollOffset: prev }) => {
|
|
709
|
+
let nextOff = prev;
|
|
710
|
+
if (rowIdx < prev)
|
|
711
|
+
nextOff = rowIdx;
|
|
712
|
+
else if (rowIdx >= prev + visibleCount)
|
|
713
|
+
nextOff = rowIdx - visibleCount + 1;
|
|
714
|
+
return { cursorRowIdx: rowIdx, sidebarScrollOffset: nextOff };
|
|
715
|
+
});
|
|
716
|
+
setJsonScrollOffset(0);
|
|
717
|
+
setDraftValue('');
|
|
718
|
+
setSaveError(null);
|
|
719
|
+
setPendingEditorFocus(null);
|
|
720
|
+
};
|
|
721
|
+
const jumpCursorToName = (name) => {
|
|
722
|
+
for (let i = 0; i < visibleRowsMemo.length; i++) {
|
|
723
|
+
const row = visibleRowsMemo[i];
|
|
724
|
+
if (row.itemIdx < 0)
|
|
725
|
+
continue;
|
|
726
|
+
if (components[row.itemIdx]?.key === name) {
|
|
727
|
+
jumpCursorToRow(i);
|
|
728
|
+
return;
|
|
228
729
|
}
|
|
229
730
|
}
|
|
230
|
-
onFinalize(acceptedCount, explicitlyRejected.length, unresolved.length);
|
|
231
731
|
};
|
|
232
732
|
const handleEditSave = () => {
|
|
233
733
|
const current = components[selectedIdx];
|
|
@@ -235,7 +735,6 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
235
735
|
return;
|
|
236
736
|
try {
|
|
237
737
|
const parsed = JSON.parse(draftValue);
|
|
238
|
-
// Accept both bare entry and wrapped { [key]: entry } forms
|
|
239
738
|
const keys = Object.keys(parsed);
|
|
240
739
|
const entry = keys.length === 1 && typeof parsed[keys[0]] === 'object' && parsed[keys[0]] !== null
|
|
241
740
|
? parsed[keys[0]]
|
|
@@ -244,7 +743,11 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
244
743
|
setSaveError('Invalid CDF entry: must have $type: "component" and $properties object');
|
|
245
744
|
return;
|
|
246
745
|
}
|
|
247
|
-
|
|
746
|
+
let updatedComponents = [];
|
|
747
|
+
setComponents((prev) => {
|
|
748
|
+
updatedComponents = prev.map((c, i) => i === selectedIdx ? { ...c, entry, status: c.status === 'needs-review' ? 'accepted' : c.status } : c);
|
|
749
|
+
return updatedComponents;
|
|
750
|
+
});
|
|
248
751
|
setDraftValue('');
|
|
249
752
|
setSaveError(null);
|
|
250
753
|
const db = openPipelineDb();
|
|
@@ -254,9 +757,9 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
254
757
|
finally {
|
|
255
758
|
db.close();
|
|
256
759
|
}
|
|
257
|
-
|
|
258
|
-
// the new state. The hook owns debounce + cred-missing short-circuit.
|
|
760
|
+
recomputeCycles(updatedComponents);
|
|
259
761
|
livePreviewHook.trigger();
|
|
762
|
+
pushHistorySnapshot(updatedComponents, autoRejected, undoSnapshot, 'edit-save');
|
|
260
763
|
}
|
|
261
764
|
catch (e) {
|
|
262
765
|
setSaveError(String(e));
|
|
@@ -266,34 +769,343 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
266
769
|
setDraftValue('');
|
|
267
770
|
setSaveError(null);
|
|
268
771
|
};
|
|
772
|
+
const breakEdges = useMemo(() => {
|
|
773
|
+
const cycle = slotCycles[cyclesCursor];
|
|
774
|
+
if (!cycle)
|
|
775
|
+
return [];
|
|
776
|
+
return enumerateCycleBreaks(cycle, components);
|
|
777
|
+
}, [slotCycles, cyclesCursor, components]);
|
|
778
|
+
const handleBreakEdge = (edge) => {
|
|
779
|
+
const idx = components.findIndex((c) => c.key === edge.fromComponent);
|
|
780
|
+
if (idx < 0)
|
|
781
|
+
return;
|
|
782
|
+
const target = components[idx];
|
|
783
|
+
const slot = target.entry.$slots?.[edge.slotName];
|
|
784
|
+
if (!slot || !Array.isArray(slot.$allowedComponents))
|
|
785
|
+
return;
|
|
786
|
+
const nextAllowed = slot.$allowedComponents.filter((v) => v !== edge.toComponent);
|
|
787
|
+
const nextEntry = {
|
|
788
|
+
...target.entry,
|
|
789
|
+
$slots: {
|
|
790
|
+
...target.entry.$slots,
|
|
791
|
+
[edge.slotName]: { ...slot, $allowedComponents: nextAllowed },
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
const next = components.map((c, i) => (i === idx ? { ...c, entry: nextEntry } : c));
|
|
795
|
+
setComponents(next);
|
|
796
|
+
const db = openPipelineDb();
|
|
797
|
+
try {
|
|
798
|
+
storeCDFComponents(db, extractSessionId, [{ key: target.key, entry: nextEntry }]);
|
|
799
|
+
}
|
|
800
|
+
finally {
|
|
801
|
+
db.close();
|
|
802
|
+
}
|
|
803
|
+
recomputeCycles(next);
|
|
804
|
+
livePreviewHook.trigger();
|
|
805
|
+
pushHistorySnapshot(next, autoRejected, undoSnapshot, 'break-cycle-edge');
|
|
806
|
+
};
|
|
807
|
+
const handleRejectComponent = (key) => {
|
|
808
|
+
const rejectCascade = computeRejectCascade(key, componentGraph);
|
|
809
|
+
const acceptCascade = computeAcceptCascade(key, componentGraph);
|
|
810
|
+
const next = components.map((c) => {
|
|
811
|
+
if (rejectCascade.has(c.key)) {
|
|
812
|
+
return { ...c, status: 'rejected' };
|
|
813
|
+
}
|
|
814
|
+
if (acceptCascade.has(c.key) && c.key !== key) {
|
|
815
|
+
return { ...c, status: 'needs-review' };
|
|
816
|
+
}
|
|
817
|
+
return c;
|
|
818
|
+
});
|
|
819
|
+
setComponents(next);
|
|
820
|
+
recomputeCycles(next);
|
|
821
|
+
pushHistorySnapshot(next, autoRejected, undoSnapshot, 'reject-cascade');
|
|
822
|
+
};
|
|
269
823
|
const dialogOpen = showFinalize || showQuit;
|
|
270
824
|
useImmediateInput((input, key) => {
|
|
271
|
-
if (loading
|
|
825
|
+
if (loading)
|
|
826
|
+
return;
|
|
827
|
+
// On a load error there's nothing to review — still let the operator quit
|
|
828
|
+
// (q / Esc / Enter) instead of trapping them on the error screen.
|
|
829
|
+
if (loadError) {
|
|
830
|
+
if (input === 'q' || key.escape || key.return)
|
|
831
|
+
onQuit();
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
if (showFinalize) {
|
|
835
|
+
// The dialog owns y/n/Enter/Esc; here we own j/k scroll of its deletion list.
|
|
836
|
+
if (input === 'j' || key.downArrow) {
|
|
837
|
+
finalizePreview.scrollBy(1);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (input === 'k' || key.upArrow) {
|
|
841
|
+
finalizePreview.scrollBy(-1);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
272
844
|
return;
|
|
845
|
+
}
|
|
273
846
|
if (dialogOpen)
|
|
274
847
|
return;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
848
|
+
if (showHelp)
|
|
849
|
+
return;
|
|
850
|
+
if (showReloadDialog) {
|
|
851
|
+
if (key.return) {
|
|
852
|
+
reloadFromSave();
|
|
853
|
+
setShowReloadDialog(false);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
if (key.escape) {
|
|
857
|
+
setShowReloadDialog(false);
|
|
858
|
+
return;
|
|
282
859
|
}
|
|
283
860
|
return;
|
|
284
861
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
862
|
+
if (key.ctrl && input === 'z') {
|
|
863
|
+
handleUndo();
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (key.ctrl && input === 'y') {
|
|
867
|
+
handleRedo();
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (key.ctrl && input === 'r') {
|
|
871
|
+
setShowReloadDialog(true);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
if (showUnsavedWarning) {
|
|
875
|
+
if (key.return) {
|
|
876
|
+
handleEditSave();
|
|
877
|
+
setShowUnsavedWarning(false);
|
|
878
|
+
if (pendingFocusAway === 'tab-to-sidebar')
|
|
879
|
+
setSidebarFocused(true);
|
|
880
|
+
setPendingFocusAway(null);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (key.escape) {
|
|
884
|
+
setDiscardTrigger((n) => n + 1);
|
|
885
|
+
setShowUnsavedWarning(false);
|
|
886
|
+
if (pendingFocusAway === 'tab-to-sidebar')
|
|
887
|
+
setSidebarFocused(true);
|
|
888
|
+
setPendingFocusAway(null);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
setShowUnsavedWarning(false);
|
|
892
|
+
setPendingFocusAway(null);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (searchOpen) {
|
|
896
|
+
if (key.escape) {
|
|
897
|
+
setSearchOpen(false);
|
|
898
|
+
setSearchQuery('');
|
|
899
|
+
setAutocompleteCandidates([]);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (key.return) {
|
|
903
|
+
setAutocompleteCandidates([]);
|
|
904
|
+
if (!searchQuery || searchMatches.length === 0) {
|
|
905
|
+
setSearchOpen(false);
|
|
906
|
+
setSearchQuery('');
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
let jumped = false;
|
|
910
|
+
for (let i = cursorRowIdx + 1; i < visibleRowsMemo.length; i++) {
|
|
911
|
+
const row = visibleRowsMemo[i];
|
|
912
|
+
if (row.itemIdx < 0)
|
|
913
|
+
continue;
|
|
914
|
+
const key2 = components[row.itemIdx]?.key;
|
|
915
|
+
if (key2 && fuzzyMatches(searchQuery, key2)) {
|
|
916
|
+
jumpCursorToRow(i);
|
|
917
|
+
jumped = true;
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (!jumped) {
|
|
922
|
+
jumpCursorToRow(searchMatches[0]);
|
|
923
|
+
}
|
|
924
|
+
setSearchOpen(false);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if (key.tab) {
|
|
928
|
+
const { completion, candidates } = computeAutocomplete(searchQuery, components.map((c) => c.key));
|
|
929
|
+
setSearchQuery(completion);
|
|
930
|
+
setAutocompleteCandidates(candidates);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (key.backspace) {
|
|
934
|
+
setAutocompleteCandidates([]);
|
|
935
|
+
setSearchQuery((q) => q.slice(0, -1));
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
if (input && input.length === 1 && input >= ' ' && input !== '\r' && input !== '\n') {
|
|
939
|
+
setAutocompleteCandidates([]);
|
|
940
|
+
setSearchQuery((q) => q + input);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (breakingPanel.isOpen) {
|
|
946
|
+
if (breakingPanel.handleInput(input, key))
|
|
947
|
+
return;
|
|
948
|
+
if (input === 'D') {
|
|
949
|
+
setBreakingDetailOpen((prev) => !prev);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (key.upArrow || input === 'k') {
|
|
953
|
+
setBreakingDetailOpen(false);
|
|
954
|
+
setBreakingCursor((c) => Math.max(0, c - 1));
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (key.downArrow || input === 'j') {
|
|
958
|
+
setBreakingDetailOpen(false);
|
|
959
|
+
setBreakingCursor((c) => Math.min(Math.max(0, breakingRows.length - 1), c + 1));
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
if (key.return) {
|
|
963
|
+
const row = breakingRows[breakingCursor];
|
|
964
|
+
if (row) {
|
|
965
|
+
jumpCursorToName(row.componentName);
|
|
966
|
+
if (row.focusTarget) {
|
|
967
|
+
setPendingEditorFocus({ componentName: row.componentName, target: row.focusTarget });
|
|
968
|
+
setSidebarFocused(false);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
breakingPanel.close();
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
if (lineagePanel.isOpen) {
|
|
977
|
+
if (lineagePanel.handleInput(input, key))
|
|
978
|
+
return;
|
|
979
|
+
if (key.upArrow || input === 'k') {
|
|
980
|
+
setLineageCursor((c) => Math.max(0, c - 1));
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (key.downArrow || input === 'j') {
|
|
984
|
+
setLineageCursor((c) => Math.min(Math.max(0, lineageJumpables.length - 1), c + 1));
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (key.tab) {
|
|
988
|
+
setLineageCursor((c) => (lineageJumpables.length === 0 ? 0 : (c + 1) % lineageJumpables.length));
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (key.return) {
|
|
992
|
+
const target = lineageJumpables[lineageCursor];
|
|
993
|
+
if (target && (target.entry.kind === 'ancestor' || target.entry.kind === 'descendant')) {
|
|
994
|
+
jumpCursorToName(target.entry.jumpTarget);
|
|
995
|
+
}
|
|
996
|
+
lineagePanel.close();
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (breakPanel.isOpen) {
|
|
1002
|
+
if (breakConfirm) {
|
|
1003
|
+
if (input === 'y') {
|
|
1004
|
+
const edge = breakEdges[breakCursor];
|
|
1005
|
+
if (edge)
|
|
1006
|
+
handleBreakEdge(edge);
|
|
1007
|
+
setBreakConfirm(false);
|
|
1008
|
+
breakPanel.close();
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (input === 'n' || key.escape) {
|
|
1012
|
+
setBreakConfirm(false);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
if (breakPanel.handleInput(input, key))
|
|
1018
|
+
return;
|
|
1019
|
+
if (key.upArrow || input === 'k') {
|
|
1020
|
+
setBreakCursor((c) => Math.max(0, c - 1));
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
if (key.downArrow || input === 'j') {
|
|
1024
|
+
setBreakCursor((c) => Math.min(Math.max(0, breakEdges.length - 1), c + 1));
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
if (key.return) {
|
|
1028
|
+
if (breakEdges[breakCursor])
|
|
1029
|
+
setBreakConfirm(true);
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
if (cyclePanel.isOpen) {
|
|
1035
|
+
if (input === 'x' && breakEdges.length > 0) {
|
|
1036
|
+
breakPanel.open();
|
|
1037
|
+
setBreakCursor(0);
|
|
1038
|
+
setBreakConfirm(false);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
if (cyclePanel.handleInput(input, key))
|
|
1042
|
+
return;
|
|
1043
|
+
if (input === 'q') {
|
|
1044
|
+
cyclePanel.close();
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
if (key.upArrow || input === 'k') {
|
|
1048
|
+
setCyclesCursor((c) => {
|
|
1049
|
+
const next = Math.max(0, c - 1);
|
|
1050
|
+
setCyclePanelScroll((scroll) => followCycleScroll(scroll, next, slotCycles, 20));
|
|
1051
|
+
return next;
|
|
1052
|
+
});
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (key.downArrow || input === 'j') {
|
|
1056
|
+
setCyclesCursor((c) => {
|
|
1057
|
+
const next = Math.min(Math.max(0, slotCycles.length - 1), c + 1);
|
|
1058
|
+
setCyclePanelScroll((scroll) => followCycleScroll(scroll, next, slotCycles, 20));
|
|
1059
|
+
return next;
|
|
1060
|
+
});
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (key.return) {
|
|
1064
|
+
const target = slotCycles[cyclesCursor];
|
|
1065
|
+
if (target && target.path.length > 0)
|
|
1066
|
+
jumpCursorToName(target.path[0]);
|
|
1067
|
+
cyclePanel.close();
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
if (input === 'c' && sidebarFocused && slotCycles.length > 0) {
|
|
1073
|
+
cyclePanel.open();
|
|
1074
|
+
setCyclePanelScroll(0);
|
|
1075
|
+
setCyclesCursor(0);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (input === 'l' && sidebarFocused && focusedComponentKey) {
|
|
1079
|
+
lineagePanel.open();
|
|
1080
|
+
setLineageCursor(0);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
if (input === 'b' && sidebarFocused && breakingChanges.length > 0) {
|
|
1084
|
+
breakingPanel.open();
|
|
1085
|
+
setBreakingCursor(0);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
if (input === 'd' && sidebarFocused && removedComponents.length > 0) {
|
|
1089
|
+
setRemovedBannerCollapsed((prev) => !prev);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (sidebarFocused && (input === 'o' || input === 'w')) {
|
|
1093
|
+
const category = input === 'o' ? 'cycles' : 'broken';
|
|
1094
|
+
setActiveFilters((prev) => {
|
|
1095
|
+
const next = new Set(prev);
|
|
1096
|
+
if (next.has(category))
|
|
1097
|
+
next.delete(category);
|
|
1098
|
+
else
|
|
1099
|
+
next.add(category);
|
|
1100
|
+
return next;
|
|
1101
|
+
});
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
if (input === 'i' && sidebarFocused && !key.tab && !key.ctrl && !key.meta && focusedComponentKey) {
|
|
1105
|
+
const t = focusedComponentKey;
|
|
1106
|
+
setJumpFilterTarget((prev) => (prev === t ? null : t));
|
|
290
1107
|
return;
|
|
291
1108
|
}
|
|
292
|
-
// Lifted rationale + source panels: i/I/s fire from anywhere (sidebar OR
|
|
293
|
-
// panel focus). Gated against text-entry surfaces inside FieldEditor
|
|
294
|
-
// (description editors, string-default editor, value-list text entry)
|
|
295
|
-
// via the `onTextEntryActiveChange` callback, plus the help/finalize/quit
|
|
296
|
-
// overlays and the JSON view.
|
|
297
1109
|
if (panelOpen !== 'none') {
|
|
298
1110
|
const PANEL_HEIGHT_LOCAL = 12;
|
|
299
1111
|
const next = computeNextScrollOffset(panelScrollOffset, input, key, 9999, PANEL_HEIGHT_LOCAL);
|
|
@@ -306,15 +1118,13 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
306
1118
|
setPanelScrollOffset(() => 0);
|
|
307
1119
|
return;
|
|
308
1120
|
}
|
|
309
|
-
// Guard against Ctrl-letter aliases (Tab is Ctrl+I in ASCII, Ctrl+S would
|
|
310
|
-
// collide with save in nested editors). Only react to bare keystrokes.
|
|
311
1121
|
const togglable = !key.ctrl && !key.tab && !key.meta && !key.return;
|
|
312
|
-
if (togglable && input === '
|
|
1122
|
+
if (togglable && input === 'p' && panelOpen === 'prop-rationale') {
|
|
313
1123
|
setPanelOpen('none');
|
|
314
1124
|
setPanelScrollOffset(() => 0);
|
|
315
1125
|
return;
|
|
316
1126
|
}
|
|
317
|
-
if (togglable && input === '
|
|
1127
|
+
if (togglable && input === 'P' && panelOpen === 'component-rationale') {
|
|
318
1128
|
setPanelOpen('none');
|
|
319
1129
|
setPanelScrollOffset(() => 0);
|
|
320
1130
|
return;
|
|
@@ -324,13 +1134,12 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
324
1134
|
setPanelScrollOffset(() => 0);
|
|
325
1135
|
return;
|
|
326
1136
|
}
|
|
327
|
-
|
|
328
|
-
if (togglable && input === 'i') {
|
|
1137
|
+
if (togglable && input === 'p') {
|
|
329
1138
|
setPanelOpen('prop-rationale');
|
|
330
1139
|
setPanelScrollOffset(() => 0);
|
|
331
1140
|
return;
|
|
332
1141
|
}
|
|
333
|
-
if (togglable && input === '
|
|
1142
|
+
if (togglable && input === 'P') {
|
|
334
1143
|
setPanelOpen('component-rationale');
|
|
335
1144
|
setPanelScrollOffset(() => 0);
|
|
336
1145
|
return;
|
|
@@ -344,39 +1153,53 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
344
1153
|
}
|
|
345
1154
|
const rationaleKeyOk = !textEntryActive && !showJson && !key.ctrl && !key.tab && !key.meta && !key.return;
|
|
346
1155
|
if (rationaleKeyOk) {
|
|
347
|
-
if (input === '
|
|
1156
|
+
if (input === 'p') {
|
|
348
1157
|
setPanelOpen('prop-rationale');
|
|
349
1158
|
setPanelScrollOffset(() => 0);
|
|
350
1159
|
return;
|
|
351
1160
|
}
|
|
352
|
-
if (input === '
|
|
1161
|
+
if (input === 'P') {
|
|
353
1162
|
setPanelOpen('component-rationale');
|
|
354
1163
|
setPanelScrollOffset(() => 0);
|
|
355
1164
|
return;
|
|
356
1165
|
}
|
|
1166
|
+
if (input === 's') {
|
|
1167
|
+
setPanelOpen('source');
|
|
1168
|
+
setPanelScrollOffset(() => 0);
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
357
1171
|
}
|
|
358
|
-
// Tab toggles focus bidirectionally between sidebar and panel. `e` is a
|
|
359
|
-
// sidebar-only alias for crossing INTO the panel — gating it to the
|
|
360
|
-
// sidebar-focused state prevents collision with FieldEditor's enum-values
|
|
361
|
-
// `e` binding (INTEG-4254) when the panel is focused. Crossing back from
|
|
362
|
-
// panel to sidebar is Tab-only.
|
|
363
1172
|
if (key.tab) {
|
|
1173
|
+
if (!sidebarFocused && editorDirty) {
|
|
1174
|
+
setPendingFocusAway('tab-to-sidebar');
|
|
1175
|
+
setShowUnsavedWarning(true);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
364
1178
|
setSidebarFocused((prev) => !prev);
|
|
365
1179
|
return;
|
|
366
1180
|
}
|
|
367
|
-
if (input === '
|
|
368
|
-
|
|
1181
|
+
if (input === ' ' && sidebarFocused && !showJson) {
|
|
1182
|
+
const current = components[selectedIdx];
|
|
1183
|
+
if (!current)
|
|
1184
|
+
return;
|
|
1185
|
+
const rootName = resolveGroupRoot(current.key, closures, cycleView.structural);
|
|
1186
|
+
if (!rootName)
|
|
1187
|
+
return;
|
|
1188
|
+
setExpandedGroups((prev) => {
|
|
1189
|
+
const next = new Set(prev);
|
|
1190
|
+
if (next.has(rootName))
|
|
1191
|
+
next.delete(rootName);
|
|
1192
|
+
else
|
|
1193
|
+
next.add(rootName);
|
|
1194
|
+
return next;
|
|
1195
|
+
});
|
|
369
1196
|
return;
|
|
370
1197
|
}
|
|
371
|
-
// JSON view + panel focused: own j/k/arrows/PageUp/PageDown/Ctrl+u/d/gg/G
|
|
372
|
-
// for scrolling. Computed against the live `selectedJson` so the
|
|
373
|
-
// viewport math matches what JsonPanel renders.
|
|
374
1198
|
if (!sidebarFocused && showJson) {
|
|
375
1199
|
const current = components[selectedIdx];
|
|
376
1200
|
const currentJson = current ? JSON.stringify({ [current.key]: current.entry }, null, 2) : '';
|
|
377
1201
|
const totalLines = currentJson.split('\n').length;
|
|
378
1202
|
const maxOffset = Math.max(0, totalLines - PANEL_HEIGHT);
|
|
379
|
-
// `gg` double-tap to jump to top; single `g` arms the pending flag.
|
|
380
1203
|
if (input === 'g' && !key.ctrl) {
|
|
381
1204
|
if (pendingGRef.current) {
|
|
382
1205
|
pendingGRef.current = false;
|
|
@@ -389,129 +1212,300 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
389
1212
|
const next = computeNextScrollOffset(jsonScrollOffset, input, key, totalLines, PANEL_HEIGHT);
|
|
390
1213
|
if (next !== null) {
|
|
391
1214
|
pendingGRef.current = false;
|
|
392
|
-
// Functional setState mirrors the cursor-stutter fix (commit 5d11e60).
|
|
393
|
-
// Clamp against maxOffset re-computed at apply time in case totalLines
|
|
394
|
-
// shifted between events (defensive — helper already clamps).
|
|
395
1215
|
const clamped = Math.min(maxOffset, Math.max(0, next));
|
|
396
1216
|
setJsonScrollOffset(() => clamped);
|
|
397
1217
|
return;
|
|
398
1218
|
}
|
|
399
|
-
// Any other key in this slice resets the gg-pending flag, then falls
|
|
400
|
-
// through to the early-return below so the panel-focused state still
|
|
401
|
-
// swallows non-scroll input.
|
|
402
1219
|
pendingGRef.current = false;
|
|
403
1220
|
}
|
|
404
|
-
// When the panel is focused, FieldEditor (or JsonPanel) owns the keys.
|
|
405
|
-
// Only Tab (handled above) should escape from the panel-focused state.
|
|
406
1221
|
if (!sidebarFocused)
|
|
407
1222
|
return;
|
|
408
|
-
|
|
1223
|
+
if (input === '/') {
|
|
1224
|
+
setSearchOpen(true);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
if (key.escape && jumpFilterTarget) {
|
|
1228
|
+
setJumpFilterTarget(null);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (key.escape && searchQuery) {
|
|
1232
|
+
setSearchQuery('');
|
|
1233
|
+
setAutocompleteCandidates([]);
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
if (input === '?') {
|
|
1237
|
+
setShowHelp(true);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
409
1240
|
if (input === 'q') {
|
|
410
1241
|
setShowQuit(true);
|
|
411
1242
|
return;
|
|
412
1243
|
}
|
|
413
|
-
if (input === 'F') {
|
|
1244
|
+
if (input === 'F' || input === 'f') {
|
|
1245
|
+
const acceptedNames = new Set(components.filter((c) => c.status === 'accepted').map((c) => c.key));
|
|
1246
|
+
const acceptedSubgraph = componentGraph.filter((n) => acceptedNames.has(n.name));
|
|
1247
|
+
const acceptedCycles = (() => {
|
|
1248
|
+
try {
|
|
1249
|
+
return findSlotCycles(acceptedSubgraph);
|
|
1250
|
+
}
|
|
1251
|
+
catch {
|
|
1252
|
+
return [];
|
|
1253
|
+
}
|
|
1254
|
+
})();
|
|
1255
|
+
if (acceptedCycles.length > 0) {
|
|
1256
|
+
const participants = new Set();
|
|
1257
|
+
for (const c of acceptedCycles)
|
|
1258
|
+
for (const p of c.path)
|
|
1259
|
+
participants.add(p);
|
|
1260
|
+
setFinalizeError(`Cannot finalize — accepted set still contains a cycle (${[...participants].sort().join(', ')}). Reject a cycle member to break it.`);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
414
1263
|
setShowFinalize(true);
|
|
415
1264
|
return;
|
|
416
1265
|
}
|
|
1266
|
+
if (input === 'L') {
|
|
1267
|
+
const currentKey = cursorRowIdx >= 0 && cursorRowIdx < visibleRowsMemo.length
|
|
1268
|
+
? (components[visibleRowsMemo[cursorRowIdx]?.itemIdx ?? -1]?.key ?? null)
|
|
1269
|
+
: null;
|
|
1270
|
+
const nextView = columnOneView === 'grouped' ? 'flat' : 'grouped';
|
|
1271
|
+
const nextRows = buildVisibleRows({
|
|
1272
|
+
items: groupedItemsMemo,
|
|
1273
|
+
cycleParticipants: cycleView.structural,
|
|
1274
|
+
expandedGroups,
|
|
1275
|
+
viewMode: nextView,
|
|
1276
|
+
graph: sidebarGraph,
|
|
1277
|
+
});
|
|
1278
|
+
let nextCursor = 0;
|
|
1279
|
+
if (currentKey) {
|
|
1280
|
+
for (let i = 0; i < nextRows.length; i++) {
|
|
1281
|
+
const r = nextRows[i];
|
|
1282
|
+
if (r.itemIdx < 0)
|
|
1283
|
+
continue;
|
|
1284
|
+
if (components[r.itemIdx]?.key === currentKey) {
|
|
1285
|
+
nextCursor = i;
|
|
1286
|
+
break;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
const nextScroll = nextCursor < sidebarScrollOffset
|
|
1291
|
+
? nextCursor
|
|
1292
|
+
: nextCursor >= sidebarScrollOffset + visibleCount
|
|
1293
|
+
? nextCursor - visibleCount + 1
|
|
1294
|
+
: sidebarScrollOffset;
|
|
1295
|
+
setColumnOneView(nextView);
|
|
1296
|
+
setNav({ cursorRowIdx: nextCursor, sidebarScrollOffset: nextScroll });
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
417
1299
|
if (input === 'a') {
|
|
418
|
-
|
|
1300
|
+
const current = components[selectedIdx];
|
|
1301
|
+
if (!current)
|
|
1302
|
+
return;
|
|
1303
|
+
const cascade = computeAcceptCascade(current.key, componentGraph);
|
|
1304
|
+
const next = components.map((c) => cascade.has(c.key) ? { ...c, status: 'accepted' } : c);
|
|
1305
|
+
setComponents(next);
|
|
419
1306
|
setFinalizeError(null);
|
|
1307
|
+
recomputeCycles(next);
|
|
1308
|
+
pushHistorySnapshot(next, autoRejected, undoSnapshot, 'accept-cascade');
|
|
420
1309
|
return;
|
|
421
1310
|
}
|
|
422
1311
|
if (input === 'r') {
|
|
423
|
-
|
|
1312
|
+
const current = components[selectedIdx];
|
|
1313
|
+
if (!current)
|
|
1314
|
+
return;
|
|
1315
|
+
handleRejectComponent(current.key);
|
|
424
1316
|
return;
|
|
425
1317
|
}
|
|
426
1318
|
if (input === 'A') {
|
|
427
|
-
|
|
1319
|
+
const next = components.map((c) => c.status === 'needs-review' ? { ...c, status: 'accepted' } : c);
|
|
1320
|
+
setComponents(next);
|
|
428
1321
|
setFinalizeError(null);
|
|
1322
|
+
pushHistorySnapshot(next, autoRejected, undoSnapshot, 'bulk-accept');
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (input === 'E') {
|
|
1326
|
+
const roots = new Set();
|
|
1327
|
+
for (const [name, closure] of closures.entries()) {
|
|
1328
|
+
if (closure.nodes.length > 1)
|
|
1329
|
+
roots.add(name);
|
|
1330
|
+
}
|
|
1331
|
+
for (const name of cycleView.structural)
|
|
1332
|
+
roots.add(name);
|
|
1333
|
+
setExpandedGroups(roots);
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
if (input === 'C') {
|
|
1337
|
+
setExpandedGroups(new Set());
|
|
429
1338
|
return;
|
|
430
1339
|
}
|
|
431
1340
|
if (input === 'J') {
|
|
432
|
-
// Toggle read-only JSON view.
|
|
433
1341
|
setShowJson((prev) => !prev);
|
|
434
1342
|
setJsonScrollOffset(0);
|
|
435
1343
|
pendingGRef.current = false;
|
|
436
1344
|
return;
|
|
437
1345
|
}
|
|
1346
|
+
if (key.return) {
|
|
1347
|
+
const current = components[selectedIdx];
|
|
1348
|
+
if (!current)
|
|
1349
|
+
return;
|
|
1350
|
+
const rs = renderStatusByKey.get(current.key);
|
|
1351
|
+
if (!rs || rs.isOwn)
|
|
1352
|
+
return;
|
|
1353
|
+
for (const closure of closures.values()) {
|
|
1354
|
+
if (!closure.nodes.some((n) => n.name === current.key))
|
|
1355
|
+
continue;
|
|
1356
|
+
const target = pickDrillTarget(current.key, closure, directIssues);
|
|
1357
|
+
if (target && target !== current.key) {
|
|
1358
|
+
jumpCursorToName(target);
|
|
1359
|
+
}
|
|
1360
|
+
break;
|
|
1361
|
+
}
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
438
1364
|
if (key.upArrow || input === 'k') {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
setSidebarScrollOffset((off) => Math.min(off, newIdx));
|
|
451
|
-
return newIdx;
|
|
1365
|
+
setNav(({ cursorRowIdx: prev, sidebarScrollOffset: off }) => {
|
|
1366
|
+
const positions = selectableRowPositions;
|
|
1367
|
+
if (positions.length === 0)
|
|
1368
|
+
return { cursorRowIdx: prev, sidebarScrollOffset: off };
|
|
1369
|
+
const pos = positions.indexOf(prev);
|
|
1370
|
+
const currentSelectableIdx = pos >= 0
|
|
1371
|
+
? pos
|
|
1372
|
+
: Math.max(0, positions.reduce((acc, p, i) => (p <= prev ? i : acc), 0));
|
|
1373
|
+
const nextSelectableIdx = Math.max(0, currentSelectableIdx - 1);
|
|
1374
|
+
const newRow = positions[nextSelectableIdx] ?? prev;
|
|
1375
|
+
return { cursorRowIdx: newRow, sidebarScrollOffset: Math.min(off, newRow) };
|
|
452
1376
|
});
|
|
453
1377
|
setJsonScrollOffset(0);
|
|
454
1378
|
setDraftValue('');
|
|
455
1379
|
setSaveError(null);
|
|
1380
|
+
setPendingEditorFocus(null);
|
|
456
1381
|
}
|
|
457
1382
|
else if (key.downArrow || input === 'j') {
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
1383
|
+
setNav(({ cursorRowIdx: prev, sidebarScrollOffset: off }) => {
|
|
1384
|
+
const positions = selectableRowPositions;
|
|
1385
|
+
if (positions.length === 0)
|
|
1386
|
+
return { cursorRowIdx: prev, sidebarScrollOffset: off };
|
|
1387
|
+
const pos = positions.indexOf(prev);
|
|
1388
|
+
const currentSelectableIdx = pos >= 0
|
|
1389
|
+
? pos
|
|
1390
|
+
: Math.max(0, positions.reduce((acc, p, i) => (p <= prev ? i : acc), 0));
|
|
1391
|
+
const nextSelectableIdx = Math.min(positions.length - 1, currentSelectableIdx + 1);
|
|
1392
|
+
const newRow = positions[nextSelectableIdx] ?? prev;
|
|
1393
|
+
const nextOff = newRow >= off + visibleCount ? newRow - visibleCount + 1 : off;
|
|
1394
|
+
return { cursorRowIdx: newRow, sidebarScrollOffset: nextOff };
|
|
462
1395
|
});
|
|
463
1396
|
setJsonScrollOffset(0);
|
|
464
1397
|
setDraftValue('');
|
|
465
1398
|
setSaveError(null);
|
|
1399
|
+
setPendingEditorFocus(null);
|
|
466
1400
|
}
|
|
467
1401
|
});
|
|
468
1402
|
if (loading) {
|
|
469
1403
|
return (_jsx(Box, { paddingX: 2, paddingY: 1, children: _jsx(Text, { dimColor: true, children: "Loading generated definitions..." }) }));
|
|
470
1404
|
}
|
|
471
1405
|
if (loadError) {
|
|
472
|
-
return (
|
|
1406
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: PALETTE.error, children: loadError }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "[q / Enter / Esc] Quit" })] }));
|
|
1407
|
+
}
|
|
1408
|
+
if (showHelp) {
|
|
1409
|
+
return _jsx(HelpOverlay, { sections: HELP_SECTIONS, onClose: () => setShowHelp(false) });
|
|
1410
|
+
}
|
|
1411
|
+
const renderBreakOverlay = (width) => {
|
|
1412
|
+
const highlightedCycle = slotCycles[cyclesCursor];
|
|
1413
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.warning, paddingX: 1, width: width, children: [_jsx(Text, { bold: true, color: PALETTE.warning, children: `BREAK CYCLE ${cyclesCursor + 1} — remove a slot edge or reject a member` }), highlightedCycle &&
|
|
1414
|
+
(() => {
|
|
1415
|
+
const segs = formatCyclePathSegments(highlightedCycle);
|
|
1416
|
+
return (_jsxs(Text, { children: [' ', segs.map((seg, si) => seg.kind === 'slot' ? (_jsx(Text, { color: PALETTE.info, children: seg.text }, si)) : seg.kind === 'arrow' ? (_jsx(Text, { dimColor: true, children: seg.text }, si)) : (_jsx(Text, { color: PALETTE.warning, children: seg.text }, si)))] }));
|
|
1417
|
+
})(), _jsx(Text, { dimColor: true, children: highlightedCycle
|
|
1418
|
+
? 'Deleting an edge removes it from $allowedComponents (undo with Ctrl+Z).'
|
|
1419
|
+
: 'No cycle highlighted.' }), _jsx(Text, { children: " " }), breakEdges.length > 0 && _jsx(Text, { dimColor: true, children: 'remove slot edge:' }), breakEdges.map((edge, idx) => {
|
|
1420
|
+
const isCursor = idx === breakCursor;
|
|
1421
|
+
return (_jsx(Text, { inverse: isCursor, children: `${isCursor ? '▶' : ' '} remove '${edge.toComponent}' from ${edge.fromComponent}.$slots.${edge.slotName}.$allowedComponents` }, `break-${idx}`));
|
|
1422
|
+
}), breakConfirm ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: PALETTE.warning, children: 'Delete this slot edge? [y] confirm [n] cancel' })] })) : (_jsx(Text, { dimColor: true, children: '[↑↓/j/k] move [Enter] delete [x/Esc] close' }))] }));
|
|
1423
|
+
};
|
|
1424
|
+
const breakOverlayFullScreen = breakPanel.isOpen &&
|
|
1425
|
+
shouldBreakOverlayGoFullScreen({
|
|
1426
|
+
rows: stdout?.rows ?? FALLBACK_ROWS,
|
|
1427
|
+
edgeCount: breakEdges.length,
|
|
1428
|
+
});
|
|
1429
|
+
if (breakOverlayFullScreen) {
|
|
1430
|
+
return renderBreakOverlay();
|
|
473
1431
|
}
|
|
474
1432
|
const selected = components[selectedIdx] ?? null;
|
|
475
1433
|
const selectedJson = selected ? JSON.stringify({ [selected.key]: selected.entry }, null, 2) : '';
|
|
476
|
-
|
|
477
|
-
// it can't be pushed to Contentful (no fields). Surface it in the sidebar via
|
|
478
|
-
// the existing warning-color path (yellow) and a "(empty)" suffix so the user
|
|
479
|
-
// can see what went wrong. They can manually add props in FieldEditor or
|
|
480
|
-
// explicitly reject the component.
|
|
481
|
-
const isEmpty = (c) => Object.keys(c.entry.$properties).length === 0;
|
|
1434
|
+
const isEmpty = (c) => Object.keys(c.entry.$properties).length === 0 && Object.keys(c.entry.$slots ?? {}).length === 0;
|
|
482
1435
|
const emptyCount = components.filter(isEmpty).length;
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const sidebarWidth = Math.min(Math.max(longestName +
|
|
1436
|
+
const cycleParticipantSet = cycleView.structural;
|
|
1437
|
+
const sidebarSuffix = (c) => {
|
|
1438
|
+
if (cycleParticipantSet.has(c.key))
|
|
1439
|
+
return ' (cycle)';
|
|
1440
|
+
if (isEmpty(c))
|
|
1441
|
+
return ' (empty)';
|
|
1442
|
+
return '';
|
|
1443
|
+
};
|
|
1444
|
+
const groupedItems = groupedItemsMemo;
|
|
1445
|
+
const previewAnnotationByKey = previewAnnotations;
|
|
1446
|
+
const longestName = components.reduce((m, c) => {
|
|
1447
|
+
const suffixLen = sidebarSuffix(c).length;
|
|
1448
|
+
const groupOverhead = 12; // "▸ (99 deps) ✗"
|
|
1449
|
+
return Math.max(m, c.key.length + Math.max(suffixLen, groupOverhead));
|
|
1450
|
+
}, 0);
|
|
1451
|
+
const sidebarWidthCap = computeSidebarWidth(terminalWidth);
|
|
1452
|
+
const sidebarWidth = Math.min(Math.max(longestName + 9, 18), sidebarWidthCap);
|
|
500
1453
|
const panelWidth = Math.max(10, terminalWidth - sidebarWidth - 4);
|
|
1454
|
+
const projectSlotGraph = components.map((c) => ({
|
|
1455
|
+
name: c.key,
|
|
1456
|
+
slots: Object.entries(c.entry.$slots ?? {}).map(([slotName, slotDef]) => ({
|
|
1457
|
+
name: slotName,
|
|
1458
|
+
allowedComponents: Array.isArray(slotDef?.$allowedComponents)
|
|
1459
|
+
? slotDef.$allowedComponents.filter((v) => typeof v === 'string')
|
|
1460
|
+
: [],
|
|
1461
|
+
})),
|
|
1462
|
+
}));
|
|
1463
|
+
const hasGroupRoots = (() => {
|
|
1464
|
+
for (const c of closures.values())
|
|
1465
|
+
if (c.nodes.length > 1)
|
|
1466
|
+
return true;
|
|
1467
|
+
return false;
|
|
1468
|
+
})();
|
|
501
1469
|
const accepted = components.filter((c) => c.status === 'accepted').length;
|
|
502
1470
|
const rejected = components.filter((c) => c.status === 'rejected').length;
|
|
503
1471
|
const needsReview = components.filter((c) => c.status === 'needs-review').length;
|
|
504
1472
|
const propCount = selected ? Object.keys(selected.entry.$properties).length : 0;
|
|
505
1473
|
const slotCount = selected?.entry.$slots ? Object.keys(selected.entry.$slots).length : 0;
|
|
506
|
-
return (_jsxs(Box, { flexDirection: "column", children: [showFinalize && (_jsx(FinalizeDialog, { accepted: accepted, rejected: rejected, needsReview: needsReview, onConfirm: handleFinalizeConfirm, onCancel: () => setShowFinalize(false) })), showQuit && _jsx(QuitDialog, { hasUnsavedDrafts: false, onConfirm: onQuit, onCancel: () => setShowQuit(false) }),
|
|
1474
|
+
return (_jsxs(Box, { flexDirection: "column", children: [showFinalize && (_jsx(FinalizeDialog, { accepted: accepted, rejected: rejected, needsReview: needsReview, removed: finalizePreview.removed, previewStatus: finalizePreview.status, removedScrollOffset: finalizePreview.scrollOffset, onConfirm: handleFinalizeConfirm, onCancel: () => setShowFinalize(false) })), showQuit && _jsx(QuitDialog, { hasUnsavedDrafts: false, onConfirm: onQuit, onCancel: () => setShowQuit(false) }), showUnsavedWarning && !dialogOpen && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.warning, paddingX: 1, children: [_jsx(Text, { bold: true, color: PALETTE.warning, children: "Unsaved changes" }), _jsx(Text, { children: "You have unsaved edits in the current field editor." }), _jsx(Text, { children: " " }), _jsx(Text, { children: ' [Enter] Save and continue' }), _jsx(Text, { children: ' [Esc] Discard changes and continue' }), _jsx(Text, { children: ' [Tab] Stay in the panel' })] })), showReloadDialog && !dialogOpen && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.warning, paddingX: 1, children: [_jsx(Text, { bold: true, color: PALETTE.warning, children: "Reload from saved state?" }), _jsx(Text, { children: "Unsaved in-memory changes will be lost." }), _jsx(Text, { children: " " }), _jsx(Text, { children: ' [Enter] Confirm' }), _jsx(Text, { children: ' [Esc] Cancel' })] })), removedComponents.length > 0 && !dialogOpen && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.error, paddingX: 1, children: [_jsx(Text, { bold: true, color: PALETTE.error, children: removedComponentsHeader(removedComponents.length, true) }), !removedBannerCollapsed && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), removedComponents.map((rc) => (_jsx(Text, { children: removedComponentLine(rc) }, rc.id)))] }))] })), breakingChanges.length > 0 && !dialogOpen && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: PALETTE.warning, children: `[b] ${breakingChanges.length} breaking change${breakingChanges.length === 1 ? '' : 's'}` }) })), breakingPanel.isOpen &&
|
|
1475
|
+
breakingDetailOpen &&
|
|
1476
|
+
!dialogOpen &&
|
|
1477
|
+
(() => {
|
|
1478
|
+
const row = breakingRows[breakingCursor];
|
|
1479
|
+
const comp = row ? breakingChanges.find((b) => b.componentName === row.componentName) : undefined;
|
|
1480
|
+
if (!comp)
|
|
1481
|
+
return null;
|
|
1482
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.warning, paddingX: 1, children: [_jsx(Text, { bold: true, color: PALETTE.warning, children: `Breaking changes — ${comp.componentName}` }), comp.impact && (_jsx(Text, { dimColor: true, children: ` affects ${comp.impact.affectedExperiences} experience${comp.impact.affectedExperiences === 1 ? '' : 's'}, ${comp.impact.affectedFragments} fragment${comp.impact.affectedFragments === 1 ? '' : 's'}` })), comp.changes.length === 0 ? (_jsx(Text, { dimColor: true, children: ' (no enumerated changes)' })) : (comp.changes.map((change, ci) => (_jsx(Text, { children: ` • ${formatBreakingChange(change, comp.current)}` }, `bd-detail-${ci}`)))), _jsx(Text, { dimColor: true, children: '[D/Esc] close detail' })] }));
|
|
1483
|
+
})(), cyclePanel.isOpen &&
|
|
1484
|
+
!dialogOpen &&
|
|
1485
|
+
(() => {
|
|
1486
|
+
const PANEL_H = 20;
|
|
1487
|
+
const lines = [];
|
|
1488
|
+
lines.push(_jsx(Text, { bold: true, color: PALETTE.warning, children: `SLOT DEPENDENCY CYCLES (${slotCycles.length})` }, "cyc-title"));
|
|
1489
|
+
lines.push(_jsx(Text, { dimColor: true, children: 'push will fail until these are resolved' }, "cyc-sub"));
|
|
1490
|
+
lines.push(_jsx(Text, { dimColor: true, children: 'To fix: reject a cycle member, or break the cycle by removing a slot edge.' }, "cyc-guide"));
|
|
1491
|
+
lines.push(_jsx(Text, { children: " " }, "cyc-space"));
|
|
1492
|
+
slotCycles.forEach((cycle, idx) => {
|
|
1493
|
+
const nodeCount = new Set(cycle.path).size;
|
|
1494
|
+
const isCursor = idx === cyclesCursor;
|
|
1495
|
+
lines.push(_jsx(Text, { bold: true, inverse: isCursor, children: `${isCursor ? '▶' : ' '} Cycle ${idx + 1} (${nodeCount} component${nodeCount === 1 ? '' : 's'}):` }, `cyc-h-${idx}`));
|
|
1496
|
+
const segs = formatCyclePathSegments(cycle, 16);
|
|
1497
|
+
lines.push(_jsxs(Text, { children: [' ', segs.map((seg, si) => seg.kind === 'slot' ? (_jsx(Text, { color: PALETTE.info, children: seg.text }, si)) : seg.kind === 'arrow' ? (_jsx(Text, { dimColor: true, children: seg.text }, si)) : (_jsx(Text, { children: seg.text }, si)))] }, `cyc-p-${idx}`));
|
|
1498
|
+
if (cycle.suggestedBreak) {
|
|
1499
|
+
const b = cycle.suggestedBreak;
|
|
1500
|
+
lines.push(_jsx(Text, { dimColor: true, children: ` Suggested fix: remove '${b.toComponent}' from ${b.fromComponent}.$slots.${b.slotName}.$allowedComponents` }, `cyc-f-${idx}`));
|
|
1501
|
+
}
|
|
1502
|
+
lines.push(_jsx(Text, { children: " " }, `cyc-s-${idx}`));
|
|
1503
|
+
});
|
|
1504
|
+
const visible = lines.slice(cyclePanelScroll, cyclePanelScroll + PANEL_H);
|
|
1505
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.warning, paddingX: 1, children: [visible, _jsx(Text, { dimColor: true, children: '[↑↓/j/k] move [Enter] jump [x] break cycle [c/q/Esc] close' })] }));
|
|
1506
|
+
})(), !dialogOpen &&
|
|
507
1507
|
livePreview &&
|
|
508
1508
|
(() => {
|
|
509
|
-
// Pilot-2026-06-23 R2: at-a-glance diff summary at the top of the
|
|
510
|
-
// step. Mutually exclusive states:
|
|
511
|
-
// - hook running (and we don't yet have annotations) → spinner.
|
|
512
|
-
// - hook disabled (creds rejected) → static disabled hint.
|
|
513
|
-
// - annotations populated → counts.
|
|
514
|
-
// - idle, no annotations, not disabled → render nothing.
|
|
515
1509
|
const counts = { new: 0, changed: 0, removed: 0, breaking: 0 };
|
|
516
1510
|
for (const v of previewAnnotations.values()) {
|
|
517
1511
|
counts[v] = (counts[v] ?? 0) + 1;
|
|
@@ -525,14 +1519,43 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
525
1519
|
}
|
|
526
1520
|
if (!hasCounts)
|
|
527
1521
|
return null;
|
|
528
|
-
return (_jsxs(Box, { children: [_jsx(Text, { children: 'Preview: ' }), _jsx(Text, { color:
|
|
529
|
-
})(), !dialogOpen &&
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
1522
|
+
return (_jsxs(Box, { children: [_jsx(Text, { children: 'Preview: ' }), _jsx(Text, { color: PALETTE.success, children: `${counts.new} new` }), _jsx(Text, { children: ' · ' }), _jsx(Text, { color: PALETTE.warning, children: `${counts.changed} changed` }), _jsx(Text, { children: ' · ' }), _jsx(Text, { dimColor: true, children: `${counts.removed} removed` }), _jsx(Text, { children: ' · ' }), _jsx(Text, { color: PALETTE.error, bold: true, children: `${counts.breaking} breaking` })] }));
|
|
1523
|
+
})(), !dialogOpen &&
|
|
1524
|
+
autoRejected.length > 0 &&
|
|
1525
|
+
(() => {
|
|
1526
|
+
const stillRejected = autoRejected.filter((name) => {
|
|
1527
|
+
const c = components.find((x) => x.key === name);
|
|
1528
|
+
return c?.status === 'rejected';
|
|
1529
|
+
});
|
|
1530
|
+
if (stillRejected.length === 0)
|
|
1531
|
+
return null;
|
|
1532
|
+
const participantSet = cycleView.structural;
|
|
1533
|
+
const members = stillRejected.filter((n) => participantSet.has(n)).sort();
|
|
1534
|
+
const ancestors = stillRejected.filter((n) => !participantSet.has(n)).sort();
|
|
1535
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: PALETTE.error, paddingX: 1, children: [_jsx(Text, { color: PALETTE.error, bold: true, children: `Cyclic manifest — auto-rejected ${stillRejected.length} component${stillRejected.length === 1 ? '' : 's'}:` }), members.length > 0 && _jsx(Text, { color: PALETTE.error, children: ` Cycle members: ${members.join(', ')}` }), ancestors.length > 0 && _jsx(Text, { color: PALETTE.error, children: ` Ancestors: ${ancestors.join(', ')}` }), _jsx(Text, { dimColor: true, children: undoSnapshot
|
|
1536
|
+
? ' [Ctrl+Z] undo · [r]/[a] manually toggle · [F] continue'
|
|
1537
|
+
: ' [r]/[a] manually toggle · [F] continue' })] }));
|
|
1538
|
+
})(), !dialogOpen && emptyCount > 0 && (_jsx(Text, { color: PALETTE.warning, children: `⚠ ${emptyCount} component${emptyCount === 1 ? '' : 's'} had no classifiable props — review with care` })), !dialogOpen && finalizeError && _jsx(Text, { color: PALETTE.error, children: `⚠ ${finalizeError}` }), !dialogOpen && (_jsxs(Box, { children: [breakingPanel.isOpen ? (_jsx(GotoBanner, { title: "Breaking changes", rows: breakingRows.map((r) => ({
|
|
1539
|
+
label: r.label,
|
|
1540
|
+
jumpTarget: r.componentName,
|
|
1541
|
+
})), cursor: breakingCursor, maxRows: panelMaxRows, width: sidebarWidth, footerHint: "[\u2191/\u2193] move \u00B7 [Enter] jump \u00B7 [D] detail \u00B7 [Esc] close" })) : lineagePanel.isOpen && focusedComponentKey ? (_jsx(LineagePanel, { focusedComponentKey: focusedComponentKey, entries: lineageEntries, cursor: lineageCursor, jumpables: lineageJumpables, maxRows: panelMaxRows, width: sidebarWidth })) : (_jsx(GroupedSidebar, { items: groupedItems, cycleParticipants: cycleParticipantSet, selectedIdx: selectedIdx, selectedRowIdx: cursorRowIdx, onSelect: (idx) => {
|
|
1542
|
+
for (let i = 0; i < visibleRowsMemo.length; i++) {
|
|
1543
|
+
if (visibleRowsMemo[i].itemIdx === idx) {
|
|
1544
|
+
jumpCursorToRow(i);
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
534
1547
|
}
|
|
535
|
-
|
|
1548
|
+
setJsonScrollOffset(0);
|
|
1549
|
+
}, expandedGroups: expandedGroups, onToggleExpanded: (rootName) => {
|
|
1550
|
+
setExpandedGroups((prev) => {
|
|
1551
|
+
const next = new Set(prev);
|
|
1552
|
+
if (next.has(rootName))
|
|
1553
|
+
next.delete(rootName);
|
|
1554
|
+
else
|
|
1555
|
+
next.add(rootName);
|
|
1556
|
+
return next;
|
|
1557
|
+
});
|
|
1558
|
+
}, width: sidebarWidth, focused: sidebarFocused, renderStatusByKey: renderStatusByKey, previewAnnotationByKey: previewAnnotationByKey, selectionStateByKey: selectionStateByKey, scrollOffset: sidebarScrollOffset, visibleCount: visibleCount, dimPredicate: dimPredicate, visibleRows: visibleRowsMemo, viewMode: columnOneView, graph: sidebarGraph })), _jsx(Box, { flexGrow: 1, paddingLeft: 1, flexDirection: "column", children: selected ? (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: selected.key }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { dimColor: true, children: [propCount, " prop", propCount !== 1 ? 's' : '', slotCount > 0 ? ` · ${slotCount} slot${slotCount !== 1 ? 's' : ''}` : '', ' ', sidebarFocused ? '[Tab] focus panel' : '[Tab] focus list'] })] }), panelOpen === 'prop-rationale' ? ((() => {
|
|
536
1559
|
const rows = [
|
|
537
1560
|
...(componentRationale?.props ?? []).map((p) => ({
|
|
538
1561
|
name: p.name,
|
|
@@ -569,21 +1592,27 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
|
|
|
569
1592
|
: undefined, onTogglePropRationale: () => {
|
|
570
1593
|
setPanelOpen('prop-rationale');
|
|
571
1594
|
setPanelScrollOffset(() => 0);
|
|
572
|
-
}, onToggleComponentRationale: () => {
|
|
1595
|
+
}, propRationaleKey: "p", componentRationaleKey: "P", onToggleComponentRationale: () => {
|
|
573
1596
|
setPanelOpen('component-rationale');
|
|
574
1597
|
setPanelScrollOffset(() => 0);
|
|
575
1598
|
}, onToggleSourceExternal: () => {
|
|
576
1599
|
setPanelOpen('source');
|
|
577
1600
|
setPanelScrollOffset(() => 0);
|
|
578
|
-
}, onTextEntryActiveChange: setTextEntryActive
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
1601
|
+
}, onTextEntryActiveChange: setTextEntryActive, projectSlotGraph: projectSlotGraph, currentComponentName: selected.key, onDirtyChange: setEditorDirty, discardTrigger: discardTrigger, initialFocusTarget: pendingEditorFocus && pendingEditorFocus.componentName === selected.key
|
|
1602
|
+
? pendingEditorFocus.target
|
|
1603
|
+
: undefined }, pendingEditorFocus && pendingEditorFocus.componentName === selected.key
|
|
1604
|
+
? `${selected.key}::${pendingEditorFocus.target.kind}:${pendingEditorFocus.target.name}`
|
|
1605
|
+
: selected.key)), saveError && _jsx(Text, { color: PALETTE.error, children: '✗ ' + saveError }), _jsxs(Text, { dimColor: true, children: [sidebarFocused
|
|
1606
|
+
? hasGroupRoots
|
|
1607
|
+
? ' [Space] expand/collapse group [E/C] expand/collapse all'
|
|
1608
|
+
: ''
|
|
584
1609
|
: showJson
|
|
585
1610
|
? ' [j/k] scroll [Ctrl+u/d] half-page [gg/G] top/bottom [Tab] focus list'
|
|
586
|
-
: ' [Tab] focus list (edit fields)', livePreviewHook.status === 'running' && _jsx(Text, { children: ` ${livePreviewSpinner} live preview` }), livePreviewHook.disabled && _jsx(Text, { children: ' · live preview disabled' })] })] })) : (_jsx(Text, { dimColor: true, children: "No component selected" })) })] })), !dialogOpen && (
|
|
1611
|
+
: ' [Tab] focus list (edit fields)', livePreviewHook.status === 'running' && _jsx(Text, { children: ` ${livePreviewSpinner} live preview` }), livePreviewHook.disabled && _jsx(Text, { children: ' · live preview disabled' })] })] })) : (_jsx(Text, { dimColor: true, children: "No component selected" })) })] })), breakPanel.isOpen && !breakOverlayFullScreen && !dialogOpen && renderBreakOverlay(), !dialogOpen && slotCycles.length > 0 && !cyclePanel.isOpen && !breakPanel.isOpen && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: PALETTE.warning, children: `⚠ ${slotCycles.length} slot dependency cycle${slotCycles.length === 1 ? '' : 's'} detected — push will fail` }), slotCycles.slice(0, 3).map((cycle, idx) => {
|
|
1612
|
+
const segs = formatCyclePathSegments(cycle);
|
|
1613
|
+
return (_jsxs(Text, { color: PALETTE.warning, children: [' Cycle: ', segs.map((seg, si) => seg.kind === 'slot' ? (_jsx(Text, { color: PALETTE.info, children: seg.text }, si)) : seg.kind === 'arrow' ? (_jsx(Text, { dimColor: true, children: seg.text }, si)) : (_jsx(Text, { color: PALETTE.warning, children: seg.text }, si)))] }, `cyc-banner-${idx}`));
|
|
1614
|
+
}), slotCycles.length > 3 && _jsx(Text, { color: PALETTE.warning, children: ` …${slotCycles.length - 3} more` }), _jsx(Text, { dimColor: true, children: ' press [c] for detail' })] })), !dialogOpen && searchOpen && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [`/${searchQuery}`, _jsx(Text, { color: PALETTE.info, children: '▎' }), searchQuery && _jsx(Text, { dimColor: true, children: ` (${searchMatchCount}/${components.length} matches)` })] }), autocompleteCandidates.length > 1 && (_jsx(Text, { dimColor: true, children: ` possibilities: ${autocompleteCandidates.join(' · ').slice(0, 120)}` }))] })), !dialogOpen && !searchOpen && searchQuery && (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: `/${searchQuery} (${searchMatchCount}/${components.length} matches) · [Esc] clear` }) })), !dialogOpen && sidebarFocused && (_jsxs(Box, { columnGap: 2, flexWrap: "wrap", children: [legendEntry('[j/k]', 'move'), legendEntry('[a]', 'accept'), legendEntry('[r]', 'reject'), legendEntry('[A]', 'accept all'), legendEntry('[F]', 'finalize'), legendEntry('[L]', 'flat', columnOneView === 'flat'), legendEntry('[l]', 'lineage', lineagePanel.isOpen), legendEntry('[i]', 'focus lineage', jumpFilterTarget !== null), legendEntry('[w]', 'only breaking', activeFilters.has('broken')), slotCycles.length > 0 && legendEntry('[o]', 'only cycles', activeFilters.has('cycles')), slotCycles.length > 0 && legendEntry('[c]', 'cycle list', cyclePanel.isOpen), legendEntry('[p]', 'prop rationale', panelOpen === 'prop-rationale'), legendEntry('[P]', 'component rationale', panelOpen === 'component-rationale'), legendEntry('[s]', 'source', panelOpen === 'source'), legendEntry('[J]', showJson ? 'hide JSON' : 'show JSON', showJson), breakingChanges.length > 0 && legendEntry('[b]', 'see breaking changes', breakingPanel.isOpen), removedComponents.length > 0 &&
|
|
1615
|
+
legendEntry('[d]', removedBannerCollapsed ? 'show removed' : 'hide removed', !removedBannerCollapsed), legendEntry('[/]', 'search', searchOpen || searchQuery.length > 0), legendEntry('[Tab]', 'focus panel'), legendEntry('[Ctrl+Z]', 'undo'), legendEntry('[Ctrl+Y]', 'redo'), legendEntry('[Ctrl+R]', 'reload'), legendEntry('[?]', 'help'), legendEntry('[q]', 'quit')] })), !dialogOpen && (_jsx(StatusBar, { accepted: accepted, rejected: rejected, reviewed: 0, needsReview: needsReview, onApproveAll: () => {
|
|
587
1616
|
setComponents((prev) => prev.map((c) => (c.status === 'needs-review' ? { ...c, status: 'accepted' } : c)));
|
|
588
1617
|
}, onFinalize: () => setShowFinalize(true) }))] }));
|
|
589
1618
|
}
|