@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
@@ -3,6 +3,8 @@ import { promises as fs } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { readWorkspaceConfig } from "@/lib/workspace-config";
5
5
  import { AppsList } from "./apps-list.jsx";
6
+ import { CodexSitesDataModelCard } from "./codex-sites-data-model-card.jsx";
7
+ import { SettingsAccordionGroup, SettingsAccordionSection } from "./settings-accordion-section.jsx";
6
8
 
7
9
  async function readForkMetadata() {
8
10
  try {
@@ -82,24 +84,39 @@ async function AppsSettingsPage() {
82
84
  <div className="workspace-settings-card-heading">
83
85
  <div>
84
86
  <h2>Apps</h2>
85
- <p>Read-only workspace app, bridge, and fork metadata already available to this workspace.</p>
87
+ <p>Workspace apps discovered from the local apps directory and governed Data Model configuration.</p>
86
88
  </div>
87
89
  </div>
88
90
 
89
- <section className="workspace-settings-section workspace-apps-linkage-section">
90
- <h3>Workspace Linkage</h3>
91
- <div className="workspace-settings-kv">
92
- <span>Workspace</span><code>{workspaceConfig.id || "workspace-builder-default"}</code>
93
- <span>Fork</span><code>{fork?.forkId || "local fork metadata unavailable"}</code>
94
- <span>Kit</span><code>{fork?.kitId || workspaceConfig.provenance?.mirrors || "growthub-custom-workspace-starter-v1"}</code>
95
- <span>Bridge</span><code>{bridge?.status || bridge?.id || "not connected"}</code>
96
- </div>
97
- </section>
91
+ <SettingsAccordionGroup defaultOpenId="workspace-apps">
92
+ <SettingsAccordionSection
93
+ id="workspace-apps"
94
+ title="Workspace Apps"
95
+ summary={`${apps.length} app${apps.length === 1 ? "" : "s"} discovered from directory and config.`}
96
+ className="workspace-apps-list-section"
97
+ >
98
+ <AppsList apps={apps} />
99
+ </SettingsAccordionSection>
100
+
101
+ <SettingsAccordionSection
102
+ id="workspace-linkage"
103
+ title="Workspace Linkage"
104
+ summary="Fork, kit, and bridge identity for this workspace."
105
+ className="workspace-apps-linkage-section"
106
+ >
107
+ <div className="workspace-settings-kv">
108
+ <span>Workspace</span><code>{workspaceConfig.id || "workspace-builder-default"}</code>
109
+ <span>Fork</span><code>{fork?.forkId || "local fork metadata unavailable"}</code>
110
+ <span>Kit</span><code>{fork?.kitId || workspaceConfig.provenance?.mirrors || "growthub-custom-workspace-starter-v1"}</code>
111
+ <span>Bridge</span><code>{bridge?.status || bridge?.id || "not connected"}</code>
112
+ </div>
113
+ </SettingsAccordionSection>
98
114
 
99
- <section className="workspace-settings-section workspace-apps-list-section">
100
- <h3>Workspace Apps</h3>
101
- <AppsList apps={apps} />
102
- </section>
115
+ <CodexSitesDataModelCard
116
+ apps={apps}
117
+ dataModel={workspaceConfig.dataModel || {}}
118
+ />
119
+ </SettingsAccordionGroup>
103
120
  </section>
104
121
  </SettingsShell>;
105
122
  }
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, useState } from "react";
4
+ import { ChevronDown } from "lucide-react";
5
+
6
+ const SettingsAccordionContext = createContext(null);
7
+
8
+ function SettingsAccordionGroup({ defaultOpenId, children }) {
9
+ const [openId, setOpenId] = useState(defaultOpenId || null);
10
+ return <SettingsAccordionContext.Provider value={{ openId, setOpenId }}>
11
+ {children}
12
+ </SettingsAccordionContext.Provider>;
13
+ }
14
+
15
+ function SettingsAccordionSection({ id, title, summary, className = "", defaultOpen = false, children }) {
16
+ const group = useContext(SettingsAccordionContext);
17
+ const [localOpen, setLocalOpen] = useState(defaultOpen);
18
+ const open = group ? group.openId === id : localOpen;
19
+ const sectionClass = [
20
+ "workspace-settings-section",
21
+ "workspace-settings-accordion",
22
+ open ? "is-open" : "is-collapsed",
23
+ className
24
+ ].filter(Boolean).join(" ");
25
+ const toggle = () => {
26
+ if (group) {
27
+ group.setOpenId(open ? null : id);
28
+ return;
29
+ }
30
+ setLocalOpen((value) => !value);
31
+ };
32
+
33
+ return <section className={sectionClass}>
34
+ <button
35
+ type="button"
36
+ className="workspace-settings-accordion-trigger"
37
+ aria-expanded={open}
38
+ onClick={toggle}
39
+ >
40
+ <span>
41
+ <h3>{title}</h3>
42
+ {summary ? <em>{summary}</em> : null}
43
+ </span>
44
+ <ChevronDown size={16} aria-hidden="true" />
45
+ </button>
46
+ {open ? <div className="workspace-settings-accordion-body">{children}</div> : null}
47
+ </section>;
48
+ }
49
+
50
+ export { SettingsAccordionGroup, SettingsAccordionSection };
@@ -45,6 +45,7 @@ import {
45
45
  Plus,
46
46
  Quote,
47
47
  RefreshCw,
48
+ Rocket,
48
49
  Rows3,
49
50
  Save,
50
51
  Search,
@@ -59,6 +60,8 @@ import {
59
60
  Trash2,
60
61
  Type,
61
62
  Users,
63
+ Eye,
64
+ Wrench,
62
65
  X,
63
66
  Zap,
64
67
  } from "lucide-react";
@@ -87,6 +90,11 @@ import {
87
90
  } from "@/lib/workspace-chart-values";
88
91
  import { selectObjectFilterableFields, selectObjectSortableFields } from "@/lib/workspace-metadata-selectors";
89
92
  import { deriveWorkspaceActivationState } from "@/lib/workspace-activation";
93
+ import {
94
+ CODEX_SITES_OBJECT_ID,
95
+ ensureCodexSitesDataModel,
96
+ isCodexSiteUrl
97
+ } from "@/lib/codex-sites-workspace-adapter";
90
98
  import { HelperSidecar } from "./data-model/components/HelperSidecar.jsx";
91
99
  import { WorkspaceRail } from "./workspace-rail.jsx";
92
100
  import { WorkspaceActivationPanel } from "./components/WorkspaceActivationPanel.jsx";
@@ -501,6 +509,34 @@ function listBuilderWorkflowItems(config) {
501
509
  });
502
510
  }
503
511
 
512
+ function listBuilderSiteItems(config) {
513
+ const object = getDataModelObject(config, CODEX_SITES_OBJECT_ID);
514
+ const rows = Array.isArray(object?.rows) ? object.rows : [];
515
+ return rows.flatMap((row, index) => {
516
+ const url = String(row?.url || "").trim();
517
+ if (!isCodexSiteUrl(url)) return [];
518
+ const title = String(row?.Name || row?.name || `Codex Site ${index + 1}`).trim();
519
+ const rawStatus = String(row?.status || "").trim().toLowerCase();
520
+ const status = rawStatus === "active" ? "live" : rawStatus || "draft";
521
+ return [{
522
+ type: "site",
523
+ id: String(row?.id || row?.Name || `codex-site-${index + 1}`),
524
+ title,
525
+ itemKind: "Site",
526
+ updatedAt: formatBuilderTimestamp(row?.lastRecordedAt || ""),
527
+ status,
528
+ site: {
529
+ row,
530
+ rowIndex: index,
531
+ title,
532
+ url,
533
+ app: String(row?.app || "apps/workspace").trim(),
534
+ client: String(row?.client || "Workspace").trim()
535
+ }
536
+ }];
537
+ });
538
+ }
539
+
504
540
  function formatBuilderTimestamp(value) {
505
541
  const raw = String(value || "").trim();
506
542
  if (!raw || raw === "new") return raw;
@@ -4244,6 +4280,7 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
4244
4280
  const canvas = config.canvas;
4245
4281
  const dashboards = config.dashboards || [];
4246
4282
  const workflows = useMemo(() => listBuilderWorkflowItems(config), [config]);
4283
+ const sites = useMemo(() => listBuilderSiteItems(config), [config]);
4247
4284
  const builderItems = useMemo(() => {
4248
4285
  const dashboardItems = dashboards.map((dashboard, index) => ({
4249
4286
  type: "dashboard",
@@ -4264,13 +4301,17 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
4264
4301
  status: workflow.lifecycleStatus || "draft",
4265
4302
  workflow
4266
4303
  }));
4304
+ const siteItems = sites.map((site) => ({
4305
+ ...site,
4306
+ updatedAt: site.updatedAt || "new"
4307
+ }));
4267
4308
  const q = builderListFilter.query.trim().toLowerCase();
4268
- return [...dashboardItems, ...workflowItems].filter((item) => {
4309
+ return [...dashboardItems, ...siteItems, ...workflowItems].filter((item) => {
4269
4310
  if (builderListFilter.type !== "all" && item.type !== builderListFilter.type) return false;
4270
4311
  if (!q) return true;
4271
- return [item.title, item.itemKind, item.status, item.type].some((part) => String(part || "").toLowerCase().includes(q));
4312
+ return [item.title, item.itemKind, item.status, item.type, item.site?.client, item.site?.app, item.site?.url].some((part) => String(part || "").toLowerCase().includes(q));
4272
4313
  });
4273
- }, [builderListFilter, dashboards, workflows]);
4314
+ }, [builderListFilter, dashboards, sites, workflows]);
4274
4315
  const resolvedActiveDashboardId = getActiveDashboardId(dashboards, activeDashboardId);
4275
4316
  const resolvedActiveDashboardIndex = activeDashboardIndex(dashboards, resolvedActiveDashboardId);
4276
4317
  const widgetTypes = config.widgetTypes;
@@ -4316,6 +4357,18 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
4316
4357
  workspaceConfig: config,
4317
4358
  workspaceSourceRecords,
4318
4359
  }), [config, workspaceSourceRecords]);
4360
+ // Safe runtime descriptor for the secondary readiness lenses — assembled from
4361
+ // the persistence/adapter props the builder already receives (no fetch, no
4362
+ // secrets; booleans only).
4363
+ const lensMetadataGraph = useMemo(() => ({
4364
+ runtime: {
4365
+ persistenceMode: persistence?.mode || "",
4366
+ persistenceAdapter: persistence?.mode === "database" ? (adapterConfig?.dataAdapter || null) : null,
4367
+ allowFsWrite: persistence?.mode === "filesystem" && persistence?.canSave === true,
4368
+ nangoConfigured: Boolean(adapterConfig?.nango?.hasSecretKey),
4369
+ deploy: { target: adapterConfig?.deployTarget || "" },
4370
+ },
4371
+ }), [persistence, adapterConfig]);
4319
4372
  const activationStarted = activationState.completedCount > 0;
4320
4373
  const activationComplete = Boolean(activationState.complete);
4321
4374
  const activationUiCache = useMemo(() => getWorkspaceUiCache(config), [config]);
@@ -4695,6 +4748,50 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
4695
4748
  }
4696
4749
  }, [config, saving]);
4697
4750
 
4751
+ const createCodexSite = useCallback(async () => {
4752
+ if (saving) return;
4753
+ const objects = Array.isArray(config.dataModel?.objects) ? config.dataModel.objects : [];
4754
+ const hasCodexSitesObject = objects.some((object) => object?.id === CODEX_SITES_OBJECT_ID);
4755
+ if (hasCodexSitesObject) {
4756
+ window.open(`/data-model?object=${encodeURIComponent(CODEX_SITES_OBJECT_ID)}`, "_self");
4757
+ return;
4758
+ }
4759
+ const nextDataModel = ensureCodexSitesDataModel(config.dataModel, []);
4760
+ setSaving(true);
4761
+ try {
4762
+ const response = await fetch("/api/workspace", {
4763
+ method: "PATCH",
4764
+ headers: { "content-type": "application/json" },
4765
+ body: JSON.stringify({ dataModel: nextDataModel })
4766
+ });
4767
+ const payload = await response.json();
4768
+ if (!response.ok || !payload.workspaceConfig) {
4769
+ throw new Error(payload.error || "Failed to create Codex Sites object");
4770
+ }
4771
+ setConfig((prev) => ({ ...prev, dataModel: payload.workspaceConfig.dataModel }));
4772
+ setBuilderListFilter({ type: "site", query: "" });
4773
+ setConfigMessage("Created Codex Sites object");
4774
+ window.open(`/data-model?object=${encodeURIComponent(CODEX_SITES_OBJECT_ID)}`, "_self");
4775
+ } catch (error) {
4776
+ setConfigMessage(error.message || "Failed to create Codex Sites object");
4777
+ } finally {
4778
+ setSaving(false);
4779
+ }
4780
+ }, [config, saving]);
4781
+
4782
+ const manageSite = useCallback((site) => {
4783
+ const rowParam = site?.rowIndex !== undefined ? `&row=${encodeURIComponent(String(site.rowIndex))}` : "";
4784
+ window.open(`/data-model?object=${encodeURIComponent(CODEX_SITES_OBJECT_ID)}${rowParam}`, "_self");
4785
+ }, []);
4786
+
4787
+ const openSite = useCallback((site) => {
4788
+ if (site?.url) {
4789
+ window.open(site.url, "_blank", "noopener,noreferrer");
4790
+ return;
4791
+ }
4792
+ manageSite(site);
4793
+ }, [manageSite]);
4794
+
4698
4795
  const selectDashboard = useCallback((index) => {
4699
4796
  setConfig((prev) => {
4700
4797
  const synced = syncActiveDashboard(prev, activeDashboardId);
@@ -5220,7 +5317,7 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5220
5317
  }
5221
5318
  const rect = event.currentTarget.getBoundingClientRect();
5222
5319
  const menuWidth = 148;
5223
- const menuHeight = item?.type === "dashboard" ? 136 : 76;
5320
+ const menuHeight = item?.type === "dashboard" ? 136 : item?.type === "site" ? 108 : 76;
5224
5321
  const margin = 8;
5225
5322
  const left = Math.min(
5226
5323
  Math.max(margin, rect.right - menuWidth),
@@ -5752,6 +5849,40 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5752
5849
  id: "workspace.builder", group: "Navigation", icon: Home, label: "Go to Builder",
5753
5850
  run: () => showDashboardHome()
5754
5851
  });
5852
+ list.push({
5853
+ id: "nav.data-model", group: "Navigation", icon: Database, label: "Go to Management (Data Model)",
5854
+ run: () => { window.location.href = "/data-model"; }
5855
+ });
5856
+
5857
+ // Workspace Lens — fast navigation into the post-activation operating
5858
+ // surface and its filtered views. Unlocks once activation completes.
5859
+ const lensReady = Boolean(activationState?.complete);
5860
+ list.push({
5861
+ id: "lens.open", group: "Workspace Lens", icon: Eye,
5862
+ label: lensReady ? "Open Workspace Lens" : "Workspace Lens (finish setup to unlock)",
5863
+ disabled: !lensReady,
5864
+ run: () => { window.location.href = "/workspace-lens"; }
5865
+ });
5866
+ list.push({
5867
+ id: "lens.blocked", group: "Workspace Lens", icon: Eye, label: "Workspace Lens — Blocked",
5868
+ disabled: !lensReady,
5869
+ run: () => { window.location.href = "/workspace-lens?filter=blocked"; }
5870
+ });
5871
+ list.push({
5872
+ id: "lens.ready", group: "Workspace Lens", icon: Eye, label: "Workspace Lens — Ready",
5873
+ disabled: !lensReady,
5874
+ run: () => { window.location.href = "/workspace-lens?filter=ready"; }
5875
+ });
5876
+ list.push({
5877
+ id: "lens.assignable", group: "Workspace Lens", icon: Eye, label: "Workspace Lens — Agent-assignable",
5878
+ disabled: !lensReady,
5879
+ run: () => { window.location.href = "/workspace-lens?filter=assignable"; }
5880
+ });
5881
+ list.push({
5882
+ id: "lens.runs", group: "Workspace Lens", icon: Eye, label: "Workspace Lens — Runs",
5883
+ disabled: !lensReady,
5884
+ run: () => { window.location.href = "/workspace-lens?filter=runs"; }
5885
+ });
5755
5886
 
5756
5887
  return list;
5757
5888
  }, [
@@ -5770,7 +5901,8 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5770
5901
  saving,
5771
5902
  selectedWidget,
5772
5903
  showDashboardHome,
5773
- workspaceView
5904
+ workspaceView,
5905
+ activationState
5774
5906
  ]);
5775
5907
 
5776
5908
  return <main className="workspace-builder" onPointerDownCapture={resetWidgetSelectionOnOutsidePointer} style={builderStyle}>
@@ -5798,10 +5930,12 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5798
5930
  dashboardsSlot={(
5799
5931
  <button
5800
5932
  type="button"
5933
+ title="Builder"
5801
5934
  className={workspaceView === "dashboards" ? "active workspace-nav-button" : "workspace-nav-button"}
5802
5935
  onClick={showDashboardHome}
5803
5936
  >
5804
- Builder
5937
+ <Wrench size={15} aria-hidden="true" />
5938
+ <span className="workspace-nav-label">Builder</span>
5805
5939
  </button>
5806
5940
  )}
5807
5941
  managementSlot={(
@@ -5865,6 +5999,7 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5865
5999
  </span>
5866
6000
  ) : null}
5867
6001
  <button type="button" onClick={addDashboard}><Plus size={15} />New Dashboard</button>
6002
+ <button type="button" onClick={createCodexSite} disabled={saving}><Rocket size={15} />New Codex Site</button>
5868
6003
  <button type="button" onClick={createWorkflow} disabled={saving}><GitBranch size={15} />New Workflow</button>
5869
6004
  <button type="button" onClick={() => importInputRef.current?.click()}><Import size={15} />Import</button>
5870
6005
  </div>}
@@ -5881,6 +6016,8 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5881
6016
  {showActivationPanel ? <WorkspaceActivationPanel
5882
6017
  workspaceConfig={config}
5883
6018
  workspaceSourceRecords={workspaceSourceRecords}
6019
+ metadataGraph={lensMetadataGraph}
6020
+ showLenses={true}
5884
6021
  onStepAction={(step) => {
5885
6022
  if (step?.id === "add-widget") return openAddWidgetBuilder();
5886
6023
  if (step?.id === "create-workflow") {
@@ -5899,13 +6036,14 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5899
6036
  <section className="workspace-table" id="dashboards" aria-label="Builder">
5900
6037
  <div className="workspace-table-heading">
5901
6038
  <strong>Builder</strong>
5902
- <span>{dashboards.length} dashboard{dashboards.length === 1 ? "" : "s"} · {workflows.length} workflow{workflows.length === 1 ? "" : "s"}</span>
6039
+ <span>{dashboards.length} dashboard{dashboards.length === 1 ? "" : "s"} · {sites.length} site{sites.length === 1 ? "" : "s"} · {workflows.length} workflow{workflows.length === 1 ? "" : "s"}</span>
5903
6040
  </div>
5904
6041
  <div className="workspace-builder-filterbar">
5905
6042
  <div className="workspace-builder-filterbar__segments" role="group" aria-label="Builder item type">
5906
6043
  {[
5907
6044
  ["all", "All"],
5908
6045
  ["dashboard", "Dashboards"],
6046
+ ["site", "Sites"],
5909
6047
  ["workflow", "Workflows"]
5910
6048
  ].map(([type, label]) => (
5911
6049
  <button
@@ -5999,6 +6137,53 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5999
6137
  </span>
6000
6138
  )}
6001
6139
  </span>
6140
+ </div> : item.type === "site" ? <div className="workspace-table-row" key={item.id}>
6141
+ <span className="workspace-dashboard-title">
6142
+ {item.site.url ? (
6143
+ <a href={item.site.url} target="_blank" rel="noreferrer">{item.title}</a>
6144
+ ) : (
6145
+ <button
6146
+ className="active"
6147
+ onClick={() => manageSite(item.site)}
6148
+ type="button"
6149
+ >{item.title}</button>
6150
+ )}
6151
+ </span>
6152
+ <span>{item.itemKind}</span>
6153
+ <span>{item.updatedAt}</span>
6154
+ <span>
6155
+ <select
6156
+ aria-label={`Status for ${item.title}`}
6157
+ value={item.status || "draft"}
6158
+ disabled
6159
+ >
6160
+ <option value="draft">draft</option>
6161
+ <option value="review">review</option>
6162
+ <option value="live">live</option>
6163
+ <option value="paused">paused</option>
6164
+ </select>
6165
+ </span>
6166
+ <span className="workspace-dashboard-actions">
6167
+ <button
6168
+ type="button"
6169
+ className="workspace-row-action-trigger"
6170
+ aria-label={`Actions for ${item.title}`}
6171
+ onClick={(event) => openBuilderActionMenu(item, event)}
6172
+ >
6173
+ <MoreVertical size={16} aria-hidden="true" />
6174
+ </button>
6175
+ {builderActionMenuId === item.id && (
6176
+ <span className="workspace-row-action-menu" style={builderActionMenuPlacement || undefined}>
6177
+ {item.site.url ? (
6178
+ <a href={item.site.url} target="_blank" rel="noreferrer" onClick={closeBuilderActionMenu}>Open URL</a>
6179
+ ) : (
6180
+ <button type="button" disabled>Open URL</button>
6181
+ )}
6182
+ <button type="button" onClick={() => { closeBuilderActionMenu(); manageSite(item.site); }}>Manage</button>
6183
+ <button type="button" onClick={() => { closeBuilderActionMenu(); window.open("/settings/apps", "_self"); }}>Apps</button>
6184
+ </span>
6185
+ )}
6186
+ </span>
6002
6187
  </div> : <div className="workspace-table-row" key={item.id}>
6003
6188
  <span className="workspace-dashboard-title">
6004
6189
  {editingWorkflowId === item.workflow.id ? <span className="workspace-dashboard-title-editor">
@@ -0,0 +1,76 @@
1
+ import { Suspense } from "react";
2
+ import Link from "next/link";
3
+ import { readAdapterConfig } from "@/lib/adapters/env";
4
+ import { describePersistenceMode, readWorkspaceConfig, readWorkspaceSourceRecords } from "@/lib/workspace-config";
5
+ import { deriveWorkspaceActivationState } from "@/lib/workspace-activation";
6
+ import { WorkspaceRail } from "../workspace-rail.jsx";
7
+ import { WorkspaceLensPanel } from "../components/WorkspaceLensPanel.jsx";
8
+
9
+ /**
10
+ * /workspace-lens — the dedicated Workspace Lens surface.
11
+ *
12
+ * Server-rendered and force-dynamic so it always reflects the LIVE workspace
13
+ * artifact (a live operating surface must not be statically baked). Reads the
14
+ * same governed helpers the home page uses, assembles a safe runtime
15
+ * descriptor (no secrets), and gates behind activation completeness:
16
+ * onboarding first, operating surface second.
17
+ */
18
+ export const dynamic = "force-dynamic";
19
+
20
+ async function WorkspaceLens() {
21
+ const adapter = readAdapterConfig();
22
+ const persistence = describePersistenceMode();
23
+ const workspaceConfig = await readWorkspaceConfig();
24
+ let workspaceSourceRecords = {};
25
+ try {
26
+ workspaceSourceRecords = (await readWorkspaceSourceRecords()) || {};
27
+ } catch {
28
+ workspaceSourceRecords = {};
29
+ }
30
+
31
+ const metadataGraph = {
32
+ runtime: {
33
+ persistenceMode: persistence?.mode || "",
34
+ persistenceAdapter: persistence?.mode === "database" ? (adapter?.dataAdapter || null) : null,
35
+ allowFsWrite: persistence?.mode === "filesystem" && persistence?.canSave === true,
36
+ nangoConfigured: Boolean(adapter?.nango?.hasSecretKey),
37
+ deploy: { target: adapter?.deployTarget || "" },
38
+ },
39
+ };
40
+
41
+ const activationComplete = deriveWorkspaceActivationState({ workspaceConfig, workspaceSourceRecords }).complete;
42
+
43
+ return (
44
+ <main className="workspace-builder workspace-lens-page">
45
+ <WorkspaceRail workspaceConfig={workspaceConfig || {}} />
46
+ <section className="workspace-surface workspace-lens-surface">
47
+ <div className="workspace-lens-shell">
48
+ {activationComplete ? (
49
+ <WorkspaceLensPanel
50
+ workspaceConfig={workspaceConfig}
51
+ workspaceSourceRecords={workspaceSourceRecords}
52
+ metadataGraph={metadataGraph}
53
+ />
54
+ ) : (
55
+ <div className="workspace-lens-locked">
56
+ <h1 className="workspace-lens-title">Workspace Lens is locked</h1>
57
+ <p className="workspace-lens-subtitle">
58
+ Finish workspace setup to unlock the live operating surface — state, blocked conditions,
59
+ next actions, and agent-assignable work.
60
+ </p>
61
+ <Link href="/" className="workspace-lens-next-link">Finish setup in the Builder</Link>
62
+ </div>
63
+ )}
64
+ </div>
65
+ </section>
66
+ </main>
67
+ );
68
+ }
69
+
70
+ export default function WorkspaceLensPage() {
71
+ return (
72
+ <Suspense fallback={null}>
73
+ <WorkspaceLens />
74
+ </Suspense>
75
+ );
76
+ }