@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.
Files changed (193) hide show
  1. package/README.md +64 -2
  2. package/dist/package.json +3 -2
  3. package/dist/src/analyze/command.d.ts +11 -0
  4. package/dist/src/analyze/command.js +298 -3
  5. package/dist/src/analyze/composite-closure.d.ts +25 -0
  6. package/dist/src/analyze/composite-closure.js +0 -0
  7. package/dist/src/analyze/composition/agent-parser/author-prompt.d.ts +4 -0
  8. package/dist/src/analyze/composition/agent-parser/author-prompt.js +22 -0
  9. package/dist/src/analyze/composition/agent-parser/extract-parser.d.ts +8 -0
  10. package/dist/src/analyze/composition/agent-parser/extract-parser.js +44 -0
  11. package/dist/src/analyze/composition/agent-parser/load-prompt.d.ts +7 -0
  12. package/dist/src/analyze/composition/agent-parser/load-prompt.js +31 -0
  13. package/dist/src/analyze/composition/agent-parser/resolve-via-parser.d.ts +46 -0
  14. package/dist/src/analyze/composition/agent-parser/resolve-via-parser.js +88 -0
  15. package/dist/src/analyze/composition/agent-parser/sandbox.d.ts +45 -0
  16. package/dist/src/analyze/composition/agent-parser/sandbox.js +187 -0
  17. package/dist/src/analyze/composition/apply-mapping.d.ts +22 -0
  18. package/dist/src/analyze/composition/apply-mapping.js +67 -0
  19. package/dist/src/analyze/composition/candidate-critic-agent.d.ts +7 -0
  20. package/dist/src/analyze/composition/candidate-critic-agent.js +39 -0
  21. package/dist/src/analyze/composition/candidate-critic.d.ts +32 -0
  22. package/dist/src/analyze/composition/candidate-critic.js +63 -0
  23. package/dist/src/analyze/composition/candidate-files.d.ts +34 -0
  24. package/dist/src/analyze/composition/candidate-files.js +131 -0
  25. package/dist/src/analyze/composition/composition-cache-key.d.ts +19 -0
  26. package/dist/src/analyze/composition/composition-cache-key.js +19 -0
  27. package/dist/src/analyze/composition/interchange-schema.d.ts +37 -0
  28. package/dist/src/analyze/composition/interchange-schema.js +61 -0
  29. package/dist/src/analyze/composition/merge-edges.d.ts +19 -0
  30. package/dist/src/analyze/composition/merge-edges.js +50 -0
  31. package/dist/src/analyze/composition/parse-map-edges.d.ts +16 -0
  32. package/dist/src/analyze/composition/parse-map-edges.js +55 -0
  33. package/dist/src/analyze/composition/resolve-mapping-cli.d.ts +23 -0
  34. package/dist/src/analyze/composition/resolve-mapping-cli.js +29 -0
  35. package/dist/src/analyze/composition/resolve-mapping.d.ts +59 -0
  36. package/dist/src/analyze/composition/resolve-mapping.js +94 -0
  37. package/dist/src/analyze/cycle-detection.d.ts +27 -0
  38. package/dist/src/analyze/cycle-detection.js +234 -0
  39. package/dist/src/analyze/cycle-view.d.ts +7 -0
  40. package/dist/src/analyze/cycle-view.js +28 -0
  41. package/dist/src/analyze/fuzzy-search.d.ts +3 -0
  42. package/dist/src/analyze/fuzzy-search.js +55 -0
  43. package/dist/src/analyze/issue-inheritance.d.ts +12 -0
  44. package/dist/src/analyze/issue-inheritance.js +108 -0
  45. package/dist/src/analyze/lineage.d.ts +29 -0
  46. package/dist/src/analyze/lineage.js +0 -0
  47. package/dist/src/analyze/scope-gate-cascade.d.ts +11 -0
  48. package/dist/src/analyze/scope-gate-cascade.js +170 -0
  49. package/dist/src/analyze/search-neighborhood.d.ts +3 -0
  50. package/dist/src/analyze/search-neighborhood.js +56 -0
  51. package/dist/src/analyze/select/tui/App.js +5 -16
  52. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +0 -4
  53. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +3 -15
  54. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +29 -38
  55. package/dist/src/analyze/select/tui/components/FieldEditor.js +309 -185
  56. package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +13 -1
  57. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +23 -2
  58. package/dist/src/analyze/select/tui/components/GotoBanner.d.ts +31 -0
  59. package/dist/src/analyze/select/tui/components/GotoBanner.js +30 -0
  60. package/dist/src/analyze/select/tui/components/GroupedSidebar.d.ts +91 -0
  61. package/dist/src/analyze/select/tui/components/GroupedSidebar.js +497 -0
  62. package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +13 -1
  63. package/dist/src/analyze/select/tui/components/HelpOverlay.js +9 -3
  64. package/dist/src/analyze/select/tui/components/JsonEditor.js +3 -3
  65. package/dist/src/analyze/select/tui/components/LineagePanel.d.ts +14 -0
  66. package/dist/src/analyze/select/tui/components/LineagePanel.js +22 -0
  67. package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +0 -7
  68. package/dist/src/analyze/select/tui/components/RationalePanel.js +3 -15
  69. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +0 -6
  70. package/dist/src/analyze/select/tui/components/Sidebar.js +11 -27
  71. package/dist/src/analyze/select/tui/components/StatusBar.js +2 -1
  72. package/dist/src/analyze/select/tui/components/removed-components-text.d.ts +7 -0
  73. package/dist/src/analyze/select/tui/components/removed-components-text.js +11 -0
  74. package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +1 -5
  75. package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +8 -10
  76. package/dist/src/analyze/select/tui/theme.d.ts +15 -0
  77. package/dist/src/analyze/select/tui/theme.js +28 -0
  78. package/dist/src/analyze/selection-cascade.d.ts +3 -0
  79. package/dist/src/analyze/selection-cascade.js +19 -0
  80. package/dist/src/analyze/slot-graph.d.ts +11 -0
  81. package/dist/src/analyze/slot-graph.js +17 -0
  82. package/dist/src/apply/api-client.d.ts +2 -1
  83. package/dist/src/apply/api-client.js +40 -6
  84. package/dist/src/apply/command.d.ts +17 -1
  85. package/dist/src/apply/command.js +69 -11
  86. package/dist/src/apply/error-parser.d.ts +16 -0
  87. package/dist/src/apply/error-parser.js +117 -0
  88. package/dist/src/apply/tui/ServerPreviewView.js +3 -1
  89. package/dist/src/credentials-store.d.ts +6 -0
  90. package/dist/src/credentials-store.js +5 -1
  91. package/dist/src/generate/agent-runner.d.ts +6 -0
  92. package/dist/src/generate/agent-runner.js +24 -8
  93. package/dist/src/import/command.js +69 -31
  94. package/dist/src/import/cycle-auto-reject.d.ts +4 -0
  95. package/dist/src/import/cycle-auto-reject.js +14 -0
  96. package/dist/src/import/orchestrator.d.ts +20 -7
  97. package/dist/src/import/orchestrator.js +139 -35
  98. package/dist/src/import/strip-allowed-components.d.ts +18 -0
  99. package/dist/src/import/strip-allowed-components.js +22 -0
  100. package/dist/src/import/tui/CustomPromptBanner.d.ts +0 -5
  101. package/dist/src/import/tui/CustomPromptBanner.js +2 -1
  102. package/dist/src/import/tui/WizardApp.d.ts +10 -74
  103. package/dist/src/import/tui/WizardApp.js +234 -246
  104. package/dist/src/import/tui/ai-flag.d.ts +5 -0
  105. package/dist/src/import/tui/ai-flag.js +6 -0
  106. package/dist/src/import/tui/autocomplete.d.ts +5 -0
  107. package/dist/src/import/tui/autocomplete.js +32 -0
  108. package/dist/src/import/tui/components/AutoFilterBanner.d.ts +10 -0
  109. package/dist/src/import/tui/components/AutoFilterBanner.js +19 -0
  110. package/dist/src/import/tui/components/CounterStrip.d.ts +12 -0
  111. package/dist/src/import/tui/components/CounterStrip.js +12 -0
  112. package/dist/src/import/tui/components/LegendEntry.d.ts +2 -0
  113. package/dist/src/import/tui/components/LegendEntry.js +6 -0
  114. package/dist/src/import/tui/cycle-panel-scroll.d.ts +8 -0
  115. package/dist/src/import/tui/cycle-panel-scroll.js +33 -0
  116. package/dist/src/import/tui/final-review-host.d.ts +3 -7
  117. package/dist/src/import/tui/final-review-host.js +9 -4
  118. package/dist/src/import/tui/group-collapse.d.ts +2 -0
  119. package/dist/src/import/tui/group-collapse.js +11 -0
  120. package/dist/src/import/tui/history.d.ts +22 -0
  121. package/dist/src/import/tui/history.js +53 -0
  122. package/dist/src/import/tui/hooks/useLineage.d.ts +32 -0
  123. package/dist/src/import/tui/hooks/useLineage.js +50 -0
  124. package/dist/src/import/tui/hooks/useOverlayPanel.d.ts +13 -0
  125. package/dist/src/import/tui/hooks/useOverlayPanel.js +27 -0
  126. package/dist/src/import/tui/lineage-layout.d.ts +75 -0
  127. package/dist/src/import/tui/lineage-layout.js +82 -0
  128. package/dist/src/import/tui/runLivePreview.d.ts +11 -0
  129. package/dist/src/import/tui/runLivePreview.js +7 -1
  130. package/dist/src/import/tui/scope-gate-columns.d.ts +31 -0
  131. package/dist/src/import/tui/scope-gate-columns.js +94 -0
  132. package/dist/src/import/tui/scope-gate-host.d.ts +3 -1
  133. package/dist/src/import/tui/scope-gate-host.js +11 -3
  134. package/dist/src/import/tui/sidebar-width.d.ts +1 -0
  135. package/dist/src/import/tui/sidebar-width.js +3 -0
  136. package/dist/src/import/tui/step-filters.d.ts +19 -0
  137. package/dist/src/import/tui/step-filters.js +42 -0
  138. package/dist/src/import/tui/steps/AtomicGenerateReviewStep.d.ts +42 -0
  139. package/dist/src/import/tui/steps/AtomicGenerateReviewStep.js +726 -0
  140. package/dist/src/import/tui/steps/AtomicScopeGateStep.d.ts +3 -0
  141. package/dist/src/import/tui/steps/AtomicScopeGateStep.js +211 -0
  142. package/dist/src/import/tui/steps/CredentialsStep.js +5 -18
  143. package/dist/src/import/tui/steps/DoneStep.d.ts +0 -2
  144. package/dist/src/import/tui/steps/DoneStep.js +3 -2
  145. package/dist/src/import/tui/steps/ErrorStep.js +2 -1
  146. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +19 -25
  147. package/dist/src/import/tui/steps/GenerateReviewStep.js +1258 -229
  148. package/dist/src/import/tui/steps/PathValidationStep.js +3 -2
  149. package/dist/src/import/tui/steps/PreviewStep.js +2 -1
  150. package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +0 -7
  151. package/dist/src/import/tui/steps/PushDecisionGateStep.js +3 -6
  152. package/dist/src/import/tui/steps/PushingStep.js +2 -1
  153. package/dist/src/import/tui/steps/RunningStep.d.ts +4 -1
  154. package/dist/src/import/tui/steps/RunningStep.js +3 -2
  155. package/dist/src/import/tui/steps/ScopeGateStep.d.ts +24 -0
  156. package/dist/src/import/tui/steps/ScopeGateStep.js +850 -193
  157. package/dist/src/import/tui/steps/TokenInputStep.js +2 -1
  158. package/dist/src/import/tui/steps/WelcomeStep.js +2 -1
  159. package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +6 -0
  160. package/dist/src/import/tui/steps/WizardPreviewStep.js +95 -56
  161. package/dist/src/import/tui/steps/auto-reject-decision.d.ts +7 -0
  162. package/dist/src/import/tui/steps/auto-reject-decision.js +9 -0
  163. package/dist/src/import/tui/steps/breaking-change-format.d.ts +2 -0
  164. package/dist/src/import/tui/steps/breaking-change-format.js +24 -0
  165. package/dist/src/import/tui/steps/enumerate-cycle-breaks.d.ts +14 -0
  166. package/dist/src/import/tui/steps/enumerate-cycle-breaks.js +0 -0
  167. package/dist/src/import/tui/steps/preview-diff.js +48 -5
  168. package/dist/src/import/tui/useFinalizePreview.d.ts +34 -0
  169. package/dist/src/import/tui/useFinalizePreview.js +0 -0
  170. package/dist/src/import/tui/useLivePreview.d.ts +3 -0
  171. package/dist/src/import/tui/useLivePreview.js +1 -0
  172. package/dist/src/import/tui/wizard-state-transitions.d.ts +5 -59
  173. package/dist/src/import/tui/wizard-state-transitions.js +5 -60
  174. package/dist/src/lib/composition-mode.d.ts +28 -0
  175. package/dist/src/lib/composition-mode.js +37 -0
  176. package/dist/src/lib/prompt-overrides.d.ts +38 -0
  177. package/dist/src/lib/prompt-overrides.js +69 -0
  178. package/dist/src/print/command.js +19 -1
  179. package/dist/src/runs/modify-launcher.d.ts +3 -0
  180. package/dist/src/runs/modify-launcher.js +2 -0
  181. package/dist/src/runs/replay-helpers.js +1 -0
  182. package/dist/src/runs/run-picker.js +2 -18
  183. package/dist/src/runs/store.d.ts +4 -0
  184. package/dist/src/session/db.d.ts +23 -40
  185. package/dist/src/session/db.js +110 -138
  186. package/dist/src/setup/composition-mode-prompt.d.ts +14 -0
  187. package/dist/src/setup/composition-mode-prompt.js +37 -0
  188. package/package.json +5 -4
  189. package/prompts/composition-dir-critic.md +7 -0
  190. package/prompts/composition-edges.md +8 -0
  191. package/prompts/composition-parser-repair-empty.md +1 -0
  192. package/prompts/composition-parser-repair-error.md +2 -0
  193. package/prompts/composition-parser.md +42 -0
@@ -1,250 +1,907 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import React, { useMemo, useState } from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text, useStdout } from 'ink';
3
+ import { PALETTE } from '../../../analyze/select/tui/theme.js';
4
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
4
5
  import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
5
- const VISIBLE_COUNT = 10;
6
- const REASON_DISPLAY_MAX = 60;
7
- // Pilot-2026-06-25 R2: AI-flagged rows render a subtle cyan `*` glyph before
8
- // the name (in place of the Round-1 `[AI]` text badge). The marker persists
9
- // regardless of whether the operator later toggles the row INCLUDED — manual
10
- // decision wins; the marker is informational only.
11
- const AI_MARKER = '*';
12
- // Indentation prefix for the wrapped reason on the focused AI row. Aligns
13
- // the reason under the row label so the eye reads it as a continuation.
14
- const REASON_WRAP_INDENT = ' ';
15
- function truncateReason(reason) {
16
- if (reason === null || reason === undefined || reason === '')
17
- return '<no reason given>';
18
- if (reason.length <= REASON_DISPLAY_MAX)
6
+ import { GroupedSidebar, buildVisibleRows, } from '../../../analyze/select/tui/components/GroupedSidebar.js';
7
+ import { computeAllClosures } from '../../../analyze/composite-closure.js';
8
+ import { buildComponentGraph } from '../../../analyze/slot-graph.js';
9
+ import { findSlotCycles } from '../../../analyze/cycle-detection.js';
10
+ import { computeAutocomplete } from '../autocomplete.js';
11
+ import { buildFlatDimPredicate, computeFilterKeys, intersectFilterKeys } from '../step-filters.js';
12
+ import { useLineage } from '../hooks/useLineage.js';
13
+ import { useOverlayPanel } from '../hooks/useOverlayPanel.js';
14
+ import { computeSidebarBudget, FALLBACK_ROWS } from '../lineage-layout.js';
15
+ import { LineagePanel } from '../../../analyze/select/tui/components/LineagePanel.js';
16
+ import { GotoBanner } from '../../../analyze/select/tui/components/GotoBanner.js';
17
+ import { HelpOverlay } from '../../../analyze/select/tui/components/HelpOverlay.js';
18
+ import { legendEntry } from '../components/LegendEntry.js';
19
+ import { AutoFilterBanner } from '../components/AutoFilterBanner.js';
20
+ import { CounterStrip } from '../components/CounterStrip.js';
21
+ import { isAiFlagged } from '../ai-flag.js';
22
+ import { resolveGroupRoot } from '../group-collapse.js';
23
+ import { buildCycleUnits, collectReachableCycleUnits, computeCycleAwareAcceptCascade, computeCycleAwareRejectCascade, } from '../../../analyze/scope-gate-cascade.js';
24
+ import { fuzzyMatches } from '../../../analyze/fuzzy-search.js';
25
+ import { computeDirectNeighborhood, findAllAncestors } from '../../../analyze/search-neighborhood.js';
26
+ import { buildAddedComponentsList, buildAddedGroupsList, computeColumnWidths, computeCounters, } from '../scope-gate-columns.js';
27
+ const FOCUSED_REASON_MAX_LINES = 4;
28
+ const HELP_SECTIONS = [
29
+ {
30
+ title: 'Navigation',
31
+ entries: [
32
+ { keys: 'j / k / ↑ / ↓', label: 'Move cursor' },
33
+ { keys: 'Tab / Shift-Tab', label: 'Switch column' },
34
+ { keys: 'Enter', label: 'Jump to main' },
35
+ ],
36
+ },
37
+ {
38
+ title: 'Selection',
39
+ entries: [
40
+ { keys: 'a', label: 'Accept' },
41
+ { keys: 'r', label: 'Reject' },
42
+ { keys: 'A', label: 'Toggle all' },
43
+ { keys: 'Y', label: 'Accept non-flagged' },
44
+ ],
45
+ },
46
+ {
47
+ title: 'Sidebar views',
48
+ entries: [
49
+ { keys: 'L', label: 'Flat view' },
50
+ { keys: 'l', label: 'Lineage' },
51
+ { keys: 'i', label: 'Focus lineage' },
52
+ { keys: 'o', label: 'Only cycles' },
53
+ { keys: 'space', label: 'Expand/collapse group' },
54
+ { keys: 'E / C', label: 'Expand/collapse all' },
55
+ ],
56
+ },
57
+ {
58
+ title: 'Panels',
59
+ entries: [
60
+ { keys: 'c', label: 'Cycle list' },
61
+ { keys: 'x', label: 'AI exclusions' },
62
+ ],
63
+ },
64
+ {
65
+ title: 'Search',
66
+ entries: [{ keys: '/', label: 'Search' }],
67
+ },
68
+ {
69
+ title: 'General',
70
+ entries: [
71
+ { keys: 'f', label: 'Continue' },
72
+ { keys: '?', label: 'Close help' },
73
+ { keys: 'q', label: 'Quit' },
74
+ ],
75
+ },
76
+ ];
77
+ function capReasonForFocusedRow(reason, width) {
78
+ const safeWidth = Math.max(20, width);
79
+ const budget = safeWidth * FOCUSED_REASON_MAX_LINES;
80
+ if (reason.length <= budget)
19
81
  return reason;
20
- return reason.slice(0, REASON_DISPLAY_MAX - 1).trimEnd() + '…';
82
+ return reason.slice(0, budget - 1).trimEnd() + '…';
21
83
  }
22
- function isAiFlagged(row) {
23
- // INTEG-4318: `failed` means the LLM omitted a decision for this component
24
- // (e.g. batch under-emit). Surface these in the AI-recommended-exclusions
25
- // section so the operator sees them and can override — silent inclusion was
26
- // the bug.
27
- return row.aiDecision === 'rejected' || row.aiDecision === 'failed';
84
+ function toSidebarEntry(c) {
85
+ const $slots = {};
86
+ if (c.slots) {
87
+ for (const s of c.slots) {
88
+ $slots[s.name] = { $allowedComponents: s.allowedComponents };
89
+ }
90
+ }
91
+ const entry = {
92
+ $type: 'component',
93
+ $properties: { __scopeGate: { $type: 'string', $category: 'content' } },
94
+ };
95
+ if (Object.keys($slots).length > 0)
96
+ entry.$slots = $slots;
97
+ return entry;
28
98
  }
29
99
  export function ScopeGateStep({ components, onConfirm, onQuit, aiFilterStatus = 'idle', aiFilterProgress = null, aiFilterError = null, onCancelAutoFilter, }) {
30
- // Pilot-2026-06-25: scope-gate UX overhaul — single unified list.
31
- //
32
- // Inverted mental model: we now track which rows are INCLUDED rather than
33
- // which are EXCLUDED. The underlying delta-on-prop pattern survives so
34
- // operator decisions stay sticky across streaming AI updates:
35
- // - userExcluded: rows the operator explicitly toggled OFF
36
- // - userUnExcluded: rows the operator explicitly toggled ON
37
- // Effective INCLUDED for a row:
38
- // if userExcluded.has(name): false
39
- // else if userUnExcluded.has(name): true
40
- // else: row.aiDecision !== 'rejected' // default: follow AI, default true if AI silent
41
- const [userExcluded, setUserExcluded] = useState(new Set());
42
- const [userUnExcluded, setUserUnExcluded] = useState(new Set());
43
- const [cursor, setCursor] = useState(0);
44
- const [scrollOffset, setScrollOffset] = useState(0);
45
- // Pilot-2026-06-23: `s` opens a side panel showing the full reject_reason
46
- // (untruncated) for an AI-flagged row under the cursor. With the focused-row
47
- // wrap landed, the panel is mostly redundant — kept for screen-reader /
48
- // overflow cases per spec.
49
- const [reasonPanelOpen, setReasonPanelOpen] = useState(false);
50
- // Pilot-2026-06-25 R2: two-section render. AI-flagged rows go to a top
51
- // section ("AI recommended exclusions"); the rest fall into "Components".
52
- // Within each section we preserve prop (extraction) order. Cursor walks
53
- // the unified flat list [...aiList, ...componentsList].
54
- const aiList = components.filter(isAiFlagged);
55
- const componentsList = components.filter((c) => !isAiFlagged(c));
56
- const flatList = [...aiList, ...componentsList];
57
- const isIncluded = (row) => {
58
- // Pilot-2026-06-25 invariant: operator decisions are ALWAYS sticky over
59
- // streaming AI updates. If the operator explicitly toggled this row
60
- // (userExcluded or userUnExcluded), that wins regardless of any later
61
- // aiDecision the auto-filter writes via prop updates. The [AI] badge
62
- // still appears on the row — it's informational, not authoritative.
63
- if (userExcluded.has(row.name))
64
- return false;
65
- if (userUnExcluded.has(row.name))
100
+ const { stdout } = useStdout();
101
+ const totalWidth = stdout?.columns ?? 80;
102
+ const columnPlan = useMemo(() => computeColumnWidths(totalWidth), [totalWidth]);
103
+ const sidebarWidth = columnPlan.main;
104
+ const [userDecisions, setUserDecisions] = useState(new Map());
105
+ const [nav, setNav] = useState({ cursor: 0, scrollOffset: 0 });
106
+ const [focusedColumn, setFocusedColumn] = useState('main');
107
+ const [addedComponentsCursor, setAddedComponentsCursor] = useState(0);
108
+ const [addedGroupsCursor, setAddedGroupsCursor] = useState(0);
109
+ const cursor = nav.cursor;
110
+ const scrollOffset = nav.scrollOffset;
111
+ const lineagePanel = useOverlayPanel({ toggleKey: 'l' });
112
+ const [lineageCursor, setLineageCursor] = useState(0);
113
+ const [cyclesPanelOpen, setCyclesPanelOpen] = useState(false);
114
+ const [cyclesCursor, setCyclesCursor] = useState(0);
115
+ const aiRationalePanel = useOverlayPanel({ toggleKey: 'x' });
116
+ const [aiCursor, setAiCursor] = useState(0);
117
+ const [pendingRejectCascade, setPendingRejectCascade] = useState(null);
118
+ const [searchOpen, setSearchOpen] = useState(false);
119
+ const [searchQuery, setSearchQuery] = useState('');
120
+ const [autocompleteCandidates, setAutocompleteCandidates] = useState([]);
121
+ const [jumpFilterTarget, setJumpFilterTarget] = useState(null);
122
+ const [columnOneView, setColumnOneView] = useState('grouped');
123
+ const [activeFilters, setActiveFilters] = useState(new Set());
124
+ const [showHelp, setShowHelp] = useState(false);
125
+ const getState = (name) => {
126
+ const v = userDecisions.get(name);
127
+ return v ?? 'undecided';
128
+ };
129
+ const isIncluded = (name) => getState(name) === 'accepted';
130
+ const applyDecisions = (entries) => {
131
+ setUserDecisions((prev) => {
132
+ const next = new Map(prev);
133
+ for (const [name, decision] of entries)
134
+ next.set(name, decision);
135
+ return next;
136
+ });
137
+ };
138
+ const groupedItems = useMemo(() => components.map((c) => ({
139
+ key: c.name,
140
+ entry: toSidebarEntry(c),
141
+ status: isAiFlagged(c) ? 'warning' : 'ok',
142
+ })), [components]);
143
+ const graph = useMemo(() => buildComponentGraph(groupedItems), [groupedItems]);
144
+ const slotCycles = useMemo(() => {
145
+ try {
146
+ return findSlotCycles(graph);
147
+ }
148
+ catch {
149
+ return [];
150
+ }
151
+ }, [graph]);
152
+ const cycleParticipants = useMemo(() => {
153
+ const set = new Set();
154
+ for (const c of slotCycles)
155
+ for (const n of c.path)
156
+ set.add(n);
157
+ return set;
158
+ }, [slotCycles]);
159
+ const cycleUnits = useMemo(() => buildCycleUnits(slotCycles), [slotCycles]);
160
+ const cyclesJumpables = useMemo(() => slotCycles.map((c, i) => ({
161
+ cycleIndex: i,
162
+ jumpTarget: c.path[0],
163
+ })), [slotCycles]);
164
+ const hasCycles = slotCycles.length > 0;
165
+ const closures = useMemo(() => computeAllClosures(graph), [graph]);
166
+ const [expandedGroups, setExpandedGroups] = useState(() => {
167
+ const seed = new Set(closures.keys());
168
+ for (const p of cycleParticipants)
169
+ seed.add(p);
170
+ return seed;
171
+ });
172
+ const seededGroupsRef = useRef(closures.size > 0 || cycleParticipants.size > 0);
173
+ useEffect(() => {
174
+ if (seededGroupsRef.current)
175
+ return;
176
+ if (closures.size === 0 && cycleParticipants.size === 0)
177
+ return;
178
+ seededGroupsRef.current = true;
179
+ const seed = new Set(closures.keys());
180
+ for (const p of cycleParticipants)
181
+ seed.add(p);
182
+ setExpandedGroups(seed);
183
+ }, [closures, cycleParticipants]);
184
+ const hasGroupRoots = useMemo(() => {
185
+ if (cycleParticipants.size > 0)
66
186
  return true;
67
- // INTEG-4318: exclude on 'rejected' AND 'failed'. Missing/null aiDecision
68
- // (auto-filter not run, or component never seen) still defaults to
69
- // included so --no-auto-filter and skip-credentials flows are unchanged.
70
- return row.aiDecision !== 'rejected' && row.aiDecision !== 'failed';
187
+ for (const c of closures.values())
188
+ if (c.nodes.length > 1)
189
+ return true;
190
+ return false;
191
+ }, [closures, cycleParticipants]);
192
+ const toggleExpanded = (rootName) => {
193
+ setExpandedGroups((prev) => {
194
+ const next = new Set(prev);
195
+ if (next.has(rootName))
196
+ next.delete(rootName);
197
+ else
198
+ next.add(rootName);
199
+ return next;
200
+ });
201
+ };
202
+ const brokenKeys = useMemo(() => {
203
+ const set = new Set();
204
+ for (const c of components)
205
+ if (isAiFlagged(c))
206
+ set.add(c.name);
207
+ return set;
208
+ }, [components]);
209
+ const filterVisibleKeys = useMemo(() => {
210
+ if (jumpFilterTarget) {
211
+ return findAllAncestors(jumpFilterTarget, graph);
212
+ }
213
+ const categoryKeys = computeFilterKeys({
214
+ filters: activeFilters,
215
+ data: { cycles: cycleParticipants, broken: brokenKeys },
216
+ });
217
+ const searchKeys = (() => {
218
+ if (!searchQuery)
219
+ return undefined;
220
+ const matches = groupedItems.map((it) => it.key).filter((k) => fuzzyMatches(searchQuery, k));
221
+ if (matches.length === 0)
222
+ return undefined;
223
+ return computeDirectNeighborhood(matches, graph);
224
+ })();
225
+ return intersectFilterKeys(categoryKeys, searchKeys);
226
+ }, [jumpFilterTarget, activeFilters, cycleParticipants, brokenKeys, searchQuery, groupedItems, graph]);
227
+ const visibleRows = useMemo(() => buildVisibleRows({
228
+ items: groupedItems,
229
+ cycleParticipants,
230
+ expandedGroups,
231
+ showFlatTier: false,
232
+ viewMode: columnOneView,
233
+ graph,
234
+ filterVisibleKeys,
235
+ }), [groupedItems, cycleParticipants, expandedGroups, columnOneView, graph, filterVisibleKeys]);
236
+ const total = visibleRows.length;
237
+ const safeCursor = Math.min(cursor, Math.max(0, total - 1));
238
+ const currentRow = visibleRows[safeCursor];
239
+ const currentRowKey = currentRow && currentRow.itemIdx >= 0 ? groupedItems[currentRow.itemIdx]?.key : undefined;
240
+ const focusedComponent = currentRowKey ? components.find((c) => c.name === currentRowKey) : undefined;
241
+ const selectionStateByKey = useMemo(() => {
242
+ const map = new Map();
243
+ for (const c of components) {
244
+ map.set(c.name, getState(c.name));
245
+ }
246
+ return map;
247
+ }, [components, userDecisions]);
248
+ const aiFlaggedByKey = useMemo(() => {
249
+ const map = new Map();
250
+ for (const c of components)
251
+ map.set(c.name, isAiFlagged(c));
252
+ return map;
253
+ }, [components]);
254
+ const dimPredicate = useMemo(() => buildFlatDimPredicate({
255
+ viewMode: columnOneView,
256
+ searchQuery,
257
+ filterVisibleKeys,
258
+ }), [columnOneView, searchQuery, filterVisibleKeys]);
259
+ const applyReject = (target) => {
260
+ const { toReject, toDeselect } = computeCycleAwareRejectCascade(target, graph, cycleUnits);
261
+ const entries = [];
262
+ for (const n of toReject)
263
+ entries.push([n, 'rejected']);
264
+ for (const n of toDeselect)
265
+ entries.push([n, 'undecided']);
266
+ applyDecisions(entries);
267
+ };
268
+ const applyAccept = (target) => {
269
+ const cascade = computeCycleAwareAcceptCascade(target, graph, cycleUnits);
270
+ applyDecisions([...cascade].map((n) => [n, 'accepted']));
271
+ };
272
+ const requestAccept = (name) => {
273
+ if (getState(name) === 'accepted')
274
+ return;
275
+ applyAccept(name);
276
+ };
277
+ const requestReject = (name) => {
278
+ if (getState(name) === 'rejected')
279
+ return;
280
+ const { toReject, toDeselect } = computeCycleAwareRejectCascade(name, graph, cycleUnits);
281
+ const ancestors = [...toReject].filter((n) => n !== name).sort();
282
+ const descendants = [...toDeselect].sort();
283
+ if (ancestors.length + descendants.length >= 2) {
284
+ setPendingRejectCascade({ target: name, ancestors, descendants });
285
+ return;
286
+ }
287
+ applyReject(name);
288
+ };
289
+ const focusedRowKey = () => {
290
+ const row = visibleRows[safeCursor];
291
+ if (!row)
292
+ return undefined;
293
+ switch (row.kind) {
294
+ case 'standalone':
295
+ case 'empty':
296
+ case 'group-root':
297
+ case 'group-child':
298
+ case 'flat':
299
+ case 'cycle':
300
+ return row.itemIdx >= 0 ? groupedItems[row.itemIdx]?.key : undefined;
301
+ default:
302
+ return undefined;
303
+ }
71
304
  };
72
305
  const partition = () => {
73
306
  const accepted = [];
74
307
  const rejected = [];
75
- for (const c of flatList) {
76
- if (isIncluded(c))
308
+ for (const c of components) {
309
+ if (getState(c.name) === 'accepted')
77
310
  accepted.push(c.name);
78
311
  else
79
312
  rejected.push(c.name);
80
313
  }
81
314
  return { accepted, rejected };
82
315
  };
83
- const toggleFocused = () => {
84
- const target = flatList[cursor];
85
- if (!target)
86
- return;
87
- const currentlyIn = isIncluded(target);
88
- if (currentlyIn) {
89
- // Flip to EXCLUDED.
90
- setUserExcluded((prev) => {
91
- if (prev.has(target.name))
92
- return prev;
93
- const next = new Set(prev);
94
- next.add(target.name);
95
- return next;
96
- });
97
- setUserUnExcluded((prev) => {
98
- if (!prev.has(target.name))
99
- return prev;
100
- const next = new Set(prev);
101
- next.delete(target.name);
102
- return next;
103
- });
104
- }
105
- else {
106
- // Flip to INCLUDED.
107
- setUserUnExcluded((prev) => {
108
- if (prev.has(target.name))
109
- return prev;
110
- const next = new Set(prev);
111
- next.add(target.name);
112
- return next;
113
- });
114
- setUserExcluded((prev) => {
115
- if (!prev.has(target.name))
116
- return prev;
117
- const next = new Set(prev);
118
- next.delete(target.name);
119
- return next;
120
- });
316
+ const { entries: lineageEntries, jumpables: lineageJumpables } = useLineage(focusedComponent?.name ?? null, graph);
317
+ const { sidebarVisibleCount: visibleCount, panelMaxRows } = computeSidebarBudget({
318
+ rows: stdout?.rows ?? FALLBACK_ROWS,
319
+ panelOpen: lineagePanel.isOpen,
320
+ entryCount: lineageEntries.length,
321
+ });
322
+ useEffect(() => {
323
+ setNav((prev) => {
324
+ const maxIdx = Math.max(0, visibleRows.length - 1);
325
+ const nextCursor = Math.min(prev.cursor, maxIdx);
326
+ const nextScroll = Math.min(prev.scrollOffset, maxIdx);
327
+ if (nextCursor === prev.cursor && nextScroll === prev.scrollOffset)
328
+ return prev;
329
+ return { cursor: nextCursor, scrollOffset: nextScroll };
330
+ });
331
+ }, [visibleRows]);
332
+ const searchMatches = useMemo(() => {
333
+ if (!searchQuery)
334
+ return [];
335
+ return components.filter((c) => fuzzyMatches(searchQuery, c.name)).map((c) => c.name);
336
+ }, [components, searchQuery]);
337
+ const findRowIndexForName = (name) => {
338
+ for (let i = 0; i < visibleRows.length; i++) {
339
+ const row = visibleRows[i];
340
+ if (row.itemIdx < 0)
341
+ continue;
342
+ if (groupedItems[row.itemIdx]?.key === name)
343
+ return i;
121
344
  }
345
+ return -1;
346
+ };
347
+ const jumpCursorTo = (name) => {
348
+ const idx = findRowIndexForName(name);
349
+ if (idx < 0)
350
+ return;
351
+ setNav(({ scrollOffset: prev }) => {
352
+ let nextScroll = prev;
353
+ if (idx < prev)
354
+ nextScroll = idx;
355
+ else if (idx >= prev + visibleCount)
356
+ nextScroll = idx - visibleCount + 1;
357
+ return { cursor: idx, scrollOffset: nextScroll };
358
+ });
122
359
  };
123
360
  useImmediateInput((input, key) => {
361
+ if (showHelp)
362
+ return;
363
+ if (pendingRejectCascade) {
364
+ if (input === 'y' || input === 'Y') {
365
+ applyReject(pendingRejectCascade.target);
366
+ setPendingRejectCascade(null);
367
+ return;
368
+ }
369
+ if (input === 'n' || input === 'N' || key.escape) {
370
+ setPendingRejectCascade(null);
371
+ return;
372
+ }
373
+ return;
374
+ }
375
+ if (searchOpen) {
376
+ if (key.escape) {
377
+ setSearchOpen(false);
378
+ setSearchQuery('');
379
+ setAutocompleteCandidates([]);
380
+ return;
381
+ }
382
+ if (key.return) {
383
+ if (!searchQuery || searchMatches.length === 0) {
384
+ setSearchOpen(false);
385
+ setSearchQuery('');
386
+ setAutocompleteCandidates([]);
387
+ return;
388
+ }
389
+ const cursorRow = visibleRows[safeCursor];
390
+ const cursorItemName = cursorRow && cursorRow.itemIdx >= 0 ? groupedItems[cursorRow.itemIdx]?.key : undefined;
391
+ let jumped = false;
392
+ for (let i = safeCursor; i < visibleRows.length; i++) {
393
+ const r = visibleRows[i];
394
+ if (r.itemIdx < 0)
395
+ continue;
396
+ const n = groupedItems[r.itemIdx]?.key;
397
+ if (n && n !== cursorItemName && fuzzyMatches(searchQuery, n)) {
398
+ jumpCursorTo(n);
399
+ jumped = true;
400
+ break;
401
+ }
402
+ }
403
+ if (!jumped) {
404
+ for (let i = 0; i < visibleRows.length; i++) {
405
+ const r = visibleRows[i];
406
+ if (r.itemIdx < 0)
407
+ continue;
408
+ const n = groupedItems[r.itemIdx]?.key;
409
+ if (n && fuzzyMatches(searchQuery, n)) {
410
+ jumpCursorTo(n);
411
+ break;
412
+ }
413
+ }
414
+ }
415
+ setSearchOpen(false);
416
+ return;
417
+ }
418
+ if (key.tab) {
419
+ const { completion, candidates } = computeAutocomplete(searchQuery, components.map((c) => c.name));
420
+ setSearchQuery(completion);
421
+ setAutocompleteCandidates(candidates);
422
+ return;
423
+ }
424
+ if (key.backspace) {
425
+ setAutocompleteCandidates([]);
426
+ setSearchQuery((q) => q.slice(0, -1));
427
+ return;
428
+ }
429
+ if (input && input.length === 1 && input >= ' ' && input !== '\r' && input !== '\n') {
430
+ setAutocompleteCandidates([]);
431
+ setSearchQuery((q) => q + input);
432
+ return;
433
+ }
434
+ return;
435
+ }
436
+ if (cyclesPanelOpen) {
437
+ if (key.escape || input === 'c') {
438
+ setCyclesPanelOpen(false);
439
+ return;
440
+ }
441
+ if (key.upArrow || input === 'k') {
442
+ setCyclesCursor((c) => Math.max(0, c - 1));
443
+ return;
444
+ }
445
+ if (key.downArrow || input === 'j') {
446
+ setCyclesCursor((c) => Math.min(Math.max(0, cyclesJumpables.length - 1), c + 1));
447
+ return;
448
+ }
449
+ if (key.return) {
450
+ const target = cyclesJumpables[cyclesCursor];
451
+ if (target)
452
+ jumpCursorTo(target.jumpTarget);
453
+ setCyclesPanelOpen(false);
454
+ return;
455
+ }
456
+ return;
457
+ }
458
+ if (lineagePanel.isOpen) {
459
+ if (input === 'c' && hasCycles) {
460
+ lineagePanel.close();
461
+ setCyclesPanelOpen(true);
462
+ setCyclesCursor(0);
463
+ return;
464
+ }
465
+ if (lineagePanel.handleInput(input, key))
466
+ return;
467
+ if (key.upArrow || input === 'k') {
468
+ setLineageCursor((c) => Math.max(0, c - 1));
469
+ return;
470
+ }
471
+ if (key.downArrow || input === 'j') {
472
+ setLineageCursor((c) => Math.min(Math.max(0, lineageJumpables.length - 1), c + 1));
473
+ return;
474
+ }
475
+ if (key.return) {
476
+ const target = lineageJumpables[lineageCursor];
477
+ if (target && (target.entry.kind === 'ancestor' || target.entry.kind === 'descendant')) {
478
+ jumpCursorTo(target.entry.jumpTarget);
479
+ }
480
+ lineagePanel.close();
481
+ return;
482
+ }
483
+ return;
484
+ }
485
+ if (aiRationalePanel.isOpen) {
486
+ if (aiRationalePanel.handleInput(input, key))
487
+ return;
488
+ if (key.upArrow || input === 'k') {
489
+ setAiCursor((c) => Math.max(0, c - 1));
490
+ return;
491
+ }
492
+ if (key.downArrow || input === 'j') {
493
+ setAiCursor((c) => Math.min(Math.max(0, aiRows.length - 1), c + 1));
494
+ return;
495
+ }
496
+ if (key.return) {
497
+ const row = aiRows[aiCursor];
498
+ if (row)
499
+ jumpCursorTo(row.jumpTarget);
500
+ aiRationalePanel.close();
501
+ return;
502
+ }
503
+ return;
504
+ }
124
505
  if (input === 'q' || key.escape) {
125
- // Feature 3: while auto-filter is running, q cancels the LLM run instead
126
- // of quitting the wizard. After completion (or if no auto-filter ran), q
127
- // falls back to the existing wizard-quit behavior.
128
506
  if (aiFilterStatus === 'running' && onCancelAutoFilter) {
129
507
  onCancelAutoFilter();
130
508
  return;
131
509
  }
132
- // Esc also closes the reason panel without quitting.
133
- if (key.escape && reasonPanelOpen) {
134
- setReasonPanelOpen(false);
510
+ if (key.escape && jumpFilterTarget) {
511
+ setJumpFilterTarget(null);
512
+ return;
513
+ }
514
+ if (key.escape && searchQuery) {
515
+ setSearchQuery('');
516
+ setAutocompleteCandidates([]);
135
517
  return;
136
518
  }
137
519
  onQuit();
138
520
  return;
139
521
  }
522
+ if (input === '?') {
523
+ setShowHelp(true);
524
+ return;
525
+ }
140
526
  if (input === 'f' || input === 'F') {
141
527
  onConfirm(partition());
142
528
  return;
143
529
  }
144
- if (input === 's') {
145
- setReasonPanelOpen((prev) => !prev);
530
+ if (input === 'l') {
531
+ if (focusedColumn === 'added-components') {
532
+ const entry = addedComponents[safeAddedComponentsCursor];
533
+ if (entry)
534
+ jumpCursorTo(entry.name);
535
+ }
536
+ else if (focusedColumn === 'added-groups') {
537
+ const g = addedGroups[safeAddedGroupsCursor];
538
+ if (g)
539
+ jumpCursorTo(g.name);
540
+ }
541
+ lineagePanel.open();
542
+ setLineageCursor(0);
543
+ setCyclesPanelOpen(false);
544
+ aiRationalePanel.close();
545
+ return;
546
+ }
547
+ if (input === 'c') {
548
+ if (!hasCycles)
549
+ return;
550
+ setCyclesPanelOpen(true);
551
+ setCyclesCursor(0);
552
+ lineagePanel.close();
553
+ aiRationalePanel.close();
554
+ return;
555
+ }
556
+ if (input === 'x') {
557
+ if (aiRows.length === 0)
558
+ return;
559
+ aiRationalePanel.open();
560
+ setAiCursor(0);
561
+ lineagePanel.close();
562
+ setCyclesPanelOpen(false);
563
+ return;
564
+ }
565
+ if (input === '/') {
566
+ setSearchOpen(true);
567
+ return;
568
+ }
569
+ if (input === 'o') {
570
+ if (!hasCycles)
571
+ return;
572
+ setActiveFilters((prev) => {
573
+ const next = new Set(prev);
574
+ if (next.has('cycles'))
575
+ next.delete('cycles');
576
+ else
577
+ next.add('cycles');
578
+ return next;
579
+ });
580
+ return;
581
+ }
582
+ if (input === 'i' && !key.tab && !key.ctrl) {
583
+ const targetKey = focusedColumn === 'main'
584
+ ? focusedRowKey()
585
+ : focusedColumn === 'added-components'
586
+ ? addedComponents[safeAddedComponentsCursor]?.name
587
+ : addedGroups[safeAddedGroupsCursor]?.name;
588
+ if (!targetKey)
589
+ return;
590
+ setJumpFilterTarget((prev) => (prev === targetKey ? null : targetKey));
591
+ return;
592
+ }
593
+ if (input === ' ') {
594
+ if (focusedColumn !== 'main')
595
+ return;
596
+ const key = focusedRowKey();
597
+ if (!key)
598
+ return;
599
+ const rootName = resolveGroupRoot(key, closures, cycleParticipants);
600
+ if (!rootName)
601
+ return;
602
+ toggleExpanded(rootName);
603
+ return;
604
+ }
605
+ if (input === 'E' && focusedColumn === 'main') {
606
+ const roots = new Set();
607
+ for (const [name, closure] of closures.entries()) {
608
+ if (closure.nodes.length > 1)
609
+ roots.add(name);
610
+ }
611
+ for (const p of cycleParticipants)
612
+ roots.add(p);
613
+ setExpandedGroups(roots);
146
614
  return;
147
615
  }
148
- // `a`, space, and `r` are all aliases for "toggle focused row INCLUDED ↔ EXCLUDED".
149
- // `r` kept for muscle-memory; it no longer means "reject".
150
- if (input === 'a' || input === ' ' || input === 'r') {
151
- toggleFocused();
616
+ if (input === 'C' && focusedColumn === 'main') {
617
+ setExpandedGroups(new Set());
618
+ return;
619
+ }
620
+ if (input === 'a' || input === 'r') {
621
+ const isReject = input === 'r';
622
+ if (focusedColumn === 'added-components') {
623
+ if (!isReject)
624
+ return;
625
+ const entry = addedComponents[safeAddedComponentsCursor];
626
+ if (entry)
627
+ requestReject(entry.name);
628
+ return;
629
+ }
630
+ if (focusedColumn === 'added-groups') {
631
+ if (!isReject)
632
+ return;
633
+ const g = addedGroups[safeAddedGroupsCursor];
634
+ if (g)
635
+ requestReject(g.name);
636
+ return;
637
+ }
638
+ const key = focusedRowKey();
639
+ if (!key)
640
+ return;
641
+ if (isReject)
642
+ requestReject(key);
643
+ else
644
+ requestAccept(key);
645
+ return;
646
+ }
647
+ if (input === 'L') {
648
+ const currentKey = currentRowKey;
649
+ const nextView = columnOneView === 'grouped' ? 'flat' : 'grouped';
650
+ const nextRows = buildVisibleRows({
651
+ items: groupedItems,
652
+ cycleParticipants,
653
+ expandedGroups,
654
+ showFlatTier: false,
655
+ viewMode: nextView,
656
+ graph,
657
+ });
658
+ let nextCursor = 0;
659
+ if (currentKey) {
660
+ for (let i = 0; i < nextRows.length; i++) {
661
+ const r = nextRows[i];
662
+ if (r.itemIdx < 0)
663
+ continue;
664
+ if (groupedItems[r.itemIdx]?.key === currentKey) {
665
+ nextCursor = i;
666
+ break;
667
+ }
668
+ }
669
+ }
670
+ const nextScroll = nextCursor < scrollOffset
671
+ ? nextCursor
672
+ : nextCursor >= scrollOffset + visibleCount
673
+ ? nextCursor - visibleCount + 1
674
+ : scrollOffset;
675
+ setColumnOneView(nextView);
676
+ setNav({ cursor: nextCursor, scrollOffset: nextScroll });
152
677
  return;
153
678
  }
154
679
  if (input === 'A') {
155
- // Pilot-2026-06-25 R2: toggle-all operates ONLY on the Components
156
- // section. The AI section's defaults stay put — operators rarely want
157
- // a mass-include of AI-flagged rows via one keystroke. Toggle behavior:
158
- // if any Components row is excluded, include them all; else exclude all.
159
- const compNames = componentsList.map((c) => c.name);
160
- const anyCompExcluded = componentsList.some((c) => !isIncluded(c));
161
- if (anyCompExcluded) {
162
- setUserUnExcluded((prev) => {
163
- const next = new Set(prev);
164
- for (const n of compNames)
165
- next.add(n);
166
- return next;
167
- });
168
- setUserExcluded((prev) => {
169
- const next = new Set(prev);
170
- for (const n of compNames)
171
- next.delete(n);
172
- return next;
173
- });
680
+ const nonCycle = components.filter((c) => !cycleParticipants.has(c.name)).map((c) => c.name);
681
+ const anyNotAccepted = nonCycle.some((n) => getState(n) !== 'accepted');
682
+ const target = anyNotAccepted ? 'accepted' : 'rejected';
683
+ if (target === 'accepted') {
684
+ const cyclesToInclude = collectReachableCycleUnits(nonCycle, graph, cycleUnits);
685
+ const entries = nonCycle.map((n) => [n, 'accepted']);
686
+ for (const n of cyclesToInclude)
687
+ entries.push([n, 'accepted']);
688
+ applyDecisions(entries);
174
689
  }
175
690
  else {
176
- setUserExcluded((prev) => {
177
- const next = new Set(prev);
178
- for (const n of compNames)
179
- next.add(n);
180
- return next;
181
- });
182
- setUserUnExcluded((prev) => {
183
- const next = new Set(prev);
184
- for (const n of compNames)
185
- next.delete(n);
186
- return next;
187
- });
691
+ const entries = nonCycle.map((n) => [n, 'rejected']);
692
+ for (const c of components) {
693
+ if (cycleParticipants.has(c.name) && getState(c.name) === 'accepted') {
694
+ entries.push([c.name, 'undecided']);
695
+ }
696
+ }
697
+ applyDecisions(entries);
698
+ }
699
+ return;
700
+ }
701
+ if (input === 'Y') {
702
+ const seeds = components.filter((c) => !cycleParticipants.has(c.name) && !isAiFlagged(c)).map((c) => c.name);
703
+ const cyclesToInclude = collectReachableCycleUnits(seeds, graph, cycleUnits);
704
+ const entries = seeds.map((n) => [n, 'accepted']);
705
+ for (const n of cyclesToInclude)
706
+ entries.push([n, 'accepted']);
707
+ applyDecisions(entries);
708
+ return;
709
+ }
710
+ if (key.tab) {
711
+ if (columnPlan.layout !== 'three-column')
712
+ return;
713
+ const forward = ['main', 'added-components', 'added-groups'];
714
+ const curIdx = forward.indexOf(focusedColumn);
715
+ const delta = key.shiftTab ? -1 : 1;
716
+ setFocusedColumn(forward[(curIdx + delta + forward.length) % forward.length]);
717
+ return;
718
+ }
719
+ if (key.return) {
720
+ if (focusedColumn === 'added-components') {
721
+ const entry = addedComponents[safeAddedComponentsCursor];
722
+ if (entry)
723
+ jumpCursorTo(entry.name);
724
+ setFocusedColumn('main');
725
+ return;
726
+ }
727
+ if (focusedColumn === 'added-groups') {
728
+ const g = addedGroups[safeAddedGroupsCursor];
729
+ if (g)
730
+ jumpCursorTo(g.name);
731
+ setFocusedColumn('main');
732
+ return;
188
733
  }
189
734
  return;
190
735
  }
191
736
  if (key.upArrow || input === 'k') {
192
- const len = flatList.length;
193
- if (len === 0)
737
+ if (focusedColumn === 'added-components') {
738
+ if (addedComponents.length === 0)
739
+ return;
740
+ setAddedComponentsCursor((c) => Math.max(0, c - 1));
741
+ return;
742
+ }
743
+ if (focusedColumn === 'added-groups') {
744
+ if (addedGroups.length === 0)
745
+ return;
746
+ setAddedGroupsCursor((c) => Math.max(0, c - 1));
194
747
  return;
195
- setCursor((c) => {
748
+ }
749
+ if (total === 0)
750
+ return;
751
+ setNav(({ cursor: c, scrollOffset: prev }) => {
196
752
  const next = c <= 0 ? 0 : c - 1;
197
- setScrollOffset((prev) => Math.min(prev, next));
198
- return next;
753
+ return { cursor: next, scrollOffset: Math.min(prev, next) };
199
754
  });
200
755
  return;
201
756
  }
202
757
  if (key.downArrow || input === 'j') {
203
- const len = flatList.length;
204
- if (len === 0)
758
+ if (focusedColumn === 'added-components') {
759
+ if (addedComponents.length === 0)
760
+ return;
761
+ setAddedComponentsCursor((c) => Math.min(addedComponents.length - 1, c + 1));
205
762
  return;
206
- setCursor((c) => {
207
- const next = c >= len - 1 ? len - 1 : c + 1;
208
- setScrollOffset((prev) => (next >= prev + VISIBLE_COUNT ? next - VISIBLE_COUNT + 1 : prev));
209
- return next;
763
+ }
764
+ if (focusedColumn === 'added-groups') {
765
+ if (addedGroups.length === 0)
766
+ return;
767
+ setAddedGroupsCursor((c) => Math.min(addedGroups.length - 1, c + 1));
768
+ return;
769
+ }
770
+ if (total === 0)
771
+ return;
772
+ setNav(({ cursor: c, scrollOffset: prev }) => {
773
+ const next = c >= total - 1 ? total - 1 : c + 1;
774
+ const nextScroll = next >= prev + visibleCount ? next - visibleCount + 1 : prev;
775
+ return { cursor: next, scrollOffset: nextScroll };
210
776
  });
211
777
  return;
212
778
  }
213
779
  });
214
- const total = flatList.length;
215
- const includedCount = useMemo(() => flatList.filter((c) => isIncluded(c)).length, [components, userExcluded, userUnExcluded]);
216
- const hasAnyAi = flatList.some(isAiFlagged);
217
- const visibleEnd = Math.min(scrollOffset + VISIBLE_COUNT, total);
218
- const visible = flatList.slice(scrollOffset, visibleEnd);
219
- const above = scrollOffset;
220
- const below = Math.max(0, total - visibleEnd);
221
- // ── Banner / status helpers ───────────────────────────────────────────────
222
- const showRunningHeader = aiFilterStatus === 'running' && aiFilterProgress !== null && aiFilterProgress.total > 0;
223
- const showCancelledBanner = aiFilterStatus === 'cancelled';
224
- const showFailedBanner = aiFilterStatus === 'failed';
225
- const allRejected = aiFilterStatus === 'complete' && total > 0 && flatList.every((c) => !isIncluded(c));
226
- return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: "green", children: "\u2713 Extraction complete" }), _jsxs(Text, { dimColor: true, children: ["Found ", total, " component", total === 1 ? '' : 's', ". Pick which ones to import. Generation runs only on the included set."] }), showRunningHeader && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsxs(Text, { color: "cyan", children: ["[AI filtering (", aiFilterProgress.done, "/", aiFilterProgress.total, ")\u2026] ", _jsx(Text, { dimColor: true, children: "[q] cancels" })] }) })), showCancelledBanner && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["AI auto-filter cancelled", aiFilterProgress ? ` at ${aiFilterProgress.done}/${aiFilterProgress.total}` : '', ". Review remaining manually."] }) })), showFailedBanner && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["AI auto-filter failed: ", aiFilterError ?? 'unknown error', ". Continuing without AI suggestions."] }) })), reasonPanelOpen && flatList[cursor] !== undefined && isAiFlagged(flatList[cursor]) && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: `AI rejection reason: ${flatList[cursor].name}` }), _jsx(Text, { children: flatList[cursor].aiReason ?? '<no reason given>' }), _jsx(Text, { dimColor: true, children: "[s] close \u00B7 [Esc] close" })] })), allRejected ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "AI excluded all components \u2014 press [a] to override or [q] to quit" }) })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [above > 0 && _jsxs(Text, { dimColor: true, children: ["\u2191 ", above, " above"] }), visible.map((c, vi) => {
227
- const i = vi + scrollOffset;
228
- const isCursor = i === cursor;
229
- const included = isIncluded(c);
230
- const aiFlagged = isAiFlagged(c);
231
- const prefix = isCursor ? '›' : ' ';
232
- // R2: color-glyphs replace word labels. Green [✓] for included,
233
- // red [✗] for excluded. The component name follows the same
234
- // color UNLESS the row is the cursor row, in which case the
235
- // name flips to cyan (state glyph keeps its red/green).
236
- const stateGlyph = included ? '[✓]' : '[✗]';
237
- const stateColor = included ? 'green' : 'red';
238
- // R2: cyan `*` glyph replaces the verbose `[AI]` badge.
239
- const aiMarkerNode = aiFlagged ? _jsx(Text, { color: "cyan", children: `${AI_MARKER} ` }) : null;
240
- const inlineReason = !isCursor && aiFlagged ? ` ${truncateReason(c.aiReason)}` : '';
241
- const showAiHeader = aiList.length > 0 && i === 0;
242
- const showComponentsHeader = componentsList.length > 0 && i === aiList.length;
243
- const header = showAiHeader ? (_jsx(Text, { bold: true, children: `AI recommended exclusions (${aiList.length})` }, `hdr-ai-${i}`)) : showComponentsHeader ? (_jsx(Text, { bold: true, children: `Components (${componentsList.length})` }, `hdr-comp-${i}`)) : null;
244
- if (isCursor) {
245
- const wrapReason = aiFlagged && c.aiReason !== null && c.aiReason !== undefined && c.aiReason.length > 0;
246
- return (_jsxs(React.Fragment, { children: [header, _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: `${prefix} ` }), aiMarkerNode, _jsx(Text, { color: stateColor, children: stateGlyph }), _jsx(Text, { color: "cyan", children: ` ${c.name}` })] }), wrapReason && _jsx(Text, { dimColor: true, children: `${REASON_WRAP_INDENT}${c.aiReason}` })] }, c.componentId));
780
+ const includedCount = useMemo(() => components.filter((c) => isIncluded(c.name)).length, [components, userDecisions]);
781
+ const hasAnyAi = components.some(isAiFlagged);
782
+ const aiExcludedCount = components.filter(isAiFlagged).length;
783
+ const aiExcludedWithReasons = components.filter((c) => isAiFlagged(c) && c.aiReason !== null && c.aiReason !== undefined && c.aiReason !== '');
784
+ const aiRows = useMemo(() => aiExcludedWithReasons.map((c) => ({
785
+ label: `${c.name} ${c.aiReason}`,
786
+ jumpTarget: c.name,
787
+ })), [aiExcludedWithReasons]);
788
+ const selectedItemIdx = currentRow && currentRow.itemIdx >= 0 ? currentRow.itemIdx : -1;
789
+ const nothingIncluded = components.length > 0 && components.every((c) => !isIncluded(c.name));
790
+ const totalComponents = components.length;
791
+ const totalMatches = searchQuery ? searchMatches.length : 0;
792
+ const addedComponents = useMemo(() => buildAddedComponentsList(components, selectionStateByKey, cycleParticipants), [components, selectionStateByKey, cycleParticipants]);
793
+ const addedGroups = useMemo(() => buildAddedGroupsList(closures, selectionStateByKey, cycleParticipants, cycleUnits), [closures, selectionStateByKey, cycleParticipants, cycleUnits]);
794
+ const counters = useMemo(() => computeCounters(components, closures, selectionStateByKey), [components, closures, selectionStateByKey]);
795
+ const safeAddedComponentsCursor = Math.min(addedComponentsCursor, Math.max(0, addedComponents.length - 1));
796
+ const safeAddedGroupsCursor = Math.min(addedGroupsCursor, Math.max(0, addedGroups.length - 1));
797
+ if (showHelp) {
798
+ return _jsx(HelpOverlay, { sections: HELP_SECTIONS, onClose: () => setShowHelp(false) });
799
+ }
800
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsx(Text, { color: PALETTE.success, children: "\u2713 Extraction complete" }), _jsxs(Text, { dimColor: true, children: ["Found ", totalComponents, " component", totalComponents === 1 ? '' : 's', ". Pick which ones to import. Generation runs only on the included set."] }), _jsx(AutoFilterBanner, { status: aiFilterStatus, progress: aiFilterProgress, error: aiFilterError }), hasAnyAi && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [`AI recommended exclusions (${aiExcludedCount})`, aiRows.length > 0 && _jsx(Text, { color: PALETTE.info, children: ' — [x] review & jump' })] }) })), _jsx(CounterStrip, { counters: counters, totalWidth: totalWidth }), hasCycles && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "If you must have components with cycles, select them together into the generate step and then use the editor to fix them." }) })), nothingIncluded && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: PALETTE.warning, children: ["nothing selected \u2014 press ", _jsx(Text, { color: PALETTE.info, children: "[Y]" }), " to accept all non-flagged,", ' ', _jsx(Text, { color: PALETTE.info, children: "[A]" }), " to toggle all, or ", _jsx(Text, { color: PALETTE.info, children: "[a]" }), " to accept the highlighted row"] }) })), _jsxs(Box, { flexDirection: "row", children: [aiRationalePanel.isOpen ? (_jsx(GotoBanner, { title: `AI recommended exclusions (${aiExcludedCount})`, rows: aiRows, cursor: aiCursor, maxRows: panelMaxRows, width: sidebarWidth, footerHint: "[\u2191/\u2193] move \u00B7 [Enter] jump \u00B7 [x/Esc] close" })) : lineagePanel.isOpen && focusedComponent ? (_jsx(LineagePanel, { focusedComponentKey: focusedComponent.name, entries: lineageEntries, cursor: lineageCursor, jumpables: lineageJumpables, maxRows: panelMaxRows, width: sidebarWidth })) : (_jsx(GroupedSidebar, { items: groupedItems, cycleParticipants: cycleParticipants, selectedIdx: selectedItemIdx, selectedRowIdx: safeCursor, onSelect: () => { }, expandedGroups: expandedGroups, onToggleExpanded: toggleExpanded, width: sidebarWidth, focused: focusedColumn === 'main', scrollOffset: scrollOffset, visibleCount: visibleCount, showFlatTier: false, selectionStateByKey: selectionStateByKey, aiFlaggedByKey: aiFlaggedByKey, dimPredicate: dimPredicate, visibleRows: visibleRows, viewMode: columnOneView, graph: graph })), columnPlan.layout === 'three-column' && (_jsxs(_Fragment, { children: [_jsx(Box, { width: 2, flexShrink: 0 }), _jsx(AddedComponentsColumn, { width: columnPlan.added, entries: addedComponents, cursor: safeAddedComponentsCursor, focused: focusedColumn === 'added-components', aiFlaggedByKey: aiFlaggedByKey, visibleCount: visibleCount }), _jsx(Box, { width: 2, flexShrink: 0 }), _jsx(AddedGroupsColumn, { width: columnPlan.groups, entries: addedGroups, cursor: safeAddedGroupsCursor, focused: focusedColumn === 'added-groups', aiFlaggedByKey: aiFlaggedByKey, visibleCount: visibleCount })] }))] }), focusedComponent && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: focusedComponent.name }), _jsx(Text, { dimColor: true, children: ' — ' }), isIncluded(focusedComponent.name) ? (_jsx(Text, { color: PALETTE.success, children: "included" })) : (_jsx(Text, { color: PALETTE.error, children: "excluded" })), isAiFlagged(focusedComponent) && (_jsx(Text, { color: PALETTE.warning, bold: true, children: ' [×]' }))] }), isAiFlagged(focusedComponent) &&
801
+ focusedComponent.aiReason !== null &&
802
+ focusedComponent.aiReason !== undefined &&
803
+ focusedComponent.aiReason !== '' && (_jsx(Box, { width: totalWidth, height: FOCUSED_REASON_MAX_LINES, flexShrink: 0, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: capReasonForFocusedRow(focusedComponent.aiReason, totalWidth) }) }))] })), cyclesPanelOpen && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: PALETTE.warning, paddingX: 1, marginTop: 1, children: [_jsx(Text, { bold: true, color: PALETTE.warning, children: `Cycles detected (${slotCycles.length}):` }), _jsx(Text, { children: " " }), slotCycles.map((cycle, i) => {
804
+ const isCursor = i === cyclesCursor;
805
+ const parts = [];
806
+ for (let idx = 0; idx < cycle.edges.length; idx++) {
807
+ parts.push(cycle.path[idx]);
808
+ parts.push(`[${cycle.edges[idx].slotName}]`);
247
809
  }
248
- return (_jsxs(React.Fragment, { children: [header, _jsxs(Text, { children: [_jsx(Text, { children: `${prefix} ` }), aiMarkerNode, _jsx(Text, { color: stateColor, children: stateGlyph }), _jsx(Text, { color: stateColor, children: ` ${c.name}` }), inlineReason !== '' && _jsx(Text, { dimColor: true, children: inlineReason })] })] }, c.componentId));
249
- }), below > 0 && _jsxs(Text, { dimColor: true, children: ["\u2193 ", below, " below"] })] })), _jsxs(Box, { gap: 3, marginTop: 1, children: [includedCount > 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: "green", children: includedCount }), _jsxs(Text, { dimColor: true, children: ["/", total, " included"] })] })) : (_jsx(Text, { color: "yellow", children: "none included" })), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "[j/k]" }), " ", _jsx(Text, { dimColor: true, children: "move" })] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "[a/space]" }), " ", _jsx(Text, { dimColor: true, children: "toggle" })] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "[A]" }), " ", _jsx(Text, { dimColor: true, children: "toggle all" })] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "[f]" }), " ", _jsx(Text, { dimColor: true, children: "continue" })] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "[q]" }), " ", _jsx(Text, { dimColor: true, children: "quit" })] }), hasAnyAi && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "[s]" }), " ", _jsx(Text, { dimColor: true, children: "AI reason" })] })), hasAnyAi && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "*" }), " ", _jsx(Text, { dimColor: true, children: "originally excluded by AI" })] }))] })] }));
810
+ parts.push(cycle.path[cycle.path.length - 1]);
811
+ const label = `Cycle ${i + 1}: ${parts.join(' ')}`;
812
+ return (_jsxs(Text, { children: [isCursor ? (_jsx(Text, { color: PALETTE.info, bold: true, children: '▶' })) : (_jsx(Text, { children: " " })), _jsx(Text, { color: PALETTE.warning, inverse: isCursor, children: ' ' + label })] }, i));
813
+ }), _jsx(Text, { dimColor: true, children: "[\u2191/\u2193] move \u00B7 [Enter] jump \u00B7 [c/Esc] close" })] })), pendingRejectCascade && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: PALETTE.warning, paddingX: 1, marginTop: 1, children: [_jsx(Text, { bold: true, color: PALETTE.warning, children: `Rejecting ${pendingRejectCascade.target} will:` }), pendingRejectCascade.ancestors.length > 0 && (_jsx(Text, { children: `- Reject ancestors: ${pendingRejectCascade.ancestors.join(', ')}` })), pendingRejectCascade.descendants.length > 0 && (_jsx(Text, { children: `- Deselect descendants: ${pendingRejectCascade.descendants.join(', ')}` })), _jsx(Text, { dimColor: true, children: "[y] confirm \u00B7 [n]/[Esc] cancel" })] })), searchOpen && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: [`/${searchQuery}`, _jsx(Text, { color: PALETTE.info, children: '▎' }), searchQuery && _jsx(Text, { dimColor: true, children: ` (${totalMatches}/${totalComponents} matches)` })] }), autocompleteCandidates.length > 1 && (_jsx(Text, { dimColor: true, children: ` possibilities: ${autocompleteCandidates.join(' · ').slice(0, 120)}` }))] })), !searchOpen && searchQuery && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: `/${searchQuery} (${totalMatches}/${totalComponents} matches) · [Esc] clear` }) })), _jsxs(Box, { columnGap: 2, marginTop: 1, flexWrap: "wrap", children: [includedCount > 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: PALETTE.success, children: includedCount }), _jsxs(Text, { dimColor: true, children: ["/", totalComponents, " included"] })] })) : (_jsx(Text, { color: PALETTE.warning, children: "none included" })), legendEntry('[j/k]', 'move'), legendEntry('[a]', 'accept'), legendEntry('[r]', 'reject'), hasGroupRoots && legendEntry('[space]', 'expand/collapse group'), hasGroupRoots && legendEntry('[E/C]', 'expand/collapse all'), legendEntry('[A]', 'toggle all'), legendEntry('[Y]', 'accept non-flagged'), legendEntry('[L]', 'flat', columnOneView === 'flat'), legendEntry('[l]', 'lineage', lineagePanel.isOpen), legendEntry('[i]', 'focus lineage', jumpFilterTarget !== null), hasCycles && legendEntry('[o]', 'only cycles', activeFilters.has('cycles')), hasCycles && legendEntry('[c]', 'cycle list', cyclesPanelOpen), legendEntry('[/]', 'search', searchOpen || searchQuery.length > 0), legendEntry('[f]', 'continue'), legendEntry('[?]', 'help'), legendEntry('[q]', 'quit'), columnPlan.layout === 'three-column' && legendEntry('[Tab/Shift-Tab]', 'switch column'), columnPlan.layout === 'three-column' && legendEntry('[Enter]', 'jump to main'), hasAnyAi && legendEntry('[x]', 'AI exclusions', aiRationalePanel.isOpen), hasAnyAi && (_jsxs(Text, { children: [_jsx(Text, { color: PALETTE.warning, bold: true, children: "[\u00D7]" }), ' ', _jsx(Text, { dimColor: true, children: "AI recommends excluding" })] }))] })] }));
814
+ }
815
+ function ColumnHeader(props) {
816
+ const { title, width, focused } = props;
817
+ const sep = '─'.repeat(Math.max(0, width - 2));
818
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: focused ? PALETTE.inverse : PALETTE.info, inverse: focused, children: title }), _jsx(Text, { dimColor: true, children: sep })] }));
819
+ }
820
+ export function sideColumnLabelStyle(input) {
821
+ const { isCycle, isSelected, focused } = input;
822
+ const isCursor = isSelected && focused;
823
+ if (isCursor) {
824
+ return {
825
+ nameColor: PALETTE.inverse,
826
+ nameBold: true,
827
+ nameInverse: true,
828
+ nameUnderline: false,
829
+ suffixColor: PALETTE.inverse,
830
+ suffixDim: false,
831
+ suffixInverse: true,
832
+ suffixUnderline: false,
833
+ };
834
+ }
835
+ const underline = isSelected && !focused;
836
+ if (isCycle) {
837
+ return {
838
+ nameColor: PALETTE.warning,
839
+ nameBold: false,
840
+ nameInverse: false,
841
+ nameUnderline: underline,
842
+ suffixColor: PALETTE.warning,
843
+ suffixDim: false,
844
+ suffixInverse: false,
845
+ suffixUnderline: underline,
846
+ };
847
+ }
848
+ return {
849
+ nameColor: PALETTE.success,
850
+ nameBold: false,
851
+ nameInverse: false,
852
+ nameUnderline: underline,
853
+ suffixColor: PALETTE.info,
854
+ suffixDim: true,
855
+ suffixInverse: false,
856
+ suffixUnderline: underline,
857
+ };
858
+ }
859
+ export function computeColumnWindow(total, cursor, visibleCount) {
860
+ if (total <= visibleCount)
861
+ return { start: 0, end: total, above: 0, below: 0 };
862
+ let start = Math.max(0, cursor - Math.floor(visibleCount / 2));
863
+ start = Math.min(start, total - visibleCount);
864
+ const end = start + visibleCount;
865
+ return { start, end, above: start, below: total - end };
866
+ }
867
+ function AddedComponentsColumn(props) {
868
+ const { width, entries, cursor, focused, aiFlaggedByKey, visibleCount } = props;
869
+ const reserveAiBadge = entries.some((e) => aiFlaggedByKey?.get(e.name) === true);
870
+ const firstNonCycleIdx = entries.findIndex((e) => !e.isCycle);
871
+ const window = computeColumnWindow(entries.length, cursor, Math.max(1, visibleCount));
872
+ return (_jsxs(Box, { flexDirection: "column", width: width, flexShrink: 0, borderStyle: "single", borderColor: focused ? PALETTE.inverse : undefined, children: [_jsx(ColumnHeader, { title: "Added components", width: width, focused: focused }), entries.length === 0 ? (_jsx(Text, { dimColor: true, children: "(none)" })) : (_jsxs(_Fragment, { children: [window.above > 0 && _jsx(Text, { dimColor: true, children: `↑ ${window.above} more` }), entries.slice(window.start, window.end).map((entry, vi) => {
873
+ const i = window.start + vi;
874
+ const isSelected = i === cursor;
875
+ const isCursor = focused && isSelected;
876
+ const aiFlagged = aiFlaggedByKey?.get(entry.name) === true;
877
+ const showSeparator = firstNonCycleIdx > 0 && i === firstNonCycleIdx;
878
+ const style = sideColumnLabelStyle({
879
+ isCycle: entry.isCycle,
880
+ isSelected,
881
+ focused,
882
+ });
883
+ return (_jsxs(React.Fragment, { children: [showSeparator && _jsx(Text, { dimColor: true, children: '─'.repeat(Math.max(0, width - 2)) }), _jsxs(Box, { children: [isCursor ? (_jsx(Text, { color: PALETTE.info, bold: true, children: '▶' })) : (_jsx(Text, { children: " " })), reserveAiBadge &&
884
+ (aiFlagged ? (_jsx(Text, { color: PALETTE.warning, bold: true, children: ' [×]' })) : (_jsx(Text, { children: ' ' }))), entry.isCycle && (_jsx(Text, { color: isCursor ? PALETTE.inverse : PALETTE.warning, bold: true, inverse: isCursor, underline: style.nameUnderline, children: ' ⚠' })), _jsx(Text, { color: style.nameColor, bold: style.nameBold, inverse: style.nameInverse, underline: style.nameUnderline, wrap: "truncate", children: ' ' + entry.name })] })] }, entry.name));
885
+ }), window.below > 0 && _jsx(Text, { dimColor: true, children: `↓ ${window.below} more` })] }))] }));
886
+ }
887
+ function AddedGroupsColumn(props) {
888
+ const { width, entries, cursor, focused, aiFlaggedByKey, visibleCount } = props;
889
+ const reserveAiBadge = entries.some((g) => aiFlaggedByKey?.get(g.name) === true);
890
+ const firstNonCycleIdx = entries.findIndex((e) => !e.isCycle);
891
+ const window = computeColumnWindow(entries.length, cursor, Math.max(1, visibleCount));
892
+ return (_jsxs(Box, { flexDirection: "column", width: width, flexShrink: 0, borderStyle: "single", borderColor: focused ? PALETTE.inverse : undefined, children: [_jsx(ColumnHeader, { title: "Added groups", width: width, focused: focused }), entries.length === 0 ? (_jsx(Text, { dimColor: true, children: "(none)" })) : (_jsxs(_Fragment, { children: [window.above > 0 && _jsx(Text, { dimColor: true, children: `↑ ${window.above} more` }), entries.slice(window.start, window.end).map((g, vi) => {
893
+ const i = window.start + vi;
894
+ const isSelected = i === cursor;
895
+ const isCursor = focused && isSelected;
896
+ const suffix = ` (${g.depCount} dep${g.depCount === 1 ? '' : 's'})`;
897
+ const aiFlagged = aiFlaggedByKey?.get(g.name) === true;
898
+ const showSeparator = firstNonCycleIdx > 0 && i === firstNonCycleIdx;
899
+ const style = sideColumnLabelStyle({
900
+ isCycle: g.isCycle,
901
+ isSelected,
902
+ focused,
903
+ });
904
+ return (_jsxs(React.Fragment, { children: [showSeparator && _jsx(Text, { dimColor: true, children: '─'.repeat(Math.max(0, width - 2)) }), _jsxs(Box, { children: [isCursor ? (_jsx(Text, { color: PALETTE.info, bold: true, children: '▶' })) : (_jsx(Text, { children: " " })), reserveAiBadge &&
905
+ (aiFlagged ? (_jsx(Text, { color: PALETTE.warning, bold: true, children: ' [×]' })) : (_jsx(Text, { children: ' ' }))), g.isCycle && (_jsx(Text, { color: isCursor ? PALETTE.inverse : PALETTE.warning, bold: true, inverse: isCursor, underline: style.nameUnderline, children: ' ⚠' })), _jsx(Text, { color: style.nameColor, bold: style.nameBold, inverse: style.nameInverse, underline: style.nameUnderline, wrap: "truncate", children: ' ' + g.name }), _jsx(Text, { color: style.suffixColor, dimColor: style.suffixDim, inverse: style.suffixInverse, underline: style.suffixUnderline, bold: style.nameBold, wrap: "truncate", children: suffix })] })] }, g.name));
906
+ }), window.below > 0 && _jsx(Text, { dimColor: true, children: `↓ ${window.below} more` })] }))] }));
250
907
  }