@growthub/cli 0.10.0 → 0.12.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 (28) 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 +664 -82
  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 +1383 -24
  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 +23 -5
  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/helpers/export-training-traces.mjs +144 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/grade-raw-pairs.mjs +279 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/harvest-cursor-traces.mjs +288 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/upload-graded-traces.mjs +128 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +19 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/templates/seeded-configs/alignment-loop.config.json +264 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/workers/custom-workspace-operator/CLAUDE.md +38 -0
  27. package/dist/index.js +1416 -2627
  28. package/package.json +1 -1
@@ -0,0 +1,497 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Shared workspace nav rail.
5
+ *
6
+ * One canonical rail rendered on every governed-workspace page. Two-row
7
+ * header (brand + utility actions, then tab toggles + Ask helper pill),
8
+ * tab-driven body:
9
+ *
10
+ * ┌──────────────────────────────────────────────┐
11
+ * │ [G] workspace-name ▾ 🔍 [▸] │ Top row
12
+ * │ [🏠 Home] [💬 Chat] [✶+ Ask helper] │ Tab row
13
+ * ├──────────────────────────────────────────────┤
14
+ * │ HOME tab body: CHAT tab body: │
15
+ * │ Dashboards Latest │
16
+ * │ Data Model 💬 Best Skills │
17
+ * │ Management 💬 Casual greet │
18
+ * │ Workspace Settings (… more threads) │
19
+ * └──────────────────────────────────────────────┘
20
+ *
21
+ * Chat threads come from the governed `helper-threads` custom object
22
+ * (id = "helper-threads") in workspaceConfig.dataModel.objects[].rows.
23
+ * Rename / archive / delete actions mutate that object in place via
24
+ * PATCH /api/workspace { dataModel } — the same PATCH allowlist used by
25
+ * the rest of the workspace builder.
26
+ *
27
+ * Surface-specific slots (`dashboardsSlot`, `dataModelSlot`,
28
+ * `managementSlot`, `settingsSlot`) let the page inject its own
29
+ * Dashboards / Data Model / Management / Workspace Settings behaviour
30
+ * while keeping the visual treatment identical across every page.
31
+ */
32
+
33
+ import { useEffect, useMemo, useRef, useState } from "react";
34
+ import Link from "next/link";
35
+ import { usePathname, useRouter } from "next/navigation";
36
+ import {
37
+ Archive,
38
+ ChevronDown,
39
+ Home,
40
+ MessageCircle,
41
+ MessageCirclePlus,
42
+ MoreHorizontal,
43
+ PanelLeftClose,
44
+ Pencil,
45
+ Search,
46
+ Trash2,
47
+ X,
48
+ } from "lucide-react";
49
+
50
+ function textColorForAccent(accent) {
51
+ const hex = String(accent || "").replace("#", "");
52
+ if (!/^[0-9a-f]{6}$/i.test(hex)) return "#ffffff";
53
+ const r = parseInt(hex.slice(0, 2), 16);
54
+ const g = parseInt(hex.slice(2, 4), 16);
55
+ const b = parseInt(hex.slice(4, 6), 16);
56
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
57
+ return luminance > 0.62 ? "#252525" : "#ffffff";
58
+ }
59
+
60
+ function relativeTime(iso) {
61
+ if (!iso) return "";
62
+ const t = new Date(iso).getTime();
63
+ if (!Number.isFinite(t)) return "";
64
+ const diff = Date.now() - t;
65
+ if (diff < 60_000) return "now";
66
+ const mins = Math.floor(diff / 60_000);
67
+ if (mins < 60) return `${mins}m`;
68
+ const hrs = Math.floor(mins / 60);
69
+ if (hrs < 24) return `${hrs}h`;
70
+ const days = Math.floor(hrs / 24);
71
+ if (days < 7) return `${days}d`;
72
+ return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric" });
73
+ }
74
+
75
+ const INTENT_LABEL = {
76
+ build_dashboard: "Build dashboard",
77
+ create_widget: "Create widget",
78
+ register_api: "Register API",
79
+ create_object: "Create object",
80
+ edit_view: "Edit view",
81
+ repair: "Repair workspace",
82
+ explain: "Explain",
83
+ };
84
+
85
+ function deriveThreadTitle(row) {
86
+ const title = typeof row?.title === "string" ? row.title.trim() : "";
87
+ if (title) return title;
88
+ const summary = typeof row?.summary === "string" ? row.summary.trim() : "";
89
+ if (summary) {
90
+ const firstClause = summary.split(/[\n\.]/)[0].trim();
91
+ if (firstClause) return firstClause.length > 56 ? `${firstClause.slice(0, 55)}…` : firstClause;
92
+ }
93
+ return INTENT_LABEL[row?.intent] || "Helper conversation";
94
+ }
95
+
96
+ function getHelperThreadRows(workspaceConfig) {
97
+ const objects = workspaceConfig?.dataModel?.objects || [];
98
+ const ht = objects.find((o) => o?.id === "helper-threads");
99
+ const rows = Array.isArray(ht?.rows) ? ht.rows : [];
100
+ return rows
101
+ .filter((r) => r && !r.archived)
102
+ .slice()
103
+ .sort((a, b) => String(b.updatedAt || "").localeCompare(String(a.updatedAt || "")));
104
+ }
105
+
106
+ export function WorkspaceRail({
107
+ workspaceConfig,
108
+ authority,
109
+ helperOpen = false,
110
+ onOpenHelper,
111
+ onOpenThread,
112
+ onConfigChange,
113
+ dashboardsSlot,
114
+ dataModelSlot,
115
+ // `managementSlot` retained as accepted-but-ignored prop for backward
116
+ // compatibility with callers that still pass it. The Management item
117
+ // moved to the Workspace Settings → Ownership tab.
118
+ managementSlot: _managementSlotDeprecated,
119
+ settingsSlot,
120
+ }) {
121
+ const branding = workspaceConfig?.branding || {};
122
+ const workspaceName = branding.name || workspaceConfig?.name || "Growthub Workspace";
123
+ const pathname = usePathname() || "/";
124
+ const router = useRouter();
125
+
126
+ const [activeTab, setActiveTab] = useState("home");
127
+ const [openMenuId, setOpenMenuId] = useState(null);
128
+ const [renamingId, setRenamingId] = useState(null);
129
+ const [renameDraft, setRenameDraft] = useState("");
130
+ const [chatSearch, setChatSearch] = useState("");
131
+ const [chatExpanded, setChatExpanded] = useState(false);
132
+ const menuWrapRef = useRef(null);
133
+ const CHAT_PREVIEW_COUNT = 10;
134
+
135
+ useEffect(() => {
136
+ if (!openMenuId) return undefined;
137
+ const onPointerDown = (e) => {
138
+ if (!menuWrapRef.current) return;
139
+ if (!menuWrapRef.current.contains(e.target)) setOpenMenuId(null);
140
+ };
141
+ document.addEventListener("pointerdown", onPointerDown);
142
+ return () => document.removeEventListener("pointerdown", onPointerDown);
143
+ }, [openMenuId]);
144
+
145
+ const threads = useMemo(() => getHelperThreadRows(workspaceConfig), [workspaceConfig]);
146
+
147
+ const handleAskHelperClick = () => {
148
+ if (onOpenHelper) {
149
+ onOpenHelper();
150
+ return;
151
+ }
152
+ router.push("/data-model?helper=open");
153
+ };
154
+
155
+ const handleOpenThread = (row) => {
156
+ if (onOpenThread) {
157
+ onOpenThread(row);
158
+ return;
159
+ }
160
+ router.push(`/data-model?thread=${encodeURIComponent(row.id)}`);
161
+ };
162
+
163
+ async function patchHelperThreads(updatedRows) {
164
+ const dm = workspaceConfig?.dataModel || {};
165
+ const objects = Array.isArray(dm.objects) ? dm.objects.slice() : [];
166
+ const idx = objects.findIndex((o) => o?.id === "helper-threads");
167
+ if (idx === -1) return;
168
+ objects[idx] = { ...objects[idx], rows: updatedRows };
169
+ const nextDataModel = { ...dm, objects };
170
+ try {
171
+ const res = await fetch("/api/workspace", {
172
+ method: "PATCH",
173
+ headers: { "content-type": "application/json" },
174
+ body: JSON.stringify({ dataModel: nextDataModel }),
175
+ });
176
+ if (res.ok) {
177
+ const body = await res.json().catch(() => ({}));
178
+ if (body?.workspaceConfig && onConfigChange) {
179
+ onConfigChange(body.workspaceConfig);
180
+ }
181
+ }
182
+ } catch {
183
+ // Best-effort: read-only runtimes return 409; the user can retry.
184
+ }
185
+ }
186
+
187
+ const beginRename = (row) => {
188
+ setOpenMenuId(null);
189
+ setRenamingId(row.id);
190
+ setRenameDraft(deriveThreadTitle(row));
191
+ };
192
+
193
+ const commitRename = async (row) => {
194
+ const next = renameDraft.trim();
195
+ setRenamingId(null);
196
+ setRenameDraft("");
197
+ if (!next || next === deriveThreadTitle(row)) return;
198
+ const ht = (workspaceConfig?.dataModel?.objects || []).find((o) => o?.id === "helper-threads");
199
+ const updated = (ht?.rows || []).map((r) => (r.id === row.id ? { ...r, title: next } : r));
200
+ await patchHelperThreads(updated);
201
+ };
202
+
203
+ const archiveThread = async (row) => {
204
+ setOpenMenuId(null);
205
+ const ht = (workspaceConfig?.dataModel?.objects || []).find((o) => o?.id === "helper-threads");
206
+ const updated = (ht?.rows || []).map((r) => (r.id === row.id ? { ...r, archived: true } : r));
207
+ await patchHelperThreads(updated);
208
+ };
209
+
210
+ const deleteThread = async (row) => {
211
+ setOpenMenuId(null);
212
+ const ht = (workspaceConfig?.dataModel?.objects || []).find((o) => o?.id === "helper-threads");
213
+ const updated = (ht?.rows || []).filter((r) => r.id !== row.id);
214
+ await patchHelperThreads(updated);
215
+ };
216
+
217
+ return (
218
+ <aside className="workspace-rail" aria-label="Workspace navigation">
219
+ {/* Row 1: brand + utility actions */}
220
+ <div className="workspace-rail-topbar">
221
+ <button type="button" className="workspace-rail-brand-button" aria-label={`Workspace ${workspaceName}`}>
222
+ <span
223
+ className="workspace-mark"
224
+ style={{
225
+ background: branding.logoUrl ? undefined : branding.accent || undefined,
226
+ color: branding.logoUrl ? undefined : textColorForAccent(branding.accent),
227
+ }}
228
+ >
229
+ {branding.logoUrl ? <img src={branding.logoUrl} alt="" /> : workspaceName.slice(0, 1).toUpperCase()}
230
+ </span>
231
+ <span className="workspace-brand-label">{workspaceName}</span>
232
+ <ChevronDown size={13} className="workspace-brand-caret" aria-hidden="true" />
233
+ </button>
234
+ <div className="workspace-rail-topbar-actions">
235
+ <button
236
+ type="button"
237
+ className="workspace-rail-icon-btn"
238
+ aria-label="Search workspace"
239
+ title="Search (⌘K)"
240
+ data-rail-search=""
241
+ onClick={() => {
242
+ // Surfaces with a command palette (DataModelShell) listen
243
+ // for this event and open the palette in place. Other
244
+ // surfaces are free to ignore it.
245
+ if (typeof window !== "undefined") {
246
+ window.dispatchEvent(new CustomEvent("growthub:open-command-palette"));
247
+ }
248
+ }}
249
+ >
250
+ <Search size={13} />
251
+ </button>
252
+ <button
253
+ type="button"
254
+ className="workspace-rail-icon-btn"
255
+ aria-label="Collapse sidebar"
256
+ title="Collapse sidebar"
257
+ >
258
+ <PanelLeftClose size={13} />
259
+ </button>
260
+ </div>
261
+ </div>
262
+
263
+ {/* Row 2: tab toggles + Ask helper pill */}
264
+ <div className="workspace-rail-tabbar">
265
+ <div role="tablist" aria-label="Sidebar mode" className="workspace-rail-tabs">
266
+ <button
267
+ type="button"
268
+ role="tab"
269
+ aria-selected={activeTab === "home"}
270
+ className={"workspace-rail-tab" + (activeTab === "home" ? " active" : "")}
271
+ onClick={() => setActiveTab("home")}
272
+ aria-label="Home"
273
+ title="Home"
274
+ >
275
+ <Home size={15} />
276
+ </button>
277
+ <button
278
+ type="button"
279
+ role="tab"
280
+ aria-selected={activeTab === "chat"}
281
+ className={"workspace-rail-tab" + (activeTab === "chat" ? " active" : "")}
282
+ onClick={() => setActiveTab("chat")}
283
+ aria-label="Helper conversations"
284
+ title="Helper conversations"
285
+ >
286
+ <MessageCircle size={15} />
287
+ </button>
288
+ </div>
289
+ <button
290
+ type="button"
291
+ className={"workspace-rail-helper-pill" + (helperOpen ? " active" : "")}
292
+ data-helper-trigger="rail"
293
+ aria-label={helperOpen ? "Close workspace helper" : "Open workspace helper"}
294
+ aria-pressed={helperOpen}
295
+ title={helperOpen ? "Close helper" : "Ask helper"}
296
+ onClick={handleAskHelperClick}
297
+ >
298
+ <MessageCirclePlus size={13} aria-hidden="true" />
299
+ <span>Ask helper</span>
300
+ </button>
301
+ </div>
302
+
303
+ {/* Body: switches by tab. The legacy `Management` nav item now
304
+ lives as the 4th Workspace Settings tab (`/settings/ownership`).
305
+ The Data Model link is renamed to `Management` since the data
306
+ model surface IS the user-facing object/list management. */}
307
+ {activeTab === "home" ? (
308
+ <nav className="workspace-nav" aria-label="Workspace pages">
309
+ {dashboardsSlot ?? (
310
+ <Link href="/" className={pathname === "/" ? "active" : undefined}>
311
+ Dashboards
312
+ </Link>
313
+ )}
314
+ {dataModelSlot ?? (
315
+ <Link
316
+ href="/data-model"
317
+ className={pathname.startsWith("/data-model") ? "active" : undefined}
318
+ >
319
+ Management
320
+ </Link>
321
+ )}
322
+ {settingsSlot ?? (
323
+ <Link
324
+ href="/settings/general"
325
+ className={"workspace-nav-bottom" + (pathname.startsWith("/settings") ? " active" : "")}
326
+ >
327
+ Workspace Settings
328
+ </Link>
329
+ )}
330
+ </nav>
331
+ ) : (
332
+ <div className="workspace-rail-chat" aria-label="Helper conversation threads">
333
+ <div className="workspace-rail-chat-search">
334
+ <Search size={12} aria-hidden="true" />
335
+ <input
336
+ type="text"
337
+ className="workspace-rail-chat-search-input"
338
+ placeholder="Search chats"
339
+ value={chatSearch}
340
+ onChange={(e) => setChatSearch(e.target.value)}
341
+ aria-label="Search helper conversations"
342
+ />
343
+ {chatSearch && (
344
+ <button
345
+ type="button"
346
+ className="workspace-rail-chat-search-clear"
347
+ onClick={() => setChatSearch("")}
348
+ aria-label="Clear search"
349
+ >
350
+ <X size={11} />
351
+ </button>
352
+ )}
353
+ </div>
354
+ <div className="workspace-rail-section-label">Latest</div>
355
+ {(() => {
356
+ const q = chatSearch.trim().toLowerCase();
357
+ const filtered = q
358
+ ? threads.filter((r) => deriveThreadTitle(r).toLowerCase().includes(q))
359
+ : threads;
360
+ if (filtered.length === 0) {
361
+ return (
362
+ <p className="workspace-rail-chat-empty">
363
+ {q ? `No threads match “${chatSearch.trim()}”.` : "No helper conversations yet. Open one with Ask helper."}
364
+ </p>
365
+ );
366
+ }
367
+ const truncate = !chatExpanded && !q && filtered.length > CHAT_PREVIEW_COUNT;
368
+ const visible = truncate ? filtered.slice(0, CHAT_PREVIEW_COUNT) : filtered;
369
+ return (
370
+ <>
371
+ <div className={`workspace-rail-thread-scroll${truncate ? " is-truncated" : ""}`}>
372
+ <ul className="workspace-rail-thread-list" role="list">
373
+ {visible.map((row) => {
374
+ const title = deriveThreadTitle(row);
375
+ const isRenaming = renamingId === row.id;
376
+ const isMenuOpen = openMenuId === row.id;
377
+ return (
378
+ <li
379
+ key={row.id}
380
+ className="workspace-rail-thread-row"
381
+ data-thread-id={row.id}
382
+ >
383
+ <button
384
+ type="button"
385
+ className="workspace-rail-thread-main"
386
+ onClick={() => handleOpenThread(row)}
387
+ title={`${title}${row.intent ? ` · ${INTENT_LABEL[row.intent] || row.intent}` : ""}`}
388
+ >
389
+ <MessageCircle size={14} className="workspace-rail-thread-icon" />
390
+ {isRenaming ? (
391
+ <input
392
+ autoFocus
393
+ className="workspace-rail-thread-rename"
394
+ value={renameDraft}
395
+ onChange={(e) => setRenameDraft(e.target.value)}
396
+ onClick={(e) => e.stopPropagation()}
397
+ onKeyDown={(e) => {
398
+ if (e.key === "Enter") {
399
+ e.preventDefault();
400
+ commitRename(row);
401
+ }
402
+ if (e.key === "Escape") {
403
+ setRenamingId(null);
404
+ setRenameDraft("");
405
+ }
406
+ }}
407
+ onBlur={() => commitRename(row)}
408
+ />
409
+ ) : (
410
+ <span className="workspace-rail-thread-title">{title}</span>
411
+ )}
412
+ <span className="workspace-rail-thread-time" aria-label={`Updated ${relativeTime(row.updatedAt)}`}>
413
+ {relativeTime(row.updatedAt)}
414
+ </span>
415
+ </button>
416
+ <div
417
+ className="workspace-rail-thread-menu-wrap"
418
+ ref={isMenuOpen ? menuWrapRef : null}
419
+ >
420
+ <button
421
+ type="button"
422
+ className="workspace-rail-thread-menu-btn"
423
+ aria-label={`Actions for ${title}`}
424
+ aria-haspopup="menu"
425
+ aria-expanded={isMenuOpen}
426
+ onClick={(e) => {
427
+ e.stopPropagation();
428
+ setOpenMenuId(isMenuOpen ? null : row.id);
429
+ }}
430
+ >
431
+ <MoreHorizontal size={14} />
432
+ </button>
433
+ {isMenuOpen && (
434
+ <div className="workspace-rail-thread-menu" role="menu">
435
+ <button
436
+ type="button"
437
+ role="menuitem"
438
+ className="workspace-rail-thread-menu-item"
439
+ onClick={() => beginRename(row)}
440
+ >
441
+ <Pencil size={13} aria-hidden="true" /> Rename
442
+ </button>
443
+ <button
444
+ type="button"
445
+ role="menuitem"
446
+ className="workspace-rail-thread-menu-item"
447
+ onClick={() => archiveThread(row)}
448
+ >
449
+ <Archive size={13} aria-hidden="true" /> Archive
450
+ </button>
451
+ <button
452
+ type="button"
453
+ role="menuitem"
454
+ className="workspace-rail-thread-menu-item is-destructive"
455
+ onClick={() => deleteThread(row)}
456
+ >
457
+ <Trash2 size={13} aria-hidden="true" /> Delete
458
+ </button>
459
+ </div>
460
+ )}
461
+ </div>
462
+ </li>
463
+ );
464
+ })}
465
+ </ul>
466
+ </div>
467
+ {truncate && (
468
+ <button
469
+ type="button"
470
+ className="workspace-rail-chat-show-more"
471
+ onClick={() => setChatExpanded(true)}
472
+ >
473
+ Show {filtered.length - CHAT_PREVIEW_COUNT} more
474
+ </button>
475
+ )}
476
+ {!truncate && filtered.length > CHAT_PREVIEW_COUNT && chatExpanded && (
477
+ <button
478
+ type="button"
479
+ className="workspace-rail-chat-show-more"
480
+ onClick={() => setChatExpanded(false)}
481
+ >
482
+ Show less
483
+ </button>
484
+ )}
485
+ </>
486
+ );
487
+ })()}
488
+ </div>
489
+ )}
490
+
491
+ <div className="workspace-rail-status">
492
+ <span className="status-dot" />
493
+ {authority || "local-catalog"}
494
+ </div>
495
+ </aside>
496
+ );
497
+ }
@@ -27,10 +27,26 @@
27
27
  }
28
28
  ],
29
29
  "widgetTypes": [
30
- { "kind": "chart", "label": "Chart", "icon": "C" },
31
- { "kind": "view", "label": "View", "icon": "V" },
32
- { "kind": "iframe", "label": "iFrame", "icon": "I" },
33
- { "kind": "rich-text", "label": "Rich Text", "icon": "T" }
30
+ {
31
+ "kind": "chart",
32
+ "label": "Chart",
33
+ "icon": "C"
34
+ },
35
+ {
36
+ "kind": "view",
37
+ "label": "View",
38
+ "icon": "V"
39
+ },
40
+ {
41
+ "kind": "iframe",
42
+ "label": "iFrame",
43
+ "icon": "I"
44
+ },
45
+ {
46
+ "kind": "rich-text",
47
+ "label": "Rich Text",
48
+ "icon": "T"
49
+ }
34
50
  ],
35
51
  "canvas": {
36
52
  "id": "workspace-canvas",
@@ -86,12 +86,27 @@ async function run(request) {
86
86
  || String(process.env.NATIVE_INTELLIGENCE_LOCAL_MODEL || process.env.OLLAMA_MODEL || "").trim()
87
87
  || "gemma3:4b";
88
88
 
89
+ // Two calling modes (both governed, both JSON-only):
90
+ // 1. Legacy single-turn: caller passes `userIntent` and gets the canonical
91
+ // workspace-sandbox system prompt + one user message.
92
+ // 2. Structured chat: caller passes `messages: [{role, content}, ...]` and
93
+ // the adapter forwards the full conversation to the OpenAI-compatible
94
+ // chat completions endpoint. This is what the workspace helper uses to
95
+ // carry thread context across turns so the local model can resume work
96
+ // inside one conversation. The caller is responsible for keeping the
97
+ // leading system message stable across turns (KV-cache friendly).
98
+ const explicitMessages = Array.isArray(box.messages)
99
+ ? box.messages.filter((m) => m && typeof m.role === "string" && typeof m.content === "string")
100
+ : null;
101
+ const messages = explicitMessages && explicitMessages.length > 0
102
+ ? explicitMessages
103
+ : [
104
+ { role: "system", content: buildSystemPrompt() },
105
+ { role: "user", content: box.userIntent },
106
+ ];
89
107
  const body = {
90
108
  model,
91
- messages: [
92
- { role: "system", content: buildSystemPrompt() },
93
- { role: "user", content: box.userIntent },
94
- ],
109
+ messages,
95
110
  temperature: 0.3,
96
111
  response_format: { type: "json_object" },
97
112
  };
@@ -275,6 +275,7 @@ function deriveManualObjectTable(object) {
275
275
  source,
276
276
  objectType: object.objectType || "custom",
277
277
  icon: object.icon || null,
278
+ pickerHidden: Boolean(object.pickerHidden),
278
279
  columns,
279
280
  rows,
280
281
  binding: object.binding || { mode: "manual", source: "Data Model" },
@@ -287,6 +288,15 @@ function deriveManualObjectTable(object) {
287
288
  };
288
289
  }
289
290
 
291
+ // Helper-owned hidden objects — system-managed, never surfaced in the
292
+ // user-facing Data Model picker / object list / dynamic title. The
293
+ // workspace-helper-sandbox row backs the helper's local-intelligence
294
+ // sandbox primitive (helper-tuned instructions live there); users
295
+ // interact with it only through the helper Setup tab.
296
+ const HIDDEN_HELPER_OBJECT_IDS = new Set([
297
+ "workspace-helper-sandbox",
298
+ ]);
299
+
290
300
  function listWorkspaceDataModelTables(workspaceConfig) {
291
301
  const widgetEntries = listWidgetEntries(workspaceConfig);
292
302
  const refsByObjectId = widgetEntries.reduce((map, { widget, location }) => {
@@ -301,10 +311,12 @@ function listWorkspaceDataModelTables(workspaceConfig) {
301
311
  map.set(binding.objectId, refs);
302
312
  return map;
303
313
  }, new Map());
304
- const manualObjects = normalizeManualObjects(workspaceConfig).map((object) => {
305
- const table = deriveManualObjectTable(object);
306
- return { ...table, widgetRefs: refsByObjectId.get(object.id) || [] };
307
- });
314
+ const manualObjects = normalizeManualObjects(workspaceConfig)
315
+ .filter((object) => !HIDDEN_HELPER_OBJECT_IDS.has(object?.id))
316
+ .map((object) => {
317
+ const table = deriveManualObjectTable(object);
318
+ return { ...table, widgetRefs: refsByObjectId.get(object.id) || [] };
319
+ });
308
320
  const widgetTables = widgetEntries
309
321
  .map(({ widget, location }) => {
310
322
  const table = deriveWidgetTable(widget, location);
@@ -1064,7 +1076,13 @@ function resolveLocalReferenceOptions(workspaceConfig, {
1064
1076
  const pageSize = Math.min(100, Math.max(1, Number(relation.pageSize) || Number(limit) || 25));
1065
1077
  const offset = decodeRefCursor(cursor);
1066
1078
 
1067
- const targets = objects.filter((o) => o.objectType === relation.targetObjectType);
1079
+ const targetObjectId = typeof relation.targetObjectId === "string" && relation.targetObjectId.trim()
1080
+ ? relation.targetObjectId.trim()
1081
+ : "";
1082
+ const targets = objects.filter((o) => (
1083
+ o.objectType === relation.targetObjectType
1084
+ && (!targetObjectId || o.id === targetObjectId)
1085
+ ));
1068
1086
  const needle = String(query || "").trim().toLowerCase();
1069
1087
 
1070
1088
  const candidates = [];