@growthub/cli 0.10.1 → 0.12.1

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 (23) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +307 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +372 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/receipts/route.js +47 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +402 -49
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +1371 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +1348 -21
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +7 -21
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/ownership/ownership-panel.jsx +222 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/ownership/page.jsx +19 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/settings-shell.jsx +2 -1
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +116 -24
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +497 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/growthub.config.json +20 -4
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +19 -4
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +15 -4
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +473 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +583 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +34 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +3 -1
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +9 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/workers/custom-workspace-operator/CLAUDE.md +38 -0
  22. package/dist/index.js +2935 -2073
  23. package/package.json +1 -1
@@ -0,0 +1,47 @@
1
+ /**
2
+ * GET /api/workspace/helper/receipts
3
+ *
4
+ * Returns the last N workspace helper apply receipts.
5
+ * Used by the review UI to display accepted proposal history and seed
6
+ * the fine-tune feedback loop.
7
+ *
8
+ * Query params:
9
+ * limit — max records to return (default 25, max 100)
10
+ * type — filter by proposal type (optional)
11
+ */
12
+
13
+ import { NextResponse } from "next/server";
14
+ import { readWorkspaceSourceRecords } from "@/lib/workspace-config";
15
+
16
+ const HELPER_APPLY_SOURCE_KEY = "helper:apply:receipts";
17
+
18
+ async function GET(request) {
19
+ const { searchParams } = new URL(request.url);
20
+ const limitRaw = parseInt(searchParams.get("limit") || "25", 10);
21
+ const limit = Number.isFinite(limitRaw) ? Math.min(Math.max(limitRaw, 1), 100) : 25;
22
+ const typeFilter = searchParams.get("type") || "";
23
+
24
+ let records = [];
25
+ try {
26
+ const existing = await readWorkspaceSourceRecords(HELPER_APPLY_SOURCE_KEY);
27
+ records = Array.isArray(existing?.records) ? existing.records : [];
28
+ } catch {
29
+ records = [];
30
+ }
31
+
32
+ if (typeFilter) {
33
+ records = records.filter((r) => r.type === typeFilter);
34
+ }
35
+
36
+ const page = records.slice(-limit).reverse();
37
+
38
+ return NextResponse.json({
39
+ ok: true,
40
+ totalCount: records.length,
41
+ recordCount: page.length,
42
+ receipts: page,
43
+ records: page,
44
+ });
45
+ }
46
+
47
+ export { GET };
@@ -43,6 +43,9 @@ import {
43
43
  Zap,
44
44
  } from "lucide-react";
45
45
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
46
+ import { HelperSidecar } from "./HelperSidecar.jsx";
47
+ import { WorkspaceRail } from "../../workspace-rail.jsx";
48
+ import { useRouter, useSearchParams } from "next/navigation";
46
49
  import {
47
50
  OBJECT_TYPE_PRESETS,
48
51
  addTableField,
@@ -302,8 +305,8 @@ function ObjectViewPicker({ tables, selectedTable, saving, onSelectSource, onSav
302
305
  {favoriteObjects.length > 0 && (
303
306
  <div className="dm-picker-section">
304
307
  <p>Favorites</p>
305
- {favoriteObjects.map((table) => (
306
- <button key={`favorite-${table.source}`} type="button" className="dm-picker-row" onClick={() => onSelectSource(table.source)}>
308
+ {favoriteObjects.map((table, favIdx) => (
309
+ <button key={`favorite-${table.id || table.source}-${favIdx}`} type="button" className="dm-picker-row" onClick={() => onSelectSource(table.source)}>
307
310
  <Pin size={14} />
308
311
  <span>{table.label}</span>
309
312
  </button>
@@ -325,8 +328,8 @@ function ObjectViewPicker({ tables, selectedTable, saving, onSelectSource, onSav
325
328
  <div className="dm-picker-section">
326
329
  <p>Objects</p>
327
330
  <div className="dm-picker-scroll">
328
- {objects.map((table) => (
329
- <div key={table.source} className={`dm-picker-item${selectedTable?.source === table.source ? " active" : ""}`}>
331
+ {objects.map((table, objIdx) => (
332
+ <div key={`${table.id || table.source}:${objIdx}`} className={`dm-picker-item${selectedTable?.source === table.source ? " active" : ""}`}>
330
333
  <button type="button" className="dm-picker-row" onClick={() => {
331
334
  onSelectSource(table.source);
332
335
  setOpen(false);
@@ -418,36 +421,9 @@ function SaveToast({ saving, message }) {
418
421
  return <span className={`dm-toast ${message.startsWith("Error") ? "error" : "ok"}`}>{message}</span>;
419
422
  }
420
423
 
421
- function NavRail({ authority, workspaceConfig }) {
422
- const branding = workspaceConfig?.branding || {};
423
- const workspaceName = branding.name || workspaceConfig?.name || "Growthub Workspace";
424
- return (
425
- <aside className="workspace-rail" aria-label="Workspace navigation">
426
- <div className="workspace-brand">
427
- <span
428
- className="workspace-mark"
429
- style={{
430
- background: branding.logoUrl ? undefined : branding.accent || undefined,
431
- color: branding.logoUrl ? undefined : textColorForAccent(branding.accent),
432
- }}
433
- >
434
- {branding.logoUrl ? <img src={branding.logoUrl} alt="" /> : workspaceName.slice(0, 1).toUpperCase()}
435
- </span>
436
- <span>{workspaceName}</span>
437
- </div>
438
- <nav className="workspace-nav">
439
- <Link href="/">Dashboards</Link>
440
- <Link className="active" href="/data-model">Data Model</Link>
441
- <span className="workspace-nav-static">Management</span>
442
- <Link className="workspace-nav-bottom" href="/settings/general">Workspace Settings</Link>
443
- </nav>
444
- <div className="workspace-rail-status">
445
- <span className="status-dot" />
446
- {authority || "local-catalog"}
447
- </div>
448
- </aside>
449
- );
450
- }
424
+ // NavRail extracted to `app/workspace-rail.jsx` (shared across all
425
+ // governed-workspace pages). The legacy local definition has been
426
+ // removed every surface now renders <WorkspaceRail />.
451
427
 
452
428
  // ─── Object list (sidebar lives in ./ObjectSidebar.jsx) ───────────────────────
453
429
 
@@ -1393,7 +1369,7 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
1393
1369
  );
1394
1370
  }
1395
1371
 
1396
- function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave }) {
1372
+ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave, onOpenThread }) {
1397
1373
  const [selectedRow, setSelectedRow] = useState(null);
1398
1374
  const [fieldName, setFieldName] = useState("");
1399
1375
  const [fieldType, setFieldType] = useState("text");
@@ -1853,9 +1829,27 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave
1853
1829
  </td>
1854
1830
  {visibleColumns.map((column) => {
1855
1831
  const relation = relationForColumn(table, column);
1832
+ // The Helper Threads object is a normal custom-typed
1833
+ // governed object. We opt the "open" column into a
1834
+ // Reopen link based on the stable well-known object id
1835
+ // so we don't need a dedicated object type.
1836
+ const isHelperThreadOpenCol = table.objectId === "helper-threads" && column === "open";
1856
1837
  return (
1857
1838
  <td key={column}>
1858
- {relation ? (
1839
+ {isHelperThreadOpenCol ? (
1840
+ <button
1841
+ type="button"
1842
+ className="dm-thread-open-link"
1843
+ data-helper-thread-open=""
1844
+ data-thread-id={row?.id || ""}
1845
+ onClick={(event) => {
1846
+ event.stopPropagation();
1847
+ if (typeof onOpenThread === "function") onOpenThread(row);
1848
+ }}
1849
+ >
1850
+ <Zap size={11} />Reopen
1851
+ </button>
1852
+ ) : relation ? (
1859
1853
  <RelationPickerOrSelect
1860
1854
  table={table}
1861
1855
  tables={tables}
@@ -2081,6 +2075,93 @@ function AddObjectSidebar({ open, saving, onClose, onCreate, allTables }) {
2081
2075
  );
2082
2076
  }
2083
2077
 
2078
+
2079
+ // ─── Command Palette ──────────────────────────────────────────────────────────
2080
+
2081
+ function DataModelCommandPalette({ commands, onClose }) {
2082
+ const [query, setQuery] = useState("");
2083
+ const [highlight, setHighlight] = useState(0);
2084
+ const inputRef = useRef(null);
2085
+ useEffect(() => { inputRef.current?.focus(); }, []);
2086
+ const filtered = useMemo(() => {
2087
+ const q = query.trim().toLowerCase();
2088
+ if (!q) return commands;
2089
+ return commands.filter((c) =>
2090
+ `${c.label} ${c.group || ""} ${(c.aliases || []).join(" ")}`.toLowerCase().includes(q)
2091
+ );
2092
+ }, [commands, query]);
2093
+ useEffect(() => {
2094
+ setHighlight((v) => Math.min(v, Math.max(0, filtered.length - 1)));
2095
+ }, [filtered.length]);
2096
+ const handleKey = (e) => {
2097
+ if (e.key === "ArrowDown") { e.preventDefault(); setHighlight((v) => Math.min(filtered.length - 1, v + 1)); }
2098
+ else if (e.key === "ArrowUp") { e.preventDefault(); setHighlight((v) => Math.max(0, v - 1)); }
2099
+ else if (e.key === "Enter") {
2100
+ e.preventDefault();
2101
+ const cmd = filtered[highlight];
2102
+ if (cmd && !cmd.disabled) { cmd.run(); onClose(); }
2103
+ } else if (e.key === "Escape") { e.preventDefault(); onClose(); }
2104
+ };
2105
+ const groups = useMemo(() => {
2106
+ const map = new Map();
2107
+ filtered.forEach((c) => {
2108
+ const key = c.group || "General";
2109
+ if (!map.has(key)) map.set(key, []);
2110
+ map.get(key).push(c);
2111
+ });
2112
+ return Array.from(map.entries());
2113
+ }, [filtered]);
2114
+ return (
2115
+ <div className="workspace-command-palette" role="dialog" aria-modal="true" aria-label="Command palette" data-palette="">
2116
+ <div className="workspace-overlay-backdrop" onClick={onClose} aria-hidden="true" />
2117
+ <section className="workspace-command-palette-panel" onKeyDown={handleKey}>
2118
+ <header className="workspace-command-palette-input">
2119
+ <span aria-hidden="true">⌘</span>
2120
+ <input
2121
+ ref={inputRef}
2122
+ value={query}
2123
+ onChange={(e) => setQuery(e.target.value)}
2124
+ placeholder="Type a command or ask helper…"
2125
+ aria-label="Command palette search"
2126
+ />
2127
+ <kbd>esc</kbd>
2128
+ </header>
2129
+ <div className="workspace-command-palette-list" role="listbox">
2130
+ {filtered.length === 0 ? <p className="workspace-panel-hint">No matching commands.</p> : null}
2131
+ {groups.map(([group, items]) => (
2132
+ <div key={group} className="workspace-command-palette-group">
2133
+ <p className="workspace-panel-label">{group}</p>
2134
+ {items.map((cmd) => {
2135
+ const gi = filtered.indexOf(cmd);
2136
+ const isHL = gi === highlight;
2137
+ return (
2138
+ <button
2139
+ key={cmd.id}
2140
+ type="button"
2141
+ role="option"
2142
+ aria-selected={isHL}
2143
+ className={"workspace-command-palette-item" + (isHL ? " active" : "") + (cmd.disabled ? " disabled" : "")}
2144
+ disabled={cmd.disabled}
2145
+ onMouseEnter={() => setHighlight(gi)}
2146
+ onClick={() => { if (!cmd.disabled) { cmd.run(); onClose(); } }}
2147
+ >
2148
+ <span aria-hidden="true"><Zap size={14} /></span>
2149
+ <span className="workspace-command-palette-label">{cmd.label}</span>
2150
+ {cmd.shortcut ? <kbd>{cmd.shortcut}</kbd> : null}
2151
+ </button>
2152
+ );
2153
+ })}
2154
+ </div>
2155
+ ))}
2156
+ </div>
2157
+ <footer className="workspace-command-palette-footer">
2158
+ <span>↑ ↓ navigate</span><span>↵ run</span><span>esc close</span>
2159
+ </footer>
2160
+ </section>
2161
+ </div>
2162
+ );
2163
+ }
2164
+
2084
2165
  // ─── Page ─────────────────────────────────────────────────────────────────────
2085
2166
 
2086
2167
  // Auto-save tempo: hold local edits in memory + localStorage, only PATCH the
@@ -2098,9 +2179,44 @@ export default function DataModelShell() {
2098
2179
  const [message, setMessage] = useState("");
2099
2180
  const [selectedSource, setSelectedSource] = useState("");
2100
2181
  const [addOpen, setAddOpen] = useState(false);
2182
+ const [helperOpen, setHelperOpen] = useState(false);
2183
+ const [helperIntent, setHelperIntent] = useState("create_object");
2184
+ const [helperInitialPrompt, setHelperInitialPrompt] = useState("");
2185
+ const [helperInitialThread, setHelperInitialThread] = useState(null);
2186
+ const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
2101
2187
  const pendingPatchRef = useRef({});
2102
2188
  const saveTimerRef = useRef(null);
2103
2189
 
2190
+ // Cross-page rail entrypoints. Settings / integrations pages render
2191
+ // <WorkspaceRail> without an in-process helper handler — clicking the
2192
+ // pill or a chat thread there navigates to `/data-model?helper=open`
2193
+ // or `/data-model?thread=<id>`. We consume those query params here
2194
+ // exactly once per change and strip them so refreshes are idempotent.
2195
+ const router = useRouter();
2196
+ const searchParams = useSearchParams();
2197
+ useEffect(() => {
2198
+ if (!workspaceConfig) return;
2199
+ const helperParam = searchParams?.get("helper");
2200
+ const threadParam = searchParams?.get("thread");
2201
+ if (!helperParam && !threadParam) return;
2202
+ if (threadParam) {
2203
+ const ht = (workspaceConfig?.dataModel?.objects || []).find((o) => o?.id === "helper-threads");
2204
+ const row = (ht?.rows || []).find((r) => r?.id === threadParam);
2205
+ if (row) {
2206
+ setHelperInitialThread(row);
2207
+ setHelperOpen(true);
2208
+ }
2209
+ } else if (helperParam === "open") {
2210
+ setHelperInitialThread(null);
2211
+ setHelperOpen(true);
2212
+ }
2213
+ const next = new URLSearchParams(searchParams.toString());
2214
+ next.delete("helper");
2215
+ next.delete("thread");
2216
+ const query = next.toString();
2217
+ router.replace(query ? `/data-model?${query}` : "/data-model", { scroll: false });
2218
+ }, [workspaceConfig, searchParams, router]);
2219
+
2104
2220
  const load = useCallback(async () => {
2105
2221
  setLoading(true);
2106
2222
  setError("");
@@ -2119,6 +2235,40 @@ export default function DataModelShell() {
2119
2235
 
2120
2236
  useEffect(() => { load(); }, [load]);
2121
2237
 
2238
+ // Cmd+K opens command palette. Slash opens it too, but only when no
2239
+ // editable element is focused — matches the dashboard builder.
2240
+ useEffect(() => {
2241
+ const handler = (e) => {
2242
+ if ((e.metaKey || e.ctrlKey) && (e.key === "k" || e.key === "K")) {
2243
+ e.preventDefault();
2244
+ setCommandPaletteOpen((v) => !v);
2245
+ return;
2246
+ }
2247
+ if (e.key === "/" && !commandPaletteOpen && !addOpen && !helperOpen) {
2248
+ const t = e.target;
2249
+ const editable = t instanceof HTMLElement && (
2250
+ t.tagName === "INPUT" ||
2251
+ t.tagName === "TEXTAREA" ||
2252
+ t.tagName === "SELECT" ||
2253
+ t.isContentEditable
2254
+ );
2255
+ if (!editable) {
2256
+ e.preventDefault();
2257
+ setCommandPaletteOpen(true);
2258
+ return;
2259
+ }
2260
+ }
2261
+ if (e.key === "Escape" && commandPaletteOpen) setCommandPaletteOpen(false);
2262
+ };
2263
+ const railOpen = () => setCommandPaletteOpen(true);
2264
+ window.addEventListener("keydown", handler);
2265
+ window.addEventListener("growthub:open-command-palette", railOpen);
2266
+ return () => {
2267
+ window.removeEventListener("keydown", handler);
2268
+ window.removeEventListener("growthub:open-command-palette", railOpen);
2269
+ };
2270
+ }, [commandPaletteOpen, addOpen, helperOpen]);
2271
+
2122
2272
  const tables = useMemo(
2123
2273
  () => (workspaceConfig ? listWorkspaceDataModelTables(workspaceConfig) : []),
2124
2274
  [workspaceConfig],
@@ -2211,15 +2361,162 @@ export default function DataModelShell() {
2211
2361
  setAddOpen(false);
2212
2362
  }, [save]);
2213
2363
 
2364
+ const INTENT_FOR_TYPE = {
2365
+ people: "edit_view",
2366
+ tasks: "edit_view",
2367
+ "api-registry": "register_api",
2368
+ "sandbox-environment": "create_object",
2369
+ "data-source": "explain",
2370
+ custom: "create_object",
2371
+ };
2372
+
2373
+ // Starter prompt seeded into the textarea when the user asks the helper
2374
+ // about a specific Data Model object. Non-technical users see context-
2375
+ // appropriate guidance instead of an empty box.
2376
+ const STARTER_PROMPT_FOR_TYPE = {
2377
+ people: (name) => `Improve the "${name}" people list. Suggest fields and a view layout that fit a sales / outreach workflow.`,
2378
+ tasks: (name) => `Improve the "${name}" tasks board. Suggest status fields, owners, and a sensible view layout.`,
2379
+ "api-registry": (name) => `Register a new API integration for "${name}". Draft the row with integration label, base URL, endpoint, auth header, and method.`,
2380
+ "sandbox-environment": (name) => `Configure the "${name}" sandbox environment. Suggest runtime, prompt, instructions, and lifecycle status fields.`,
2381
+ "data-source": (name) => `Explain how the "${name}" data source is wired up and what changes would make it more reliable.`,
2382
+ custom: (name) => `Improve the "${name}" object. Suggest fields, relations, and starter rows that fit my use case.`,
2383
+ };
2384
+
2385
+ const openHelperForTable = (table) => {
2386
+ const intent = INTENT_FOR_TYPE[table?.objectType] || "create_object";
2387
+ const fill = STARTER_PROMPT_FOR_TYPE[table?.objectType];
2388
+ setHelperIntent(intent);
2389
+ setHelperInitialPrompt(fill ? fill(table?.label || table?.source || "this object") : "");
2390
+ setHelperInitialThread(null);
2391
+ setHelperOpen(true);
2392
+ };
2393
+
2394
+ const openHelperWith = (intent, prompt) => {
2395
+ setHelperIntent(intent);
2396
+ setHelperInitialPrompt(prompt || "");
2397
+ setHelperInitialThread(null);
2398
+ setHelperOpen(true);
2399
+ };
2400
+
2401
+ // Reopen a helper thread row from the Helper Threads Data Model object.
2402
+ // The row already holds the full prior turn (intent, prompt, proposals,
2403
+ // warnings, receipts) — passing it through initialThread rehydrates the
2404
+ // sidecar state so the user reads the conversation exactly where it ended.
2405
+ const openHelperThreadFromRow = (row) => {
2406
+ if (!row || !row.id) return;
2407
+ const proposals = Array.isArray(row.proposals) ? row.proposals : [];
2408
+ const warnings = Array.isArray(row.warnings) ? row.warnings : [];
2409
+ const result = {
2410
+ summary: row.summary || "",
2411
+ proposals,
2412
+ warnings,
2413
+ receipts: row.receipts || null,
2414
+ threadId: row.id,
2415
+ };
2416
+ setHelperIntent(row.intent || "explain");
2417
+ setHelperInitialPrompt(typeof row.prompt === "string" ? row.prompt : "");
2418
+ setHelperInitialThread({
2419
+ id: row.id,
2420
+ intent: row.intent || "explain",
2421
+ prompt: typeof row.prompt === "string" ? row.prompt : "",
2422
+ result,
2423
+ });
2424
+ setHelperOpen(true);
2425
+ };
2426
+
2427
+ const paletteCommands = [
2428
+ {
2429
+ id: "helper.build_dashboard", group: "Ask helper", label: "Ask helper — build a dashboard",
2430
+ run: () => openHelperWith("build_dashboard", "Draft a dashboard for a local agency with pipeline stages, weekly revenue, and a leaderboard widget.")
2431
+ },
2432
+ {
2433
+ id: "helper.create_object", group: "Ask helper", label: "Ask helper — create a custom object",
2434
+ run: () => openHelperWith("create_object", "Create a custom object for tracking client engagements: name, owner, status, value, next step.")
2435
+ },
2436
+ {
2437
+ id: "helper.register_api", group: "Ask helper", label: "Ask helper — register an API",
2438
+ run: () => openHelperWith("register_api", "Register an API integration: integration label, base URL, endpoint, auth header, and method.")
2439
+ },
2440
+ {
2441
+ id: "helper.repair", group: "Ask helper", label: "Ask helper — repair workspace",
2442
+ run: () => openHelperWith("repair", "Inspect this workspace for missing references, broken bindings, or incomplete views. Propose the smallest fix for each issue.")
2443
+ },
2444
+ {
2445
+ id: "helper.explain", group: "Ask helper", label: "Ask helper — explain this workspace",
2446
+ run: () => openHelperWith("explain", "Explain what this workspace contains and how the objects, dashboards, and bindings relate to each other.")
2447
+ },
2448
+ {
2449
+ id: "object.new", group: "Data Model", label: "New object",
2450
+ run: () => setAddOpen(true)
2451
+ },
2452
+ {
2453
+ id: "nav.dashboards", group: "Navigation", label: "Go to Dashboards",
2454
+ run: () => { window.location.href = "/"; }
2455
+ },
2456
+ {
2457
+ id: "nav.settings", group: "Navigation", label: "Go to Settings",
2458
+ run: () => { window.location.href = "/settings/general"; }
2459
+ },
2460
+ ];
2461
+
2214
2462
  return (
2215
2463
  <main className="workspace-builder workspace-settings-page">
2216
- <NavRail authority={authority} workspaceConfig={workspaceConfig} />
2464
+ <WorkspaceRail
2465
+ authority={authority}
2466
+ workspaceConfig={workspaceConfig}
2467
+ helperOpen={helperOpen}
2468
+ onOpenHelper={() => {
2469
+ if (helperOpen) { setHelperOpen(false); return; }
2470
+ // Rail pill ALWAYS opens a fresh thread (empty state, chip
2471
+ // stack visible). Reopening a specific conversation goes
2472
+ // through onOpenThread from the Chat tab.
2473
+ setHelperInitialThread(null);
2474
+ setHelperIntent("create_object");
2475
+ setHelperInitialPrompt("");
2476
+ setHelperOpen(true);
2477
+ }}
2478
+ onOpenThread={(row) => {
2479
+ setHelperInitialThread(row);
2480
+ setHelperOpen(true);
2481
+ }}
2482
+ onConfigChange={(next) => {
2483
+ if (typeof setWorkspaceConfig === "function") setWorkspaceConfig(next);
2484
+ }}
2485
+ />
2217
2486
 
2218
2487
  <section className="workspace-surface">
2219
2488
  <header className="workspace-toolbar">
2220
- <div><p>Workspace</p><h1>Data Model</h1></div>
2489
+ {selectedTable ? (
2490
+ <div className="workspace-toolbar-object">
2491
+ <div className="workspace-toolbar-object-title">
2492
+ <span className="workspace-toolbar-object-icon" aria-hidden="true">
2493
+ <LucideIcon
2494
+ name={selectedTable.icon || OBJECT_TYPE_PRESETS[selectedTable.objectType]?.icon || "Database"}
2495
+ size={16}
2496
+ />
2497
+ </span>
2498
+ <h1>{selectedTable.label}</h1>
2499
+ </div>
2500
+ <p className="workspace-toolbar-object-meta">
2501
+ {(selectedTable.columns?.length || 0)} {(selectedTable.columns?.length || 0) === 1 ? "Field" : "Fields"}
2502
+ {" · "}
2503
+ {(selectedTable.rows?.length || 0)} {(selectedTable.rows?.length || 0) === 1 ? "Record" : "Records"}
2504
+ </p>
2505
+ </div>
2506
+ ) : (
2507
+ <div><p>Workspace</p><h1>Data Model</h1></div>
2508
+ )}
2221
2509
  <div className="workspace-toolbar-actions">
2222
2510
  <SaveToast saving={saving} message={message} />
2511
+ {selectedTable && (
2512
+ <ObjectViewPicker
2513
+ tables={tables}
2514
+ selectedTable={selectedTable}
2515
+ saving={saving}
2516
+ onSelectSource={setSelectedSource}
2517
+ onSave={save}
2518
+ />
2519
+ )}
2223
2520
  <button type="button" className="dm-btn-primary" onClick={() => setAddOpen(true)}>
2224
2521
  <Plus size={14} />New object
2225
2522
  </button>
@@ -2234,6 +2531,55 @@ export default function DataModelShell() {
2234
2531
  allTables={tables}
2235
2532
  />
2236
2533
 
2534
+ <HelperSidecar
2535
+ open={helperOpen}
2536
+ onClose={() => setHelperOpen(false)}
2537
+ workspaceConfig={workspaceConfig}
2538
+ initialIntent={helperIntent}
2539
+ initialPrompt={helperInitialPrompt}
2540
+ initialThread={helperInitialThread}
2541
+ onOpenArtifact={(target) => {
2542
+ // Close the chat and route the user to the artifact they
2543
+ // just created — data-model object/row stays in-page, a
2544
+ // dashboard navigates to the workspace home with a query
2545
+ // param the builder reads to focus it.
2546
+ if (!target) return;
2547
+ if (target.surface === "data-model" && target.source) {
2548
+ setSelectedSource(target.source);
2549
+ setHelperOpen(false);
2550
+ return;
2551
+ }
2552
+ if (target.surface === "dashboard" && target.dashboardId) {
2553
+ setHelperOpen(false);
2554
+ router.push(`/?dashboard=${encodeURIComponent(target.dashboardId)}`);
2555
+ }
2556
+ }}
2557
+ onApplied={(updatedConfig) => {
2558
+ // Anchor the user on the most recently created/updated Data Model
2559
+ // object so a helper-driven object.create lands on the surface
2560
+ // instead of needing a manual click.
2561
+ setWorkspaceConfig(updatedConfig);
2562
+ const nextObjects = updatedConfig?.dataModel?.objects || [];
2563
+ const prevIds = new Set(
2564
+ (workspaceConfig?.dataModel?.objects || []).map((o) => o?.id).filter(Boolean)
2565
+ );
2566
+ const newlyCreated = nextObjects.find((o) => o?.id && !prevIds.has(o.id));
2567
+ const nextSource = (newlyCreated?.label || newlyCreated?.id)
2568
+ ? (newlyCreated.label || newlyCreated.id)
2569
+ : selectedSource;
2570
+ if (nextSource && nextSource !== selectedSource) {
2571
+ setSelectedSource(nextSource);
2572
+ }
2573
+ }}
2574
+ />
2575
+
2576
+ {commandPaletteOpen && (
2577
+ <DataModelCommandPalette
2578
+ commands={paletteCommands}
2579
+ onClose={() => setCommandPaletteOpen(false)}
2580
+ />
2581
+ )}
2582
+
2237
2583
  {loading && <div className="dm-loading">Loading workspace…</div>}
2238
2584
 
2239
2585
  {error && (
@@ -2248,13 +2594,8 @@ export default function DataModelShell() {
2248
2594
  {!loading && !error && tables.length > 0 && (
2249
2595
  selectedTable && (
2250
2596
  <section className="dm-detail-v2 dm-detail-v3">
2251
- <div className="dm-detail-v2-head dm-detail-v3-head">
2252
- <div className="dm-detail-v2-title">
2253
- <ObjectViewPicker tables={tables} selectedTable={selectedTable} saving={saving} onSelectSource={setSelectedSource} onSave={save} />
2254
- </div>
2255
- <SourceValidationBanner table={selectedTable} />
2256
- </div>
2257
- <DataModelTableSurface workspaceConfig={workspaceConfig} table={selectedTable} tables={tables} saving={saving} onSave={save} />
2597
+ <SourceValidationBanner table={selectedTable} />
2598
+ <DataModelTableSurface workspaceConfig={workspaceConfig} table={selectedTable} tables={tables} saving={saving} onSave={save} onOpenThread={openHelperThreadFromRow} />
2258
2599
  </section>
2259
2600
  )
2260
2601
  )}
@@ -2263,10 +2604,22 @@ export default function DataModelShell() {
2263
2604
  <div className="dm-page-empty">
2264
2605
  <Database size={32} />
2265
2606
  <strong>No objects yet</strong>
2266
- <p>Create a Data Source, API Registry, People, Tasks, or Custom object to get started.</p>
2267
- <button type="button" className="dm-btn-primary" onClick={() => setAddOpen(true)}>
2268
- <Plus size={14} />New object
2269
- </button>
2607
+ <p>Create your first Data Source, API Registry, People list, or custom object to get started.</p>
2608
+ <div className="dm-page-empty-actions">
2609
+ <button type="button" className="dm-btn-primary" onClick={() => setAddOpen(true)}>
2610
+ <Plus size={14} />New object
2611
+ </button>
2612
+ <button
2613
+ type="button"
2614
+ className="dm-btn-outline"
2615
+ onClick={() => openHelperWith(
2616
+ "create_object",
2617
+ "I run a local agency. Create my first business object: a client list with name, owner, status, deal value, and next step. Then suggest a starter dashboard."
2618
+ )}
2619
+ >
2620
+ <Zap size={14} />Try the helper
2621
+ </button>
2622
+ </div>
2270
2623
  </div>
2271
2624
  )}
2272
2625
  </section>