@growthub/cli 0.14.6 → 0.14.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 (18) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/SKILL.md +21 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +1 -1
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +5 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensPanel.jsx +5 -4
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +58 -4
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +31 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/StatusPill.jsx +22 -6
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ToggleField.jsx +5 -4
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/WorkspaceDataModelCanvas.jsx +457 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +188 -2
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +67 -3
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-map/page.jsx +14 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +48 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-node-status.js +55 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-impact.js +198 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +1 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/skills/governed-workspace-mutation/SKILL.md +11 -0
  18. package/package.json +1 -1
@@ -8977,10 +8977,14 @@ body.workspace-rail-collapsed .workspace-builder.workspace-lens-page,
8977
8977
  .workspace-lens-surface { width: 100%; padding: 0; background: #f7f7f8; overflow-y: auto; overflow-x: hidden; }
8978
8978
  .workspace-lens-shell { width: 100%; max-width: none; margin: 0; padding: 24px 28px 32px; }
8979
8979
  .workspace-lens-locked { border: 1px solid #e5e7eb; border-radius: 10px; background: #fff; padding: 28px; max-width: 560px; margin: 48px auto; display: flex; flex-direction: column; gap: 8px; }
8980
- .workspace-lens-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 16px; }
8980
+ .workspace-lens-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
8981
+ .workspace-lens-head > div { min-width: 0; }
8981
8982
  .workspace-lens-title { font-size: 18px; font-weight: 600; color: #111827; margin: 0; }
8982
8983
  .workspace-lens-subtitle { font-size: 13px; color: #6b7280; margin: 4px 0 0; }
8983
- .workspace-lens-score { font-size: 12px; color: #4b5563; font-variant-numeric: tabular-nums; margin: 2px 0 0; }
8984
+ .workspace-lens-map-link { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 34px; border: 1px solid #d8dee8; border-radius: 8px; background: #ffffff; color: #111827; padding: 0 12px; text-decoration: none; font-size: 13px; font-weight: 600; box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05); }
8985
+ .workspace-lens-map-link:hover { background: #f9fafb; border-color: #cfd7e3; color: #111827; }
8986
+ .workspace-lens-map-link:focus-visible { outline: 2px solid #bfdbfe; outline-offset: 2px; }
8987
+ .workspace-lens-map-link svg { color: #4b5563; flex-shrink: 0; }
8984
8988
  .workspace-lens-controls.workspace-builder-filterbar { margin: 0 0 14px; border: 1px solid #e8edf3; border-radius: 8px; background: #fff; }
8985
8989
  .workspace-lens-filters.workspace-builder-filterbar__segments { max-width: 100%; overflow-x: auto; }
8986
8990
  .workspace-lens-filter { white-space: nowrap; }
@@ -9034,12 +9038,17 @@ body.workspace-rail-collapsed .workspace-builder.workspace-lens-page,
9034
9038
  .workspace-lens-action-menu button:hover,
9035
9039
  .workspace-lens-action-menu a:hover { background: #f3f4f6; }
9036
9040
  @media (max-width: 1100px) {
9041
+ .workspace-lens-head { align-items: flex-start; }
9037
9042
  .workspace-lens-control-grid { grid-template-columns: 1fr; }
9038
9043
  .workspace-lens-branch-row { grid-template-columns: 1fr; align-items: flex-start; padding: 10px 12px; gap: 8px; }
9039
9044
  .workspace-lens-branch-actions { justify-content: flex-start; flex-wrap: wrap; }
9040
9045
  .workspace-lens-branch-summary,
9041
9046
  .workspace-lens-branch-next { margin-left: 22px; }
9042
9047
  }
9048
+ @media (max-width: 640px) {
9049
+ .workspace-lens-head { flex-direction: column; }
9050
+ .workspace-lens-map-link { align-self: stretch; }
9051
+ }
9043
9052
  .workspace-lens-stream { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
9044
9053
  .workspace-lens-card { border: 1px solid #e8edf3; border-radius: 10px; background: #fff; padding: 12px 14px; }
9045
9054
  .workspace-lens-card.is-ready { opacity: 0.7; }
@@ -9261,3 +9270,180 @@ body.workspace-rail-collapsed .workspace-builder.workspace-lens-page,
9261
9270
  .dm-swarm-expand-head { justify-content: flex-start; gap: 8px; }
9262
9271
  .dm-swarm-expand-body { flex: 1; min-height: 0; overflow-y: auto; padding: 12px 14px; }
9263
9272
  .dm-swarm-expand-body .dm-helper-toolcall-json { max-height: none; }
9273
+
9274
+ /* ════════════════════════════════════════════════════════════════════════
9275
+ SaaS facelift layer — Attio-inspired polish (v0.14.8)
9276
+ Additive only. Refines existing dm-* light-theme primitives via the
9277
+ cascade (equal specificity, later wins) and introduces the shared
9278
+ vocabulary consumed by the table, drawer, Workspace Map, and run-status
9279
+ work. No new theme, no token renames — existing classes keep working.
9280
+ ──────────────────────────────────────────────────────────────────────── */
9281
+ :root {
9282
+ /* Calm, unified surface system for the light data-model surfaces. */
9283
+ --dm-line: #e7eaee; /* default hairline border */
9284
+ --dm-line-soft: #f0f2f5; /* inner cell / row separators */
9285
+ --dm-surface: #ffffff;
9286
+ --dm-surface-soft: #fafbfc; /* headers, toolbars, footers */
9287
+ --dm-hover: #f6f8fa; /* calm row/control hover */
9288
+ --dm-ink: #1f2733; /* primary text */
9289
+ --dm-muted: #6b7686; /* secondary text */
9290
+ --dm-faint: #9aa4b2; /* tertiary / placeholder */
9291
+ --dm-accent: #4f6bed; /* single accent — active states only */
9292
+ --dm-accent-soft: #eef1fe; /* accent background wash */
9293
+ --dm-accent-line: #c9d3fb; /* accent border */
9294
+ --dm-radius: 8px;
9295
+ --dm-radius-sm: 6px;
9296
+ --dm-radius-lg: 12px;
9297
+ --dm-shadow-pop: 0 12px 32px rgba(20, 28, 46, 0.12), 0 2px 6px rgba(20, 28, 46, 0.06);
9298
+ --dm-ok: #16a34a;
9299
+ --dm-ok-soft: #f0fdf4;
9300
+ --dm-ok-line: #bbf7d0;
9301
+ --dm-bad: #dc2626;
9302
+ --dm-bad-soft: #fef2f2;
9303
+ --dm-bad-line: #fecaca;
9304
+ --dm-warn: #d97706;
9305
+ --dm-warn-soft: #fffbeb;
9306
+ --dm-warn-line: #fde68a;
9307
+ --dm-run: #4f6bed;
9308
+ --dm-run-soft: #eef1fe;
9309
+ --dm-run-line: #c9d3fb;
9310
+ }
9311
+
9312
+ /* ── Toolbar + buttons: softer borders, unified radius/hover ───────────── */
9313
+ .dm-db-toolbar { border-bottom-color: var(--dm-line); }
9314
+ .dm-btn-ghost { border-radius: var(--dm-radius-sm); border-color: var(--dm-line); color: var(--dm-muted); }
9315
+ .dm-btn-ghost:hover { background: var(--dm-hover); border-color: #d3d9e0; color: var(--dm-ink); }
9316
+ .dm-btn-ghost.is-active { background: var(--dm-accent-soft); border-color: var(--dm-accent-line); color: var(--dm-accent); }
9317
+ .dm-btn-outline { border-radius: var(--dm-radius-sm); border-color: var(--dm-line); }
9318
+ .dm-btn-primary-sm { border-radius: var(--dm-radius-sm); }
9319
+
9320
+ /* ── Compact search field for the table toolbar (P1) ──────────────────── */
9321
+ .dm-toolbar-search { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 9px; border: 1px solid var(--dm-line); border-radius: var(--dm-radius-sm); background: var(--dm-surface); color: var(--dm-faint); transition: border-color .12s, box-shadow .12s; }
9322
+ .dm-toolbar-search:focus-within { border-color: var(--dm-accent-line); box-shadow: 0 0 0 3px var(--dm-accent-soft); }
9323
+ .dm-toolbar-search input { border: 0; outline: 0; background: transparent; font: inherit; font-size: 12px; color: var(--dm-ink); width: 150px; }
9324
+ .dm-toolbar-search input::placeholder { color: var(--dm-faint); }
9325
+ .dm-toolbar-count { display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: 999px; background: var(--dm-surface-soft); border: 1px solid var(--dm-line); color: var(--dm-muted); font-size: 11px; font-weight: 650; font-variant-numeric: tabular-nums; }
9326
+ .dm-toolbar-divider { width: 1px; align-self: stretch; margin: 2px 2px; background: var(--dm-line); }
9327
+
9328
+ /* ── Filter / sort pill chips: calmer, clearer active state ───────────── */
9329
+ .dm-filter-chip { border-radius: var(--dm-radius-sm); border-color: var(--dm-accent-line); background: var(--dm-accent-soft); color: var(--dm-accent); font-weight: 600; transition: background .12s, border-color .12s; }
9330
+ .dm-filter-chip:hover { background: #e4e9fd; }
9331
+ .dm-filter-chip-count { margin-left: 1px; min-width: 16px; height: 16px; padding: 0 4px; display: inline-flex; align-items: center; justify-content: center; border-radius: 999px; background: var(--dm-accent); color: #fff; font-size: 10px; font-weight: 700; }
9332
+ .dm-selection-count { border-color: var(--dm-line); background: var(--dm-surface); color: var(--dm-muted); }
9333
+
9334
+ /* ── Grid: calmer hairlines + hover, refined sticky header ────────────── */
9335
+ .dm-db-grid-wrap { border-color: var(--dm-line); border-radius: var(--dm-radius); }
9336
+ .dm-db-grid th { background: var(--dm-surface-soft); color: var(--dm-muted); border-bottom-color: var(--dm-line); border-right-color: var(--dm-line-soft); letter-spacing: .01em; }
9337
+ .dm-db-grid td { border-bottom-color: var(--dm-line-soft); border-right-color: var(--dm-line-soft); }
9338
+ .dm-db-grid tbody tr:hover td { background: var(--dm-hover); }
9339
+ .dm-db-grid tbody tr.selected td { background: var(--dm-accent-soft); box-shadow: inset 2px 0 0 var(--dm-accent); }
9340
+ .dm-db-field-type { background: var(--dm-surface-soft); color: var(--dm-faint); border-radius: 5px; }
9341
+
9342
+ /* ── Object sidebar: calmer active state (less heavy than solid black) ── */
9343
+ .dm-obj-row { border-radius: var(--dm-radius-sm); }
9344
+ .dm-obj-row:hover { background: var(--dm-hover); }
9345
+ .dm-obj-row.active { background: var(--dm-accent-soft); color: var(--dm-accent); font-weight: 600; }
9346
+ .dm-obj-row.active .dm-obj-icon { color: var(--dm-accent); }
9347
+ .dm-obj-row.active .dm-badge { background: rgba(79,107,237,.12); color: var(--dm-accent); border-color: var(--dm-accent-line); }
9348
+ /* Record count on the live object picker rows (P2). The picker — not the
9349
+ legacy ObjectSidebar — is the rendered object nav. */
9350
+ .dm-picker-meta { margin-left: auto; flex: 0 0 auto; color: var(--dm-faint); font-size: 11px; font-weight: 500; font-variant-numeric: tabular-nums; }
9351
+ .dm-picker-item.active .dm-picker-meta { color: var(--dm-muted); font-weight: 500; }
9352
+
9353
+ /* ── Badges: thinner, calmer ──────────────────────────────────────────── */
9354
+ .dm-badge { border-radius: 5px; border-color: var(--dm-line); }
9355
+
9356
+ /* ════ Live status chips — shared by StatusPill + run-status work (P4) ══ */
9357
+ .dm-status-chip { display: inline-flex; align-items: center; gap: 5px; height: 20px; padding: 0 8px; border: 1px solid var(--dm-line); border-radius: 999px; background: var(--dm-surface-soft); color: var(--dm-muted); font-size: 11px; font-weight: 650; line-height: 1; white-space: nowrap; }
9358
+ .dm-status-chip > .dm-status-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--dm-faint); flex: 0 0 auto; }
9359
+ .dm-status-chip.is-ok { border-color: var(--dm-ok-line); background: var(--dm-ok-soft); color: #166534; }
9360
+ .dm-status-chip.is-ok > .dm-status-dot { background: var(--dm-ok); }
9361
+ .dm-status-chip.is-bad { border-color: var(--dm-bad-line); background: var(--dm-bad-soft); color: #991b1b; }
9362
+ .dm-status-chip.is-bad > .dm-status-dot { background: var(--dm-bad); }
9363
+ .dm-status-chip.is-warn { border-color: var(--dm-warn-line); background: var(--dm-warn-soft); color: #92400e; }
9364
+ .dm-status-chip.is-warn > .dm-status-dot { background: var(--dm-warn); }
9365
+ .dm-status-chip.is-running { border-color: var(--dm-run-line); background: var(--dm-run-soft); color: #3a4fc0; }
9366
+ .dm-status-chip.is-running > .dm-status-dot { background: var(--dm-run); animation: dm-pulse 1.15s ease-in-out infinite; }
9367
+ .dm-status-chip.is-waiting > .dm-status-dot { background: var(--dm-faint); }
9368
+ @keyframes dm-pulse { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: .35; transform: scale(.72); } }
9369
+ @media (prefers-reduced-motion: reduce) { .dm-status-chip.is-running > .dm-status-dot { animation: none; } }
9370
+
9371
+ /* ── Toggle switch — upgrades ToggleField's checkbox into a real switch ─ */
9372
+ .dm-switch-row { display: flex; align-items: flex-start; gap: 10px; cursor: pointer; font-size: 13px; color: var(--dm-ink); }
9373
+ .dm-switch-row.is-disabled { opacity: .55; cursor: not-allowed; }
9374
+ .dm-switch-row > input { position: absolute; opacity: 0; width: 0; height: 0; }
9375
+ .dm-switch-track { position: relative; flex: 0 0 auto; width: 34px; height: 20px; margin-top: 1px; border-radius: 999px; background: #d7dce3; transition: background .14s; }
9376
+ .dm-switch-track::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(20,28,46,.28); transition: transform .14s; }
9377
+ .dm-switch-row > input:checked + .dm-switch-track { background: var(--dm-accent); }
9378
+ .dm-switch-row > input:checked + .dm-switch-track::after { transform: translateX(14px); }
9379
+ .dm-switch-row > input:focus-visible + .dm-switch-track { box-shadow: 0 0 0 3px var(--dm-accent-soft); }
9380
+ .dm-switch-label { display: grid; gap: 2px; }
9381
+ .dm-switch-desc { color: var(--dm-muted); font-size: 12px; }
9382
+
9383
+ /* ════ Workspace Map / Data Model Canvas (P3) ═════════════════════════════
9384
+ Schema node-canvas reusing the dotted-grid + card language. Read-only. */
9385
+ .wm-shell { display: flex; flex-direction: column; min-height: 0; flex: 1; background: var(--dm-surface); }
9386
+ .wm-toolbar { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--dm-line); background: var(--dm-surface); flex-wrap: wrap; }
9387
+ .wm-back-link { width: 34px; height: 34px; display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--dm-line); border-radius: 8px; background: var(--dm-surface); color: var(--dm-muted); text-decoration: none; box-shadow: 0 1px 2px rgba(20,28,46,.04); flex: 0 0 auto; }
9388
+ .wm-back-link:hover { background: var(--dm-hover); color: var(--dm-ink); border-color: #cfd6df; }
9389
+ .wm-back-link:focus-visible { outline: 2px solid #bfdbfe; outline-offset: 2px; }
9390
+ .wm-toolbar h1 { margin: 0; font-size: 15px; font-weight: 650; color: var(--dm-ink); }
9391
+ .wm-toolbar .wm-sub { font-size: 12px; color: var(--dm-faint); }
9392
+ .wm-legend { display: inline-flex; align-items: center; gap: 12px; margin-left: auto; flex-wrap: wrap; }
9393
+ .wm-legend-item { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; color: var(--dm-muted); }
9394
+ .wm-legend-swatch { width: 9px; height: 9px; border-radius: 3px; }
9395
+ .wm-canvas { position: relative; flex: 1; min-height: 460px; overflow: auto; background-color: #fbfcfd; background-image: radial-gradient(circle, #dde2e8 1px, transparent 1px); background-size: 22px 22px; cursor: grab; touch-action: none; }
9396
+ .wm-canvas.is-panning { cursor: grabbing; user-select: none; }
9397
+ .wm-canvas-inner { position: relative; }
9398
+ .wm-canvas-scale { position: relative; transform-origin: 0 0; }
9399
+ .wm-edge { position: absolute; pointer-events: none; }
9400
+ .wm-edge path { stroke: #cbd5e1; stroke-width: 1.25; fill: none; stroke-linecap: round; stroke-linejoin: round; }
9401
+ .wm-edge .wm-arrow-head { fill: #cbd5e1; stroke: none; }
9402
+ .wm-edge path.is-source { stroke: #cbd5e1; }
9403
+ .wm-edge path.is-warn { stroke: var(--dm-warn-line); stroke-dasharray: 4 4; }
9404
+ .wm-node { position: absolute; width: 220px; border: 1px solid var(--dm-line); border-radius: var(--dm-radius-lg); background: var(--dm-surface); box-shadow: 0 1px 2px rgba(20,28,46,.06); cursor: pointer; transition: box-shadow .12s, border-color .12s, transform .12s; text-align: left; padding: 0; font: inherit; }
9405
+ .wm-node:hover { border-color: #cfd6df; box-shadow: var(--dm-shadow-pop); transform: translateY(-1px); }
9406
+ .wm-node.is-selected { border-color: var(--dm-accent); box-shadow: 0 0 0 3px var(--dm-accent-soft); }
9407
+ .wm-node-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--dm-line-soft); }
9408
+ .wm-node-icon { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 7px; background: var(--dm-surface-soft); color: var(--dm-muted); flex: 0 0 auto; }
9409
+ .wm-node-title { flex: 1; min-width: 0; font-size: 13px; font-weight: 650; color: var(--dm-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
9410
+ .wm-node-body { padding: 9px 12px; display: grid; gap: 4px; }
9411
+ .wm-node-stat { font-size: 12px; color: var(--dm-muted); font-variant-numeric: tabular-nums; }
9412
+ .wm-node-fields { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
9413
+ .wm-node-field { font-size: 10.5px; color: var(--dm-faint); background: var(--dm-surface-soft); border: 1px solid var(--dm-line-soft); border-radius: 4px; padding: 1px 5px; }
9414
+ .wm-node-warn { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; color: var(--dm-warn); }
9415
+ .wm-zoom { position: sticky; bottom: 12px; left: 12px; display: inline-flex; gap: 4px; padding: 4px; border: 1px solid var(--dm-line); border-radius: 999px; background: var(--dm-surface); box-shadow: var(--dm-shadow-pop); width: fit-content; margin: 12px; }
9416
+ .wm-zoom button { width: 28px; height: 28px; border: 0; border-radius: 999px; background: transparent; color: var(--dm-muted); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
9417
+ .wm-zoom button:hover { background: var(--dm-hover); color: var(--dm-ink); }
9418
+ .wm-empty { display: grid; place-items: center; gap: 8px; padding: 64px 24px; text-align: center; color: var(--dm-muted); }
9419
+ .wm-empty strong { font-size: 16px; color: var(--dm-ink); }
9420
+ /* Read-only selected-node detail panel — additive, docks top-right of canvas. */
9421
+ .wm-detail { position: sticky; top: 12px; margin: -12px 12px 12px auto; float: right; width: 248px; background: var(--dm-surface); border: 1px solid var(--dm-line); border-radius: var(--dm-radius-lg); box-shadow: var(--dm-shadow-pop); z-index: 5; }
9422
+ .wm-detail-head { display: flex; align-items: center; gap: 8px; padding: 10px 10px 10px 12px; border-bottom: 1px solid var(--dm-line-soft); }
9423
+ .wm-detail-title { flex: 1; min-width: 0; font-size: 13px; font-weight: 650; color: var(--dm-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
9424
+ .wm-detail-close { width: 24px; height: 24px; border: 0; border-radius: 6px; background: transparent; color: var(--dm-faint); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; }
9425
+ .wm-detail-close:hover { background: var(--dm-hover); color: var(--dm-ink); }
9426
+ .wm-detail-meta { margin: 0; padding: 10px 12px; display: grid; gap: 6px; }
9427
+ .wm-detail-row { display: flex; justify-content: space-between; gap: 10px; font-size: 12px; }
9428
+ .wm-detail-row dt { color: var(--dm-faint); }
9429
+ .wm-detail-row dd { margin: 0; color: var(--dm-ink); text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
9430
+ .wm-detail-cta { margin: 0 12px 12px; width: calc(100% - 24px); justify-content: center; }
9431
+
9432
+ /* Per-node run-status pill on the Workflow Canvas — docked outside the node's
9433
+ top-right corner. Driven by the general orchestration stream
9434
+ (orchestration.node.* deltas) + the persisted nodeTrace; clickable to open
9435
+ the Live Runs trace. */
9436
+ .dm-orchestration-node { position: relative; overflow: visible; }
9437
+ .dm-orchestration-canvas__step { overflow: visible; }
9438
+ .dm-orchestration-node__status { position: absolute; top: -18px; right: 14px; z-index: 3; height: 16px; min-height: 16px; padding: 0 6px; gap: 3px; border: 1px solid var(--dm-line); background: var(--dm-surface); box-shadow: 0 1px 3px rgba(20,28,46,.12); cursor: pointer; font-size: 9.5px; font-weight: 700; line-height: 1; transform: translateY(-2px); }
9439
+ .dm-orchestration-node__status > .dm-status-dot { width: 5px; height: 5px; }
9440
+ .dm-orchestration-node__status:hover { box-shadow: 0 2px 6px rgba(20,28,46,.2); }
9441
+ .dm-orchestration-node__status:focus-visible { outline: 2px solid var(--dm-accent); outline-offset: 1px; }
9442
+
9443
+ /* ── Table empty / no-results state (P1) ──────────────────────────────── */
9444
+ .dm-db-empty-row td { padding: 0 !important; background: var(--dm-surface) !important; cursor: default !important; }
9445
+ .dm-db-empty-row:hover td { background: var(--dm-surface) !important; }
9446
+ .dm-db-empty-state { display: grid; justify-items: center; gap: 6px; padding: 48px 24px; text-align: center; }
9447
+ .dm-db-empty-state strong { font-size: 15px; color: var(--dm-ink); font-weight: 650; }
9448
+ .dm-db-empty-state span { font-size: 12.5px; color: var(--dm-muted); max-width: 360px; }
9449
+ .dm-db-empty-actions { display: inline-flex; gap: 8px; margin-top: 8px; }
@@ -44,6 +44,7 @@ import {
44
44
  validateOrchestrationGraph
45
45
  } from "@/lib/orchestration-graph";
46
46
  import { resolveConnectorAction } from "@/lib/orchestration-sidecar-routing";
47
+ import { deriveOrchestrationNodeStatuses } from "@/lib/orchestration-node-status";
47
48
  import {
48
49
  nodeSandboxRecordRef,
49
50
  patchSandboxRowInConfig,
@@ -83,6 +84,45 @@ function withUiCacheFlag(workspaceConfig, flag, value) {
83
84
  return { ...workspaceConfig, dataModel: { ...dm, objects: nextObjects } };
84
85
  }
85
86
 
87
+ // Read the sandbox-run NDJSON delta stream (same shape SwarmRunCockpit
88
+ // consumes): push each growthub-sandbox-run-delta-v1 event to `onEvent` for
89
+ // live canvas hydration, and return the sandbox-run.final payload (the run
90
+ // result). Falls back to plain JSON when the response is not a stream.
91
+ async function readSandboxRunStream(response, onEvent) {
92
+ if (!response?.body || typeof response.body.getReader !== "function") {
93
+ return response.json().catch(() => null);
94
+ }
95
+ const reader = response.body.getReader();
96
+ const decoder = new TextDecoder();
97
+ let buffer = "";
98
+ let finalPayload = null;
99
+ const handle = async (line) => {
100
+ const trimmed = line.trim();
101
+ if (!trimmed) return;
102
+ try {
103
+ const event = JSON.parse(trimmed);
104
+ if (event.kind !== "growthub-sandbox-run-delta-v1") return;
105
+ if (event.type === "sandbox-run.final") finalPayload = event.payload || finalPayload;
106
+ else if (typeof onEvent === "function") {
107
+ onEvent((prev) => [...prev, event].slice(-300));
108
+ await new Promise((resolve) => setTimeout(resolve, 90));
109
+ }
110
+ } catch {
111
+ // Ignore malformed cosmetic chunks; the final payload still arrives.
112
+ }
113
+ };
114
+ while (true) {
115
+ const { value, done } = await reader.read();
116
+ if (done) break;
117
+ buffer += decoder.decode(value, { stream: true });
118
+ const lines = buffer.split("\n");
119
+ buffer = lines.pop() || "";
120
+ for (const line of lines) await handle(line);
121
+ }
122
+ if (buffer.trim()) await handle(buffer);
123
+ return finalPayload;
124
+ }
125
+
86
126
  // Workspace Metadata Graph V1 — read-only dependency metadata for workflow
87
127
  // sidecars. The runtime path (sandbox-run, publish, draft/live) is
88
128
  // unchanged; this only exposes typed dependency descriptors so the sidecar
@@ -281,6 +321,7 @@ export default function WorkflowSurface() {
281
321
  const [publishing, setPublishing] = useState(false);
282
322
  const [saveMessage, setSaveMessage] = useState("");
283
323
  const [running, setRunning] = useState(false);
324
+ const [liveRunEvents, setLiveRunEvents] = useState([]);
284
325
  const [runMessage, setRunMessage] = useState("");
285
326
  const [sidecarMode, setSidecarMode] = useState(runId ? "trace" : "graph");
286
327
 
@@ -327,12 +368,29 @@ export default function WorkflowSurface() {
327
368
 
328
369
  useEffect(() => { load(); }, [load]);
329
370
 
371
+ // Reset live per-node deltas when the active workflow changes, so a prior
372
+ // run's stream never bleeds onto a different workflow's canvas — the new
373
+ // workflow settles from its own persisted nodeTrace until it is run.
374
+ useEffect(() => { setLiveRunEvents([]); }, [objectId, rowId]);
375
+
330
376
  const resolved = useMemo(
331
377
  () => (workspaceConfig ? findSandboxRowByWorkflowRef(workspaceConfig, objectId, rowId) : { object: null, row: null, rowIndex: -1 }),
332
378
  [workspaceConfig, objectId, rowId]
333
379
  );
334
380
 
335
381
  const sandboxRow = resolved.row;
382
+
383
+ // Per-node Workflow Canvas pill status — GENERAL orchestration (not swarm).
384
+ // Live from the streamed orchestration.node.* deltas while a run is in
385
+ // flight; settled from the persisted run record's nodeTrace once complete.
386
+ const runNodeStatuses = useMemo(() => {
387
+ let record = sandboxRow?.lastResponse;
388
+ if (typeof record === "string") {
389
+ try { record = JSON.parse(record); } catch { record = null; }
390
+ }
391
+ const map = deriveOrchestrationNodeStatuses({ events: liveRunEvents, record });
392
+ return Object.keys(map).length ? map : null;
393
+ }, [liveRunEvents, sandboxRow]);
336
394
  const hasGraphValue = (value) => Boolean(parseOrchestrationGraph(value));
337
395
  const effectiveFieldName = hasGraphValue(sandboxRow?.[fieldName])
338
396
  ? fieldName
@@ -564,17 +622,21 @@ export default function WorkflowSurface() {
564
622
  : null;
565
623
  setRunning(true);
566
624
  setRunMessage("");
625
+ setLiveRunEvents([]);
567
626
  try {
568
627
  const draft = await saveDraft({ orchestrationDraftStatus: "testing" });
569
628
  const draftGraph = draft?.serialized || serializeCurrentGraph();
570
- const body = { objectId, name: rowId, useDraft: true, draftGraph };
629
+ const body = { objectId, name: rowId, useDraft: true, draftGraph, stream: true };
571
630
  if (runInputs) body.runInputs = runInputs;
572
631
  const res = await fetch("/api/workspace/sandbox-run", {
573
632
  method: "POST",
574
- headers: { "content-type": "application/json" },
633
+ headers: { "content-type": "application/json", accept: "application/x-ndjson" },
575
634
  body: JSON.stringify(body),
576
635
  });
577
- const payload = await res.json();
636
+ // Consume the NDJSON delta stream: per-node orchestration.node.* events
637
+ // hydrate the canvas pills live; the sandbox-run.final payload is the
638
+ // run result we persist. Falls back to plain JSON if not a stream.
639
+ const payload = (await readSandboxRunStream(res, setLiveRunEvents)) || {};
578
640
  const responseText = redactSecretsFromText(JSON.stringify(payload.response ?? payload, null, 2));
579
641
  const status = payload.ok && String(payload.status || "").toLowerCase() === "connected" ? "connected" : "failed";
580
642
  const pass = isPassingRun(payload);
@@ -1042,6 +1104,8 @@ export default function WorkflowSurface() {
1042
1104
  setConfigTab("node");
1043
1105
  }}
1044
1106
  onConnectorAction={handleConnectorAction}
1107
+ nodeStatuses={runNodeStatuses}
1108
+ onNodeStatusClick={(node) => { setSelectedNodeId(String(node?.id || "")); openTraceMode(); }}
1045
1109
  statusLabel={isDraftMode ? "Draft" : "Live"}
1046
1110
  />
1047
1111
  {nextNodeId && (
@@ -0,0 +1,14 @@
1
+ "use client";
2
+
3
+ import { Suspense } from "react";
4
+ import WorkspaceDataModelCanvas from "../data-model/components/WorkspaceDataModelCanvas.jsx";
5
+
6
+ // Read-only workspace-level schema canvas. No mutation lane, no new runtime —
7
+ // it reads /api/workspace and renders the derived metadata graph.
8
+ export default function WorkspaceMapPage() {
9
+ return (
10
+ <Suspense fallback={null}>
11
+ <WorkspaceDataModelCanvas />
12
+ </Suspense>
13
+ );
14
+ }
@@ -301,11 +301,43 @@ async function runOrchestrationGraphIfPresent({ workspaceConfig, row, timeoutMs,
301
301
  const inputPayload = { ...baseInputPayload, ...manualPayload };
302
302
  const consumedInputKeys = Object.keys(manualPayload);
303
303
  const transformConfig = extractTransformConfig(graph);
304
+ const transformNode = (Array.isArray(graph.nodes) ? graph.nodes : [])
305
+ .find((n) => n?.type === "transform-filter" || n?.type === "normalize-output");
304
306
  const resultNode = graph.nodes?.find((n) => n?.type === "tool-result");
305
307
  const successCodes = Array.isArray(resultNode?.config?.successStatusCodes)
306
308
  ? resultNode.config.successStatusCodes.map(Number).filter(Number.isFinite)
307
309
  : [200];
308
310
 
311
+ // Per-node execution deltas for the GENERAL orchestration pipeline (not
312
+ // swarm-specific): stream node lifecycle through the same onEvent NDJSON hook
313
+ // the route already wires, and record a terminal nodeTrace persisted on the
314
+ // run record. Every event corresponds to a real pipeline stage executing.
315
+ const onEvent = typeof executionContext?.onEvent === "function" ? executionContext.onEvent : null;
316
+ const runId = String(executionContext?.runId || "").trim();
317
+ const nodeTrace = [];
318
+ const emitNode = (nodeId, status, error) => {
319
+ const id = String(nodeId || "").trim();
320
+ if (!id) return;
321
+ if (status === "completed" || status === "failed" || status === "skipped") {
322
+ nodeTrace.push(error ? { id, status, error: String(error) } : { id, status });
323
+ }
324
+ if (onEvent) {
325
+ onEvent({
326
+ kind: "growthub-sandbox-run-delta-v1",
327
+ type: `orchestration.node.${status}`,
328
+ nodeId: id,
329
+ runId,
330
+ emittedAt: new Date().toISOString(),
331
+ ...(error ? { error: String(error) } : {})
332
+ });
333
+ }
334
+ };
335
+
336
+ // Input stage — payload is assembled above; it ran.
337
+ if (inputNode?.id) { emitNode(inputNode.id, "started"); emitNode(inputNode.id, "completed"); }
338
+
339
+ // API Registry call stage.
340
+ if (apiNode?.id) emitNode(apiNode.id, "started");
309
341
  const raw = await executeApiRegistryCall(
310
342
  workspaceConfig,
311
343
  apiNode.config,
@@ -320,14 +352,29 @@ async function runOrchestrationGraphIfPresent({ workspaceConfig, row, timeoutMs,
320
352
  raw.exitCode = 1;
321
353
  raw.error = `HTTP ${httpStatus} is not in successStatusCodes`;
322
354
  }
355
+ }
356
+
357
+ if (apiNode?.id) {
358
+ if (raw.ok) emitNode(apiNode.id, "completed");
359
+ else emitNode(apiNode.id, "failed", raw.error);
360
+ }
361
+
362
+ if (raw.ok && raw.rawPayload !== undefined) {
363
+ if (transformNode?.id) emitNode(transformNode.id, "started");
323
364
  const transformed = transformProviderPayload(raw.rawPayload, transformConfig);
324
365
  raw.stdout = typeof transformed === "string"
325
366
  ? transformed
326
367
  : normalizeJsonAtPath(transformed, "");
327
368
  delete raw.rawPayload;
328
369
  delete raw.httpStatus;
370
+ if (transformNode?.id) emitNode(transformNode.id, "completed");
371
+ if (resultNode?.id) { emitNode(resultNode.id, "started"); emitNode(resultNode.id, "completed"); }
329
372
  } else if (raw.error) {
330
373
  raw.error = redactSecretsFromText(raw.error);
374
+ // Downstream stages never executed → record them as skipped (not-run), not
375
+ // failed: only the api stage actually failed.
376
+ if (transformNode?.id) emitNode(transformNode.id, "skipped");
377
+ if (resultNode?.id) emitNode(resultNode.id, "skipped");
331
378
  }
332
379
 
333
380
  if (raw.stdout) {
@@ -336,6 +383,7 @@ async function runOrchestrationGraphIfPresent({ workspaceConfig, row, timeoutMs,
336
383
 
337
384
  return {
338
385
  ...raw,
386
+ nodeTrace,
339
387
  adapterMeta: {
340
388
  ...(raw.adapterMeta || {}),
341
389
  orchestrationProvider: graph.provider,
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Orchestration per-node run status — GENERAL (not swarm).
3
+ *
4
+ * Maps the real per-node execution signal of a sandbox/orchestration run onto
5
+ * a plain { nodeId: status } map the Workflow Canvas pills consume:
6
+ *
7
+ * - live: growthub-sandbox-run-delta-v1 events of type
8
+ * `orchestration.node.{started|completed|failed|skipped}` streamed
9
+ * from POST /api/workspace/sandbox-run while the run is in flight.
10
+ * - settled: the persisted run record's `nodeTrace` (written by the
11
+ * orchestration runner) once the run completes.
12
+ *
13
+ * This is the general orchestration pipeline signal (input → api-registry-call
14
+ * → transform → tool-result, human-input, etc.) — distinct from the swarm
15
+ * cockpit projection. Each entry corresponds to a real pipeline stage that
16
+ * executed; nothing is fabricated. Pure, never throws.
17
+ *
18
+ * Status vocabulary returned: "running" | "completed" | "failed" | "skipped".
19
+ */
20
+
21
+ const NODE_EVENT_PREFIX = "orchestration.node.";
22
+
23
+ function deriveOrchestrationNodeStatuses({ events, record } = {}) {
24
+ const out = {};
25
+ try {
26
+ // Live events win while a run streams — later events overwrite earlier.
27
+ const list = Array.isArray(events) ? events : [];
28
+ for (const event of list) {
29
+ if (!event || typeof event !== "object") continue;
30
+ if (event.kind && event.kind !== "growthub-sandbox-run-delta-v1") continue;
31
+ const type = String(event.type || "");
32
+ if (!type.startsWith(NODE_EVENT_PREFIX)) continue;
33
+ const id = String(event.nodeId || "").trim();
34
+ if (!id) continue;
35
+ const phase = type.slice(NODE_EVENT_PREFIX.length);
36
+ out[id] = phase === "started" ? "running" : phase;
37
+ }
38
+ if (Object.keys(out).length) return out;
39
+
40
+ // Settled from the persisted per-node trace.
41
+ const trace = record && Array.isArray(record.nodeTrace) ? record.nodeTrace : [];
42
+ for (const entry of trace) {
43
+ if (!entry || typeof entry !== "object") continue;
44
+ const id = String(entry.id || "").trim();
45
+ if (!id) continue;
46
+ const status = String(entry.status || "").trim();
47
+ if (status) out[id] = status;
48
+ }
49
+ return out;
50
+ } catch {
51
+ return {};
52
+ }
53
+ }
54
+
55
+ export { deriveOrchestrationNodeStatuses };