@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.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +98 -34
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/swarm-condition/route.js +106 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceContributionGraph.jsx +119 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +357 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensPanel.jsx +488 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensWalkthrough.jsx +69 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +37 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +267 -25
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +55 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-lens/page.jsx +76 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +140 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +1025 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +2 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +24 -8
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -0
- package/dist/index.js +5224 -5225
- 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
|
-
|
|
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>
|