@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.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +307 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +372 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/receipts/route.js +47 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +664 -82
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +1371 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +1383 -24
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +7 -21
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/ownership/ownership-panel.jsx +222 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/ownership/page.jsx +19 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/settings-shell.jsx +2 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +116 -24
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +497 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/growthub.config.json +20 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +19 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +23 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +473 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +583 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +34 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +3 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/export-training-traces.mjs +144 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/grade-raw-pairs.mjs +279 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/harvest-cursor-traces.mjs +288 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/upload-graded-traces.mjs +128 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +19 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/templates/seeded-configs/alignment-loop.config.json +264 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/workers/custom-workspace-operator/CLAUDE.md +38 -0
- package/dist/index.js +1416 -2627
- 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
|
+
}
|
package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/growthub.config.json
CHANGED
|
@@ -27,10 +27,26 @@
|
|
|
27
27
|
}
|
|
28
28
|
],
|
|
29
29
|
"widgetTypes": [
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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)
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
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 = [];
|