@growthub/cli 0.13.7 → 0.13.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/codex-sites/route.js +13 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +98 -34
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/swarm-condition/route.js +106 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceContributionGraph.jsx +119 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +357 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensPanel.jsx +488 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensWalkthrough.jsx +69 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +105 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +37 -2
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +382 -32
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/codex-sites-data-model-card.jsx +81 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/page.jsx +31 -14
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/settings-accordion-section.jsx +50 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +192 -7
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-lens/page.jsx +76 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +140 -4
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/codex-sites-local-state.js +139 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/codex-sites-workspace-adapter.js +156 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +1025 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +2 -3
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +24 -8
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -0
  24. package/dist/index.js +5224 -5225
  25. 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
+ }