@growthub/cli 0.13.9 → 0.14.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 (25) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/env-status/route.js +31 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +130 -5
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -1
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +5 -2
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryCreationCockpit.jsx +200 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +396 -5
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +75 -55
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ReferencePicker.jsx +2 -2
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +100 -6
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +176 -5
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +2 -4
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-registry-creation-flow.js +317 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-response-profile.js +207 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/creation-error-recovery.js +103 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +100 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +63 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +215 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-resolver-write.js +67 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-upgrade.js +89 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +11 -4
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +8 -1
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +7 -1
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-resolver-proposal.js +200 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -1
  25. package/package.json +1 -1
@@ -0,0 +1,31 @@
1
+ /**
2
+ * GET /api/workspace/env-status
3
+ *
4
+ * The honest, secret-safe auth-readiness signal the creation cockpit needs.
5
+ * Returns the referenced auth/env ref SLUGS that currently resolve to a value
6
+ * in the server runtime (process.env) — so the api-registry drawer can mark
7
+ * "auth configured" from real runtime truth instead of guessing. Never returns,
8
+ * logs, or hashes a value.
9
+ */
10
+
11
+ import { NextResponse } from "next/server";
12
+ import { readWorkspaceConfig } from "@/lib/workspace-config";
13
+ import { computeConfiguredEnvRefs, listPersistenceAdapterReadiness } from "@/lib/env-status";
14
+
15
+ async function GET() {
16
+ let workspaceConfig = {};
17
+ try {
18
+ workspaceConfig = await readWorkspaceConfig();
19
+ } catch {
20
+ workspaceConfig = {};
21
+ }
22
+ const configuredEnvRefs = computeConfiguredEnvRefs(workspaceConfig, process.env);
23
+ const persistenceAdapters = listPersistenceAdapterReadiness(process.env);
24
+ return NextResponse.json({
25
+ kind: "growthub-env-status-v1",
26
+ configuredEnvRefs,
27
+ persistenceAdapters,
28
+ });
29
+ }
30
+
31
+ export { GET };
@@ -41,9 +41,98 @@ import {
41
41
  buildApplyReceipt,
42
42
  upsertHelperThreadRow,
43
43
  } from "@/lib/workspace-helper-apply";
44
+ import { RESOLVER_PROPOSAL_TYPE, buildResolverProposal, validateResolverProposal } from "@/lib/workspace-resolver-proposal";
45
+ import { writeResolverProposalFile } from "@/lib/server-resolver-write";
44
46
 
45
47
  const HELPER_APPLY_SOURCE_KEY = "helper:apply:receipts";
46
48
 
49
+ function findRegistryRow(config, integrationId) {
50
+ const id = String(integrationId || "").trim();
51
+ if (!id) return null;
52
+ const objects = Array.isArray(config?.dataModel?.objects) ? config.dataModel.objects : [];
53
+ for (const object of objects) {
54
+ if (object?.objectType !== "api-registry") continue;
55
+ const row = (Array.isArray(object.rows) ? object.rows : []).find((candidate) =>
56
+ String(candidate?.integrationId || "").trim() === id
57
+ );
58
+ if (row) return row;
59
+ }
60
+ return null;
61
+ }
62
+
63
+ function findDataSourceForRegistry(config, integrationId) {
64
+ const id = String(integrationId || "").trim();
65
+ if (!id) return null;
66
+ const objects = Array.isArray(config?.dataModel?.objects) ? config.dataModel.objects : [];
67
+ for (const object of objects) {
68
+ if (object?.objectType !== "data-source") continue;
69
+ const row = (Array.isArray(object.rows) ? object.rows : []).find((candidate) =>
70
+ String(candidate?.registryId || "").trim() === id
71
+ );
72
+ if (row) return { object, row };
73
+ }
74
+ return null;
75
+ }
76
+
77
+ function inferRootPathFromLastResponse(lastResponse) {
78
+ if (!lastResponse) return "";
79
+ let parsed = lastResponse;
80
+ if (typeof lastResponse === "string") {
81
+ try { parsed = JSON.parse(lastResponse); } catch { return ""; }
82
+ }
83
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "";
84
+ for (const key of ["records", "items", "data", "results", "capabilities"]) {
85
+ if (Array.isArray(parsed[key])) return key;
86
+ }
87
+ return "";
88
+ }
89
+
90
+ function normalizeResolverProposal(proposal, config) {
91
+ if (proposal?.type !== RESOLVER_PROPOSAL_TYPE || String(proposal?.code || "").trim()) return proposal;
92
+ const integrationId = proposal?.payload?.integrationId;
93
+ const registryRow = findRegistryRow(config, integrationId);
94
+ if (!registryRow) return proposal;
95
+ const generated = buildResolverProposal({
96
+ integrationId: registryRow.integrationId,
97
+ baseUrl: registryRow.baseUrl,
98
+ endpoint: registryRow.endpoint,
99
+ method: registryRow.method,
100
+ authRef: registryRow.authRef,
101
+ rootPath: proposal?.payload?.rootPath || inferRootPathFromLastResponse(registryRow.lastResponse),
102
+ entityType: proposal?.payload?.entityType || registryRow.entityTypes || "records",
103
+ });
104
+ return {
105
+ ...generated,
106
+ rationale: proposal.rationale || generated.rationale,
107
+ confidence: proposal.confidence || generated.confidence,
108
+ };
109
+ }
110
+
111
+ function normalizeDataModelObjectProposal(proposal, config, fallbackIntegrationId = "") {
112
+ if (proposal?.type !== "dataModel.object.update" || proposal?.payload?.id) return proposal;
113
+ const integrationId = proposal?.payload?.registryId || proposal?.payload?.integrationId || fallbackIntegrationId;
114
+ const target = findDataSourceForRegistry(config, integrationId);
115
+ if (!target?.object) return proposal;
116
+ return {
117
+ ...proposal,
118
+ payload: {
119
+ ...proposal.payload,
120
+ id: target.object.id,
121
+ sourceId: target.object.sourceId || target.row?.sourceId || target.object.binding?.sourceId || "",
122
+ binding: {
123
+ ...(target.object.binding || {}),
124
+ sourceStorage: target.object.binding?.sourceStorage || target.row?.sourceStorage || "workspace-source-records",
125
+ sourceId: target.object.binding?.sourceId || target.row?.sourceId || target.object.sourceId || "",
126
+ registryId: target.object.binding?.registryId || integrationId,
127
+ },
128
+ },
129
+ };
130
+ }
131
+
132
+ function normalizeApplyProposal(proposal, config, context = {}) {
133
+ return normalizeDataModelObjectProposal(normalizeResolverProposal(proposal, config), config, context.integrationId);
134
+ }
135
+
47
136
  async function POST(request) {
48
137
  let body;
49
138
  try {
@@ -78,7 +167,41 @@ async function POST(request) {
78
167
  const skipped = [];
79
168
  let workingConfig = currentConfig;
80
169
 
81
- for (const proposal of body.proposals) {
170
+ // Resolver-file lane (AWaC: server file, NOT a config PATCH field). Handled
171
+ // separately so it never touches writeWorkspaceConfig and never widens the
172
+ // PATCH allowlist. Gated by filesystem/read-only; emits a receipt either way.
173
+ const fallbackIntegrationId = body.proposals
174
+ .map((proposal) => proposal?.payload?.integrationId || proposal?.payload?.registryId)
175
+ .find((value) => String(value || "").trim());
176
+ const normalizedProposals = body.proposals.map((proposal) =>
177
+ normalizeApplyProposal(proposal, currentConfig, { integrationId: fallbackIntegrationId })
178
+ );
179
+ const resolverProposals = normalizedProposals.filter((p) => p?.type === RESOLVER_PROPOSAL_TYPE);
180
+ const configProposals = normalizedProposals.filter((p) => p?.type !== RESOLVER_PROPOSAL_TYPE);
181
+
182
+ for (const proposal of resolverProposals) {
183
+ const validation = validateResolverProposal(proposal);
184
+ if (!validation.ok) {
185
+ skipped.push({ proposal, reason: validation.error || "invalid resolver proposal" });
186
+ continue;
187
+ }
188
+ try {
189
+ const result = await writeResolverProposalFile(proposal);
190
+ applied.push({
191
+ ...buildApplyReceipt(proposal, appliedAt, reviewedBy, sessionId),
192
+ resolverPath: result.path,
193
+ resolverFilename: result.filename,
194
+ });
195
+ } catch (err) {
196
+ if (err?.code === "WORKSPACE_PERSISTENCE_READ_ONLY") {
197
+ skipped.push({ proposal, reason: `read-only runtime — ${err.guidance || "resolver not written"}` });
198
+ } else {
199
+ skipped.push({ proposal, reason: err?.message || "resolver write failed" });
200
+ }
201
+ }
202
+ }
203
+
204
+ for (const proposal of configProposals) {
82
205
  if (
83
206
  !proposal ||
84
207
  typeof proposal.type !== "string" ||
@@ -113,7 +236,9 @@ async function POST(request) {
113
236
  // Patch — collect every affected field from accepted proposals AND
114
237
  // append the thread row update (so the user-visible Helper Threads object
115
238
  // refreshes in the same atomic write as the proposed mutations).
116
- const mutatingApplied = applied.filter((r) => r.type !== "explain.object");
239
+ // resolver.create writes a server file (affectedField "server-file"), so it
240
+ // must NOT contribute a field to the config PATCH — exclude it here.
241
+ const mutatingApplied = applied.filter((r) => r.type !== "explain.object" && r.affectedField !== "server-file");
117
242
 
118
243
  // Upsert the thread row so audit history reflects this apply turn even
119
244
  // when nothing mutated (all skipped / explain-only) and even when the
@@ -129,7 +254,7 @@ async function POST(request) {
129
254
  try {
130
255
  const existingRows = (workingConfig?.dataModel?.objects || []).find((o) => o?.id === "helper-threads")?.rows || [];
131
256
  const existingRow = existingRows.find((r) => r?.id === threadId) || {};
132
- const firstProposal = body.proposals?.[0];
257
+ const firstProposal = normalizedProposals?.[0];
133
258
  const seedTitle = existingRow.title
134
259
  || (firstProposal?.rationale ? String(firstProposal.rationale).slice(0, 72) : "Helper thread");
135
260
 
@@ -192,7 +317,7 @@ async function POST(request) {
192
317
  intent: safeIntent,
193
318
  prompt: existingRow.prompt || "",
194
319
  summary: existingRow.summary || "",
195
- proposals: existingRow.proposals || body.proposals || [],
320
+ proposals: existingRow.proposals || normalizedProposals || [],
196
321
  warnings: existingRow.warnings || [],
197
322
  receipts: existingRow.receipts || null,
198
323
  model: existingRow.model || "external-apply",
@@ -207,7 +332,7 @@ async function POST(request) {
207
332
  affectedField: a.affectedField,
208
333
  rationale: a.rationale,
209
334
  confidence: a.confidence,
210
- payload: body.proposals?.[idx]?.payload ?? null,
335
+ payload: normalizedProposals?.[idx]?.payload ?? null,
211
336
  })),
212
337
  lastSkipped: skipped.map((s) => ({
213
338
  type: s.proposal?.type,
@@ -51,6 +51,7 @@ export function WorkspaceActivationPanel({
51
51
  workspaceSourceRecords,
52
52
  metadataGraph,
53
53
  onOpenHelper,
54
+ onStepAction,
54
55
  compact = false,
55
56
  showLenses = false,
56
57
  }) {
@@ -136,7 +137,22 @@ export function WorkspaceActivationPanel({
136
137
  <span>{step.hint}</span>
137
138
  </p>
138
139
  ) : null}
139
- {step.href ? (
140
+ {step.action && onStepAction ? (
141
+ <a
142
+ href={`#${step.action}`}
143
+ className={
144
+ "workspace-activation-step-cta"
145
+ + (isNext ? " is-primary" : "")
146
+ }
147
+ onClick={(event) => {
148
+ event.preventDefault();
149
+ onStepAction(step);
150
+ }}
151
+ >
152
+ <span>{step.cta || (step.status === "complete" ? "Review" : "Open")}</span>
153
+ <ArrowRight size={12} aria-hidden="true" />
154
+ </a>
155
+ ) : step.href ? (
140
156
  <Link
141
157
  href={step.href}
142
158
  className={
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useEffect, useState } from "react";
4
+ import { createPortal } from "react-dom";
4
5
  import { X } from "lucide-react";
5
6
 
6
7
  const HELPER_SANDBOX_OBJECT_ID = "workspace-helper-sandbox";
@@ -185,7 +186,9 @@ function WorkspaceHelperSetupModal({ workspaceConfig, open, onClose, onSaved })
185
186
  }
186
187
  }
187
188
 
188
- return (
189
+ if (typeof document === "undefined") return null;
190
+
191
+ return createPortal((
189
192
  <div className="workspace-helper-setup-modal-backdrop" role="presentation">
190
193
  <div className="workspace-helper-setup-modal" role="dialog" aria-modal="true" aria-label="Set up workspace helper">
191
194
  <button type="button" className="workspace-helper-setup-modal-close" onClick={onClose} aria-label="Close setup">
@@ -342,7 +345,7 @@ function WorkspaceHelperSetupModal({ workspaceConfig, open, onClose, onSaved })
342
345
  </div>
343
346
  </div>
344
347
  </div>
345
- );
348
+ ), document.body);
346
349
  }
347
350
 
348
351
  export {
@@ -0,0 +1,200 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+
5
+ /**
6
+ * ApiRegistryCreationCockpit — the governed creation interface for one API,
7
+ * rendered inside the existing api-registry record drawer (DataModelShell).
8
+ *
9
+ * Renders the ordered journey from `deriveApiRegistryCreationState`
10
+ * (lib/api-registry-creation-flow.js) plus, once the API is tested, the response
11
+ * Shape analysis (`profileApiResponse` / `recommendResolver`) and a receipts log
12
+ * of real actions. Every actionable step's button calls back into the drawer's
13
+ * existing governed handlers via `onAction(action)` — status, the highlighted
14
+ * "next" step, the activation score, and which buttons are live all come from
15
+ * derivation/real responses, never guessed.
16
+ *
17
+ * Visual language is the workspace's own: the `.dm-db-status` chip and `dm-btn-*`
18
+ * buttons. No invented colors, no new primitive.
19
+ */
20
+
21
+ const STEP_STATUS = {
22
+ complete: { mod: "ok", label: "Done" },
23
+ active: { mod: "warn", label: "Next" },
24
+ pending: { mod: "", label: "Pending" },
25
+ blocked: { mod: "", label: "Blocked" },
26
+ optional: { mod: "", label: "Optional" },
27
+ };
28
+
29
+ const RESOLVER_LEVEL = {
30
+ optional: { mod: "", label: "Resolver optional" },
31
+ recommended: { mod: "warn", label: "Resolver recommended" },
32
+ required: { mod: "bad", label: "Resolver required" },
33
+ };
34
+
35
+ function StatusChip({ mod, children, className = "" }) {
36
+ return (
37
+ <span className={`dm-db-status${mod ? ` ${mod}` : ""}${className ? ` ${className}` : ""}`}>
38
+ <span />
39
+ {children}
40
+ </span>
41
+ );
42
+ }
43
+
44
+ export function ApiRegistryCreationCockpit({
45
+ state,
46
+ onAction,
47
+ busyAction = "",
48
+ disabled = false,
49
+ profile = null,
50
+ resolverRec = null,
51
+ receipts = [],
52
+ dataSourcePreview = null,
53
+ eyebrow = "Governed creation",
54
+ defaultCollapsed = false,
55
+ hideWhenComplete = false,
56
+ onCollapsedChange,
57
+ }) {
58
+ const [collapsed, setCollapsed] = useState(defaultCollapsed);
59
+ useEffect(() => {
60
+ setCollapsed(defaultCollapsed || Boolean(hideWhenComplete && state?.complete));
61
+ }, [defaultCollapsed, hideWhenComplete, state?.complete, state?.integrationId]);
62
+ if (!state || !Array.isArray(state.steps)) return null;
63
+ if (hideWhenComplete && state.complete) return null;
64
+ const candidates = profile?.candidates || {};
65
+ const candidateEntries = Object.entries(candidates).filter(([, v]) => v);
66
+ const previewRow = dataSourcePreview?.row || null;
67
+ const toggleCollapsed = () => {
68
+ const next = !collapsed;
69
+ setCollapsed(next);
70
+ onCollapsedChange?.(next);
71
+ };
72
+ const runAction = (action) => {
73
+ if (action?.id === "edit") {
74
+ setCollapsed(true);
75
+ onCollapsedChange?.(true);
76
+ }
77
+ onAction?.(action);
78
+ };
79
+
80
+ return (
81
+ <section className={`dm-api-action-card dm-cockpit${collapsed ? " is-collapsed" : ""}`} aria-label="API creation journey">
82
+ <button
83
+ type="button"
84
+ className="dm-cockpit-head"
85
+ aria-expanded={!collapsed}
86
+ onClick={toggleCollapsed}
87
+ >
88
+ <div className="dm-api-action-card-body">
89
+ <p className="dm-api-action-card-eyebrow">{eyebrow} · {state.score}% activated</p>
90
+ <h3>{state.headline}</h3>
91
+ </div>
92
+ <span className="dm-cockpit-count">{state.completedCount}/{state.totalCount}</span>
93
+ </button>
94
+
95
+ {!collapsed && <ol className="dm-cockpit-steps">
96
+ {state.steps.map((step) => {
97
+ const meta = STEP_STATUS[step.status] || STEP_STATUS.pending;
98
+ const isNext = step.id === state.nextStepId;
99
+ const action = step.action;
100
+ const isBusy = action && busyAction === `${step.id}:${action.id}`;
101
+ return (
102
+ <li
103
+ key={step.id}
104
+ className={`dm-cockpit-step${isNext ? " dm-cockpit-step-next" : ""}${step.status === "blocked" ? " dm-cockpit-step-muted" : ""}`}
105
+ >
106
+ <StatusChip mod={meta.mod} className="dm-cockpit-step-chip">{meta.label}</StatusChip>
107
+ <div className="dm-cockpit-step-body">
108
+ <p className="dm-cockpit-step-label">{step.label}</p>
109
+ <p className="dm-cockpit-step-desc">{step.description}</p>
110
+ {step.hint ? <p className="dm-cockpit-step-hint">{step.hint}</p> : null}
111
+ </div>
112
+ {action ? (
113
+ <button
114
+ type="button"
115
+ className={isNext ? "dm-btn-primary-sm" : "dm-btn-outline"}
116
+ disabled={disabled || Boolean(busyAction)}
117
+ onClick={() => runAction(action)}
118
+ >
119
+ {isBusy ? "Working…" : action.label}
120
+ </button>
121
+ ) : null}
122
+ </li>
123
+ );
124
+ })}
125
+ </ol>}
126
+
127
+ {!collapsed && profile && profile.parsed ? (
128
+ <div className="dm-cockpit-shape">
129
+ <div className="dm-cockpit-shape-head">
130
+ <p className="dm-api-action-card-eyebrow">Response shape</p>
131
+ {resolverRec ? (
132
+ <StatusChip mod={(RESOLVER_LEVEL[resolverRec.level] || RESOLVER_LEVEL.optional).mod}>
133
+ {(RESOLVER_LEVEL[resolverRec.level] || RESOLVER_LEVEL.optional).label}
134
+ </StatusChip>
135
+ ) : null}
136
+ </div>
137
+ <p className="dm-cockpit-step-desc">
138
+ {profile.usable
139
+ ? `${profile.recordCount} record${profile.recordCount === 1 ? "" : "s"}${profile.arrayPath ? ` at "${profile.arrayPath}"` : " (top-level)"} · entity "${profile.suggestedEntityType}".`
140
+ : "No record array detected in the response."}
141
+ </p>
142
+ {resolverRec ? <p className="dm-cockpit-step-hint">{resolverRec.reason}</p> : null}
143
+ {candidateEntries.length ? (
144
+ <div className="dm-cockpit-fields">
145
+ {candidateEntries.map(([role, name]) => (
146
+ <span key={role} className="dm-cockpit-field"><b>{role}</b>{name}</span>
147
+ ))}
148
+ </div>
149
+ ) : null}
150
+ {profile.hasPagination ? (
151
+ <p className="dm-cockpit-step-hint">Pagination keys present — a resolver is needed to fetch every page.</p>
152
+ ) : null}
153
+ </div>
154
+ ) : null}
155
+
156
+ {!collapsed && previewRow ? (
157
+ <div className="dm-cockpit-shape">
158
+ <p className="dm-api-action-card-eyebrow">Data Source preview</p>
159
+ <p className="dm-cockpit-step-desc">
160
+ Create will add a live-backed Data Source object that references this API by <code>registryId</code> and writes records to the source-records sidecar. Nothing fetches until you Refresh.
161
+ </p>
162
+ <div className="dm-cockpit-fields">
163
+ <span className="dm-cockpit-field"><b>name</b>{previewRow.Name}</span>
164
+ <span className="dm-cockpit-field"><b>sourceId</b>{previewRow.sourceId}</span>
165
+ <span className="dm-cockpit-field"><b>storage</b>{previewRow.sourceStorage}</span>
166
+ <span className="dm-cockpit-field"><b>entity</b>{previewRow.entityType}</span>
167
+ <span className="dm-cockpit-field"><b>registryId</b>{previewRow.registryId}</span>
168
+ {previewRow.authRef ? <span className="dm-cockpit-field"><b>authRef</b>{previewRow.authRef}</span> : null}
169
+ </div>
170
+ {Array.isArray(dataSourcePreview.fields) && dataSourcePreview.fields.length ? (
171
+ <>
172
+ <p className="dm-cockpit-step-hint">Detected fields it will carry:</p>
173
+ <div className="dm-cockpit-fields">
174
+ {dataSourcePreview.fields.slice(0, 10).map((f) => (
175
+ <span key={f.name} className="dm-cockpit-field"><b>{f.role || f.type}</b>{f.name}</span>
176
+ ))}
177
+ </div>
178
+ </>
179
+ ) : null}
180
+ </div>
181
+ ) : null}
182
+
183
+ {!collapsed && Array.isArray(receipts) && receipts.length ? (
184
+ <div className="dm-cockpit-receipts">
185
+ <p className="dm-api-action-card-eyebrow">Receipts</p>
186
+ <ul>
187
+ {receipts.slice(0, 6).map((r, i) => (
188
+ <li key={`${r.at}-${i}`} className="dm-cockpit-receipt">
189
+ <StatusChip mod={r.ok ? "ok" : "bad"} className="dm-cockpit-receipt-chip">{r.kind}</StatusChip>
190
+ <span className="dm-cockpit-receipt-text">{r.detail}</span>
191
+ </li>
192
+ ))}
193
+ </ul>
194
+ </div>
195
+ ) : null}
196
+ </section>
197
+ );
198
+ }
199
+
200
+ export default ApiRegistryCreationCockpit;