diff --git a/.claude/launch.json b/.claude/launch.json
index 3f017ca3b..853b8bf65 100644
--- a/.claude/launch.json
+++ b/.claude/launch.json
@@ -1,6 +1,12 @@
{
"version": "0.0.1",
"configurations": [
+ {
+ "name": "preview-dist",
+ "runtimeExecutable": "npx",
+ "runtimeArgs": ["vite", "preview", "--port", "4173", "--strictPort"],
+ "port": 4173
+ },
{
"name": "dev",
"runtimeExecutable": "npm",
diff --git a/.github/workflows/deploy-gh-pages.yml b/.github/workflows/deploy-gh-pages.yml
new file mode 100644
index 000000000..e57c67d5f
--- /dev/null
+++ b/.github/workflows/deploy-gh-pages.yml
@@ -0,0 +1,66 @@
+name: Rollback deploy to GitHub Pages
+
+# Emergency path only. The press runs on Cloudflare Pages
+# (deploy.yml); this workflow re-publishes to the gh-pages branch by
+# hand if Cloudflare is down or the cutover has to be reversed.
+# Run it, then point kernel.chat DNS back at GitHub Pages.
+on:
+ workflow_dispatch: {}
+
+permissions:
+ contents: write
+ pages: write
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Design adherence — no raw hex in the editorial design-system layer
+ run: npm run lint:adherence
+
+ - name: Editorial law — artifact-first, no POPEYE naming, no emoji
+ run: npm run lint:editorial
+
+ - name: Type-check
+ run: npx tsc --noEmit
+
+ - name: Build sitemap from the issue registry
+ run: npm run sitemap
+
+ - name: Build
+ run: npm run build
+ env:
+ VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
+ VITE_SUPABASE_KEY: ${{ secrets.VITE_SUPABASE_KEY }}
+ VITE_STRIPE_PUBLISHABLE_KEY: ${{ secrets.VITE_STRIPE_PUBLISHABLE_KEY }}
+ VITE_STRIPE_KERNEL_PRICE_ID: ${{ secrets.VITE_STRIPE_KERNEL_PRICE_ID }}
+ VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
+ VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
+
+ - name: Build artifact index
+ run: npm run artifacts:index
+
+ - name: Include editorial artifacts
+ run: |
+ mkdir -p dist/artifacts
+ cp artifacts/*.html dist/artifacts/
+
+ - name: Deploy to GitHub Pages
+ uses: peaceiris/actions-gh-pages@v4
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./dist
+ cname: kernel.chat
+ enable_jekyll: false
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 726af81ff..98ea7e64c 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -1,4 +1,4 @@
-name: Deploy to GitHub Pages
+name: Deploy to Cloudflare Pages
on:
push:
@@ -6,8 +6,7 @@ on:
workflow_dispatch: {}
permissions:
- contents: write
- pages: write
+ contents: read
jobs:
deploy:
@@ -34,6 +33,9 @@ jobs:
- name: Type-check
run: npx tsc --noEmit
+ - name: Build sitemap from the issue registry
+ run: npm run sitemap
+
- name: Build
run: npm run build
env:
@@ -52,10 +54,9 @@ jobs:
mkdir -p dist/artifacts
cp artifacts/*.html dist/artifacts/
- - name: Deploy to GitHub Pages
- uses: peaceiris/actions-gh-pages@v4
+ - name: Deploy to Cloudflare Pages
+ uses: cloudflare/wrangler-action@v3
with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- publish_dir: ./dist
- cname: kernel.chat
- enable_jekyll: false
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ command: pages deploy dist --project-name=kernel-chat
diff --git a/SCRATCHPAD.md b/SCRATCHPAD.md
index 178031964..af8be08df 100644
--- a/SCRATCHPAD.md
+++ b/SCRATCHPAD.md
@@ -2,6 +2,35 @@
> This file persists context between Claude Code sessions.
+## Session 2026-07-20 — UX anti-pattern audit → real URLs (PR #62)
+
+- Ran the design-QA audit against live kernel.chat. Editorial surface
+ healthy (no overflow, alt clean, reduced-motion complete, focus
+ visible). The real anti-patterns: (1) artifact index + 421 were
+ 404 live (fixed by merging fix/editorial-enforcement → main, now
+ 200); (2) every deep link served HTTP 404 → hash bounce; (3) the
+ outline:none / clickable-div debt all sits in UNROUTED legacy
+ surfaces — EnginePage is the parked chat product, not junk.
+- Merge to main taught check-editorial.mjs the documented-exception
+ path: ARTIFACT_WAIVER ("no separate artifact edition" in rendered
+ colophon copy) — 427 uses it legitimately.
+- **PR #62 (`feat/real-urls-cloudflare`)**, three commits:
+ 1. createBrowserRouter + legacyHashRedirect.ts (module-scope, BEFORE
+ router creation — the router captures location at import time, a
+ boot-sequence shim renders the wrong route); _redirects; sitemap
+ generator (scripts/build-sitemap.mjs, 77 URLs); PostHog
+ capture_pageview 'history_change'; deploy.yml → Cloudflare Pages
+ (wrangler-action v3, project kernel-chat); deploy-gh-pages.yml
+ rollback; e2e path URLs; PUBLISHING.md §VII rewritten.
+ 2. Cut nine dead pages (4,068 lines) incl. PricingPage; engine
+ tree parked, untouched.
+ 3. A11y: back-to-top 98×13 → 122×44 (nowrap needed — 40px-wide
+ colophon column wraps padded labels); skip link → #feature-well.
+- **BLOCKED on Isaac before merge:** create CF Pages project
+ `kernel-chat` + repo secrets CLOUDFLARE_API_TOKEN /
+ CLOUDFLARE_ACCOUNT_ID; DNS cutover after pages.dev verifies.
+- Gates at branch head: tsc clean, 794/794 tests, 236KB gzip.
+
## Session 2026-07-20 (cont.) — ISSUE 427: THE MOAT IS REALITY
- Pressed ISSUE 427 · FEB 2027 from a reader-supplied transcript of
diff --git a/e2e/tests/auth.spec.ts b/e2e/tests/auth.spec.ts
index a683b9696..52b49071d 100644
--- a/e2e/tests/auth.spec.ts
+++ b/e2e/tests/auth.spec.ts
@@ -28,7 +28,7 @@ test.describe('Authentication', () => {
})
test('redirects unauthenticated users from protected routes', async ({ page }) => {
- await page.goto('/#/admin')
+ await page.goto('/admin')
await page.waitForSelector('.ka-gate, .ka-landing, .engine-body', { timeout: 15000 })
// Should not show admin page
const admin = await page.$('.admin-page')
diff --git a/e2e/tests/issue-415-close.spec.ts b/e2e/tests/issue-415-close.spec.ts
index 84969f892..c812a8fac 100644
--- a/e2e/tests/issue-415-close.spec.ts
+++ b/e2e/tests/issue-415-close.spec.ts
@@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test'
test.describe('ISSUE 415 — close primitive', () => {
test('reader can show more items, then stop, and gets a receipt', async ({ page }) => {
- await page.goto('/#/issues/415')
+ await page.goto('/issues/415')
await expect(page.getByRole('button', { name: 'Show me one more' })).toBeVisible()
await expect(page.getByRole('button', { name: "I'll stop here" })).toBeVisible()
@@ -21,7 +21,7 @@ test.describe('ISSUE 415 — close primitive', () => {
test('mobile viewport renders both controls at equal size', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
- await page.goto('/#/issues/415')
+ await page.goto('/issues/415')
const more = page.getByRole('button', { name: 'Show me one more' })
const stop = page.getByRole('button', { name: "I'll stop here" })
await expect(more).toBeVisible()
@@ -36,7 +36,7 @@ test.describe('ISSUE 415 — close primitive', () => {
page.on('console', (msg) => {
if (msg.type() === 'error') errors.push(msg.text())
})
- await page.goto('/#/issues/415')
+ await page.goto('/issues/415')
await page.getByRole('button', { name: 'Show me one more' }).click()
await page.getByRole('button', { name: "I'll stop here" }).click()
expect(errors).toEqual([])
diff --git a/package.json b/package.json
index 5da90de5b..77567d217 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"lint:adherence": "node scripts/check-adherence.mjs",
"lint:editorial": "node scripts/check-editorial.mjs",
"artifacts:index": "node scripts/build-artifact-index.mjs",
+ "sitemap": "node scripts/build-sitemap.mjs",
"preview": "vite preview",
"test": "vitest run",
"test:e2e": "playwright test",
diff --git a/public/_redirects b/public/_redirects
new file mode 100644
index 000000000..df4f6d1f1
--- /dev/null
+++ b/public/_redirects
@@ -0,0 +1,4 @@
+# Cloudflare Pages SPA fallback. Real files in the upload manifest
+# (artifacts/*.html, sitemap.xml, issue plates) are served before this
+# rule applies, so only unmatched paths fall through to the app shell.
+/* /index.html 200
diff --git a/public/sitemap.xml b/public/sitemap.xml
index 4ef855f04..c87233e84 100644
--- a/public/sitemap.xml
+++ b/public/sitemap.xml
@@ -2,26 +2,387 @@
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 0c583b133..3155eb38c 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -46,10 +46,15 @@ export function Layout() { }, [location.pathname]) return ( -
{t('modal.legalAgreement')}{' '} - {t('modal.termsLink')} + {t('modal.termsLink')} {' & '} - {t('modal.privacyLink')} + {t('modal.privacyLink')}
> )} diff --git a/src/components/MagazineFrame.tsx b/src/components/MagazineFrame.tsx index e37675db5..bb3cda6f8 100644 --- a/src/components/MagazineFrame.tsx +++ b/src/components/MagazineFrame.tsx @@ -1,5 +1,6 @@ import { useEffect } from 'react' import type { ReactNode } from 'react' +import { useNavigate } from 'react-router-dom' import { ISSUE } from '../content/issue' import type { IssueRecord } from '../content/issues' import { PopIcon } from './ornaments' @@ -58,12 +59,13 @@ export function MagazineFrame({ return () => { document.body.classList.remove('ka-scrollable-page') } }, []) + const navigate = useNavigate() const stockClass = `pop-stock-${stock}` const issue = issueOverride ?? ISSUE const folio = page === undefined ? kicker : `${kicker} · P. ${String(page).padStart(2, '0')}` const goHome = () => { - window.location.hash = '#/' + navigate('/') } return ( @@ -126,13 +128,13 @@ export function MagazineFrame({ > ← BACK TO COVER - + ABOUT → - + MADE TO ORDER → - + ISSUES → diff --git a/src/components/PreviouslyStrip.tsx b/src/components/PreviouslyStrip.tsx index 636227809..de183ec5a 100644 --- a/src/components/PreviouslyStrip.tsx +++ b/src/components/PreviouslyStrip.tsx @@ -14,7 +14,7 @@ interface PreviouslyStripProps { */ export function PreviouslyStrip({ previous }: PreviouslyStripProps) { return ( - + PREVIOUSLY ISSUE {previous.number} · {previous.month} {previous.year} diff --git a/src/components/discovery/SearchBar.tsx b/src/components/discovery/SearchBar.tsx deleted file mode 100644 index c252ec7d5..000000000 --- a/src/components/discovery/SearchBar.tsx +++ /dev/null @@ -1,73 +0,0 @@ -// ─── Search Bar ──────────────────────────────────────────────── -// -// Debounced full-text search input for the explore page. - -import { useState, useCallback, useRef, useEffect } from 'react' - -interface SearchBarProps { - value: string - onSearch: (query: string) => void - placeholder?: string - debounceMs?: number -} - -export function SearchBar({ - value, - onSearch, - placeholder = 'Search articles...', - debounceMs = 400, -}: SearchBarProps) { - const [localValue, setLocalValue] = useState(value) - const timerRef = useRefLoading dashboard...
-- {searchQuery ? 'No matching users' : 'No users yet'} -
- )} -- No learned memory yet -
- )} -- Select a user to view their profile -
- )} -No recent activity
- )} -- No collective insights yet -
- )} -Loading...
- ) : modQueue.length === 0 ? ( -- No content pending review -
- ) : ( -
- No client scores yet. Clients type kernel.hat to submit.
-
- Access Kernel's 17-agent intelligence layer via a simple REST API. - One call gets specialist routing, multi-agent collaboration, and structured responses. -
- - {/* ── K:BOT Terminal Agent ── */} -- K:BOT gives you the full power of Kernel's agent system from your terminal. - It runs tools locally (file ops, git, bash) for free — only AI reasoning uses your API quota. -
-{`# Install
-curl -fsSL https://kernel.chat/install.sh | bash
-
-# Or via npm
-npm install -g @kernel.chat/kbot
-
-# Configure
-kbot auth
-
-# Use
-kbot "fix the TypeScript errors in src/utils"
-kbot "research the latest React Server Components changes"
-kbot # interactive REPL`}
-
- - K:BOT is designed to conserve your tokens and messages. Simple commands (file reads, git status, directory - listings) are handled locally without any API call. The agent batches context upfront for one-shot accuracy, - reducing round trips. Tool execution always runs on your machine for free. -
-
- All API requests require a Bearer token using your API key.
- Keys are prefixed with kn_live_ and can be managed from your account dashboard.
-
{`curl -X POST https://eoxxpyixdieprsxlpwcs.supabase.co/functions/v1/kernel-api/chat \\
- -H "Authorization: Bearer kn_live_your_key_here" \\
- -H "Content-Type: application/json" \\
- -d '{"message": "Hello"}'`}
- Send a message and get an agent-routed response. If no agent is specified, Kernel auto-classifies your intent and routes to the best specialist.
- -{`{
- "message": "Explain quantum computing",
- "agent": "researcher", // optional — auto-routes if omitted
- "mode": "json", // "json" (default) or "stream" (SSE)
- "system": "Be concise.", // enterprise only — custom system prompt
- "max_tokens": 4096, // optional, max 8192
- "previous_messages": [ // optional conversation context
- { "role": "user", "content": "..." },
- { "role": "assistant", "content": "..." }
- ],
- "tools": [ // optional — tool definitions for tool-use
- { "name": "web_search", "description": "Search the web" }
- ],
- "tool_results": [ // optional — results from previous tool calls
- { "tool_call_id": "tc_1", "result": "..." }
- ]
-}`}
-
- {`{
- "id": "msg_uuid",
- "agent": "researcher",
- "type": "text",
- "content": "Quantum computing is...",
- "model": "claude-sonnet-4-6",
- "usage": {
- "input_tokens": 234,
- "output_tokens": 1023,
- "cost_usd": 0.018
- },
- "classification": { // present when auto-routed
- "agent_id": "researcher",
- "confidence": 0.92,
- "complexity": 0.45
- }
-}`}
-
- When the agent wants to use tools, the response contains tool_calls instead of content:
-{`{
- "id": "msg_uuid",
- "agent": "researcher",
- "type": "tool_calls",
- "tool_calls": [
- { "id": "tc_1", "name": "web_search", "arguments": { "query": "quantum computing 2026" } }
- ],
- "model": "claude-sonnet-4-6",
- "usage": { ... }
-}`}
- Execute the tool locally, then send results back via tool_results in the next request.
Classify intent without generating a full response. Uses collective intelligence patterns when available — may skip the classifier entirely for high-confidence matches.
- -{`{
- "message": "Write a Python script to sort a list"
-}`}
-
- {`{
- "agent_id": "coder",
- "confidence": 0.95,
- "complexity": 0.3,
- "reasoning": "Code generation request — Python scripting task"
-}`}
- Multi-agent collaboration. Multiple agents contribute in parallel, then a synthesis model combines their perspectives. Requires Pro tier.
- -{`{
- "message": "Design a microservices architecture for a real-time trading platform",
- "agents": ["architect", "coder", "guardian"],
- "synthesis_model": "sonnet" // optional, default: sonnet
-}`}
-
- {`{
- "id": "msg_uuid",
- "agents": ["architect", "coder", "guardian"],
- "contributions": [
- { "agent": "architect", "content": "For a real-time trading platform..." },
- { "agent": "coder", "content": "The implementation should use..." },
- { "agent": "guardian", "content": "Security considerations include..." }
- ],
- "synthesis": "Combined analysis: The optimal architecture...",
- "model": "claude-sonnet-4-6"
-}`}
- Query the collective intelligence — learned patterns from all API interactions. The Kernel Matrix gets smarter with every request across all users.
- -{`GET /knowledge?category=coder`}
-
- {`{
- "patterns": [
- {
- "type": "routing_rule",
- "pattern": {
- "category": "coder",
- "agent": "coder",
- "accuracy": 0.94,
- "avg_confidence": 0.91
- },
- "confidence": 0.94,
- "sample_count": 1247
- }
- ]
-}`}
- List available agents for your API key tier.
-{`{
- "agents": [
- { "id": "kernel", "name": "Kernel", "role": "General Assistant" },
- { "id": "researcher", "name": "Researcher", "role": "Research & Analysis" },
- { "id": "coder", "name": "Coder", "role": "Programming" },
- { "id": "writer", "name": "Writer", "role": "Content Creation" },
- { "id": "analyst", "name": "Analyst", "role": "Strategy & Evaluation" }
- ],
- "tier": "free"
-}`}
- Get monthly usage statistics for your API key, including per-agent breakdown.
-{`{
- "tier": "pro",
- "monthly_messages": { "count": 87, "limit": 200 },
- "monthly_window_start": "2026-03-01T00:00:00Z",
- "per_agent": {
- "coder": { "messages": 43, "input_tokens": 12500, "output_tokens": 89000, "cost_usd": 1.37 },
- "researcher": { "messages": 31, "input_tokens": 9800, "output_tokens": 45000, "cost_usd": 0.70 }
- }
-}`}
- | Tier | -Price | -Messages / mo | -Agents | -Swarm | -K:BOT Tools | -Rate Limit | -
|---|---|---|---|---|---|---|
| Free | -$0 | -10 | -Core 5 | -No | -Files, Git | -10/min | -
| Pro | -$15/mo | -200 | -All 17 | -Yes | -All tools | -30/min | -
| Status | Error | Description |
|---|---|---|
| 401 | Invalid API key | Key is missing, malformed, or revoked |
| 403 | Monthly limit exceeded | Monthly message quota exhausted |
| 403 | Pro required | Feature requires Pro tier |
| 429 | Rate limited | Per-minute rate limit exceeded. Check Retry-After header. |
| 400 | Invalid request | Missing required fields or invalid agent ID |
| 502 | Upstream error | AI provider returned an error |
All errors return JSON with an error field:
{`{
- "error": "rate_limited",
- "retry_after": 12,
- "limit": 60
-}`}
- {`# Install and start using in 30 seconds
-npm install -g @kernel.chat/kbot
-kbot auth
-kbot "analyze the code quality of this repo"`}
-
- {`const response = await fetch(
- 'https://eoxxpyixdieprsxlpwcs.supabase.co/functions/v1/kernel-api/chat',
- {
- method: 'POST',
- headers: {
- 'Authorization': 'Bearer kn_live_your_key_here',
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- message: 'Analyze the competitive landscape for AI coding assistants',
- agent: 'analyst',
- }),
- }
-)
-
-const data = await response.json()
-console.log(data.content) // The analyst's response
-console.log(data.usage) // { input_tokens, output_tokens, cost_usd }`}
-
- {`const response = await fetch(url, {
- method: 'POST',
- headers: { 'Authorization': 'Bearer kn_live_...', 'Content-Type': 'application/json' },
- body: JSON.stringify({ message: 'Write a poem', mode: 'stream' }),
-})
-
-const reader = response.body.getReader()
-const decoder = new TextDecoder()
-
-while (true) {
- const { done, value } = await reader.read()
- if (done) break
- process.stdout.write(decoder.decode(value))
-}`}
-
- {`const response = await fetch(url + '/swarm', {
- method: 'POST',
- headers: { 'Authorization': 'Bearer kn_live_...', 'Content-Type': 'application/json' },
- body: JSON.stringify({
- message: 'Design a real-time notification system',
- agents: ['architect', 'coder', 'guardian'],
- }),
-})
-
-const data = await response.json()
-console.log(data.synthesis) // Combined analysis
-data.contributions.forEach(c => // Individual perspectives
- console.log(c.agent + ':', c.content)
-)`}
- {error || 'Author not found'}
- Back to Explore -No published articles yet.
- ) : ( -- {feedMode === 'search' - ? 'No results found' - : feedMode === 'personalized' - ? 'Follow authors to see their posts here' - : 'No published content yet' - } -
-- {feedMode === 'search' - ? 'Try a different search term' - : 'Be the first to publish something' - } -
-- Every kbot session grows your companion. See who's climbed the ranks. -
- - {/* Summary */} - {!loading && !error && entries.length > 0 && ( -{error}
- -- {filter !== 'all' - ? `No ${filter} buddies on the leaderboard yet. Be the first!` - : 'The leaderboard is empty. Install kbot to get your buddy.'} -
-| # | -Buddy | -XP | -Achievements | -Sessions | -- {entry.rank <= 3 ? ['', '\uD83E\uDD47', '\uD83E\uDD48', '\uD83E\uDD49'][entry.rank] : entry.rank} - | -
-
-
- {SPECIES_EMOJI[entry.species as SpeciesKey] || '\uD83D\uDC3E'}
-
-
-
- {entry.species}
-
- L{entry.level} {getLevelTitle(entry.species, entry.level)}
-
-
- |
-
-
- {formatNumber(entry.xp)} XP
-
-
-
- |
- - - {'\uD83C\uDFC6'} - {entry.achievement_count} - - | -- {formatNumber(entry.sessions)} - | -
|---|
- Get your own buddy companion. It grows with every session. -
-npm i -g @kernel.chat/kbot
- {copied ? 'Copied!' : 'Copy'}
- {c.rule}
-Waiting for synthesis data...
-The daemon pushes every 15 minutes
-kbot's closed-loop intelligence compounding
-Injected into every kbot prompt — learned from failures
-- Bayesian ratings (Bradley-Terry) · - {proven} proven · - {developing} developing · - {untested} untested -
-- Evaluated against failure patterns · - {data.stats.toolsAdopted ?? 0} adopted, {data.stats.toolsRejected ?? 0} rejected -
-Academic techniques matched to kbot patterns
-What kbot knows — accumulated across {ls.sessions ?? 0} sessions
-- Chat 20 times a day. No credit card. No catch. -
-Everything you need to get started
-Got questions? Send us an email at hello@kernel.chat
-Kernel stores your conversations to provide continuity across sessions. It analyzes your messages to build a memory profile, knowledge graph, and multi-agent perception model - (see our Privacy Policy for details on the Convergence system). + (see our Privacy Policy for details on the Convergence system). This analysis is solely to improve your experience with Kernel.
@@ -317,7 +317,7 @@ export function TermsPage() {- These terms, together with the Privacy Policy, constitute the + These terms, together with the Privacy Policy, constitute the entire agreement between you and Isaac Hernandez regarding your use of Kernel. These terms supersede any prior agreements or understandings, whether written or oral, relating to the service. diff --git a/src/pages/WorkspaceAdminPage.tsx b/src/pages/WorkspaceAdminPage.tsx deleted file mode 100644 index 91084c982..000000000 --- a/src/pages/WorkspaceAdminPage.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { useState } from 'react' -import { useAuthContext } from '../providers/AuthProvider' -import { useWorkspace } from '../hooks/useWorkspace' -import { IconTrash, IconPlus, IconCheck } from '../components/KernelIcons' - -export function WorkspaceAdminPage() { - const { user } = useAuthContext() - const workspace = useWorkspace(user?.id ?? null) - const [inviteEmail, setInviteEmail] = useState('') - const [inviteRole, setInviteRole] = useState('member') - const [inviting, setInviting] = useState(false) - const [inviteSuccess, setInviteSuccess] = useState(false) - - if (!workspace.activeWorkspace) { - return ( -
Select a workspace from the switcher to manage it.
-- {workspace.members.length} / {workspace.activeWorkspace.max_members} members -
- - {/* Members list */} -- Manage your team subscription and payment method. -
- -{message}
{error?.stack || JSON.stringify(error, null, 2)}
-