@growthub/cli 0.13.7 → 0.13.8

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 (18) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +98 -34
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/swarm-condition/route.js +106 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceContributionGraph.jsx +119 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +357 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensPanel.jsx +488 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensWalkthrough.jsx +69 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +37 -2
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +267 -25
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +55 -2
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-lens/page.jsx +76 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +140 -4
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +1025 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +2 -3
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +24 -8
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -0
  17. package/dist/index.js +5224 -5225
  18. package/package.json +1 -1
@@ -0,0 +1,488 @@
1
+ "use client";
2
+
3
+ /**
4
+ * WorkspaceLensPanel — the post-activation operating surface.
5
+ *
6
+ * Workspace Lens is NOT onboarding. It is the ongoing, minimal, filterable,
7
+ * agent-assignable stream of derived workspace state that the user unlocks
8
+ * after completing setup. It renders the secondary lenses from
9
+ * `deriveWorkspaceState` as aggregate-first cards (summaries + next action +
10
+ * drill-down), never raw records, and keeps the human view aligned with the
11
+ * machine `deriveSwarmConditionPacket` packet.
12
+ *
13
+ * Invariants (inherited from the lens layer):
14
+ * - Pure derivation in. No mutation, no secrets in the output.
15
+ * - Aggregate-first: one card per lens. Detail rows live in Data Model /
16
+ * run console / dashboards — this surface shows the causal summary.
17
+ * - Neutral, calm presentation: gray scale, collapsed by default, no
18
+ * semantic color overload, no icon spam.
19
+ */
20
+
21
+ import { useEffect, useMemo, useState } from "react";
22
+ import Link from "next/link";
23
+ import { Activity, BarChart3, Check, Copy, Eye, GitBranch, MoreVertical, Search } from "lucide-react";
24
+ import { deriveWorkspaceState, deriveSwarmConditionPacket, deriveWorkspaceContributions, deriveLensWalkthroughState, LENS_WALKTHROUGH_DISMISS_FLAG } from "@/lib/workspace-activation";
25
+ import { WorkspaceContributionGraph } from "./WorkspaceContributionGraph.jsx";
26
+ import { WorkspaceLensWalkthrough } from "./WorkspaceLensWalkthrough.jsx";
27
+ import { HelperSidecar } from "../data-model/components/HelperSidecar.jsx";
28
+ import {
29
+ getHelperSandboxRow,
30
+ isHelperHandoffDismissed,
31
+ isHelperConfigured,
32
+ WorkspaceHelperSetupModal,
33
+ } from "./WorkspaceHelperSetupModal.jsx";
34
+
35
+ // Read the guided-tour step from the ?walkthrough= param (steps 2–3 land here
36
+ // after the rail reveal). Anything outside 2–3 means no in-panel tour.
37
+ function readWalkthroughStep() {
38
+ if (typeof window === "undefined") return 0;
39
+ const n = parseInt(new URLSearchParams(window.location.search).get("walkthrough") || "", 10);
40
+ return n === 2 || n === 3 ? n : 0;
41
+ }
42
+
43
+ // Same workspace-ui-cache flag transform the rail/onboarding dismiss use.
44
+ function withUiCacheFlag(config, key, value) {
45
+ const dataModel = config?.dataModel && typeof config.dataModel === "object" ? config.dataModel : {};
46
+ const objects = Array.isArray(dataModel.objects) ? dataModel.objects : [];
47
+ const existing = objects.find((o) => o?.id === "workspace-ui-cache");
48
+ const cacheObject = existing || {
49
+ id: "workspace-ui-cache", label: "Workspace UI Cache", source: "Workspace UI Cache",
50
+ objectType: "custom", icon: "Settings", columns: ["id", key], rows: [],
51
+ binding: { mode: "manual", source: "Workspace UI Cache" },
52
+ };
53
+ const columns = Array.from(new Set([...(Array.isArray(cacheObject.columns) ? cacheObject.columns : ["id"]), key]));
54
+ const rows = Array.isArray(cacheObject.rows) ? cacheObject.rows : [];
55
+ const hasRow = rows.some((r) => r?.id === "activation");
56
+ const nextRows = hasRow
57
+ ? rows.map((r) => (r?.id === "activation" ? { ...r, [key]: value } : r))
58
+ : [...rows, { id: "activation", [key]: value }];
59
+ const nextCache = { ...cacheObject, columns, rows: nextRows };
60
+ const nextObjects = existing
61
+ ? objects.map((o) => (o?.id === "workspace-ui-cache" ? nextCache : o))
62
+ : [...objects, nextCache];
63
+ return { ...config, dataModel: { ...dataModel, objects: nextObjects } };
64
+ }
65
+
66
+ // Map a ?filter= query value (and the contribution graph's "runs") onto a
67
+ // canonical filter id so tooltip deep-links open the right filtered view.
68
+ function readInitialFilter() {
69
+ if (typeof window === "undefined") return "all";
70
+ const raw = (new URLSearchParams(window.location.search).get("filter") || "").trim().toLowerCase();
71
+ if (!raw) return "all";
72
+ if (raw === "runs") return "observability";
73
+ return raw;
74
+ }
75
+
76
+ // Lens state → a single neutral status word the whole surface filters on.
77
+ function lensStatusKind(lens) {
78
+ if (lens.complete) return "ready";
79
+ if ((lens.steps || []).some((s) => s.status === "blocked")) return "blocked";
80
+ return "pending";
81
+ }
82
+
83
+ const STATUS_LABEL = { ready: "Ready", blocked: "Blocked", pending: "In progress" };
84
+
85
+ const FILTERS = [
86
+ { id: "all", label: "All" },
87
+ { id: "blocked", label: "Blocked" },
88
+ { id: "ready", label: "Ready" },
89
+ { id: "assignable", label: "Agent-assignable" },
90
+ { id: "persistence", label: "Persistence" },
91
+ { id: "observability", label: "Runs" },
92
+ { id: "deploy", label: "Deploy" },
93
+ { id: "tasks", label: "Tasks" },
94
+ { id: "app-build", label: "App build" },
95
+ ];
96
+
97
+ export function WorkspaceLensPanel({ workspaceConfig, workspaceSourceRecords, metadataGraph }) {
98
+ const [localConfig, setLocalConfig] = useState(workspaceConfig);
99
+ const [filter, setFilter] = useState(readInitialFilter);
100
+ const [query, setQuery] = useState("");
101
+ const [expanded, setExpanded] = useState(null);
102
+ const [walkStep, setWalkStep] = useState(0);
103
+ const [openActionMenu, setOpenActionMenu] = useState(null);
104
+ const effectiveConfig = localConfig || workspaceConfig;
105
+ const helperRow = getHelperSandboxRow(effectiveConfig);
106
+ const helperConfigured = isHelperConfigured(effectiveConfig);
107
+ const helperHandoffDismissed = helperConfigured || isHelperHandoffDismissed(effectiveConfig);
108
+ const [setupOpen, setSetupOpen] = useState(false);
109
+ const [helperOpen, setHelperOpen] = useState(false);
110
+
111
+ useEffect(() => {
112
+ setLocalConfig(workspaceConfig);
113
+ }, [workspaceConfig]);
114
+
115
+ // URL params (?filter=, ?walkthrough=) are read on the client after mount —
116
+ // useState initializers run during SSR where window is undefined, so the
117
+ // deep-links from the contribution graph and the rail reveal land here.
118
+ useEffect(() => {
119
+ const f = readInitialFilter();
120
+ if (f !== "all") setFilter(f);
121
+ const w = readWalkthroughStep();
122
+ if (w) setWalkStep(w);
123
+ }, []);
124
+
125
+ const walkthrough = useMemo(
126
+ () => deriveLensWalkthroughState({ workspaceConfig, workspaceSourceRecords, metadataGraph }),
127
+ [workspaceConfig, workspaceSourceRecords, metadataGraph],
128
+ );
129
+ // Steps 2–3 show only when arrived via the guided reveal and still eligible
130
+ // (lens unlocked, no activity yet, not previously dismissed).
131
+ const showWalk = walkStep >= 2 && walkthrough.show;
132
+
133
+ const dismissWalkthrough = useMemo(() => async () => {
134
+ setWalkStep(0);
135
+ try {
136
+ const next = withUiCacheFlag(workspaceConfig || {}, LENS_WALKTHROUGH_DISMISS_FLAG, true);
137
+ await fetch("/api/workspace", {
138
+ method: "PATCH",
139
+ headers: { "content-type": "application/json" },
140
+ body: JSON.stringify({ dataModel: next.dataModel }),
141
+ });
142
+ } catch {
143
+ /* best-effort; local state already hides the tour */
144
+ }
145
+ }, [workspaceConfig]);
146
+
147
+ const onWalkPrimary = useMemo(() => (step) => {
148
+ if (step === 2) setWalkStep(3);
149
+ else dismissWalkthrough();
150
+ }, [dismissWalkthrough]);
151
+
152
+ const openHelperHandoff = useMemo(() => () => {
153
+ if (helperConfigured) {
154
+ setHelperOpen(true);
155
+ return;
156
+ }
157
+ setSetupOpen(true);
158
+ }, [helperConfigured]);
159
+
160
+ const contributions = useMemo(
161
+ () => deriveWorkspaceContributions({ workspaceConfig, workspaceSourceRecords, metadataGraph }),
162
+ [workspaceConfig, workspaceSourceRecords, metadataGraph],
163
+ );
164
+
165
+ const composed = useMemo(
166
+ () => deriveWorkspaceState({ workspaceConfig, workspaceSourceRecords, metadataGraph }),
167
+ [workspaceConfig, workspaceSourceRecords, metadataGraph],
168
+ );
169
+ const lenses = useMemo(() => Object.values(composed.lenses || {}), [composed]);
170
+
171
+ const counts = useMemo(() => {
172
+ let ready = 0; let blocked = 0; let assignable = 0;
173
+ for (const lens of lenses) {
174
+ const kind = lensStatusKind(lens);
175
+ if (kind === "ready") ready += 1;
176
+ if (kind === "blocked") blocked += 1;
177
+ if (!lens.complete && lens.nextStepId) assignable += 1;
178
+ }
179
+ return { total: lenses.length, ready, blocked, assignable };
180
+ }, [lenses]);
181
+
182
+ const visible = useMemo(() => {
183
+ const q = query.trim().toLowerCase();
184
+ return lenses.filter((lens) => {
185
+ const kind = lensStatusKind(lens);
186
+ if (filter === "blocked" && kind !== "blocked") return false;
187
+ if (filter === "ready" && kind !== "ready") return false;
188
+ if (filter === "assignable" && (lens.complete || !lens.nextStepId)) return false;
189
+ if (["persistence", "observability", "deploy", "tasks", "app-build"].includes(filter) && lens.lensId !== filter) return false;
190
+ if (!FILTERS.some((f) => f.id === filter) && lens.lensId !== filter) return false;
191
+ if (q) {
192
+ const hay = `${lens.title} ${lens.headline} ${(lens.steps || []).map((s) => s.label).join(" ")}`.toLowerCase();
193
+ if (!hay.includes(q)) return false;
194
+ }
195
+ return true;
196
+ });
197
+ }, [lenses, filter, query]);
198
+
199
+ const productionItems = useMemo(() => {
200
+ return lenses.flatMap((lens) => (lens.steps || []).map((step) => ({
201
+ id: `${lens.lensId}:${step.id}`,
202
+ label: step.cta || step.label,
203
+ complete: step.status === "complete",
204
+ href: step.href || `/workspace-lens?filter=${lens.lensId}`,
205
+ }))).slice(0, 5);
206
+ }, [lenses]);
207
+
208
+ const productionCounts = useMemo(() => {
209
+ const steps = lenses.flatMap((lens) => lens.steps || []);
210
+ return {
211
+ total: steps.length,
212
+ complete: steps.filter((step) => step.status === "complete").length,
213
+ };
214
+ }, [lenses]);
215
+
216
+ const observabilityStats = useMemo(() => {
217
+ const objects = Array.isArray(effectiveConfig?.dataModel?.objects) ? effectiveConfig.dataModel.objects : [];
218
+ const sourceCount = workspaceSourceRecords && typeof workspaceSourceRecords === "object"
219
+ ? Object.keys(workspaceSourceRecords).length
220
+ : 0;
221
+ const sandboxCount = objects.filter((object) => object?.objectType === "sandbox-environment").length;
222
+ return [
223
+ { label: "Ready lenses", value: counts.ready },
224
+ { label: "Open actions", value: counts.assignable },
225
+ { label: "Sandbox environments", value: sandboxCount },
226
+ { label: "Source records", value: sourceCount },
227
+ ];
228
+ }, [counts.assignable, counts.ready, effectiveConfig, workspaceSourceRecords]);
229
+
230
+ const helperStatusLabel = helperConfigured
231
+ ? `Agent connected: ${helperRow?.agentHost || "local agent"}`
232
+ : "Helper setup needed";
233
+
234
+ const copyLensUrl = (lensId) => {
235
+ if (typeof window === "undefined" || !navigator?.clipboard) return;
236
+ navigator.clipboard.writeText(`${window.location.origin}/workspace-lens?filter=${lensId}`);
237
+ };
238
+
239
+ return (
240
+ <div className="workspace-lens">
241
+ <header className="workspace-lens-head">
242
+ <div>
243
+ <h1 className="workspace-lens-title">Workspace Lens</h1>
244
+ <p className="workspace-lens-subtitle">Live derived state for this workspace.</p>
245
+ </div>
246
+ <p className="workspace-lens-score" aria-label="Workspace lens summary">
247
+ {counts.total} lenses · {counts.ready} ready · {counts.blocked} blocked · {counts.assignable} agent-assignable
248
+ </p>
249
+ </header>
250
+
251
+ {showWalk ? (
252
+ <WorkspaceLensWalkthrough
253
+ step={walkStep}
254
+ className="is-panel"
255
+ onPrimary={onWalkPrimary}
256
+ onDismiss={dismissWalkthrough}
257
+ />
258
+ ) : null}
259
+
260
+ {!helperHandoffDismissed ? (
261
+ <div className="workspace-lens-helper-callout" role="note">
262
+ <div className="workspace-lens-helper-callout-text">
263
+ <p className="workspace-lens-helper-callout-title">Connect the helper</p>
264
+ <p className="workspace-lens-helper-callout-body">
265
+ Pick the agent that powers the helper widget. Codex is recommended.
266
+ </p>
267
+ </div>
268
+ <button
269
+ type="button"
270
+ className="workspace-lens-helper-callout-btn"
271
+ onClick={openHelperHandoff}
272
+ >
273
+ {helperConfigured ? "Open helper" : "Set up helper"}
274
+ </button>
275
+ </div>
276
+ ) : null}
277
+
278
+ <WorkspaceHelperSetupModal
279
+ workspaceConfig={effectiveConfig}
280
+ open={setupOpen}
281
+ onClose={() => setSetupOpen(false)}
282
+ onSaved={(nextConfig) => {
283
+ setLocalConfig(nextConfig);
284
+ setSetupOpen(false);
285
+ setHelperOpen(true);
286
+ }}
287
+ />
288
+
289
+ <WorkspaceContributionGraph
290
+ data={contributions}
291
+ onSelectDay={() => setFilter("observability")}
292
+ buildDayHref={(date) => `/workspace-lens?filter=runs&day=${date}`}
293
+ />
294
+
295
+ <div className="workspace-lens-controls workspace-builder-filterbar">
296
+ <div className="workspace-lens-filters workspace-builder-filterbar__segments" role="tablist" aria-label="Filter lenses">
297
+ {FILTERS.map((f) => (
298
+ <button
299
+ key={f.id}
300
+ type="button"
301
+ role="tab"
302
+ aria-selected={filter === f.id}
303
+ className={"workspace-lens-filter" + (filter === f.id ? " is-active" : "")}
304
+ onClick={() => setFilter(f.id)}
305
+ >
306
+ {f.label}
307
+ </button>
308
+ ))}
309
+ </div>
310
+ <label className="workspace-builder-filterbar__search workspace-lens-search-wrap">
311
+ <Search size={14} aria-hidden="true" />
312
+ <input
313
+ type="text"
314
+ className="workspace-lens-search"
315
+ placeholder="Search lenses, workflows, objects"
316
+ value={query}
317
+ onChange={(e) => setQuery(e.target.value)}
318
+ aria-label="Search lenses"
319
+ />
320
+ </label>
321
+ </div>
322
+
323
+ <section className="workspace-lens-control-grid" aria-label="Workspace control panel">
324
+ <article className="workspace-lens-control-card">
325
+ <div className="workspace-lens-control-card-head">
326
+ <div>
327
+ <h2>Production Checklist</h2>
328
+ <span>{productionCounts.complete}/{productionCounts.total}</span>
329
+ </div>
330
+ <button type="button" className="workspace-lens-icon-btn" aria-label="Checklist options">
331
+ <MoreVertical size={15} aria-hidden="true" />
332
+ </button>
333
+ </div>
334
+ <div className="workspace-lens-checklist" role="list">
335
+ {productionItems.map((item) => (
336
+ <Link
337
+ key={item.id}
338
+ href={item.href}
339
+ className={"workspace-lens-check-item" + (item.complete ? " is-complete" : "")}
340
+ >
341
+ <span>{item.label}</span>
342
+ {item.complete ? <Check size={14} aria-hidden="true" /> : null}
343
+ </Link>
344
+ ))}
345
+ </div>
346
+ </article>
347
+
348
+ <article className="workspace-lens-control-card">
349
+ <div className="workspace-lens-control-card-head">
350
+ <div>
351
+ <h2>Observability</h2>
352
+ <span>Live</span>
353
+ </div>
354
+ <button type="button" className="workspace-lens-icon-btn" aria-label="Observability options">
355
+ <MoreVertical size={15} aria-hidden="true" />
356
+ </button>
357
+ </div>
358
+ <div className="workspace-lens-stat-list">
359
+ {observabilityStats.map((stat) => (
360
+ <div key={stat.label} className="workspace-lens-stat-row">
361
+ <span>{stat.label}</span>
362
+ <strong>{stat.value}</strong>
363
+ </div>
364
+ ))}
365
+ </div>
366
+ </article>
367
+
368
+ <article className="workspace-lens-control-card workspace-lens-helper-card">
369
+ <div className="workspace-lens-control-card-head">
370
+ <div>
371
+ <h2>Helper Analytics</h2>
372
+ <span>{helperConfigured ? "Active" : "Setup"}</span>
373
+ </div>
374
+ <button type="button" className="workspace-lens-icon-btn" aria-label="Helper options">
375
+ <MoreVertical size={15} aria-hidden="true" />
376
+ </button>
377
+ </div>
378
+ <div className="workspace-lens-helper-card-body">
379
+ <BarChart3 size={28} aria-hidden="true" />
380
+ <strong>{helperStatusLabel}</strong>
381
+ <p>Workspace Lens actions run through the same helper widget sandbox.</p>
382
+ <button type="button" onClick={openHelperHandoff}>
383
+ {helperConfigured ? "Open helper" : "Set up helper"}
384
+ </button>
385
+ </div>
386
+ </article>
387
+ </section>
388
+
389
+ <section className="workspace-lens-branches" aria-label="Active branches">
390
+ <div className="workspace-lens-branches-head">
391
+ <h2>Active Branches</h2>
392
+ <span>{visible.length}/{lenses.length}</span>
393
+ </div>
394
+ <div className="workspace-lens-branches-table" role="table">
395
+ {visible.map((lens) => {
396
+ const kind = lensStatusKind(lens);
397
+ const next = (lens.steps || []).find((s) => s.id === lens.nextStepId) || null;
398
+ const blockedStep = (lens.steps || []).find((s) => s.status === "blocked") || null;
399
+ const isOpen = openActionMenu === lens.lensId;
400
+ const packet = expanded === lens.lensId
401
+ ? deriveSwarmConditionPacket({ workspaceConfig, workspaceSourceRecords, metadataGraph }, { lensId: lens.lensId })
402
+ : null;
403
+ return (
404
+ <div key={lens.lensId} className="workspace-lens-branch-row" role="row" data-lens={lens.lensId}>
405
+ <div className="workspace-lens-branch-name" role="cell">
406
+ <GitBranch size={14} aria-hidden="true" />
407
+ <span>{lens.title}</span>
408
+ </div>
409
+ <div className="workspace-lens-branch-actions" role="cell">
410
+ {next?.href ? (
411
+ <Link href={next.href} className="workspace-lens-preview-pill">
412
+ <Eye size={12} aria-hidden="true" />
413
+ Preview
414
+ </Link>
415
+ ) : (
416
+ <span className="workspace-lens-preview-pill">
417
+ <Activity size={12} aria-hidden="true" />
418
+ Healthy
419
+ </span>
420
+ )}
421
+ <span className={"workspace-lens-chip is-" + kind}>{STATUS_LABEL[kind]}</span>
422
+ <span className="workspace-lens-progress-pill">{lens.completedCount}/{lens.totalCount}</span>
423
+ <span className="workspace-lens-owner-pill">workspace-lens</span>
424
+ <button
425
+ type="button"
426
+ className="workspace-lens-icon-btn"
427
+ aria-label={`${lens.title} actions`}
428
+ aria-expanded={isOpen}
429
+ onClick={() => setOpenActionMenu(isOpen ? null : lens.lensId)}
430
+ >
431
+ <MoreVertical size={15} aria-hidden="true" />
432
+ </button>
433
+ {isOpen ? (
434
+ <div className="workspace-lens-action-menu">
435
+ <button type="button" onClick={() => { setExpanded(lens.lensId); setOpenActionMenu(null); }}>
436
+ View condition packet
437
+ </button>
438
+ {next?.href ? <Link href={next.href}>Open next action</Link> : null}
439
+ <button type="button" onClick={() => { setFilter(lens.lensId); setOpenActionMenu(null); }}>
440
+ Filter to this lens
441
+ </button>
442
+ <button type="button" onClick={() => copyLensUrl(lens.lensId)}>
443
+ Copy Lens URL <Copy size={13} aria-hidden="true" />
444
+ </button>
445
+ </div>
446
+ ) : null}
447
+ </div>
448
+ <p className="workspace-lens-branch-summary">{lens.headline}</p>
449
+ {!lens.complete && next ? (
450
+ <p className="workspace-lens-branch-next">Next: {next.cta || next.label}</p>
451
+ ) : null}
452
+ {blockedStep ? (
453
+ <p className="workspace-lens-branch-next">Blocked: {blockedStep.label}</p>
454
+ ) : null}
455
+ {packet ? (
456
+ <div className="workspace-lens-agent">
457
+ <p className="workspace-lens-agent-title">
458
+ {lens.complete ? "Agent condition (resolved)" : "Assignable to an agent"}
459
+ </p>
460
+ <p className="workspace-lens-agent-row"><span>Goal</span>{packet.goal}</p>
461
+ <p className="workspace-lens-agent-row"><span>State</span>{packet.currentState}</p>
462
+ {packet.prerequisite ? (
463
+ <p className="workspace-lens-agent-row"><span>Prerequisite</span>{packet.prerequisite}</p>
464
+ ) : null}
465
+ <p className="workspace-lens-agent-row"><span>Tools</span>{packet.availableTools.join(" · ")}</p>
466
+ <p className="workspace-lens-agent-row"><span>Evidence</span>{packet.expectedEvidence.join(" · ")}</p>
467
+ </div>
468
+ ) : null}
469
+ </div>
470
+ );
471
+ })}
472
+ {visible.length === 0 ? (
473
+ <div className="workspace-lens-empty">No lenses match this filter.</div>
474
+ ) : null}
475
+ </div>
476
+ </section>
477
+
478
+ <HelperSidecar
479
+ open={helperOpen}
480
+ onClose={() => setHelperOpen(false)}
481
+ workspaceConfig={effectiveConfig}
482
+ initialIntent="explain"
483
+ initialPrompt=""
484
+ onApplied={(nextConfig) => setLocalConfig(nextConfig)}
485
+ />
486
+ </div>
487
+ );
488
+ }
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ /**
4
+ * WorkspaceLensWalkthrough — one-time guided reveal of Workspace Lens.
5
+ *
6
+ * A small, calm popover (white bg, 1px grey border, 5px radius, soft shadow)
7
+ * with a single X (top-right) to dismiss and a step counter + primary action
8
+ * (bottom-right). It is rendered only in the in-between state (onboarding
9
+ * complete, lens unlocked, no activity yet) and is dismissed permanently via
10
+ * the same workspace-ui-cache flag the onboarding dismiss uses.
11
+ *
12
+ * Three steps across the handoff:
13
+ * 1. (anchored to the new Workspace Lens nav item) — the reveal.
14
+ * 2. (on the lens surface) — what this is. Don't overwhelm.
15
+ * 3. (on the lens surface) — take a first action; the daily ritual.
16
+ */
17
+
18
+ import { X } from "lucide-react";
19
+
20
+ const STEPS = {
21
+ 1: {
22
+ eyebrow: "New",
23
+ title: "Workspace Lens is unlocked",
24
+ body: "You finished setup — your workspace now has a live operating surface. Take a look.",
25
+ cta: "Open Workspace Lens",
26
+ },
27
+ 2: {
28
+ eyebrow: "Workspace Lens",
29
+ title: "This is your live workspace state",
30
+ body: "A derived view of what's healthy, what's blocked, and what's ready to act on. Nothing to configure — it reflects reality.",
31
+ cta: "Next",
32
+ },
33
+ 3: {
34
+ eyebrow: "Daily ritual",
35
+ title: "Start each session here",
36
+ body: "Check your activity graph, then act on the top blocked or agent-assignable item. That's the loop.",
37
+ cta: "Got it",
38
+ },
39
+ };
40
+
41
+ export function WorkspaceLensWalkthrough({ step = 1, onPrimary, onDismiss, className, style }) {
42
+ const s = STEPS[step] || STEPS[1];
43
+ return (
44
+ <div
45
+ className={"workspace-lens-walkthrough" + (className ? " " + className : "")}
46
+ style={style}
47
+ role="dialog"
48
+ aria-label="Workspace Lens walkthrough"
49
+ >
50
+ <button
51
+ type="button"
52
+ className="workspace-lens-walkthrough-x"
53
+ aria-label="Dismiss walkthrough"
54
+ onClick={onDismiss}
55
+ >
56
+ <X size={13} aria-hidden="true" />
57
+ </button>
58
+ {s.eyebrow ? <p className="workspace-lens-walkthrough-eyebrow">{s.eyebrow}</p> : null}
59
+ <h3 className="workspace-lens-walkthrough-title">{s.title}</h3>
60
+ <p className="workspace-lens-walkthrough-body">{s.body}</p>
61
+ <div className="workspace-lens-walkthrough-footer">
62
+ <span className="workspace-lens-walkthrough-steps">{step} / 3</span>
63
+ <button type="button" className="workspace-lens-walkthrough-next" onClick={() => onPrimary?.(step)}>
64
+ {s.cta}
65
+ </button>
66
+ </div>
67
+ </div>
68
+ );
69
+ }
@@ -42,6 +42,11 @@ import {
42
42
  Wrench as RepairIcon,
43
43
  X,
44
44
  } from "lucide-react";
45
+ import {
46
+ HELPER_SANDBOX_OBJECT_ID,
47
+ isHelperConfigured,
48
+ WorkspaceHelperSetupModal,
49
+ } from "../../components/WorkspaceHelperSetupModal.jsx";
45
50
 
46
51
  // Generic "Tool Call Output" title matches the reference grammar — the
47
52
  // user already sees the prompt + assistant response in the chat above,
@@ -229,8 +234,10 @@ let persistedWidth = 420;
229
234
 
230
235
  function resolveSandboxEnvRow(workspaceConfig) {
231
236
  const objects = workspaceConfig?.dataModel?.objects || [];
237
+ const helper = objects.find((obj) => obj?.id === HELPER_SANDBOX_OBJECT_ID && obj?.objectType === "sandbox-environment");
238
+ if (Array.isArray(helper?.rows) && helper.rows.length > 0) return helper.rows[0];
232
239
  for (const obj of objects) {
233
- if (obj.objectType === "sandbox-environment" && Array.isArray(obj.rows) && obj.rows.length > 0) {
240
+ if (obj.id !== HELPER_SANDBOX_OBJECT_ID && obj.objectType === "sandbox-environment" && Array.isArray(obj.rows) && obj.rows.length > 0) {
234
241
  return obj.rows[0];
235
242
  }
236
243
  }
@@ -306,6 +313,7 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
306
313
  const [setupSaveError, setSetupSaveError] = useState("");
307
314
  const [setupSaveOk, setSetupSaveOk] = useState(false);
308
315
  const [copiedCommand, setCopiedCommand] = useState(false);
316
+ const [agentSetupOpen, setAgentSetupOpen] = useState(false);
309
317
 
310
318
  // Drag state
311
319
  const [panelWidth, setPanelWidth] = useState(persistedWidth);
@@ -611,6 +619,7 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
611
619
  }, [open, activeTab]);
612
620
 
613
621
  const sandboxRow = resolveSandboxEnvRow(workspaceConfig);
622
+ const helperAgentConfigured = isHelperConfigured(workspaceConfig);
614
623
  const liveModel = sandboxRow?.localModel || "";
615
624
  const liveEndpoint = sandboxRow?.localEndpoint || "";
616
625
  const liveAdapter = sandboxRow?.intelligenceAdapterMode || "ollama";
@@ -1193,9 +1202,26 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
1193
1202
  {activeTab === "setup" && (
1194
1203
  <div className="dm-sidecar-body dm-helper-setup-body">
1195
1204
  <p className="dm-helper-setup-intro">
1196
- The helper sends your prompt to a local model. Credentials are never stored in the workspace.
1205
+ Connect the helper to Codex, Claude, or another local agent. Advanced local model setup stays below.
1197
1206
  </p>
1198
1207
 
1208
+ <div className={`dm-helper-setup-status state-${helperAgentConfigured ? "connected" : "unconfigured"}`}>
1209
+ <div className="dm-helper-setup-status-row">
1210
+ <span className={`dm-connection-dot dm-connection-${helperAgentConfigured ? "ok" : "amber"}`} />
1211
+ <span className="dm-helper-setup-status-label">
1212
+ {helperAgentConfigured ? `Agent connected: ${sandboxRow?.agentHost}` : "No helper agent configured yet"}
1213
+ </span>
1214
+ <button type="button" className="dm-helper-setup-recheck" onClick={() => setAgentSetupOpen(true)}>
1215
+ {helperAgentConfigured ? "Change agent" : "Set up agent"}
1216
+ </button>
1217
+ </div>
1218
+ <span className="dm-helper-setup-status-meta">
1219
+ Uses workspace-helper-sandbox for the same helper widget.
1220
+ </span>
1221
+ </div>
1222
+
1223
+ <p className="dm-helper-setup-intro">Advanced local model fallback.</p>
1224
+
1199
1225
  <div
1200
1226
  className={`dm-helper-setup-status state-${setupStatusState}`}
1201
1227
  data-connection-status=""
@@ -1321,6 +1347,15 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
1321
1347
  {copiedCommand ? "Copied" : "Copy command"}
1322
1348
  </button>
1323
1349
  </div>
1350
+ <WorkspaceHelperSetupModal
1351
+ workspaceConfig={workspaceConfig}
1352
+ open={agentSetupOpen}
1353
+ onClose={() => setAgentSetupOpen(false)}
1354
+ onSaved={(nextConfig) => {
1355
+ setAgentSetupOpen(false);
1356
+ if (onApplied) onApplied(nextConfig);
1357
+ }}
1358
+ />
1324
1359
  </div>
1325
1360
  )}
1326
1361
  </aside>