@contentful/experience-design-system-cli 2.11.4-dev-build-d47ab65.0 → 2.12.1-dev-build-589b615.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 (142) hide show
  1. package/README.md +214 -357
  2. package/dist/package.json +1 -1
  3. package/dist/src/analyze/extract/astro.js +9 -0
  4. package/dist/src/analyze/extract/react.js +23 -0
  5. package/dist/src/analyze/extract/stencil.js +3 -0
  6. package/dist/src/analyze/extract/vue.js +6 -0
  7. package/dist/src/analyze/extract/web-components.js +10 -0
  8. package/dist/src/analyze/pre-classify.d.ts +1 -1
  9. package/dist/src/analyze/pre-classify.js +3 -3
  10. package/dist/src/analyze/select/persistence.d.ts +13 -0
  11. package/dist/src/analyze/select/persistence.js +38 -0
  12. package/dist/src/analyze/select/preview-annotations.d.ts +26 -0
  13. package/dist/src/analyze/select/preview-annotations.js +62 -0
  14. package/dist/src/analyze/select/tui/App.js +5 -2
  15. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +30 -0
  16. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +137 -0
  17. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +58 -1
  18. package/dist/src/analyze/select/tui/components/FieldEditor.js +637 -161
  19. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +1 -1
  20. package/dist/src/analyze/select/tui/components/JsonPanel.js +5 -1
  21. package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +34 -0
  22. package/dist/src/analyze/select/tui/components/RationalePanel.js +92 -0
  23. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +13 -1
  24. package/dist/src/analyze/select/tui/components/Sidebar.js +27 -3
  25. package/dist/src/analyze/select/tui/hooks/scroll-offset.d.ts +29 -0
  26. package/dist/src/analyze/select/tui/hooks/scroll-offset.js +26 -0
  27. package/dist/src/analyze/select-agent/command.d.ts +2 -0
  28. package/dist/src/analyze/select-agent/command.js +259 -45
  29. package/dist/src/analyze/select-agent/show-rationale.d.ts +35 -0
  30. package/dist/src/analyze/select-agent/show-rationale.js +92 -0
  31. package/dist/src/apply/command.js +7 -3
  32. package/dist/src/apply/tui/ServerApplyView.d.ts +3 -1
  33. package/dist/src/apply/tui/ServerApplyView.js +3 -2
  34. package/dist/src/credentials-store.d.ts +5 -0
  35. package/dist/src/credentials-store.js +7 -1
  36. package/dist/src/generate/agent-runner.d.ts +14 -0
  37. package/dist/src/generate/agent-runner.js +16 -0
  38. package/dist/src/generate/command.d.ts +6 -0
  39. package/dist/src/generate/command.js +45 -10
  40. package/dist/src/generate/progress.d.ts +9 -0
  41. package/dist/src/generate/progress.js +11 -0
  42. package/dist/src/generate/prompt-builder.d.ts +8 -0
  43. package/dist/src/generate/prompt-builder.js +22 -11
  44. package/dist/src/import/agent-model-resolve.d.ts +23 -0
  45. package/dist/src/import/agent-model-resolve.js +35 -0
  46. package/dist/src/import/auto-filter-resolve.d.ts +12 -0
  47. package/dist/src/import/auto-filter-resolve.js +16 -0
  48. package/dist/src/import/command.js +248 -8
  49. package/dist/src/import/orchestrator.d.ts +2 -0
  50. package/dist/src/import/orchestrator.js +11 -3
  51. package/dist/src/import/print-prompt.d.ts +35 -0
  52. package/dist/src/import/print-prompt.js +31 -0
  53. package/dist/src/import/tui/CustomPromptBanner.d.ts +11 -0
  54. package/dist/src/import/tui/CustomPromptBanner.js +7 -0
  55. package/dist/src/import/tui/WizardApp.d.ts +112 -8
  56. package/dist/src/import/tui/WizardApp.js +814 -333
  57. package/dist/src/import/tui/auto-filter-error.d.ts +2 -0
  58. package/dist/src/import/tui/auto-filter-error.js +29 -0
  59. package/dist/src/import/tui/final-review-host.d.ts +15 -0
  60. package/dist/src/import/tui/final-review-host.js +20 -0
  61. package/dist/src/import/tui/push-decision-gate-helpers.d.ts +8 -0
  62. package/dist/src/import/tui/push-decision-gate-helpers.js +8 -0
  63. package/dist/src/import/tui/push-progress.d.ts +23 -0
  64. package/dist/src/import/tui/push-progress.js +14 -0
  65. package/dist/src/import/tui/run-print-files-helpers.d.ts +26 -0
  66. package/dist/src/import/tui/run-print-files-helpers.js +8 -0
  67. package/dist/src/import/tui/run-teaser.d.ts +5 -0
  68. package/dist/src/import/tui/run-teaser.js +9 -0
  69. package/dist/src/import/tui/runLivePreview.d.ts +35 -0
  70. package/dist/src/import/tui/runLivePreview.js +73 -0
  71. package/dist/src/import/tui/runScopeGate.d.ts +22 -0
  72. package/dist/src/import/tui/runScopeGate.js +28 -0
  73. package/dist/src/import/tui/scope-gate-host.d.ts +21 -0
  74. package/dist/src/import/tui/scope-gate-host.js +20 -0
  75. package/dist/src/import/tui/spawn-generate.d.ts +33 -0
  76. package/dist/src/import/tui/spawn-generate.js +53 -0
  77. package/dist/src/import/tui/steps/CredentialsStep.d.ts +25 -1
  78. package/dist/src/import/tui/steps/CredentialsStep.js +31 -3
  79. package/dist/src/import/tui/steps/DoneStep.d.ts +6 -1
  80. package/dist/src/import/tui/steps/DoneStep.js +10 -4
  81. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +29 -2
  82. package/dist/src/import/tui/steps/GenerateReviewStep.js +412 -48
  83. package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +19 -0
  84. package/dist/src/import/tui/steps/PushDecisionGateStep.js +76 -0
  85. package/dist/src/import/tui/steps/PushingStep.d.ts +10 -0
  86. package/dist/src/import/tui/steps/PushingStep.js +40 -0
  87. package/dist/src/import/tui/steps/ScopeGateStep.d.ts +23 -0
  88. package/dist/src/import/tui/steps/ScopeGateStep.js +243 -0
  89. package/dist/src/import/tui/steps/TokenInputStep.js +20 -3
  90. package/dist/src/import/tui/useLivePreview.d.ts +35 -0
  91. package/dist/src/import/tui/useLivePreview.js +120 -0
  92. package/dist/src/import/tui/wizard-generate-progress.d.ts +21 -0
  93. package/dist/src/import/tui/wizard-generate-progress.js +25 -0
  94. package/dist/src/import/tui/wizard-save-flow.d.ts +37 -0
  95. package/dist/src/import/tui/wizard-save-flow.js +20 -0
  96. package/dist/src/import/tui/wizard-state-transitions.d.ts +80 -0
  97. package/dist/src/import/tui/wizard-state-transitions.js +79 -0
  98. package/dist/src/lib/contentful-urls.d.ts +34 -0
  99. package/dist/src/lib/contentful-urls.js +41 -0
  100. package/dist/src/program.js +2 -0
  101. package/dist/src/runs/export-helpers.d.ts +30 -0
  102. package/dist/src/runs/export-helpers.js +59 -0
  103. package/dist/src/runs/fingerprint.d.ts +47 -0
  104. package/dist/src/runs/fingerprint.js +75 -0
  105. package/dist/src/runs/ls-command.d.ts +12 -0
  106. package/dist/src/runs/ls-command.js +137 -0
  107. package/dist/src/runs/modify-launcher.d.ts +24 -0
  108. package/dist/src/runs/modify-launcher.js +36 -0
  109. package/dist/src/runs/path-prompt.d.ts +8 -0
  110. package/dist/src/runs/path-prompt.js +72 -0
  111. package/dist/src/runs/push-creds-prompt.d.ts +25 -0
  112. package/dist/src/runs/push-creds-prompt.js +39 -0
  113. package/dist/src/runs/push-helpers.d.ts +29 -0
  114. package/dist/src/runs/push-helpers.js +69 -0
  115. package/dist/src/runs/replay-helpers.d.ts +67 -0
  116. package/dist/src/runs/replay-helpers.js +148 -0
  117. package/dist/src/runs/resolve-run-target.d.ts +12 -0
  118. package/dist/src/runs/resolve-run-target.js +45 -0
  119. package/dist/src/runs/run-picker-mount.d.ts +46 -0
  120. package/dist/src/runs/run-picker-mount.js +72 -0
  121. package/dist/src/runs/run-picker.d.ts +17 -0
  122. package/dist/src/runs/run-picker.js +145 -0
  123. package/dist/src/runs/save-conflict.d.ts +8 -0
  124. package/dist/src/runs/save-conflict.js +56 -0
  125. package/dist/src/runs/save-path-resolver.d.ts +55 -0
  126. package/dist/src/runs/save-path-resolver.js +71 -0
  127. package/dist/src/runs/staleness.d.ts +38 -0
  128. package/dist/src/runs/staleness.js +140 -0
  129. package/dist/src/runs/store.d.ts +78 -0
  130. package/dist/src/runs/store.js +136 -0
  131. package/dist/src/session/cache-keys.d.ts +15 -0
  132. package/dist/src/session/cache-keys.js +38 -0
  133. package/dist/src/session/db.d.ts +89 -3
  134. package/dist/src/session/db.js +447 -104
  135. package/dist/src/setup/auto-filter-prompt.d.ts +13 -0
  136. package/dist/src/setup/auto-filter-prompt.js +24 -0
  137. package/dist/src/setup/command.d.ts +7 -0
  138. package/dist/src/setup/command.js +58 -2
  139. package/dist/src/types.d.ts +6 -0
  140. package/package.json +2 -2
  141. package/skills/generate-components.md +54 -6
  142. package/skills/select-components.md +13 -4
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.11.4-dev-build-d47ab65.0",
3
+ "version": "2.12.1-dev-build-589b615.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -108,6 +108,8 @@ function extractPropsFromFrontmatter(frontmatter) {
108
108
  type: typeText,
109
109
  required,
110
110
  ...(allowedValues && { allowedValues }),
111
+ sourceStartLine: member.getStartLineNumber(),
112
+ sourceEndLine: member.getEndLineNumber(),
111
113
  });
112
114
  }
113
115
  }
@@ -127,6 +129,12 @@ function extractPropsFromFrontmatter(frontmatter) {
127
129
  type: typeText,
128
130
  required,
129
131
  ...(allowedValues && { allowedValues }),
132
+ ...(typeof decl.getStartLineNumber === 'function'
133
+ ? {
134
+ sourceStartLine: decl.getStartLineNumber(),
135
+ sourceEndLine: decl.getEndLineNumber(),
136
+ }
137
+ : {}),
130
138
  });
131
139
  }
132
140
  }
@@ -236,6 +244,7 @@ function extractFromAstroFile(filePath, source) {
236
244
  return {
237
245
  name,
238
246
  source: filePath,
247
+ sourcePath: filePath,
239
248
  framework: 'astro',
240
249
  props: propsWithDefaults,
241
250
  slots,
@@ -485,6 +485,12 @@ function extractPropsFromTypeSymbols(type, slotNames, suppressNeverChildrenSlot
485
485
  type: typeText,
486
486
  required,
487
487
  ...(allowedValues && { allowedValues }),
488
+ ...(typeof declaration.getStartLineNumber === 'function'
489
+ ? {
490
+ sourceStartLine: declaration.getStartLineNumber(),
491
+ sourceEndLine: declaration.getEndLineNumber(),
492
+ }
493
+ : {}),
488
494
  });
489
495
  }
490
496
  return {
@@ -539,6 +545,12 @@ function extractPropsFromInterfaceDeclaration(declaration, slotNames, seen, excl
539
545
  type: typeText,
540
546
  required,
541
547
  ...(allowedValues && { allowedValues }),
548
+ ...(typeof decl.getStartLineNumber === 'function'
549
+ ? {
550
+ sourceStartLine: decl.getStartLineNumber(),
551
+ sourceEndLine: decl.getEndLineNumber(),
552
+ }
553
+ : {}),
542
554
  });
543
555
  return acc;
544
556
  }, { props: [], hasChildren: false })
@@ -921,6 +933,13 @@ function extractDestructuredBindingFallbackProps(func, param, existingPropNames,
921
933
  name: propName,
922
934
  type: propertyType === 'unknown' ? 'any' : propertyType,
923
935
  required: property ? !property.isOptional() : false,
936
+ ...(declaration &&
937
+ typeof declaration.getStartLineNumber === 'function'
938
+ ? {
939
+ sourceStartLine: declaration.getStartLineNumber(),
940
+ sourceEndLine: declaration.getEndLineNumber(),
941
+ }
942
+ : {}),
924
943
  });
925
944
  }
926
945
  }
@@ -1674,6 +1693,7 @@ function extractFromSourceFile(sourceFile, isNext) {
1674
1693
  components.push({
1675
1694
  name,
1676
1695
  source: sourceFile.getFilePath(),
1696
+ sourcePath: sourceFile.getFilePath(),
1677
1697
  framework: isNext ? 'next' : 'react',
1678
1698
  props: [],
1679
1699
  slots: [],
@@ -1759,6 +1779,7 @@ function extractFromSourceFile(sourceFile, isNext) {
1759
1779
  components.push({
1760
1780
  name,
1761
1781
  source: sourceFile.getFilePath(),
1782
+ sourcePath: sourceFile.getFilePath(),
1762
1783
  framework: isNext ? 'next' : 'react',
1763
1784
  props: propsAfterSlotExpansion,
1764
1785
  slots: finalSlots,
@@ -1823,6 +1844,8 @@ function extractPropTypes(sourceFile, componentName) {
1823
1844
  type,
1824
1845
  required,
1825
1846
  ...(allowedValues && { allowedValues }),
1847
+ sourceStartLine: property.getStartLineNumber(),
1848
+ sourceEndLine: property.getEndLineNumber(),
1826
1849
  });
1827
1850
  }
1828
1851
  return props.sort((a, b) => a.name.localeCompare(b.name));
@@ -99,6 +99,8 @@ function extractProps(classDecl) {
99
99
  ...(defaultValue !== undefined && { defaultValue }),
100
100
  ...(description && { description }),
101
101
  ...(allowedValues && { allowedValues }),
102
+ sourceStartLine: property.getStartLineNumber(),
103
+ sourceEndLine: property.getEndLineNumber(),
102
104
  });
103
105
  }
104
106
  return props.sort((a, b) => a.name.localeCompare(b.name));
@@ -231,6 +233,7 @@ function extractFromSourceFile(sourceFile, warnings) {
231
233
  components.push({
232
234
  name,
233
235
  source: sourceFile.getFilePath(),
236
+ sourcePath: sourceFile.getFilePath(),
234
237
  framework: 'stencil',
235
238
  props,
236
239
  slots,
@@ -78,6 +78,7 @@ async function extractFromVueSFC(filePath, source) {
78
78
  component: {
79
79
  name,
80
80
  source: filePath,
81
+ sourcePath: filePath,
81
82
  framework: 'vue',
82
83
  props,
83
84
  slots,
@@ -104,6 +105,7 @@ async function extractOptionsComponent(filePath, scriptContent, fileWarnings, na
104
105
  component: {
105
106
  name,
106
107
  source: filePath,
108
+ sourcePath: filePath,
107
109
  framework: 'vue',
108
110
  props,
109
111
  slots,
@@ -552,6 +554,8 @@ function parseObjectProps(obj) {
552
554
  name,
553
555
  type: VUE_TYPE_MAP[initText] ?? 'any',
554
556
  required: false,
557
+ sourceStartLine: prop.getStartLineNumber(),
558
+ sourceEndLine: prop.getEndLineNumber(),
555
559
  });
556
560
  continue;
557
561
  }
@@ -585,6 +589,8 @@ function parseObjectProps(obj) {
585
589
  type,
586
590
  required,
587
591
  ...(defaultValue !== undefined && { defaultValue }),
592
+ sourceStartLine: prop.getStartLineNumber(),
593
+ sourceEndLine: prop.getEndLineNumber(),
588
594
  });
589
595
  }
590
596
  return result;
@@ -364,6 +364,8 @@ function extractClassProperties(classDecl, applyRuntimeFieldDenylist = false) {
364
364
  type,
365
365
  required: false,
366
366
  ...(defaultValue !== undefined && { defaultValue }),
367
+ sourceStartLine: property.getStartLineNumber(),
368
+ sourceEndLine: property.getEndLineNumber(),
367
369
  });
368
370
  }
369
371
  return props;
@@ -417,10 +419,17 @@ function extractAccessorProperties(classDecl, applyRuntimeFieldDenylist = false)
417
419
  if (!hasPropertyDecorator && !(getter && setter))
418
420
  continue;
419
421
  const type = getter?.getReturnTypeNode()?.getText() ?? setter?.getParameters()[0]?.getTypeNode()?.getText() ?? 'any';
422
+ const accessorNode = getter ?? setter;
420
423
  props.push({
421
424
  name,
422
425
  type,
423
426
  required: false,
427
+ ...(accessorNode
428
+ ? {
429
+ sourceStartLine: accessorNode.getStartLineNumber(),
430
+ sourceEndLine: accessorNode.getEndLineNumber(),
431
+ }
432
+ : {}),
424
433
  });
425
434
  }
426
435
  return props;
@@ -824,6 +833,7 @@ function extractFromSourceFile(sourceFile, project) {
824
833
  components.push({
825
834
  name,
826
835
  source: sourceFile.getFilePath(),
836
+ sourcePath: sourceFile.getFilePath(),
827
837
  framework: 'web-component',
828
838
  props,
829
839
  slots,
@@ -1,7 +1,7 @@
1
1
  import type { RawPropDefinition, RawComponentDefinition } from '../types.js';
2
2
  export interface PreClassification {
3
3
  category: 'content' | 'design' | 'state' | 'exclude';
4
- cdfTypeHint?: 'string' | 'enum' | 'richtext' | 'media';
4
+ cdfTypeHint?: 'string' | 'enum' | 'richtext' | 'media' | 'boolean';
5
5
  }
6
6
  /**
7
7
  * Deterministic pre-classification rule engine.
@@ -142,14 +142,14 @@ export function preClassifyProp(prop) {
142
142
  if (isBooleanType(type)) {
143
143
  const stateNames = ['disabled', 'loading', 'expanded', 'isOpen', 'selected', 'checked', 'active', 'preview'];
144
144
  if (stateNames.includes(name)) {
145
- return { category: 'state', cdfTypeHint: 'string' };
145
+ return { category: 'state', cdfTypeHint: 'boolean' };
146
146
  }
147
147
  }
148
148
  // Rule 9: Boolean + visual toggle name
149
149
  if (isBooleanType(type)) {
150
150
  const visualToggle = /^(hide|show|enable|disable|vertical|horizontal|reverse|bold|italic|imageOn|with)/i;
151
151
  if (visualToggle.test(name)) {
152
- return { category: 'design', cdfTypeHint: 'string' };
152
+ return { category: 'design', cdfTypeHint: 'boolean' };
153
153
  }
154
154
  }
155
155
  // Rule 11: State identifiers
@@ -178,7 +178,7 @@ export function preClassifyProp(prop) {
178
178
  }
179
179
  // Rule 15: Remaining booleans
180
180
  if (isBooleanType(type)) {
181
- return { category: 'design', cdfTypeHint: 'string' };
181
+ return { category: 'design', cdfTypeHint: 'boolean' };
182
182
  }
183
183
  // Rule 16: Remaining numbers
184
184
  if (isNumberType(type)) {
@@ -1,3 +1,4 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
1
2
  import type { ReviewSessionPaths, ReviewSessionSnapshot } from './types.js';
2
3
  export declare function getRefineArtifactsRoot(): string;
3
4
  export declare function getRefineSessionPaths(sessionId: string, artifactsRoot: string): Promise<ReviewSessionPaths>;
@@ -7,3 +8,15 @@ export declare function appendReviewEvent(eventsPath: string, event: {
7
8
  payload: Record<string, unknown>;
8
9
  }): Promise<void>;
9
10
  export declare function ensureRefineSession(sessionId: string, artifactsRoot: string, initialSnapshot: ReviewSessionSnapshot): Promise<ReviewSessionSnapshot>;
11
+ /**
12
+ * Persist scope-gate decisions to `current-review-state.json` so downstream
13
+ * consumers (notably `loadAcceptedNames` in `generate components`) can filter
14
+ * out rejected components. The wizard's scope-gate doesn't drive the rich
15
+ * `analyze select` TUI, so fields that file consumes (resolvedSourcePath,
16
+ * sourceCode) are populated with safe placeholders. Only `name` and `status`
17
+ * matter for `loadAcceptedNames`.
18
+ */
19
+ export declare function writeScopeDecisionsSnapshot(db: DatabaseSync, sessionId: string, decisions: {
20
+ accepted: string[];
21
+ rejected: string[];
22
+ }): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { access, appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { resolve } from 'node:path';
4
+ import { loadRawComponents } from '../../session/db.js';
4
5
  export function getRefineArtifactsRoot() {
5
6
  if (process.env.EDS_REVIEW_ARTIFACTS_DIR) {
6
7
  return resolve(process.env.EDS_REVIEW_ARTIFACTS_DIR);
@@ -40,3 +41,40 @@ export async function ensureRefineSession(sessionId, artifactsRoot, initialSnaps
40
41
  return initialSnapshot;
41
42
  }
42
43
  }
44
+ /**
45
+ * Persist scope-gate decisions to `current-review-state.json` so downstream
46
+ * consumers (notably `loadAcceptedNames` in `generate components`) can filter
47
+ * out rejected components. The wizard's scope-gate doesn't drive the rich
48
+ * `analyze select` TUI, so fields that file consumes (resolvedSourcePath,
49
+ * sourceCode) are populated with safe placeholders. Only `name` and `status`
50
+ * matter for `loadAcceptedNames`.
51
+ */
52
+ export async function writeScopeDecisionsSnapshot(db, sessionId, decisions) {
53
+ const acceptedSet = new Set(decisions.accepted);
54
+ const rawComponents = loadRawComponents(db, sessionId);
55
+ const records = rawComponents.map((c) => {
56
+ const status = acceptedSet.has(c.name) ? 'accepted' : 'rejected';
57
+ const proposal = {
58
+ name: c.name,
59
+ source: c.source,
60
+ framework: c.framework,
61
+ props: c.props,
62
+ slots: c.slots,
63
+ ...(c.extractionConfidence !== undefined ? { extractionConfidence: c.extractionConfidence } : {}),
64
+ ...(c.reviewReasons !== undefined ? { reviewReasons: c.reviewReasons } : {}),
65
+ ...(c.needsReview !== undefined ? { needsReview: c.needsReview } : {}),
66
+ };
67
+ return {
68
+ id: c.component_id,
69
+ name: c.name,
70
+ resolvedSourcePath: '',
71
+ sourceCode: null,
72
+ originalProposal: proposal,
73
+ editedProposal: proposal,
74
+ status,
75
+ };
76
+ });
77
+ const paths = await getRefineSessionPaths(sessionId, getRefineArtifactsRoot());
78
+ await mkdir(paths.sessionDir, { recursive: true });
79
+ await saveReviewState(paths.statePath, { components: records });
80
+ }
@@ -0,0 +1,26 @@
1
+ import type { ServerPreviewResponse } from '@contentful/experience-design-system-types';
2
+ import type { PreviewAnnotation } from './types.js';
3
+ /**
4
+ * Pure mapper from a `ServerPreviewResponse` to a per-component-name
5
+ * `PreviewAnnotation` map. Consumed by the wizard's final-review sidebar
6
+ * (and any other surface that wants to render diff badges next to a
7
+ * component row).
8
+ *
9
+ * The `new` bucket of `ServerPreviewResponse.components` is `CDFComponentEntry[]`
10
+ * — those entries do NOT carry a `name` field on the wire (the name is the
11
+ * KEY in the parent manifest). So instead of trying to read names off entries,
12
+ * we derive the "new" set as:
13
+ *
14
+ * new = localNames \ (unchangedNames ∪ changedNames ∪ removedNames)
15
+ *
16
+ * Callers must pass the local manifest's component keys as `localNames` so
17
+ * we can perform the set difference. Removed components from the server that
18
+ * aren't in the local manifest are still annotated as `'removed'` (so the
19
+ * detail panel can list them).
20
+ *
21
+ * Precedence: `breaking` > `changed` > `removed` > `new`.
22
+ *
23
+ * Unchanged entries are omitted from the map — callers should treat
24
+ * `map.get(name) === undefined` as "no annotation".
25
+ */
26
+ export declare function applyPreviewAnnotations(preview: ServerPreviewResponse, localNames: readonly string[]): Map<string, PreviewAnnotation>;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Pure mapper from a `ServerPreviewResponse` to a per-component-name
3
+ * `PreviewAnnotation` map. Consumed by the wizard's final-review sidebar
4
+ * (and any other surface that wants to render diff badges next to a
5
+ * component row).
6
+ *
7
+ * The `new` bucket of `ServerPreviewResponse.components` is `CDFComponentEntry[]`
8
+ * — those entries do NOT carry a `name` field on the wire (the name is the
9
+ * KEY in the parent manifest). So instead of trying to read names off entries,
10
+ * we derive the "new" set as:
11
+ *
12
+ * new = localNames \ (unchangedNames ∪ changedNames ∪ removedNames)
13
+ *
14
+ * Callers must pass the local manifest's component keys as `localNames` so
15
+ * we can perform the set difference. Removed components from the server that
16
+ * aren't in the local manifest are still annotated as `'removed'` (so the
17
+ * detail panel can list them).
18
+ *
19
+ * Precedence: `breaking` > `changed` > `removed` > `new`.
20
+ *
21
+ * Unchanged entries are omitted from the map — callers should treat
22
+ * `map.get(name) === undefined` as "no annotation".
23
+ */
24
+ export function applyPreviewAnnotations(preview, localNames) {
25
+ const out = new Map();
26
+ const unchanged = new Set(preview.components.unchanged);
27
+ // Annotate changed first; collect their names so we can exclude from "new".
28
+ const changedNames = new Set();
29
+ for (const item of preview.components.changed) {
30
+ const name = item.current?.name;
31
+ if (typeof name !== 'string')
32
+ continue;
33
+ changedNames.add(name);
34
+ if (item.changeClassification?.classification === 'breaking') {
35
+ out.set(name, 'breaking');
36
+ }
37
+ else {
38
+ out.set(name, 'changed');
39
+ }
40
+ }
41
+ // Annotate removed.
42
+ const removedNames = new Set();
43
+ for (const entity of preview.components.removed) {
44
+ if (entity.name) {
45
+ removedNames.add(entity.name);
46
+ out.set(entity.name, 'removed');
47
+ }
48
+ }
49
+ // Derive `new` = localNames \ (unchanged ∪ changed ∪ removed).
50
+ for (const name of localNames) {
51
+ if (unchanged.has(name))
52
+ continue;
53
+ if (changedNames.has(name))
54
+ continue;
55
+ if (removedNames.has(name))
56
+ continue;
57
+ if (out.has(name))
58
+ continue;
59
+ out.set(name, 'new');
60
+ }
61
+ return out;
62
+ }
@@ -363,8 +363,11 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
363
363
  const collapsed = terminalWidth < 80;
364
364
  const visibleCount = 20;
365
365
  const longestName = session.components.reduce((max, c) => Math.max(max, c.name.length), 0);
366
- // icon + space + name + 2 border chars; min 14, max 22
367
- const sidebarWidth = collapsed ? 3 : Math.min(Math.max(longestName + 4, 14), 22);
366
+ // icon + badge + space + name + 2 border chars; min 14, max 22.
367
+ // The badge column is reserved (pilot R2) even when no annotation is set —
368
+ // see Sidebar.previewBadge — so the column width is stable as live-preview
369
+ // annotations flip in/out.
370
+ const sidebarWidth = collapsed ? 3 : Math.min(Math.max(longestName + 5, 14), 22);
368
371
  const handleDraftSave = async (draft) => {
369
372
  if (!selectedId || !session || !paths)
370
373
  return;
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import type { ComponentRationale } from '../../../../session/db.js';
3
+ export type ComponentRationalePanelProps = {
4
+ data: ComponentRationale;
5
+ scrollOffset: number;
6
+ width: number;
7
+ height: number;
8
+ active: boolean;
9
+ };
10
+ type RenderedLine = {
11
+ kind: 'heading';
12
+ text: string;
13
+ } | {
14
+ kind: 'text';
15
+ text: string;
16
+ dim?: boolean;
17
+ } | {
18
+ kind: 'list-name';
19
+ text: string;
20
+ sublabel?: string;
21
+ } | {
22
+ kind: 'blank';
23
+ };
24
+ /**
25
+ * Flatten the component rationale into a list of rendered lines so scrollOffset
26
+ * slicing matches what the operator sees.
27
+ */
28
+ export declare function renderComponentRationaleLines(data: ComponentRationale, innerWidth: number): RenderedLine[];
29
+ export declare function ComponentRationalePanel({ data, scrollOffset, width, height, active, }: ComponentRationalePanelProps): React.ReactElement;
30
+ export {};
@@ -0,0 +1,137 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ const PLACEHOLDER = '(no rationale captured)';
4
+ /**
5
+ * Word-wrap a paragraph to fit `innerWidth` columns. Mirrors the helper in
6
+ * RationalePanel.tsx so wrapping is identical across both panels.
7
+ */
8
+ function wrapText(text, innerWidth) {
9
+ if (!text)
10
+ return [''];
11
+ const width = Math.max(1, innerWidth);
12
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
13
+ if (words.length === 0)
14
+ return [''];
15
+ const lines = [];
16
+ let current = '';
17
+ for (const w of words) {
18
+ if (current.length === 0) {
19
+ if (w.length > width) {
20
+ let rest = w;
21
+ while (rest.length > width) {
22
+ lines.push(rest.slice(0, width));
23
+ rest = rest.slice(width);
24
+ }
25
+ current = rest;
26
+ }
27
+ else {
28
+ current = w;
29
+ }
30
+ continue;
31
+ }
32
+ if (current.length + 1 + w.length <= width) {
33
+ current += ' ' + w;
34
+ }
35
+ else {
36
+ lines.push(current);
37
+ if (w.length > width) {
38
+ let rest = w;
39
+ while (rest.length > width) {
40
+ lines.push(rest.slice(0, width));
41
+ rest = rest.slice(width);
42
+ }
43
+ current = rest;
44
+ }
45
+ else {
46
+ current = w;
47
+ }
48
+ }
49
+ }
50
+ if (current.length > 0)
51
+ lines.push(current);
52
+ return lines.length > 0 ? lines : [''];
53
+ }
54
+ /**
55
+ * Flatten the component rationale into a list of rendered lines so scrollOffset
56
+ * slicing matches what the operator sees.
57
+ */
58
+ export function renderComponentRationaleLines(data, innerWidth) {
59
+ const out = [];
60
+ const pushSection = (heading, body) => {
61
+ out.push({ kind: 'heading', text: heading });
62
+ const text = body && body.trim().length > 0 ? body : PLACEHOLDER;
63
+ for (const ln of wrapText(text, Math.max(1, innerWidth - 2))) {
64
+ out.push({ kind: 'text', text: ' ' + ln, dim: !body });
65
+ }
66
+ out.push({ kind: 'blank' });
67
+ };
68
+ // "Description" section shows the human-facing component description.
69
+ // The `descriptionRationale` (WHY the component was classified this way)
70
+ // is appended as a small "why" line so the operator can see the LLM's
71
+ // reasoning without leaving the panel.
72
+ out.push({ kind: 'heading', text: 'Description' });
73
+ const descBody = data.description && data.description.trim().length > 0 ? data.description : PLACEHOLDER;
74
+ for (const ln of wrapText(descBody, Math.max(1, innerWidth - 2))) {
75
+ out.push({ kind: 'text', text: ' ' + ln, dim: !data.description });
76
+ }
77
+ if (data.descriptionRationale && data.descriptionRationale.trim().length > 0) {
78
+ for (const ln of wrapText(`why: ${data.descriptionRationale}`, Math.max(1, innerWidth - 2))) {
79
+ out.push({ kind: 'text', text: ' ' + ln, dim: true });
80
+ }
81
+ }
82
+ out.push({ kind: 'blank' });
83
+ pushSection('Why these props', data.propsRationale);
84
+ pushSection('Why these slots', data.slotsRationale);
85
+ out.push({ kind: 'heading', text: 'Props' });
86
+ if (data.props.length === 0) {
87
+ out.push({ kind: 'text', text: ' ' + PLACEHOLDER, dim: true });
88
+ }
89
+ else {
90
+ for (const p of data.props) {
91
+ const sub = p.category ? `(${p.category})` : undefined;
92
+ out.push({ kind: 'list-name', text: p.name, sublabel: sub });
93
+ const text = p.rationale && p.rationale.trim().length > 0 ? p.rationale : PLACEHOLDER;
94
+ for (const ln of wrapText(text, Math.max(1, innerWidth - 4))) {
95
+ out.push({ kind: 'text', text: ' ' + ln, dim: !p.rationale });
96
+ }
97
+ }
98
+ }
99
+ out.push({ kind: 'blank' });
100
+ out.push({ kind: 'heading', text: 'Slots' });
101
+ if (data.slots.length === 0) {
102
+ out.push({ kind: 'text', text: ' ' + PLACEHOLDER, dim: true });
103
+ }
104
+ else {
105
+ for (const s of data.slots) {
106
+ out.push({ kind: 'list-name', text: s.name });
107
+ const text = s.rationale && s.rationale.trim().length > 0 ? s.rationale : PLACEHOLDER;
108
+ for (const ln of wrapText(text, Math.max(1, innerWidth - 4))) {
109
+ out.push({ kind: 'text', text: ' ' + ln, dim: !s.rationale });
110
+ }
111
+ }
112
+ }
113
+ return out;
114
+ }
115
+ export function ComponentRationalePanel({ data, scrollOffset, width, height, active, }) {
116
+ const innerWidth = Math.max(1, width - 2);
117
+ const all = renderComponentRationaleLines(data, innerWidth);
118
+ const totalLines = all.length;
119
+ // Reserve one line at the bottom for the legend.
120
+ const contentHeight = Math.max(1, height - 1);
121
+ const visible = all.slice(scrollOffset, scrollOffset + contentHeight);
122
+ const overflowed = totalLines > contentHeight;
123
+ const visibleStart = totalLines === 0 ? 0 : scrollOffset + 1;
124
+ const visibleEnd = Math.min(totalLines, scrollOffset + contentHeight);
125
+ return (_jsxs(Box, { flexDirection: "column", width: width, height: height + 2, borderStyle: "single", borderColor: active ? 'white' : undefined, children: [_jsx(Box, { children: _jsx(Text, { bold: true, dimColor: !active, children: `Component rationale: ${data.name}` }) }), visible.map((line, i) => {
126
+ if (line.kind === 'blank') {
127
+ return (_jsx(Box, { children: _jsx(Text, { children: " " }) }, i));
128
+ }
129
+ if (line.kind === 'heading') {
130
+ return (_jsx(Box, { children: _jsx(Text, { bold: true, color: "cyan", dimColor: !active, children: line.text }) }, i));
131
+ }
132
+ if (line.kind === 'list-name') {
133
+ return (_jsxs(Box, { children: [_jsx(Text, { children: ' - ' }), _jsx(Text, { bold: true, dimColor: !active, children: line.text }), line.sublabel ? _jsx(Text, { dimColor: true, children: ' ' + line.sublabel }) : null] }, i));
134
+ }
135
+ return (_jsx(Box, { children: _jsx(Text, { dimColor: !active || line.dim, children: line.text }) }, i));
136
+ }), _jsx(Box, { children: overflowed ? (_jsx(Text, { dimColor: true, children: `${visibleStart}-${visibleEnd}/${totalLines} [j/k] scroll [I/Esc] close` })) : (_jsx(Text, { dimColor: true, children: '[I/Esc] close' })) })] }));
137
+ }
@@ -1,11 +1,68 @@
1
1
  import React from 'react';
2
+ /** Per-prop / per-component metadata captured by the extractor + generate phase.
3
+ * Optional; when omitted, the FieldEditor renders without rationale/source
4
+ * affordances (backwards-compatible with mounts that pre-date Feature 1). */
5
+ export type PropMetadata = {
6
+ rationale?: string | null;
7
+ sourceStartLine?: number | null;
8
+ sourceEndLine?: number | null;
9
+ };
10
+ export type FieldEditorMetadata = {
11
+ /** Absolute path to the component's source file. Null/undefined = unknown. */
12
+ sourcePath?: string | null;
13
+ /** Full source text of the component (used by the source-view panel). */
14
+ componentSource?: string | null;
15
+ /** Per-prop metadata keyed by prop name. */
16
+ props?: Record<string, PropMetadata>;
17
+ };
2
18
  type FieldEditorProps = {
3
19
  value: string;
4
20
  width: number;
5
21
  height: number;
22
+ /**
23
+ * When false, the editor is mounted but does not consume keystrokes.
24
+ * Used by callers (e.g. GenerateReviewStep) that toggle focus between
25
+ * a sidebar and this editor — the editor stays visible while the sidebar
26
+ * has the keyboard.
27
+ */
28
+ active?: boolean;
6
29
  onChange: (value: string) => void;
7
30
  onSave: () => void;
8
31
  onDiscard: () => void;
32
+ /**
33
+ * Called when the user requests to exit the panel from row-level (Esc).
34
+ * Distinct from onDiscard, which drops pending edits without changing focus.
35
+ * Callers that embed FieldEditor inside a sidebar+panel layout wire this to
36
+ * return focus to the sidebar (e.g. setSidebarFocused(true)). When omitted,
37
+ * row-level Esc falls back to onDiscard for backward compatibility.
38
+ */
39
+ onExit?: () => void;
40
+ /** Feature 1: source code + LLM rationale metadata. Optional. */
41
+ metadata?: FieldEditorMetadata;
42
+ /**
43
+ * When provided, the parent owns the prop-rationale panel: pressing `i`
44
+ * inside FieldEditor calls this callback (subject to text-entry gating)
45
+ * instead of toggling internal state. The panel is rendered by the parent
46
+ * in place of FieldEditor — FieldEditor no longer renders its own panel.
47
+ */
48
+ onTogglePropRationale?: () => void;
49
+ /**
50
+ * When provided, the parent owns the component-rationale panel: pressing
51
+ * `I` (uppercase) calls this callback (subject to text-entry gating).
52
+ */
53
+ onToggleComponentRationale?: () => void;
54
+ /**
55
+ * When provided, the parent owns the source-view panel: pressing `s`
56
+ * calls this callback (subject to text-entry gating). FieldEditor will not
57
+ * render its own source panel in that case.
58
+ */
59
+ onToggleSourceExternal?: () => void;
60
+ /**
61
+ * Reports text-entry-active state changes to the parent so the parent can
62
+ * gate top-level keybinds (i/I/s) against literal keystrokes inside
63
+ * description editors, string default editors, and value-list text entry.
64
+ */
65
+ onTextEntryActiveChange?: (active: boolean) => void;
9
66
  };
10
- export declare function FieldEditor({ value, width, height, onChange, onSave, onDiscard, }: FieldEditorProps): React.ReactElement;
67
+ export declare function FieldEditor({ value, width, height, active, onChange, onSave, onDiscard, onExit, metadata, onTogglePropRationale, onToggleComponentRationale, onToggleSourceExternal, onTextEntryActiveChange, }: FieldEditorProps): React.ReactElement;
11
68
  export {};