improved a lot of things

This commit is contained in:
SinachPat
2026-04-26 12:31:31 +01:00
parent f9528702c8
commit 7a4c1d3147
30 changed files with 1316 additions and 59 deletions
+61 -1
View File
@@ -57,7 +57,67 @@
"Bash(echo \"BUILD_EXIT:$?\")",
"Bash(node /Users/USER/Desktop/originmain/node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/bin/next build)",
"Bash(pkill -f \"next build\")",
"Bash(pnpm --filter @originmain/app build)"
"Bash(pnpm --filter @originmain/app build)",
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/linear.svg\")",
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/slack.svg\")",
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/cursor.svg\")",
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/visualstudiocode.svg\")",
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/anthropic.svg\")",
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/typescript.svg\")",
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/nextdotjs.svg\")",
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/react.svg\")",
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/slack.svg\" -o /dev/null -w \"%{http_code}\")",
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/visualstudiocode.svg\" -o /dev/null -w \"%{http_code}\")",
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/_data/simple-icons.json\")",
"Bash(python3 -c ' *)",
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/simple-icons.json\")",
"Bash(curl -s \"https://api.github.com/repos/simple-icons/simple-icons/contents/icons\")",
"Bash(curl -sI \"https://cdn.simpleicons.org/slack\")",
"Bash(curl -sI \"https://cdn.simpleicons.org/visualstudiocode\")",
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/master/icons/slack.svg\")",
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/master/icons/visualstudiocode.svg\")",
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(d.get\\('tag_name',''\\)\\)\")",
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/16.18.0/icons/slack.svg\")",
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/16.18.0/icons/visualstudiocode.svg\")",
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/16.18.0/_data/simple-icons.json\")",
"Bash(curl -s \"https://registry.npmjs.org/simple-icons/latest\")",
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(d.get\\('version',''\\)\\)\")",
"Bash(curl -s \"https://unpkg.com/simple-icons/icons/slack.svg\")",
"Bash(curl -s \"https://unpkg.com/simple-icons/icons/visualstudiocode.svg\")",
"Bash(curl -sL \"https://unpkg.com/simple-icons/icons/slack.svg\")",
"Bash(curl -sL \"https://unpkg.com/simple-icons/icons/visualstudiocode.svg\")",
"Bash(curl -sL \"https://unpkg.com/simple-icons/simple-icons.json\")",
"Bash(curl -sL \"https://unpkg.com/simple-icons@16.18.0/\")",
"Bash(curl -sL \"https://unpkg.com/simple-icons@16.18.0/?meta\")",
"Bash(curl -sL \"https://unpkg.com/simple-icons@13.22.0/icons/slack.svg\")",
"Bash(curl -sL \"https://unpkg.com/simple-icons@13.22.0/icons/visualstudiocode.svg\")",
"Bash(curl -sL \"https://unpkg.com/simple-icons@14.0.0/icons/slack.svg\")",
"Bash(curl -sL \"https://unpkg.com/simple-icons@14.0.0/icons/visualstudiocode.svg\")",
"Bash(curl -sL \"https://unpkg.com/simple-icons@14.0.0/?meta\")",
"Bash(curl -sL \"https://unpkg.com/simple-icons@9.0.0/icons/visualstudiocode.svg\")",
"Bash(curl -sL \"https://unpkg.com/simple-icons@9.0.0/simple-icons.json\")",
"Bash(curl -sL \"https://raw.githubusercontent.com/simple-icons/simple-icons/14.0.0/_data/simple-icons.json\" -o /tmp/si14.json)",
"Read(//tmp/**)",
"Bash(curl -sL \"https://raw.githubusercontent.com/simple-icons/simple-icons/9.0.0/_data/simple-icons.json\" -o /tmp/si9.json)",
"Bash(curl -sL \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/_data/simple-icons.json\" -o /tmp/si_dev.json)",
"Bash(curl -sL \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/cursor.svg\")",
"Bash(curl -s \"https://api.github.com/repos/simple-icons/simple-icons/git/trees/develop?recursive=0\")",
"Bash(curl -sL \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/data/simple-icons.json\" -o /tmp/si_develop.json)",
"Bash(pnpm --filter @originmain/agent-bridge exec tsc --noEmit)",
"Bash(pnpm --filter @originmain/integrations exec tsc --noEmit)",
"Bash(pnpm --filter @originmain/multiplayer exec tsc --noEmit)",
"Bash(pnpm --filter @originmain/platform exec tsc --noEmit)",
"Bash(pnpm --filter @originmain/ai-layer exec tsc --noEmit)",
"Bash(pnpm --filter @originmain/design-language exec tsc --noEmit)",
"Bash(pnpm --filter @originmain/origin-graph exec tsc --noEmit)",
"Bash(pnpm --filter originmain-app exec tsc --noEmit)",
"WebFetch(domain:raw.githubusercontent.com)",
"WebFetch(domain:cdn.simpleicons.org)",
"WebFetch(domain:brandfetch.com)",
"WebFetch(domain:simpleicons.org)",
"WebFetch(domain:www.svgrepo.com)",
"WebFetch(domain:worldvectorlogo.com)",
"Bash(pnpm --filter @originmain/app exec tsc --noEmit)"
]
}
}
+2 -3
View File
@@ -2135,9 +2135,8 @@ footer {
<div class="int-card reveal"
style="--glow:#ffffff;--glow-dim:rgba(255,255,255,.07);--glow-soft:rgba(255,255,255,.03);--glow-ambient:rgba(255,255,255,.02);--ico-bg:rgba(255,255,255,.06);--ico-bd:rgba(255,255,255,.1)">
<div class="int-ico">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none">
<circle cx="12" cy="12" r="10" stroke="rgba(255,255,255,.5)" stroke-width="1.2"/>
<path d="M8 8v8l8-8v8" stroke="rgba(255,255,255,.85)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg viewBox="0 0 24 24" width="22" height="22" fill="none">
<path d="M5 4v16l14-16v16" stroke="rgba(255,255,255,.88)" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div>
+33
View File
@@ -0,0 +1,33 @@
# ── Clerk (authentication) ────────────────────────────────────────────────────
# https://dashboard.clerk.com → API Keys
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/app
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/app
# ── Supabase ──────────────────────────────────────────────────────────────────
# https://supabase.com/dashboard → Project Settings → API
NEXT_PUBLIC_SUPABASE_URL=https://<project-ref>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
# ── Anthropic ─────────────────────────────────────────────────────────────────
# https://console.anthropic.com/keys
ANTHROPIC_API_KEY=sk-ant-...
# ── Agent Bridge ─────────────────────────────────────────────────────────────
# Random secret used to sign workspace tokens issued to Cursor / Claude Code.
# Generate with: openssl rand -hex 32
AGENT_BRIDGE_SECRET=
# ── Webhook signing secrets (optional — skip verification in dev if absent) ──
# Linear: Settings → API → Webhooks → signing secret
LINEAR_WEBHOOK_SECRET=
# Slack: App → Basic Information → Signing Secret
SLACK_SIGNING_SECRET=
# GitHub: Repository → Settings → Webhooks → secret
GITHUB_WEBHOOK_SECRET=
# Intercom: Developer Hub → Webhooks → client secret
INTERCOM_WEBHOOK_SECRET=
+13 -1
View File
@@ -1,7 +1,19 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
transpilePackages: ['@originmain/ui', '@originmain/diff-engine', '@fluentui/react-components'],
transpilePackages: [
'@originmain/agent-bridge',
'@originmain/ai-layer',
'@originmain/design-language',
'@originmain/diff-engine',
'@originmain/integrations',
'@originmain/multiplayer',
'@originmain/origin-graph',
'@originmain/platform',
'@originmain/renderer',
'@originmain/ui',
'@fluentui/react-components',
],
experimental: {
optimizePackageImports: ['@fluentui/react-components'],
},
+8
View File
@@ -9,15 +9,23 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.56.0",
"@clerk/nextjs": "^7.2.7",
"@fluentui/react-components": "^9.54.0",
"@fluentui/react-icons": "^2.0.0",
"@originmain/agent-bridge": "workspace:*",
"@originmain/ai-layer": "workspace:*",
"@originmain/design-language": "workspace:*",
"@originmain/diff-engine": "workspace:*",
"@originmain/integrations": "workspace:*",
"@originmain/multiplayer": "workspace:*",
"@originmain/origin-graph": "workspace:*",
"@originmain/platform": "workspace:*",
"@originmain/renderer": "workspace:*",
"@originmain/ui": "workspace:*",
"@pierre/diffs": "^1.1.19",
"@pierre/trees": "1.0.0-beta.3",
"@supabase/supabase-js": "^2.0.0",
"@tanstack/react-query": "^5.62.0",
"next": "^15.1.0",
"react": "^19.0.0",
+25 -18
View File
@@ -2022,9 +2022,9 @@ footer {
<div class="int-card reveal"
style="--glow:#5e6ad2;--glow-dim:rgba(94,106,210,.14);--glow-soft:rgba(94,106,210,.06);--glow-ambient:rgba(94,106,210,.04);--ico-bg:rgba(94,106,210,.12);--ico-bd:rgba(94,106,210,.22)">
<div class="int-ico">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none">
<path d="M4 20L20 4" stroke="#5e6ad2" stroke-width="2.5" stroke-linecap="round"/>
<path d="M20 4v10M20 4H10" stroke="#5e6ad2" stroke-width="2.5" stroke-linecap="round"/>
<!-- Linear official mark — fetched from simple-icons/simple-icons/icons/linear.svg -->
<svg viewBox="0 0 24 24" width="20" height="20" fill="#5e6ad2">
<path d="M2.886 4.18A11.982 11.982 0 0 1 11.99 0C18.624 0 24 5.376 24 12.009c0 3.64-1.62 6.903-4.18 9.105L2.887 4.18ZM1.817 5.626l16.556 16.556c-.524.33-1.075.62-1.65.866L.951 7.277c.247-.575.537-1.126.866-1.65ZM.322 9.163l14.515 14.515c-.71.172-1.443.282-2.195.322L0 11.358a12 12 0 0 1 .322-2.195Zm-.17 4.862 9.823 9.824a12.02 12.02 0 0 1-9.824-9.824Z"/>
</svg>
</div>
<div>
@@ -2041,11 +2041,12 @@ footer {
<div class="int-card reveal"
style="--glow:#2eb67d;--glow-dim:rgba(46,182,125,.12);--glow-soft:rgba(46,182,125,.05);--glow-ambient:rgba(46,182,125,.04);--ico-bg:rgba(46,182,125,.1);--ico-bd:rgba(46,182,125,.18)">
<div class="int-ico">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none">
<path d="M9.07 3a2.07 2.07 0 00-2.07 2.07v5.79M7 10.86A2.07 2.07 0 113 10.86M16.93 3a2.07 2.07 0 012.07 2.07v5.79" stroke="#2eb67d" stroke-width="1.8" stroke-linecap="round"/>
<path d="M19 10.86a2.07 2.07 0 110 4.14M9.07 21a2.07 2.07 0 002.07-2.07v-5.79M11.14 13.14a2.07 2.07 0 110-4.14" stroke="#e01d5a" stroke-width="1.8" stroke-linecap="round"/>
<path d="M7 13.14h5.79M5 15.21a2.07 2.07 0 104.14 0" stroke="#ecb22e" stroke-width="1.8" stroke-linecap="round"/>
<path d="M13.14 15a2.07 2.07 0 104.14 0v-2.07M13.14 17.07a2.07 2.07 0 100 4.14" stroke="#36c5f0" stroke-width="1.8" stroke-linecap="round"/>
<!-- Slack official mark — paths from gilbarbara/logos slack-icon.svg -->
<svg viewBox="0 0 256 256" width="20" height="20" fill="none">
<path d="M53.841 161.321C53.841 176.152 41.854 188.14 27.022 188.14 12.19 188.14.203 176.152.203 161.321c0-14.832 11.987-26.82 26.819-26.82h26.82v26.82zm13.41 0c0-14.832 11.987-26.82 26.819-26.82 14.832 0 26.819 11.988 26.819 26.82v67.047c0 14.832-11.987 26.82-26.819 26.82-14.832 0-26.819-11.988-26.819-26.82V161.32z" fill="#E01E5A"/>
<path d="M94.07 53.638C79.238 53.638 67.251 41.651 67.251 26.819 67.251 11.987 79.238 0 94.07 0c14.832 0 26.819 11.987 26.819 26.819v26.82H94.07zm0 13.613c14.832 0 26.819 11.987 26.819 26.819 0 14.832-11.987 26.819-26.819 26.819H26.819C11.987 120.889 0 108.902 0 94.07c0-14.832 11.987-26.819 26.819-26.819H94.07z" fill="#36C5F0"/>
<path d="M201.549 94.07c0-14.832 11.987-26.819 26.819-26.819C243.2 67.251 255.187 79.238 255.187 94.07c0 14.832-11.987 26.819-26.819 26.819h-26.819V94.07zm-13.41 0c0 14.832-11.987 26.819-26.819 26.819-14.832 0-26.819-11.987-26.819-26.819V26.819C134.501 11.987 146.488 0 161.32 0c14.832 0 26.819 11.987 26.819 26.819V94.07z" fill="#2EB67D"/>
<path d="M161.321 201.549c14.832 0 26.819 11.987 26.819 26.819 0 14.832-11.987 26.819-26.819 26.819-14.832 0-26.819-11.987-26.819-26.819v-26.819h26.819zm0-13.41c-14.832 0-26.819-11.987-26.819-26.819 0-14.832 11.987-26.819 26.819-26.819h67.25c14.832 0 26.819 11.987 26.819 26.819 0 14.832-11.987 26.819-26.819 26.819h-67.25z" fill="#ECB22E"/>
</svg>
</div>
<div>
@@ -2062,8 +2063,9 @@ footer {
<div class="int-card w2 reveal"
style="--glow:#d4d4d4;--glow-dim:rgba(212,212,212,.09);--glow-soft:rgba(212,212,212,.04);--glow-ambient:rgba(212,212,212,.02);--ico-bg:rgba(255,255,255,.06);--ico-bd:rgba(255,255,255,.1)">
<div class="int-ico">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none">
<path d="M5 3l14 9.5-8.5 1.5L8 21 5 3z" fill="rgba(255,255,255,.8)" stroke="rgba(255,255,255,.5)" stroke-width="1" stroke-linejoin="round"/>
<!-- Cursor official mark — fetched from simple-icons/simple-icons/icons/cursor.svg -->
<svg viewBox="0 0 24 24" width="20" height="20" fill="rgba(255,255,255,.88)">
<path d="M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23"/>
</svg>
</div>
<div>
@@ -2080,8 +2082,9 @@ footer {
<div class="int-card reveal"
style="--glow:#007acc;--glow-dim:rgba(0,122,204,.14);--glow-soft:rgba(0,122,204,.06);--glow-ambient:rgba(0,122,204,.04);--ico-bg:rgba(0,122,204,.12);--ico-bd:rgba(0,122,204,.22)">
<div class="int-ico">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none">
<path d="M17.5 2L3 9.5l4.5 3.5L17.5 6v12L7.5 12 3 15.5 17.5 22l3-1.5V3.5L17.5 2z" fill="#007acc"/>
<!-- VS Code official mark: folded-ribbon square — simpleicons.org/visualstudiocode -->
<svg viewBox="0 0 24 24" width="20" height="20" fill="#007acc">
<path d="M23.15 2.587 18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"/>
</svg>
</div>
<div>
@@ -2098,8 +2101,9 @@ footer {
<div class="int-card reveal"
style="--glow:#f59e0b;--glow-dim:rgba(245,158,11,.13);--glow-soft:rgba(245,158,11,.05);--glow-ambient:rgba(245,158,11,.04);--ico-bg:rgba(245,158,11,.1);--ico-bd:rgba(245,158,11,.2)">
<div class="int-ico">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none">
<path d="M12 3L5 19h4.5l2.5-5 2.5 5H19L12 3z" fill="#f59e0b"/>
<!-- Claude official mark — fetched from simple-icons/simple-icons/icons/claude.svg -->
<svg viewBox="0 0 24 24" width="20" height="20" fill="#f59e0b">
<path d="m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z"/>
</svg>
</div>
<div>
@@ -2116,9 +2120,13 @@ footer {
<div class="int-card reveal"
style="--glow:#3b82f6;--glow-dim:rgba(59,130,246,.14);--glow-soft:rgba(59,130,246,.06);--glow-ambient:rgba(59,130,246,.04);--ico-bg:#1e3a5f;--ico-bd:rgba(59,130,246,.3)">
<div class="int-ico" style="background:#1d4ed8;border-color:rgba(59,130,246,.4)">
<!-- TypeScript official mark: blue square with correct TS letterforms -->
<svg viewBox="0 0 18 18" width="18" height="18">
<rect width="18" height="18" rx="2" fill="#3178c6"/>
<path d="M3 10.5h4.5M5.25 8v5M10 10.5c.5-1.5 3.5-1.5 3.5.5s-3.5 1-3.5 2.5 3 2 3.5.5" stroke="white" stroke-width="1.4" stroke-linecap="round"/>
<!-- T: crossbar at TOP, stem descends from centre -->
<path d="M2 6.5h6M5 6.5v7" stroke="white" stroke-width="1.5" stroke-linecap="round" fill="none"/>
<!-- S: top curve right, mid narrows, bottom curve left -->
<path d="M10 13.5c.5.8 1.4 1.2 2.6 1.2 1.5 0 2.8-.8 2.8-2.2 0-1.1-.7-1.8-2.1-2.3-1.1-.4-1.8-.8-1.8-1.6 0-.7.6-1.3 1.7-1.3.9 0 1.6.3 2.1 1" stroke="white" stroke-width="1.5" stroke-linecap="round" fill="none"/>
</svg>
</div>
<div>
@@ -2135,9 +2143,8 @@ footer {
<div class="int-card reveal"
style="--glow:#ffffff;--glow-dim:rgba(255,255,255,.07);--glow-soft:rgba(255,255,255,.03);--glow-ambient:rgba(255,255,255,.02);--ico-bg:rgba(255,255,255,.06);--ico-bd:rgba(255,255,255,.1)">
<div class="int-ico">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none">
<circle cx="12" cy="12" r="10" stroke="rgba(255,255,255,.5)" stroke-width="1.2"/>
<path d="M8 8v8l8-8v8" stroke="rgba(255,255,255,.85)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg viewBox="0 0 24 24" width="22" height="22" fill="none">
<path d="M5 4v16l14-16v16" stroke="rgba(255,255,255,.88)" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div>
@@ -0,0 +1,102 @@
// POST /api/agent-bridge
// JSON-RPC 2.0 endpoint consumed by the Cursor / Claude Code MCP adapters.
// Authentication: Bearer <workspace_token> (HMAC-SHA256, issued by issueWorkspaceToken).
import { NextRequest, NextResponse } from 'next/server';
import { verifyWorkspaceToken, TOOL_MAP } from '@originmain/agent-bridge';
import type { JsonRpcRequest, ToolContext } from '@originmain/agent-bridge';
import {
getDiffsByStatus,
getDiff,
getArtboard,
getActiveDesignLanguageFile,
updateDiffStatus,
} from '@originmain/origin-graph';
import { AIGateway, answerAgentQuestion } from '@originmain/ai-layer';
import { serverClient } from '@/lib/supabase';
export async function POST(req: NextRequest) {
// ── Auth ────────────────────────────────────────────────────────────────────
const authHeader = req.headers.get('authorization') ?? '';
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
if (!token) {
return NextResponse.json(
{ jsonrpc: '2.0', id: null, error: { code: -32001, message: 'Missing Bearer token' } },
{ status: 401 },
);
}
const workspaceToken = verifyWorkspaceToken(token);
if (!workspaceToken) {
return NextResponse.json(
{ jsonrpc: '2.0', id: null, error: { code: -32001, message: 'Invalid or expired token' } },
{ status: 401 },
);
}
// ── Parse JSON-RPC body ─────────────────────────────────────────────────────
let body: JsonRpcRequest;
try {
body = (await req.json()) as JsonRpcRequest;
} catch {
return NextResponse.json(
{ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } },
{ status: 400 },
);
}
// tools/list — meta-endpoint; return available tools without executing one.
if (body.method === 'tools/list') {
const tools = [...TOOL_MAP.values()].map(t => ({
name: t.name,
description: t.description,
}));
return NextResponse.json({ jsonrpc: '2.0', id: body.id, result: { tools } });
}
// ── Dispatch ────────────────────────────────────────────────────────────────
const tool = TOOL_MAP.get(body.method);
if (!tool) {
return NextResponse.json({
jsonrpc: '2.0',
id: body.id,
error: { code: -32601, message: `Method not found: ${body.method}` },
});
}
const db = serverClient();
const { workspaceId } = workspaceToken;
const ctx: ToolContext = {
workspaceId,
db: {
getDiffsByStatus: (wsId, status) => getDiffsByStatus(db, wsId, status),
getDiff: (id) => getDiff(db, id),
getArtboard: (id) => getArtboard(db, id),
getDesignLanguageFile: (wsId) => getActiveDesignLanguageFile(db, wsId),
updateDiffStatus: (id, status, notes) =>
updateDiffStatus(db, id, status, notes).then(() => undefined),
},
ai: {
answerAgentQuestion: (diffId: string, question: string, artboardContext: unknown) =>
answerAgentQuestion(new AIGateway(), {
diffId,
question,
artboardContextJson: JSON.stringify(artboardContext),
}).then(r => r.answer),
},
};
try {
const result = await tool.execute(body.params ?? {}, ctx);
return NextResponse.json({ jsonrpc: '2.0', id: body.id, result });
} catch (err) {
const message = err instanceof Error ? err.message : 'Internal error';
return NextResponse.json({
jsonrpc: '2.0',
id: body.id,
error: { code: -32603, message },
});
}
}
@@ -0,0 +1,23 @@
// POST /api/ai/completion-zone
// Fills a design completion zone given a component tree and user intent.
import { auth } from '@clerk/nextjs/server';
import { NextRequest, NextResponse } from 'next/server';
import { AIGateway, fillCompletionZone } from '@originmain/ai-layer';
import type { CompletionZoneInput } from '@originmain/ai-layer';
export async function POST(req: NextRequest) {
const { userId } = await auth();
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const body = (await req.json()) as CompletionZoneInput;
try {
const gateway = new AIGateway();
const result = await fillCompletionZone(gateway, body);
return NextResponse.json({ proposedTree: result.proposedTree, description: result.description });
} catch (err) {
const message = err instanceof Error ? err.message : 'AI error';
return NextResponse.json({ error: message }, { status: 500 });
}
}
@@ -0,0 +1,23 @@
// POST /api/ai/drift-report
// Analyzes a screenshot against the active Design Language File for drift violations.
import { auth } from '@clerk/nextjs/server';
import { NextRequest, NextResponse } from 'next/server';
import { AIGateway, generateDriftReport } from '@originmain/ai-layer';
import type { DriftReportInput } from '@originmain/ai-layer';
export async function POST(req: NextRequest) {
const { userId } = await auth();
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const body = (await req.json()) as DriftReportInput;
try {
const gateway = new AIGateway();
const result = await generateDriftReport(gateway, body);
return NextResponse.json(result);
} catch (err) {
const message = err instanceof Error ? err.message : 'AI error';
return NextResponse.json({ error: message }, { status: 500 });
}
}
@@ -0,0 +1,23 @@
// POST /api/ai/query
// Cross-artboard search: finds artboards matching a natural language query.
import { auth } from '@clerk/nextjs/server';
import { NextRequest, NextResponse } from 'next/server';
import { AIGateway, queryCrossArtboard } from '@originmain/ai-layer';
import type { ArtboardQueryInput } from '@originmain/ai-layer';
export async function POST(req: NextRequest) {
const { userId } = await auth();
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const body = (await req.json()) as ArtboardQueryInput;
try {
const gateway = new AIGateway();
const result = await queryCrossArtboard(gateway, body);
return NextResponse.json(result);
} catch (err) {
const message = err instanceof Error ? err.message : 'AI error';
return NextResponse.json({ error: message }, { status: 500 });
}
}
@@ -0,0 +1,26 @@
// GET /api/artboards/:id → fetch single artboard
import { auth } from '@clerk/nextjs/server';
import { NextRequest, NextResponse } from 'next/server';
import { serverClient } from '@/lib/supabase';
import { getArtboard } from '@originmain/origin-graph';
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { userId } = await auth();
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { id } = await params;
try {
const db = serverClient();
const artboard = await getArtboard(db, id);
return NextResponse.json(artboard);
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
const status = message.includes('not found') || message.includes('0 rows') ? 404 : 500;
return NextResponse.json({ error: message }, { status });
}
}
@@ -0,0 +1,41 @@
// GET /api/artboards?workspaceId=<uuid> → list artboards for workspace
// POST /api/artboards → create artboard
import { auth } from '@clerk/nextjs/server';
import { NextRequest, NextResponse } from 'next/server';
import { serverClient } from '@/lib/supabase';
import { getArtboards, createArtboard } from '@originmain/origin-graph';
import type { InsertArtboard } from '@originmain/origin-graph';
export async function GET(req: NextRequest) {
const { userId } = await auth();
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const workspaceId = req.nextUrl.searchParams.get('workspaceId');
if (!workspaceId) return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 });
try {
const db = serverClient();
const artboards = await getArtboards(db, workspaceId);
return NextResponse.json(artboards);
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
return NextResponse.json({ error: message }, { status: 500 });
}
}
export async function POST(req: NextRequest) {
const { userId } = await auth();
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const body = (await req.json()) as InsertArtboard;
try {
const db = serverClient();
const artboard = await createArtboard(db, body);
return NextResponse.json(artboard, { status: 201 });
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
return NextResponse.json({ error: message }, { status: 500 });
}
}
@@ -0,0 +1,47 @@
// GET /api/design-language?workspaceId=<uuid> → active design language file
// POST /api/design-language → upsert (new version)
import { auth } from '@clerk/nextjs/server';
import { NextRequest, NextResponse } from 'next/server';
import { serverClient } from '@/lib/supabase';
import { getActiveDesignLanguageFile } from '@originmain/origin-graph';
import type { InsertDesignLanguageFile } from '@originmain/origin-graph';
export async function GET(req: NextRequest) {
const { userId } = await auth();
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const workspaceId = req.nextUrl.searchParams.get('workspaceId');
if (!workspaceId) return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 });
try {
const db = serverClient();
const file = await getActiveDesignLanguageFile(db, workspaceId);
if (!file) return NextResponse.json(null, { status: 204 });
return NextResponse.json(file);
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
return NextResponse.json({ error: message }, { status: 500 });
}
}
export async function POST(req: NextRequest) {
const { userId } = await auth();
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const body = (await req.json()) as InsertDesignLanguageFile;
const db = serverClient();
// Compute the next version: max(existing) + 1.
const existing = await getActiveDesignLanguageFile(db, body.workspace_id);
const version = existing ? existing.version + 1 : 1;
const { data, error } = await db
.from('design_language_files')
.insert({ ...body, version })
.select()
.single();
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json(data, { status: 201 });
}
@@ -0,0 +1,48 @@
// GET /api/diffs/:id → fetch single diff
// PATCH /api/diffs/:id → update status + notes
import { auth } from '@clerk/nextjs/server';
import { NextRequest, NextResponse } from 'next/server';
import { serverClient } from '@/lib/supabase';
import { getDiff, updateDiffStatus } from '@originmain/origin-graph';
import type { DiffStatus } from '@originmain/origin-graph';
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { userId } = await auth();
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { id } = await params;
try {
const db = serverClient();
const diff = await getDiff(db, id);
return NextResponse.json(diff);
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
const status = message.includes('0 rows') ? 404 : 500;
return NextResponse.json({ error: message }, { status });
}
}
export async function PATCH(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { userId } = await auth();
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { id } = await params;
const body = (await req.json()) as { status: DiffStatus; notes?: string };
try {
const db = serverClient();
const updated = await updateDiffStatus(db, id, body.status, body.notes);
return NextResponse.json(updated);
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
return NextResponse.json({ error: message }, { status: 500 });
}
}
+42
View File
@@ -0,0 +1,42 @@
// GET /api/diffs?artboardId=<uuid> → list diffs for an artboard
// POST /api/diffs → create draft diff
import { auth } from '@clerk/nextjs/server';
import { NextRequest, NextResponse } from 'next/server';
import { serverClient } from '@/lib/supabase';
import { getDiffs, createDiff } from '@originmain/origin-graph';
import type { InsertIntentDiff } from '@originmain/origin-graph';
export async function GET(req: NextRequest) {
const { userId } = await auth();
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const artboardId = req.nextUrl.searchParams.get('artboardId');
if (!artboardId) return NextResponse.json({ error: 'artboardId is required' }, { status: 400 });
try {
const db = serverClient();
const diffs = await getDiffs(db, artboardId);
return NextResponse.json(diffs);
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
return NextResponse.json({ error: message }, { status: 500 });
}
}
export async function POST(req: NextRequest) {
const { userId } = await auth();
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const body = (await req.json()) as Omit<InsertIntentDiff, 'author_id'>;
const insert: InsertIntentDiff = { ...body, author_id: userId };
try {
const db = serverClient();
const diff = await createDiff(db, insert);
return NextResponse.json(diff, { status: 201 });
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
return NextResponse.json({ error: message }, { status: 500 });
}
}
@@ -0,0 +1,161 @@
// POST /api/webhooks/:provider
// Ingests webhook events from Linear, Slack, GitHub, and Intercom.
// workspaceId must be passed as a query param: ?workspace=<uuid>
// Signature verification is skipped gracefully when the secret env var is absent
// (useful for local development), but enforced in production.
import { createHmac, timingSafeEqual } from 'node:crypto';
import { NextRequest, NextResponse } from 'next/server';
import {
linearIngester,
slackIngester,
githubIngester,
intercomIngester,
} from '@originmain/integrations';
import type { OriginIngester } from '@originmain/integrations';
import { serverClient } from '@/lib/supabase';
import { createOrigin, createArtboard } from '@originmain/origin-graph';
// ── Signature helpers ─────────────────────────────────────────────────────────
function hmac(algo: 'sha256' | 'sha1', secret: string, body: Buffer): string {
return createHmac(algo, secret).update(body).digest('hex');
}
function safeEqual(a: string, b: string): boolean {
try {
const ab = Buffer.from(a.padEnd(b.length, '\0'));
const bb = Buffer.from(b.padEnd(a.length, '\0'));
return timingSafeEqual(ab, bb) && a.length === b.length;
} catch {
return false;
}
}
function verifyLinear(rawBody: Buffer, headers: Headers): boolean {
const secret = process.env.LINEAR_WEBHOOK_SECRET;
if (!secret) return true; // skip verification in dev
const sig = headers.get('x-linear-signature') ?? '';
return safeEqual(hmac('sha256', secret, rawBody), sig);
}
function verifySlack(rawBody: Buffer, headers: Headers): boolean {
const secret = process.env.SLACK_SIGNING_SECRET;
if (!secret) return true;
const ts = headers.get('x-slack-request-timestamp') ?? '';
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const baseString = `v0:${ts}:${rawBody.toString()}`;
const expected = `v0=${hmac('sha256', secret, Buffer.from(baseString))}`;
const provided = headers.get('x-slack-signature') ?? '';
return safeEqual(expected, provided);
}
function verifyGitHub(rawBody: Buffer, headers: Headers): boolean {
const secret = process.env.GITHUB_WEBHOOK_SECRET;
if (!secret) return true;
const expected = `sha256=${hmac('sha256', secret, rawBody)}`;
const provided = headers.get('x-hub-signature-256') ?? '';
return safeEqual(expected, provided);
}
function verifyIntercom(rawBody: Buffer, headers: Headers): boolean {
const secret = process.env.INTERCOM_WEBHOOK_SECRET;
if (!secret) return true;
const expected = `sha1=${hmac('sha1', secret, rawBody)}`;
const provided = headers.get('x-hub-signature') ?? '';
return safeEqual(expected, provided);
}
// ── Route config ──────────────────────────────────────────────────────────────
const INGESTER_MAP: Record<
string,
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ingester: OriginIngester<any>;
verify: (body: Buffer, headers: Headers) => boolean;
}
> = {
linear: { ingester: linearIngester, verify: verifyLinear },
slack: { ingester: slackIngester, verify: verifySlack },
github: { ingester: githubIngester, verify: verifyGitHub },
intercom: { ingester: intercomIngester, verify: verifyIntercom },
};
// ── Handler ───────────────────────────────────────────────────────────────────
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ provider: string }> },
) {
const { provider } = await params;
const entry = INGESTER_MAP[provider];
if (!entry) {
return NextResponse.json({ error: `Unknown provider: ${provider}` }, { status: 404 });
}
const workspaceId = req.nextUrl.searchParams.get('workspace');
if (!workspaceId) {
return NextResponse.json({ error: 'workspace query param is required' }, { status: 400 });
}
const rawBody = Buffer.from(await req.arrayBuffer());
if (!entry.verify(rawBody, req.headers)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
let parsed: unknown;
try {
parsed = JSON.parse(rawBody.toString('utf-8'));
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
}
// Slack sends a challenge request during webhook setup — respond immediately.
if (
provider === 'slack' &&
typeof parsed === 'object' &&
parsed !== null &&
'type' in parsed &&
(parsed as { type: string }).type === 'url_verification'
) {
return NextResponse.json({ challenge: (parsed as { challenge?: string }).challenge ?? '' });
}
let payload: unknown;
try {
payload = entry.ingester.parsePayload(parsed);
} catch (err) {
const message = err instanceof Error ? err.message : 'Payload validation failed';
return NextResponse.json({ error: message }, { status: 422 });
}
const result = entry.ingester.ingest(payload);
const db = serverClient();
try {
const origin = await createOrigin(db, result.origin);
const meta: Record<string, unknown> = {
x: 120,
y: 100,
width: 360,
height: 240,
};
if (result.renderUrl !== undefined) meta['renderUrl'] = result.renderUrl;
const artboard = await createArtboard(db, {
workspace_id: workspaceId,
name: result.artboardTitle,
origin_id: origin.id,
parent_artboard_id: null,
metadata_jsonb: meta,
});
return NextResponse.json({ originId: origin.id, artboardId: artboard.id }, { status: 201 });
} catch (err) {
const message = err instanceof Error ? err.message : 'Database error';
return NextResponse.json({ error: message }, { status: 500 });
}
}
@@ -0,0 +1,66 @@
// GET /api/workspace
// Returns the authenticated user's workspace. Auto-creates one on first visit
// (FREE plan, name derived from Clerk user display name or email).
import { auth, currentUser } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';
import { serverClient } from '@/lib/supabase';
import type { Workspace, InsertWorkspace } from '@originmain/origin-graph';
export async function GET() {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const db = serverClient();
// Look up an existing workspace owned by this Clerk user.
const { data: existing, error: findError } = await db
.from('workspaces')
.select('*')
.eq('owner_id', userId)
.limit(1)
.single();
if (findError && findError.code !== 'PGRST116') {
return NextResponse.json({ error: findError.message }, { status: 500 });
}
if (existing) {
return NextResponse.json(existing as Workspace);
}
// No workspace yet — auto-create one. Fetch the Clerk user for a friendly name.
const user = await currentUser();
const name =
user?.fullName ??
user?.emailAddresses[0]?.emailAddress ??
'My Workspace';
const insert: InsertWorkspace = {
owner_id: userId,
name,
plan: 'FREE',
settings_jsonb: {},
};
const { data: created, error: insertError } = await db
.from('workspaces')
.insert(insert)
.select()
.single();
if (insertError) {
return NextResponse.json({ error: insertError.message }, { status: 500 });
}
// Also add the owner as a team member.
await db.from('team_members').insert({
workspace_id: (created as Workspace).id,
user_id: userId,
role: 'OWNER',
});
return NextResponse.json(created as Workspace, { status: 201 });
}
+24 -4
View File
@@ -1,13 +1,33 @@
'use client';
import type { ReactNode } from 'react';
import { useState, type ReactNode } from 'react';
import { FluentProvider } from '@fluentui/react-components';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { originmainLightTheme } from '@originmain/ui';
export function Providers({ children }: { children: ReactNode }) {
// TanStack Query v5: create QueryClient inside useState so it's stable across
// re-renders and each client-side navigation gets the same instance. Creating
// it outside the component would cause it to be shared across requests on the
// server (SSR memory leak / cross-request data pollution).
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000, // treat data as fresh for 30 s
retry: 1, // one automatic retry on transient errors
refetchOnWindowFocus: false,
},
},
}),
);
return (
<FluentProvider theme={originmainLightTheme} style={{ height: '100%' }}>
{children}
</FluentProvider>
<QueryClientProvider client={queryClient}>
<FluentProvider theme={originmainLightTheme} style={{ height: '100%' }}>
{children}
</FluentProvider>
</QueryClientProvider>
);
}
@@ -1,6 +1,10 @@
'use client';
import { useState, useCallback } from 'react';
import { useCanvas } from '@/store/canvas';
import { LiveArtboard } from './LiveArtboard';
import { SelectionOverlay } from './SelectionOverlay';
import type { FiberNode } from '@originmain/renderer';
interface ArtboardProps {
id: string;
@@ -9,11 +13,19 @@ interface ArtboardProps {
y: number;
width: number;
height: number;
renderUrl?: string;
}
export function Artboard({ id, label, x, y, width, height }: ArtboardProps) {
export function Artboard({ id, label, x, y, width, height, renderUrl }: ArtboardProps) {
const { selectedArtboardId, selectArtboard } = useCanvas();
const selected = selectedArtboardId === id;
const [fiberRoot, setFiberRoot] = useState<FiberNode | undefined>(undefined);
const handleFiberUpdate = useCallback((root: FiberNode) => setFiberRoot(root), []);
const handleComponentSelected = useCallback(
(nodeId: string) => { void nodeId; /* future: highlight in inspector */ },
[]
);
return (
<div
@@ -66,7 +78,26 @@ export function Artboard({ id, label, x, y, width, height }: ArtboardProps) {
)}
{/* Per-artboard content */}
<ArtboardContent id={id} />
{renderUrl ? (
<>
<LiveArtboard
id={id}
src={renderUrl}
width={width}
height={height}
onFiberTreeUpdate={handleFiberUpdate}
onComponentSelected={handleComponentSelected}
/>
<SelectionOverlay
artboardId={id}
{...(fiberRoot !== undefined ? { fiberRoot } : {})}
width={width}
height={height}
/>
</>
) : (
<ArtboardContent id={id} />
)}
</div>
</div>
);
@@ -3,15 +3,10 @@
import { useRef, useEffect, useCallback } from 'react';
import { useViewport } from '@/store/viewport';
import { useCanvas } from '@/store/canvas';
import { useWorkspace } from '@/hooks/useWorkspace';
import { useArtboards } from '@/hooks/useArtboards';
import { Artboard } from './Artboard';
const ARTBOARDS = [
{ id: 'dashboard-card', label: 'DashboardCard', x: 120, y: 100, width: 280, height: 200 },
{ id: 'user-profile', label: 'UserProfile', x: 460, y: 100, width: 200, height: 260 },
{ id: 'nav-sidebar', label: 'NavSidebar', x: 120, y: 360, width: 200, height: 340 },
{ id: 'data-table', label: 'DataTable', x: 380, y: 380, width: 420, height: 280 },
];
export function Canvas() {
const containerRef = useRef<HTMLDivElement>(null);
const panX = useViewport((s) => s.panX);
@@ -19,6 +14,9 @@ export function Canvas() {
const zoom = useViewport((s) => s.zoom);
const { activeTool, selectArtboard } = useCanvas();
const { data: workspace } = useWorkspace();
const { artboards } = useArtboards(workspace?.id);
const isPanning = useRef(false);
const lastPos = useRef({ x: 0, y: 0 });
const spaceDown = useRef(false);
@@ -124,7 +122,7 @@ export function Canvas() {
zIndex: 2,
}}
>
{ARTBOARDS.map((ab) => (
{artboards.map((ab) => (
<Artboard key={ab.id} {...ab} />
))}
</div>
@@ -1,11 +1,33 @@
'use client';
import { useEffect } from 'react';
import { Toolbar } from './Toolbar';
import { ArtboardNavigator } from '../navigator/ArtboardNavigator';
import { Canvas } from '../canvas/Canvas';
import { Inspector } from '../inspector/Inspector';
import { useHistory } from '@/store/history';
import { useCanvas } from '@/store/canvas';
export function AppChrome() {
const selectedArtboardId = useCanvas((s) => s.selectedArtboardId);
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
if (!(e.metaKey || e.ctrlKey)) return;
if (!selectedArtboardId) return;
if (e.key === 'z' && !e.shiftKey) {
e.preventDefault();
useHistory.getState().undo(selectedArtboardId);
} else if ((e.key === 'z' && e.shiftKey) || e.key === 'y') {
e.preventDefault();
useHistory.getState().redo(selectedArtboardId);
}
}
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [selectedArtboardId]);
return (
<div
style={{
@@ -1,17 +1,12 @@
'use client';
import { useState } from 'react';
import { useFileTree, FileTree, useFileTreeSelection } from '@pierre/trees/react';
import { useFileTree, FileTree } from '@pierre/trees/react';
import { themeToTreeStyles } from '@pierre/trees';
import { SquareRegular } from '@fluentui/react-icons';
import { useCanvas } from '@/store/canvas';
const ARTBOARDS = [
{ id: 'dashboard-card', label: 'DashboardCard' },
{ id: 'user-profile', label: 'UserProfile' },
{ id: 'nav-sidebar', label: 'NavSidebar' },
{ id: 'data-table', label: 'DataTable' },
];
import { useWorkspace } from '@/hooks/useWorkspace';
import { useArtboards } from '@/hooks/useArtboards';
const FILE_PATHS = [
'src/components/DashboardCard.tsx',
@@ -54,6 +49,8 @@ const treeThemeStyles = themeToTreeStyles({
export function ArtboardNavigator() {
const { selectedArtboardId, selectArtboard } = useCanvas();
const { data: workspace } = useWorkspace();
const { artboards } = useArtboards(workspace?.id);
const { model } = useFileTree({
paths: FILE_PATHS,
@@ -78,7 +75,7 @@ export function ArtboardNavigator() {
{/* ── Artboards ── */}
<SectionLabel>Artboards</SectionLabel>
<div style={{ padding: '2px 6px 0' }}>
{ARTBOARDS.map((ab) => {
{artboards.map((ab) => {
const sel = selectedArtboardId === ab.id;
return (
<NavRow
+53
View File
@@ -0,0 +1,53 @@
import { useQuery } from '@tanstack/react-query';
import type { Artboard } from '@originmain/origin-graph';
export interface CanvasArtboard {
id: string;
label: string;
x: number;
y: number;
width: number;
height: number;
renderUrl?: string;
}
const DEMO_ARTBOARDS: CanvasArtboard[] = [
{ id: 'dashboard-card', label: 'DashboardCard', x: 120, y: 100, width: 280, height: 200 },
{ id: 'user-profile', label: 'UserProfile', x: 460, y: 100, width: 200, height: 260 },
{ id: 'nav-sidebar', label: 'NavSidebar', x: 120, y: 360, width: 200, height: 340 },
{ id: 'data-table', label: 'DataTable', x: 380, y: 380, width: 420, height: 280 },
];
function toCanvasArtboard(ab: Artboard): CanvasArtboard | null {
const meta = ab.metadata_jsonb;
const x = typeof meta['x'] === 'number' ? meta['x'] : null;
const y = typeof meta['y'] === 'number' ? meta['y'] : null;
const width = typeof meta['width'] === 'number' ? meta['width'] : null;
const height = typeof meta['height'] === 'number' ? meta['height'] : null;
if (x === null || y === null || width === null || height === null) return null;
const base: CanvasArtboard = { id: ab.id, label: ab.name, x, y, width, height };
if (typeof meta['renderUrl'] === 'string') base.renderUrl = meta['renderUrl'];
return base;
}
async function fetchArtboards(workspaceId: string): Promise<CanvasArtboard[]> {
const res = await fetch(`/api/artboards?workspaceId=${encodeURIComponent(workspaceId)}`);
if (!res.ok) throw new Error(`Artboard fetch failed: ${res.status}`);
const rows = (await res.json()) as Artboard[];
return rows.map(toCanvasArtboard).filter((ab): ab is CanvasArtboard => ab !== null);
}
export function useArtboards(workspaceId: string | undefined) {
const query = useQuery({
queryKey: ['artboards', workspaceId],
queryFn: () => fetchArtboards(workspaceId!),
enabled: workspaceId !== undefined,
staleTime: 30_000,
});
// Show demo artboards while loading or when workspace has no artboards yet.
const artboards =
!query.data || query.data.length === 0 ? DEMO_ARTBOARDS : query.data;
return { artboards, isLoading: query.isLoading, error: query.error };
}
+17
View File
@@ -0,0 +1,17 @@
import { useQuery } from '@tanstack/react-query';
import type { Workspace } from '@originmain/origin-graph';
async function fetchWorkspace(): Promise<Workspace> {
const res = await fetch('/api/workspace');
if (!res.ok) throw new Error(`Workspace fetch failed: ${res.status}`);
return res.json() as Promise<Workspace>;
}
export function useWorkspace() {
return useQuery({
queryKey: ['workspace'],
queryFn: fetchWorkspace,
staleTime: 5 * 60_000, // workspace data rarely changes
retry: 2,
});
}
+33
View File
@@ -0,0 +1,33 @@
// ── Supabase client helpers ───────────────────────────────────────────────────
// browserClient() — uses the anon key; safe to call on the client side.
// RLS policies apply (currently: deny all anon).
// serverClient() — uses the service-role key; ONLY call from Server
// Components or API route handlers. Never expose to the
// browser. Service role bypasses RLS entirely.
import { createClient } from '@supabase/supabase-js';
import type { DbClient } from '@originmain/origin-graph';
function requireEnv(name: string): string {
const val = process.env[name];
if (!val) throw new Error(`Missing required environment variable: ${name}`);
return val;
}
/** Browser-safe Supabase client (anon key, RLS enforced). */
export function browserClient(): DbClient {
return createClient(
requireEnv('NEXT_PUBLIC_SUPABASE_URL'),
requireEnv('NEXT_PUBLIC_SUPABASE_ANON_KEY'),
) as unknown as DbClient;
}
/** Server-only Supabase client (service-role key, bypasses RLS). */
export function serverClient(): DbClient {
return createClient(
requireEnv('NEXT_PUBLIC_SUPABASE_URL'),
requireEnv('SUPABASE_SERVICE_ROLE_KEY'),
// Disable the auto-refresh token flow — this client never runs in a browser.
{ auth: { autoRefreshToken: false, persistSession: false } },
) as unknown as DbClient;
}
+12 -12
View File
@@ -62,12 +62,13 @@ export const useHistory = create<HistoryStore>((set, get) => ({
},
undo(artboardId) {
const stack = get().stacks[artboardId] ?? emptyStack();
const last = stack.past[stack.past.length - 1];
if (!last) return undefined;
// Read + write atomically inside set() to avoid TOCTOU between get() and set().
let undone: EditEntry | undefined;
set(state => {
const current = state.stacks[artboardId] ?? emptyStack();
const last = current.past[current.past.length - 1];
if (!last) return state; // nothing to undo — no-op
undone = last;
return {
stacks: {
...state.stacks,
@@ -78,17 +79,17 @@ export const useHistory = create<HistoryStore>((set, get) => ({
},
};
});
return last;
return undone;
},
redo(artboardId) {
const stack = get().stacks[artboardId] ?? emptyStack();
const next = stack.future[0];
if (!next) return undefined;
// Read + write atomically inside set() to avoid TOCTOU between get() and set().
let redone: EditEntry | undefined;
set(state => {
const current = state.stacks[artboardId] ?? emptyStack();
const next = current.future[0];
if (!next) return state; // nothing to redo — no-op
redone = next;
return {
stacks: {
...state.stacks,
@@ -99,8 +100,7 @@ export const useHistory = create<HistoryStore>((set, get) => ({
},
};
});
return next;
return redone;
},
canUndo(artboardId) {
File diff suppressed because one or more lines are too long
+14
View File
@@ -7,11 +7,13 @@
import type {
Artboard,
Workspace,
Origin,
IntentDiff,
DesignLanguageFile,
AgentSession,
TeamMember,
InsertArtboard,
InsertOrigin,
InsertIntentDiff,
InsertAgentSession,
DiffStatus,
@@ -166,6 +168,18 @@ export async function getActiveDesignLanguageFile(
return data;
}
// ── Origin queries ────────────────────────────────────────────────────────────
export async function createOrigin(db: DbClient, row: InsertOrigin): Promise<Origin> {
const { data, error } = await (db
.from('origins')
.insert(row)
.select()
.single() as Promise<{ data: Origin; error: DbError | null }>);
if (error) throw new Error(error.message);
return data;
}
// ── Agent session queries ─────────────────────────────────────────────────────
export async function createAgentSession(db: DbClient, row: InsertAgentSession): Promise<AgentSession> {
+118
View File
@@ -70,6 +70,9 @@ importers:
packages/app:
dependencies:
'@anthropic-ai/sdk':
specifier: ^0.56.0
version: 0.56.0
'@clerk/nextjs':
specifier: ^7.2.7
version: 7.2.7(next@15.5.15(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -79,12 +82,30 @@ importers:
'@fluentui/react-icons':
specifier: ^2.0.0
version: 2.0.324(react@19.2.5)
'@originmain/agent-bridge':
specifier: workspace:*
version: link:../agent-bridge
'@originmain/ai-layer':
specifier: workspace:*
version: link:../ai-layer
'@originmain/design-language':
specifier: workspace:*
version: link:../design-language
'@originmain/diff-engine':
specifier: workspace:*
version: link:../diff-engine
'@originmain/integrations':
specifier: workspace:*
version: link:../integrations
'@originmain/multiplayer':
specifier: workspace:*
version: link:../multiplayer
'@originmain/origin-graph':
specifier: workspace:*
version: link:../origin-graph
'@originmain/platform':
specifier: workspace:*
version: link:../platform
'@originmain/renderer':
specifier: workspace:*
version: link:../renderer
@@ -97,6 +118,9 @@ importers:
'@pierre/trees':
specifier: 1.0.0-beta.3
version: 1.0.0-beta.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@supabase/supabase-js':
specifier: ^2.0.0
version: 2.104.1
'@tanstack/react-query':
specifier: ^5.62.0
version: 5.99.2(react@19.2.5)
@@ -1510,6 +1534,33 @@ packages:
'@stablelib/base64@1.0.1':
resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==}
'@supabase/auth-js@2.104.1':
resolution: {integrity: sha512-pqFnDKekq1isqlqnzqzyJ3mzmho+o+FjfVTqhKY3PFlwj2anx3OPznO1kbo1ZEwD8zg1r4EAFf/7pplLyX0ocQ==}
engines: {node: '>=20.0.0'}
'@supabase/functions-js@2.104.1':
resolution: {integrity: sha512-JjAH4JN9rZzxh4plQnILPrQZXAG6ccoRS6z9hQAGmXpRSwJA+7CWbsDV2R82I8MROlGDsjqj1Ot/cWpTfdf6xg==}
engines: {node: '>=20.0.0'}
'@supabase/phoenix@0.4.0':
resolution: {integrity: sha512-RHSx8bHS02xwfHdAbX5Lpbo6PXbgyf7lTaXTlwtFDPwOIw64NnVRwFAXGojHhjtVYI+PEPNSWwkL90f4agN3bw==}
'@supabase/postgrest-js@2.104.1':
resolution: {integrity: sha512-RqlLpvgXsjcc27fLyHNGm3zN0KDWXbkdTdaFtaEdX83RsTEqH7BAmshH7zoUMml5lL04naUeRjS3B81O6jZcJw==}
engines: {node: '>=20.0.0'}
'@supabase/realtime-js@2.104.1':
resolution: {integrity: sha512-dVJHhFB2ErBd0/2qE9G8CedCrGoAtBfL9Q4zbSMXO7b1Cpld916ljSiX21mURUqijPf1WoPQG4Bp/averUzk/g==}
engines: {node: '>=20.0.0'}
'@supabase/storage-js@2.104.1':
resolution: {integrity: sha512-2bQaLbkRshctkUVuqamwYZDEd+0cGSc9DY9sjh92DcA5hu1F/1AP8p6gxGr76sgdK9Ngi0rh+2Kdh+uC4hcnGA==}
engines: {node: '>=20.0.0'}
'@supabase/supabase-js@2.104.1':
resolution: {integrity: sha512-E0H/CtVmaGjiAy+ieZ5ZB/1EqxXcGdaFaAc23AE5zaYfz6NtCNDcmaEdoGPYMPFH5pE6drGG6e3ljPmkFoGVxQ==}
engines: {node: '>=20.0.0'}
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
@@ -1553,6 +1604,9 @@ packages:
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
'@typescript-eslint/eslint-plugin@8.59.0':
resolution: {integrity: sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1961,6 +2015,10 @@ packages:
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
iceberg-js@0.8.1:
resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==}
engines: {node: '>=20.0.0'}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -2504,6 +2562,18 @@ packages:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
ws@8.20.0:
resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@@ -4227,6 +4297,46 @@ snapshots:
'@stablelib/base64@1.0.1': {}
'@supabase/auth-js@2.104.1':
dependencies:
tslib: 2.8.1
'@supabase/functions-js@2.104.1':
dependencies:
tslib: 2.8.1
'@supabase/phoenix@0.4.0': {}
'@supabase/postgrest-js@2.104.1':
dependencies:
tslib: 2.8.1
'@supabase/realtime-js@2.104.1':
dependencies:
'@supabase/phoenix': 0.4.0
'@types/ws': 8.18.1
tslib: 2.8.1
ws: 8.20.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@supabase/storage-js@2.104.1':
dependencies:
iceberg-js: 0.8.1
tslib: 2.8.1
'@supabase/supabase-js@2.104.1':
dependencies:
'@supabase/auth-js': 2.104.1
'@supabase/functions-js': 2.104.1
'@supabase/postgrest-js': 2.104.1
'@supabase/realtime-js': 2.104.1
'@supabase/storage-js': 2.104.1
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
@@ -4270,6 +4380,10 @@ snapshots:
'@types/unist@3.0.3': {}
'@types/ws@8.18.1':
dependencies:
'@types/node': 22.19.17
'@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@@ -4731,6 +4845,8 @@ snapshots:
html-void-elements@3.0.0: {}
iceberg-js@0.8.1: {}
ignore@5.3.2: {}
ignore@7.0.5: {}
@@ -5315,6 +5431,8 @@ snapshots:
string-width: 5.1.2
strip-ansi: 7.2.0
ws@8.20.0: {}
yocto-queue@0.1.0: {}
zod@3.25.76: {}
+233
View File
@@ -0,0 +1,233 @@
-- ═══════════════════════════════════════════════════════════════════════════
-- Originmain — Initial Schema
-- Run: supabase db push (or paste into Supabase SQL Editor)
-- ═══════════════════════════════════════════════════════════════════════════
-- ── Extensions ───────────────────────────────────────────────────────────────
create extension if not exists "uuid-ossp";
-- ── Enums (CHECK constraints on text columns) ─────────────────────────────
-- Using text + CHECK rather than postgres enums so adding values is a
-- non-destructive ALTER rather than a DROP/RE-CREATE cycle.
-- ── Tables ───────────────────────────────────────────────────────────────────
-- workspaces
create table if not exists workspaces (
id uuid primary key default uuid_generate_v4(),
name text not null,
owner_id text not null, -- Clerk user ID
plan text not null default 'FREE'
check (plan in ('FREE','TEAM','ENTERPRISE')),
settings_jsonb jsonb not null default '{}',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists workspaces_owner_id_idx on workspaces (owner_id);
-- origins
create table if not exists origins (
id uuid primary key default uuid_generate_v4(),
type text not null
check (type in ('GIT_COMMIT','LINEAR_ISSUE','SLACK_MESSAGE','URL','FORK')),
source_ref text not null,
source_metadata_jsonb jsonb not null default '{}',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
-- artboards
create table if not exists artboards (
id uuid primary key default uuid_generate_v4(),
workspace_id uuid not null references workspaces (id) on delete cascade,
name text not null,
origin_id uuid references origins (id) on delete set null,
parent_artboard_id uuid references artboards (id) on delete set null,
-- metadata_jsonb stores canvas position/size and optional renderUrl:
-- { x, y, width, height, renderUrl? }
metadata_jsonb jsonb not null default '{}',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists artboards_workspace_id_idx on artboards (workspace_id);
create index if not exists artboards_parent_artboard_id_idx on artboards (parent_artboard_id);
create index if not exists artboards_created_at_idx on artboards (created_at desc);
-- intent_diffs
create table if not exists intent_diffs (
id uuid primary key default uuid_generate_v4(),
artboard_id uuid not null references artboards (id) on delete cascade,
author_id text not null, -- Clerk user ID
changes_jsonb jsonb not null default '{}',
summary text not null default '',
status text not null default 'DRAFT'
check (status in ('DRAFT','EXPORTED','IMPLEMENTED','BLOCKED')),
notes text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists intent_diffs_artboard_id_idx on intent_diffs (artboard_id);
create index if not exists intent_diffs_status_idx on intent_diffs (status);
create index if not exists intent_diffs_created_at_idx on intent_diffs (created_at desc);
-- agent_sessions
create table if not exists agent_sessions (
id uuid primary key default uuid_generate_v4(),
artboard_id uuid not null references artboards (id) on delete cascade,
diff_id uuid references intent_diffs (id) on delete set null,
agent_type text not null
check (agent_type in ('CURSOR','CLAUDE_CODE','GENERIC')),
messages_jsonb jsonb not null default '[]',
status text not null default 'ACTIVE'
check (status in ('ACTIVE','COMPLETED','FAILED')),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists agent_sessions_artboard_id_idx on agent_sessions (artboard_id);
create index if not exists agent_sessions_diff_id_idx on agent_sessions (diff_id);
-- design_language_files
create table if not exists design_language_files (
id uuid primary key default uuid_generate_v4(),
workspace_id uuid not null references workspaces (id) on delete cascade,
name text not null,
schema_jsonb jsonb not null default '{}',
version integer not null default 1,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists design_language_files_workspace_id_idx on design_language_files (workspace_id);
create index if not exists design_language_files_version_idx on design_language_files (workspace_id, version desc);
-- team_members
create table if not exists team_members (
id uuid primary key default uuid_generate_v4(),
workspace_id uuid not null references workspaces (id) on delete cascade,
user_id text not null, -- Clerk user ID
role text not null default 'VIEWER'
check (role in ('OWNER','DESIGNER','ENGINEER','PM','VIEWER')),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (workspace_id, user_id)
);
create index if not exists team_members_workspace_id_idx on team_members (workspace_id);
create index if not exists team_members_user_id_idx on team_members (user_id);
-- ── Artboard ancestry (closure table) ────────────────────────────────────────
-- Maintained by triggers below. Stores every ancestor at any depth so hierarchy
-- queries are O(1) lookups rather than recursive CTEs.
create table if not exists artboard_ancestry (
artboard_id uuid not null references artboards (id) on delete cascade,
ancestor_id uuid not null references artboards (id) on delete cascade,
depth integer not null,
primary key (artboard_id, ancestor_id)
);
create index if not exists artboard_ancestry_ancestor_idx on artboard_ancestry (ancestor_id);
-- ── Triggers: updated_at ─────────────────────────────────────────────────────
create or replace function set_updated_at()
returns trigger language plpgsql as $$
begin
new.updated_at = now();
return new;
end;
$$;
create or replace trigger workspaces_updated_at
before update on workspaces
for each row execute function set_updated_at();
create or replace trigger artboards_updated_at
before update on artboards
for each row execute function set_updated_at();
create or replace trigger intent_diffs_updated_at
before update on intent_diffs
for each row execute function set_updated_at();
create or replace trigger agent_sessions_updated_at
before update on agent_sessions
for each row execute function set_updated_at();
create or replace trigger design_language_files_updated_at
before update on design_language_files
for each row execute function set_updated_at();
create or replace trigger team_members_updated_at
before update on team_members
for each row execute function set_updated_at();
-- ── Trigger: artboard ancestry maintenance ────────────────────────────────────
create or replace function maintain_artboard_ancestry()
returns trigger language plpgsql as $$
begin
-- Self-row (depth 0)
insert into artboard_ancestry (artboard_id, ancestor_id, depth)
values (new.id, new.id, 0)
on conflict do nothing;
-- Inherit all ancestors of the parent
if new.parent_artboard_id is not null then
insert into artboard_ancestry (artboard_id, ancestor_id, depth)
select new.id, ancestor_id, depth + 1
from artboard_ancestry
where artboard_id = new.parent_artboard_id
on conflict do nothing;
end if;
return new;
end;
$$;
create or replace trigger artboards_ancestry_insert
after insert on artboards
for each row execute function maintain_artboard_ancestry();
-- ── RPC: get_diffs_by_status ──────────────────────────────────────────────────
-- Used by getDiffsByStatus() in origin-graph/queries.ts.
-- Joins intent_diffs → artboards to scope by workspace_id.
create or replace function get_diffs_by_status(p_workspace_id uuid, p_status text)
returns setof intent_diffs
language sql stable as $$
select d.*
from intent_diffs d
join artboards a on a.id = d.artboard_id
where a.workspace_id = p_workspace_id
and d.status = p_status
order by d.created_at desc;
$$;
-- ── Row Level Security ────────────────────────────────────────────────────────
-- API routes use the service-role key (bypasses RLS). Policies below apply
-- when the anon/browser key is used for direct-to-Supabase reads.
-- All tables are locked down; only authenticated Postgres users may read.
alter table workspaces enable row level security;
alter table origins enable row level security;
alter table artboards enable row level security;
alter table intent_diffs enable row level security;
alter table agent_sessions enable row level security;
alter table design_language_files enable row level security;
alter table team_members enable row level security;
alter table artboard_ancestry enable row level security;
-- Service role bypasses RLS automatically. For completeness, deny all anon.
-- Add permissive policies when you add Supabase Auth integration.
create policy "No anon access — workspaces" on workspaces for all to anon using (false);
create policy "No anon access — origins" on origins for all to anon using (false);
create policy "No anon access — artboards" on artboards for all to anon using (false);
create policy "No anon access — intent_diffs" on intent_diffs for all to anon using (false);
create policy "No anon access — agent_sessions" on agent_sessions for all to anon using (false);
create policy "No anon access — design_language_files" on design_language_files for all to anon using (false);
create policy "No anon access — team_members" on team_members for all to anon using (false);
create policy "No anon access — artboard_ancestry" on artboard_ancestry for all to anon using (false);