feat(canvas): warm-paper theme + Tailwind v4 migration
Brings the canvas onto the warm-paper design system already shipped to landing, marketplace, and SaaS surfaces, and migrates the build from Tailwind v3 → v4 to match molecule-app. Plumbing: - swap tailwindcss v3 → v4, drop autoprefixer, add @tailwindcss/postcss - delete tailwind.config.ts (v4 reads tokens from @theme blocks in CSS) - globals.css: @import "tailwindcss" + @plugin "@tailwindcss/typography" - two @theme blocks: warm-paper light defaults + always-dark surface tokens (bg-bg / ink-mute / line-strong) for terminal/console panels - [data-theme="dark"] cascade overrides the warm-paper tokens for dark - React Flow edge stroke + scrollbar + selection colour pull from semantic tokens so they flip with the theme Theme infra (ported from molecule-app, identical contracts): - lib/theme-cookie.ts: mol_theme cookie + boot script (no "use client" so server components can read the constants) - lib/theme-provider.tsx: ThemeProvider + useTheme + cookie writer with Domain=.moleculesai.app so the preference follows the user across canvas/app/market/landing subdomains AND tenant subdomains - lib/theme.ts: ColorToken union + cssVar() helper - components/ThemeToggle.tsx: 3-way System/Light/Dark picker - layout.tsx: SSR cookie read + nonce'd inline boot script (CSP needs the explicit nonce — strict-dynamic doesn't forgive an un-nonce'd inline sibling) + ThemeProvider wrapper + bg-surface/text-ink body Component migration (62 files): - Mechanical bg-zinc-* / text-zinc-* / border-zinc-* / text-white → semantic surface/ink/line tokens via perl negative-lookahead pass (preserves opacity modifiers like /80, /60) - bg-blue-500/600 → bg-accent / bg-accent-strong - text-red-* / amber-* / emerald-* → text-bad / warm / good - Tinted-state banner backgrounds (bg-red-950, bg-amber-950, bg-blue-950 etc.) intentionally left literal — they remain readable on warm-paper in light mode without inventing new state-soft tokens - TerminalTab.tsx skipped — xterm renders to canvas, not DOM - 3 unit-test assertions updated to match new token strings (credits pillTone, AuthGate overlay class, A2AEdge accent) Verification: - pnpm test: 1214/1214 pass - pnpm tsc --noEmit: clean - next build: ✓ Compiled successfully (8 routes) - dev server inspection: html data-theme stamped, body uses bg-surface text-ink, boot script carries nonce, compiled CSS contains both @theme blocks + [data-theme="dark"] override Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d58185b8a8
commit
c0eca8d0e1
1292
canvas/package-lock.json
generated
1292
canvas/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -37,10 +37,10 @@
|
|||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"@vitest/coverage-v8": "^4.1.5",
|
"@vitest/coverage-v8": "^4.1.5",
|
||||||
"autoprefixer": "^10.4.0",
|
"@tailwindcss/postcss": "^4.0.0",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
"postcss": "^8.5.13",
|
"postcss": "^8.5.13",
|
||||||
"tailwindcss": "^3.4.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
"vitest": "^4.1.2"
|
"vitest": "^4.1.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
"@tailwindcss/postcss": {},
|
||||||
autoprefixer: {},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,28 +1,130 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Load order:
|
||||||
|
* 1. Tailwind core (v4) — provides preflight + utility generation.
|
||||||
|
* 2. xterm — overrides preflight on its own .xterm-* class names; must
|
||||||
|
* load AFTER tailwind so its specificity wins.
|
||||||
|
* 3. theme-tokens.css — canvas-only motion + deploy animation vars
|
||||||
|
* (--mol-duration-*, --mol-easing-*, --mol-deploy-*). NOT colour
|
||||||
|
* tokens; the warm-paper @theme block below owns those.
|
||||||
|
* 4. settings-panel.css / org-deploy.css — feature stylesheets that
|
||||||
|
* reference the variables above.
|
||||||
|
*/
|
||||||
@import "xterm/css/xterm.css";
|
@import "xterm/css/xterm.css";
|
||||||
/* Theme tokens MUST load before any feature stylesheet that
|
|
||||||
references them so custom properties are in scope. */
|
|
||||||
@import "../styles/theme-tokens.css";
|
@import "../styles/theme-tokens.css";
|
||||||
@import "../styles/settings-panel.css";
|
@import "../styles/settings-panel.css";
|
||||||
@import "../styles/org-deploy.css";
|
@import "../styles/org-deploy.css";
|
||||||
|
|
||||||
@tailwind base;
|
/*
|
||||||
@tailwind components;
|
* Warm-paper semantic tokens — light defaults via @theme, dark
|
||||||
@tailwind utilities;
|
* overrides via [data-theme="dark"]. Names are role-based
|
||||||
|
* (`bg-surface`, `text-ink`, `border-line`) not colour-based, so the
|
||||||
|
* same component classes work in either mode.
|
||||||
|
*
|
||||||
|
* Source of truth: molecule-app/app/globals.css. Keep aligned across
|
||||||
|
* surfaces (landing, market, app, canvas) so a token tweak ripples
|
||||||
|
* everywhere via a single PR per repo.
|
||||||
|
*
|
||||||
|
* Theme preference is persisted in the `mol_theme` cookie scoped to
|
||||||
|
* Domain=.moleculesai.app so the choice follows the user across
|
||||||
|
* subdomains. The inline boot script in app/layout.tsx applies it
|
||||||
|
* before paint to eliminate flash.
|
||||||
|
*/
|
||||||
|
@theme {
|
||||||
|
/* Surface — page / elevated card / sunken input / deep card */
|
||||||
|
--color-surface: #fafaf7;
|
||||||
|
--color-surface-elevated: #ffffff;
|
||||||
|
--color-surface-sunken: #f3f1ec;
|
||||||
|
--color-surface-card: #efece4;
|
||||||
|
|
||||||
|
/* Borders */
|
||||||
|
--color-line: #e6e2d8;
|
||||||
|
--color-line-soft: #efece4;
|
||||||
|
|
||||||
|
/* Text */
|
||||||
|
--color-ink: #15181c;
|
||||||
|
--color-ink-mid: #5a5e66;
|
||||||
|
--color-ink-soft: #8b8e95;
|
||||||
|
|
||||||
|
/* Brand + state */
|
||||||
|
--color-accent: #3b5bdb;
|
||||||
|
--color-accent-strong: #1a2f99;
|
||||||
|
--color-warm: #c0532b;
|
||||||
|
--color-good: #2f7a4d;
|
||||||
|
--color-bad: #b94e4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--color-surface: #0e1014;
|
||||||
|
--color-surface-elevated: #15181c;
|
||||||
|
--color-surface-sunken: #0a0b0e;
|
||||||
|
--color-surface-card: #1a1d23;
|
||||||
|
|
||||||
|
--color-line: #2a2f3a;
|
||||||
|
--color-line-soft: #1f2329;
|
||||||
|
|
||||||
|
--color-ink: #f4f1e9;
|
||||||
|
--color-ink-mid: #c8c2b4;
|
||||||
|
--color-ink-soft: #8d92a0;
|
||||||
|
|
||||||
|
/* Accents brighten slightly for AA contrast on dark backgrounds. */
|
||||||
|
--color-accent: #6883e8;
|
||||||
|
--color-accent-strong: #8aa1ee;
|
||||||
|
--color-warm: #d96f48;
|
||||||
|
--color-good: #4ca06e;
|
||||||
|
--color-bad: #d27773;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Always-dark surface tokens. Terminals (xterm), the console modal,
|
||||||
|
* and log streams stay dark in both modes — readable green-on-black
|
||||||
|
* code surfaces don't translate cleanly to a light theme. Components
|
||||||
|
* that should not light-flip use `bg-bg`, `bg-bg-elev`, `bg-bg-card`,
|
||||||
|
* `text-ink-mute`, `text-ink-dim`, `border-line-strong` instead of
|
||||||
|
* the warm-paper utilities above.
|
||||||
|
*
|
||||||
|
* Distinct names (bg-* / ink-mute / ink-dim / line-strong) so they
|
||||||
|
* don't collide with the warm-paper namespace (surface / ink /
|
||||||
|
* line). Both palettes coexist; the choice between them is per
|
||||||
|
* component, not per theme.
|
||||||
|
*/
|
||||||
|
@theme {
|
||||||
|
--color-bg: rgb(9 9 11); /* zinc-950 */
|
||||||
|
--color-bg-elev: rgb(24 24 27); /* zinc-900 */
|
||||||
|
--color-bg-card: rgb(39 39 42); /* zinc-800 */
|
||||||
|
--color-line-strong: rgb(63 63 70); /* zinc-700 */
|
||||||
|
--color-ink-mute: rgb(161 161 170); /* zinc-400 */
|
||||||
|
--color-ink-dim: rgb(113 113 122); /* zinc-500 */
|
||||||
|
--color-accent-dim: rgb(96 165 250);/* blue-400 */
|
||||||
|
--color-plasma: rgb(59 130 246); /* blue-500 */
|
||||||
|
--color-warn: rgb(251 191 36); /* amber-400 */
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #09090b;
|
background-color: var(--color-surface);
|
||||||
color: #e4e4e7;
|
color: var(--color-ink);
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* React Flow overrides for dark theme */
|
/* React Flow overrides for both themes. Edge stroke pulls from the
|
||||||
|
semantic line token so dark mode keeps its existing zinc-700 look
|
||||||
|
and light mode picks up the warm-paper line colour. */
|
||||||
.react-flow__edge-path {
|
.react-flow__edge-path {
|
||||||
stroke: #3f3f46 !important;
|
stroke: var(--color-line) !important;
|
||||||
stroke-width: 1.5 !important;
|
stroke-width: 1.5 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +160,8 @@ body {
|
|||||||
transition: box-shadow var(--mol-duration-fast) ease;
|
transition: box-shadow var(--mol-duration-fast) ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scrollbar styling */
|
/* Scrollbar styling. Track + thumb pull from the surface tokens so
|
||||||
|
they feel native to either theme. */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
@ -69,17 +172,17 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #3f3f46;
|
background: var(--color-line);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #52525b;
|
background: var(--color-line-strong, var(--color-ink-soft));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Selection */
|
/* Selection */
|
||||||
::selection {
|
::selection {
|
||||||
background: rgba(59, 130, 246, 0.3);
|
background: color-mix(in srgb, var(--color-accent) 30%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Panel slide animation */
|
/* Panel slide animation */
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { headers } from "next/headers";
|
import { cookies, headers } from "next/headers";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { AuthGate } from "@/components/AuthGate";
|
import { AuthGate } from "@/components/AuthGate";
|
||||||
import { CookieConsent } from "@/components/CookieConsent";
|
import { CookieConsent } from "@/components/CookieConsent";
|
||||||
|
import { ThemeProvider } from "@/lib/theme-provider";
|
||||||
|
import {
|
||||||
|
THEME_COOKIE,
|
||||||
|
readThemeCookie,
|
||||||
|
themeBootScript,
|
||||||
|
} from "@/lib/theme-cookie";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Molecule AI",
|
title: "Molecule AI",
|
||||||
@ -15,7 +21,7 @@ export default async function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
// Read the per-request CSP nonce that middleware.ts sets via the
|
// Read the per-request CSP nonce that middleware.ts sets via the
|
||||||
// `x-nonce` request header. This call is load-bearing for TWO
|
// `x-nonce` request header. This call is load-bearing for THREE
|
||||||
// independent reasons:
|
// independent reasons:
|
||||||
//
|
//
|
||||||
// 1. It opts the root layout into dynamic rendering. Without a
|
// 1. It opts the root layout into dynamic rendering. Without a
|
||||||
@ -31,22 +37,56 @@ export default async function RootLayout({
|
|||||||
// is actually read via `headers()`. The header's existence on
|
// is actually read via `headers()`. The header's existence on
|
||||||
// the request isn't enough — Next.js watches for the read.
|
// the request isn't enough — Next.js watches for the read.
|
||||||
//
|
//
|
||||||
// Keeping the `nonce` variable unused is intentional: we don't need
|
// 3. We need the nonce to attach to the inline theme boot script
|
||||||
// to pass it to any custom <Script nonce={...}> tags right now, the
|
// below, otherwise CSP rejects it in production where
|
||||||
// framework takes care of its own bootstrap scripts once the read
|
// script-src is `'self' 'nonce-{nonce}' 'strict-dynamic'`.
|
||||||
// happens. Destructuring via `await` + `.get()` is the minimum shape
|
// 'strict-dynamic' propagates trust from a nonce'd script to
|
||||||
// Next.js recognizes as "dynamic server-side access".
|
// scripts it inserts, but does NOT forgive an un-nonce'd
|
||||||
await headers();
|
// sibling — the boot script must carry its own nonce.
|
||||||
|
const hdrs = await headers();
|
||||||
|
const nonce = hdrs.get("x-nonce") ?? undefined;
|
||||||
|
|
||||||
|
// SSR: read the user's saved preference. For light/dark we can stamp
|
||||||
|
// data-theme on <html> here so the very first paint matches; for
|
||||||
|
// "system" we leave the attribute off and let the inline boot script
|
||||||
|
// resolve from matchMedia before paint.
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const theme = readThemeCookie(cookieStore.get(THEME_COOKIE)?.value);
|
||||||
|
const initialDataTheme = theme === "system" ? undefined : theme;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
// suppressHydrationWarning on <html>: the inline boot script below
|
||||||
<body className="bg-zinc-950 text-white">
|
// mutates `data-theme` before React hydrates (system mode reads
|
||||||
|
// matchMedia + writes the attribute). That's the entire point of the
|
||||||
|
// script — eliminate the flash — and it's the documented escape hatch
|
||||||
|
// for "the server-rendered HTML is intentionally not what React would
|
||||||
|
// produce client-side at this exact attribute."
|
||||||
|
<html lang="en" data-theme={initialDataTheme} suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
{/*
|
||||||
|
* Boot script: runs synchronously before the body paints, sets
|
||||||
|
* data-theme on <html> for "system" preference based on the OS
|
||||||
|
* media query. For explicit light/dark, SSR already set the
|
||||||
|
* attribute above and the script's write is a no-op.
|
||||||
|
*
|
||||||
|
* `nonce` comes from middleware's per-request CSP nonce — see
|
||||||
|
* the comment block above for why CSP requires this even though
|
||||||
|
* the page also has 'strict-dynamic'.
|
||||||
|
*/}
|
||||||
|
<script
|
||||||
|
nonce={nonce}
|
||||||
|
dangerouslySetInnerHTML={{ __html: themeBootScript }}
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body className="bg-surface text-ink">
|
||||||
|
<ThemeProvider initialTheme={theme}>
|
||||||
{/* AuthGate is a client component; it checks the session on mount
|
{/* AuthGate is a client component; it checks the session on mount
|
||||||
and bounces anonymous users to the control plane's login page
|
and bounces anonymous users to the control plane's login page
|
||||||
when running on a tenant subdomain. Non-SaaS hosts (localhost,
|
when running on a tenant subdomain. Non-SaaS hosts (localhost,
|
||||||
vercel preview URL, apex) pass through unchanged. */}
|
vercel preview URL, apex) pass through unchanged. */}
|
||||||
<AuthGate>{children}</AuthGate>
|
<AuthGate>{children}</AuthGate>
|
||||||
<CookieConsent />
|
<CookieConsent />
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -110,15 +110,15 @@ export default function OrgsPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (session === "loading" || (orgs === null && error === null)) {
|
if (session === "loading" || (orgs === null && error === null)) {
|
||||||
return <Shell><p className="text-zinc-400">Loading…</p></Shell>;
|
return <Shell><p className="text-ink-mid">Loading…</p></Shell>;
|
||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Shell>
|
<Shell>
|
||||||
<p role="alert" className="text-red-400">Error: {error}</p>
|
<p role="alert" className="text-bad">Error: {error}</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => window.location.reload()}
|
onClick={() => window.location.reload()}
|
||||||
className="mt-4 rounded bg-zinc-800 px-4 py-2 text-sm text-zinc-200 hover:bg-zinc-700"
|
className="mt-4 rounded bg-surface-card px-4 py-2 text-sm text-ink hover:bg-surface-card"
|
||||||
>
|
>
|
||||||
Retry
|
Retry
|
||||||
</button>
|
</button>
|
||||||
@ -136,7 +136,7 @@ export default function OrgsPage() {
|
|||||||
<OrgRow key={o.id} org={o} />
|
<OrgRow key={o.id} org={o} />
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<div className="mt-8 border-t border-zinc-800 pt-6">
|
<div className="mt-8 border-t border-line pt-6">
|
||||||
<CreateOrgForm
|
<CreateOrgForm
|
||||||
onCreated={(slug) => {
|
onCreated={(slug) => {
|
||||||
// Refresh the list so the new org appears + its CTA fires.
|
// Refresh the list so the new org appears + its CTA fires.
|
||||||
@ -162,11 +162,11 @@ function CheckoutBanner() {
|
|||||||
|
|
||||||
function Shell({ children }: { children: React.ReactNode }) {
|
function Shell({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-zinc-950 text-zinc-100">
|
<main className="min-h-screen bg-surface text-ink">
|
||||||
<TermsGate>
|
<TermsGate>
|
||||||
<div className="mx-auto max-w-2xl px-6 pt-20 pb-12">
|
<div className="mx-auto max-w-2xl px-6 pt-20 pb-12">
|
||||||
<h1 className="text-3xl font-bold text-white">Your organizations</h1>
|
<h1 className="text-3xl font-bold text-ink">Your organizations</h1>
|
||||||
<p className="mt-2 text-zinc-400">
|
<p className="mt-2 text-ink-mid">
|
||||||
Each org is an isolated Molecule workspace.
|
Each org is an isolated Molecule workspace.
|
||||||
</p>
|
</p>
|
||||||
<DataResidencyNotice />
|
<DataResidencyNotice />
|
||||||
@ -184,7 +184,7 @@ function Shell({ children }: { children: React.ReactNode }) {
|
|||||||
// region dropdown.
|
// region dropdown.
|
||||||
function DataResidencyNotice() {
|
function DataResidencyNotice() {
|
||||||
return (
|
return (
|
||||||
<p className="mt-3 rounded border border-zinc-800 bg-zinc-900/60 px-3 py-2 text-xs text-zinc-400">
|
<p className="mt-3 rounded border border-line bg-surface-sunken/60 px-3 py-2 text-xs text-ink-mid">
|
||||||
Workspaces run in AWS us-east-2 (Ohio, United States). EU region support is on the roadmap — reach out to
|
Workspaces run in AWS us-east-2 (Ohio, United States). EU region support is on the roadmap — reach out to
|
||||||
{" "}
|
{" "}
|
||||||
<a href="mailto:support@moleculesai.app" className="underline">
|
<a href="mailto:support@moleculesai.app" className="underline">
|
||||||
@ -197,11 +197,11 @@ function DataResidencyNotice() {
|
|||||||
|
|
||||||
function OrgRow({ org }: { org: Org }) {
|
function OrgRow({ org }: { org: Org }) {
|
||||||
return (
|
return (
|
||||||
<li className="rounded-lg border border-zinc-800 bg-zinc-900 p-4">
|
<li className="rounded-lg border border-line bg-surface-sunken p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-white">{org.name}</div>
|
<div className="font-medium text-ink">{org.name}</div>
|
||||||
<div className="text-sm text-zinc-400">
|
<div className="text-sm text-ink-mid">
|
||||||
{org.slug} · <StatusLabel status={org.status} /> · {org.plan || "free"}
|
{org.slug} · <StatusLabel status={org.status} /> · {org.plan || "free"}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<div className="mt-2 flex items-center gap-2">
|
||||||
@ -237,21 +237,21 @@ function LowCreditsBanner({ org }: { org: Org }) {
|
|||||||
if (kind === "overage") {
|
if (kind === "overage") {
|
||||||
const used = (org.overage_used_credits ?? 0).toLocaleString();
|
const used = (org.overage_used_credits ?? 0).toLocaleString();
|
||||||
return (
|
return (
|
||||||
<span className="text-xs text-amber-300">
|
<span className="text-xs text-warm">
|
||||||
overage active · {used} used
|
overage active · {used} used
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (kind === "out-of-credits") {
|
if (kind === "out-of-credits") {
|
||||||
return (
|
return (
|
||||||
<a href={`/pricing?org=${encodeURIComponent(org.slug)}`} className="text-xs text-red-300 underline">
|
<a href={`/pricing?org=${encodeURIComponent(org.slug)}`} className="text-xs text-bad underline">
|
||||||
out of credits — upgrade to keep running
|
out of credits — upgrade to keep running
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// trial-tail
|
// trial-tail
|
||||||
return (
|
return (
|
||||||
<a href={`/pricing?org=${encodeURIComponent(org.slug)}`} className="text-xs text-amber-300 underline">
|
<a href={`/pricing?org=${encodeURIComponent(org.slug)}`} className="text-xs text-warm underline">
|
||||||
trial almost out
|
trial almost out
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
@ -260,11 +260,11 @@ function LowCreditsBanner({ org }: { org: Org }) {
|
|||||||
function StatusLabel({ status }: { status: OrgStatus }) {
|
function StatusLabel({ status }: { status: OrgStatus }) {
|
||||||
const cls =
|
const cls =
|
||||||
status === "running"
|
status === "running"
|
||||||
? "text-emerald-400"
|
? "text-good"
|
||||||
: status === "awaiting_payment"
|
: status === "awaiting_payment"
|
||||||
? "text-amber-400"
|
? "text-warm"
|
||||||
: status === "failed"
|
: status === "failed"
|
||||||
? "text-red-400"
|
? "text-bad"
|
||||||
: "text-sky-400";
|
: "text-sky-400";
|
||||||
const label =
|
const label =
|
||||||
status === "awaiting_payment"
|
status === "awaiting_payment"
|
||||||
@ -283,7 +283,7 @@ function OrgCTA({ org }: { org: Org }) {
|
|||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
className="rounded bg-emerald-600 px-4 py-2 text-sm font-medium text-white hover:bg-emerald-500"
|
className="rounded bg-emerald-600 px-4 py-2 text-sm font-medium text-ink hover:bg-emerald-500"
|
||||||
>
|
>
|
||||||
Open
|
Open
|
||||||
</a>
|
</a>
|
||||||
@ -293,7 +293,7 @@ function OrgCTA({ org }: { org: Org }) {
|
|||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={`/pricing?org=${encodeURIComponent(org.slug)}`}
|
href={`/pricing?org=${encodeURIComponent(org.slug)}`}
|
||||||
className="rounded bg-amber-600 px-4 py-2 text-sm font-medium text-white hover:bg-amber-500"
|
className="rounded bg-amber-600 px-4 py-2 text-sm font-medium text-ink hover:bg-amber-500"
|
||||||
>
|
>
|
||||||
Complete payment
|
Complete payment
|
||||||
</a>
|
</a>
|
||||||
@ -303,21 +303,21 @@ function OrgCTA({ org }: { org: Org }) {
|
|||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href="mailto:support@moleculesai.app"
|
href="mailto:support@moleculesai.app"
|
||||||
className="rounded bg-zinc-700 px-4 py-2 text-sm font-medium text-zinc-200 hover:bg-zinc-600"
|
className="rounded bg-surface-card px-4 py-2 text-sm font-medium text-ink hover:bg-zinc-600"
|
||||||
>
|
>
|
||||||
Contact support
|
Contact support
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// provisioning / unknown — non-interactive
|
// provisioning / unknown — non-interactive
|
||||||
return <span className="text-sm text-zinc-500">{org.status}…</span>;
|
return <span className="text-sm text-ink-soft">{org.status}…</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyState({ banner }: { banner?: React.ReactNode }) {
|
function EmptyState({ banner }: { banner?: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<Shell>
|
<Shell>
|
||||||
{banner}
|
{banner}
|
||||||
<p className="text-zinc-300">
|
<p className="text-ink-mid">
|
||||||
You don't have any organizations yet. Create one to get started — your
|
You don't have any organizations yet. Create one to get started — your
|
||||||
workspace spins up automatically once billing is set up.
|
workspace spins up automatically once billing is set up.
|
||||||
</p>
|
</p>
|
||||||
@ -365,7 +365,7 @@ function CreateOrgForm({ onCreated }: { onCreated: (slug: string) => void }) {
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={submit} className="space-y-3">
|
<form onSubmit={submit} className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="org-slug" className="block text-sm text-zinc-300">Slug (URL)</label>
|
<label htmlFor="org-slug" className="block text-sm text-ink-mid">Slug (URL)</label>
|
||||||
<input
|
<input
|
||||||
id="org-slug"
|
id="org-slug"
|
||||||
value={slug}
|
value={slug}
|
||||||
@ -374,28 +374,28 @@ function CreateOrgForm({ onCreated }: { onCreated: (slug: string) => void }) {
|
|||||||
placeholder="acme"
|
placeholder="acme"
|
||||||
required
|
required
|
||||||
aria-describedby="org-slug-hint"
|
aria-describedby="org-slug-hint"
|
||||||
className="mt-1 w-full rounded border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100"
|
className="mt-1 w-full rounded border border-line bg-surface-card px-3 py-2 text-sm text-ink"
|
||||||
/>
|
/>
|
||||||
<p id="org-slug-hint" className="mt-1 text-xs text-zinc-500">
|
<p id="org-slug-hint" className="mt-1 text-xs text-ink-soft">
|
||||||
Lowercase letters, numbers, and hyphens only. Cannot be changed later.
|
Lowercase letters, numbers, and hyphens only. Cannot be changed later.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="org-name" className="block text-sm text-zinc-300">Display name</label>
|
<label htmlFor="org-name" className="block text-sm text-ink-mid">Display name</label>
|
||||||
<input
|
<input
|
||||||
id="org-name"
|
id="org-name"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
placeholder="Acme Corp"
|
placeholder="Acme Corp"
|
||||||
required
|
required
|
||||||
className="mt-1 w-full rounded border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100"
|
className="mt-1 w-full rounded border border-line bg-surface-card px-3 py-2 text-sm text-ink"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{err && <p role="alert" className="text-sm text-red-400">{err}</p>}
|
{err && <p role="alert" className="text-sm text-bad">{err}</p>}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500 disabled:opacity-50"
|
className="rounded bg-accent-strong px-4 py-2 text-sm font-medium text-ink hover:bg-accent disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{submitting ? "Creating…" : "Create organization"}
|
{submitting ? "Creating…" : "Create organization"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -53,10 +53,10 @@ export default function Home() {
|
|||||||
|
|
||||||
if (hydrating) {
|
if (hydrating) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex items-center justify-center bg-zinc-950">
|
<div className="fixed inset-0 flex items-center justify-center bg-surface">
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<Spinner size="lg" />
|
<Spinner size="lg" />
|
||||||
<span className="text-xs text-zinc-500">Loading canvas...</span>
|
<span className="text-xs text-ink-soft">Loading canvas...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -79,15 +79,15 @@ export default function Home() {
|
|||||||
// selector that's used by other transient toasts. Don't rename
|
// selector that's used by other transient toasts. Don't rename
|
||||||
// without updating that spec.
|
// without updating that spec.
|
||||||
data-testid="hydration-error"
|
data-testid="hydration-error"
|
||||||
className="fixed inset-0 flex flex-col items-center justify-center bg-zinc-950 text-zinc-300 gap-4 z-[9999]"
|
className="fixed inset-0 flex flex-col items-center justify-center bg-surface text-ink-mid gap-4 z-[9999]"
|
||||||
>
|
>
|
||||||
<p className="text-zinc-400 text-sm">{hydrationError}</p>
|
<p className="text-ink-mid text-sm">{hydrationError}</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setHydrationError(null);
|
setHydrationError(null);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-md text-sm"
|
className="px-4 py-2 bg-accent-strong hover:bg-accent text-ink rounded-md text-sm"
|
||||||
>
|
>
|
||||||
Retry
|
Retry
|
||||||
</button>
|
</button>
|
||||||
@ -108,28 +108,28 @@ function PlatformDownDiagnostic() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
className="fixed inset-0 flex flex-col items-center justify-center bg-zinc-950 text-zinc-300 gap-5 z-[9999] px-6"
|
className="fixed inset-0 flex flex-col items-center justify-center bg-surface text-ink-mid gap-5 z-[9999] px-6"
|
||||||
>
|
>
|
||||||
<div className="text-amber-400 text-sm font-semibold uppercase tracking-wider">
|
<div className="text-warm text-sm font-semibold uppercase tracking-wider">
|
||||||
Platform infrastructure unreachable
|
Platform infrastructure unreachable
|
||||||
</div>
|
</div>
|
||||||
<p className="text-zinc-400 text-sm max-w-lg text-center leading-relaxed">
|
<p className="text-ink-mid text-sm max-w-lg text-center leading-relaxed">
|
||||||
The platform server returned <code className="font-mono text-amber-300">503 platform_unavailable</code>.
|
The platform server returned <code className="font-mono text-warm">503 platform_unavailable</code>.
|
||||||
That means it can't reach Postgres or Redis to validate your session.
|
That means it can't reach Postgres or Redis to validate your session.
|
||||||
Most common cause on a dev host: one of those services stopped.
|
Most common cause on a dev host: one of those services stopped.
|
||||||
</p>
|
</p>
|
||||||
<div className="bg-zinc-900/80 border border-zinc-700/50 rounded-lg px-4 py-3 max-w-lg w-full">
|
<div className="bg-surface-sunken/80 border border-line/50 rounded-lg px-4 py-3 max-w-lg w-full">
|
||||||
<div className="text-[10px] uppercase tracking-wider text-zinc-500 mb-2">Try first</div>
|
<div className="text-[10px] uppercase tracking-wider text-ink-soft mb-2">Try first</div>
|
||||||
<pre className="text-[12px] text-zinc-300 font-mono whitespace-pre-wrap leading-relaxed">{`brew services start postgresql@14
|
<pre className="text-[12px] text-ink-mid font-mono whitespace-pre-wrap leading-relaxed">{`brew services start postgresql@14
|
||||||
brew services start redis`}</pre>
|
brew services start redis`}</pre>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[11px] text-zinc-500 max-w-lg text-center">
|
<p className="text-[11px] text-ink-soft max-w-lg text-center">
|
||||||
If both are running, check <code className="font-mono">/tmp/molecule-server.log</code> for
|
If both are running, check <code className="font-mono">/tmp/molecule-server.log</code> for
|
||||||
the underlying error. If you're on hosted SaaS, this is a platform incident — try again in a moment.
|
the underlying error. If you're on hosted SaaS, this is a platform incident — try again in a moment.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => window.location.reload()}
|
onClick={() => window.location.reload()}
|
||||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-md text-sm mt-2"
|
className="px-4 py-2 bg-accent-strong hover:bg-accent text-ink rounded-md text-sm mt-2"
|
||||||
>
|
>
|
||||||
Reload
|
Reload
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -19,17 +19,17 @@ export const metadata = {
|
|||||||
|
|
||||||
export default function PricingPage() {
|
export default function PricingPage() {
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-zinc-950 text-zinc-100">
|
<main className="min-h-screen bg-surface text-ink">
|
||||||
<div className="mx-auto max-w-5xl px-6 pt-20 pb-8 text-center">
|
<div className="mx-auto max-w-5xl px-6 pt-20 pb-8 text-center">
|
||||||
<h1 className="text-5xl font-bold tracking-tight text-white md:text-6xl">
|
<h1 className="text-5xl font-bold tracking-tight text-ink md:text-6xl">
|
||||||
Pricing
|
Pricing
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mx-auto mt-4 max-w-2xl text-lg text-zinc-300">
|
<p className="mx-auto mt-4 max-w-2xl text-lg text-ink-mid">
|
||||||
One flat price per org — not per seat. Every paid tier includes the
|
One flat price per org — not per seat. Every paid tier includes the
|
||||||
full runtime stack. You upgrade for scale, support, and dedicated
|
full runtime stack. You upgrade for scale, support, and dedicated
|
||||||
infrastructure.
|
infrastructure.
|
||||||
</p>
|
</p>
|
||||||
<p className="mx-auto mt-2 max-w-xl text-sm text-zinc-400">
|
<p className="mx-auto mt-2 max-w-xl text-sm text-ink-mid">
|
||||||
5-person team? You pay $29/month — not $200. No seat math, ever.
|
5-person team? You pay $29/month — not $200. No seat math, ever.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -37,42 +37,42 @@ export default function PricingPage() {
|
|||||||
<PricingTable />
|
<PricingTable />
|
||||||
|
|
||||||
<section className="mx-auto mt-20 max-w-3xl px-6 text-center">
|
<section className="mx-auto mt-20 max-w-3xl px-6 text-center">
|
||||||
<h2 className="text-2xl font-semibold text-white">Questions?</h2>
|
<h2 className="text-2xl font-semibold text-ink">Questions?</h2>
|
||||||
<p className="mt-2 text-zinc-400">
|
<p className="mt-2 text-ink-mid">
|
||||||
We publish the{" "}
|
We publish the{" "}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/Molecule-AI/molecule-monorepo"
|
href="https://github.com/Molecule-AI/molecule-monorepo"
|
||||||
className="text-blue-400 underline hover:text-blue-300"
|
className="text-accent underline hover:text-accent"
|
||||||
>
|
>
|
||||||
full source on GitHub
|
full source on GitHub
|
||||||
</a>
|
</a>
|
||||||
{" "}— if something's ambiguous, file an issue or{" "}
|
{" "}— if something's ambiguous, file an issue or{" "}
|
||||||
<a
|
<a
|
||||||
href="mailto:support@moleculesai.app"
|
href="mailto:support@moleculesai.app"
|
||||||
className="text-blue-400 underline hover:text-blue-300"
|
className="text-accent underline hover:text-accent"
|
||||||
>
|
>
|
||||||
email support
|
email support
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-6 text-sm text-zinc-500">
|
<p className="mt-6 text-sm text-ink-soft">
|
||||||
Prices shown in USD. Flat-rate per org — no per-seat fees on any paid tier.
|
Prices shown in USD. Flat-rate per org — no per-seat fees on any paid tier.
|
||||||
Enterprise / self-hosted licensing available — contact us.
|
Enterprise / self-hosted licensing available — contact us.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer className="mx-auto mt-20 max-w-5xl border-t border-zinc-800 px-6 py-6 text-center text-sm text-zinc-500">
|
<footer className="mx-auto mt-20 max-w-5xl border-t border-line px-6 py-6 text-center text-sm text-ink-soft">
|
||||||
<p>
|
<p>
|
||||||
© {new Date().getFullYear()} Molecule AI, Inc. ·{" "}
|
© {new Date().getFullYear()} Molecule AI, Inc. ·{" "}
|
||||||
<a href="/legal/terms" className="hover:text-zinc-300">
|
<a href="/legal/terms" className="hover:text-ink-mid">
|
||||||
Terms
|
Terms
|
||||||
</a>
|
</a>
|
||||||
{" "}·{" "}
|
{" "}·{" "}
|
||||||
<a href="/legal/privacy" className="hover:text-zinc-300">
|
<a href="/legal/privacy" className="hover:text-ink-mid">
|
||||||
Privacy
|
Privacy
|
||||||
</a>
|
</a>
|
||||||
{" "}·{" "}
|
{" "}·{" "}
|
||||||
<a href="/legal/dpa" className="hover:text-zinc-300">
|
<a href="/legal/dpa" className="hover:text-ink-mid">
|
||||||
DPA
|
DPA
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -61,26 +61,26 @@ export function ApprovalBanner() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="w-8 h-8 rounded-lg bg-amber-800/40 flex items-center justify-center shrink-0 mt-0.5">
|
<div className="w-8 h-8 rounded-lg bg-amber-800/40 flex items-center justify-center shrink-0 mt-0.5">
|
||||||
<span className="text-amber-300 text-lg" aria-hidden="true">⚠</span>
|
<span className="text-warm text-lg" aria-hidden="true">⚠</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-xs text-amber-200 font-semibold">{approval.workspace_name} needs approval</div>
|
<div className="text-xs text-amber-200 font-semibold">{approval.workspace_name} needs approval</div>
|
||||||
<div className="text-sm text-amber-100 mt-0.5 font-medium">{approval.action}</div>
|
<div className="text-sm text-amber-100 mt-0.5 font-medium">{approval.action}</div>
|
||||||
{approval.reason && (
|
{approval.reason && (
|
||||||
<div className="text-xs text-amber-300/70 mt-1">{approval.reason}</div>
|
<div className="text-xs text-warm/70 mt-1">{approval.reason}</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex gap-2 mt-3">
|
<div className="flex gap-2 mt-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleDecide(approval, "approved")}
|
onClick={() => handleDecide(approval, "approved")}
|
||||||
className="px-3 py-1.5 bg-emerald-600 hover:bg-emerald-500 text-xs rounded-lg text-white font-medium transition-colors"
|
className="px-3 py-1.5 bg-emerald-600 hover:bg-emerald-500 text-xs rounded-lg text-ink font-medium transition-colors"
|
||||||
>
|
>
|
||||||
Approve
|
Approve
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleDecide(approval, "denied")}
|
onClick={() => handleDecide(approval, "denied")}
|
||||||
className="px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-xs rounded-lg text-zinc-300 transition-colors"
|
className="px-3 py-1.5 bg-surface-card hover:bg-zinc-600 text-xs rounded-lg text-ink-mid transition-colors"
|
||||||
>
|
>
|
||||||
Deny
|
Deny
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import type { AuditEntry, AuditResponse } from "@/types/audit";
|
|||||||
type EventFilter = "all" | AuditEntry["event_type"];
|
type EventFilter = "all" | AuditEntry["event_type"];
|
||||||
|
|
||||||
const BADGE_COLORS: Record<AuditEntry["event_type"], { text: string; bg: string; border: string }> = {
|
const BADGE_COLORS: Record<AuditEntry["event_type"], { text: string; bg: string; border: string }> = {
|
||||||
delegation: { text: "text-blue-400", bg: "bg-blue-950/40", border: "border-blue-800/40" },
|
delegation: { text: "text-accent", bg: "bg-blue-950/40", border: "border-blue-800/40" },
|
||||||
decision: { text: "text-violet-400", bg: "bg-violet-950/40", border: "border-violet-800/40" },
|
decision: { text: "text-violet-400", bg: "bg-violet-950/40", border: "border-violet-800/40" },
|
||||||
gate: { text: "text-yellow-400", bg: "bg-yellow-950/40", border: "border-yellow-800/40" },
|
gate: { text: "text-yellow-400", bg: "bg-yellow-950/40", border: "border-yellow-800/40" },
|
||||||
hitl: { text: "text-orange-400", bg: "bg-orange-950/40", border: "border-orange-800/40" },
|
hitl: { text: "text-orange-400", bg: "bg-orange-950/40", border: "border-orange-800/40" },
|
||||||
@ -127,7 +127,7 @@ export function AuditTrailPanel({ workspaceId }: Props) {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-32">
|
<div className="flex items-center justify-center h-32">
|
||||||
<span className="text-xs text-zinc-500">Loading audit trail…</span>
|
<span className="text-xs text-ink-soft">Loading audit trail…</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -135,7 +135,7 @@ export function AuditTrailPanel({ workspaceId }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Filter bar */}
|
{/* Filter bar */}
|
||||||
<div className="px-4 py-2.5 border-b border-zinc-800/40 flex items-center gap-1 overflow-x-auto shrink-0">
|
<div className="px-4 py-2.5 border-b border-line/40 flex items-center gap-1 overflow-x-auto shrink-0">
|
||||||
{FILTERS.map((f) => (
|
{FILTERS.map((f) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -144,8 +144,8 @@ export function AuditTrailPanel({ workspaceId }: Props) {
|
|||||||
aria-pressed={filter === f.id}
|
aria-pressed={filter === f.id}
|
||||||
className={`px-2 py-1 text-[10px] rounded-md font-medium transition-all shrink-0 ${
|
className={`px-2 py-1 text-[10px] rounded-md font-medium transition-all shrink-0 ${
|
||||||
filter === f.id
|
filter === f.id
|
||||||
? "bg-zinc-700 text-zinc-100 ring-1 ring-zinc-600"
|
? "bg-surface-card text-ink ring-1 ring-zinc-600"
|
||||||
: "text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800/60"
|
: "text-ink-soft hover:text-ink-mid hover:bg-surface-card/60"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{f.label}
|
{f.label}
|
||||||
@ -155,7 +155,7 @@ export function AuditTrailPanel({ workspaceId }: Props) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={loadEntries}
|
onClick={loadEntries}
|
||||||
className="px-2 py-1 text-[10px] bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded transition-colors shrink-0"
|
className="px-2 py-1 text-[10px] bg-surface-card hover:bg-surface-card text-ink-mid rounded transition-colors shrink-0"
|
||||||
aria-label="Refresh audit trail"
|
aria-label="Refresh audit trail"
|
||||||
>
|
>
|
||||||
↻
|
↻
|
||||||
@ -164,7 +164,7 @@ export function AuditTrailPanel({ workspaceId }: Props) {
|
|||||||
|
|
||||||
{/* Error banner */}
|
{/* Error banner */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mx-4 mt-3 px-3 py-2 bg-red-950/30 border border-red-800/40 rounded text-xs text-red-400 shrink-0">
|
<div className="mx-4 mt-3 px-3 py-2 bg-red-950/30 border border-red-800/40 rounded text-xs text-bad shrink-0">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -175,8 +175,8 @@ export function AuditTrailPanel({ workspaceId }: Props) {
|
|||||||
/* Empty state */
|
/* Empty state */
|
||||||
<div className="flex flex-col items-center justify-center py-16 gap-3 text-center">
|
<div className="flex flex-col items-center justify-center py-16 gap-3 text-center">
|
||||||
<span className="text-4xl text-zinc-700" aria-hidden="true">⊟</span>
|
<span className="text-4xl text-zinc-700" aria-hidden="true">⊟</span>
|
||||||
<p className="text-sm font-medium text-zinc-400">No audit events yet</p>
|
<p className="text-sm font-medium text-ink-mid">No audit events yet</p>
|
||||||
<p className="text-[11px] text-zinc-600 max-w-[200px] leading-relaxed">
|
<p className="text-[11px] text-ink-soft max-w-[200px] leading-relaxed">
|
||||||
Delegation, decision, gate, and human-in-the-loop events will appear here.
|
Delegation, decision, gate, and human-in-the-loop events will appear here.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -195,7 +195,7 @@ export function AuditTrailPanel({ workspaceId }: Props) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={loadMore}
|
onClick={loadMore}
|
||||||
disabled={loadingMore}
|
disabled={loadingMore}
|
||||||
className="px-4 py-2 text-[11px] bg-zinc-800 hover:bg-zinc-700 disabled:opacity-50 disabled:cursor-not-allowed text-zinc-300 rounded-lg transition-colors"
|
className="px-4 py-2 text-[11px] bg-surface-card hover:bg-surface-card disabled:opacity-50 disabled:cursor-not-allowed text-ink-mid rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
{loadingMore ? "Loading…" : "Load more"}
|
{loadingMore ? "Loading…" : "Load more"}
|
||||||
</button>
|
</button>
|
||||||
@ -203,7 +203,7 @@ export function AuditTrailPanel({ workspaceId }: Props) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Entry count footer */}
|
{/* Entry count footer */}
|
||||||
<p className="mt-3 text-center text-[9px] text-zinc-600">
|
<p className="mt-3 text-center text-[9px] text-ink-soft">
|
||||||
{entries.length} event{entries.length !== 1 ? "s" : ""} loaded
|
{entries.length} event{entries.length !== 1 ? "s" : ""} loaded
|
||||||
{cursor ? " · more available" : " · all loaded"}
|
{cursor ? " · more available" : " · all loaded"}
|
||||||
</p>
|
</p>
|
||||||
@ -227,15 +227,15 @@ export interface AuditEntryRowProps {
|
|||||||
*/
|
*/
|
||||||
export function AuditEntryRow({ entry, now }: AuditEntryRowProps) {
|
export function AuditEntryRow({ entry, now }: AuditEntryRowProps) {
|
||||||
const badge = BADGE_COLORS[entry.event_type] ?? {
|
const badge = BADGE_COLORS[entry.event_type] ?? {
|
||||||
text: "text-zinc-400",
|
text: "text-ink-mid",
|
||||||
bg: "bg-zinc-800/40",
|
bg: "bg-surface-card/40",
|
||||||
border: "border-zinc-700/40",
|
border: "border-line/40",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="listitem"
|
role="listitem"
|
||||||
className="rounded-lg border border-zinc-800/60 bg-zinc-900/50 px-3 py-2.5 space-y-1.5"
|
className="rounded-lg border border-line/60 bg-surface-sunken/50 px-3 py-2.5 space-y-1.5"
|
||||||
>
|
>
|
||||||
{/* Header row: badge · actor · tamper flag · timestamp */}
|
{/* Header row: badge · actor · tamper flag · timestamp */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -248,14 +248,14 @@ export function AuditEntryRow({ entry, now }: AuditEntryRowProps) {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Actor name */}
|
{/* Actor name */}
|
||||||
<span className="text-[10px] text-zinc-400 truncate flex-1 min-w-0 font-mono">
|
<span className="text-[10px] text-ink-mid truncate flex-1 min-w-0 font-mono">
|
||||||
{entry.actor}
|
{entry.actor}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Tamper warning — only rendered when chain is invalid */}
|
{/* Tamper warning — only rendered when chain is invalid */}
|
||||||
{!entry.chain_valid && (
|
{!entry.chain_valid && (
|
||||||
<span
|
<span
|
||||||
className="shrink-0 text-[11px] text-red-400 font-bold leading-none"
|
className="shrink-0 text-[11px] text-bad font-bold leading-none"
|
||||||
title="Chain integrity check failed — this entry may have been tampered with"
|
title="Chain integrity check failed — this entry may have been tampered with"
|
||||||
aria-label="Chain integrity warning: tampered entry"
|
aria-label="Chain integrity warning: tampered entry"
|
||||||
role="img"
|
role="img"
|
||||||
@ -265,13 +265,13 @@ export function AuditEntryRow({ entry, now }: AuditEntryRowProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Relative timestamp */}
|
{/* Relative timestamp */}
|
||||||
<span className="shrink-0 text-[9px] text-zinc-600">
|
<span className="shrink-0 text-[9px] text-ink-soft">
|
||||||
{formatAuditRelativeTime(entry.created_at, now)}
|
{formatAuditRelativeTime(entry.created_at, now)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary text */}
|
{/* Summary text */}
|
||||||
<p className="text-[11px] text-zinc-300 leading-relaxed break-words">
|
<p className="text-[11px] text-ink-mid leading-relaxed break-words">
|
||||||
{entry.summary}
|
{entry.summary}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -63,7 +63,7 @@ export function AuthGate({ children }: { children: ReactNode }) {
|
|||||||
if (state.kind === "loading") {
|
if (state.kind === "loading") {
|
||||||
// Zinc-950 backdrop matches the canvas background so the browser
|
// Zinc-950 backdrop matches the canvas background so the browser
|
||||||
// never paints a white flash while the session round-trip resolves.
|
// never paints a white flash while the session round-trip resolves.
|
||||||
return <div className="fixed inset-0 bg-zinc-950" aria-hidden="true" />;
|
return <div className="fixed inset-0 bg-surface" aria-hidden="true" />;
|
||||||
}
|
}
|
||||||
if (state.kind === "anonymous" && !state.skipRedirect) {
|
if (state.kind === "anonymous" && !state.skipRedirect) {
|
||||||
// Redirect already firing from the effect above; render nothing in
|
// Redirect already firing from the effect above; render nothing in
|
||||||
|
|||||||
@ -80,14 +80,14 @@ export function BatchActionBar() {
|
|||||||
<div
|
<div
|
||||||
role="toolbar"
|
role="toolbar"
|
||||||
aria-label="Batch workspace actions"
|
aria-label="Batch workspace actions"
|
||||||
className="fixed bottom-6 left-1/2 -translate-x-1/2 z-[200] flex items-center gap-3 px-4 py-2.5 rounded-2xl bg-zinc-900/95 border border-zinc-700/70 shadow-2xl shadow-black/50 backdrop-blur-md"
|
className="fixed bottom-6 left-1/2 -translate-x-1/2 z-[200] flex items-center gap-3 px-4 py-2.5 rounded-2xl bg-surface-sunken/95 border border-line/70 shadow-2xl shadow-black/50 backdrop-blur-md"
|
||||||
>
|
>
|
||||||
{/* Selection count badge */}
|
{/* Selection count badge */}
|
||||||
<span className="text-[12px] font-semibold text-zinc-100 bg-blue-600/80 px-2.5 py-0.5 rounded-full tabular-nums">
|
<span className="text-[12px] font-semibold text-ink bg-accent-strong/80 px-2.5 py-0.5 rounded-full tabular-nums">
|
||||||
{count} selected
|
{count} selected
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="w-px h-5 bg-zinc-700/60" aria-hidden="true" />
|
<div className="w-px h-5 bg-surface-card/60" aria-hidden="true" />
|
||||||
|
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
<button
|
<button
|
||||||
@ -104,7 +104,7 @@ export function BatchActionBar() {
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={() => setPending("pause")}
|
onClick={() => setPending("pause")}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[12px] font-medium text-amber-300 bg-amber-900/30 hover:bg-amber-800/50 border border-amber-700/30 hover:border-amber-600/50 transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/70"
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[12px] font-medium text-warm bg-amber-900/30 hover:bg-amber-800/50 border border-amber-700/30 hover:border-amber-600/50 transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/70"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">⏸</span>
|
<span aria-hidden="true">⏸</span>
|
||||||
Pause All
|
Pause All
|
||||||
@ -114,13 +114,13 @@ export function BatchActionBar() {
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={() => setPending("delete")}
|
onClick={() => setPending("delete")}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[12px] font-medium text-red-300 bg-red-900/30 hover:bg-red-800/50 border border-red-700/30 hover:border-red-600/50 transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/70"
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[12px] font-medium text-bad bg-red-900/30 hover:bg-red-800/50 border border-red-700/30 hover:border-red-600/50 transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/70"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">✕</span>
|
<span aria-hidden="true">✕</span>
|
||||||
Delete All
|
Delete All
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="w-px h-5 bg-zinc-700/60" aria-hidden="true" />
|
<div className="w-px h-5 bg-surface-card/60" aria-hidden="true" />
|
||||||
|
|
||||||
{/* Deselect */}
|
{/* Deselect */}
|
||||||
<button
|
<button
|
||||||
@ -129,7 +129,7 @@ export function BatchActionBar() {
|
|||||||
onClick={clearSelection}
|
onClick={clearSelection}
|
||||||
aria-label="Clear selection"
|
aria-label="Clear selection"
|
||||||
title="Clear selection (Escape)"
|
title="Clear selection (Escape)"
|
||||||
className="p-1.5 rounded-lg text-[12px] text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700/50 transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-500/70"
|
className="p-1.5 rounded-lg text-[12px] text-ink-mid hover:text-ink hover:bg-surface-card/50 transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-500/70"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -112,7 +112,7 @@ export function BundleDropZone() {
|
|||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
aria-label="Import bundle file"
|
aria-label="Import bundle file"
|
||||||
aria-controls="bundle-file-input"
|
aria-controls="bundle-file-input"
|
||||||
className="sr-only focus:not-sr-only fixed bottom-20 right-4 z-30 px-3 py-1.5 bg-zinc-900/90 border border-zinc-700/50 rounded-lg text-[10px] text-zinc-400 hover:text-zinc-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 transition-colors"
|
className="sr-only focus:not-sr-only fixed bottom-20 right-4 z-30 px-3 py-1.5 bg-surface-sunken/90 border border-line/50 rounded-lg text-[10px] text-ink-mid hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent transition-colors"
|
||||||
>
|
>
|
||||||
📦 Import bundle
|
📦 Import bundle
|
||||||
</button>
|
</button>
|
||||||
@ -120,19 +120,19 @@ export function BundleDropZone() {
|
|||||||
{/* Visual overlay when dragging */}
|
{/* Visual overlay when dragging */}
|
||||||
{isDragging && (
|
{isDragging && (
|
||||||
<div className="fixed inset-0 z-20 flex items-center justify-center bg-blue-950/40 backdrop-blur-sm border-2 border-dashed border-blue-400/50 pointer-events-none">
|
<div className="fixed inset-0 z-20 flex items-center justify-center bg-blue-950/40 backdrop-blur-sm border-2 border-dashed border-blue-400/50 pointer-events-none">
|
||||||
<div className="bg-zinc-900/95 border border-blue-500/50 rounded-2xl px-8 py-6 shadow-2xl text-center">
|
<div className="bg-surface-sunken/95 border border-accent/50 rounded-2xl px-8 py-6 shadow-2xl text-center">
|
||||||
<div className="text-3xl mb-2" aria-hidden="true">📦</div>
|
<div className="text-3xl mb-2" aria-hidden="true">📦</div>
|
||||||
<div className="text-sm font-semibold text-zinc-100">Drop Bundle to Import</div>
|
<div className="text-sm font-semibold text-ink">Drop Bundle to Import</div>
|
||||||
<div className="text-xs text-zinc-500 mt-1">.bundle.json files only</div>
|
<div className="text-xs text-ink-soft mt-1">.bundle.json files only</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Importing spinner */}
|
{/* Importing spinner */}
|
||||||
{importing && (
|
{importing && (
|
||||||
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 bg-zinc-900/95 border border-zinc-700/60 rounded-xl px-5 py-3 shadow-2xl flex items-center gap-3">
|
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 bg-surface-sunken/95 border border-line/60 rounded-xl px-5 py-3 shadow-2xl flex items-center gap-3">
|
||||||
<div className="w-4 h-4 border-2 border-sky-400 border-t-transparent rounded-full animate-spin" />
|
<div className="w-4 h-4 border-2 border-sky-400 border-t-transparent rounded-full animate-spin" />
|
||||||
<span className="text-sm text-zinc-200">Importing bundle...</span>
|
<span className="text-sm text-ink">Importing bundle...</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -244,11 +244,11 @@ function CanvasInner() {
|
|||||||
<>
|
<>
|
||||||
<a
|
<a
|
||||||
href="#canvas-main"
|
href="#canvas-main"
|
||||||
className="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:px-4 focus:py-2 focus:bg-zinc-900 focus:text-zinc-100 focus:rounded-lg focus:border focus:border-zinc-700"
|
className="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:px-4 focus:py-2 focus:bg-surface-sunken focus:text-ink focus:rounded-lg focus:border focus:border-line"
|
||||||
>
|
>
|
||||||
Skip to canvas
|
Skip to canvas
|
||||||
</a>
|
</a>
|
||||||
<main id="canvas-main" className="w-screen h-screen bg-zinc-950">
|
<main id="canvas-main" className="w-screen h-screen bg-surface">
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
colorMode="dark"
|
colorMode="dark"
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@ -276,11 +276,11 @@ function CanvasInner() {
|
|||||||
color="#27272a"
|
color="#27272a"
|
||||||
/>
|
/>
|
||||||
<Controls
|
<Controls
|
||||||
className="!bg-zinc-900/90 !border-zinc-700/50 !rounded-lg !shadow-xl !shadow-black/20 [&>button]:!bg-zinc-800 [&>button]:!border-zinc-700/50 [&>button]:!text-zinc-400 [&>button:hover]:!bg-zinc-700 [&>button:hover]:!text-zinc-200"
|
className="!bg-surface-sunken/90 !border-line/50 !rounded-lg !shadow-xl !shadow-black/20 [&>button]:!bg-surface-card [&>button]:!border-line/50 [&>button]:!text-ink-mid [&>button:hover]:!bg-surface-card [&>button:hover]:!text-ink"
|
||||||
showInteractive={false}
|
showInteractive={false}
|
||||||
/>
|
/>
|
||||||
<MiniMap
|
<MiniMap
|
||||||
className="!bg-zinc-900/90 !border-zinc-700/50 !rounded-lg !shadow-xl !shadow-black/20"
|
className="!bg-surface-sunken/90 !border-line/50 !rounded-lg !shadow-xl !shadow-black/20"
|
||||||
maskColor="rgba(0, 0, 0, 0.7)"
|
maskColor="rgba(0, 0, 0, 0.7)"
|
||||||
nodeColor={(node) => {
|
nodeColor={(node) => {
|
||||||
// Parents show as a filled region — hierarchy visible at
|
// Parents show as a filled region — hierarchy visible at
|
||||||
|
|||||||
@ -102,7 +102,7 @@ export function CommunicationOverlay() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setVisible(true)}
|
onClick={() => setVisible(true)}
|
||||||
aria-label="Show communications panel"
|
aria-label="Show communications panel"
|
||||||
className="fixed top-16 right-4 z-30 px-3 py-1.5 bg-zinc-900/90 border border-zinc-700/50 rounded-lg text-[10px] text-zinc-400 hover:text-zinc-200 transition-colors"
|
className="fixed top-16 right-4 z-30 px-3 py-1.5 bg-surface-sunken/90 border border-line/50 rounded-lg text-[10px] text-ink-mid hover:text-ink transition-colors"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">↗↙ </span>{comms.length > 0 ? `${comms.length} comms` : "Communications"}
|
<span aria-hidden="true">↗↙ </span>{comms.length > 0 ? `${comms.length} comms` : "Communications"}
|
||||||
</button>
|
</button>
|
||||||
@ -110,16 +110,16 @@ export function CommunicationOverlay() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed top-16 right-4 z-30 w-[320px] max-h-[400px] bg-zinc-900/95 border border-zinc-700/50 rounded-xl shadow-xl shadow-black/30 backdrop-blur-sm overflow-hidden">
|
<div className="fixed top-16 right-4 z-30 w-[320px] max-h-[400px] bg-surface-sunken/95 border border-line/50 rounded-xl shadow-xl shadow-black/30 backdrop-blur-sm overflow-hidden">
|
||||||
<div className="flex items-center justify-between px-3 py-2 border-b border-zinc-800/60">
|
<div className="flex items-center justify-between px-3 py-2 border-b border-line/60">
|
||||||
<div className="text-[10px] font-semibold text-zinc-400 uppercase tracking-wider">
|
<div className="text-[10px] font-semibold text-ink-mid uppercase tracking-wider">
|
||||||
<span aria-hidden="true">↗↙ </span>Communications ({comms.length})
|
<span aria-hidden="true">↗↙ </span>Communications ({comms.length})
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setVisible(false)}
|
onClick={() => setVisible(false)}
|
||||||
aria-label="Close communications panel"
|
aria-label="Close communications panel"
|
||||||
className="text-zinc-500 hover:text-zinc-300 text-xs"
|
className="text-ink-soft hover:text-ink-mid text-xs"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">✕</span>
|
<span aria-hidden="true">✕</span>
|
||||||
</button>
|
</button>
|
||||||
@ -128,10 +128,10 @@ export function CommunicationOverlay() {
|
|||||||
<div className="overflow-y-auto max-h-[350px] p-2 space-y-1">
|
<div className="overflow-y-auto max-h-[350px] p-2 space-y-1">
|
||||||
{comms.map((c) => {
|
{comms.map((c) => {
|
||||||
const isSelected = selectedNodeId === c.sourceId || selectedNodeId === c.targetId;
|
const isSelected = selectedNodeId === c.sourceId || selectedNodeId === c.targetId;
|
||||||
const typeColor = c.type === "a2a_send" ? "text-cyan-400" : c.type === "a2a_receive" ? "text-blue-400" : "text-amber-400";
|
const typeColor = c.type === "a2a_send" ? "text-cyan-400" : c.type === "a2a_receive" ? "text-accent" : "text-warm";
|
||||||
const typeIcon = c.type === "a2a_send" ? "↗" : c.type === "a2a_receive" ? "↙" : "◆";
|
const typeIcon = c.type === "a2a_send" ? "↗" : c.type === "a2a_receive" ? "↙" : "◆";
|
||||||
const statusIcon = c.status === "ok" ? "✓" : c.status === "error" ? "✕" : "⏱";
|
const statusIcon = c.status === "ok" ? "✓" : c.status === "error" ? "✕" : "⏱";
|
||||||
const statusColor = c.status === "ok" ? "text-emerald-400" : c.status === "error" ? "text-red-400" : "text-amber-400";
|
const statusColor = c.status === "ok" ? "text-good" : c.status === "error" ? "text-bad" : "text-warm";
|
||||||
const age = formatAge(c.timestamp);
|
const age = formatAge(c.timestamp);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -140,31 +140,31 @@ export function CommunicationOverlay() {
|
|||||||
className={`rounded-lg px-2.5 py-1.5 text-[9px] border transition-all ${
|
className={`rounded-lg px-2.5 py-1.5 text-[9px] border transition-all ${
|
||||||
isSelected
|
isSelected
|
||||||
? "bg-blue-950/30 border-blue-800/40"
|
? "bg-blue-950/30 border-blue-800/40"
|
||||||
: "bg-zinc-800/30 border-zinc-700/20 hover:bg-zinc-800/50"
|
: "bg-surface-card/30 border-line/20 hover:bg-surface-card/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-1.5 min-w-0">
|
<div className="flex items-center gap-1.5 min-w-0">
|
||||||
<span className={typeColor} aria-hidden="true">{typeIcon}</span>
|
<span className={typeColor} aria-hidden="true">{typeIcon}</span>
|
||||||
<span className="sr-only">{COMM_TYPE_LABELS[c.type] ?? c.type}</span>
|
<span className="sr-only">{COMM_TYPE_LABELS[c.type] ?? c.type}</span>
|
||||||
<span className="text-zinc-300 font-medium truncate">
|
<span className="text-ink-mid font-medium truncate">
|
||||||
{c.sourceName}
|
{c.sourceName}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-zinc-400" aria-hidden="true">→</span>
|
<span className="text-ink-mid" aria-hidden="true">→</span>
|
||||||
<span className="sr-only">to</span>
|
<span className="sr-only">to</span>
|
||||||
<span className="text-zinc-300 truncate">{c.targetName}</span>
|
<span className="text-ink-mid truncate">{c.targetName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
<span className={statusColor} aria-hidden="true">{statusIcon}</span>
|
<span className={statusColor} aria-hidden="true">{statusIcon}</span>
|
||||||
<span className="sr-only">{c.status}</span>
|
<span className="sr-only">{c.status}</span>
|
||||||
<span className="text-zinc-400">{age}</span>
|
<span className="text-ink-mid">{age}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{c.summary && (
|
{c.summary && (
|
||||||
<div className="text-zinc-500 truncate mt-0.5 pl-4">{c.summary}</div>
|
<div className="text-ink-soft truncate mt-0.5 pl-4">{c.summary}</div>
|
||||||
)}
|
)}
|
||||||
{c.durationMs && (
|
{c.durationMs && (
|
||||||
<div className="text-zinc-400 pl-4">{c.durationMs}ms</div>
|
<div className="text-ink-mid pl-4">{c.durationMs}ms</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -93,10 +93,10 @@ export function ConfirmDialog({
|
|||||||
|
|
||||||
const confirmColors =
|
const confirmColors =
|
||||||
confirmVariant === "danger"
|
confirmVariant === "danger"
|
||||||
? "bg-red-600 hover:bg-red-500 text-white"
|
? "bg-red-600 hover:bg-red-500 text-ink"
|
||||||
: confirmVariant === "warning"
|
: confirmVariant === "warning"
|
||||||
? "bg-amber-600 hover:bg-amber-500 text-white"
|
? "bg-amber-600 hover:bg-amber-500 text-ink"
|
||||||
: "bg-blue-600 hover:bg-blue-500 text-white";
|
: "bg-accent-strong hover:bg-accent text-ink";
|
||||||
|
|
||||||
// Render via Portal so the fixed-position dialog escapes any containing block
|
// Render via Portal so the fixed-position dialog escapes any containing block
|
||||||
// (e.g. parents with transform, filter, will-change that break position:fixed).
|
// (e.g. parents with transform, filter, will-change that break position:fixed).
|
||||||
@ -111,19 +111,19 @@ export function ConfirmDialog({
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="confirm-dialog-title"
|
aria-labelledby="confirm-dialog-title"
|
||||||
className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl shadow-black/50 max-w-[380px] w-full mx-4 overflow-hidden"
|
className="relative bg-surface-sunken border border-line rounded-xl shadow-2xl shadow-black/50 max-w-[380px] w-full mx-4 overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
<h3 id="confirm-dialog-title" className="text-sm font-semibold text-zinc-100 mb-2">{title}</h3>
|
<h3 id="confirm-dialog-title" className="text-sm font-semibold text-ink mb-2">{title}</h3>
|
||||||
<p className="text-[13px] text-zinc-400 leading-relaxed">{message}</p>
|
<p className="text-[13px] text-ink-mid leading-relaxed">{message}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-zinc-800 bg-zinc-950/50">
|
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-line bg-surface/50">
|
||||||
{!singleButton && (
|
{!singleButton && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
className="px-3.5 py-1.5 text-[13px] text-zinc-400 hover:text-zinc-200 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg transition-colors"
|
className="px-3.5 py-1.5 text-[13px] text-ink-mid hover:text-ink bg-surface-card hover:bg-surface-card border border-line rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -95,15 +95,15 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="console-modal-title"
|
aria-labelledby="console-modal-title"
|
||||||
className="relative bg-zinc-950 border border-zinc-800 rounded-xl shadow-2xl w-[min(900px,90vw)] h-[min(70vh,700px)] flex flex-col overflow-hidden"
|
className="relative bg-surface border border-line rounded-xl shadow-2xl w-[min(900px,90vw)] h-[min(70vh,700px)] flex flex-col overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-zinc-800">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-line">
|
||||||
<div>
|
<div>
|
||||||
<h3 id="console-modal-title" className="text-sm font-semibold text-zinc-100">
|
<h3 id="console-modal-title" className="text-sm font-semibold text-ink">
|
||||||
EC2 console output
|
EC2 console output
|
||||||
</h3>
|
</h3>
|
||||||
{workspaceName && (
|
{workspaceName && (
|
||||||
<div className="text-[11px] text-zinc-500 mt-0.5 truncate max-w-[600px]">
|
<div className="text-[11px] text-ink-soft mt-0.5 truncate max-w-[600px]">
|
||||||
{workspaceName}
|
{workspaceName}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -113,7 +113,7 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
|
|||||||
ref={closeButtonRef}
|
ref={closeButtonRef}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
className="text-zinc-400 hover:text-zinc-100 text-sm px-2"
|
className="text-ink-mid hover:text-ink text-sm px-2"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
@ -121,14 +121,14 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
|
|||||||
|
|
||||||
<div className="flex-1 overflow-auto bg-black/80 p-4">
|
<div className="flex-1 overflow-auto bg-black/80 p-4">
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="text-[12px] text-zinc-500" data-testid="console-loading">
|
<div className="text-[12px] text-ink-soft" data-testid="console-loading">
|
||||||
Loading console output…
|
Loading console output…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!loading && error && (
|
{!loading && error && (
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
className="text-[12px] text-amber-300 bg-amber-950/30 border border-amber-900/40 rounded px-3 py-2"
|
className="text-[12px] text-warm bg-amber-950/30 border border-amber-900/40 rounded px-3 py-2"
|
||||||
data-testid="console-error"
|
data-testid="console-error"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
@ -136,7 +136,7 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
|
|||||||
)}
|
)}
|
||||||
{!loading && !error && output !== null && (
|
{!loading && !error && output !== null && (
|
||||||
<pre
|
<pre
|
||||||
className="text-[11px] text-zinc-300 font-mono whitespace-pre-wrap break-all leading-tight"
|
className="text-[11px] text-ink-mid font-mono whitespace-pre-wrap break-all leading-tight"
|
||||||
data-testid="console-output"
|
data-testid="console-output"
|
||||||
>
|
>
|
||||||
{output || "(console output is empty — the instance may still be booting)"}
|
{output || "(console output is empty — the instance may still be booting)"}
|
||||||
@ -144,7 +144,7 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-2 px-4 py-3 border-t border-zinc-800 bg-zinc-900/40">
|
<div className="flex items-center justify-end gap-2 px-4 py-3 border-t border-line bg-surface-sunken/40">
|
||||||
{output && (
|
{output && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -155,7 +155,7 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
|
|||||||
showToast("Copy requires HTTPS — please select and copy manually", "info");
|
showToast("Copy requires HTTPS — please select and copy manually", "info");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="px-3 py-1.5 text-[11px] text-zinc-400 hover:text-zinc-200 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg transition-colors"
|
className="px-3 py-1.5 text-[11px] text-ink-mid hover:text-ink bg-surface-card hover:bg-surface-card border border-line rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
Copy
|
Copy
|
||||||
</button>
|
</button>
|
||||||
@ -163,7 +163,7 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-3 py-1.5 text-[11px] text-zinc-300 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg transition-colors"
|
className="px-3 py-1.5 text-[11px] text-ink-mid bg-surface-card hover:bg-surface-card border border-line rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -287,24 +287,24 @@ export function ContextMenu() {
|
|||||||
role="menu"
|
role="menu"
|
||||||
aria-label={`Actions for ${contextMenu.nodeData.name}`}
|
aria-label={`Actions for ${contextMenu.nodeData.name}`}
|
||||||
onKeyDown={handleMenuKeyDown}
|
onKeyDown={handleMenuKeyDown}
|
||||||
className="fixed z-[60] min-w-[200px] bg-zinc-950/95 backdrop-blur-xl border border-zinc-800/60 rounded-xl shadow-2xl shadow-black/60 py-1 overflow-hidden"
|
className="fixed z-[60] min-w-[200px] bg-surface/95 backdrop-blur-xl border border-line/60 rounded-xl shadow-2xl shadow-black/60 py-1 overflow-hidden"
|
||||||
style={{ left: contextMenu.x, top: contextMenu.y }}
|
style={{ left: contextMenu.x, top: contextMenu.y }}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="px-3.5 py-2 border-b border-zinc-800/40 mb-0.5">
|
<div className="px-3.5 py-2 border-b border-line/40 mb-0.5">
|
||||||
<div className="text-[11px] font-semibold text-zinc-200 truncate">{contextMenu.nodeData.name}</div>
|
<div className="text-[11px] font-semibold text-ink truncate">{contextMenu.nodeData.name}</div>
|
||||||
<div className="flex items-center gap-1.5 mt-0.5">
|
<div className="flex items-center gap-1.5 mt-0.5">
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={`w-1.5 h-1.5 rounded-full ${statusDotClass(contextMenu.nodeData.status)}`}
|
className={`w-1.5 h-1.5 rounded-full ${statusDotClass(contextMenu.nodeData.status)}`}
|
||||||
/>
|
/>
|
||||||
<span className="text-[10px] text-zinc-500">{contextMenu.nodeData.status}</span>
|
<span className="text-[10px] text-ink-soft">{contextMenu.nodeData.status}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{items.map((item, i) => {
|
{items.map((item, i) => {
|
||||||
if (item.divider) {
|
if (item.divider) {
|
||||||
return <div key={i} role="separator" className="h-px bg-zinc-800/60 my-1" />;
|
return <div key={i} role="separator" className="h-px bg-surface-card/60 my-1" />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@ -316,8 +316,8 @@ export function ContextMenu() {
|
|||||||
aria-disabled={item.disabled}
|
aria-disabled={item.disabled}
|
||||||
className={`w-full px-3.5 py-1.5 flex items-center gap-2.5 text-left text-[11px] transition-colors focus:outline-none focus:ring-1 focus:ring-inset focus:ring-zinc-600 disabled:opacity-25 disabled:cursor-not-allowed ${
|
className={`w-full px-3.5 py-1.5 flex items-center gap-2.5 text-left text-[11px] transition-colors focus:outline-none focus:ring-1 focus:ring-inset focus:ring-zinc-600 disabled:opacity-25 disabled:cursor-not-allowed ${
|
||||||
item.danger
|
item.danger
|
||||||
? "text-red-400 hover:bg-red-950/40 hover:text-red-300"
|
? "text-bad hover:bg-red-950/40 hover:text-bad"
|
||||||
: "text-zinc-300 hover:bg-zinc-800/40 hover:text-zinc-100"
|
: "text-ink-mid hover:bg-surface-card/40 hover:text-ink"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span aria-hidden="true" className="w-4 text-center text-[10px] shrink-0 opacity-50">{item.icon}</span>
|
<span aria-hidden="true" className="w-4 text-center text-[10px] shrink-0 opacity-50">{item.icon}</span>
|
||||||
|
|||||||
@ -99,14 +99,14 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
|
|||||||
aria-label="Conversation trace"
|
aria-label="Conversation trace"
|
||||||
>
|
>
|
||||||
{/* Modal panel */}
|
{/* Modal panel */}
|
||||||
<div className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl max-w-[700px] w-full max-h-[85vh] flex flex-col overflow-hidden">
|
<div className="relative bg-surface-sunken border border-line rounded-xl shadow-2xl max-w-[700px] w-full max-h-[85vh] flex flex-col overflow-hidden">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-5 py-3 border-b border-zinc-800">
|
<div className="flex items-center justify-between px-5 py-3 border-b border-line">
|
||||||
<div>
|
<div>
|
||||||
<Dialog.Title className="text-sm font-semibold text-zinc-100">
|
<Dialog.Title className="text-sm font-semibold text-ink">
|
||||||
Conversation Trace
|
Conversation Trace
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<p className="text-[10px] text-zinc-500 mt-0.5">
|
<p className="text-[10px] text-ink-soft mt-0.5">
|
||||||
{entries.length} events across all workspaces
|
{entries.length} events across all workspaces
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -114,7 +114,7 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Close conversation trace"
|
aria-label="Close conversation trace"
|
||||||
className="text-zinc-500 hover:text-zinc-300 text-lg px-2"
|
className="text-ink-soft hover:text-ink-mid text-lg px-2"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
@ -124,13 +124,13 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
|
|||||||
{/* Timeline */}
|
{/* Timeline */}
|
||||||
<div className="flex-1 overflow-y-auto px-5 py-4">
|
<div className="flex-1 overflow-y-auto px-5 py-4">
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="text-xs text-zinc-500 text-center py-8">
|
<div className="text-xs text-ink-soft text-center py-8">
|
||||||
Loading trace from all workspaces...
|
Loading trace from all workspaces...
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && entries.length === 0 && (
|
{!loading && entries.length === 0 && (
|
||||||
<div className="text-xs text-zinc-500 text-center py-8">
|
<div className="text-xs text-ink-soft text-center py-8">
|
||||||
No activity found
|
No activity found
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -160,28 +160,28 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
|
|||||||
: isSend
|
: isSend
|
||||||
? "bg-cyan-500"
|
? "bg-cyan-500"
|
||||||
: isReceive
|
: isReceive
|
||||||
? "bg-blue-500"
|
? "bg-accent"
|
||||||
: "bg-zinc-600"
|
: "bg-zinc-600"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<div className="w-px flex-1 bg-zinc-800 min-h-[8px]" />
|
<div className="w-px flex-1 bg-surface-card min-h-[8px]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 pb-3 min-w-0">
|
<div className="flex-1 pb-3 min-w-0">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<span className="text-[9px] text-zinc-400 font-mono">
|
<span className="text-[9px] text-ink-mid font-mono">
|
||||||
{time}
|
{time}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`text-[9px] font-semibold px-1.5 py-0.5 rounded ${
|
className={`text-[9px] font-semibold px-1.5 py-0.5 rounded ${
|
||||||
isError
|
isError
|
||||||
? "bg-red-950/50 text-red-400"
|
? "bg-red-950/50 text-bad"
|
||||||
: isSend
|
: isSend
|
||||||
? "bg-cyan-950/50 text-cyan-400"
|
? "bg-cyan-950/50 text-cyan-400"
|
||||||
: isReceive
|
: isReceive
|
||||||
? "bg-blue-950/50 text-blue-400"
|
? "bg-blue-950/50 text-accent"
|
||||||
: "bg-zinc-800 text-zinc-400"
|
: "bg-surface-card text-ink-mid"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isSend
|
{isSend
|
||||||
@ -191,7 +191,7 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
|
|||||||
: entry.activity_type.toUpperCase()}
|
: entry.activity_type.toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
{entry.duration_ms != null && entry.duration_ms > 0 && (
|
{entry.duration_ms != null && entry.duration_ms > 0 && (
|
||||||
<span className="text-[9px] text-zinc-400">
|
<span className="text-[9px] text-ink-mid">
|
||||||
{entry.duration_ms > 1000
|
{entry.duration_ms > 1000
|
||||||
? `${Math.round(entry.duration_ms / 1000)}s`
|
? `${Math.round(entry.duration_ms / 1000)}s`
|
||||||
: `${entry.duration_ms}ms`}
|
: `${entry.duration_ms}ms`}
|
||||||
@ -207,19 +207,19 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
|
|||||||
<span className="text-cyan-400 font-medium">
|
<span className="text-cyan-400 font-medium">
|
||||||
{sourceName || wsName}
|
{sourceName || wsName}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-zinc-400"> → </span>
|
<span className="text-ink-mid"> → </span>
|
||||||
<span className="text-blue-400 font-medium">
|
<span className="text-accent font-medium">
|
||||||
{targetName}
|
{targetName}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span>
|
<span>
|
||||||
<span className="text-blue-400 font-medium">
|
<span className="text-accent font-medium">
|
||||||
{targetName || wsName}
|
{targetName || wsName}
|
||||||
</span>
|
</span>
|
||||||
{sourceName && (
|
{sourceName && (
|
||||||
<>
|
<>
|
||||||
<span className="text-zinc-400">
|
<span className="text-ink-mid">
|
||||||
{" "}← {" "}
|
{" "}← {" "}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-cyan-400 font-medium">
|
<span className="text-cyan-400 font-medium">
|
||||||
@ -234,40 +234,40 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
|
|||||||
|
|
||||||
{/* Summary */}
|
{/* Summary */}
|
||||||
{entry.summary && !isA2A(entry) && (
|
{entry.summary && !isA2A(entry) && (
|
||||||
<div className="text-[10px] text-zinc-400 mt-1">
|
<div className="text-[10px] text-ink-mid mt-1">
|
||||||
<span className="text-zinc-300 font-medium">{wsName}:</span>{" "}
|
<span className="text-ink-mid font-medium">{wsName}:</span>{" "}
|
||||||
{entry.summary}
|
{entry.summary}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error */}
|
{/* Error */}
|
||||||
{isError && entry.error_detail && (
|
{isError && entry.error_detail && (
|
||||||
<div className="text-[10px] text-red-400/80 mt-1 truncate">
|
<div className="text-[10px] text-bad/80 mt-1 truncate">
|
||||||
{entry.error_detail.slice(0, 200)}
|
{entry.error_detail.slice(0, 200)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Message content — show request and/or response */}
|
{/* Message content — show request and/or response */}
|
||||||
{requestText && (
|
{requestText && (
|
||||||
<div className="mt-1.5 bg-zinc-950/60 border border-zinc-800/50 rounded-lg px-3 py-2 max-h-32 overflow-y-auto">
|
<div className="mt-1.5 bg-surface/60 border border-line/50 rounded-lg px-3 py-2 max-h-32 overflow-y-auto">
|
||||||
<div className="text-[8px] text-zinc-500 uppercase mb-1">
|
<div className="text-[8px] text-ink-soft uppercase mb-1">
|
||||||
{isSend ? "Task" : "Request"}
|
{isSend ? "Task" : "Request"}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] text-zinc-300 whitespace-pre-wrap break-words leading-relaxed">
|
<div className="text-[10px] text-ink-mid whitespace-pre-wrap break-words leading-relaxed">
|
||||||
{requestText.slice(0, 2000)}
|
{requestText.slice(0, 2000)}
|
||||||
{requestText.length > 2000 && (
|
{requestText.length > 2000 && (
|
||||||
<span className="text-zinc-400"> ...({requestText.length} chars)</span>
|
<span className="text-ink-mid"> ...({requestText.length} chars)</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{responseText && (
|
{responseText && (
|
||||||
<div className="mt-1 bg-zinc-950/60 border border-emerald-900/30 rounded-lg px-3 py-2 max-h-32 overflow-y-auto">
|
<div className="mt-1 bg-surface/60 border border-emerald-900/30 rounded-lg px-3 py-2 max-h-32 overflow-y-auto">
|
||||||
<div className="text-[8px] text-emerald-500/60 uppercase mb-1">Response</div>
|
<div className="text-[8px] text-good/60 uppercase mb-1">Response</div>
|
||||||
<div className="text-[10px] text-zinc-300 whitespace-pre-wrap break-words leading-relaxed">
|
<div className="text-[10px] text-ink-mid whitespace-pre-wrap break-words leading-relaxed">
|
||||||
{responseText.slice(0, 2000)}
|
{responseText.slice(0, 2000)}
|
||||||
{responseText.length > 2000 && (
|
{responseText.length > 2000 && (
|
||||||
<span className="text-zinc-400"> ...({responseText.length} chars)</span>
|
<span className="text-ink-mid"> ...({responseText.length} chars)</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -281,11 +281,11 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="px-5 py-3 border-t border-zinc-800 bg-zinc-950/50 flex justify-end">
|
<div className="px-5 py-3 border-t border-line bg-surface/50 flex justify-end">
|
||||||
<Dialog.Close asChild>
|
<Dialog.Close asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="px-4 py-1.5 text-[12px] bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg transition-colors"
|
className="px-4 py-1.5 text-[12px] bg-surface-card hover:bg-surface-card text-ink-mid rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -103,21 +103,21 @@ export function CookieConsent() {
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="cookie-consent-title"
|
aria-labelledby="cookie-consent-title"
|
||||||
aria-describedby="cookie-consent-body"
|
aria-describedby="cookie-consent-body"
|
||||||
className="fixed bottom-0 left-0 right-0 z-[9999] border-t border-zinc-800 bg-zinc-950/95 backdrop-blur-sm p-4 shadow-[0_-4px_12px_rgba(0,0,0,0.4)]"
|
className="fixed bottom-0 left-0 right-0 z-[9999] border-t border-line bg-surface/95 backdrop-blur-sm p-4 shadow-[0_-4px_12px_rgba(0,0,0,0.4)]"
|
||||||
>
|
>
|
||||||
<div className="mx-auto flex max-w-5xl flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
<div className="mx-auto flex max-w-5xl flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
<div className="text-sm text-zinc-300">
|
<div className="text-sm text-ink-mid">
|
||||||
<p id="cookie-consent-title" className="font-medium text-zinc-100">
|
<p id="cookie-consent-title" className="font-medium text-ink">
|
||||||
Cookies & your privacy
|
Cookies & your privacy
|
||||||
</p>
|
</p>
|
||||||
<p id="cookie-consent-body" className="mt-1 text-zinc-400">
|
<p id="cookie-consent-body" className="mt-1 text-ink-mid">
|
||||||
We use strictly-necessary cookies for authentication and session
|
We use strictly-necessary cookies for authentication and session
|
||||||
continuity. Accept to also allow optional functional cookies that
|
continuity. Accept to also allow optional functional cookies that
|
||||||
improve your canvas experience (layout preferences, recent
|
improve your canvas experience (layout preferences, recent
|
||||||
workspaces). See our{" "}
|
workspaces). See our{" "}
|
||||||
<a
|
<a
|
||||||
href="https://moleculesai.app/legal/privacy"
|
href="https://moleculesai.app/legal/privacy"
|
||||||
className="text-blue-400 underline hover:text-blue-300"
|
className="text-accent underline hover:text-accent"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
@ -130,14 +130,14 @@ export function CookieConsent() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => decide("rejected")}
|
onClick={() => decide("rejected")}
|
||||||
className="rounded border border-zinc-700 bg-zinc-900 px-4 py-2 text-sm text-zinc-200 hover:bg-zinc-800"
|
className="rounded border border-line bg-surface-sunken px-4 py-2 text-sm text-ink hover:bg-surface-card"
|
||||||
>
|
>
|
||||||
Necessary only
|
Necessary only
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => decide("accepted")}
|
onClick={() => decide("accepted")}
|
||||||
className="rounded border border-blue-600 bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500"
|
className="rounded border border-accent bg-accent-strong px-4 py-2 text-sm font-medium text-ink hover:bg-accent"
|
||||||
>
|
>
|
||||||
Accept all
|
Accept all
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -310,7 +310,7 @@ export function CreateWorkspaceButton() {
|
|||||||
return (
|
return (
|
||||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||||
<Dialog.Trigger asChild>
|
<Dialog.Trigger asChild>
|
||||||
<button type="button" className="fixed bottom-6 right-6 z-40 px-5 py-2.5 bg-blue-600 hover:bg-blue-500 active:bg-blue-700 text-sm font-medium rounded-xl text-white shadow-lg shadow-blue-600/20 hover:shadow-xl hover:shadow-blue-500/30 transition-all duration-200 flex items-center gap-2">
|
<button type="button" className="fixed bottom-6 right-6 z-40 px-5 py-2.5 bg-accent-strong hover:bg-accent active:bg-accent-strong text-sm font-medium rounded-xl text-ink shadow-lg shadow-blue-600/20 hover:shadow-xl hover:shadow-blue-500/30 transition-all duration-200 flex items-center gap-2">
|
||||||
<svg
|
<svg
|
||||||
width="14"
|
width="14"
|
||||||
height="14"
|
height="14"
|
||||||
@ -333,12 +333,12 @@ export function CreateWorkspaceButton() {
|
|||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm" />
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm" />
|
||||||
<Dialog.Content
|
<Dialog.Content
|
||||||
className="fixed z-50 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-zinc-900 border border-zinc-700/60 rounded-2xl shadow-2xl shadow-black/40 w-[400px] max-h-[90vh] overflow-y-auto p-6"
|
className="fixed z-50 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-surface-sunken border border-line/60 rounded-2xl shadow-2xl shadow-black/40 w-[400px] max-h-[90vh] overflow-y-auto p-6"
|
||||||
>
|
>
|
||||||
<Dialog.Title className="text-base font-semibold text-zinc-100 mb-1">
|
<Dialog.Title className="text-base font-semibold text-ink mb-1">
|
||||||
Create Workspace
|
Create Workspace
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<p className="text-xs text-zinc-500 mb-5">
|
<p className="text-xs text-ink-soft mb-5">
|
||||||
Add a new workspace node to the canvas
|
Add a new workspace node to the canvas
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -367,7 +367,7 @@ export function CreateWorkspaceButton() {
|
|||||||
{/* External toggle — when on, this workspace is BYO-compute:
|
{/* External toggle — when on, this workspace is BYO-compute:
|
||||||
no template, no model, no hermes provider fields. Backend
|
no template, no model, no hermes provider fields. Backend
|
||||||
returns a copyable connection snippet via the modal. */}
|
returns a copyable connection snippet via the modal. */}
|
||||||
<label className="flex items-start gap-2 rounded-lg border border-zinc-800 p-3 cursor-pointer hover:border-zinc-700 transition-colors">
|
<label className="flex items-start gap-2 rounded-lg border border-line p-3 cursor-pointer hover:border-line transition-colors">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isExternal}
|
checked={isExternal}
|
||||||
@ -375,8 +375,8 @@ export function CreateWorkspaceButton() {
|
|||||||
className="mt-0.5"
|
className="mt-0.5"
|
||||||
/>
|
/>
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
<div className="text-zinc-200 font-medium">External agent (bring your own compute)</div>
|
<div className="text-ink font-medium">External agent (bring your own compute)</div>
|
||||||
<div className="text-zinc-500 mt-0.5">
|
<div className="text-ink-soft mt-0.5">
|
||||||
Skip the container. We'll return a workspace_id + auth token + ready-to-paste snippet so an agent running on your laptop / server / CI can register via A2A.
|
Skip the container. We'll return a workspace_id + auth token + ready-to-paste snippet so an agent running on your laptop / server / CI can register via A2A.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -398,7 +398,7 @@ export function CreateWorkspaceButton() {
|
|||||||
aria-label="Workspace tier"
|
aria-label="Workspace tier"
|
||||||
className={`grid gap-1.5 ${isSaaS ? "grid-cols-1" : "grid-cols-4"}`}
|
className={`grid gap-1.5 ${isSaaS ? "grid-cols-1" : "grid-cols-4"}`}
|
||||||
>
|
>
|
||||||
<div className={`text-[11px] text-zinc-400 mb-1 ${isSaaS ? "" : "col-span-4"}`}>
|
<div className={`text-[11px] text-ink-mid mb-1 ${isSaaS ? "" : "col-span-4"}`}>
|
||||||
Tier{isSaaS ? " — dedicated VM" : ""}
|
Tier{isSaaS ? " — dedicated VM" : ""}
|
||||||
</div>
|
</div>
|
||||||
{TIERS.map((t, idx) => (
|
{TIERS.map((t, idx) => (
|
||||||
@ -413,8 +413,8 @@ export function CreateWorkspaceButton() {
|
|||||||
onKeyDown={(e) => handleRadioKeyDown(e, idx)}
|
onKeyDown={(e) => handleRadioKeyDown(e, idx)}
|
||||||
className={`py-2 rounded-lg text-center transition-colors ${
|
className={`py-2 rounded-lg text-center transition-colors ${
|
||||||
tier === t.value
|
tier === t.value
|
||||||
? "bg-blue-600/20 border border-blue-500/50 text-blue-300"
|
? "bg-accent-strong/20 border border-accent/50 text-accent"
|
||||||
: "bg-zinc-800/60 border border-zinc-700/40 text-zinc-400 hover:text-zinc-300 hover:border-zinc-600"
|
: "bg-surface-card/60 border border-line/40 text-ink-mid hover:text-ink-mid hover:border-line"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="text-xs font-mono font-semibold">
|
<div className="text-xs font-mono font-semibold">
|
||||||
@ -429,13 +429,13 @@ export function CreateWorkspaceButton() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[11px] text-zinc-400 block mb-1">
|
<label className="text-[11px] text-ink-mid block mb-1">
|
||||||
Parent Workspace
|
Parent Workspace
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={parentId}
|
value={parentId}
|
||||||
onChange={(e) => setParentId(e.target.value)}
|
onChange={(e) => setParentId(e.target.value)}
|
||||||
className="w-full bg-zinc-800/60 border border-zinc-700/50 rounded-lg px-3 py-2 text-sm text-zinc-100 focus:outline-none focus:border-blue-500/60 focus:ring-1 focus:ring-blue-500/20 transition-colors"
|
className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink focus:outline-none focus:border-accent/60 focus:ring-1 focus:ring-accent/20 transition-colors"
|
||||||
>
|
>
|
||||||
<option value="">None (root level)</option>
|
<option value="">None (root level)</option>
|
||||||
{workspaces.map((ws) => (
|
{workspaces.map((ws) => (
|
||||||
@ -456,7 +456,7 @@ export function CreateWorkspaceButton() {
|
|||||||
<p className="text-[11px] font-semibold text-violet-400 uppercase tracking-wide">
|
<p className="text-[11px] font-semibold text-violet-400 uppercase tracking-wide">
|
||||||
Hermes Provider
|
Hermes Provider
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[11px] text-zinc-500 -mt-1">
|
<p className="text-[11px] text-ink-soft -mt-1">
|
||||||
Choose the AI provider and paste your API key. The key is
|
Choose the AI provider and paste your API key. The key is
|
||||||
stored as an encrypted workspace secret.
|
stored as an encrypted workspace secret.
|
||||||
</p>
|
</p>
|
||||||
@ -464,7 +464,7 @@ export function CreateWorkspaceButton() {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="hermes-provider-select"
|
htmlFor="hermes-provider-select"
|
||||||
className="text-[11px] text-zinc-400 block mb-1"
|
className="text-[11px] text-ink-mid block mb-1"
|
||||||
>
|
>
|
||||||
Provider
|
Provider
|
||||||
</label>
|
</label>
|
||||||
@ -473,7 +473,7 @@ export function CreateWorkspaceButton() {
|
|||||||
value={hermesProvider}
|
value={hermesProvider}
|
||||||
onChange={(e) => setHermesProvider(e.target.value)}
|
onChange={(e) => setHermesProvider(e.target.value)}
|
||||||
aria-label="Hermes provider"
|
aria-label="Hermes provider"
|
||||||
className="w-full bg-zinc-800/60 border border-zinc-700/50 rounded-lg px-3 py-2 text-sm text-zinc-100 focus:outline-none focus:border-violet-500/60 focus:ring-1 focus:ring-violet-500/20 transition-colors"
|
className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink focus:outline-none focus:border-violet-500/60 focus:ring-1 focus:ring-violet-500/20 transition-colors"
|
||||||
>
|
>
|
||||||
{availableProviders.map((p) => (
|
{availableProviders.map((p) => (
|
||||||
<option key={p.id} value={p.id}>
|
<option key={p.id} value={p.id}>
|
||||||
@ -486,10 +486,10 @@ export function CreateWorkspaceButton() {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="hermes-api-key-input"
|
htmlFor="hermes-api-key-input"
|
||||||
className="text-[11px] text-zinc-400 block mb-1"
|
className="text-[11px] text-ink-mid block mb-1"
|
||||||
>
|
>
|
||||||
API Key{" "}
|
API Key{" "}
|
||||||
<span aria-hidden="true" className="text-red-400">
|
<span aria-hidden="true" className="text-bad">
|
||||||
*
|
*
|
||||||
</span>
|
</span>
|
||||||
<span className="sr-only"> (required)</span>
|
<span className="sr-only"> (required)</span>
|
||||||
@ -502,17 +502,17 @@ export function CreateWorkspaceButton() {
|
|||||||
placeholder="sk-…"
|
placeholder="sk-…"
|
||||||
aria-label="Hermes API key"
|
aria-label="Hermes API key"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className="w-full bg-zinc-800/60 border border-zinc-700/50 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-violet-500/60 focus:ring-1 focus:ring-violet-500/20 transition-colors font-mono"
|
className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink placeholder-zinc-600 focus:outline-none focus:border-violet-500/60 focus:ring-1 focus:ring-violet-500/20 transition-colors font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="hermes-model-input"
|
htmlFor="hermes-model-input"
|
||||||
className="text-[11px] text-zinc-400 block mb-1"
|
className="text-[11px] text-ink-mid block mb-1"
|
||||||
>
|
>
|
||||||
Model{" "}
|
Model{" "}
|
||||||
<span aria-hidden="true" className="text-red-400">
|
<span aria-hidden="true" className="text-bad">
|
||||||
*
|
*
|
||||||
</span>
|
</span>
|
||||||
<span className="sr-only"> (required)</span>
|
<span className="sr-only"> (required)</span>
|
||||||
@ -527,14 +527,14 @@ export function CreateWorkspaceButton() {
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
list="hermes-model-suggestions"
|
list="hermes-model-suggestions"
|
||||||
className="w-full bg-zinc-800/60 border border-zinc-700/50 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-violet-500/60 focus:ring-1 focus:ring-violet-500/20 transition-colors font-mono"
|
className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink placeholder-zinc-600 focus:outline-none focus:border-violet-500/60 focus:ring-1 focus:ring-violet-500/20 transition-colors font-mono"
|
||||||
/>
|
/>
|
||||||
<datalist id="hermes-model-suggestions">
|
<datalist id="hermes-model-suggestions">
|
||||||
{HERMES_PROVIDERS.find((p) => p.id === hermesProvider)?.models.map(
|
{HERMES_PROVIDERS.find((p) => p.id === hermesProvider)?.models.map(
|
||||||
(m) => <option key={m} value={m} />,
|
(m) => <option key={m} value={m} />,
|
||||||
)}
|
)}
|
||||||
</datalist>
|
</datalist>
|
||||||
<p className="text-[10px] text-zinc-500 mt-1">
|
<p className="text-[10px] text-ink-soft mt-1">
|
||||||
Slug determines which provider hermes routes to at install time.
|
Slug determines which provider hermes routes to at install time.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -544,7 +544,7 @@ export function CreateWorkspaceButton() {
|
|||||||
{error && (
|
{error && (
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
className="mt-4 px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-xs text-red-400"
|
className="mt-4 px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-xs text-bad"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
@ -552,7 +552,7 @@ export function CreateWorkspaceButton() {
|
|||||||
|
|
||||||
<div className="flex justify-end gap-2.5 mt-6">
|
<div className="flex justify-end gap-2.5 mt-6">
|
||||||
<Dialog.Close asChild>
|
<Dialog.Close asChild>
|
||||||
<button type="button" className="px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-sm rounded-lg text-zinc-300 transition-colors">
|
<button type="button" className="px-4 py-2 bg-surface-card hover:bg-surface-card text-sm rounded-lg text-ink-mid transition-colors">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
@ -560,7 +560,7 @@ export function CreateWorkspaceButton() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
disabled={creating}
|
disabled={creating}
|
||||||
className="px-5 py-2 bg-blue-600 hover:bg-blue-500 active:bg-blue-700 text-sm rounded-lg text-white disabled:opacity-50 transition-colors"
|
className="px-5 py-2 bg-accent-strong hover:bg-accent active:bg-accent-strong text-sm rounded-lg text-ink disabled:opacity-50 transition-colors"
|
||||||
>
|
>
|
||||||
{creating ? "Creating..." : "Create"}
|
{creating ? "Creating..." : "Create"}
|
||||||
</button>
|
</button>
|
||||||
@ -604,11 +604,11 @@ function InputField({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={inputId} className="text-[11px] text-zinc-400 block mb-1">
|
<label htmlFor={inputId} className="text-[11px] text-ink-mid block mb-1">
|
||||||
{label}{" "}
|
{label}{" "}
|
||||||
{required && (
|
{required && (
|
||||||
<>
|
<>
|
||||||
<span aria-hidden="true" className="text-red-400">
|
<span aria-hidden="true" className="text-bad">
|
||||||
*
|
*
|
||||||
</span>
|
</span>
|
||||||
<span className="sr-only"> (required)</span>
|
<span className="sr-only"> (required)</span>
|
||||||
@ -623,10 +623,10 @@ function InputField({
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
min={type === "number" ? "0" : undefined}
|
min={type === "number" ? "0" : undefined}
|
||||||
step={type === "number" ? "0.01" : undefined}
|
step={type === "number" ? "0.01" : undefined}
|
||||||
className={`w-full bg-zinc-800/60 border border-zinc-700/50 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-500 focus:outline-none focus:border-blue-500/60 focus:ring-1 focus:ring-blue-500/20 transition-colors ${mono ? "font-mono text-xs" : ""}`}
|
className={`w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink placeholder-zinc-500 focus:outline-none focus:border-accent/60 focus:ring-1 focus:ring-accent/20 transition-colors ${mono ? "font-mono text-xs" : ""}`}
|
||||||
/>
|
/>
|
||||||
{helper && (
|
{helper && (
|
||||||
<p className="mt-1 text-xs text-zinc-500">{helper}</p>
|
<p className="mt-1 text-xs text-ink-soft">{helper}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -89,10 +89,10 @@ export function DeleteCascadeConfirmDialog({
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="cascade-dialog-title"
|
aria-labelledby="cascade-dialog-title"
|
||||||
className="relative bg-zinc-900 border border-red-800/60 rounded-xl shadow-2xl shadow-black/50 max-w-[420px] w-full mx-4 overflow-hidden"
|
className="relative bg-surface-sunken border border-red-800/60 rounded-xl shadow-2xl shadow-black/50 max-w-[420px] w-full mx-4 overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="px-5 py-4 border-b border-zinc-800">
|
<div className="px-5 py-4 border-b border-line">
|
||||||
<h3 id="cascade-dialog-title" className="text-sm font-semibold text-red-400">
|
<h3 id="cascade-dialog-title" className="text-sm font-semibold text-bad">
|
||||||
Delete Workspace and Children
|
Delete Workspace and Children
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
@ -101,20 +101,20 @@ export function DeleteCascadeConfirmDialog({
|
|||||||
{/* Warning */}
|
{/* Warning */}
|
||||||
<div className="flex gap-3 mb-4">
|
<div className="flex gap-3 mb-4">
|
||||||
<div className="mt-0.5 shrink-0 w-8 h-8 rounded-full bg-red-900/30 flex items-center justify-center">
|
<div className="mt-0.5 shrink-0 w-8 h-8 rounded-full bg-red-900/30 flex items-center justify-center">
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-red-400" aria-hidden="true">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-bad" aria-hidden="true">
|
||||||
<path d="M8 3L14 13H2L8 3Z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/>
|
<path d="M8 3L14 13H2L8 3Z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/>
|
||||||
<path d="M8 7v3M8 11.5v.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
<path d="M8 7v3M8 11.5v.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[13px] text-zinc-300 leading-relaxed">
|
<p className="text-[13px] text-ink-mid leading-relaxed">
|
||||||
<span className="font-medium text-red-300">"{name}"</span> has{" "}
|
<span className="font-medium text-bad">"{name}"</span> has{" "}
|
||||||
<strong className="text-zinc-100">{children.length}</strong> child{" "}
|
<strong className="text-ink">{children.length}</strong> child{" "}
|
||||||
{children.length === 1 ? "workspace" : "workspaces"}:
|
{children.length === 1 ? "workspace" : "workspaces"}:
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Child list */}
|
{/* Child list */}
|
||||||
<ul className="space-y-1.5 mb-4 ml-4 list-disc list-inside text-[12px] text-zinc-400 max-h-32 overflow-y-auto">
|
<ul className="space-y-1.5 mb-4 ml-4 list-disc list-inside text-[12px] text-ink-mid max-h-32 overflow-y-auto">
|
||||||
{children.map((c) => (
|
{children.map((c) => (
|
||||||
<li key={c.id} className="truncate" title={c.name}>{c.name}</li>
|
<li key={c.id} className="truncate" title={c.name}>{c.name}</li>
|
||||||
))}
|
))}
|
||||||
@ -122,7 +122,7 @@ export function DeleteCascadeConfirmDialog({
|
|||||||
|
|
||||||
{/* Cascade warning */}
|
{/* Cascade warning */}
|
||||||
<div className="rounded border border-red-900/40 bg-red-950/20 px-3 py-2.5 mb-4">
|
<div className="rounded border border-red-900/40 bg-red-950/20 px-3 py-2.5 mb-4">
|
||||||
<p className="text-[12px] text-red-300/80 leading-relaxed">
|
<p className="text-[12px] text-bad/80 leading-relaxed">
|
||||||
Deleting will cascade — <strong className="text-red-200">all child workspaces and their data will be permanently removed.</strong> This cannot be undone.
|
Deleting will cascade — <strong className="text-red-200">all child workspaces and their data will be permanently removed.</strong> This cannot be undone.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -133,19 +133,19 @@ export function DeleteCascadeConfirmDialog({
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={(e) => onCheckedChange(e.target.checked)}
|
onChange={(e) => onCheckedChange(e.target.checked)}
|
||||||
className="mt-0.5 w-4 h-4 rounded border-zinc-600 bg-zinc-800 text-red-500 focus:ring-red-500 focus:ring-offset-0 focus:ring-offset-zinc-900 cursor-pointer"
|
className="mt-0.5 w-4 h-4 rounded border-line bg-surface-card text-bad focus:ring-red-500 focus:ring-offset-0 focus:ring-offset-zinc-900 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<span className="text-[12px] text-zinc-400 group-hover:text-zinc-300 leading-relaxed">
|
<span className="text-[12px] text-ink-mid group-hover:text-ink-mid leading-relaxed">
|
||||||
I understand this will permanently delete all listed workspaces and their data
|
I understand this will permanently delete all listed workspaces and their data
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-zinc-800 bg-zinc-950/50">
|
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-line bg-surface/50">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
className="px-3.5 py-1.5 text-[13px] text-zinc-400 hover:text-zinc-200 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg transition-colors"
|
className="px-3.5 py-1.5 text-[13px] text-ink-mid hover:text-ink bg-surface-card hover:bg-surface-card border border-line rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@ -155,8 +155,8 @@ export function DeleteCascadeConfirmDialog({
|
|||||||
disabled={!checked}
|
disabled={!checked}
|
||||||
className={`px-3.5 py-1.5 text-[13px] rounded-lg transition-colors
|
className={`px-3.5 py-1.5 text-[13px] rounded-lg transition-colors
|
||||||
${checked
|
${checked
|
||||||
? "bg-red-600 hover:bg-red-500 text-white cursor-pointer"
|
? "bg-red-600 hover:bg-red-500 text-ink cursor-pointer"
|
||||||
: "bg-red-900/30 text-red-500/40 cursor-not-allowed"
|
: "bg-red-900/30 text-bad/40 cursor-not-allowed"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Delete All
|
Delete All
|
||||||
|
|||||||
@ -75,11 +75,11 @@ export function EmptyState() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 flex items-start justify-center pointer-events-none z-[1] overflow-y-auto py-8">
|
<div className="absolute inset-0 flex items-start justify-center pointer-events-none z-[1] overflow-y-auto py-8">
|
||||||
<div className="relative max-w-2xl w-full rounded-3xl border border-zinc-800/70 bg-zinc-950/80 backdrop-blur-xl px-8 py-8 text-center shadow-2xl shadow-black/40 pointer-events-auto mx-4">
|
<div className="relative max-w-2xl w-full rounded-3xl border border-line/70 bg-surface/80 backdrop-blur-xl px-8 py-8 text-center shadow-2xl shadow-black/40 pointer-events-auto mx-4">
|
||||||
<div className="absolute inset-x-8 top-0 h-px bg-gradient-to-r from-transparent via-blue-500/50 to-transparent" />
|
<div className="absolute inset-x-8 top-0 h-px bg-gradient-to-r from-transparent via-blue-500/50 to-transparent" />
|
||||||
|
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-sky-500/20 via-blue-500/20 to-violet-500/20 border border-blue-500/20 flex items-center justify-center">
|
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-sky-500/20 via-blue-500/20 to-violet-500/20 border border-accent/20 flex items-center justify-center">
|
||||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
|
||||||
<rect x="3" y="3" width="10" height="10" rx="2" stroke="#60a5fa" strokeWidth="1.5" opacity="0.65" />
|
<rect x="3" y="3" width="10" height="10" rx="2" stroke="#60a5fa" strokeWidth="1.5" opacity="0.65" />
|
||||||
<rect x="15" y="3" width="10" height="10" rx="2" stroke="#60a5fa" strokeWidth="1.5" opacity="0.65" />
|
<rect x="15" y="3" width="10" height="10" rx="2" stroke="#60a5fa" strokeWidth="1.5" opacity="0.65" />
|
||||||
@ -91,16 +91,16 @@ export function EmptyState() {
|
|||||||
<p className="text-[10px] font-semibold uppercase tracking-[0.28em] text-sky-400/80 mb-2">
|
<p className="text-[10px] font-semibold uppercase tracking-[0.28em] text-sky-400/80 mb-2">
|
||||||
Welcome to Molecule AI
|
Welcome to Molecule AI
|
||||||
</p>
|
</p>
|
||||||
<h2 className="text-xl font-semibold text-zinc-100 mb-1">
|
<h2 className="text-xl font-semibold text-ink mb-1">
|
||||||
Deploy your first agent
|
Deploy your first agent
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-zinc-400 mb-6 leading-relaxed">
|
<p className="text-sm text-ink-mid mb-6 leading-relaxed">
|
||||||
Pick a template to get started instantly, or create a blank workspace.
|
Pick a template to get started instantly, or create a blank workspace.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Template grid */}
|
{/* Template grid */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center gap-2 text-xs text-zinc-400 py-4">
|
<div className="flex items-center justify-center gap-2 text-xs text-ink-mid py-4">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
Loading templates...
|
Loading templates...
|
||||||
</div>
|
</div>
|
||||||
@ -114,21 +114,21 @@ export function EmptyState() {
|
|||||||
key={t.id}
|
key={t.id}
|
||||||
onClick={() => void deploy(t)}
|
onClick={() => void deploy(t)}
|
||||||
disabled={anyDeploying}
|
disabled={anyDeploying}
|
||||||
className="group rounded-xl border border-zinc-800/60 bg-zinc-900/50 px-3.5 py-3 hover:border-blue-500/40 hover:bg-zinc-900/80 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:border-zinc-800/60 disabled:hover:bg-zinc-900/50 text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/70"
|
className="group rounded-xl border border-line/60 bg-surface-sunken/50 px-3.5 py-3 hover:border-accent/40 hover:bg-surface-sunken/80 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:border-line/60 disabled:hover:bg-surface-sunken/50 text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/70"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<span className="text-sm font-medium text-zinc-200 group-hover:text-zinc-100 truncate">
|
<span className="text-sm font-medium text-ink group-hover:text-ink truncate">
|
||||||
{deploying === t.id ? "Deploying..." : t.name}
|
{deploying === t.id ? "Deploying..." : t.name}
|
||||||
</span>
|
</span>
|
||||||
<span className={`text-[8px] font-mono font-semibold px-1.5 py-0.5 rounded-md border ${tierColor}`}>
|
<span className={`text-[8px] font-mono font-semibold px-1.5 py-0.5 rounded-md border ${tierColor}`}>
|
||||||
T{t.tier}
|
T{t.tier}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[11px] text-zinc-500 line-clamp-2 leading-relaxed">
|
<p className="text-[11px] text-ink-soft line-clamp-2 leading-relaxed">
|
||||||
{t.description || "No description"}
|
{t.description || "No description"}
|
||||||
</p>
|
</p>
|
||||||
{t.skill_count > 0 && (
|
{t.skill_count > 0 && (
|
||||||
<p className="text-[9px] text-zinc-500 mt-1.5">
|
<p className="text-[9px] text-ink-soft mt-1.5">
|
||||||
{t.skill_count} skill{t.skill_count !== 1 ? "s" : ""}
|
{t.skill_count} skill{t.skill_count !== 1 ? "s" : ""}
|
||||||
{t.model ? ` · ${t.model}` : ""}
|
{t.model ? ` · ${t.model}` : ""}
|
||||||
</p>
|
</p>
|
||||||
@ -144,18 +144,18 @@ export function EmptyState() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={createBlank}
|
onClick={createBlank}
|
||||||
disabled={anyDeploying}
|
disabled={anyDeploying}
|
||||||
className="w-full rounded-xl border border-dashed border-zinc-700/60 bg-zinc-900/30 px-4 py-3 text-sm text-zinc-400 hover:text-zinc-200 hover:border-zinc-600 hover:bg-zinc-900/50 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:text-zinc-400 disabled:hover:border-zinc-700/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/70"
|
className="w-full rounded-xl border border-dashed border-line/60 bg-surface-sunken/30 px-4 py-3 text-sm text-ink-mid hover:text-ink hover:border-line hover:bg-surface-sunken/50 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:text-ink-mid disabled:hover:border-line/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/70"
|
||||||
>
|
>
|
||||||
{blankCreating ? "Creating..." : "+ Create blank workspace"}
|
{blankCreating ? "Creating..." : "+ Create blank workspace"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Org templates — instantiate a whole team in one click */}
|
{/* Org templates — instantiate a whole team in one click */}
|
||||||
<div className="mt-4 pt-4 border-t border-zinc-800/50 text-left">
|
<div className="mt-4 pt-4 border-t border-line/50 text-left">
|
||||||
<OrgTemplatesSection />
|
<OrgTemplatesSection />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{displayError && (
|
{displayError && (
|
||||||
<div role="alert" className="mt-3 px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-xs text-red-400">
|
<div role="alert" className="mt-3 px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-xs text-bad">
|
||||||
{displayError}
|
{displayError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -166,13 +166,13 @@ export function EmptyState() {
|
|||||||
{modal}
|
{modal}
|
||||||
|
|
||||||
{/* Tips */}
|
{/* Tips */}
|
||||||
<div className="mt-5 pt-4 border-t border-zinc-800/50">
|
<div className="mt-5 pt-4 border-t border-line/50">
|
||||||
<div className="flex items-center justify-center gap-6 text-[10px] text-zinc-400">
|
<div className="flex items-center justify-center gap-6 text-[10px] text-ink-mid">
|
||||||
<span>Drag to nest workspaces into teams</span>
|
<span>Drag to nest workspaces into teams</span>
|
||||||
<span className="text-zinc-700">|</span>
|
<span className="text-zinc-700">|</span>
|
||||||
<span>Right-click for actions</span>
|
<span>Right-click for actions</span>
|
||||||
<span className="text-zinc-700">|</span>
|
<span className="text-zinc-700">|</span>
|
||||||
<span>Press <kbd className="px-1 py-0.5 bg-zinc-800 rounded text-zinc-500 font-mono">⌘K</kbd> to search</span>
|
<span>Press <kbd className="px-1 py-0.5 bg-surface-card rounded text-ink-soft font-mono">⌘K</kbd> to search</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -51,8 +51,8 @@ export class ErrorBoundary extends React.Component<
|
|||||||
render() {
|
render() {
|
||||||
if (this.state.hasError) {
|
if (this.state.hasError) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex items-center justify-center bg-zinc-950 z-50">
|
<div className="fixed inset-0 flex items-center justify-center bg-surface z-50">
|
||||||
<div className="max-w-md rounded-2xl border border-red-500/30 bg-zinc-900/90 px-8 py-8 text-center shadow-2xl shadow-black/40">
|
<div className="max-w-md rounded-2xl border border-red-500/30 bg-surface-sunken/90 px-8 py-8 text-center shadow-2xl shadow-black/40">
|
||||||
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-red-500/10 border border-red-500/30">
|
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-red-500/10 border border-red-500/30">
|
||||||
<svg
|
<svg
|
||||||
width="24"
|
width="24"
|
||||||
@ -70,20 +70,20 @@ export class ErrorBoundary extends React.Component<
|
|||||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-lg font-semibold text-zinc-100 mb-2">
|
<h2 className="text-lg font-semibold text-ink mb-2">
|
||||||
Something went wrong
|
Something went wrong
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-zinc-400 mb-1">
|
<p className="text-sm text-ink-mid mb-1">
|
||||||
An unexpected error occurred while rendering the application.
|
An unexpected error occurred while rendering the application.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-red-400/80 mb-6 font-mono break-all">
|
<p className="text-xs text-bad/80 mb-6 font-mono break-all">
|
||||||
{this.state.error?.message ?? "Unknown error"}
|
{this.state.error?.message ?? "Unknown error"}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center justify-center gap-3">
|
<div className="flex items-center justify-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={this.handleReload}
|
onClick={this.handleReload}
|
||||||
className="rounded-lg bg-blue-600 hover:bg-blue-500 px-5 py-2 text-sm font-medium text-white transition-colors"
|
className="rounded-lg bg-accent-strong hover:bg-accent px-5 py-2 text-sm font-medium text-ink transition-colors"
|
||||||
>
|
>
|
||||||
Reload
|
Reload
|
||||||
</button>
|
</button>
|
||||||
@ -93,7 +93,7 @@ export class ErrorBoundary extends React.Component<
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.handleReport();
|
this.handleReport();
|
||||||
}}
|
}}
|
||||||
className="rounded-lg border border-zinc-700 hover:border-zinc-600 px-5 py-2 text-sm font-medium text-zinc-300 hover:text-zinc-100 transition-colors"
|
className="rounded-lg border border-line hover:border-line px-5 py-2 text-sm font-medium text-ink-mid hover:text-ink transition-colors"
|
||||||
>
|
>
|
||||||
Report
|
Report
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -113,13 +113,13 @@ export function ExternalConnectModal({ info, onClose }: Props) {
|
|||||||
<Dialog.Root open onOpenChange={(o) => !o && onClose()}>
|
<Dialog.Root open onOpenChange={(o) => !o && onClose()}>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay className="fixed inset-0 bg-black/60 z-50" />
|
<Dialog.Overlay className="fixed inset-0 bg-black/60 z-50" />
|
||||||
<Dialog.Content className="fixed left-1/2 top-1/2 z-50 w-[min(720px,92vw)] -translate-x-1/2 -translate-y-1/2 rounded-xl bg-zinc-900 border border-zinc-700 p-6 shadow-2xl">
|
<Dialog.Content className="fixed left-1/2 top-1/2 z-50 w-[min(720px,92vw)] -translate-x-1/2 -translate-y-1/2 rounded-xl bg-surface-sunken border border-line p-6 shadow-2xl">
|
||||||
<Dialog.Title className="text-lg font-semibold text-white">
|
<Dialog.Title className="text-lg font-semibold text-ink">
|
||||||
Connect your external agent
|
Connect your external agent
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<Dialog.Description className="mt-1 text-sm text-zinc-400">
|
<Dialog.Description className="mt-1 text-sm text-ink-mid">
|
||||||
Paste the snippet below into your agent's deployment. The
|
Paste the snippet below into your agent's deployment. The
|
||||||
auth token is shown <span className="text-amber-400">only once</span>
|
auth token is shown <span className="text-warm">only once</span>
|
||||||
{" "}— save it somewhere safe before closing this dialog.
|
{" "}— save it somewhere safe before closing this dialog.
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ export function ExternalConnectModal({ info, onClose }: Props) {
|
|||||||
<div
|
<div
|
||||||
role="tablist"
|
role="tablist"
|
||||||
aria-label="Connection snippet format"
|
aria-label="Connection snippet format"
|
||||||
className="mt-4 flex gap-1 border-b border-zinc-800"
|
className="mt-4 flex gap-1 border-b border-line"
|
||||||
>
|
>
|
||||||
{(() => {
|
{(() => {
|
||||||
// Build the tab order dynamically. Claude Code first
|
// Build the tab order dynamically. Claude Code first
|
||||||
@ -150,8 +150,8 @@ export function ExternalConnectModal({ info, onClose }: Props) {
|
|||||||
onClick={() => setTab(t)}
|
onClick={() => setTab(t)}
|
||||||
className={`px-3 py-2 text-sm border-b-2 -mb-px transition-colors ${
|
className={`px-3 py-2 text-sm border-b-2 -mb-px transition-colors ${
|
||||||
tab === t
|
tab === t
|
||||||
? "border-blue-500 text-white"
|
? "border-accent text-ink"
|
||||||
: "border-transparent text-zinc-500 hover:text-zinc-300"
|
: "border-transparent text-ink-soft hover:text-ink-mid"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{t === "claude"
|
{t === "claude"
|
||||||
@ -226,7 +226,7 @@ export function ExternalConnectModal({ info, onClose }: Props) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-4 py-2 text-sm rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-200"
|
className="px-4 py-2 text-sm rounded-lg bg-surface-card hover:bg-surface-card text-ink"
|
||||||
>
|
>
|
||||||
I've saved it — close
|
I've saved it — close
|
||||||
</button>
|
</button>
|
||||||
@ -252,16 +252,16 @@ function SnippetBlock({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between pb-1">
|
<div className="flex items-center justify-between pb-1">
|
||||||
<span className="text-xs text-zinc-500">{label}</span>
|
<span className="text-xs text-ink-soft">{label}</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCopy}
|
onClick={onCopy}
|
||||||
className="text-xs px-2 py-1 rounded bg-blue-600/80 hover:bg-blue-500 text-white"
|
className="text-xs px-2 py-1 rounded bg-accent-strong/80 hover:bg-accent text-ink"
|
||||||
>
|
>
|
||||||
{copied ? "Copied!" : "Copy"}
|
{copied ? "Copied!" : "Copy"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<pre className="text-xs bg-zinc-950 border border-zinc-800 rounded-lg p-3 max-h-80 overflow-auto whitespace-pre-wrap break-all font-mono text-zinc-200">
|
<pre className="text-xs bg-surface border border-line rounded-lg p-3 max-h-80 overflow-auto whitespace-pre-wrap break-all font-mono text-ink">
|
||||||
{value}
|
{value}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@ -283,9 +283,9 @@ function Field({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-zinc-500 w-36 shrink-0">{label}</span>
|
<span className="text-xs text-ink-soft w-36 shrink-0">{label}</span>
|
||||||
<code
|
<code
|
||||||
className={`flex-1 text-xs bg-zinc-950 border border-zinc-800 rounded px-2 py-1 text-zinc-200 break-all ${mono ? "font-mono" : ""}`}
|
className={`flex-1 text-xs bg-surface border border-line rounded px-2 py-1 text-ink break-all ${mono ? "font-mono" : ""}`}
|
||||||
>
|
>
|
||||||
{value || "(missing)"}
|
{value || "(missing)"}
|
||||||
</code>
|
</code>
|
||||||
@ -293,7 +293,7 @@ function Field({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onCopy}
|
onClick={onCopy}
|
||||||
disabled={!value}
|
disabled={!value}
|
||||||
className="text-xs px-2 py-1 rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-200 disabled:opacity-40"
|
className="text-xs px-2 py-1 rounded bg-surface-card hover:bg-surface-card text-ink disabled:opacity-40"
|
||||||
>
|
>
|
||||||
{copied ? "Copied!" : "Copy"}
|
{copied ? "Copied!" : "Copy"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export function Legend() {
|
|||||||
onClick={openLegend}
|
onClick={openLegend}
|
||||||
aria-label="Show legend"
|
aria-label="Show legend"
|
||||||
title="Show legend"
|
title="Show legend"
|
||||||
className={`fixed bottom-6 ${leftClass} z-30 flex items-center gap-1.5 rounded-full bg-zinc-900/95 border border-zinc-700/50 px-3 py-1.5 text-[11px] font-semibold text-zinc-400 uppercase tracking-wider shadow-xl shadow-black/30 backdrop-blur-sm hover:text-zinc-200 hover:border-zinc-600 transition-[left,colors] duration-200`}
|
className={`fixed bottom-6 ${leftClass} z-30 flex items-center gap-1.5 rounded-full bg-surface-sunken/95 border border-line/50 px-3 py-1.5 text-[11px] font-semibold text-ink-mid uppercase tracking-wider shadow-xl shadow-black/30 backdrop-blur-sm hover:text-ink hover:border-line transition-[left,colors] duration-200`}
|
||||||
>
|
>
|
||||||
<span aria-hidden="true" className="text-[10px]">ⓘ</span>
|
<span aria-hidden="true" className="text-[10px]">ⓘ</span>
|
||||||
Legend
|
Legend
|
||||||
@ -74,15 +74,15 @@ export function Legend() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`fixed bottom-6 ${leftClass} z-30 bg-zinc-900/95 border border-zinc-700/50 rounded-xl px-4 py-3 shadow-xl shadow-black/30 backdrop-blur-sm max-w-[280px] transition-[left] duration-200`}>
|
<div className={`fixed bottom-6 ${leftClass} z-30 bg-surface-sunken/95 border border-line/50 rounded-xl px-4 py-3 shadow-xl shadow-black/30 backdrop-blur-sm max-w-[280px] transition-[left] duration-200`}>
|
||||||
<div className="flex items-start justify-between mb-2">
|
<div className="flex items-start justify-between mb-2">
|
||||||
<div className="text-[11px] font-semibold text-zinc-400 uppercase tracking-wider">Legend</div>
|
<div className="text-[11px] font-semibold text-ink-mid uppercase tracking-wider">Legend</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={closeLegend}
|
onClick={closeLegend}
|
||||||
aria-label="Hide legend"
|
aria-label="Hide legend"
|
||||||
title="Hide legend"
|
title="Hide legend"
|
||||||
className="-mt-0.5 -mr-1 px-1.5 text-[14px] leading-none text-zinc-500 hover:text-zinc-200 transition-colors"
|
className="-mt-0.5 -mr-1 px-1.5 text-[14px] leading-none text-ink-soft hover:text-ink transition-colors"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
@ -90,7 +90,7 @@ export function Legend() {
|
|||||||
|
|
||||||
{/* Status */}
|
{/* Status */}
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<div className="text-[11px] text-zinc-500 font-medium mb-1">Status</div>
|
<div className="text-[11px] text-ink-soft font-medium mb-1">Status</div>
|
||||||
<div className="flex flex-wrap gap-x-3 gap-y-1">
|
<div className="flex flex-wrap gap-x-3 gap-y-1">
|
||||||
{LEGEND_STATUSES.map((s) => (
|
{LEGEND_STATUSES.map((s) => (
|
||||||
<StatusItem key={s} color={STATUS_CONFIG[s].dot} label={STATUS_CONFIG[s].label} />
|
<StatusItem key={s} color={STATUS_CONFIG[s].dot} label={STATUS_CONFIG[s].label} />
|
||||||
@ -100,22 +100,22 @@ export function Legend() {
|
|||||||
|
|
||||||
{/* Tiers */}
|
{/* Tiers */}
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<div className="text-[11px] text-zinc-500 font-medium mb-1">Tier</div>
|
<div className="text-[11px] text-ink-soft font-medium mb-1">Tier</div>
|
||||||
<div className="flex flex-wrap gap-x-3 gap-y-1">
|
<div className="flex flex-wrap gap-x-3 gap-y-1">
|
||||||
<TierItem tier={1} label="Sandboxed" color="text-sky-300 bg-sky-950/40 border-sky-700/30" />
|
<TierItem tier={1} label="Sandboxed" color="text-sky-300 bg-sky-950/40 border-sky-700/30" />
|
||||||
<TierItem tier={2} label="Standard" color="text-violet-300 bg-violet-950/40 border-violet-700/30" />
|
<TierItem tier={2} label="Standard" color="text-violet-300 bg-violet-950/40 border-violet-700/30" />
|
||||||
<TierItem tier={3} label="Full Access" color="text-amber-300 bg-amber-950/40 border-amber-700/30" />
|
<TierItem tier={3} label="Full Access" color="text-warm bg-amber-950/40 border-amber-700/30" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Communication */}
|
{/* Communication */}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[11px] text-zinc-500 font-medium mb-1">Communication</div>
|
<div className="text-[11px] text-ink-soft font-medium mb-1">Communication</div>
|
||||||
<div className="flex flex-wrap gap-x-3 gap-y-1">
|
<div className="flex flex-wrap gap-x-3 gap-y-1">
|
||||||
<CommItem icon="↗" color="text-cyan-400" label="A2A Out" />
|
<CommItem icon="↗" color="text-cyan-400" label="A2A Out" />
|
||||||
<CommItem icon="↙" color="text-blue-400" label="A2A In" />
|
<CommItem icon="↙" color="text-accent" label="A2A In" />
|
||||||
<CommItem icon="◆" color="text-amber-400" label="Task" />
|
<CommItem icon="◆" color="text-warm" label="Task" />
|
||||||
<CommItem icon="!" color="text-red-400" label="Error" />
|
<CommItem icon="!" color="text-bad" label="Error" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -126,7 +126,7 @@ function StatusItem({ color, label }: { color: string; label: string }) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<div className={`w-1.5 h-1.5 rounded-full ${color}`} />
|
<div className={`w-1.5 h-1.5 rounded-full ${color}`} />
|
||||||
<span className="text-[11px] text-zinc-400">{label}</span>
|
<span className="text-[11px] text-ink-mid">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -135,7 +135,7 @@ function TierItem({ tier, label, color }: { tier: number; label: string; color:
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className={`text-[11px] font-mono px-1 py-0.5 rounded border ${color}`}>T{tier}</span>
|
<span className={`text-[11px] font-mono px-1 py-0.5 rounded border ${color}`}>T{tier}</span>
|
||||||
<span className="text-[11px] text-zinc-400">{label}</span>
|
<span className="text-[11px] text-ink-mid">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -144,7 +144,7 @@ function CommItem({ icon, color, label }: { icon: string; color: string; label:
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className={`text-[11px] ${color}`}>{icon}</span>
|
<span className={`text-[11px] ${color}`}>{icon}</span>
|
||||||
<span className="text-[11px] text-zinc-400">{label}</span>
|
<span className="text-[11px] text-ink-mid">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,13 +54,13 @@ function MemorySkeletonRows() {
|
|||||||
{Array.from({ length: 3 }).map((_, i) => (
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="rounded-lg border border-zinc-800/60 bg-zinc-900/50 px-3 py-3 animate-pulse"
|
className="rounded-lg border border-line/60 bg-surface-sunken/50 px-3 py-3 animate-pulse"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-2 rounded bg-zinc-700/50 flex-1" />
|
<div className="h-2 rounded bg-surface-card/50 flex-1" />
|
||||||
<div className="h-2 rounded bg-zinc-700/50 w-8" />
|
<div className="h-2 rounded bg-surface-card/50 w-8" />
|
||||||
<div className="h-2 rounded bg-zinc-700/50 w-6" />
|
<div className="h-2 rounded bg-surface-card/50 w-6" />
|
||||||
<div className="h-2 rounded bg-zinc-700/50 w-10" />
|
<div className="h-2 rounded bg-surface-card/50 w-10" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -148,7 +148,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
|
|||||||
if (loading && entries.length === 0 && !error) {
|
if (loading && entries.length === 0 && !error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-32">
|
<div className="flex items-center justify-center h-32">
|
||||||
<span className="text-xs text-zinc-500">Loading memories…</span>
|
<span className="text-xs text-ink-soft">Loading memories…</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -156,7 +156,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Scope tabs */}
|
{/* Scope tabs */}
|
||||||
<div className="px-4 pt-3 pb-2 border-b border-zinc-800/40 shrink-0">
|
<div className="px-4 pt-3 pb-2 border-b border-line/40 shrink-0">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{SCOPES.map((scope) => (
|
{SCOPES.map((scope) => (
|
||||||
<button
|
<button
|
||||||
@ -167,8 +167,8 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
|
|||||||
className={[
|
className={[
|
||||||
"px-3 py-1 text-[11px] rounded transition-colors",
|
"px-3 py-1 text-[11px] rounded transition-colors",
|
||||||
activeScope === scope
|
activeScope === scope
|
||||||
? "bg-blue-600 text-white"
|
? "bg-accent-strong text-ink"
|
||||||
: "bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200",
|
: "bg-surface-card text-ink-mid hover:bg-surface-card hover:text-ink",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
{scope}
|
{scope}
|
||||||
@ -178,7 +178,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search bar + namespace filter */}
|
{/* Search bar + namespace filter */}
|
||||||
<div className="px-4 pt-3 pb-2 border-b border-zinc-800/40 shrink-0 space-y-2">
|
<div className="px-4 pt-3 pb-2 border-b border-line/40 shrink-0 space-y-2">
|
||||||
<div className="relative flex items-center">
|
<div className="relative flex items-center">
|
||||||
{/* Magnifying glass icon */}
|
{/* Magnifying glass icon */}
|
||||||
<svg
|
<svg
|
||||||
@ -186,7 +186,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
|
|||||||
height="12"
|
height="12"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
fill="none"
|
fill="none"
|
||||||
className="absolute left-2.5 text-zinc-500 pointer-events-none shrink-0"
|
className="absolute left-2.5 text-ink-soft pointer-events-none shrink-0"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<circle cx="7" cy="7" r="4.5" stroke="currentColor" strokeWidth="1.5" />
|
<circle cx="7" cy="7" r="4.5" stroke="currentColor" strokeWidth="1.5" />
|
||||||
@ -198,7 +198,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
|
|||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
placeholder="Semantic search…"
|
placeholder="Semantic search…"
|
||||||
aria-label="Search memories"
|
aria-label="Search memories"
|
||||||
className="w-full bg-zinc-900 border border-zinc-700/60 focus:border-blue-500/60 rounded-lg pl-8 pr-7 py-1.5 text-[11px] text-zinc-200 placeholder-zinc-600 focus:outline-none transition-colors"
|
className="w-full bg-surface-sunken border border-line/60 focus:border-accent/60 rounded-lg pl-8 pr-7 py-1.5 text-[11px] text-ink placeholder-zinc-600 focus:outline-none transition-colors"
|
||||||
/>
|
/>
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
<button
|
<button
|
||||||
@ -208,7 +208,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
|
|||||||
setDebouncedQuery("");
|
setDebouncedQuery("");
|
||||||
}}
|
}}
|
||||||
aria-label="Clear search"
|
aria-label="Clear search"
|
||||||
className="absolute right-2 text-zinc-500 hover:text-zinc-200 transition-colors text-sm leading-none"
|
className="absolute right-2 text-ink-soft hover:text-ink transition-colors text-sm leading-none"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
@ -217,7 +217,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
|
|||||||
|
|
||||||
{/* Namespace filter */}
|
{/* Namespace filter */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label htmlFor="namespace-filter" className="text-[10px] text-zinc-500 shrink-0">
|
<label htmlFor="namespace-filter" className="text-[10px] text-ink-soft shrink-0">
|
||||||
Namespace:
|
Namespace:
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -227,14 +227,14 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
|
|||||||
onChange={(e) => setActiveNamespace(e.target.value)}
|
onChange={(e) => setActiveNamespace(e.target.value)}
|
||||||
placeholder="all namespaces"
|
placeholder="all namespaces"
|
||||||
aria-label="Filter by namespace"
|
aria-label="Filter by namespace"
|
||||||
className="flex-1 bg-zinc-900 border border-zinc-700/60 focus:border-blue-500/60 rounded px-2 py-1 text-[11px] text-zinc-200 placeholder-zinc-600 focus:outline-none transition-colors min-w-0"
|
className="flex-1 bg-surface-sunken border border-line/60 focus:border-accent/60 rounded px-2 py-1 text-[11px] text-ink placeholder-zinc-600 focus:outline-none transition-colors min-w-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className="px-4 py-2.5 border-b border-zinc-800/40 flex items-center justify-between shrink-0">
|
<div className="px-4 py-2.5 border-b border-line/40 flex items-center justify-between shrink-0">
|
||||||
<span className="text-[11px] text-zinc-500">
|
<span className="text-[11px] text-ink-soft">
|
||||||
{debouncedQuery
|
{debouncedQuery
|
||||||
? `${entries.length} result${entries.length !== 1 ? "s" : ""}`
|
? `${entries.length} result${entries.length !== 1 ? "s" : ""}`
|
||||||
: entries.length === 1
|
: entries.length === 1
|
||||||
@ -244,7 +244,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={loadEntries}
|
onClick={loadEntries}
|
||||||
className="px-2 py-1 text-[11px] bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded transition-colors"
|
className="px-2 py-1 text-[11px] bg-surface-card hover:bg-surface-card text-ink-mid rounded transition-colors"
|
||||||
aria-label="Refresh memories"
|
aria-label="Refresh memories"
|
||||||
>
|
>
|
||||||
↻ Refresh
|
↻ Refresh
|
||||||
@ -256,7 +256,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
|
|||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
aria-live="assertive"
|
aria-live="assertive"
|
||||||
className="mx-4 mt-3 px-3 py-2 bg-red-950/30 border border-red-800/40 rounded text-xs text-red-400 shrink-0"
|
className="mx-4 mt-3 px-3 py-2 bg-red-950/30 border border-red-800/40 rounded text-xs text-bad shrink-0"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
@ -270,10 +270,10 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
|
|||||||
debouncedQuery ? (
|
debouncedQuery ? (
|
||||||
<div className="flex flex-col items-center justify-center py-16 gap-3 text-center">
|
<div className="flex flex-col items-center justify-center py-16 gap-3 text-center">
|
||||||
<span className="text-4xl text-zinc-700" aria-hidden="true">◇</span>
|
<span className="text-4xl text-zinc-700" aria-hidden="true">◇</span>
|
||||||
<p className="text-sm font-medium text-zinc-400">
|
<p className="text-sm font-medium text-ink-mid">
|
||||||
No memories match your search
|
No memories match your search
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[11px] text-zinc-600 max-w-[200px] leading-relaxed">
|
<p className="text-[11px] text-ink-soft max-w-[200px] leading-relaxed">
|
||||||
Try a different query or{" "}
|
Try a different query or{" "}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -281,7 +281,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
|
|||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
setDebouncedQuery("");
|
setDebouncedQuery("");
|
||||||
}}
|
}}
|
||||||
className="text-blue-500 hover:text-blue-400 underline transition-colors"
|
className="text-accent hover:text-accent underline transition-colors"
|
||||||
>
|
>
|
||||||
clear the search
|
clear the search
|
||||||
</button>
|
</button>
|
||||||
@ -291,8 +291,8 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center py-16 gap-3 text-center">
|
<div className="flex flex-col items-center justify-center py-16 gap-3 text-center">
|
||||||
<span className="text-4xl text-zinc-700" aria-hidden="true">◇</span>
|
<span className="text-4xl text-zinc-700" aria-hidden="true">◇</span>
|
||||||
<p className="text-sm font-medium text-zinc-400">No {activeScope} memories</p>
|
<p className="text-sm font-medium text-ink-mid">No {activeScope} memories</p>
|
||||||
<p className="text-[11px] text-zinc-600 max-w-[200px] leading-relaxed">
|
<p className="text-[11px] text-ink-soft max-w-[200px] leading-relaxed">
|
||||||
{activeScope === "LOCAL"
|
{activeScope === "LOCAL"
|
||||||
? "This workspace has not written any local memories yet."
|
? "This workspace has not written any local memories yet."
|
||||||
: activeScope === "TEAM"
|
: activeScope === "TEAM"
|
||||||
@ -340,11 +340,11 @@ function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) {
|
|||||||
const bodyId = `mem-body-${sanitizeId(entry.id)}`;
|
const bodyId = `mem-body-${sanitizeId(entry.id)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-zinc-800/60 bg-zinc-900/50 overflow-hidden">
|
<div className="rounded-lg border border-line/60 bg-surface-sunken/50 overflow-hidden">
|
||||||
{/* Header row */}
|
{/* Header row */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full flex items-center gap-2 px-3 py-2.5 text-left hover:bg-zinc-800/30 transition-colors"
|
className="w-full flex items-center gap-2 px-3 py-2.5 text-left hover:bg-surface-card/30 transition-colors"
|
||||||
onClick={() => setExpanded((prev) => !prev)}
|
onClick={() => setExpanded((prev) => !prev)}
|
||||||
aria-expanded={expanded}
|
aria-expanded={expanded}
|
||||||
aria-controls={bodyId}
|
aria-controls={bodyId}
|
||||||
@ -354,9 +354,9 @@ function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) {
|
|||||||
className={[
|
className={[
|
||||||
"text-[9px] shrink-0 font-mono px-1 py-0.5 rounded",
|
"text-[9px] shrink-0 font-mono px-1 py-0.5 rounded",
|
||||||
entry.scope === "LOCAL"
|
entry.scope === "LOCAL"
|
||||||
? "bg-zinc-700 text-zinc-400"
|
? "bg-surface-card text-ink-mid"
|
||||||
: entry.scope === "TEAM"
|
: entry.scope === "TEAM"
|
||||||
? "bg-blue-950 text-blue-400"
|
? "bg-blue-950 text-accent"
|
||||||
: "bg-violet-950 text-violet-400",
|
: "bg-violet-950 text-violet-400",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
title={`Scope: ${entry.scope}`}
|
title={`Scope: ${entry.scope}`}
|
||||||
@ -365,12 +365,12 @@ function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Namespace tag */}
|
{/* Namespace tag */}
|
||||||
<span className="text-[9px] shrink-0 font-mono text-zinc-500 truncate max-w-[80px]" title={entry.namespace}>
|
<span className="text-[9px] shrink-0 font-mono text-ink-soft truncate max-w-[80px]" title={entry.namespace}>
|
||||||
{entry.namespace}
|
{entry.namespace}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Content preview */}
|
{/* Content preview */}
|
||||||
<span className="flex-1 min-w-0 text-[10px] font-mono text-zinc-300 truncate text-left">
|
<span className="flex-1 min-w-0 text-[10px] font-mono text-ink-mid truncate text-left">
|
||||||
{entry.content.length > 60 ? entry.content.slice(0, 60) + "…" : entry.content}
|
{entry.content.length > 60 ? entry.content.slice(0, 60) + "…" : entry.content}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@ -380,8 +380,8 @@ function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) {
|
|||||||
className={[
|
className={[
|
||||||
"text-[9px] shrink-0 font-mono tabular-nums",
|
"text-[9px] shrink-0 font-mono tabular-nums",
|
||||||
entry.similarity_score >= 0.8
|
entry.similarity_score >= 0.8
|
||||||
? "text-blue-500"
|
? "text-accent"
|
||||||
: "text-zinc-400",
|
: "text-ink-mid",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
title={`Similarity: ${(entry.similarity_score * 100).toFixed(1)}%`}
|
title={`Similarity: ${(entry.similarity_score * 100).toFixed(1)}%`}
|
||||||
data-testid="similarity-badge"
|
data-testid="similarity-badge"
|
||||||
@ -390,10 +390,10 @@ function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span className="text-[9px] text-zinc-600 shrink-0">
|
<span className="text-[9px] text-ink-soft shrink-0">
|
||||||
{formatRelativeTime(entry.created_at)}
|
{formatRelativeTime(entry.created_at)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[9px] text-zinc-500 shrink-0" aria-hidden="true">
|
<span className="text-[9px] text-ink-soft shrink-0" aria-hidden="true">
|
||||||
{expanded ? "▼" : "▶"}
|
{expanded ? "▼" : "▶"}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@ -404,13 +404,13 @@ function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) {
|
|||||||
id={bodyId}
|
id={bodyId}
|
||||||
role="region"
|
role="region"
|
||||||
aria-label="Memory details"
|
aria-label="Memory details"
|
||||||
className="border-t border-zinc-800/50 px-3 pb-3 pt-2 space-y-2"
|
className="border-t border-line/50 px-3 pb-3 pt-2 space-y-2"
|
||||||
>
|
>
|
||||||
<pre className="text-[10px] font-mono text-zinc-300 bg-zinc-950 rounded p-2 overflow-x-auto max-h-48 whitespace-pre-wrap break-all">
|
<pre className="text-[10px] font-mono text-ink-mid bg-surface rounded p-2 overflow-x-auto max-h-48 whitespace-pre-wrap break-all">
|
||||||
{entry.content}
|
{entry.content}
|
||||||
</pre>
|
</pre>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="text-[9px] text-zinc-600">
|
<span className="text-[9px] text-ink-soft">
|
||||||
Created: {new Date(entry.created_at).toLocaleString()}
|
Created: {new Date(entry.created_at).toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
@ -420,7 +420,7 @@ function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) {
|
|||||||
onDelete();
|
onDelete();
|
||||||
}}
|
}}
|
||||||
aria-label="Delete memory"
|
aria-label="Delete memory"
|
||||||
className="text-[10px] px-2 py-0.5 bg-red-950/40 hover:bg-red-900/50 border border-red-900/30 rounded text-red-400 transition-colors shrink-0"
|
className="text-[10px] px-2 py-0.5 bg-red-950/40 hover:bg-red-900/50 border border-red-900/30 rounded text-bad transition-colors shrink-0"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -368,9 +368,9 @@ function ProviderPickerModal({
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="missing-keys-title"
|
aria-labelledby="missing-keys-title"
|
||||||
className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl shadow-black/50 max-w-[480px] w-full mx-4 max-h-[80vh] overflow-auto"
|
className="relative bg-surface-sunken border border-line rounded-xl shadow-2xl shadow-black/50 max-w-[480px] w-full mx-4 max-h-[80vh] overflow-auto"
|
||||||
>
|
>
|
||||||
<div className="px-5 py-4 border-b border-zinc-800">
|
<div className="px-5 py-4 border-b border-line">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<div
|
<div
|
||||||
className="w-5 h-5 rounded-md bg-amber-600/20 border border-amber-500/30 flex items-center justify-center"
|
className="w-5 h-5 rounded-md bg-amber-600/20 border border-amber-500/30 flex items-center justify-center"
|
||||||
@ -382,14 +382,14 @@ function ProviderPickerModal({
|
|||||||
<circle cx="6" cy="8.5" r="0.5" fill="#fbbf24" />
|
<circle cx="6" cy="8.5" r="0.5" fill="#fbbf24" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 id="missing-keys-title" className="text-sm font-semibold text-zinc-100">
|
<h3 id="missing-keys-title" className="text-sm font-semibold text-ink">
|
||||||
{title ?? "Missing API Keys"}
|
{title ?? "Missing API Keys"}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[12px] text-zinc-400 leading-relaxed">
|
<p className="text-[12px] text-ink-mid leading-relaxed">
|
||||||
{description ?? (
|
{description ?? (
|
||||||
<>
|
<>
|
||||||
The <span className="text-amber-300 font-medium">{runtimeLabel}</span>{" "}
|
The <span className="text-warm font-medium">{runtimeLabel}</span>{" "}
|
||||||
runtime supports multiple providers. Pick one and paste its API key.
|
runtime supports multiple providers. Pick one and paste its API key.
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -414,17 +414,17 @@ function ProviderPickerModal({
|
|||||||
{entries.map((entry, index) => (
|
{entries.map((entry, index) => (
|
||||||
<div
|
<div
|
||||||
key={entry.key}
|
key={entry.key}
|
||||||
className="bg-zinc-800/50 rounded-lg px-3 py-2.5 border border-zinc-700/50"
|
className="bg-surface-card/50 rounded-lg px-3 py-2.5 border border-line/50"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[11px] text-zinc-300 font-medium">
|
<div className="text-[11px] text-ink-mid font-medium">
|
||||||
{getKeyLabel(entry.key)}
|
{getKeyLabel(entry.key)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[9px] font-mono text-zinc-500">{entry.key}</div>
|
<div className="text-[9px] font-mono text-ink-soft">{entry.key}</div>
|
||||||
</div>
|
</div>
|
||||||
{entry.saved && (
|
{entry.saved && (
|
||||||
<span className="text-[9px] text-emerald-400 bg-emerald-900/30 px-1.5 py-0.5 rounded flex items-center gap-1">
|
<span className="text-[9px] text-good bg-emerald-900/30 px-1.5 py-0.5 rounded flex items-center gap-1">
|
||||||
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" aria-hidden="true">
|
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" aria-hidden="true">
|
||||||
<path d="M1.5 4L3.5 6L6.5 2" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M1.5 4L3.5 6L6.5 2" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -446,12 +446,12 @@ function ProviderPickerModal({
|
|||||||
handleSaveKey(index);
|
handleSaveKey(index);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="flex-1 bg-zinc-900 border border-zinc-600 rounded px-2 py-1.5 text-[11px] text-zinc-100 font-mono focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 transition-colors"
|
className="flex-1 bg-surface-sunken border border-line rounded px-2 py-1.5 text-[11px] text-ink font-mono focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/20 transition-colors"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSaveKey(index)}
|
onClick={() => handleSaveKey(index)}
|
||||||
disabled={!entry.value.trim() || entry.saving}
|
disabled={!entry.value.trim() || entry.saving}
|
||||||
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-[11px] rounded text-white disabled:opacity-30 transition-colors shrink-0"
|
className="px-3 py-1.5 bg-accent-strong hover:bg-accent text-[11px] rounded text-ink disabled:opacity-30 transition-colors shrink-0"
|
||||||
>
|
>
|
||||||
{entry.saving ? "..." : "Save"}
|
{entry.saving ? "..." : "Save"}
|
||||||
</button>
|
</button>
|
||||||
@ -459,19 +459,19 @@ function ProviderPickerModal({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{entry.error && (
|
{entry.error && (
|
||||||
<div className="mt-1.5 text-[10px] text-red-400">{entry.error}</div>
|
<div className="mt-1.5 text-[10px] text-bad">{entry.error}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-5 py-3 border-t border-zinc-800 bg-zinc-950/50 flex items-center justify-between gap-2">
|
<div className="px-5 py-3 border-t border-line bg-surface/50 flex items-center justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
{onOpenSettings && (
|
{onOpenSettings && (
|
||||||
<button
|
<button
|
||||||
onClick={onOpenSettings}
|
onClick={onOpenSettings}
|
||||||
className="text-[11px] text-blue-400 hover:text-blue-300 transition-colors"
|
className="text-[11px] text-accent hover:text-accent transition-colors"
|
||||||
>
|
>
|
||||||
Open Settings Panel
|
Open Settings Panel
|
||||||
</button>
|
</button>
|
||||||
@ -480,7 +480,7 @@ function ProviderPickerModal({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
className="px-3.5 py-1.5 text-[12px] text-zinc-400 hover:text-zinc-200 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg transition-colors"
|
className="px-3.5 py-1.5 text-[12px] text-ink-mid hover:text-ink bg-surface-card hover:bg-surface-card border border-line rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
Cancel Deploy
|
Cancel Deploy
|
||||||
</button>
|
</button>
|
||||||
@ -492,7 +492,7 @@ function ProviderPickerModal({
|
|||||||
!selectorValue.providerId ||
|
!selectorValue.providerId ||
|
||||||
(showModelInput && model.trim() === "")
|
(showModelInput && model.trim() === "")
|
||||||
}
|
}
|
||||||
className="px-3.5 py-1.5 text-[12px] bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition-colors disabled:opacity-40"
|
className="px-3.5 py-1.5 text-[12px] bg-accent-strong hover:bg-accent text-ink rounded-lg transition-colors disabled:opacity-40"
|
||||||
>
|
>
|
||||||
{allSaved ? "Deploy" : entries.length > 1 ? "Add Keys" : "Add Key"}
|
{allSaved ? "Deploy" : entries.length > 1 ? "Add Keys" : "Add Key"}
|
||||||
</button>
|
</button>
|
||||||
@ -640,9 +640,9 @@ function AllKeysModal({
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="missing-keys-title"
|
aria-labelledby="missing-keys-title"
|
||||||
className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl shadow-black/50 max-w-[440px] w-full mx-4 max-h-[80vh] overflow-auto"
|
className="relative bg-surface-sunken border border-line rounded-xl shadow-2xl shadow-black/50 max-w-[440px] w-full mx-4 max-h-[80vh] overflow-auto"
|
||||||
>
|
>
|
||||||
<div className="px-5 py-4 border-b border-zinc-800">
|
<div className="px-5 py-4 border-b border-line">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<div
|
<div
|
||||||
className="w-5 h-5 rounded-md bg-amber-600/20 border border-amber-500/30 flex items-center justify-center"
|
className="w-5 h-5 rounded-md bg-amber-600/20 border border-amber-500/30 flex items-center justify-center"
|
||||||
@ -654,12 +654,12 @@ function AllKeysModal({
|
|||||||
<circle cx="6" cy="8.5" r="0.5" fill="#fbbf24" />
|
<circle cx="6" cy="8.5" r="0.5" fill="#fbbf24" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 id="missing-keys-title" className="text-sm font-semibold text-zinc-100">
|
<h3 id="missing-keys-title" className="text-sm font-semibold text-ink">
|
||||||
Missing API Keys
|
Missing API Keys
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[12px] text-zinc-400 leading-relaxed">
|
<p className="text-[12px] text-ink-mid leading-relaxed">
|
||||||
The <span className="text-amber-300 font-medium">{runtimeLabel}</span>{" "}
|
The <span className="text-warm font-medium">{runtimeLabel}</span>{" "}
|
||||||
runtime requires the following keys to be configured before deploying.
|
runtime requires the following keys to be configured before deploying.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -668,17 +668,17 @@ function AllKeysModal({
|
|||||||
{entries.map((entry, index) => (
|
{entries.map((entry, index) => (
|
||||||
<div
|
<div
|
||||||
key={entry.key}
|
key={entry.key}
|
||||||
className="bg-zinc-800/50 rounded-lg px-3 py-2.5 border border-zinc-700/50"
|
className="bg-surface-card/50 rounded-lg px-3 py-2.5 border border-line/50"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[11px] text-zinc-300 font-medium">
|
<div className="text-[11px] text-ink-mid font-medium">
|
||||||
{getKeyLabel(entry.key)}
|
{getKeyLabel(entry.key)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[9px] font-mono text-zinc-500">{entry.key}</div>
|
<div className="text-[9px] font-mono text-ink-soft">{entry.key}</div>
|
||||||
</div>
|
</div>
|
||||||
{entry.saved && (
|
{entry.saved && (
|
||||||
<span className="text-[9px] text-emerald-400 bg-emerald-900/30 px-1.5 py-0.5 rounded flex items-center gap-1">
|
<span className="text-[9px] text-good bg-emerald-900/30 px-1.5 py-0.5 rounded flex items-center gap-1">
|
||||||
<svg width="8" height="8" viewBox="0 0 8 8" fill="none">
|
<svg width="8" height="8" viewBox="0 0 8 8" fill="none">
|
||||||
<path d="M1.5 4L3.5 6L6.5 2" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M1.5 4L3.5 6L6.5 2" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -700,37 +700,37 @@ function AllKeysModal({
|
|||||||
handleSaveKey(index);
|
handleSaveKey(index);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="flex-1 bg-zinc-900 border border-zinc-600 rounded px-2 py-1.5 text-[11px] text-zinc-100 font-mono focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 transition-colors"
|
className="flex-1 bg-surface-sunken border border-line rounded px-2 py-1.5 text-[11px] text-ink font-mono focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/20 transition-colors"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleSaveKey(index)}
|
onClick={() => handleSaveKey(index)}
|
||||||
disabled={!entry.value.trim() || entry.saving}
|
disabled={!entry.value.trim() || entry.saving}
|
||||||
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-[11px] rounded text-white disabled:opacity-30 transition-colors shrink-0"
|
className="px-3 py-1.5 bg-accent-strong hover:bg-accent text-[11px] rounded text-ink disabled:opacity-30 transition-colors shrink-0"
|
||||||
>
|
>
|
||||||
{entry.saving ? "..." : "Save"}
|
{entry.saving ? "..." : "Save"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{entry.error && <div className="mt-1.5 text-[10px] text-red-400">{entry.error}</div>}
|
{entry.error && <div className="mt-1.5 text-[10px] text-bad">{entry.error}</div>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{globalError && (
|
{globalError && (
|
||||||
<div className="px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-[11px] text-red-400">
|
<div className="px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-[11px] text-bad">
|
||||||
{globalError}
|
{globalError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-5 py-3 border-t border-zinc-800 bg-zinc-950/50 flex items-center justify-between gap-2">
|
<div className="px-5 py-3 border-t border-line bg-surface/50 flex items-center justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
{onOpenSettings && (
|
{onOpenSettings && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onOpenSettings}
|
onClick={onOpenSettings}
|
||||||
className="text-[11px] text-blue-400 hover:text-blue-300 transition-colors"
|
className="text-[11px] text-accent hover:text-accent transition-colors"
|
||||||
>
|
>
|
||||||
Open Settings Panel
|
Open Settings Panel
|
||||||
</button>
|
</button>
|
||||||
@ -740,7 +740,7 @@ function AllKeysModal({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
className="px-3.5 py-1.5 text-[12px] text-zinc-400 hover:text-zinc-200 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg transition-colors"
|
className="px-3.5 py-1.5 text-[12px] text-ink-mid hover:text-ink bg-surface-card hover:bg-surface-card border border-line rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
Cancel Deploy
|
Cancel Deploy
|
||||||
</button>
|
</button>
|
||||||
@ -748,7 +748,7 @@ function AllKeysModal({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleAddKeysAndDeploy}
|
onClick={handleAddKeysAndDeploy}
|
||||||
disabled={!allSaved || anySaving}
|
disabled={!allSaved || anySaving}
|
||||||
className="px-3.5 py-1.5 text-[12px] bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition-colors disabled:opacity-40"
|
className="px-3.5 py-1.5 text-[12px] bg-accent-strong hover:bg-accent text-ink rounded-lg transition-colors disabled:opacity-40"
|
||||||
>
|
>
|
||||||
{anySaving ? "Saving..." : allSaved ? "Deploy" : "Add Keys"}
|
{anySaving ? "Saving..." : allSaved ? "Deploy" : "Add Keys"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -132,10 +132,10 @@ export function OnboardingWizard() {
|
|||||||
<div
|
<div
|
||||||
role="complementary"
|
role="complementary"
|
||||||
aria-label="Onboarding guide"
|
aria-label="Onboarding guide"
|
||||||
className="fixed bottom-20 left-4 z-50 w-80 rounded-2xl border border-zinc-700/60 bg-zinc-900/95 backdrop-blur-xl shadow-2xl shadow-black/40 overflow-hidden"
|
className="fixed bottom-20 left-4 z-50 w-80 rounded-2xl border border-line/60 bg-surface-sunken/95 backdrop-blur-xl shadow-2xl shadow-black/40 overflow-hidden"
|
||||||
>
|
>
|
||||||
{/* Progress bar */}
|
{/* Progress bar */}
|
||||||
<div className="h-1 bg-zinc-800">
|
<div className="h-1 bg-surface-card">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-gradient-to-r from-blue-500 to-sky-400 transition-all duration-500"
|
className="h-full bg-gradient-to-r from-blue-500 to-sky-400 transition-all duration-500"
|
||||||
style={{ width: `${((currentStepIdx + 1) / STEPS.length) * 100}%` }}
|
style={{ width: `${((currentStepIdx + 1) / STEPS.length) * 100}%` }}
|
||||||
@ -162,17 +162,17 @@ export function OnboardingWizard() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={dismiss}
|
onClick={dismiss}
|
||||||
aria-label="Skip onboarding guide"
|
aria-label="Skip onboarding guide"
|
||||||
className="text-[10px] text-zinc-400 hover:text-zinc-200 transition-colors"
|
className="text-[10px] text-ink-mid hover:text-ink transition-colors"
|
||||||
>
|
>
|
||||||
Skip guide
|
Skip guide
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<h3 className="text-sm font-medium text-zinc-100 mb-1">
|
<h3 className="text-sm font-medium text-ink mb-1">
|
||||||
{currentStep.title}
|
{currentStep.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[11px] text-zinc-400 leading-relaxed mb-3">
|
<p className="text-[11px] text-ink-mid leading-relaxed mb-3">
|
||||||
{currentStep.description}
|
{currentStep.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -181,7 +181,7 @@ export function OnboardingWizard() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleAction}
|
onClick={handleAction}
|
||||||
className="flex-1 px-3 py-1.5 bg-blue-600/90 hover:bg-blue-500 rounded-lg text-[11px] font-medium text-white transition-colors"
|
className="flex-1 px-3 py-1.5 bg-accent-strong/90 hover:bg-accent rounded-lg text-[11px] font-medium text-ink transition-colors"
|
||||||
>
|
>
|
||||||
{step === "welcome"
|
{step === "welcome"
|
||||||
? "Create Workspace"
|
? "Create Workspace"
|
||||||
@ -199,7 +199,7 @@ export function OnboardingWizard() {
|
|||||||
if (next) setStep(next.id);
|
if (next) setStep(next.id);
|
||||||
else dismiss();
|
else dismiss();
|
||||||
}}
|
}}
|
||||||
className="px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 rounded-lg text-[11px] text-zinc-400 transition-colors"
|
className="px-3 py-1.5 bg-surface-card hover:bg-surface-card rounded-lg text-[11px] text-ink-mid transition-colors"
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -240,14 +240,14 @@ export function OrgImportPreflightModal({
|
|||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="w-[560px] max-h-[80vh] overflow-auto rounded-xl bg-zinc-900 border border-zinc-700 shadow-2xl"
|
className="w-[560px] max-h-[80vh] overflow-auto rounded-xl bg-surface-sunken border border-line shadow-2xl"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<header className="px-5 py-4 border-b border-zinc-800">
|
<header className="px-5 py-4 border-b border-line">
|
||||||
<h2 id="org-preflight-title" className="text-sm font-semibold text-zinc-100">
|
<h2 id="org-preflight-title" className="text-sm font-semibold text-ink">
|
||||||
Deploy {orgName}
|
Deploy {orgName}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-0.5 text-[11px] text-zinc-500">
|
<p className="mt-0.5 text-[11px] text-ink-soft">
|
||||||
{workspaceCount} workspace{workspaceCount === 1 ? "" : "s"}.
|
{workspaceCount} workspace{workspaceCount === 1 ? "" : "s"}.
|
||||||
Review the credentials needed before import.
|
Review the credentials needed before import.
|
||||||
</p>
|
</p>
|
||||||
@ -283,23 +283,23 @@ export function OrgImportPreflightModal({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{requiredEnv.length === 0 && recommendedEnv.length === 0 && (
|
{requiredEnv.length === 0 && recommendedEnv.length === 0 && (
|
||||||
<p className="text-[12px] text-zinc-400">
|
<p className="text-[12px] text-ink-mid">
|
||||||
No additional credentials required for this template.
|
No additional credentials required for this template.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer className="px-5 py-3 border-t border-zinc-800 flex items-center justify-between">
|
<footer className="px-5 py-3 border-t border-line flex items-center justify-between">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
className="px-3 py-1.5 text-[11px] rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-300"
|
className="px-3 py-1.5 text-[11px] rounded bg-surface-card hover:bg-surface-card text-ink-mid"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{missingRecommended.length > 0 && canProceed && (
|
{missingRecommended.length > 0 && canProceed && (
|
||||||
<span className="text-[10px] text-amber-400/90">
|
<span className="text-[10px] text-warm/90">
|
||||||
{missingRecommended.length} recommended key
|
{missingRecommended.length} recommended key
|
||||||
{missingRecommended.length === 1 ? "" : "s"} still unset
|
{missingRecommended.length === 1 ? "" : "s"} still unset
|
||||||
</span>
|
</span>
|
||||||
@ -308,7 +308,7 @@ export function OrgImportPreflightModal({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onProceed}
|
onClick={onProceed}
|
||||||
disabled={!canProceed}
|
disabled={!canProceed}
|
||||||
className="px-4 py-1.5 text-[11px] font-semibold rounded bg-blue-600 hover:bg-blue-500 text-white disabled:bg-zinc-700 disabled:text-zinc-500 disabled:cursor-not-allowed"
|
className="px-4 py-1.5 text-[11px] font-semibold rounded bg-accent-strong hover:bg-accent text-ink disabled:bg-surface-card disabled:text-ink-soft disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
Import
|
Import
|
||||||
</button>
|
</button>
|
||||||
@ -346,14 +346,14 @@ function EnvList({
|
|||||||
? "border-red-800/60 bg-red-950/20"
|
? "border-red-800/60 bg-red-950/20"
|
||||||
: "border-amber-800/50 bg-amber-950/15";
|
: "border-amber-800/50 bg-amber-950/15";
|
||||||
const headerColor =
|
const headerColor =
|
||||||
tone === "required" ? "text-red-300" : "text-amber-300";
|
tone === "required" ? "text-bad" : "text-warm";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`rounded-lg border ${accent} p-3`}>
|
<div className={`rounded-lg border ${accent} p-3`}>
|
||||||
<h3 className={`text-[11px] font-semibold uppercase tracking-wide ${headerColor}`}>
|
<h3 className={`text-[11px] font-semibold uppercase tracking-wide ${headerColor}`}>
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-0.5 mb-2 text-[10px] text-zinc-400">{subtitle}</p>
|
<p className="mt-0.5 mb-2 text-[10px] text-ink-mid">{subtitle}</p>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{entries.map((entry) =>
|
{entries.map((entry) =>
|
||||||
typeof entry === "string" ? (
|
typeof entry === "string" ? (
|
||||||
@ -397,16 +397,16 @@ function StrictEnvRow({
|
|||||||
onSave,
|
onSave,
|
||||||
}: StrictEnvRowProps) {
|
}: StrictEnvRowProps) {
|
||||||
return (
|
return (
|
||||||
<li className="flex items-center gap-2 rounded bg-zinc-900/70 border border-zinc-800 px-2 py-1.5">
|
<li className="flex items-center gap-2 rounded bg-surface-sunken/70 border border-line px-2 py-1.5">
|
||||||
<code
|
<code
|
||||||
className={`text-[11px] font-mono flex-1 ${
|
className={`text-[11px] font-mono flex-1 ${
|
||||||
configured ? "text-zinc-500 line-through" : "text-zinc-200"
|
configured ? "text-ink-soft line-through" : "text-ink"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{envKey}
|
{envKey}
|
||||||
</code>
|
</code>
|
||||||
{configured ? (
|
{configured ? (
|
||||||
<span className="text-[10px] text-emerald-400">✓ set</span>
|
<span className="text-[10px] text-good">✓ set</span>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
@ -422,20 +422,20 @@ function StrictEnvRow({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={d?.saving}
|
disabled={d?.saving}
|
||||||
className="flex-1 px-2 py-1 rounded bg-zinc-800 border border-zinc-700 text-[11px] text-zinc-200 focus:outline-none focus:border-blue-500 disabled:opacity-50"
|
className="flex-1 px-2 py-1 rounded bg-surface-card border border-line text-[11px] text-ink focus:outline-none focus:border-accent disabled:opacity-50"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSave(envKey)}
|
onClick={() => onSave(envKey)}
|
||||||
disabled={d?.saving || !d?.value.trim()}
|
disabled={d?.saving || !d?.value.trim()}
|
||||||
className="px-2 py-1 text-[10px] rounded bg-blue-600 hover:bg-blue-500 text-white disabled:opacity-40 disabled:cursor-not-allowed"
|
className="px-2 py-1 text-[10px] rounded bg-accent-strong hover:bg-accent text-ink disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{d?.saving ? "…" : "Save"}
|
{d?.saving ? "…" : "Save"}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{d?.error && (
|
{d?.error && (
|
||||||
<span className="text-[9px] text-red-400 basis-full pl-1">
|
<span className="text-[9px] text-bad basis-full pl-1">
|
||||||
{d.error}
|
{d.error}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -467,13 +467,13 @@ function AnyOfEnvGroup({
|
|||||||
}: AnyOfEnvGroupProps) {
|
}: AnyOfEnvGroupProps) {
|
||||||
const satisfiedBy = members.find((m) => configuredKeys.has(m));
|
const satisfiedBy = members.find((m) => configuredKeys.has(m));
|
||||||
return (
|
return (
|
||||||
<li className="rounded border border-zinc-800 bg-zinc-900/50 px-2.5 py-2">
|
<li className="rounded border border-line bg-surface-sunken/50 px-2.5 py-2">
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
<span className="text-[10px] uppercase tracking-wide text-zinc-400">
|
<span className="text-[10px] uppercase tracking-wide text-ink-mid">
|
||||||
Configure any one
|
Configure any one
|
||||||
</span>
|
</span>
|
||||||
{satisfiedBy && (
|
{satisfiedBy && (
|
||||||
<span className="text-[10px] text-emerald-400">
|
<span className="text-[10px] text-good">
|
||||||
✓ using <code className="font-mono">{satisfiedBy}</code>
|
✓ using <code className="font-mono">{satisfiedBy}</code>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -486,19 +486,19 @@ function AnyOfEnvGroup({
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={m}
|
key={m}
|
||||||
className={`flex items-center gap-2 rounded bg-zinc-900/70 border border-zinc-800 px-2 py-1 ${
|
className={`flex items-center gap-2 rounded bg-surface-sunken/70 border border-line px-2 py-1 ${
|
||||||
dimmed ? "opacity-50" : ""
|
dimmed ? "opacity-50" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<code
|
<code
|
||||||
className={`text-[11px] font-mono flex-1 ${
|
className={`text-[11px] font-mono flex-1 ${
|
||||||
isConfigured ? "text-zinc-500 line-through" : "text-zinc-200"
|
isConfigured ? "text-ink-soft line-through" : "text-ink"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{m}
|
{m}
|
||||||
</code>
|
</code>
|
||||||
{isConfigured ? (
|
{isConfigured ? (
|
||||||
<span className="text-[10px] text-emerald-400">✓ set</span>
|
<span className="text-[10px] text-good">✓ set</span>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
@ -514,20 +514,20 @@ function AnyOfEnvGroup({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={d?.saving}
|
disabled={d?.saving}
|
||||||
className="flex-1 px-2 py-1 rounded bg-zinc-800 border border-zinc-700 text-[11px] text-zinc-200 focus:outline-none focus:border-blue-500 disabled:opacity-50"
|
className="flex-1 px-2 py-1 rounded bg-surface-card border border-line text-[11px] text-ink focus:outline-none focus:border-accent disabled:opacity-50"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSave(m)}
|
onClick={() => onSave(m)}
|
||||||
disabled={d?.saving || !d?.value.trim()}
|
disabled={d?.saving || !d?.value.trim()}
|
||||||
className="px-2 py-1 text-[10px] rounded bg-blue-600 hover:bg-blue-500 text-white disabled:opacity-40 disabled:cursor-not-allowed"
|
className="px-2 py-1 text-[10px] rounded bg-accent-strong hover:bg-accent text-ink disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{d?.saving ? "…" : "Save"}
|
{d?.saving ? "…" : "Save"}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{d?.error && (
|
{d?.error && (
|
||||||
<span className="text-[9px] text-red-400 basis-full pl-1">
|
<span className="text-[9px] text-bad basis-full pl-1">
|
||||||
{d.error}
|
{d.error}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -97,27 +97,27 @@ function PlanCard({
|
|||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
}) {
|
}) {
|
||||||
const ring = plan.highlighted
|
const ring = plan.highlighted
|
||||||
? "border-blue-600 ring-2 ring-blue-600/30"
|
? "border-accent ring-2 ring-blue-600/30"
|
||||||
: "border-zinc-800";
|
: "border-line";
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
className={`flex flex-col rounded-lg border ${ring} bg-zinc-900/40 p-6`}
|
className={`flex flex-col rounded-lg border ${ring} bg-surface-sunken/40 p-6`}
|
||||||
aria-labelledby={`plan-${plan.id}-name`}
|
aria-labelledby={`plan-${plan.id}-name`}
|
||||||
>
|
>
|
||||||
{plan.highlighted && (
|
{plan.highlighted && (
|
||||||
<span className="mb-3 inline-block rounded-full bg-blue-600/20 px-3 py-1 text-xs font-medium text-blue-300">
|
<span className="mb-3 inline-block rounded-full bg-accent-strong/20 px-3 py-1 text-xs font-medium text-accent">
|
||||||
Most popular
|
Most popular
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<h2 id={`plan-${plan.id}-name`} className="text-xl font-semibold text-white">
|
<h2 id={`plan-${plan.id}-name`} className="text-xl font-semibold text-ink">
|
||||||
{plan.name}
|
{plan.name}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-1 text-sm text-zinc-400">{plan.tagline}</p>
|
<p className="mt-1 text-sm text-ink-mid">{plan.tagline}</p>
|
||||||
<p className="mt-4 text-3xl font-bold text-white">{plan.price}</p>
|
<p className="mt-4 text-3xl font-bold text-ink">{plan.price}</p>
|
||||||
<ul className="mt-6 flex-1 space-y-2 text-sm text-zinc-300">
|
<ul className="mt-6 flex-1 space-y-2 text-sm text-ink-mid">
|
||||||
{plan.features.map((f) => (
|
{plan.features.map((f) => (
|
||||||
<li key={f} className="flex items-start">
|
<li key={f} className="flex items-start">
|
||||||
<span className="mr-2 text-blue-400" aria-hidden>
|
<span className="mr-2 text-accent" aria-hidden>
|
||||||
✓
|
✓
|
||||||
</span>
|
</span>
|
||||||
{f}
|
{f}
|
||||||
@ -130,8 +130,8 @@ function PlanCard({
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
className={`mt-6 rounded-lg px-4 py-3 text-sm font-medium ${
|
className={`mt-6 rounded-lg px-4 py-3 text-sm font-medium ${
|
||||||
plan.highlighted
|
plan.highlighted
|
||||||
? "bg-blue-600 text-white hover:bg-blue-500 disabled:bg-blue-900"
|
? "bg-accent-strong text-ink hover:bg-accent disabled:bg-blue-900"
|
||||||
: "border border-zinc-700 bg-zinc-900 text-zinc-100 hover:bg-zinc-800 disabled:opacity-50"
|
: "border border-line bg-surface-sunken text-ink hover:bg-surface-card disabled:opacity-50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{loading ? "Opening checkout…" : plan.ctaLabel}
|
{loading ? "Opening checkout…" : plan.ctaLabel}
|
||||||
|
|||||||
@ -356,9 +356,9 @@ export function ProviderModelSelector({
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor={providerSelectId}
|
htmlFor={providerSelectId}
|
||||||
className="text-[10px] uppercase tracking-wide text-zinc-500 font-semibold mb-1.5 block"
|
className="text-[10px] uppercase tracking-wide text-ink-soft font-semibold mb-1.5 block"
|
||||||
>
|
>
|
||||||
Provider <span aria-hidden="true" className="text-red-400">*</span>
|
Provider <span aria-hidden="true" className="text-bad">*</span>
|
||||||
<span className="sr-only"> (required)</span>
|
<span className="sr-only"> (required)</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
@ -368,7 +368,7 @@ export function ProviderModelSelector({
|
|||||||
disabled={disabled || catalog.length === 0}
|
disabled={disabled || catalog.length === 0}
|
||||||
aria-describedby={selected?.tooltip ? `${providerSelectId}-help` : undefined}
|
aria-describedby={selected?.tooltip ? `${providerSelectId}-help` : undefined}
|
||||||
data-testid="provider-select"
|
data-testid="provider-select"
|
||||||
className="w-full bg-zinc-900 border border-zinc-600 rounded px-2 py-1.5 text-[11px] text-zinc-100 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 transition-colors disabled:opacity-50"
|
className="w-full bg-surface-sunken border border-line rounded px-2 py-1.5 text-[11px] text-ink focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/20 transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<option value="" disabled>
|
<option value="" disabled>
|
||||||
— select provider —
|
— select provider —
|
||||||
@ -382,13 +382,13 @@ export function ProviderModelSelector({
|
|||||||
{selected?.tooltip && (
|
{selected?.tooltip && (
|
||||||
<p
|
<p
|
||||||
id={`${providerSelectId}-help`}
|
id={`${providerSelectId}-help`}
|
||||||
className="text-[9px] text-zinc-500 mt-1 leading-relaxed"
|
className="text-[9px] text-ink-soft mt-1 leading-relaxed"
|
||||||
>
|
>
|
||||||
{selected.tooltip}
|
{selected.tooltip}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{selected && selected.envVars.length > 0 && (
|
{selected && selected.envVars.length > 0 && (
|
||||||
<p className="text-[9px] text-zinc-600 mt-0.5 font-mono">
|
<p className="text-[9px] text-ink-soft mt-0.5 font-mono">
|
||||||
requires: {selected.envVars.join(", ")}
|
requires: {selected.envVars.join(", ")}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -397,9 +397,9 @@ export function ProviderModelSelector({
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor={modelSelectId}
|
htmlFor={modelSelectId}
|
||||||
className="text-[10px] uppercase tracking-wide text-zinc-500 font-semibold mb-1.5 block"
|
className="text-[10px] uppercase tracking-wide text-ink-soft font-semibold mb-1.5 block"
|
||||||
>
|
>
|
||||||
Model <span aria-hidden="true" className="text-red-400">*</span>
|
Model <span aria-hidden="true" className="text-bad">*</span>
|
||||||
<span className="sr-only"> (required)</span>
|
<span className="sr-only"> (required)</span>
|
||||||
</label>
|
</label>
|
||||||
{useTextInput ? (
|
{useTextInput ? (
|
||||||
@ -420,9 +420,9 @@ export function ProviderModelSelector({
|
|||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
data-testid="model-input"
|
data-testid="model-input"
|
||||||
className="w-full bg-zinc-900 border border-zinc-600 rounded px-2 py-1.5 text-[11px] text-zinc-100 font-mono focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 transition-colors disabled:opacity-50"
|
className="w-full bg-surface-sunken border border-line rounded px-2 py-1.5 text-[11px] text-ink font-mono focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/20 transition-colors disabled:opacity-50"
|
||||||
/>
|
/>
|
||||||
<p className="text-[9px] text-zinc-500 mt-1 leading-relaxed">
|
<p className="text-[9px] text-ink-soft mt-1 leading-relaxed">
|
||||||
{selected?.wildcard
|
{selected?.wildcard
|
||||||
? wildcardHelpText(selected)
|
? wildcardHelpText(selected)
|
||||||
: "Free-text model id. Make sure the provider can resolve it."}
|
: "Free-text model id. Make sure the provider can resolve it."}
|
||||||
@ -437,7 +437,7 @@ export function ProviderModelSelector({
|
|||||||
handleModelChange(selected.models[0]?.id ?? "");
|
handleModelChange(selected.models[0]?.id ?? "");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="text-[9px] text-blue-400 hover:text-blue-300 mt-0.5"
|
className="text-[9px] text-accent hover:text-accent mt-0.5"
|
||||||
>
|
>
|
||||||
← back to model list
|
← back to model list
|
||||||
</button>
|
</button>
|
||||||
@ -460,7 +460,7 @@ export function ProviderModelSelector({
|
|||||||
}}
|
}}
|
||||||
disabled={disabled || !selected || selected.models.length === 0}
|
disabled={disabled || !selected || selected.models.length === 0}
|
||||||
data-testid="model-select"
|
data-testid="model-select"
|
||||||
className="w-full bg-zinc-900 border border-zinc-600 rounded px-2 py-1.5 text-[11px] text-zinc-100 font-mono focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 transition-colors disabled:opacity-50"
|
className="w-full bg-surface-sunken border border-line rounded px-2 py-1.5 text-[11px] text-ink font-mono focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/20 transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<option value="" disabled>
|
<option value="" disabled>
|
||||||
{selected ? "— select model —" : "— select provider first —"}
|
{selected ? "— select model —" : "— select provider first —"}
|
||||||
|
|||||||
@ -321,17 +321,17 @@ export function ProvisioningTimeout({
|
|||||||
onClick={() => handleDismiss(entry.workspaceId)}
|
onClick={() => handleDismiss(entry.workspaceId)}
|
||||||
aria-label="Dismiss provisioning timeout warning"
|
aria-label="Dismiss provisioning timeout warning"
|
||||||
title="Dismiss — keep this workspace running without the warning"
|
title="Dismiss — keep this workspace running without the warning"
|
||||||
className="shrink-0 text-amber-400/60 hover:text-amber-200 transition-colors -mr-1"
|
className="shrink-0 text-warm/60 hover:text-amber-200 transition-colors -mr-1"
|
||||||
>
|
>
|
||||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||||
<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[11px] text-amber-300/80 leading-relaxed">
|
<div className="text-[11px] text-warm/80 leading-relaxed">
|
||||||
<span className="font-medium text-amber-200">{entry.workspaceName}</span>{" "}
|
<span className="font-medium text-amber-200">{entry.workspaceName}</span>{" "}
|
||||||
has been provisioning for{" "}
|
has been provisioning for{" "}
|
||||||
<span className="font-mono text-amber-300">{formatDuration(elapsed)}</span>.
|
<span className="font-mono text-warm">{formatDuration(elapsed)}</span>.
|
||||||
It may have encountered an issue.
|
It may have encountered an issue.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -341,7 +341,7 @@ export function ProvisioningTimeout({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleRetry(entry.workspaceId)}
|
onClick={() => handleRetry(entry.workspaceId)}
|
||||||
disabled={isRetrying || isCancelling || retryCooldown.has(entry.workspaceId)}
|
disabled={isRetrying || isCancelling || retryCooldown.has(entry.workspaceId)}
|
||||||
className="px-3 py-1.5 bg-amber-600 hover:bg-amber-500 text-[11px] font-medium rounded-lg text-white disabled:opacity-40 transition-colors"
|
className="px-3 py-1.5 bg-amber-600 hover:bg-amber-500 text-[11px] font-medium rounded-lg text-ink disabled:opacity-40 transition-colors"
|
||||||
>
|
>
|
||||||
{isRetrying ? "Retrying..." : retryCooldown.has(entry.workspaceId) ? "Wait..." : "Retry"}
|
{isRetrying ? "Retrying..." : retryCooldown.has(entry.workspaceId) ? "Wait..." : "Retry"}
|
||||||
</button>
|
</button>
|
||||||
@ -349,14 +349,14 @@ export function ProvisioningTimeout({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleCancelRequest(entry.workspaceId)}
|
onClick={() => handleCancelRequest(entry.workspaceId)}
|
||||||
disabled={isRetrying || isCancelling}
|
disabled={isRetrying || isCancelling}
|
||||||
className="px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-[11px] text-zinc-300 rounded-lg border border-zinc-600 disabled:opacity-40 transition-colors"
|
className="px-3 py-1.5 bg-surface-card hover:bg-surface-card text-[11px] text-ink-mid rounded-lg border border-line disabled:opacity-40 transition-colors"
|
||||||
>
|
>
|
||||||
{isCancelling ? "Cancelling..." : "Cancel"}
|
{isCancelling ? "Cancelling..." : "Cancel"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleViewLogs(entry.workspaceId)}
|
onClick={() => handleViewLogs(entry.workspaceId)}
|
||||||
className="px-3 py-1.5 text-[11px] text-amber-400 hover:text-amber-300 transition-colors"
|
className="px-3 py-1.5 text-[11px] text-warm hover:text-warm transition-colors"
|
||||||
>
|
>
|
||||||
View Logs
|
View Logs
|
||||||
</button>
|
</button>
|
||||||
@ -371,25 +371,25 @@ export function ProvisioningTimeout({
|
|||||||
{confirmingCancel && (
|
{confirmingCancel && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
<div aria-hidden="true" className="absolute inset-0 bg-black/60" onClick={() => setConfirmingCancel(null)} />
|
<div aria-hidden="true" className="absolute inset-0 bg-black/60" onClick={() => setConfirmingCancel(null)} />
|
||||||
<div className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl p-5 max-w-[340px] w-full mx-4">
|
<div className="relative bg-surface-sunken border border-line rounded-xl shadow-2xl p-5 max-w-[340px] w-full mx-4">
|
||||||
<h3 className="text-sm font-semibold text-zinc-100 mb-2">
|
<h3 className="text-sm font-semibold text-ink mb-2">
|
||||||
Cancel deployment?
|
Cancel deployment?
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[12px] text-zinc-400 mb-4 leading-relaxed">
|
<p className="text-[12px] text-ink-mid mb-4 leading-relaxed">
|
||||||
This will permanently remove the workspace. This action cannot be undone.
|
This will permanently remove the workspace. This action cannot be undone.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setConfirmingCancel(null)}
|
onClick={() => setConfirmingCancel(null)}
|
||||||
className="px-3.5 py-1.5 text-[12px] text-zinc-400 hover:text-zinc-200 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg transition-colors"
|
className="px-3.5 py-1.5 text-[12px] text-ink-mid hover:text-ink bg-surface-card hover:bg-surface-card border border-line rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
Keep
|
Keep
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleCancelConfirm}
|
onClick={handleCancelConfirm}
|
||||||
className="px-3.5 py-1.5 text-[12px] bg-red-600 hover:bg-red-500 text-white rounded-lg transition-colors"
|
className="px-3.5 py-1.5 text-[12px] bg-red-600 hover:bg-red-500 text-ink rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
Remove Workspace
|
Remove Workspace
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -92,12 +92,12 @@ export function SearchDialog() {
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Search workspaces"
|
aria-label="Search workspaces"
|
||||||
className="w-[420px] bg-zinc-950/95 backdrop-blur-xl border border-zinc-800/60 rounded-2xl shadow-2xl shadow-black/50 overflow-hidden"
|
className="w-[420px] bg-surface/95 backdrop-blur-xl border border-line/60 rounded-2xl shadow-2xl shadow-black/50 overflow-hidden"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Search input */}
|
{/* Search input */}
|
||||||
<div className="flex items-center gap-3 px-4 py-3 border-b border-zinc-800/40">
|
<div className="flex items-center gap-3 px-4 py-3 border-b border-line/40">
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="shrink-0 text-zinc-500" aria-hidden="true">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="shrink-0 text-ink-soft" aria-hidden="true">
|
||||||
<circle cx="7" cy="7" r="5.5" stroke="currentColor" strokeWidth="1.5" />
|
<circle cx="7" cy="7" r="5.5" stroke="currentColor" strokeWidth="1.5" />
|
||||||
<path d="M11 11l3.5 3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
<path d="M11 11l3.5 3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -113,9 +113,9 @@ export function SearchDialog() {
|
|||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
onKeyDown={handleInputKeyDown}
|
onKeyDown={handleInputKeyDown}
|
||||||
placeholder="Search workspaces..."
|
placeholder="Search workspaces..."
|
||||||
className="flex-1 bg-transparent text-sm text-zinc-100 placeholder-zinc-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus:outline-none rounded"
|
className="flex-1 bg-transparent text-sm text-ink placeholder-zinc-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus:outline-none rounded"
|
||||||
/>
|
/>
|
||||||
<kbd className="text-[9px] text-zinc-400 bg-zinc-800/60 px-1.5 py-0.5 rounded border border-zinc-700/40">ESC</kbd>
|
<kbd className="text-[9px] text-ink-mid bg-surface-card/60 px-1.5 py-0.5 rounded border border-line/40">ESC</kbd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Results */}
|
{/* Results */}
|
||||||
@ -126,7 +126,7 @@ export function SearchDialog() {
|
|||||||
className="max-h-[300px] overflow-y-auto py-1"
|
className="max-h-[300px] overflow-y-auto py-1"
|
||||||
>
|
>
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<div role="status" aria-live="polite" className="px-4 py-6 text-center text-xs text-zinc-400">
|
<div role="status" aria-live="polite" className="px-4 py-6 text-center text-xs text-ink-mid">
|
||||||
{query ? "No workspaces match" : "No workspaces yet"}
|
{query ? "No workspaces match" : "No workspaces yet"}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -139,7 +139,7 @@ export function SearchDialog() {
|
|||||||
aria-selected={index === focusedIndex}
|
aria-selected={index === focusedIndex}
|
||||||
onClick={() => handleSelect(node.id)}
|
onClick={() => handleSelect(node.id)}
|
||||||
className={`w-full px-4 py-2.5 flex items-center gap-3 text-left transition-colors ${
|
className={`w-full px-4 py-2.5 flex items-center gap-3 text-left transition-colors ${
|
||||||
index === focusedIndex ? "bg-zinc-800/60" : "hover:bg-zinc-800/40"
|
index === focusedIndex ? "bg-surface-card/60" : "hover:bg-surface-card/40"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -147,13 +147,13 @@ export function SearchDialog() {
|
|||||||
className={`w-2 h-2 rounded-full shrink-0 ${statusDotClass(node.data.status)}`}
|
className={`w-2 h-2 rounded-full shrink-0 ${statusDotClass(node.data.status)}`}
|
||||||
/>
|
/>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-sm text-zinc-200 truncate">{node.data.name}</div>
|
<div className="text-sm text-ink truncate">{node.data.name}</div>
|
||||||
{node.data.role && (
|
{node.data.role && (
|
||||||
<div className="text-[10px] text-zinc-500 truncate">{node.data.role}</div>
|
<div className="text-[10px] text-ink-soft truncate">{node.data.role}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className="text-[9px] font-mono text-zinc-400"
|
className="text-[9px] font-mono text-ink-mid"
|
||||||
aria-label={`Tier ${node.data.tier}`}
|
aria-label={`Tier ${node.data.tier}`}
|
||||||
>
|
>
|
||||||
T{node.data.tier}
|
T{node.data.tier}
|
||||||
@ -164,11 +164,11 @@ export function SearchDialog() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="px-4 py-2 border-t border-zinc-800/40 flex items-center justify-between">
|
<div className="px-4 py-2 border-t border-line/40 flex items-center justify-between">
|
||||||
<span className="text-[9px] text-zinc-400">{filtered.length} workspace{filtered.length !== 1 ? "s" : ""}</span>
|
<span className="text-[9px] text-ink-mid">{filtered.length} workspace{filtered.length !== 1 ? "s" : ""}</span>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<kbd className="text-[9px] text-zinc-400 bg-zinc-800/60 px-1.5 py-0.5 rounded border border-zinc-700/40">↑↓ navigate</kbd>
|
<kbd className="text-[9px] text-ink-mid bg-surface-card/60 px-1.5 py-0.5 rounded border border-line/40">↑↓ navigate</kbd>
|
||||||
<kbd className="text-[9px] text-zinc-400 bg-zinc-800/60 px-1.5 py-0.5 rounded border border-zinc-700/40">↵ select</kbd>
|
<kbd className="text-[9px] text-ink-mid bg-surface-card/60 px-1.5 py-0.5 rounded border border-line/40">↵ select</kbd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -137,7 +137,7 @@ export function SidePanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed top-0 right-0 h-full bg-zinc-950/95 backdrop-blur-xl border-l border-zinc-800/50 flex flex-col z-50 shadow-2xl shadow-black/50 animate-in slide-in-from-right duration-200"
|
className="fixed top-0 right-0 h-full bg-surface/95 backdrop-blur-xl border-l border-line/50 flex flex-col z-50 shadow-2xl shadow-black/50 animate-in slide-in-from-right duration-200"
|
||||||
style={{ width }}
|
style={{ width }}
|
||||||
>
|
>
|
||||||
{/* Resize handle */}
|
{/* Resize handle */}
|
||||||
@ -151,26 +151,26 @@ export function SidePanel() {
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onKeyDown={onResizeKeyDown}
|
onKeyDown={onResizeKeyDown}
|
||||||
className="absolute left-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-blue-500/30 active:bg-blue-500/50 transition-colors z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-inset"
|
className="absolute left-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-accent/30 active:bg-accent/50 transition-colors z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-inset"
|
||||||
/>
|
/>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-5 py-4 border-b border-zinc-800/40 bg-zinc-900/30">
|
<div className="flex items-center justify-between px-5 py-4 border-b border-line/40 bg-surface-sunken/30">
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<StatusDot status={node.data.status} size="md" />
|
<StatusDot status={node.data.status} size="md" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h2 className="text-[14px] font-semibold text-zinc-100 truncate leading-tight">
|
<h2 className="text-[14px] font-semibold text-ink truncate leading-tight">
|
||||||
{node.data.name}
|
{node.data.name}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex items-center gap-2 mt-0.5">
|
<div className="flex items-center gap-2 mt-0.5">
|
||||||
{node.data.role && (
|
{node.data.role && (
|
||||||
<span className="text-[10px] text-zinc-500 truncate">
|
<span className="text-[10px] text-ink-soft truncate">
|
||||||
{node.data.role}
|
{node.data.role}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className={`text-[9px] px-1.5 py-0.5 rounded-md font-mono ${
|
<span className={`text-[9px] px-1.5 py-0.5 rounded-md font-mono ${
|
||||||
isOnline ? "text-emerald-400 bg-emerald-950/30" : "text-zinc-500 bg-zinc-800/50"
|
isOnline ? "text-good bg-emerald-950/30" : "text-ink-soft bg-surface-card/50"
|
||||||
}`}>
|
}`}>
|
||||||
T{node.data.tier}
|
T{node.data.tier}
|
||||||
</span>
|
</span>
|
||||||
@ -181,7 +181,7 @@ export function SidePanel() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => selectNode(null)}
|
onClick={() => selectNode(null)}
|
||||||
aria-label="Close workspace panel"
|
aria-label="Close workspace panel"
|
||||||
className="w-7 h-7 flex items-center justify-center rounded-lg text-zinc-500 hover:text-zinc-200 hover:bg-zinc-800/60 transition-colors"
|
className="w-7 h-7 flex items-center justify-center rounded-lg text-ink-soft hover:text-ink hover:bg-surface-card/60 transition-colors"
|
||||||
>
|
>
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||||
<path d="M1 1l10 10M11 1L1 11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
<path d="M1 1l10 10M11 1L1 11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||||
@ -190,7 +190,7 @@ export function SidePanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Capability summary */}
|
{/* Capability summary */}
|
||||||
<div className="px-5 py-3 border-b border-zinc-800/40 bg-zinc-900/20">
|
<div className="px-5 py-3 border-b border-line/40 bg-surface-sunken/20">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<MetaPill label="Tier" value={`T${node.data.tier}`} />
|
<MetaPill label="Tier" value={`T${node.data.tier}`} />
|
||||||
<MetaPill label="Runtime" value={capability.runtime || "unknown"} />
|
<MetaPill label="Runtime" value={capability.runtime || "unknown"} />
|
||||||
@ -200,13 +200,13 @@ export function SidePanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs — relative wrapper lets the fade gradient position against the scroll container */}
|
{/* Tabs — relative wrapper lets the fade gradient position against the scroll container */}
|
||||||
<div className="relative border-b border-zinc-800/40">
|
<div className="relative border-b border-line/40">
|
||||||
{/* Right-edge fade: signals more tabs are hidden off-screen when the bar overflows */}
|
{/* Right-edge fade: signals more tabs are hidden off-screen when the bar overflows */}
|
||||||
<div className="pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-zinc-950 to-transparent z-10" aria-hidden="true" />
|
<div className="pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-zinc-950 to-transparent z-10" aria-hidden="true" />
|
||||||
<div
|
<div
|
||||||
role="tablist"
|
role="tablist"
|
||||||
aria-label="Workspace panel tabs"
|
aria-label="Workspace panel tabs"
|
||||||
className="flex overflow-x-auto bg-zinc-900/20 px-1"
|
className="flex overflow-x-auto bg-surface-sunken/20 px-1"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
const idx = TABS.findIndex((t) => t.id === panelTab);
|
const idx = TABS.findIndex((t) => t.id === panelTab);
|
||||||
let next: number | null = null;
|
let next: number | null = null;
|
||||||
@ -230,10 +230,10 @@ export function SidePanel() {
|
|||||||
aria-controls={`panel-${tab.id}`}
|
aria-controls={`panel-${tab.id}`}
|
||||||
tabIndex={panelTab === tab.id ? 0 : -1}
|
tabIndex={panelTab === tab.id ? 0 : -1}
|
||||||
onClick={() => setPanelTab(tab.id)}
|
onClick={() => setPanelTab(tab.id)}
|
||||||
className={`shrink-0 px-3 py-2.5 text-[10px] font-medium tracking-wide transition-all rounded-t-lg mx-0.5 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/70 ${
|
className={`shrink-0 px-3 py-2.5 text-[10px] font-medium tracking-wide transition-all rounded-t-lg mx-0.5 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/70 ${
|
||||||
panelTab === tab.id
|
panelTab === tab.id
|
||||||
? "text-zinc-100 bg-zinc-800/40 border-b-2 border-blue-500"
|
? "text-ink bg-surface-card/40 border-b-2 border-accent"
|
||||||
: "text-zinc-500 hover:text-zinc-200 hover:bg-zinc-800/40"
|
: "text-ink-soft hover:text-ink hover:bg-surface-card/40"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="mr-1 opacity-50" aria-hidden="true">{tab.icon}</span>
|
<span className="mr-1 opacity-50" aria-hidden="true">{tab.icon}</span>
|
||||||
@ -264,7 +264,7 @@ export function SidePanel() {
|
|||||||
<Tooltip text={node.data.currentTask as string}>
|
<Tooltip text={node.data.currentTask as string}>
|
||||||
<div className="px-4 py-2 bg-amber-950/20 border-b border-amber-800/20 flex items-center gap-2 cursor-default">
|
<div className="px-4 py-2 bg-amber-950/20 border-b border-amber-800/20 flex items-center gap-2 cursor-default">
|
||||||
<div className="w-1.5 h-1.5 rounded-full bg-amber-400 motion-safe:animate-pulse shrink-0" />
|
<div className="w-1.5 h-1.5 rounded-full bg-amber-400 motion-safe:animate-pulse shrink-0" />
|
||||||
<span className="text-[10px] text-amber-300/90 truncate">
|
<span className="text-[10px] text-warm/90 truncate">
|
||||||
{node.data.currentTask}
|
{node.data.currentTask}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -295,8 +295,8 @@ export function SidePanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer — workspace ID */}
|
{/* Footer — workspace ID */}
|
||||||
<div className="px-5 py-2 border-t border-zinc-800/40 bg-zinc-900/20">
|
<div className="px-5 py-2 border-t border-line/40 bg-surface-sunken/20">
|
||||||
<span className="text-[9px] font-mono text-zinc-500 select-all">
|
<span className="text-[9px] font-mono text-ink-soft select-all">
|
||||||
{selectedNodeId}
|
{selectedNodeId}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -306,9 +306,9 @@ export function SidePanel() {
|
|||||||
|
|
||||||
function MetaPill({ label, value, tone = "zinc" }: { label: string; value: string; tone?: "zinc" | "emerald" | "amber" }) {
|
function MetaPill({ label, value, tone = "zinc" }: { label: string; value: string; tone?: "zinc" | "emerald" | "amber" }) {
|
||||||
const toneClasses = {
|
const toneClasses = {
|
||||||
zinc: "border-zinc-700/50 bg-zinc-900/70 text-zinc-400",
|
zinc: "border-line/50 bg-surface-sunken/70 text-ink-mid",
|
||||||
emerald: "border-emerald-500/20 bg-emerald-950/20 text-emerald-300",
|
emerald: "border-emerald-500/20 bg-emerald-950/20 text-good",
|
||||||
amber: "border-amber-500/20 bg-amber-950/20 text-amber-300",
|
amber: "border-amber-500/20 bg-amber-950/20 text-warm",
|
||||||
}[tone];
|
}[tone];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -236,7 +236,7 @@ export function OrgTemplatesSection() {
|
|||||||
onClick={() => setExpanded((v) => !v)}
|
onClick={() => setExpanded((v) => !v)}
|
||||||
aria-expanded={expanded}
|
aria-expanded={expanded}
|
||||||
aria-controls="org-templates-body"
|
aria-controls="org-templates-body"
|
||||||
className="flex items-center gap-1.5 text-[10px] uppercase tracking-wide text-zinc-500 hover:text-zinc-300 font-semibold transition-colors"
|
className="flex items-center gap-1.5 text-[10px] uppercase tracking-wide text-ink-soft hover:text-ink-mid font-semibold transition-colors"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@ -246,7 +246,7 @@ export function OrgTemplatesSection() {
|
|||||||
</span>
|
</span>
|
||||||
Org Templates
|
Org Templates
|
||||||
{orgs.length > 0 && (
|
{orgs.length > 0 && (
|
||||||
<span className="text-zinc-600 normal-case tracking-normal">
|
<span className="text-ink-soft normal-case tracking-normal">
|
||||||
({orgs.length})
|
({orgs.length})
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -255,7 +255,7 @@ export function OrgTemplatesSection() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={loadOrgs}
|
onClick={loadOrgs}
|
||||||
aria-label="Refresh org templates"
|
aria-label="Refresh org templates"
|
||||||
className="text-[10px] text-zinc-500 hover:text-zinc-300"
|
className="text-[10px] text-ink-soft hover:text-ink-mid"
|
||||||
>
|
>
|
||||||
↻
|
↻
|
||||||
</button>
|
</button>
|
||||||
@ -264,20 +264,20 @@ export function OrgTemplatesSection() {
|
|||||||
{expanded && (
|
{expanded && (
|
||||||
<div id="org-templates-body" className="space-y-2">
|
<div id="org-templates-body" className="space-y-2">
|
||||||
{loading && (
|
{loading && (
|
||||||
<div role="status" aria-live="polite" className="flex items-center gap-1.5 text-[10px] text-zinc-500">
|
<div role="status" aria-live="polite" className="flex items-center gap-1.5 text-[10px] text-ink-soft">
|
||||||
<Spinner size="sm" />
|
<Spinner size="sm" />
|
||||||
Loading…
|
Loading…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && orgs.length === 0 && (
|
{!loading && orgs.length === 0 && (
|
||||||
<div className="text-[10px] text-zinc-500">
|
<div className="text-[10px] text-ink-soft">
|
||||||
No org templates in <code>org-templates/</code>
|
No org templates in <code>org-templates/</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="px-2 py-1 bg-red-950/40 border border-red-800/50 rounded text-[10px] text-red-400">
|
<div className="px-2 py-1 bg-red-950/40 border border-red-800/50 rounded text-[10px] text-bad">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -287,10 +287,10 @@ export function OrgTemplatesSection() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={o.dir}
|
key={o.dir}
|
||||||
className="bg-zinc-900/50 border border-zinc-800/60 rounded-xl p-3 hover:border-zinc-700/60 transition-all"
|
className="bg-surface-sunken/50 border border-line/60 rounded-xl p-3 hover:border-line/60 transition-all"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<span className="text-[12px] font-semibold text-zinc-200 truncate">
|
<span className="text-[12px] font-semibold text-ink truncate">
|
||||||
{o.name || o.dir}
|
{o.name || o.dir}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[9px] font-mono text-sky-400 bg-sky-950/40 px-1.5 py-0.5 rounded-md shrink-0">
|
<span className="text-[9px] font-mono text-sky-400 bg-sky-950/40 px-1.5 py-0.5 rounded-md shrink-0">
|
||||||
@ -298,7 +298,7 @@ export function OrgTemplatesSection() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{o.description && (
|
{o.description && (
|
||||||
<p className="text-[10px] text-zinc-500 mb-2.5 line-clamp-2 leading-relaxed">
|
<p className="text-[10px] text-ink-soft mb-2.5 line-clamp-2 leading-relaxed">
|
||||||
{o.description}
|
{o.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -306,7 +306,7 @@ export function OrgTemplatesSection() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleImport(o)}
|
onClick={() => handleImport(o)}
|
||||||
disabled={isImporting}
|
disabled={isImporting}
|
||||||
className="w-full px-2 py-1.5 bg-blue-600/20 hover:bg-blue-600/30 border border-blue-500/30 rounded-lg text-[10px] text-blue-300 font-medium transition-colors disabled:opacity-50"
|
className="w-full px-2 py-1.5 bg-accent-strong/20 hover:bg-accent-strong/30 border border-accent/30 rounded-lg text-[10px] text-accent font-medium transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{isImporting ? "Importing…" : "Import org"}
|
{isImporting ? "Importing…" : "Import org"}
|
||||||
</button>
|
</button>
|
||||||
@ -411,7 +411,7 @@ function ImportAgentButton({ onImported }: { onImported: () => void }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
disabled={importing}
|
disabled={importing}
|
||||||
className="w-full px-3 py-2 bg-blue-600/20 hover:bg-blue-600/30 border border-blue-500/30 rounded-lg text-[11px] text-blue-300 font-medium transition-colors disabled:opacity-50"
|
className="w-full px-3 py-2 bg-accent-strong/20 hover:bg-accent-strong/30 border border-accent/30 rounded-lg text-[11px] text-accent font-medium transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{importing ? "Importing..." : "Import Agent Folder"}
|
{importing ? "Importing..." : "Import Agent Folder"}
|
||||||
</button>
|
</button>
|
||||||
@ -476,8 +476,8 @@ export function TemplatePalette() {
|
|||||||
onClick={() => setOpen(!open)}
|
onClick={() => setOpen(!open)}
|
||||||
className={`fixed top-4 left-4 z-40 w-9 h-9 flex items-center justify-center rounded-lg transition-colors ${
|
className={`fixed top-4 left-4 z-40 w-9 h-9 flex items-center justify-center rounded-lg transition-colors ${
|
||||||
open
|
open
|
||||||
? "bg-blue-600 text-white"
|
? "bg-accent-strong text-ink"
|
||||||
: "bg-zinc-900/90 border border-zinc-700/50 text-zinc-400 hover:text-zinc-200 hover:border-zinc-600"
|
: "bg-surface-sunken/90 border border-line/50 text-ink-mid hover:text-ink hover:border-line"
|
||||||
}`}
|
}`}
|
||||||
title="Template Palette"
|
title="Template Palette"
|
||||||
aria-label={open ? "Close template palette" : "Open template palette"}
|
aria-label={open ? "Close template palette" : "Open template palette"}
|
||||||
@ -496,10 +496,10 @@ export function TemplatePalette() {
|
|||||||
|
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
{open && (
|
{open && (
|
||||||
<div className="fixed top-0 left-0 h-full w-[280px] bg-zinc-900/95 backdrop-blur-md border-r border-zinc-800/60 z-30 flex flex-col shadow-2xl shadow-black/40">
|
<div className="fixed top-0 left-0 h-full w-[280px] bg-surface-sunken/95 backdrop-blur-md border-r border-line/60 z-30 flex flex-col shadow-2xl shadow-black/40">
|
||||||
<div className="px-4 pt-14 pb-3 border-b border-zinc-800/60">
|
<div className="px-4 pt-14 pb-3 border-b border-line/60">
|
||||||
<h2 className="text-sm font-semibold text-zinc-100">Templates</h2>
|
<h2 className="text-sm font-semibold text-ink">Templates</h2>
|
||||||
<p className="text-[10px] text-zinc-500 mt-0.5">Click to deploy a workspace</p>
|
<p className="text-[10px] text-ink-soft mt-0.5">Click to deploy a workspace</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-3 space-y-2">
|
<div className="flex-1 overflow-y-auto p-3 space-y-2">
|
||||||
@ -509,20 +509,20 @@ export function TemplatePalette() {
|
|||||||
<OrgTemplatesSection />
|
<OrgTemplatesSection />
|
||||||
|
|
||||||
{loading && (
|
{loading && (
|
||||||
<div role="status" aria-live="polite" className="flex items-center justify-center gap-2 text-xs text-zinc-500 text-center py-8">
|
<div role="status" aria-live="polite" className="flex items-center justify-center gap-2 text-xs text-ink-soft text-center py-8">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
Loading…
|
Loading…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && templates.length === 0 && (
|
{!loading && templates.length === 0 && (
|
||||||
<div role="status" aria-live="polite" className="text-xs text-zinc-500 text-center py-8">
|
<div role="status" aria-live="polite" className="text-xs text-ink-soft text-center py-8">
|
||||||
No templates found in<br />workspace-configs-templates/
|
No templates found in<br />workspace-configs-templates/
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="px-3 py-1.5 bg-red-950/40 border border-red-800/50 rounded-lg text-xs text-red-400">
|
<div className="px-3 py-1.5 bg-red-950/40 border border-red-800/50 rounded-lg text-xs text-bad">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -537,10 +537,10 @@ export function TemplatePalette() {
|
|||||||
key={t.id}
|
key={t.id}
|
||||||
onClick={() => void handleDeploy(t)}
|
onClick={() => void handleDeploy(t)}
|
||||||
disabled={isDeploying}
|
disabled={isDeploying}
|
||||||
className="w-full text-left bg-zinc-800/40 hover:bg-zinc-800/70 border border-zinc-700/40 hover:border-zinc-600/50 rounded-xl p-3 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-zinc-800/40 disabled:hover:border-zinc-700/40 group focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/70"
|
className="w-full text-left bg-surface-card/40 hover:bg-surface-card/70 border border-line/40 hover:border-line/50 rounded-xl p-3 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-surface-card/40 disabled:hover:border-line/40 group focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/70"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<span className="text-[12px] font-semibold text-zinc-200 group-hover:text-zinc-100 truncate">
|
<span className="text-[12px] font-semibold text-ink group-hover:text-ink truncate">
|
||||||
{t.name}
|
{t.name}
|
||||||
</span>
|
</span>
|
||||||
<span className={`text-[9px] font-mono px-1.5 py-0.5 rounded-md shrink-0 ${tierCfg.color}`}>
|
<span className={`text-[9px] font-mono px-1.5 py-0.5 rounded-md shrink-0 ${tierCfg.color}`}>
|
||||||
@ -549,7 +549,7 @@ export function TemplatePalette() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{t.description && (
|
{t.description && (
|
||||||
<p className="text-[10px] text-zinc-500 mb-2 line-clamp-2 leading-relaxed">
|
<p className="text-[10px] text-ink-soft mb-2 line-clamp-2 leading-relaxed">
|
||||||
{t.description}
|
{t.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -557,12 +557,12 @@ export function TemplatePalette() {
|
|||||||
{t.skills?.length > 0 && (
|
{t.skills?.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{t.skills.slice(0, 3).map((s) => (
|
{t.skills.slice(0, 3).map((s) => (
|
||||||
<span key={s} className="text-[8px] text-zinc-400 bg-zinc-700/40 px-1.5 py-0.5 rounded">
|
<span key={s} className="text-[8px] text-ink-mid bg-surface-card/40 px-1.5 py-0.5 rounded">
|
||||||
{s}
|
{s}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{t.skills.length > 3 && (
|
{t.skills.length > 3 && (
|
||||||
<span className="text-[8px] text-zinc-500">+{t.skills.length - 3}</span>
|
<span className="text-[8px] text-ink-soft">+{t.skills.length - 3}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -575,12 +575,12 @@ export function TemplatePalette() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-4 py-3 border-t border-zinc-800/60 space-y-3">
|
<div className="px-4 py-3 border-t border-line/60 space-y-3">
|
||||||
<ImportAgentButton onImported={loadTemplates} />
|
<ImportAgentButton onImported={loadTemplates} />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={loadTemplates}
|
onClick={loadTemplates}
|
||||||
className="text-[10px] text-zinc-500 hover:text-zinc-300 transition-colors block"
|
className="text-[10px] text-ink-soft hover:text-ink-mid transition-colors block"
|
||||||
>
|
>
|
||||||
Refresh templates
|
Refresh templates
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -77,15 +77,15 @@ export function TermsGate({ children }: { children: React.ReactNode }) {
|
|||||||
<>
|
<>
|
||||||
{children}
|
{children}
|
||||||
{status === "pending" && (
|
{status === "pending" && (
|
||||||
<div aria-hidden="true" className="fixed inset-0 z-50 flex items-center justify-center bg-zinc-950/80 backdrop-blur-sm">
|
<div aria-hidden="true" className="fixed inset-0 z-50 flex items-center justify-center bg-surface/80 backdrop-blur-sm">
|
||||||
<div
|
<div
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="terms-dialog-title"
|
aria-labelledby="terms-dialog-title"
|
||||||
className="mx-4 max-w-lg rounded-lg border border-zinc-700 bg-zinc-900 p-6 shadow-xl"
|
className="mx-4 max-w-lg rounded-lg border border-line bg-surface-sunken p-6 shadow-xl"
|
||||||
>
|
>
|
||||||
<h2 id="terms-dialog-title" className="text-lg font-semibold text-white">Terms & conditions</h2>
|
<h2 id="terms-dialog-title" className="text-lg font-semibold text-ink">Terms & conditions</h2>
|
||||||
<p className="mt-3 text-sm text-zinc-300">
|
<p className="mt-3 text-sm text-ink-mid">
|
||||||
Before you create an organization, please review our{" "}
|
Before you create an organization, please review our{" "}
|
||||||
<a href="/legal/terms" className="text-sky-400 underline" target="_blank" rel="noreferrer">
|
<a href="/legal/terms" className="text-sky-400 underline" target="_blank" rel="noreferrer">
|
||||||
Terms of Service
|
Terms of Service
|
||||||
@ -96,16 +96,16 @@ export function TermsGate({ children }: { children: React.ReactNode }) {
|
|||||||
</a>
|
</a>
|
||||||
. Click agree to continue.
|
. Click agree to continue.
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-3 text-xs text-zinc-500">
|
<p className="mt-3 text-xs text-ink-soft">
|
||||||
By agreeing you acknowledge that workspace data is stored in AWS us-east-2 (Ohio, United States).
|
By agreeing you acknowledge that workspace data is stored in AWS us-east-2 (Ohio, United States).
|
||||||
</p>
|
</p>
|
||||||
{error && <p role="alert" className="mt-3 text-sm text-red-400">{error}</p>}
|
{error && <p role="alert" className="mt-3 text-sm text-bad">{error}</p>}
|
||||||
<div className="mt-5 flex justify-end gap-2">
|
<div className="mt-5 flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={accept}
|
onClick={accept}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="rounded bg-emerald-600 px-4 py-2 text-sm font-medium text-white hover:bg-emerald-500 disabled:opacity-50"
|
className="rounded bg-emerald-600 px-4 py-2 text-sm font-medium text-ink hover:bg-emerald-500 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{submitting ? "Saving…" : "I agree"}
|
{submitting ? "Saving…" : "I agree"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
81
canvas/src/components/ThemeToggle.tsx
Normal file
81
canvas/src/components/ThemeToggle.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTheme, type ThemePreference } from "@/lib/theme-provider";
|
||||||
|
|
||||||
|
const OPTIONS: { value: ThemePreference; label: string; icon: string }[] = [
|
||||||
|
// Sun: explicit light
|
||||||
|
{
|
||||||
|
value: "light",
|
||||||
|
label: "Light",
|
||||||
|
icon: "M12 3v1.5M12 19.5V21M4.22 4.22l1.06 1.06M18.72 18.72l1.06 1.06M3 12h1.5M19.5 12H21M4.22 19.78l1.06-1.06M18.72 5.28l1.06-1.06M16 12a4 4 0 11-8 0 4 4 0 018 0z",
|
||||||
|
},
|
||||||
|
// Monitor: follow OS
|
||||||
|
{
|
||||||
|
value: "system",
|
||||||
|
label: "System",
|
||||||
|
icon: "M3 5h18v11H3zM8 21h8M9 21l1-5h4l1 5",
|
||||||
|
},
|
||||||
|
// Moon: explicit dark
|
||||||
|
{
|
||||||
|
value: "dark",
|
||||||
|
label: "Dark",
|
||||||
|
icon: "M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Three-way preference picker: System / Light / Dark.
|
||||||
|
*
|
||||||
|
* Highlights the user's *picked* preference, not the resolved render
|
||||||
|
* mode. So "System" stays highlighted while the screen renders dark
|
||||||
|
* (because the OS is dark) — that's the user's mental model: "I told
|
||||||
|
* the app to follow my OS."
|
||||||
|
*
|
||||||
|
* Aligned with molecule-app/components/theme-toggle.tsx so the picker
|
||||||
|
* behaves identically across surfaces.
|
||||||
|
*/
|
||||||
|
export function ThemeToggle({ className = "" }: { className?: string }) {
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-label="Theme preference"
|
||||||
|
className={`inline-flex items-center gap-0.5 rounded-md border border-line bg-surface-sunken p-0.5 ${className}`}
|
||||||
|
>
|
||||||
|
{OPTIONS.map((opt) => {
|
||||||
|
const active = theme === opt.value;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={active}
|
||||||
|
aria-label={opt.label}
|
||||||
|
onClick={() => setTheme(opt.value)}
|
||||||
|
className={
|
||||||
|
"flex h-6 w-6 items-center justify-center rounded transition-colors " +
|
||||||
|
(active
|
||||||
|
? "bg-surface-elevated text-ink shadow-sm"
|
||||||
|
: "text-ink-soft hover:text-ink-mid")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width={13}
|
||||||
|
height={13}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.6"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d={opt.icon} />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -44,7 +44,7 @@ export function Toaster() {
|
|||||||
? "bg-emerald-950/90 border border-emerald-700/40 text-emerald-200"
|
? "bg-emerald-950/90 border border-emerald-700/40 text-emerald-200"
|
||||||
: type === "error"
|
: type === "error"
|
||||||
? "bg-red-950/90 border border-red-700/40 text-red-200"
|
? "bg-red-950/90 border border-red-700/40 text-red-200"
|
||||||
: "bg-zinc-900/90 border border-zinc-700/40 text-zinc-200"
|
: "bg-surface-sunken/90 border border-line/40 text-ink"
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const pos =
|
const pos =
|
||||||
@ -66,7 +66,7 @@ export function Toaster() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => dismiss(toast.id)}
|
onClick={() => dismiss(toast.id)}
|
||||||
aria-label="Dismiss notification"
|
aria-label="Dismiss notification"
|
||||||
className="ml-1 p-1 rounded hover:bg-zinc-700/50 transition-colors opacity-70 hover:opacity-100 shrink-0"
|
className="ml-1 p-1 rounded hover:bg-surface-card/50 transition-colors opacity-70 hover:opacity-100 shrink-0"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
@ -94,7 +94,7 @@ export function Toaster() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => dismiss(toast.id)}
|
onClick={() => dismiss(toast.id)}
|
||||||
aria-label="Dismiss notification"
|
aria-label="Dismiss notification"
|
||||||
className="ml-1 p-1 rounded hover:bg-zinc-700/50 transition-colors opacity-70 hover:opacity-100 shrink-0"
|
className="ml-1 p-1 rounded hover:bg-surface-card/50 transition-colors opacity-70 hover:opacity-100 shrink-0"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { SettingsButton } from "@/components/settings/SettingsButton";
|
|||||||
import { settingsGearRef } from "@/components/settings/SettingsPanel";
|
import { settingsGearRef } from "@/components/settings/SettingsPanel";
|
||||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||||
import { showToast } from "@/components/Toaster";
|
import { showToast } from "@/components/Toaster";
|
||||||
|
import { ThemeToggle } from "@/components/ThemeToggle";
|
||||||
import { statusDotClass } from "@/lib/design-tokens";
|
import { statusDotClass } from "@/lib/design-tokens";
|
||||||
|
|
||||||
export function Toolbar() {
|
export function Toolbar() {
|
||||||
@ -128,13 +129,13 @@ export function Toolbar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed top-3 left-1/2 -translate-x-1/2 z-20 flex items-center gap-3 bg-zinc-900/80 backdrop-blur-md border border-zinc-800/60 rounded-xl px-4 py-2 shadow-xl shadow-black/20 transition-[margin-left] duration-200"
|
className="fixed top-3 left-1/2 -translate-x-1/2 z-20 flex items-center gap-3 bg-surface-sunken/80 backdrop-blur-md border border-line/60 rounded-xl px-4 py-2 shadow-xl shadow-black/20 transition-[margin-left] duration-200"
|
||||||
style={toolbarOffsetStyle}
|
style={toolbarOffsetStyle}
|
||||||
>
|
>
|
||||||
{/* Logo / Title */}
|
{/* Logo / Title */}
|
||||||
<div className="flex items-center gap-2 pr-3 border-r border-zinc-800/60">
|
<div className="flex items-center gap-2 pr-3 border-r border-line/60">
|
||||||
<img src="/molecule-icon.png" alt="Molecule AI" className="w-5 h-5" />
|
<img src="/molecule-icon.png" alt="Molecule AI" className="w-5 h-5" />
|
||||||
<span className="text-[11px] font-semibold text-zinc-300 tracking-wide">Molecule AI</span>
|
<span className="text-[11px] font-semibold text-ink-mid tracking-wide">Molecule AI</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status pills + workspace total in one segment — previously two
|
{/* Status pills + workspace total in one segment — previously two
|
||||||
@ -154,14 +155,14 @@ export function Toolbar() {
|
|||||||
<StatusPill color={statusDotClass("failed")} count={counts.failed} label="failed" />
|
<StatusPill color={statusDotClass("failed")} count={counts.failed} label="failed" />
|
||||||
)}
|
)}
|
||||||
<span className="text-zinc-700" aria-hidden="true">·</span>
|
<span className="text-zinc-700" aria-hidden="true">·</span>
|
||||||
<span className="text-[10px] text-zinc-500 whitespace-nowrap">
|
<span className="text-[10px] text-ink-soft whitespace-nowrap">
|
||||||
{counts.roots} workspace{counts.roots !== 1 ? "s" : ""}
|
{counts.roots} workspace{counts.roots !== 1 ? "s" : ""}
|
||||||
{counts.children > 0 && <span className="text-zinc-600"> + {counts.children} sub</span>}
|
{counts.children > 0 && <span className="text-ink-soft"> + {counts.children} sub</span>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* WebSocket connection status */}
|
{/* WebSocket connection status */}
|
||||||
<div className="pl-3 border-l border-zinc-800/60">
|
<div className="pl-3 border-l border-line/60">
|
||||||
<WsStatusPill status={wsStatus} />
|
<WsStatusPill status={wsStatus} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -175,10 +176,10 @@ export function Toolbar() {
|
|||||||
title={`Stop all running tasks (${counts.activeTasks} active)`}
|
title={`Stop all running tasks (${counts.activeTasks} active)`}
|
||||||
aria-label={stopping ? "Stopping all running tasks" : `Stop all running tasks (${counts.activeTasks} active)`}
|
aria-label={stopping ? "Stopping all running tasks" : `Stop all running tasks (${counts.activeTasks} active)`}
|
||||||
>
|
>
|
||||||
<svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor" className="text-red-400" aria-hidden="true">
|
<svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor" className="text-bad" aria-hidden="true">
|
||||||
<rect x="2" y="2" width="12" height="12" rx="2" />
|
<rect x="2" y="2" width="12" height="12" rx="2" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="text-[10px] text-red-300 font-medium">
|
<span className="text-[10px] text-bad font-medium">
|
||||||
{stopping ? "Stopping..." : `Stop All (${counts.activeTasks})`}
|
{stopping ? "Stopping..." : `Stop All (${counts.activeTasks})`}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@ -194,10 +195,10 @@ export function Toolbar() {
|
|||||||
title={`Restart ${needsRestartNodes.length} workspace${needsRestartNodes.length === 1 ? "" : "s"} that need to pick up config or secret changes`}
|
title={`Restart ${needsRestartNodes.length} workspace${needsRestartNodes.length === 1 ? "" : "s"} that need to pick up config or secret changes`}
|
||||||
aria-label={restartingAll ? "Restarting workspaces" : `Restart ${needsRestartNodes.length} workspace${needsRestartNodes.length === 1 ? "" : "s"} pending config or secret changes`}
|
aria-label={restartingAll ? "Restarting workspaces" : `Restart ${needsRestartNodes.length} workspace${needsRestartNodes.length === 1 ? "" : "s"} pending config or secret changes`}
|
||||||
>
|
>
|
||||||
<svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.8" className="text-amber-400" aria-hidden="true">
|
<svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.8" className="text-warm" aria-hidden="true">
|
||||||
<path d="M2 8a6 6 0 1 1 1.76 4.24M2 13v-3h3" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M2 8a6 6 0 1 1 1.76 4.24M2 13v-3h3" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="text-[10px] text-amber-300 font-medium">
|
<span className="text-[10px] text-warm font-medium">
|
||||||
{restartingAll ? "Restarting..." : `Restart Pending (${needsRestartNodes.length})`}
|
{restartingAll ? "Restarting..." : `Restart Pending (${needsRestartNodes.length})`}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@ -217,8 +218,8 @@ export function Toolbar() {
|
|||||||
title={showA2AEdges ? "Hide A2A delegation edges" : "Show A2A delegation edges (last 60 min)"}
|
title={showA2AEdges ? "Hide A2A delegation edges" : "Show A2A delegation edges (last 60 min)"}
|
||||||
className={`flex items-center justify-center w-7 h-7 border rounded-lg transition-colors ${
|
className={`flex items-center justify-center w-7 h-7 border rounded-lg transition-colors ${
|
||||||
showA2AEdges
|
showA2AEdges
|
||||||
? "bg-blue-950/50 hover:bg-blue-900/50 border-blue-800/40 text-blue-300"
|
? "bg-blue-950/50 hover:bg-blue-900/50 border-blue-800/40 text-accent"
|
||||||
: "bg-zinc-800/50 hover:bg-zinc-700/50 border-zinc-700/40 text-zinc-500 hover:text-zinc-300"
|
: "bg-surface-card/50 hover:bg-surface-card/50 border-line/40 text-ink-soft hover:text-ink-mid"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Mesh / network icon */}
|
{/* Mesh / network icon */}
|
||||||
@ -254,7 +255,7 @@ export function Toolbar() {
|
|||||||
}}
|
}}
|
||||||
aria-label="Open audit trail for selected workspace"
|
aria-label="Open audit trail for selected workspace"
|
||||||
title="Audit — view ledger for the selected workspace"
|
title="Audit — view ledger for the selected workspace"
|
||||||
className="flex items-center justify-center w-7 h-7 bg-zinc-800/50 hover:bg-zinc-700/50 border border-zinc-700/40 rounded-lg transition-colors text-zinc-500 hover:text-zinc-300"
|
className="flex items-center justify-center w-7 h-7 bg-surface-card/50 hover:bg-surface-card/50 border border-line/40 rounded-lg transition-colors text-ink-soft hover:text-ink-mid"
|
||||||
>
|
>
|
||||||
{/* Scroll / ledger icon */}
|
{/* Scroll / ledger icon */}
|
||||||
<svg
|
<svg
|
||||||
@ -276,7 +277,7 @@ export function Toolbar() {
|
|||||||
onClick={() => useCanvasStore.getState().setSearchOpen(true)}
|
onClick={() => useCanvasStore.getState().setSearchOpen(true)}
|
||||||
aria-label="Search workspaces"
|
aria-label="Search workspaces"
|
||||||
title="Search (⌘K)"
|
title="Search (⌘K)"
|
||||||
className="flex items-center justify-center w-7 h-7 bg-zinc-800/50 hover:bg-zinc-700/50 border border-zinc-700/40 rounded-lg transition-colors text-zinc-500 hover:text-zinc-300"
|
className="flex items-center justify-center w-7 h-7 bg-surface-card/50 hover:bg-surface-card/50 border border-line/40 rounded-lg transition-colors text-ink-soft hover:text-ink-mid"
|
||||||
>
|
>
|
||||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||||
<circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="1.5" />
|
<circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="1.5" />
|
||||||
@ -289,7 +290,7 @@ export function Toolbar() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setHelpOpen((open) => !open)}
|
onClick={() => setHelpOpen((open) => !open)}
|
||||||
className="flex items-center justify-center w-7 h-7 bg-zinc-800/50 hover:bg-zinc-700/50 border border-zinc-700/40 rounded-lg transition-colors text-zinc-500 hover:text-zinc-300"
|
className="flex items-center justify-center w-7 h-7 bg-surface-card/50 hover:bg-surface-card/50 border border-line/40 rounded-lg transition-colors text-ink-soft hover:text-ink-mid"
|
||||||
aria-expanded={helpOpen}
|
aria-expanded={helpOpen}
|
||||||
aria-label="Open quick help"
|
aria-label="Open quick help"
|
||||||
title="Help — shortcuts & quick start"
|
title="Help — shortcuts & quick start"
|
||||||
@ -301,13 +302,13 @@ export function Toolbar() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{helpOpen && (
|
{helpOpen && (
|
||||||
<div className="absolute right-0 top-full mt-2 w-72 rounded-xl border border-zinc-700/60 bg-zinc-950/95 p-3 shadow-2xl shadow-black/50 backdrop-blur-md">
|
<div className="absolute right-0 top-full mt-2 w-72 rounded-xl border border-line/60 bg-surface/95 p-3 shadow-2xl shadow-black/50 backdrop-blur-md">
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<span className="text-[10px] font-semibold uppercase tracking-[0.24em] text-zinc-400">Quick start</span>
|
<span className="text-[10px] font-semibold uppercase tracking-[0.24em] text-ink-mid">Quick start</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setHelpOpen(false)}
|
onClick={() => setHelpOpen(false)}
|
||||||
className="text-[10px] text-zinc-600 hover:text-zinc-300 transition-colors"
|
className="text-[10px] text-ink-soft hover:text-ink-mid transition-colors"
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
@ -324,6 +325,9 @@ export function Toolbar() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Theme picker — System / Light / Dark */}
|
||||||
|
<ThemeToggle />
|
||||||
|
|
||||||
{/* Settings gear icon */}
|
{/* Settings gear icon */}
|
||||||
<SettingsButton ref={settingsGearRef} />
|
<SettingsButton ref={settingsGearRef} />
|
||||||
|
|
||||||
@ -344,7 +348,7 @@ function StatusPill({ color, count, label }: { color: string; count: number; lab
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1.5" title={`${count} ${label}`} aria-label={`${count} ${label}`}>
|
<div className="flex items-center gap-1.5" title={`${count} ${label}`} aria-label={`${count} ${label}`}>
|
||||||
<div className={`w-1.5 h-1.5 rounded-full ${color}`} aria-hidden="true" />
|
<div className={`w-1.5 h-1.5 rounded-full ${color}`} aria-hidden="true" />
|
||||||
<span className="text-[10px] text-zinc-400 tabular-nums" aria-hidden="true">{count}</span>
|
<span className="text-[10px] text-ink-mid tabular-nums" aria-hidden="true">{count}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -354,7 +358,7 @@ function WsStatusPill({ status }: { status: "connected" | "connecting" | "discon
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1.5" title="Real-time updates: connected" aria-label="Real-time updates: connected">
|
<div className="flex items-center gap-1.5" title="Real-time updates: connected" aria-label="Real-time updates: connected">
|
||||||
<div className={`w-1.5 h-1.5 rounded-full ${statusDotClass("online")}`} aria-hidden="true" />
|
<div className={`w-1.5 h-1.5 rounded-full ${statusDotClass("online")}`} aria-hidden="true" />
|
||||||
<span className="text-[10px] text-zinc-500" aria-hidden="true">Live</span>
|
<span className="text-[10px] text-ink-soft" aria-hidden="true">Live</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -362,25 +366,25 @@ function WsStatusPill({ status }: { status: "connected" | "connecting" | "discon
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1.5" title="Real-time updates: reconnecting…" aria-label="Real-time updates: reconnecting">
|
<div className="flex items-center gap-1.5" title="Real-time updates: reconnecting…" aria-label="Real-time updates: reconnecting">
|
||||||
<div className="w-1.5 h-1.5 rounded-full bg-amber-400 motion-safe:animate-pulse" aria-hidden="true" />
|
<div className="w-1.5 h-1.5 rounded-full bg-amber-400 motion-safe:animate-pulse" aria-hidden="true" />
|
||||||
<span className="text-[10px] text-zinc-500" aria-hidden="true">Reconnecting</span>
|
<span className="text-[10px] text-ink-soft" aria-hidden="true">Reconnecting</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1.5" title="Real-time updates: disconnected" aria-label="Real-time updates: disconnected">
|
<div className="flex items-center gap-1.5" title="Real-time updates: disconnected" aria-label="Real-time updates: disconnected">
|
||||||
<div className={`w-1.5 h-1.5 rounded-full ${statusDotClass("failed")}`} aria-hidden="true" />
|
<div className={`w-1.5 h-1.5 rounded-full ${statusDotClass("failed")}`} aria-hidden="true" />
|
||||||
<span className="text-[10px] text-zinc-500" aria-hidden="true">Offline</span>
|
<span className="text-[10px] text-ink-soft" aria-hidden="true">Offline</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function HelpRow({ shortcut, text }: { shortcut: string; text: string }) {
|
function HelpRow({ shortcut, text }: { shortcut: string; text: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start gap-3 rounded-lg border border-zinc-800/70 bg-zinc-900/45 px-3 py-2">
|
<div className="flex items-start gap-3 rounded-lg border border-line/70 bg-surface-sunken/45 px-3 py-2">
|
||||||
<span className="shrink-0 rounded-md border border-zinc-700/60 bg-zinc-950/70 px-2 py-0.5 text-[9px] font-medium uppercase tracking-[0.18em] text-zinc-400">
|
<span className="shrink-0 rounded-md border border-line/60 bg-surface/70 px-2 py-0.5 text-[9px] font-medium uppercase tracking-[0.18em] text-ink-mid">
|
||||||
{shortcut}
|
{shortcut}
|
||||||
</span>
|
</span>
|
||||||
<p className="text-[11px] leading-relaxed text-zinc-500">{text}</p>
|
<p className="text-[11px] leading-relaxed text-ink-soft">{text}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,10 +66,10 @@ export function Tooltip({ text, children }: Props) {
|
|||||||
<div
|
<div
|
||||||
id={tooltipId.current}
|
id={tooltipId.current}
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
className="fixed z-[9999] max-w-[400px] max-h-[300px] overflow-y-auto px-3 py-2 bg-zinc-800 border border-zinc-600 rounded-lg shadow-2xl shadow-black/60 pointer-events-none"
|
className="fixed z-[9999] max-w-[400px] max-h-[300px] overflow-y-auto px-3 py-2 bg-surface-card border border-line rounded-lg shadow-2xl shadow-black/60 pointer-events-none"
|
||||||
style={{ left: pos.x, top: Math.max(8, pos.y - 8), transform: "translateY(-100%)" }}
|
style={{ left: pos.x, top: Math.max(8, pos.y - 8), transform: "translateY(-100%)" }}
|
||||||
>
|
>
|
||||||
<div className="text-[11px] text-zinc-200 whitespace-pre-wrap break-words leading-relaxed">
|
<div className="text-[11px] text-ink whitespace-pre-wrap break-words leading-relaxed">
|
||||||
{text}
|
{text}
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
|
|||||||
@ -36,7 +36,7 @@ function EjectIcon(props: React.SVGProps<SVGSVGElement>) {
|
|||||||
|
|
||||||
export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>) {
|
export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>) {
|
||||||
const statusCfg = STATUS_CONFIG[data.status] || STATUS_CONFIG.offline;
|
const statusCfg = STATUS_CONFIG[data.status] || STATUS_CONFIG.offline;
|
||||||
const tierCfg = TIER_CONFIG[data.tier] || { label: `T${data.tier}`, color: "text-zinc-500 bg-zinc-800" };
|
const tierCfg = TIER_CONFIG[data.tier] || { label: `T${data.tier}`, color: "text-ink-soft bg-surface-card" };
|
||||||
// Org-deploy context — four derived flags off one store subscription.
|
// Org-deploy context — four derived flags off one store subscription.
|
||||||
// Drives the shimmer while provisioning, the dimmed/non-draggable
|
// Drives the shimmer while provisioning, the dimmed/non-draggable
|
||||||
// treatment on locked descendants, and the Cancel pill on the root.
|
// treatment on locked descendants, and the Cancel pill on the root.
|
||||||
@ -69,8 +69,8 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
|
|||||||
isVisible={isSelected}
|
isVisible={isSelected}
|
||||||
minWidth={hasChildren ? 360 : 210}
|
minWidth={hasChildren ? 360 : 210}
|
||||||
minHeight={hasChildren ? 200 : 110}
|
minHeight={hasChildren ? 200 : 110}
|
||||||
lineClassName="!border-blue-500/40"
|
lineClassName="!border-accent/40"
|
||||||
handleClassName="!w-2 !h-2 !bg-blue-500 !border !border-blue-300"
|
handleClassName="!w-2 !h-2 !bg-accent !border !border-blue-300"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
@ -137,13 +137,13 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
|
|||||||
${isDragTarget
|
${isDragTarget
|
||||||
? "bg-emerald-950/40 border-2 border-emerald-400/60 ring-2 ring-emerald-400/20 scale-[1.03]"
|
? "bg-emerald-950/40 border-2 border-emerald-400/60 ring-2 ring-emerald-400/20 scale-[1.03]"
|
||||||
: isBatchSelected
|
: isBatchSelected
|
||||||
? "bg-zinc-900/95 border-2 border-blue-500/80 ring-2 ring-blue-500/30 shadow-lg shadow-blue-500/15"
|
? "bg-surface-sunken/95 border-2 border-accent/80 ring-2 ring-accent/30 shadow-lg shadow-blue-500/15"
|
||||||
: isSelected
|
: isSelected
|
||||||
? "bg-zinc-900/95 border border-blue-500/70 ring-1 ring-blue-500/30 shadow-lg shadow-blue-500/10"
|
? "bg-surface-sunken/95 border border-accent/70 ring-1 ring-accent/30 shadow-lg shadow-blue-500/10"
|
||||||
: "bg-zinc-900/90 border border-zinc-700/80 hover:border-zinc-500/60 shadow-lg shadow-black/30 hover:shadow-xl hover:shadow-black/40"
|
: "bg-surface-sunken/90 border border-line/80 hover:border-zinc-500/60 shadow-lg shadow-black/30 hover:shadow-xl hover:shadow-black/40"
|
||||||
}
|
}
|
||||||
backdrop-blur-sm
|
backdrop-blur-sm
|
||||||
focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/70 focus-visible:ring-offset-1 focus-visible:ring-offset-zinc-950
|
focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/70 focus-visible:ring-offset-1 focus-visible:ring-offset-zinc-950
|
||||||
${deploy.isActivelyProvisioning ? "mol-deploy-shimmer" : ""}
|
${deploy.isActivelyProvisioning ? "mol-deploy-shimmer" : ""}
|
||||||
${deploy.isLockedChild ? "mol-deploy-locked" : ""}
|
${deploy.isLockedChild ? "mol-deploy-locked" : ""}
|
||||||
`}
|
`}
|
||||||
@ -173,7 +173,7 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
|
|||||||
<div className="flex items-center justify-between gap-2 mb-1">
|
<div className="flex items-center justify-between gap-2 mb-1">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<div className={`w-2 h-2 rounded-full shrink-0 ${statusCfg.dot} ${statusCfg.glow} shadow-sm`} />
|
<div className={`w-2 h-2 rounded-full shrink-0 ${statusCfg.dot} ${statusCfg.glow} shadow-sm`} />
|
||||||
<span className="text-[13px] font-semibold text-zinc-100 truncate leading-tight">
|
<span className="text-[13px] font-semibold text-ink truncate leading-tight">
|
||||||
{data.name}
|
{data.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -213,7 +213,7 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
|
|||||||
★ REMOTE
|
★ REMOTE
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-[7px] font-mono px-1.5 py-0.5 rounded-md text-zinc-400 bg-zinc-800/60 border border-zinc-700/30">
|
<span className="text-[7px] font-mono px-1.5 py-0.5 rounded-md text-ink-mid bg-surface-card/60 border border-line/30">
|
||||||
{runtime}
|
{runtime}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -226,7 +226,7 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
|
|||||||
* grow arbitrarily tall, which wrecks the grid-slot layout
|
* grow arbitrarily tall, which wrecks the grid-slot layout
|
||||||
* because siblings all plan for the same CHILD_DEFAULT_HEIGHT. */}
|
* because siblings all plan for the same CHILD_DEFAULT_HEIGHT. */}
|
||||||
{data.role && (
|
{data.role && (
|
||||||
<div className="text-[10px] text-zinc-400 mb-1.5 leading-tight line-clamp-2">{data.role}</div>
|
<div className="text-[10px] text-ink-mid mb-1.5 leading-tight line-clamp-2">{data.role}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Skills */}
|
{/* Skills */}
|
||||||
@ -237,15 +237,15 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
|
|||||||
key={skill}
|
key={skill}
|
||||||
className={`text-[10px] px-1.5 py-0.5 rounded-md border ${
|
className={`text-[10px] px-1.5 py-0.5 rounded-md border ${
|
||||||
isOnline
|
isOnline
|
||||||
? "text-emerald-300/80 bg-emerald-950/30 border-emerald-800/30"
|
? "text-good/80 bg-emerald-950/30 border-emerald-800/30"
|
||||||
: "text-zinc-400 bg-zinc-800/60 border-zinc-700/40"
|
: "text-ink-mid bg-surface-card/60 border-line/40"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{skill}
|
{skill}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{skills.length > 4 && (
|
{skills.length > 4 && (
|
||||||
<span className="text-[10px] text-zinc-500 self-center">
|
<span className="text-[10px] text-ink-soft self-center">
|
||||||
+{skills.length - 4}
|
+{skills.length - 4}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -261,7 +261,7 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
|
|||||||
<Tooltip text={String(data.currentTask)}>
|
<Tooltip text={String(data.currentTask)}>
|
||||||
<div className="flex items-center gap-1.5 mt-1 bg-amber-950/20 px-2 py-1 rounded-md border border-amber-800/20 cursor-default">
|
<div className="flex items-center gap-1.5 mt-1 bg-amber-950/20 px-2 py-1 rounded-md border border-amber-800/20 cursor-default">
|
||||||
<div className="w-1.5 h-1.5 rounded-full bg-amber-400 motion-safe:animate-pulse shrink-0" />
|
<div className="w-1.5 h-1.5 rounded-full bg-amber-400 motion-safe:animate-pulse shrink-0" />
|
||||||
<span className="text-[10px] text-amber-300/80 truncate">{data.currentTask}</span>
|
<span className="text-[10px] text-warm/80 truncate">{data.currentTask}</span>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@ -274,7 +274,7 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
useCanvasStore.getState().restartWorkspace(id).catch(() => showToast("Restart failed", "error"));
|
useCanvasStore.getState().restartWorkspace(id).catch(() => showToast("Restart failed", "error"));
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-1.5 mt-1 w-full bg-sky-950/30 px-2 py-1 rounded-md border border-sky-800/30 hover:bg-sky-900/40 transition-colors text-left focus-visible:ring-2 focus-visible:ring-blue-500/70 focus-visible:outline-none"
|
className="flex items-center gap-1.5 mt-1 w-full bg-sky-950/30 px-2 py-1 rounded-md border border-sky-800/30 hover:bg-sky-900/40 transition-colors text-left focus-visible:ring-2 focus-visible:ring-accent/70 focus-visible:outline-none"
|
||||||
>
|
>
|
||||||
<span className="text-[10px]">↻</span>
|
<span className="text-[10px]">↻</span>
|
||||||
<span className="text-[10px] text-sky-300/80">Restart to apply changes</span>
|
<span className="text-[10px] text-sky-300/80">Restart to apply changes</span>
|
||||||
@ -285,10 +285,10 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
|
|||||||
<div className="flex items-center justify-between mt-0.5">
|
<div className="flex items-center justify-between mt-0.5">
|
||||||
{data.status !== "online" ? (
|
{data.status !== "online" ? (
|
||||||
<div className={`text-[10px] uppercase tracking-widest font-medium ${
|
<div className={`text-[10px] uppercase tracking-widest font-medium ${
|
||||||
data.status === "failed" ? "text-red-400" :
|
data.status === "failed" ? "text-bad" :
|
||||||
data.status === "degraded" ? "text-amber-300" :
|
data.status === "degraded" ? "text-warm" :
|
||||||
data.status === "provisioning" ? "text-sky-400" :
|
data.status === "provisioning" ? "text-sky-400" :
|
||||||
"text-zinc-500"
|
"text-ink-soft"
|
||||||
}`}>
|
}`}>
|
||||||
{statusCfg.label}
|
{statusCfg.label}
|
||||||
</div>
|
</div>
|
||||||
@ -297,7 +297,7 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
|
|||||||
{data.activeTasks > 0 && (
|
{data.activeTasks > 0 && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<div className="w-1 h-1 rounded-full bg-amber-400 motion-safe:animate-pulse" />
|
<div className="w-1 h-1 rounded-full bg-amber-400 motion-safe:animate-pulse" />
|
||||||
<span className="text-[10px] text-amber-300/80 tabular-nums">
|
<span className="text-[10px] text-warm/80 tabular-nums">
|
||||||
{data.activeTasks} task{data.activeTasks > 1 ? "s" : ""}
|
{data.activeTasks} task{data.activeTasks > 1 ? "s" : ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -307,7 +307,7 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
|
|||||||
{/* Degraded error preview */}
|
{/* Degraded error preview */}
|
||||||
{data.status === "degraded" && data.lastSampleError && (
|
{data.status === "degraded" && data.lastSampleError && (
|
||||||
<div
|
<div
|
||||||
className="text-[10px] text-amber-300/60 truncate mt-1 bg-amber-950/20 px-1.5 py-0.5 rounded border border-amber-800/20"
|
className="text-[10px] text-warm/60 truncate mt-1 bg-amber-950/20 px-1.5 py-0.5 rounded border border-amber-800/20"
|
||||||
title={data.lastSampleError}
|
title={data.lastSampleError}
|
||||||
>
|
>
|
||||||
{data.lastSampleError}
|
{data.lastSampleError}
|
||||||
@ -357,7 +357,7 @@ function TeamMemberChip({
|
|||||||
}) {
|
}) {
|
||||||
const { data } = node;
|
const { data } = node;
|
||||||
const statusCfg = STATUS_CONFIG[data.status] || STATUS_CONFIG.offline;
|
const statusCfg = STATUS_CONFIG[data.status] || STATUS_CONFIG.offline;
|
||||||
const tierCfg = TIER_CONFIG[data.tier] || { label: `T${data.tier}`, color: "text-zinc-500 bg-zinc-800" };
|
const tierCfg = TIER_CONFIG[data.tier] || { label: `T${data.tier}`, color: "text-ink-soft bg-surface-card" };
|
||||||
const isOnline = data.status === "online";
|
const isOnline = data.status === "online";
|
||||||
const skills = getSkillNames(data.agentCard);
|
const skills = getSkillNames(data.agentCard);
|
||||||
|
|
||||||
@ -376,7 +376,7 @@ function TeamMemberChip({
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label={`Select ${data.name}`}
|
aria-label={`Select ${data.name}`}
|
||||||
className="group/child relative rounded-lg bg-zinc-800/60 hover:bg-zinc-700/70 border border-zinc-700/30 hover:border-zinc-600/40 overflow-hidden transition-colors cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/70"
|
className="group/child relative rounded-lg bg-surface-card/60 hover:bg-surface-card/70 border border-line/30 hover:border-line/40 overflow-hidden transition-colors cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/70"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onSelect(node.id);
|
onSelect(node.id);
|
||||||
@ -402,7 +402,7 @@ function TeamMemberChip({
|
|||||||
<div className="flex items-center justify-between gap-1 mb-0.5">
|
<div className="flex items-center justify-between gap-1 mb-0.5">
|
||||||
<div className="flex items-center gap-1.5 min-w-0">
|
<div className="flex items-center gap-1.5 min-w-0">
|
||||||
<div className={`w-1.5 h-1.5 rounded-full shrink-0 ${statusCfg.dot}`} />
|
<div className={`w-1.5 h-1.5 rounded-full shrink-0 ${statusCfg.dot}`} />
|
||||||
<span className="text-[10px] font-semibold text-zinc-200 truncate leading-tight">
|
<span className="text-[10px] font-semibold text-ink truncate leading-tight">
|
||||||
{data.name}
|
{data.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -423,7 +423,7 @@ function TeamMemberChip({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onExtract(node.id);
|
onExtract(node.id);
|
||||||
}}
|
}}
|
||||||
className="opacity-0 group-hover/child:opacity-100 text-zinc-500 hover:text-sky-400 transition-all focus-visible:ring-2 focus-visible:ring-blue-500/70 focus-visible:outline-none rounded"
|
className="opacity-0 group-hover/child:opacity-100 text-ink-soft hover:text-sky-400 transition-all focus-visible:ring-2 focus-visible:ring-accent/70 focus-visible:outline-none rounded"
|
||||||
>
|
>
|
||||||
<EjectIcon aria-hidden="true" />
|
<EjectIcon aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
@ -432,7 +432,7 @@ function TeamMemberChip({
|
|||||||
|
|
||||||
{/* Role */}
|
{/* Role */}
|
||||||
{data.role && (
|
{data.role && (
|
||||||
<div className="text-[10px] text-zinc-500 mb-1 leading-tight truncate">{data.role}</div>
|
<div className="text-[10px] text-ink-soft mb-1 leading-tight truncate">{data.role}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Skills */}
|
{/* Skills */}
|
||||||
@ -443,15 +443,15 @@ function TeamMemberChip({
|
|||||||
key={skill}
|
key={skill}
|
||||||
className={`text-[10px] px-1 py-0.5 rounded border ${
|
className={`text-[10px] px-1 py-0.5 rounded border ${
|
||||||
isOnline
|
isOnline
|
||||||
? "text-emerald-300/70 bg-emerald-950/20 border-emerald-800/20"
|
? "text-good/70 bg-emerald-950/20 border-emerald-800/20"
|
||||||
: "text-zinc-500 bg-zinc-800/40 border-zinc-700/30"
|
: "text-ink-soft bg-surface-card/40 border-line/30"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{skill}
|
{skill}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{skills.length > 3 && (
|
{skills.length > 3 && (
|
||||||
<span className="text-[10px] text-zinc-400 self-center">+{skills.length - 3}</span>
|
<span className="text-[10px] text-ink-mid self-center">+{skills.length - 3}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -460,10 +460,10 @@ function TeamMemberChip({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{data.status !== "online" ? (
|
{data.status !== "online" ? (
|
||||||
<span className={`text-[10px] uppercase tracking-widest font-medium ${
|
<span className={`text-[10px] uppercase tracking-widest font-medium ${
|
||||||
data.status === "failed" ? "text-red-400" :
|
data.status === "failed" ? "text-bad" :
|
||||||
data.status === "degraded" ? "text-amber-300" :
|
data.status === "degraded" ? "text-warm" :
|
||||||
data.status === "provisioning" ? "text-sky-400" :
|
data.status === "provisioning" ? "text-sky-400" :
|
||||||
"text-zinc-500"
|
"text-ink-soft"
|
||||||
}`}>
|
}`}>
|
||||||
{statusCfg.label}
|
{statusCfg.label}
|
||||||
</span>
|
</span>
|
||||||
@ -471,7 +471,7 @@ function TeamMemberChip({
|
|||||||
{data.activeTasks > 0 && (
|
{data.activeTasks > 0 && (
|
||||||
<div className="flex items-center gap-0.5">
|
<div className="flex items-center gap-0.5">
|
||||||
<div className="w-1 h-1 rounded-full bg-amber-400 motion-safe:animate-pulse" />
|
<div className="w-1 h-1 rounded-full bg-amber-400 motion-safe:animate-pulse" />
|
||||||
<span className="text-[10px] text-amber-300 tabular-nums">
|
<span className="text-[10px] text-warm tabular-nums">
|
||||||
{data.activeTasks}
|
{data.activeTasks}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -483,15 +483,15 @@ function TeamMemberChip({
|
|||||||
<Tooltip text={String(data.currentTask)}>
|
<Tooltip text={String(data.currentTask)}>
|
||||||
<div className="flex items-center gap-1 mt-0.5 px-1.5 py-0.5 bg-amber-950/20 rounded border border-amber-800/20 cursor-default">
|
<div className="flex items-center gap-1 mt-0.5 px-1.5 py-0.5 bg-amber-950/20 rounded border border-amber-800/20 cursor-default">
|
||||||
<div className="w-1 h-1 rounded-full bg-amber-400 motion-safe:animate-pulse shrink-0" />
|
<div className="w-1 h-1 rounded-full bg-amber-400 motion-safe:animate-pulse shrink-0" />
|
||||||
<span className="text-[10px] text-amber-300 truncate">{data.currentTask}</span>
|
<span className="text-[10px] text-warm truncate">{data.currentTask}</span>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Recursive sub-children rendered inside this card */}
|
{/* Recursive sub-children rendered inside this card */}
|
||||||
{hasSubChildren && depth < MAX_NESTING_DEPTH && (
|
{hasSubChildren && depth < MAX_NESTING_DEPTH && (
|
||||||
<div className="mt-1.5 pt-1.5 border-t border-zinc-700/20">
|
<div className="mt-1.5 pt-1.5 border-t border-line/20">
|
||||||
<div className="text-[10px] text-zinc-400 uppercase tracking-widest mb-1">Team</div>
|
<div className="text-[10px] text-ink-mid uppercase tracking-widest mb-1">Team</div>
|
||||||
<div className={subChildren.length >= 2 ? "grid grid-cols-2 gap-1" : "space-y-1"}>
|
<div className={subChildren.length >= 2 ? "grid grid-cols-2 gap-1" : "space-y-1"}>
|
||||||
{subChildren.map((sub) => (
|
{subChildren.map((sub) => (
|
||||||
<TeamMemberChip key={sub.id} node={sub} allNodes={allNodes} depth={depth + 1} onSelect={onSelect} onExtract={onExtract} />
|
<TeamMemberChip key={sub.id} node={sub} allNodes={allNodes} depth={depth + 1} onSelect={onSelect} onExtract={onExtract} />
|
||||||
|
|||||||
@ -46,16 +46,16 @@ export function WorkspaceUsage({ workspaceId }: WorkspaceUsageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="rounded-md border border-zinc-700 bg-zinc-900 p-3 space-y-2"
|
className="rounded-md border border-line bg-surface-sunken p-3 space-y-2"
|
||||||
data-testid="workspace-usage"
|
data-testid="workspace-usage"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-xs font-semibold text-zinc-400 uppercase tracking-wider">
|
<h4 className="text-xs font-semibold text-ink-mid uppercase tracking-wider">
|
||||||
Usage
|
Usage
|
||||||
</h4>
|
</h4>
|
||||||
{!loading && metrics && (
|
{!loading && metrics && (
|
||||||
<span
|
<span
|
||||||
className="text-[10px] text-zinc-600 font-mono"
|
className="text-[10px] text-ink-soft font-mono"
|
||||||
data-testid="usage-period"
|
data-testid="usage-period"
|
||||||
>
|
>
|
||||||
{formatPeriod(metrics.period_start, metrics.period_end)}
|
{formatPeriod(metrics.period_start, metrics.period_end)}
|
||||||
@ -71,7 +71,7 @@ export function WorkspaceUsage({ workspaceId }: WorkspaceUsageProps) {
|
|||||||
<SkeletonRow />
|
<SkeletonRow />
|
||||||
</>
|
</>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<p className="text-xs text-red-400" data-testid="usage-error">
|
<p className="text-xs text-bad" data-testid="usage-error">
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
) : metrics ? (
|
) : metrics ? (
|
||||||
@ -114,8 +114,8 @@ function SkeletonRow() {
|
|||||||
className="flex justify-between items-center animate-pulse"
|
className="flex justify-between items-center animate-pulse"
|
||||||
data-testid="usage-skeleton-row"
|
data-testid="usage-skeleton-row"
|
||||||
>
|
>
|
||||||
<div className="h-3 w-20 rounded bg-zinc-700" />
|
<div className="h-3 w-20 rounded bg-surface-card" />
|
||||||
<div className="h-3 w-16 rounded bg-zinc-700" />
|
<div className="h-3 w-16 rounded bg-surface-card" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -131,8 +131,8 @@ function StatRow({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between items-center" data-testid={testId}>
|
<div className="flex justify-between items-center" data-testid={testId}>
|
||||||
<span className="text-xs text-zinc-500">{label}</span>
|
<span className="text-xs text-ink-soft">{label}</span>
|
||||||
<span className="text-xs text-zinc-400 font-mono">{value}</span>
|
<span className="text-xs text-ink-mid font-mono">{value}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,7 +51,7 @@ describe("AuthGate — loading state", () => {
|
|||||||
</AuthGate>
|
</AuthGate>
|
||||||
);
|
);
|
||||||
|
|
||||||
const overlay = container.querySelector(".bg-zinc-950.fixed.inset-0");
|
const overlay = container.querySelector(".bg-surface.fixed.inset-0");
|
||||||
expect(overlay).not.toBeNull();
|
expect(overlay).not.toBeNull();
|
||||||
expect(overlay?.getAttribute("aria-hidden")).toBe("true");
|
expect(overlay?.getAttribute("aria-hidden")).toBe("true");
|
||||||
});
|
});
|
||||||
|
|||||||
@ -89,7 +89,7 @@ function A2AEdgeImpl({
|
|||||||
// The edge stroke color matches what buildA2AEdges sets on the SVG
|
// The edge stroke color matches what buildA2AEdges sets on the SVG
|
||||||
// path style. Mirror it on the badge border so the visual identity
|
// path style. Mirror it on the badge border so the visual identity
|
||||||
// (hot=violet vs warm=blue) carries to the clickable label.
|
// (hot=violet vs warm=blue) carries to the clickable label.
|
||||||
const accent = isHot ? "border-violet-500/60" : "border-blue-500/60";
|
const accent = isHot ? "border-violet-500/60" : "border-accent/60";
|
||||||
const accentText = isHot ? "text-violet-200" : "text-blue-200";
|
const accentText = isHot ? "text-violet-200" : "text-blue-200";
|
||||||
const ariaLabel = `${count} delegation${count === 1 ? "" : "s"} from ${
|
const ariaLabel = `${count} delegation${count === 1 ? "" : "s"} from ${
|
||||||
edgeData.label?.split(" · ")[1] ?? "recent"
|
edgeData.label?.split(" · ")[1] ?? "recent"
|
||||||
@ -119,7 +119,7 @@ function A2AEdgeImpl({
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
title="Open source workspace's activity feed"
|
title="Open source workspace's activity feed"
|
||||||
className={`px-2 py-0.5 rounded-full bg-zinc-900/95 border ${accent} ${accentText} text-[10px] font-medium shadow-md shadow-black/40 backdrop-blur-sm hover:bg-zinc-800 hover:border-opacity-100 transition-colors cursor-pointer`}
|
className={`px-2 py-0.5 rounded-full bg-surface-sunken/95 border ${accent} ${accentText} text-[10px] font-medium shadow-md shadow-black/40 backdrop-blur-sm hover:bg-surface-card hover:border-opacity-100 transition-colors cursor-pointer`}
|
||||||
>
|
>
|
||||||
{labelText}
|
{labelText}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -112,10 +112,10 @@ export function OrgCancelButton({ rootId, rootName, workspaceCount }: Props) {
|
|||||||
if (confirming) {
|
if (confirming) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="nodrag absolute -top-10 right-0 z-20 flex items-center gap-1.5 rounded-lg bg-zinc-900/95 px-2 py-1 shadow-lg border border-red-800/60"
|
className="nodrag absolute -top-10 right-0 z-20 flex items-center gap-1.5 rounded-lg bg-surface-sunken/95 px-2 py-1 shadow-lg border border-red-800/60"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<span className="text-[10px] text-zinc-300">
|
<span className="text-[10px] text-ink-mid">
|
||||||
Delete {workspaceCount} workspace{workspaceCount === 1 ? "" : "s"}?
|
Delete {workspaceCount} workspace{workspaceCount === 1 ? "" : "s"}?
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
@ -130,7 +130,7 @@ export function OrgCancelButton({ rootId, rootName, workspaceCount }: Props) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setConfirming(false)}
|
onClick={() => setConfirming(false)}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="px-2 py-0.5 rounded bg-zinc-700/80 hover:bg-zinc-600 text-[10px] text-zinc-200"
|
className="px-2 py-0.5 rounded bg-surface-card/80 hover:bg-zinc-600 text-[10px] text-ink"
|
||||||
>
|
>
|
||||||
No
|
No
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -168,7 +168,10 @@ describe("A2AEdge — render", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
const btn = screen.getByRole("button");
|
const btn = screen.getByRole("button");
|
||||||
expect(btn.className).toContain("border-blue-500/60");
|
// Warm-paper migration: blue-500 border was mapped to the semantic
|
||||||
|
// accent token; the text-blue-200 literal is intentionally retained
|
||||||
|
// because tinted-state pill text reads in both themes.
|
||||||
|
expect(btn.className).toContain("border-accent/60");
|
||||||
expect(btn.className).toContain("text-blue-200");
|
expect(btn.className).toContain("text-blue-200");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -105,11 +105,11 @@ export function OrgTokensTab() {
|
|||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<h3 className="text-sm font-semibold text-zinc-200">
|
<h3 className="text-sm font-semibold text-ink">
|
||||||
Organization API Keys
|
Organization API Keys
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-zinc-500 leading-relaxed">
|
<p className="text-[10px] text-ink-soft leading-relaxed">
|
||||||
Full-admin bearer tokens for this organization. Use with external
|
Full-admin bearer tokens for this organization. Use with external
|
||||||
integrations, CLI tools, or AI agents that need to manage
|
integrations, CLI tools, or AI agents that need to manage
|
||||||
workspaces, settings, and secrets. Each key has the same
|
workspaces, settings, and secrets. Each key has the same
|
||||||
@ -126,12 +126,12 @@ export function OrgTokensTab() {
|
|||||||
placeholder="Label (e.g. zapier, my-ci)"
|
placeholder="Label (e.g. zapier, my-ci)"
|
||||||
maxLength={100}
|
maxLength={100}
|
||||||
aria-label="Organization API key label"
|
aria-label="Organization API key label"
|
||||||
className="flex-1 text-[11px] bg-zinc-900/60 border border-zinc-700/50 rounded px-2 py-1.5 text-zinc-200 placeholder-zinc-600"
|
className="flex-1 text-[11px] bg-surface-sunken/60 border border-line/50 rounded px-2 py-1.5 text-ink placeholder-zinc-600"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
disabled={creating}
|
disabled={creating}
|
||||||
className="px-3 py-1.5 bg-blue-600/20 hover:bg-blue-600/30 border border-blue-500/30 rounded-lg text-[11px] text-blue-300 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
|
className="px-3 py-1.5 bg-accent-strong/20 hover:bg-accent-strong/30 border border-accent/30 rounded-lg text-[11px] text-accent font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
{creating ? (
|
{creating ? (
|
||||||
<>
|
<>
|
||||||
@ -147,10 +147,10 @@ export function OrgTokensTab() {
|
|||||||
{newToken && (
|
{newToken && (
|
||||||
<div className="bg-emerald-950/30 border border-emerald-800/40 rounded-lg p-3 space-y-2">
|
<div className="bg-emerald-950/30 border border-emerald-800/40 rounded-lg p-3 space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[10px] text-emerald-400 font-semibold uppercase tracking-wider">
|
<span className="text-[10px] text-good font-semibold uppercase tracking-wider">
|
||||||
{newTokenName ? `New Key: ${newTokenName}` : 'New Key Created'}
|
{newTokenName ? `New Key: ${newTokenName}` : 'New Key Created'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[9px] text-emerald-500/70">
|
<span className="text-[9px] text-good/70">
|
||||||
Copy now — it won't be shown again
|
Copy now — it won't be shown again
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -160,14 +160,14 @@ export function OrgTokensTab() {
|
|||||||
</code>
|
</code>
|
||||||
<button
|
<button
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className="shrink-0 px-2 py-1.5 bg-emerald-800/40 hover:bg-emerald-700/50 border border-emerald-700/40 rounded text-[10px] text-emerald-300 transition-colors"
|
className="shrink-0 px-2 py-1.5 bg-emerald-800/40 hover:bg-emerald-700/50 border border-emerald-700/40 rounded text-[10px] text-good transition-colors"
|
||||||
>
|
>
|
||||||
{copied ? 'Copied' : 'Copy'}
|
{copied ? 'Copied' : 'Copy'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setNewToken(null)}
|
onClick={() => setNewToken(null)}
|
||||||
className="text-[9px] text-emerald-500/60 hover:text-emerald-400 transition-colors"
|
className="text-[9px] text-good/60 hover:text-good transition-colors"
|
||||||
>
|
>
|
||||||
Dismiss
|
Dismiss
|
||||||
</button>
|
</button>
|
||||||
@ -175,20 +175,20 @@ export function OrgTokensTab() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-[10px] text-red-400">
|
<div className="px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-[10px] text-bad">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Token list */}
|
{/* Token list */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center gap-2 py-6 text-zinc-500 text-xs">
|
<div className="flex items-center justify-center gap-2 py-6 text-ink-soft text-xs">
|
||||||
<Spinner /> Loading keys...
|
<Spinner /> Loading keys...
|
||||||
</div>
|
</div>
|
||||||
) : tokens.length === 0 ? (
|
) : tokens.length === 0 ? (
|
||||||
<div className="text-center py-6">
|
<div className="text-center py-6">
|
||||||
<p className="text-xs text-zinc-500">No active keys</p>
|
<p className="text-xs text-ink-soft">No active keys</p>
|
||||||
<p className="text-[10px] text-zinc-600 mt-1">
|
<p className="text-[10px] text-ink-soft mt-1">
|
||||||
Create a key above to authenticate API calls to this organization.
|
Create a key above to authenticate API calls to this organization.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -197,19 +197,19 @@ export function OrgTokensTab() {
|
|||||||
{tokens.map((t) => (
|
{tokens.map((t) => (
|
||||||
<div
|
<div
|
||||||
key={t.id}
|
key={t.id}
|
||||||
className="flex items-center justify-between bg-zinc-800/40 border border-zinc-700/30 rounded-lg px-3 py-2"
|
className="flex items-center justify-between bg-surface-card/40 border border-line/30 rounded-lg px-3 py-2"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||||
<code className="text-[11px] font-mono text-zinc-300 bg-zinc-900/60 px-1.5 py-0.5 rounded shrink-0">
|
<code className="text-[11px] font-mono text-ink-mid bg-surface-sunken/60 px-1.5 py-0.5 rounded shrink-0">
|
||||||
{t.prefix}...
|
{t.prefix}...
|
||||||
</code>
|
</code>
|
||||||
<div className="flex flex-col min-w-0">
|
<div className="flex flex-col min-w-0">
|
||||||
{t.name && (
|
{t.name && (
|
||||||
<span className="text-[11px] text-zinc-200 truncate">
|
<span className="text-[11px] text-ink truncate">
|
||||||
{t.name}
|
{t.name}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div className="text-[9px] text-zinc-500 space-x-3">
|
<div className="text-[9px] text-ink-soft space-x-3">
|
||||||
<span>Created {formatAge(t.created_at)}</span>
|
<span>Created {formatAge(t.created_at)}</span>
|
||||||
{t.last_used_at && (
|
{t.last_used_at && (
|
||||||
<span>Last used {formatAge(t.last_used_at)}</span>
|
<span>Last used {formatAge(t.last_used_at)}</span>
|
||||||
@ -219,7 +219,7 @@ export function OrgTokensTab() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setRevokeTarget(t)}
|
onClick={() => setRevokeTarget(t)}
|
||||||
className="text-[10px] text-red-400/70 hover:text-red-400 transition-colors px-2 py-1 shrink-0"
|
className="text-[10px] text-bad/70 hover:text-bad transition-colors px-2 py-1 shrink-0"
|
||||||
>
|
>
|
||||||
Revoke
|
Revoke
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -80,15 +80,15 @@ export function TokensTab({ workspaceId }: TokensTabProps) {
|
|||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-zinc-200">API Tokens</h3>
|
<h3 className="text-sm font-semibold text-ink">API Tokens</h3>
|
||||||
<p className="text-[10px] text-zinc-500 mt-0.5">
|
<p className="text-[10px] text-ink-soft mt-0.5">
|
||||||
Bearer tokens for authenticating API calls to this workspace.
|
Bearer tokens for authenticating API calls to this workspace.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
disabled={creating}
|
disabled={creating}
|
||||||
className="px-3 py-1.5 bg-blue-600/20 hover:bg-blue-600/30 border border-blue-500/30 rounded-lg text-[11px] text-blue-300 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
|
className="px-3 py-1.5 bg-accent-strong/20 hover:bg-accent-strong/30 border border-accent/30 rounded-lg text-[11px] text-accent font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
{creating ? <><Spinner size="sm" /> Creating...</> : '+ New Token'}
|
{creating ? <><Spinner size="sm" /> Creating...</> : '+ New Token'}
|
||||||
</button>
|
</button>
|
||||||
@ -98,8 +98,8 @@ export function TokensTab({ workspaceId }: TokensTabProps) {
|
|||||||
{newToken && (
|
{newToken && (
|
||||||
<div className="bg-emerald-950/30 border border-emerald-800/40 rounded-lg p-3 space-y-2">
|
<div className="bg-emerald-950/30 border border-emerald-800/40 rounded-lg p-3 space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[10px] text-emerald-400 font-semibold uppercase tracking-wider">New Token Created</span>
|
<span className="text-[10px] text-good font-semibold uppercase tracking-wider">New Token Created</span>
|
||||||
<span className="text-[9px] text-emerald-500/70">Copy now — it won't be shown again</span>
|
<span className="text-[9px] text-good/70">Copy now — it won't be shown again</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<code className="flex-1 text-[11px] text-emerald-200 bg-emerald-950/50 px-2 py-1.5 rounded font-mono break-all select-all">
|
<code className="flex-1 text-[11px] text-emerald-200 bg-emerald-950/50 px-2 py-1.5 rounded font-mono break-all select-all">
|
||||||
@ -107,14 +107,14 @@ export function TokensTab({ workspaceId }: TokensTabProps) {
|
|||||||
</code>
|
</code>
|
||||||
<button
|
<button
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className="shrink-0 px-2 py-1.5 bg-emerald-800/40 hover:bg-emerald-700/50 border border-emerald-700/40 rounded text-[10px] text-emerald-300 transition-colors"
|
className="shrink-0 px-2 py-1.5 bg-emerald-800/40 hover:bg-emerald-700/50 border border-emerald-700/40 rounded text-[10px] text-good transition-colors"
|
||||||
>
|
>
|
||||||
{copied ? 'Copied' : 'Copy'}
|
{copied ? 'Copied' : 'Copy'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setNewToken(null)}
|
onClick={() => setNewToken(null)}
|
||||||
className="text-[9px] text-emerald-500/60 hover:text-emerald-400 transition-colors"
|
className="text-[9px] text-good/60 hover:text-good transition-colors"
|
||||||
>
|
>
|
||||||
Dismiss
|
Dismiss
|
||||||
</button>
|
</button>
|
||||||
@ -122,20 +122,20 @@ export function TokensTab({ workspaceId }: TokensTabProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-[10px] text-red-400">
|
<div className="px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-[10px] text-bad">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Token list */}
|
{/* Token list */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center gap-2 py-6 text-zinc-500 text-xs">
|
<div className="flex items-center justify-center gap-2 py-6 text-ink-soft text-xs">
|
||||||
<Spinner /> Loading tokens...
|
<Spinner /> Loading tokens...
|
||||||
</div>
|
</div>
|
||||||
) : tokens.length === 0 ? (
|
) : tokens.length === 0 ? (
|
||||||
<div className="text-center py-6">
|
<div className="text-center py-6">
|
||||||
<p className="text-xs text-zinc-500">No active tokens</p>
|
<p className="text-xs text-ink-soft">No active tokens</p>
|
||||||
<p className="text-[10px] text-zinc-600 mt-1">
|
<p className="text-[10px] text-ink-soft mt-1">
|
||||||
Create a token to authenticate API calls.
|
Create a token to authenticate API calls.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -144,13 +144,13 @@ export function TokensTab({ workspaceId }: TokensTabProps) {
|
|||||||
{tokens.map((t) => (
|
{tokens.map((t) => (
|
||||||
<div
|
<div
|
||||||
key={t.id}
|
key={t.id}
|
||||||
className="flex items-center justify-between bg-zinc-800/40 border border-zinc-700/30 rounded-lg px-3 py-2"
|
className="flex items-center justify-between bg-surface-card/40 border border-line/30 rounded-lg px-3 py-2"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<code className="text-[11px] font-mono text-zinc-300 bg-zinc-900/60 px-1.5 py-0.5 rounded">
|
<code className="text-[11px] font-mono text-ink-mid bg-surface-sunken/60 px-1.5 py-0.5 rounded">
|
||||||
{t.prefix}...
|
{t.prefix}...
|
||||||
</code>
|
</code>
|
||||||
<div className="text-[9px] text-zinc-500 space-x-3">
|
<div className="text-[9px] text-ink-soft space-x-3">
|
||||||
<span>Created {formatAge(t.created_at)}</span>
|
<span>Created {formatAge(t.created_at)}</span>
|
||||||
{t.last_used_at && (
|
{t.last_used_at && (
|
||||||
<span>Last used {formatAge(t.last_used_at)}</span>
|
<span>Last used {formatAge(t.last_used_at)}</span>
|
||||||
@ -159,7 +159,7 @@ export function TokensTab({ workspaceId }: TokensTabProps) {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setRevokeTarget(t)}
|
onClick={() => setRevokeTarget(t)}
|
||||||
className="text-[10px] text-red-400/70 hover:text-red-400 transition-colors px-2 py-1"
|
className="text-[10px] text-bad/70 hover:text-bad transition-colors px-2 py-1"
|
||||||
>
|
>
|
||||||
Revoke
|
Revoke
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -24,18 +24,18 @@ const FILTERS: { id: FilterType; label: string; icon: string }[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const TYPE_COLORS: Record<string, { text: string; bg: string; border: string }> = {
|
const TYPE_COLORS: Record<string, { text: string; bg: string; border: string }> = {
|
||||||
a2a_receive: { text: "text-blue-400", bg: "bg-blue-950/30", border: "border-blue-800/30" },
|
a2a_receive: { text: "text-accent", bg: "bg-blue-950/30", border: "border-blue-800/30" },
|
||||||
a2a_send: { text: "text-cyan-400", bg: "bg-cyan-950/30", border: "border-cyan-800/30" },
|
a2a_send: { text: "text-cyan-400", bg: "bg-cyan-950/30", border: "border-cyan-800/30" },
|
||||||
task_update: { text: "text-amber-400", bg: "bg-amber-950/30", border: "border-amber-800/30" },
|
task_update: { text: "text-warm", bg: "bg-amber-950/30", border: "border-amber-800/30" },
|
||||||
skill_promotion: { text: "text-violet-300", bg: "bg-violet-950/30", border: "border-violet-800/30" },
|
skill_promotion: { text: "text-violet-300", bg: "bg-violet-950/30", border: "border-violet-800/30" },
|
||||||
agent_log: { text: "text-zinc-400", bg: "bg-zinc-800/30", border: "border-zinc-700/30" },
|
agent_log: { text: "text-ink-mid", bg: "bg-surface-card/30", border: "border-line/30" },
|
||||||
error: { text: "text-red-400", bg: "bg-red-950/30", border: "border-red-800/30" },
|
error: { text: "text-bad", bg: "bg-red-950/30", border: "border-red-800/30" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_ICONS: Record<string, { icon: string; color: string }> = {
|
const STATUS_ICONS: Record<string, { icon: string; color: string }> = {
|
||||||
ok: { icon: "✓", color: "text-emerald-400" },
|
ok: { icon: "✓", color: "text-good" },
|
||||||
error: { icon: "✕", color: "text-red-400" },
|
error: { icon: "✕", color: "text-bad" },
|
||||||
timeout: { icon: "⏱", color: "text-amber-400" },
|
timeout: { icon: "⏱", color: "text-warm" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ActivityTab({ workspaceId }: Props) {
|
export function ActivityTab({ workspaceId }: Props) {
|
||||||
@ -75,7 +75,7 @@ export function ActivityTab({ workspaceId }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Filter bar */}
|
{/* Filter bar */}
|
||||||
<div className="px-3 pt-3 pb-2 border-b border-zinc-800/40">
|
<div className="px-3 pt-3 pb-2 border-b border-line/40">
|
||||||
<div className="flex items-center gap-1 flex-wrap">
|
<div className="flex items-center gap-1 flex-wrap">
|
||||||
{FILTERS.map((f) => (
|
{FILTERS.map((f) => (
|
||||||
<button
|
<button
|
||||||
@ -84,8 +84,8 @@ export function ActivityTab({ workspaceId }: Props) {
|
|||||||
aria-pressed={filter === f.id}
|
aria-pressed={filter === f.id}
|
||||||
className={`px-2 py-1 text-[11px] rounded-md font-medium transition-all ${
|
className={`px-2 py-1 text-[11px] rounded-md font-medium transition-all ${
|
||||||
filter === f.id
|
filter === f.id
|
||||||
? "bg-zinc-700 text-zinc-100 ring-1 ring-zinc-600"
|
? "bg-surface-card text-ink ring-1 ring-zinc-600"
|
||||||
: "text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800/60"
|
: "text-ink-soft hover:text-ink-mid hover:bg-surface-card/60"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="mr-0.5 opacity-60">{f.icon}</span> {f.label}
|
<span className="mr-0.5 opacity-60">{f.icon}</span> {f.label}
|
||||||
@ -96,7 +96,7 @@ export function ActivityTab({ workspaceId }: Props) {
|
|||||||
onClick={() => setAutoRefresh(!autoRefresh)}
|
onClick={() => setAutoRefresh(!autoRefresh)}
|
||||||
aria-pressed={autoRefresh}
|
aria-pressed={autoRefresh}
|
||||||
className={`text-[11px] px-1.5 py-0.5 rounded ${
|
className={`text-[11px] px-1.5 py-0.5 rounded ${
|
||||||
autoRefresh ? "text-emerald-400 bg-emerald-950/30" : "text-zinc-500"
|
autoRefresh ? "text-good bg-emerald-950/30" : "text-ink-soft"
|
||||||
}`}
|
}`}
|
||||||
title={autoRefresh ? "Auto-refresh ON" : "Auto-refresh OFF"}
|
title={autoRefresh ? "Auto-refresh ON" : "Auto-refresh OFF"}
|
||||||
>
|
>
|
||||||
@ -104,20 +104,20 @@ export function ActivityTab({ workspaceId }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setTraceOpen(true)}
|
onClick={() => setTraceOpen(true)}
|
||||||
className="px-2 py-1 bg-blue-900/40 hover:bg-blue-800/50 text-[11px] rounded text-blue-300 border border-blue-800/30"
|
className="px-2 py-1 bg-blue-900/40 hover:bg-blue-800/50 text-[11px] rounded text-accent border border-blue-800/30"
|
||||||
title="View full conversation trace across all workspaces"
|
title="View full conversation trace across all workspaces"
|
||||||
>
|
>
|
||||||
Full Trace
|
Full Trace
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={loadActivities}
|
onClick={loadActivities}
|
||||||
className="px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-[11px] rounded text-zinc-300"
|
className="px-2 py-1 bg-surface-card hover:bg-zinc-600 text-[11px] rounded text-ink-mid"
|
||||||
>
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1.5 text-[10px] text-zinc-500">
|
<div className="mt-1.5 text-[10px] text-ink-soft">
|
||||||
{activities.length} {filter === "all" ? "activities" : filter.replace("_", " ") + " entries"}
|
{activities.length} {filter === "all" ? "activities" : filter.replace("_", " ") + " entries"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -125,18 +125,18 @@ export function ActivityTab({ workspaceId }: Props) {
|
|||||||
{/* Activity list */}
|
{/* Activity list */}
|
||||||
<div className="flex-1 overflow-y-auto p-3 space-y-1.5">
|
<div className="flex-1 overflow-y-auto p-3 space-y-1.5">
|
||||||
{loading && activities.length === 0 && (
|
{loading && activities.length === 0 && (
|
||||||
<div className="text-xs text-zinc-500 text-center py-8">Loading activity...</div>
|
<div className="text-xs text-ink-soft text-center py-8">Loading activity...</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400">
|
<div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && !error && activities.length === 0 && (
|
{!loading && !error && activities.length === 0 && (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<div className="text-zinc-600 text-xs">No activity recorded yet</div>
|
<div className="text-ink-soft text-xs">No activity recorded yet</div>
|
||||||
<div className="text-zinc-700 text-[9px] mt-1">
|
<div className="text-zinc-700 text-[9px] mt-1">
|
||||||
Activity logs appear when agents communicate or perform tasks
|
Activity logs appear when agents communicate or perform tasks
|
||||||
</div>
|
</div>
|
||||||
@ -184,7 +184,7 @@ function ActivityRow({
|
|||||||
className={`rounded-lg border transition-colors ${
|
className={`rounded-lg border transition-colors ${
|
||||||
isError
|
isError
|
||||||
? "bg-red-950/20 border-red-900/30"
|
? "bg-red-950/20 border-red-900/30"
|
||||||
: "bg-zinc-800/60 border-zinc-700/40"
|
: "bg-surface-card/60 border-line/40"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<button type="button" onClick={onToggle} className="w-full text-left px-3 py-2">
|
<button type="button" onClick={onToggle} className="w-full text-left px-3 py-2">
|
||||||
@ -195,7 +195,7 @@ function ActivityRow({
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
{entry.method && (
|
{entry.method && (
|
||||||
<span className="text-[10px] font-mono text-zinc-300 truncate">
|
<span className="text-[10px] font-mono text-ink-mid truncate">
|
||||||
{entry.method}
|
{entry.method}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -205,23 +205,23 @@ function ActivityRow({
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
{entry.duration_ms != null && (
|
{entry.duration_ms != null && (
|
||||||
<span className="text-[8px] text-zinc-500 font-mono tabular-nums shrink-0">
|
<span className="text-[8px] text-ink-soft font-mono tabular-nums shrink-0">
|
||||||
{entry.duration_ms}ms
|
{entry.duration_ms}ms
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span className="text-[8px] text-zinc-500 shrink-0">
|
<span className="text-[8px] text-ink-soft shrink-0">
|
||||||
{formatTime(entry.created_at)}
|
{formatTime(entry.created_at)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="text-[9px] text-zinc-500">
|
<span className="text-[9px] text-ink-soft">
|
||||||
{expanded ? "▼" : "▶"}
|
{expanded ? "▼" : "▶"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary — replace raw IDs with workspace names */}
|
{/* Summary — replace raw IDs with workspace names */}
|
||||||
{entry.summary && (
|
{entry.summary && (
|
||||||
<div className="text-[10px] text-zinc-400 mt-1 truncate">
|
<div className="text-[10px] text-ink-mid mt-1 truncate">
|
||||||
{entry.summary
|
{entry.summary
|
||||||
.replace(entry.source_id || "", resolveName(entry.source_id))
|
.replace(entry.source_id || "", resolveName(entry.source_id))
|
||||||
.replace(entry.target_id || "", resolveName(entry.target_id))}
|
.replace(entry.target_id || "", resolveName(entry.target_id))}
|
||||||
@ -236,9 +236,9 @@ function ActivityRow({
|
|||||||
{resolveName(entry.source_id)}
|
{resolveName(entry.source_id)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="text-[9px] text-zinc-500">→</span>
|
<span className="text-[9px] text-ink-soft">→</span>
|
||||||
{entry.target_id && (
|
{entry.target_id && (
|
||||||
<span className="text-[9px] text-blue-400/80 truncate max-w-[140px]" title={entry.target_id}>
|
<span className="text-[9px] text-accent/80 truncate max-w-[140px]" title={entry.target_id}>
|
||||||
{resolveName(entry.target_id)}
|
{resolveName(entry.target_id)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -247,7 +247,7 @@ function ActivityRow({
|
|||||||
|
|
||||||
{/* Error detail */}
|
{/* Error detail */}
|
||||||
{isError && entry.error_detail && (
|
{isError && entry.error_detail && (
|
||||||
<div className="text-[9px] text-red-400/80 mt-1 truncate">
|
<div className="text-[9px] text-bad/80 mt-1 truncate">
|
||||||
{entry.error_detail}
|
{entry.error_detail}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -255,7 +255,7 @@ function ActivityRow({
|
|||||||
|
|
||||||
{/* Expanded details */}
|
{/* Expanded details */}
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="px-3 pb-3 space-y-2 border-t border-zinc-700/30 mt-1 pt-2">
|
<div className="px-3 pb-3 space-y-2 border-t border-line/30 mt-1 pt-2">
|
||||||
{entry.source_id && (
|
{entry.source_id && (
|
||||||
<Detail label="Source" value={`${resolveName(entry.source_id)} (${entry.source_id.slice(0, 8)})`} />
|
<Detail label="Source" value={`${resolveName(entry.source_id)} (${entry.source_id.slice(0, 8)})`} />
|
||||||
)}
|
)}
|
||||||
@ -278,7 +278,7 @@ function ActivityRow({
|
|||||||
{entry.response_body && (
|
{entry.response_body && (
|
||||||
<JsonBlock label="Response" data={entry.response_body} />
|
<JsonBlock label="Response" data={entry.response_body} />
|
||||||
)}
|
)}
|
||||||
<div className="text-[8px] text-zinc-500 font-mono select-all">
|
<div className="text-[8px] text-ink-soft font-mono select-all">
|
||||||
ID: {entry.id}
|
ID: {entry.id}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -298,10 +298,10 @@ function A2AErrorPreview({ label, raw }: { label: string; raw: string }) {
|
|||||||
const hint = inferA2AErrorHint(detail);
|
const hint = inferA2AErrorHint(detail);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] text-red-400/80 uppercase tracking-wider mb-1">{label} — delivery failed</div>
|
<div className="text-[8px] text-bad/80 uppercase tracking-wider mb-1">{label} — delivery failed</div>
|
||||||
<div className="text-[10px] text-red-300 bg-red-950/30 border border-red-800/40 rounded p-2 space-y-1.5">
|
<div className="text-[10px] text-bad bg-red-950/30 border border-red-800/40 rounded p-2 space-y-1.5">
|
||||||
<div className="font-mono whitespace-pre-wrap break-words max-h-32 overflow-y-auto">{detail}</div>
|
<div className="font-mono whitespace-pre-wrap break-words max-h-32 overflow-y-auto">{detail}</div>
|
||||||
<div className="text-[9px] text-red-300/70 leading-relaxed border-t border-red-800/30 pt-1.5">{hint}</div>
|
<div className="text-[9px] text-bad/70 leading-relaxed border-t border-red-800/30 pt-1.5">{hint}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -326,8 +326,8 @@ function MessagePreview({ label, body }: { label: string; body: Record<string, u
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] text-zinc-500 uppercase tracking-wider mb-1">{label}</div>
|
<div className="text-[8px] text-ink-soft uppercase tracking-wider mb-1">{label}</div>
|
||||||
<div className="text-[10px] text-zinc-300 bg-zinc-900/60 rounded p-2 max-h-32 overflow-y-auto whitespace-pre-wrap break-words">
|
<div className="text-[10px] text-ink-mid bg-surface-sunken/60 rounded p-2 max-h-32 overflow-y-auto whitespace-pre-wrap break-words">
|
||||||
{text.slice(0, 2000)}
|
{text.slice(0, 2000)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -369,8 +369,8 @@ function MessagePreview({ label, body }: { label: string; body: Record<string, u
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] text-zinc-500 uppercase tracking-wider mb-1">{label}</div>
|
<div className="text-[8px] text-ink-soft uppercase tracking-wider mb-1">{label}</div>
|
||||||
<div className="text-[10px] text-zinc-300 bg-zinc-900/60 rounded p-2 max-h-32 overflow-y-auto whitespace-pre-wrap break-words">
|
<div className="text-[10px] text-ink-mid bg-surface-sunken/60 rounded p-2 max-h-32 overflow-y-auto whitespace-pre-wrap break-words">
|
||||||
{text.slice(0, 2000)}
|
{text.slice(0, 2000)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -380,8 +380,8 @@ function MessagePreview({ label, body }: { label: string; body: Record<string, u
|
|||||||
function Detail({ label, value, mono, error: isError }: { label: string; value: string; mono?: boolean; error?: boolean }) {
|
function Detail({ label, value, mono, error: isError }: { label: string; value: string; mono?: boolean; error?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<span className="text-[8px] text-zinc-500 uppercase tracking-wider w-14 shrink-0 pt-0.5">{label}</span>
|
<span className="text-[8px] text-ink-soft uppercase tracking-wider w-14 shrink-0 pt-0.5">{label}</span>
|
||||||
<span className={`text-[9px] break-all ${isError ? "text-red-400" : "text-zinc-300"} ${mono ? "font-mono" : ""}`}>
|
<span className={`text-[9px] break-all ${isError ? "text-bad" : "text-ink-mid"} ${mono ? "font-mono" : ""}`}>
|
||||||
{value}
|
{value}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -391,8 +391,8 @@ function Detail({ label, value, mono, error: isError }: { label: string; value:
|
|||||||
function JsonBlock({ label, data }: { label: string; data: Record<string, unknown> }) {
|
function JsonBlock({ label, data }: { label: string; data: Record<string, unknown> }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] text-zinc-500 uppercase tracking-wider mb-1">{label}</div>
|
<div className="text-[8px] text-ink-soft uppercase tracking-wider mb-1">{label}</div>
|
||||||
<pre className="text-[9px] text-zinc-300 bg-zinc-900/80 rounded p-2 overflow-x-auto max-h-48 font-mono">
|
<pre className="text-[9px] text-ink-mid bg-surface-sunken/80 rounded p-2 overflow-x-auto max-h-48 font-mono">
|
||||||
{JSON.stringify(data, null, 2)}
|
{JSON.stringify(data, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -116,10 +116,10 @@ export function BudgetSection({ workspaceId }: Props) {
|
|||||||
<div className="space-y-3" data-testid="budget-section">
|
<div className="space-y-3" data-testid="budget-section">
|
||||||
{/* Section header */}
|
{/* Section header */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xs font-semibold text-zinc-400 uppercase tracking-wider">
|
<h3 className="text-xs font-semibold text-ink-mid uppercase tracking-wider">
|
||||||
Budget
|
Budget
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[11px] text-zinc-400 mt-0.5">
|
<p className="text-[11px] text-ink-mid mt-0.5">
|
||||||
Limit total message credits for this workspace
|
Limit total message credits for this workspace
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -129,7 +129,7 @@ export function BudgetSection({ workspaceId }: Props) {
|
|||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
data-testid="budget-exceeded-banner"
|
data-testid="budget-exceeded-banner"
|
||||||
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-zinc-950 border border-amber-700/50 text-amber-400 text-xs font-medium"
|
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-surface border border-amber-700/50 text-warm text-xs font-medium"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="13"
|
width="13"
|
||||||
@ -158,21 +158,21 @@ export function BudgetSection({ workspaceId }: Props) {
|
|||||||
|
|
||||||
{/* Usage stats */}
|
{/* Usage stats */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="text-xs text-zinc-500" data-testid="budget-loading">
|
<p className="text-xs text-ink-soft" data-testid="budget-loading">
|
||||||
Loading…
|
Loading…
|
||||||
</p>
|
</p>
|
||||||
) : fetchError ? (
|
) : fetchError ? (
|
||||||
<p className="text-xs text-red-400" data-testid="budget-fetch-error">
|
<p className="text-xs text-bad" data-testid="budget-fetch-error">
|
||||||
{fetchError}
|
{fetchError}
|
||||||
</p>
|
</p>
|
||||||
) : budget ? (
|
) : budget ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{/* Stats row */}
|
{/* Stats row */}
|
||||||
<div className="flex items-baseline justify-between" data-testid="budget-stats-row">
|
<div className="flex items-baseline justify-between" data-testid="budget-stats-row">
|
||||||
<span className="text-xs text-zinc-400">Credits used</span>
|
<span className="text-xs text-ink-mid">Credits used</span>
|
||||||
<span className="text-xs font-mono text-zinc-300">
|
<span className="text-xs font-mono text-ink-mid">
|
||||||
<span data-testid="budget-used-value">{(budget.budget_used ?? 0).toLocaleString()}</span>
|
<span data-testid="budget-used-value">{(budget.budget_used ?? 0).toLocaleString()}</span>
|
||||||
<span className="text-zinc-500 mx-1">/</span>
|
<span className="text-ink-soft mx-1">/</span>
|
||||||
<span data-testid="budget-limit-value">
|
<span data-testid="budget-limit-value">
|
||||||
{budget.budget_limit != null
|
{budget.budget_limit != null
|
||||||
? budget.budget_limit.toLocaleString()
|
? budget.budget_limit.toLocaleString()
|
||||||
@ -189,11 +189,11 @@ export function BudgetSection({ workspaceId }: Props) {
|
|||||||
aria-valuenow={progressPct}
|
aria-valuenow={progressPct}
|
||||||
aria-valuemin={0}
|
aria-valuemin={0}
|
||||||
aria-valuemax={100}
|
aria-valuemax={100}
|
||||||
className="h-1.5 w-full rounded-full bg-zinc-800 overflow-hidden"
|
className="h-1.5 w-full rounded-full bg-surface-card overflow-hidden"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="budget-progress-fill"
|
data-testid="budget-progress-fill"
|
||||||
className="h-full rounded-full bg-blue-500 transition-all duration-300"
|
className="h-full rounded-full bg-accent transition-all duration-300"
|
||||||
style={{ width: `${progressPct}%` }}
|
style={{ width: `${progressPct}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -201,7 +201,7 @@ export function BudgetSection({ workspaceId }: Props) {
|
|||||||
|
|
||||||
{/* Remaining credits */}
|
{/* Remaining credits */}
|
||||||
{budget.budget_remaining != null && (
|
{budget.budget_remaining != null && (
|
||||||
<p className="text-[11px] text-zinc-500" data-testid="budget-remaining">
|
<p className="text-[11px] text-ink-soft" data-testid="budget-remaining">
|
||||||
{budget.budget_remaining.toLocaleString()} credits remaining
|
{budget.budget_remaining.toLocaleString()} credits remaining
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -212,7 +212,7 @@ export function BudgetSection({ workspaceId }: Props) {
|
|||||||
<div className="space-y-1.5 pt-1">
|
<div className="space-y-1.5 pt-1">
|
||||||
<label
|
<label
|
||||||
htmlFor={`budget-limit-input-${workspaceId}`}
|
htmlFor={`budget-limit-input-${workspaceId}`}
|
||||||
className="text-[11px] text-zinc-400 block"
|
className="text-[11px] text-ink-mid block"
|
||||||
>
|
>
|
||||||
Budget limit (credits)
|
Budget limit (credits)
|
||||||
</label>
|
</label>
|
||||||
@ -225,15 +225,15 @@ export function BudgetSection({ workspaceId }: Props) {
|
|||||||
onChange={(e) => setLimitInput(e.target.value)}
|
onChange={(e) => setLimitInput(e.target.value)}
|
||||||
placeholder="e.g. 1000 — blank for unlimited"
|
placeholder="e.g. 1000 — blank for unlimited"
|
||||||
data-testid="budget-limit-input"
|
data-testid="budget-limit-input"
|
||||||
className="w-full bg-zinc-800 border border-zinc-700 rounded-lg px-3 py-2 text-sm text-zinc-300 placeholder-zinc-500 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-colors"
|
className="w-full bg-surface-card border border-line rounded-lg px-3 py-2 text-sm text-ink-mid placeholder-zinc-500 focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30 transition-colors"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-zinc-500">Leave blank for unlimited</p>
|
<p className="text-xs text-ink-soft">Leave blank for unlimited</p>
|
||||||
|
|
||||||
{saveError && (
|
{saveError && (
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
data-testid="budget-save-error"
|
data-testid="budget-save-error"
|
||||||
className="px-3 py-1.5 rounded-lg bg-red-950/40 border border-red-800/50 text-xs text-red-400"
|
className="px-3 py-1.5 rounded-lg bg-red-950/40 border border-red-800/50 text-xs text-bad"
|
||||||
>
|
>
|
||||||
{saveError}
|
{saveError}
|
||||||
</div>
|
</div>
|
||||||
@ -243,7 +243,7 @@ export function BudgetSection({ workspaceId }: Props) {
|
|||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
data-testid="budget-save-btn"
|
data-testid="budget-save-btn"
|
||||||
className="px-4 py-1.5 bg-blue-600 hover:bg-blue-500 active:bg-blue-700 rounded-lg text-xs font-medium text-white disabled:opacity-50 transition-colors"
|
className="px-4 py-1.5 bg-accent-strong hover:bg-accent active:bg-accent-strong rounded-lg text-xs font-medium text-ink disabled:opacity-50 transition-colors"
|
||||||
>
|
>
|
||||||
{saving ? "Saving…" : "Save"}
|
{saving ? "Saving…" : "Save"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -242,7 +242,7 @@ export function ChannelsTab({ workspaceId }: Props) {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 text-zinc-500 text-xs">Loading channels...</div>
|
<div className="p-4 text-ink-soft text-xs">Loading channels...</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,33 +250,33 @@ export function ChannelsTab({ workspaceId }: Props) {
|
|||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-xs font-semibold text-zinc-300 tracking-wide uppercase">
|
<h3 className="text-xs font-semibold text-ink-mid tracking-wide uppercase">
|
||||||
Channels
|
Channels
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowForm(!showForm)}
|
onClick={() => setShowForm(!showForm)}
|
||||||
className="text-[10px] px-2.5 py-1 rounded bg-blue-600/20 text-blue-400 hover:bg-blue-600/30 transition"
|
className="text-[10px] px-2.5 py-1 rounded bg-accent-strong/20 text-accent hover:bg-accent-strong/30 transition"
|
||||||
>
|
>
|
||||||
{showForm ? "Cancel" : "+ Connect"}
|
{showForm ? "Cancel" : "+ Connect"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400">
|
<div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Create form — schema-driven */}
|
{/* Create form — schema-driven */}
|
||||||
{showForm && (
|
{showForm && (
|
||||||
<div className="space-y-2 p-3 bg-zinc-800/40 rounded border border-zinc-700/50">
|
<div className="space-y-2 p-3 bg-surface-card/40 rounded border border-line/50">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={platformId} className="text-[10px] text-zinc-500 block mb-1">Platform</label>
|
<label htmlFor={platformId} className="text-[10px] text-ink-soft block mb-1">Platform</label>
|
||||||
<select
|
<select
|
||||||
id={platformId}
|
id={platformId}
|
||||||
value={formType}
|
value={formType}
|
||||||
onChange={(e) => setFormType(e.target.value)}
|
onChange={(e) => setFormType(e.target.value)}
|
||||||
className="w-full text-xs bg-zinc-900 border border-zinc-700 rounded px-2 py-1.5 text-zinc-300"
|
className="w-full text-xs bg-surface-sunken border border-line rounded px-2 py-1.5 text-ink-mid"
|
||||||
>
|
>
|
||||||
{adapters.map((a) => (
|
{adapters.map((a) => (
|
||||||
<option key={a.type} value={a.type}>{a.display_name}</option>
|
<option key={a.type} value={a.type}>{a.display_name}</option>
|
||||||
@ -308,7 +308,7 @@ export function ChannelsTab({ workspaceId }: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={handleDiscover}
|
onClick={handleDiscover}
|
||||||
disabled={discovering || !formValues["bot_token"]}
|
disabled={discovering || !formValues["bot_token"]}
|
||||||
className="text-[10px] px-2 py-0.5 rounded bg-blue-600/20 text-blue-400 hover:bg-blue-600/30 transition disabled:opacity-40"
|
className="text-[10px] px-2 py-0.5 rounded bg-accent-strong/20 text-accent hover:bg-accent-strong/30 transition disabled:opacity-40"
|
||||||
>
|
>
|
||||||
{discovering ? "Detecting..." : "Detect Chats"}
|
{discovering ? "Detecting..." : "Detect Chats"}
|
||||||
</button>
|
</button>
|
||||||
@ -318,21 +318,21 @@ export function ChannelsTab({ workspaceId }: Props) {
|
|||||||
{discoveredChats.map((chat) => (
|
{discoveredChats.map((chat) => (
|
||||||
<label
|
<label
|
||||||
key={chat.chat_id}
|
key={chat.chat_id}
|
||||||
className="flex items-center gap-2 px-2 py-1.5 bg-zinc-900/50 rounded border border-zinc-700/50 cursor-pointer hover:bg-zinc-800/50"
|
className="flex items-center gap-2 px-2 py-1.5 bg-surface-sunken/50 rounded border border-line/50 cursor-pointer hover:bg-surface-card/50"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedChats.has(chat.chat_id)}
|
checked={selectedChats.has(chat.chat_id)}
|
||||||
onChange={() => toggleChat(chat.chat_id)}
|
onChange={() => toggleChat(chat.chat_id)}
|
||||||
className="rounded border-zinc-600"
|
className="rounded border-line"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-zinc-300">{chat.name || "Unknown"}</span>
|
<span className="text-xs text-ink-mid">{chat.name || "Unknown"}</span>
|
||||||
<span className="text-[10px] text-zinc-500 ml-auto">{chat.type} {chat.chat_id}</span>
|
<span className="text-[10px] text-ink-soft ml-auto">{chat.type} {chat.chat_id}</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowManualInput(!showManualInput)}
|
onClick={() => setShowManualInput(!showManualInput)}
|
||||||
className="text-[10px] text-blue-400 hover:underline"
|
className="text-[10px] text-accent hover:underline"
|
||||||
>
|
>
|
||||||
{showManualInput ? "hide manual input" : "edit manually"}
|
{showManualInput ? "hide manual input" : "edit manually"}
|
||||||
</button>
|
</button>
|
||||||
@ -347,26 +347,26 @@ export function ChannelsTab({ workspaceId }: Props) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={allowedUsersId} className="text-[10px] text-zinc-500 block mb-1">
|
<label htmlFor={allowedUsersId} className="text-[10px] text-ink-soft block mb-1">
|
||||||
Allowed Users <span className="text-zinc-600">(optional, comma-separated)</span>
|
Allowed Users <span className="text-ink-soft">(optional, comma-separated)</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id={allowedUsersId}
|
id={allowedUsersId}
|
||||||
value={formAllowedUsers}
|
value={formAllowedUsers}
|
||||||
onChange={(e) => setFormAllowedUsers(e.target.value)}
|
onChange={(e) => setFormAllowedUsers(e.target.value)}
|
||||||
placeholder="123456789, 987654321"
|
placeholder="123456789, 987654321"
|
||||||
className="w-full text-xs bg-zinc-900 border border-zinc-700 rounded px-2 py-1.5 text-zinc-300 placeholder-zinc-600"
|
className="w-full text-xs bg-surface-sunken border border-line rounded px-2 py-1.5 text-ink-mid placeholder-zinc-600"
|
||||||
/>
|
/>
|
||||||
<p className="text-[11px] text-zinc-500 mt-0.5">
|
<p className="text-[11px] text-ink-soft mt-0.5">
|
||||||
Platform-specific user IDs. Leave empty to allow everyone.
|
Platform-specific user IDs. Leave empty to allow everyone.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{formError && (
|
{formError && (
|
||||||
<p className="text-[10px] text-red-400">{formError}</p>
|
<p className="text-[10px] text-bad">{formError}</p>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
className="w-full text-xs py-1.5 rounded bg-blue-600 hover:bg-blue-500 text-white transition"
|
className="w-full text-xs py-1.5 rounded bg-accent-strong hover:bg-accent text-ink transition"
|
||||||
>
|
>
|
||||||
Connect Channel
|
Connect Channel
|
||||||
</button>
|
</button>
|
||||||
@ -376,8 +376,8 @@ export function ChannelsTab({ workspaceId }: Props) {
|
|||||||
{/* Channel list */}
|
{/* Channel list */}
|
||||||
{channels.length === 0 && !showForm && (
|
{channels.length === 0 && !showForm && (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<p className="text-zinc-500 text-xs">No channels connected</p>
|
<p className="text-ink-soft text-xs">No channels connected</p>
|
||||||
<p className="text-zinc-600 text-[10px] mt-1">
|
<p className="text-ink-soft text-[10px] mt-1">
|
||||||
Connect Telegram, Slack, Discord, or Lark / Feishu to chat with this agent from social platforms.
|
Connect Telegram, Slack, Discord, or Lark / Feishu to chat with this agent from social platforms.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -386,7 +386,7 @@ export function ChannelsTab({ workspaceId }: Props) {
|
|||||||
{channels.map((ch) => (
|
{channels.map((ch) => (
|
||||||
<div
|
<div
|
||||||
key={ch.id}
|
key={ch.id}
|
||||||
className="p-3 bg-zinc-800/30 rounded border border-zinc-700/40 space-y-2"
|
className="p-3 bg-surface-card/30 rounded border border-line/40 space-y-2"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -395,10 +395,10 @@ export function ChannelsTab({ workspaceId }: Props) {
|
|||||||
ch.enabled ? "bg-emerald-500" : "bg-zinc-600"
|
ch.enabled ? "bg-emerald-500" : "bg-zinc-600"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs font-medium text-zinc-200">
|
<span className="text-xs font-medium text-ink">
|
||||||
{ch.channel_type.charAt(0).toUpperCase() + ch.channel_type.slice(1)}
|
{ch.channel_type.charAt(0).toUpperCase() + ch.channel_type.slice(1)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-zinc-500">
|
<span className="text-[10px] text-ink-soft">
|
||||||
{ch.config.chat_id || ch.config.channel_id || ""}
|
{ch.config.chat_id || ch.config.channel_id || ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -406,7 +406,7 @@ export function ChannelsTab({ workspaceId }: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleTest(ch)}
|
onClick={() => handleTest(ch)}
|
||||||
disabled={testing === ch.id}
|
disabled={testing === ch.id}
|
||||||
className="text-[10px] px-2 py-0.5 rounded bg-zinc-700/50 text-zinc-400 hover:text-zinc-200 transition disabled:opacity-50"
|
className="text-[10px] px-2 py-0.5 rounded bg-surface-card/50 text-ink-mid hover:text-ink transition disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{testing === ch.id ? "Sent!" : "Test"}
|
{testing === ch.id ? "Sent!" : "Test"}
|
||||||
</button>
|
</button>
|
||||||
@ -414,21 +414,21 @@ export function ChannelsTab({ workspaceId }: Props) {
|
|||||||
onClick={() => handleToggle(ch)}
|
onClick={() => handleToggle(ch)}
|
||||||
className={`text-[10px] px-2 py-0.5 rounded transition ${
|
className={`text-[10px] px-2 py-0.5 rounded transition ${
|
||||||
ch.enabled
|
ch.enabled
|
||||||
? "bg-emerald-900/30 text-emerald-400 hover:bg-emerald-900/50"
|
? "bg-emerald-900/30 text-good hover:bg-emerald-900/50"
|
||||||
: "bg-zinc-700/50 text-zinc-500 hover:text-zinc-300"
|
: "bg-surface-card/50 text-ink-soft hover:text-ink-mid"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{ch.enabled ? "On" : "Off"}
|
{ch.enabled ? "On" : "Off"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPendingDelete(ch)}
|
onClick={() => setPendingDelete(ch)}
|
||||||
className="text-[10px] px-2 py-0.5 rounded bg-red-900/20 text-red-400 hover:bg-red-900/40 transition"
|
className="text-[10px] px-2 py-0.5 rounded bg-red-900/20 text-bad hover:bg-red-900/40 transition"
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4 text-[10px] text-zinc-500">
|
<div className="flex items-center gap-4 text-[10px] text-ink-soft">
|
||||||
<span>{ch.message_count} messages</span>
|
<span>{ch.message_count} messages</span>
|
||||||
<span>Last: {relativeTime(ch.last_message_at)}</span>
|
<span>Last: {relativeTime(ch.last_message_at)}</span>
|
||||||
{ch.allowed_users.length > 0 && (
|
{ch.allowed_users.length > 0 && (
|
||||||
@ -467,12 +467,12 @@ function SchemaField({
|
|||||||
}) {
|
}) {
|
||||||
const inputId = useId();
|
const inputId = useId();
|
||||||
const common =
|
const common =
|
||||||
"w-full text-xs bg-zinc-900 border border-zinc-700 rounded px-2 py-1.5 text-zinc-300 placeholder-zinc-600";
|
"w-full text-xs bg-surface-sunken border border-line rounded px-2 py-1.5 text-ink-mid placeholder-zinc-600";
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={inputId} className="text-[10px] text-zinc-500 block mb-1">
|
<label htmlFor={inputId} className="text-[10px] text-ink-soft block mb-1">
|
||||||
{field.label}
|
{field.label}
|
||||||
{!field.required && <span className="text-zinc-600"> (optional)</span>}
|
{!field.required && <span className="text-ink-soft"> (optional)</span>}
|
||||||
</label>
|
</label>
|
||||||
{field.type === "textarea" ? (
|
{field.type === "textarea" ? (
|
||||||
<textarea
|
<textarea
|
||||||
@ -495,7 +495,7 @@ function SchemaField({
|
|||||||
)}
|
)}
|
||||||
{renderExtras?.()}
|
{renderExtras?.()}
|
||||||
{field.help && (
|
{field.help && (
|
||||||
<p className="text-[11px] text-zinc-500 mt-0.5">{field.help}</p>
|
<p className="text-[11px] text-ink-soft mt-0.5">{field.help}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -162,7 +162,7 @@ export function ChatTab({ workspaceId, data }: Props) {
|
|||||||
{/* Sub-tab bar — role="tablist" so screen readers expose tab context */}
|
{/* Sub-tab bar — role="tablist" so screen readers expose tab context */}
|
||||||
<div
|
<div
|
||||||
role="tablist"
|
role="tablist"
|
||||||
className="flex border-b border-zinc-800/40 bg-zinc-900/30 px-2 shrink-0"
|
className="flex border-b border-line/40 bg-surface-sunken/30 px-2 shrink-0"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
const tabs: ChatSubTab[] = ["my-chat", "agent-comms"];
|
const tabs: ChatSubTab[] = ["my-chat", "agent-comms"];
|
||||||
const idx = tabs.indexOf(subTab);
|
const idx = tabs.indexOf(subTab);
|
||||||
@ -179,8 +179,8 @@ export function ChatTab({ workspaceId, data }: Props) {
|
|||||||
onClick={() => setSubTab("my-chat")}
|
onClick={() => setSubTab("my-chat")}
|
||||||
className={`px-3 py-1.5 text-[10px] font-medium transition-colors ${
|
className={`px-3 py-1.5 text-[10px] font-medium transition-colors ${
|
||||||
subTab === "my-chat"
|
subTab === "my-chat"
|
||||||
? "text-zinc-200 border-b-2 border-blue-500"
|
? "text-ink border-b-2 border-accent"
|
||||||
: "text-zinc-500 hover:text-zinc-300"
|
: "text-ink-soft hover:text-ink-mid"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
My Chat
|
My Chat
|
||||||
@ -194,8 +194,8 @@ export function ChatTab({ workspaceId, data }: Props) {
|
|||||||
onClick={() => setSubTab("agent-comms")}
|
onClick={() => setSubTab("agent-comms")}
|
||||||
className={`px-3 py-1.5 text-[10px] font-medium transition-colors ${
|
className={`px-3 py-1.5 text-[10px] font-medium transition-colors ${
|
||||||
subTab === "agent-comms"
|
subTab === "agent-comms"
|
||||||
? "text-zinc-200 border-b-2 border-blue-500"
|
? "text-ink border-b-2 border-accent"
|
||||||
: "text-zinc-500 hover:text-zinc-300"
|
: "text-ink-soft hover:text-ink-mid"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Agent Comms
|
Agent Comms
|
||||||
@ -726,10 +726,10 @@ function MyChatPanel({ workspaceId, data }: Props) {
|
|||||||
>
|
>
|
||||||
{dragOver && (
|
{dragOver && (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 z-20 flex items-center justify-center bg-blue-500/10 border-2 border-dashed border-blue-400 rounded pointer-events-none"
|
className="absolute inset-0 z-20 flex items-center justify-center bg-accent/10 border-2 border-dashed border-blue-400 rounded pointer-events-none"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
>
|
>
|
||||||
<div className="bg-zinc-900/90 border border-blue-400/50 rounded-lg px-4 py-2 text-xs text-blue-200">
|
<div className="bg-surface-sunken/90 border border-blue-400/50 rounded-lg px-4 py-2 text-xs text-blue-200">
|
||||||
Drop to attach
|
Drop to attach
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -737,14 +737,14 @@ function MyChatPanel({ workspaceId, data }: Props) {
|
|||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<div className="flex-1 overflow-y-auto p-3 space-y-3">
|
<div className="flex-1 overflow-y-auto p-3 space-y-3">
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="text-xs text-zinc-500 text-center py-4">Loading chat history...</div>
|
<div className="text-xs text-ink-soft text-center py-4">Loading chat history...</div>
|
||||||
)}
|
)}
|
||||||
{!loading && loadError !== null && messages.length === 0 && (
|
{!loading && loadError !== null && messages.length === 0 && (
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
className="mx-2 mt-2 rounded-lg border border-red-800/50 bg-red-950/30 px-3 py-2.5"
|
className="mx-2 mt-2 rounded-lg border border-red-800/50 bg-red-950/30 px-3 py-2.5"
|
||||||
>
|
>
|
||||||
<p className="text-[11px] text-red-400 mb-1.5">
|
<p className="text-[11px] text-bad mb-1.5">
|
||||||
Failed to load chat history: {loadError}
|
Failed to load chat history: {loadError}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
@ -757,14 +757,14 @@ function MyChatPanel({ workspaceId, data }: Props) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="text-[10px] px-2 py-0.5 rounded bg-red-800/40 text-red-300 hover:bg-red-700/50 transition-colors"
|
className="text-[10px] px-2 py-0.5 rounded bg-red-800/40 text-bad hover:bg-red-700/50 transition-colors"
|
||||||
>
|
>
|
||||||
Retry
|
Retry
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!loading && loadError === null && messages.length === 0 && (
|
{!loading && loadError === null && messages.length === 0 && (
|
||||||
<div className="text-xs text-zinc-500 text-center py-8">
|
<div className="text-xs text-ink-soft text-center py-8">
|
||||||
No messages yet. Send a message to start chatting with this agent.
|
No messages yet. Send a message to start chatting with this agent.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -773,10 +773,10 @@ function MyChatPanel({ workspaceId, data }: Props) {
|
|||||||
<div
|
<div
|
||||||
className={`max-w-[85%] rounded-lg px-3 py-2 text-xs ${
|
className={`max-w-[85%] rounded-lg px-3 py-2 text-xs ${
|
||||||
msg.role === "user"
|
msg.role === "user"
|
||||||
? "bg-blue-600/30 text-blue-100 border border-blue-500/20"
|
? "bg-accent-strong/30 text-blue-100 border border-accent/20"
|
||||||
: msg.role === "system"
|
: msg.role === "system"
|
||||||
? "bg-red-900/30 text-red-200 border border-red-800/30"
|
? "bg-red-900/30 text-red-200 border border-red-800/30"
|
||||||
: "bg-zinc-800/80 text-zinc-200 border border-zinc-700/30"
|
: "bg-surface-card/80 text-ink border border-line/30"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{msg.content && (
|
{msg.content && (
|
||||||
@ -796,7 +796,7 @@ function MyChatPanel({ workspaceId, data }: Props) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-[9px] text-zinc-500 mt-1">
|
<div className="text-[9px] text-ink-soft mt-1">
|
||||||
{new Date(msg.timestamp).toLocaleTimeString()}
|
{new Date(msg.timestamp).toLocaleTimeString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -809,8 +809,8 @@ function MyChatPanel({ workspaceId, data }: Props) {
|
|||||||
without locking the Send button on a stale currentTask). */}
|
without locking the Send button on a stale currentTask). */}
|
||||||
{(sending || !!data.currentTask) && (
|
{(sending || !!data.currentTask) && (
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<div className="bg-zinc-800/50 border border-zinc-700/30 rounded-lg px-3 py-2 max-w-[85%]">
|
<div className="bg-surface-card/50 border border-line/30 rounded-lg px-3 py-2 max-w-[85%]">
|
||||||
<div className="flex items-center gap-2 text-xs text-zinc-400">
|
<div className="flex items-center gap-2 text-xs text-ink-mid">
|
||||||
<span className="flex gap-0.5">
|
<span className="flex gap-0.5">
|
||||||
<span className="w-1.5 h-1.5 bg-zinc-500 rounded-full motion-safe:animate-bounce" style={{ animationDelay: "0ms" }} />
|
<span className="w-1.5 h-1.5 bg-zinc-500 rounded-full motion-safe:animate-bounce" style={{ animationDelay: "0ms" }} />
|
||||||
<span className="w-1.5 h-1.5 bg-zinc-500 rounded-full motion-safe:animate-bounce" style={{ animationDelay: "150ms" }} />
|
<span className="w-1.5 h-1.5 bg-zinc-500 rounded-full motion-safe:animate-bounce" style={{ animationDelay: "150ms" }} />
|
||||||
@ -819,10 +819,10 @@ function MyChatPanel({ workspaceId, data }: Props) {
|
|||||||
{thinkingElapsed}s
|
{thinkingElapsed}s
|
||||||
</div>
|
</div>
|
||||||
{activityLog.length > 0 && (
|
{activityLog.length > 0 && (
|
||||||
<div className="mt-1.5 text-[9px] text-zinc-500 space-y-0.5">
|
<div className="mt-1.5 text-[9px] text-ink-soft space-y-0.5">
|
||||||
<div className="text-zinc-400">Processing with {runtimeDisplayName(data.runtime)}...</div>
|
<div className="text-ink-mid">Processing with {runtimeDisplayName(data.runtime)}...</div>
|
||||||
{activityLog.map((line, i) => (
|
{activityLog.map((line, i) => (
|
||||||
<div key={line + i} className="pl-2 border-l border-zinc-700">◇ {line}</div>
|
<div key={line + i} className="pl-2 border-l border-line">◇ {line}</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -836,11 +836,11 @@ function MyChatPanel({ workspaceId, data }: Props) {
|
|||||||
{error && (
|
{error && (
|
||||||
<div className="px-3 py-2 bg-red-900/20 border-t border-red-800/30">
|
<div className="px-3 py-2 bg-red-900/20 border-t border-red-800/30">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-[10px] text-red-400">{error}</span>
|
<span className="text-[10px] text-bad">{error}</span>
|
||||||
{!isOnline && (
|
{!isOnline && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfirmRestart(true)}
|
onClick={() => setConfirmRestart(true)}
|
||||||
className="text-[11px] px-2 py-0.5 bg-red-800/40 text-red-300 rounded hover:bg-red-700/50"
|
className="text-[11px] px-2 py-0.5 bg-red-800/40 text-bad rounded hover:bg-red-700/50"
|
||||||
>
|
>
|
||||||
Restart
|
Restart
|
||||||
</button>
|
</button>
|
||||||
@ -850,7 +850,7 @@ function MyChatPanel({ workspaceId, data }: Props) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Input */}
|
{/* Input */}
|
||||||
<div className="p-3 border-t border-zinc-800">
|
<div className="p-3 border-t border-line">
|
||||||
{pendingFiles.length > 0 && (
|
{pendingFiles.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1.5 mb-2">
|
<div className="flex flex-wrap gap-1.5 mb-2">
|
||||||
{pendingFiles.map((f, i) => (
|
{pendingFiles.map((f, i) => (
|
||||||
@ -876,7 +876,7 @@ function MyChatPanel({ workspaceId, data }: Props) {
|
|||||||
disabled={!agentReachable || sending || uploading}
|
disabled={!agentReachable || sending || uploading}
|
||||||
aria-label="Attach file"
|
aria-label="Attach file"
|
||||||
title="Attach file"
|
title="Attach file"
|
||||||
className="p-2 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg text-zinc-400 hover:text-zinc-200 transition-colors shrink-0 disabled:opacity-40"
|
className="p-2 bg-surface-card hover:bg-surface-card border border-line rounded-lg text-ink-mid hover:text-ink transition-colors shrink-0 disabled:opacity-40"
|
||||||
>
|
>
|
||||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||||
<path d="M11 6.5 7 10.5a2 2 0 1 0 2.8 2.8l4-4a3.5 3.5 0 0 0-5-5l-4.5 4.5a5 5 0 0 0 7 7l4-4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M11 6.5 7 10.5a2 2 0 1 0 2.8 2.8l4-4a3.5 3.5 0 0 0-5-5l-4.5 4.5a5 5 0 0 0 7 7l4-4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
@ -896,12 +896,12 @@ function MyChatPanel({ workspaceId, data }: Props) {
|
|||||||
placeholder={agentReachable ? "Send a message... (Shift+Enter for new line, paste images to attach)" : `Agent is ${data.status}`}
|
placeholder={agentReachable ? "Send a message... (Shift+Enter for new line, paste images to attach)" : `Agent is ${data.status}`}
|
||||||
disabled={!agentReachable || sending}
|
disabled={!agentReachable || sending}
|
||||||
rows={1}
|
rows={1}
|
||||||
className="flex-1 bg-zinc-800 border border-zinc-700 rounded-lg px-3 py-2 text-xs text-zinc-200 placeholder-zinc-500 focus:outline-none focus:border-blue-500 resize-none disabled:opacity-50"
|
className="flex-1 bg-surface-card border border-line rounded-lg px-3 py-2 text-xs text-ink placeholder-zinc-500 focus:outline-none focus:border-accent resize-none disabled:opacity-50"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={sendMessage}
|
onClick={sendMessage}
|
||||||
disabled={(!input.trim() && pendingFiles.length === 0) || !agentReachable || sending || uploading}
|
disabled={(!input.trim() && pendingFiles.length === 0) || !agentReachable || sending || uploading}
|
||||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-xs font-medium rounded-lg text-white disabled:opacity-30 transition-colors shrink-0"
|
className="px-4 py-2 bg-accent-strong hover:bg-accent text-xs font-medium rounded-lg text-ink disabled:opacity-30 transition-colors shrink-0"
|
||||||
>
|
>
|
||||||
{uploading ? "Uploading…" : "Send"}
|
{uploading ? "Uploading…" : "Send"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -53,37 +53,37 @@ function AgentCardSection({ workspaceId }: { workspaceId: string }) {
|
|||||||
return (
|
return (
|
||||||
<Section title="Agent Card" defaultOpen={false}>
|
<Section title="Agent Card" defaultOpen={false}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-[10px] text-zinc-500">Loading...</div>
|
<div className="text-[10px] text-ink-soft">Loading...</div>
|
||||||
) : editing ? (
|
) : editing ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<textarea
|
<textarea
|
||||||
aria-label="Agent card JSON editor"
|
aria-label="Agent card JSON editor"
|
||||||
value={draft} onChange={(e) => setDraft(e.target.value)}
|
value={draft} onChange={(e) => setDraft(e.target.value)}
|
||||||
spellCheck={false} rows={12}
|
spellCheck={false} rows={12}
|
||||||
className="w-full bg-zinc-800 border border-zinc-700 rounded p-2 text-[10px] font-mono text-zinc-200 focus:outline-none focus:border-blue-500 resize-none"
|
className="w-full bg-surface-card border border-line rounded p-2 text-[10px] font-mono text-ink focus:outline-none focus:border-accent resize-none"
|
||||||
/>
|
/>
|
||||||
{error && <div className="px-2 py-1 bg-red-900/30 border border-red-800 rounded text-[10px] text-red-400">{error}</div>}
|
{error && <div className="px-2 py-1 bg-red-900/30 border border-red-800 rounded text-[10px] text-bad">{error}</div>}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button type="button" onClick={handleSave} disabled={saving}
|
<button type="button" onClick={handleSave} disabled={saving}
|
||||||
className="px-2 py-1 bg-blue-600 hover:bg-blue-500 text-[10px] rounded text-white disabled:opacity-50">
|
className="px-2 py-1 bg-accent-strong hover:bg-accent text-[10px] rounded text-ink disabled:opacity-50">
|
||||||
{saving ? "Saving..." : "Save"}
|
{saving ? "Saving..." : "Save"}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => setEditing(false)}
|
<button type="button" onClick={() => setEditing(false)}
|
||||||
className="px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-300">Cancel</button>
|
className="px-2 py-1 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink-mid">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{card ? (
|
{card ? (
|
||||||
<pre className="text-[9px] text-zinc-400 bg-zinc-800/50 rounded p-2 overflow-x-auto max-h-48 border border-zinc-700/50">
|
<pre className="text-[9px] text-ink-mid bg-surface-card/50 rounded p-2 overflow-x-auto max-h-48 border border-line/50">
|
||||||
{JSON.stringify(card, null, 2)}
|
{JSON.stringify(card, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-[10px] text-zinc-500">No agent card</div>
|
<div className="text-[10px] text-ink-soft">No agent card</div>
|
||||||
)}
|
)}
|
||||||
{success && <div className="mt-2 px-2 py-1 bg-green-900/30 border border-green-800 rounded text-[10px] text-green-400">Updated</div>}
|
{success && <div className="mt-2 px-2 py-1 bg-green-900/30 border border-green-800 rounded text-[10px] text-good">Updated</div>}
|
||||||
<button type="button" onClick={() => { setDraft(JSON.stringify(card || {}, null, 2)); setEditing(true); setError(null); setSuccess(false); }}
|
<button type="button" onClick={() => { setDraft(JSON.stringify(card || {}, null, 2)); setEditing(true); setError(null); setSuccess(false); }}
|
||||||
className="mt-2 text-[10px] text-blue-400 hover:text-blue-300">Edit Agent Card</button>
|
className="mt-2 text-[10px] text-accent hover:text-accent">Edit Agent Card</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
@ -592,16 +592,16 @@ export function ConfigTab({ workspaceId }: Props) {
|
|||||||
const isDirty = (rawMode ? rawDraft !== originalYaml : toYaml(config) !== originalYaml) || providerDirty;
|
const isDirty = (rawMode ? rawDraft !== originalYaml : toYaml(config) !== originalYaml) || providerDirty;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="p-4 text-xs text-zinc-500">Loading config...</div>;
|
return <div className="p-4 text-xs text-ink-soft">Loading config...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Mode toggle */}
|
{/* Mode toggle */}
|
||||||
<div className="flex items-center justify-between px-3 py-1.5 border-b border-zinc-800/40 bg-zinc-900/30">
|
<div className="flex items-center justify-between px-3 py-1.5 border-b border-line/40 bg-surface-sunken/30">
|
||||||
<span className="text-[10px] text-zinc-500">config.yaml</span>
|
<span className="text-[10px] text-ink-soft">config.yaml</span>
|
||||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||||
<span className="text-[9px] text-zinc-500">Raw YAML</span>
|
<span className="text-[9px] text-ink-soft">Raw YAML</span>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={rawMode}
|
checked={rawMode}
|
||||||
@ -626,7 +626,7 @@ export function ConfigTab({ workspaceId }: Props) {
|
|||||||
value={rawDraft}
|
value={rawDraft}
|
||||||
onChange={(e) => setRawDraft(e.target.value)}
|
onChange={(e) => setRawDraft(e.target.value)}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="w-full h-full min-h-[300px] bg-zinc-800 border border-zinc-700 rounded p-3 text-xs font-mono text-zinc-200 focus:outline-none focus:border-blue-500 resize-none"
|
className="w-full h-full min-h-[300px] bg-surface-card border border-line rounded p-3 text-xs font-mono text-ink focus:outline-none focus:border-accent resize-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -634,24 +634,24 @@ export function ConfigTab({ workspaceId }: Props) {
|
|||||||
<Section title="General">
|
<Section title="General">
|
||||||
<TextInput label="Name" value={config.name} onChange={(v) => update("name", v)} />
|
<TextInput label="Name" value={config.name} onChange={(v) => update("name", v)} />
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={descriptionId} className="text-[10px] text-zinc-500 block mb-1">Description</label>
|
<label htmlFor={descriptionId} className="text-[10px] text-ink-soft block mb-1">Description</label>
|
||||||
<textarea
|
<textarea
|
||||||
id={descriptionId}
|
id={descriptionId}
|
||||||
value={config.description}
|
value={config.description}
|
||||||
onChange={(e) => update("description", e.target.value)}
|
onChange={(e) => update("description", e.target.value)}
|
||||||
rows={3}
|
rows={3}
|
||||||
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500 resize-none"
|
className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent resize-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<TextInput label="Version" value={config.version} onChange={(v) => update("version", v)} mono />
|
<TextInput label="Version" value={config.version} onChange={(v) => update("version", v)} mono />
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={tierId} className="text-[10px] text-zinc-500 block mb-1">Tier</label>
|
<label htmlFor={tierId} className="text-[10px] text-ink-soft block mb-1">Tier</label>
|
||||||
<select
|
<select
|
||||||
id={tierId}
|
id={tierId}
|
||||||
value={config.tier}
|
value={config.tier}
|
||||||
onChange={(e) => update("tier", parseInt(e.target.value, 10))}
|
onChange={(e) => update("tier", parseInt(e.target.value, 10))}
|
||||||
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500"
|
className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent"
|
||||||
>
|
>
|
||||||
<option value={1}>T1 — Sandboxed</option>
|
<option value={1}>T1 — Sandboxed</option>
|
||||||
<option value={2}>T2 — Standard</option>
|
<option value={2}>T2 — Standard</option>
|
||||||
@ -663,12 +663,12 @@ export function ConfigTab({ workspaceId }: Props) {
|
|||||||
|
|
||||||
<Section title="Runtime">
|
<Section title="Runtime">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={runtimeId} className="text-[10px] text-zinc-500 block mb-1">Runtime</label>
|
<label htmlFor={runtimeId} className="text-[10px] text-ink-soft block mb-1">Runtime</label>
|
||||||
<select
|
<select
|
||||||
id={runtimeId}
|
id={runtimeId}
|
||||||
value={config.runtime || ""}
|
value={config.runtime || ""}
|
||||||
onChange={(e) => update("runtime", e.target.value)}
|
onChange={(e) => update("runtime", e.target.value)}
|
||||||
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500"
|
className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent"
|
||||||
>
|
>
|
||||||
{runtimeOptions.map((opt) => (
|
{runtimeOptions.map((opt) => (
|
||||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||||
@ -747,7 +747,7 @@ export function ConfigTab({ workspaceId }: Props) {
|
|||||||
// workspace_secrets MODEL_PROVIDER override.
|
// workspace_secrets MODEL_PROVIDER override.
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] text-zinc-500 block mb-1">Model</label>
|
<label className="text-[10px] text-ink-soft block mb-1">Model</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={currentModelId}
|
value={currentModelId}
|
||||||
@ -760,13 +760,13 @@ export function ConfigTab({ workspaceId }: Props) {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
placeholder="e.g. anthropic:claude-sonnet-4-6"
|
placeholder="e.g. anthropic:claude-sonnet-4-6"
|
||||||
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 font-mono focus:outline-none focus:border-blue-500"
|
className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink font-mono focus:outline-none focus:border-accent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={`${runtimeId}-provider`} className="text-[10px] text-zinc-500 block mb-1">
|
<label htmlFor={`${runtimeId}-provider`} className="text-[10px] text-ink-soft block mb-1">
|
||||||
Provider
|
Provider
|
||||||
<span className="ml-1 text-zinc-600">
|
<span className="ml-1 text-ink-soft">
|
||||||
(override — leave empty to auto-derive from model slug)
|
(override — leave empty to auto-derive from model slug)
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@ -787,7 +787,7 @@ export function ConfigTab({ workspaceId }: Props) {
|
|||||||
}
|
}
|
||||||
aria-label="LLM provider override"
|
aria-label="LLM provider override"
|
||||||
data-testid="provider-input"
|
data-testid="provider-input"
|
||||||
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 font-mono focus:outline-none focus:border-blue-500"
|
className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink font-mono focus:outline-none focus:border-accent"
|
||||||
/>
|
/>
|
||||||
{providerSuggestionsList.length > 0 && (
|
{providerSuggestionsList.length > 0 && (
|
||||||
<datalist id={`${runtimeId}-providers`}>
|
<datalist id={`${runtimeId}-providers`}>
|
||||||
@ -800,7 +800,7 @@ export function ConfigTab({ workspaceId }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{provider && provider !== originalProvider && (
|
{provider && provider !== originalProvider && (
|
||||||
<p className="text-[10px] text-amber-500 mt-1">
|
<p className="text-[10px] text-warm mt-1">
|
||||||
Provider change → workspace will auto-restart on Save.
|
Provider change → workspace will auto-restart on Save.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -815,7 +815,7 @@ export function ConfigTab({ workspaceId }: Props) {
|
|||||||
onChange={(v) => updateNested("runtime_config" as keyof ConfigData, "required_env", v)}
|
onChange={(v) => updateNested("runtime_config" as keyof ConfigData, "required_env", v)}
|
||||||
placeholder="variable NAME (e.g. ANTHROPIC_API_KEY) — not the value"
|
placeholder="variable NAME (e.g. ANTHROPIC_API_KEY) — not the value"
|
||||||
/>
|
/>
|
||||||
<p className="text-[10px] text-zinc-500 mt-1">
|
<p className="text-[10px] text-ink-soft mt-1">
|
||||||
This declares which env var <em>names</em> the workspace needs.
|
This declares which env var <em>names</em> the workspace needs.
|
||||||
Set the actual values in the <strong>Secrets</strong> section
|
Set the actual values in the <strong>Secrets</strong> section
|
||||||
below — those are encrypted and mounted into the container at
|
below — those are encrypted and mounted into the container at
|
||||||
@ -823,16 +823,16 @@ export function ConfigTab({ workspaceId }: Props) {
|
|||||||
</p>
|
</p>
|
||||||
{currentModelSpec?.required_env?.length &&
|
{currentModelSpec?.required_env?.length &&
|
||||||
!arraysEqual(config.runtime_config?.required_env ?? [], currentModelSpec.required_env) && (
|
!arraysEqual(config.runtime_config?.required_env ?? [], currentModelSpec.required_env) && (
|
||||||
<div className="text-[10px] text-zinc-500 mt-1 flex items-center gap-2">
|
<div className="text-[10px] text-ink-soft mt-1 flex items-center gap-2">
|
||||||
<span>
|
<span>
|
||||||
Template suggests{" "}
|
Template suggests{" "}
|
||||||
<code className="text-zinc-400">{currentModelSpec.required_env.join(", ")}</code>{" "}
|
<code className="text-ink-mid">{currentModelSpec.required_env.join(", ")}</code>{" "}
|
||||||
for <code className="text-zinc-400">{currentModelSpec.name || currentModelSpec.id}</code>.
|
for <code className="text-ink-mid">{currentModelSpec.name || currentModelSpec.id}</code>.
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateNested("runtime_config" as keyof ConfigData, "required_env", currentModelSpec.required_env)}
|
onClick={() => updateNested("runtime_config" as keyof ConfigData, "required_env", currentModelSpec.required_env)}
|
||||||
className="text-blue-400 hover:text-blue-300 underline"
|
className="text-accent hover:text-accent underline"
|
||||||
>
|
>
|
||||||
Apply
|
Apply
|
||||||
</button>
|
</button>
|
||||||
@ -846,15 +846,15 @@ export function ConfigTab({ workspaceId }: Props) {
|
|||||||
(config.runtime_config?.model || config.model || "").toLowerCase().includes("anthropic")) && (
|
(config.runtime_config?.model || config.model || "").toLowerCase().includes("anthropic")) && (
|
||||||
<Section title="Claude Settings" defaultOpen={false}>
|
<Section title="Claude Settings" defaultOpen={false}>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={effortId} className="text-[10px] text-zinc-500 block mb-1">
|
<label htmlFor={effortId} className="text-[10px] text-ink-soft block mb-1">
|
||||||
Effort
|
Effort
|
||||||
<span className="ml-1 text-zinc-600">(output_config.effort — Opus 4.7+)</span>
|
<span className="ml-1 text-ink-soft">(output_config.effort — Opus 4.7+)</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id={effortId}
|
id={effortId}
|
||||||
value={config.effort || ""}
|
value={config.effort || ""}
|
||||||
onChange={(e) => update("effort", e.target.value)}
|
onChange={(e) => update("effort", e.target.value)}
|
||||||
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500"
|
className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent"
|
||||||
data-testid="effort-select"
|
data-testid="effort-select"
|
||||||
>
|
>
|
||||||
<option value="">— unset (model default) —</option>
|
<option value="">— unset (model default) —</option>
|
||||||
@ -866,9 +866,9 @@ export function ConfigTab({ workspaceId }: Props) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={taskBudgetId} className="text-[10px] text-zinc-500 block mb-1">
|
<label htmlFor={taskBudgetId} className="text-[10px] text-ink-soft block mb-1">
|
||||||
Task Budget (tokens)
|
Task Budget (tokens)
|
||||||
<span className="ml-1 text-zinc-600">(output_config.task_budget.total — 0 = unset)</span>
|
<span className="ml-1 text-ink-soft">(output_config.task_budget.total — 0 = unset)</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id={taskBudgetId}
|
id={taskBudgetId}
|
||||||
@ -878,7 +878,7 @@ export function ConfigTab({ workspaceId }: Props) {
|
|||||||
value={config.task_budget ?? 0}
|
value={config.task_budget ?? 0}
|
||||||
onChange={(e) => update("task_budget", parseInt(e.target.value, 10) || 0)}
|
onChange={(e) => update("task_budget", parseInt(e.target.value, 10) || 0)}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500 font-mono"
|
className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent font-mono"
|
||||||
data-testid="task-budget-input"
|
data-testid="task-budget-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -909,12 +909,12 @@ export function ConfigTab({ workspaceId }: Props) {
|
|||||||
|
|
||||||
<Section title="Sandbox" defaultOpen={false}>
|
<Section title="Sandbox" defaultOpen={false}>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={sandboxBackendId} className="text-[10px] text-zinc-500 block mb-1">Backend</label>
|
<label htmlFor={sandboxBackendId} className="text-[10px] text-ink-soft block mb-1">Backend</label>
|
||||||
<select
|
<select
|
||||||
id={sandboxBackendId}
|
id={sandboxBackendId}
|
||||||
value={config.sandbox?.backend || "docker"}
|
value={config.sandbox?.backend || "docker"}
|
||||||
onChange={(e) => updateNested("sandbox" as keyof ConfigData, "backend", e.target.value)}
|
onChange={(e) => updateNested("sandbox" as keyof ConfigData, "backend", e.target.value)}
|
||||||
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500"
|
className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent"
|
||||||
>
|
>
|
||||||
<option value="subprocess">subprocess</option>
|
<option value="subprocess">subprocess</option>
|
||||||
<option value="docker">docker</option>
|
<option value="docker">docker</option>
|
||||||
@ -937,25 +937,25 @@ export function ConfigTab({ workspaceId }: Props) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mx-3 mb-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400">{error}</div>
|
<div className="mx-3 mb-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">{error}</div>
|
||||||
)}
|
)}
|
||||||
{!error && RUNTIMES_WITH_OWN_CONFIG.has(config.runtime || "") && (
|
{!error && RUNTIMES_WITH_OWN_CONFIG.has(config.runtime || "") && (
|
||||||
<div className="mx-3 mb-2 px-3 py-1.5 bg-zinc-900/50 border border-zinc-700 rounded text-xs text-zinc-400">
|
<div className="mx-3 mb-2 px-3 py-1.5 bg-surface-sunken/50 border border-line rounded text-xs text-ink-mid">
|
||||||
{config.runtime === "hermes"
|
{config.runtime === "hermes"
|
||||||
? "Hermes manages its own config at ~/.hermes/config.yaml on the workspace host. Edit it via the Terminal tab or the hermes CLI, not this form."
|
? "Hermes manages its own config at ~/.hermes/config.yaml on the workspace host. Edit it via the Terminal tab or the hermes CLI, not this form."
|
||||||
: "This runtime manages its own config outside the platform template."}
|
: "This runtime manages its own config outside the platform template."}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{success && (
|
{success && (
|
||||||
<div className="mx-3 mb-2 px-3 py-1.5 bg-green-900/30 border border-green-800 rounded text-xs text-green-400">Saved</div>
|
<div className="mx-3 mb-2 px-3 py-1.5 bg-green-900/30 border border-green-800 rounded text-xs text-good">Saved</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="p-3 border-t border-zinc-800 flex gap-2">
|
<div className="p-3 border-t border-line flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleSave(true)}
|
onClick={() => handleSave(true)}
|
||||||
disabled={!isDirty || saving}
|
disabled={!isDirty || saving}
|
||||||
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-xs rounded text-white disabled:opacity-30 transition-colors"
|
className="px-3 py-1.5 bg-accent-strong hover:bg-accent text-xs rounded text-ink disabled:opacity-30 transition-colors"
|
||||||
>
|
>
|
||||||
{saving ? "Restarting..." : "Save & Restart"}
|
{saving ? "Restarting..." : "Save & Restart"}
|
||||||
</button>
|
</button>
|
||||||
@ -963,14 +963,14 @@ export function ConfigTab({ workspaceId }: Props) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleSave(false)}
|
onClick={() => handleSave(false)}
|
||||||
disabled={!isDirty || saving}
|
disabled={!isDirty || saving}
|
||||||
className="px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-xs rounded text-zinc-300 disabled:opacity-30 transition-colors"
|
className="px-3 py-1.5 bg-surface-card hover:bg-zinc-600 text-xs rounded text-ink-mid disabled:opacity-30 transition-colors"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={loadConfig}
|
onClick={loadConfig}
|
||||||
className="px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-xs rounded text-zinc-300 ml-auto"
|
className="px-3 py-1.5 bg-surface-card hover:bg-zinc-600 text-xs rounded text-ink-mid ml-auto"
|
||||||
>
|
>
|
||||||
Reload
|
Reload
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -133,7 +133,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
|||||||
<input
|
<input
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
className="w-full bg-zinc-800 border border-zinc-600 rounded px-2 py-1 text-sm text-zinc-100 focus:outline-none focus:border-blue-500"
|
className="w-full bg-surface-card border border-line rounded px-2 py-1 text-sm text-ink focus:outline-none focus:border-accent"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Role">
|
<Field label="Role">
|
||||||
@ -141,14 +141,14 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
|||||||
value={role}
|
value={role}
|
||||||
onChange={(e) => setRole(e.target.value)}
|
onChange={(e) => setRole(e.target.value)}
|
||||||
placeholder="e.g. SEO Specialist"
|
placeholder="e.g. SEO Specialist"
|
||||||
className="w-full bg-zinc-800 border border-zinc-600 rounded px-2 py-1 text-sm text-zinc-100 focus:outline-none focus:border-blue-500"
|
className="w-full bg-surface-card border border-line rounded px-2 py-1 text-sm text-ink focus:outline-none focus:border-accent"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Tier">
|
<Field label="Tier">
|
||||||
<select
|
<select
|
||||||
value={tier}
|
value={tier}
|
||||||
onChange={(e) => setTier(Number(e.target.value))}
|
onChange={(e) => setTier(Number(e.target.value))}
|
||||||
className="w-full bg-zinc-800 border border-zinc-600 rounded px-2 py-1 text-sm text-zinc-100 focus:outline-none focus:border-blue-500"
|
className="w-full bg-surface-card border border-line rounded px-2 py-1 text-sm text-ink focus:outline-none focus:border-accent"
|
||||||
>
|
>
|
||||||
<option value={1}>Tier 1 — No privileges</option>
|
<option value={1}>Tier 1 — No privileges</option>
|
||||||
<option value={2}>Tier 2 — Browser</option>
|
<option value={2}>Tier 2 — Browser</option>
|
||||||
@ -157,7 +157,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
|||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
{saveError && (
|
{saveError && (
|
||||||
<div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400">
|
<div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">
|
||||||
{saveError}
|
{saveError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -166,7 +166,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className="px-3 py-1 bg-blue-600 hover:bg-blue-500 text-xs rounded text-white disabled:opacity-50"
|
className="px-3 py-1 bg-accent-strong hover:bg-accent text-xs rounded text-ink disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{saving ? "Saving..." : "Save"}
|
{saving ? "Saving..." : "Save"}
|
||||||
</button>
|
</button>
|
||||||
@ -179,7 +179,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
|||||||
setRole(data.role || "");
|
setRole(data.role || "");
|
||||||
setTier(data.tier);
|
setTier(data.tier);
|
||||||
}}
|
}}
|
||||||
className="px-3 py-1 bg-zinc-700 hover:bg-zinc-600 text-xs rounded text-zinc-300"
|
className="px-3 py-1 bg-surface-card hover:bg-zinc-600 text-xs rounded text-ink-mid"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@ -200,7 +200,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
|||||||
{isRestartable && (
|
{isRestartable && (
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
{restartError && (
|
{restartError && (
|
||||||
<div className="mb-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400">
|
<div className="mb-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">
|
||||||
{restartError}
|
{restartError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -208,7 +208,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleRestart}
|
onClick={handleRestart}
|
||||||
disabled={restarting}
|
disabled={restarting}
|
||||||
className="px-3 py-1 bg-green-700 hover:bg-green-600 text-xs rounded text-white disabled:opacity-50"
|
className="px-3 py-1 bg-green-700 hover:bg-green-600 text-xs rounded text-ink disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{restarting ? "Restarting..." : data.status === "failed" ? "Retry" : "Restart"}
|
{restarting ? "Restarting..." : data.status === "failed" ? "Retry" : "Restart"}
|
||||||
</button>
|
</button>
|
||||||
@ -217,7 +217,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setEditing(true)}
|
onClick={() => setEditing(true)}
|
||||||
className="mt-2 px-3 py-1 bg-zinc-700 hover:bg-zinc-600 text-xs rounded text-zinc-300"
|
className="mt-2 px-3 py-1 bg-surface-card hover:bg-zinc-600 text-xs rounded text-ink-mid"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
@ -234,17 +234,17 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
|||||||
{data.lastSampleError ? (
|
{data.lastSampleError ? (
|
||||||
<pre
|
<pre
|
||||||
data-testid="details-error-log"
|
data-testid="details-error-log"
|
||||||
className="text-[11px] text-red-300 font-mono whitespace-pre-wrap break-all bg-red-950/20 border border-red-900/40 rounded p-2 max-h-[240px] overflow-auto leading-tight"
|
className="text-[11px] text-bad font-mono whitespace-pre-wrap break-all bg-red-950/20 border border-red-900/40 rounded p-2 max-h-[240px] overflow-auto leading-tight"
|
||||||
>
|
>
|
||||||
{data.lastSampleError}
|
{data.lastSampleError}
|
||||||
</pre>
|
</pre>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-zinc-500">No error detail recorded.</p>
|
<p className="text-xs text-ink-soft">No error detail recorded.</p>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setConsoleOpen(true)}
|
onClick={() => setConsoleOpen(true)}
|
||||||
className="mt-2 px-3 py-1 bg-zinc-800 hover:bg-zinc-700 text-xs rounded text-zinc-300 border border-zinc-700"
|
className="mt-2 px-3 py-1 bg-surface-card hover:bg-surface-card text-xs rounded text-ink-mid border border-line"
|
||||||
>
|
>
|
||||||
View console output
|
View console output
|
||||||
</button>
|
</button>
|
||||||
@ -263,9 +263,9 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{skills.map((s) => (
|
{skills.map((s) => (
|
||||||
<div key={s.id} className="flex items-start gap-2">
|
<div key={s.id} className="flex items-start gap-2">
|
||||||
<span className="text-xs text-blue-400 font-mono shrink-0">{s.id}</span>
|
<span className="text-xs text-accent font-mono shrink-0">{s.id}</span>
|
||||||
{s.description && (
|
{s.description && (
|
||||||
<span className="text-xs text-zinc-500">{s.description}</span>
|
<span className="text-xs text-ink-soft">{s.description}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -276,13 +276,13 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
|||||||
{/* Peers */}
|
{/* Peers */}
|
||||||
<Section title={`Peers (${peers.length})`}>
|
<Section title={`Peers (${peers.length})`}>
|
||||||
{peersError ? (
|
{peersError ? (
|
||||||
<p className="text-xs text-red-400">{peersError}</p>
|
<p className="text-xs text-bad">{peersError}</p>
|
||||||
) : peers.length === 0 && data.status !== "online" && data.status !== "degraded" ? (
|
) : peers.length === 0 && data.status !== "online" && data.status !== "degraded" ? (
|
||||||
<p className="text-xs text-zinc-500">
|
<p className="text-xs text-ink-soft">
|
||||||
Peers are only discoverable while the workspace is online.
|
Peers are only discoverable while the workspace is online.
|
||||||
</p>
|
</p>
|
||||||
) : peers.length === 0 ? (
|
) : peers.length === 0 ? (
|
||||||
<p className="text-xs text-zinc-500">No reachable peers</p>
|
<p className="text-xs text-ink-soft">No reachable peers</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{peers.map((p) => (
|
{peers.map((p) => (
|
||||||
@ -290,11 +290,11 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
|||||||
key={p.id}
|
key={p.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => selectNode(p.id)}
|
onClick={() => selectNode(p.id)}
|
||||||
className="w-full flex items-center gap-2 px-2 py-1 rounded hover:bg-zinc-800 text-left"
|
className="w-full flex items-center gap-2 px-2 py-1 rounded hover:bg-surface-card text-left"
|
||||||
>
|
>
|
||||||
<StatusDot status={p.status} />
|
<StatusDot status={p.status} />
|
||||||
<span className="text-xs text-zinc-200">{p.name}</span>
|
<span className="text-xs text-ink">{p.name}</span>
|
||||||
{p.role && <span className="text-[10px] text-zinc-500">{p.role}</span>}
|
{p.role && <span className="text-[10px] text-ink-soft">{p.role}</span>}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -304,7 +304,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
|||||||
{/* Delete */}
|
{/* Delete */}
|
||||||
<Section title="Danger Zone">
|
<Section title="Danger Zone">
|
||||||
{deleteError && (
|
{deleteError && (
|
||||||
<div className="mb-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400">
|
<div className="mb-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">
|
||||||
{deleteError}
|
{deleteError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -315,14 +315,14 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
|||||||
aria-labelledby="delete-confirm-title"
|
aria-labelledby="delete-confirm-title"
|
||||||
className="space-y-2"
|
className="space-y-2"
|
||||||
>
|
>
|
||||||
<h3 id="delete-confirm-title" className="text-xs font-medium text-red-400">
|
<h3 id="delete-confirm-title" className="text-xs font-medium text-bad">
|
||||||
Confirm deletion
|
Confirm deletion
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
className="px-3 py-1 bg-red-600 hover:bg-red-500 text-xs rounded text-white"
|
className="px-3 py-1 bg-red-600 hover:bg-red-500 text-xs rounded text-ink"
|
||||||
>
|
>
|
||||||
Confirm Delete
|
Confirm Delete
|
||||||
</button>
|
</button>
|
||||||
@ -334,7 +334,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
|||||||
// Return focus to the trigger so keyboard users aren't stranded
|
// Return focus to the trigger so keyboard users aren't stranded
|
||||||
deleteButtonRef.current?.focus();
|
deleteButtonRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
className="px-3 py-1 bg-zinc-700 hover:bg-zinc-600 text-xs rounded text-zinc-300"
|
className="px-3 py-1 bg-surface-card hover:bg-zinc-600 text-xs rounded text-ink-mid"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@ -345,7 +345,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
|||||||
type="button"
|
type="button"
|
||||||
ref={deleteButtonRef}
|
ref={deleteButtonRef}
|
||||||
onClick={() => setConfirmDelete(true)}
|
onClick={() => setConfirmDelete(true)}
|
||||||
className="px-3 py-1 bg-zinc-800 hover:bg-red-900 border border-zinc-700 hover:border-red-700 text-xs rounded text-zinc-400 hover:text-red-400 transition-colors"
|
className="px-3 py-1 bg-surface-card hover:bg-red-900 border border-line hover:border-red-700 text-xs rounded text-ink-mid hover:text-bad transition-colors"
|
||||||
>
|
>
|
||||||
Delete Workspace
|
Delete Workspace
|
||||||
</button>
|
</button>
|
||||||
@ -367,7 +367,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
|||||||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xs font-semibold text-zinc-400 uppercase tracking-wider mb-2">{title}</h3>
|
<h3 className="text-xs font-semibold text-ink-mid uppercase tracking-wider mb-2">{title}</h3>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -377,7 +377,7 @@ function Field({ label, children }: { label: string; children: React.ReactNode }
|
|||||||
const fieldId = useId();
|
const fieldId = useId();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={fieldId} className="text-[10px] text-zinc-500 block mb-0.5">{label}</label>
|
<label htmlFor={fieldId} className="text-[10px] text-ink-soft block mb-0.5">{label}</label>
|
||||||
{cloneElement(children as ReactElement<{ id?: string }>, { id: fieldId })}
|
{cloneElement(children as ReactElement<{ id?: string }>, { id: fieldId })}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -386,8 +386,8 @@ function Field({ label, children }: { label: string; children: React.ReactNode }
|
|||||||
function Row({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
|
function Row({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-xs text-zinc-500">{label}</span>
|
<span className="text-xs text-ink-soft">{label}</span>
|
||||||
<span className={`text-xs text-zinc-200 ${mono ? "font-mono" : ""} text-right max-w-[200px] truncate`}>
|
<span className={`text-xs text-ink ${mono ? "font-mono" : ""} text-right max-w-[200px] truncate`}>
|
||||||
{value}
|
{value}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -16,12 +16,12 @@ interface EventEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EVENT_COLORS: Record<string, string> = {
|
const EVENT_COLORS: Record<string, string> = {
|
||||||
WORKSPACE_ONLINE: "text-green-400",
|
WORKSPACE_ONLINE: "text-good",
|
||||||
WORKSPACE_OFFLINE: "text-zinc-400",
|
WORKSPACE_OFFLINE: "text-ink-mid",
|
||||||
WORKSPACE_DEGRADED: "text-yellow-400",
|
WORKSPACE_DEGRADED: "text-yellow-400",
|
||||||
WORKSPACE_PROVISIONING: "text-blue-400",
|
WORKSPACE_PROVISIONING: "text-accent",
|
||||||
WORKSPACE_REMOVED: "text-red-400",
|
WORKSPACE_REMOVED: "text-bad",
|
||||||
WORKSPACE_PROVISION_FAILED: "text-red-400",
|
WORKSPACE_PROVISION_FAILED: "text-bad",
|
||||||
AGENT_CARD_UPDATED: "text-purple-400",
|
AGENT_CARD_UPDATED: "text-purple-400",
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -56,58 +56,58 @@ export function EventsTab({ workspaceId }: Props) {
|
|||||||
}, [loadEvents]);
|
}, [loadEvents]);
|
||||||
|
|
||||||
if (loading && events.length === 0) {
|
if (loading && events.length === 0) {
|
||||||
return <div className="p-4 text-xs text-zinc-500">Loading events...</div>;
|
return <div className="p-4 text-xs text-ink-soft">Loading events...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 space-y-2">
|
<div className="p-4 space-y-2">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-xs text-zinc-400">{events.length} events</span>
|
<span className="text-xs text-ink-mid">{events.length} events</span>
|
||||||
<button
|
<button
|
||||||
onClick={loadEvents}
|
onClick={loadEvents}
|
||||||
className="px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-300"
|
className="px-2 py-1 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink-mid"
|
||||||
>
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400">
|
<div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!error && events.length === 0 ? (
|
{!error && events.length === 0 ? (
|
||||||
<p className="text-xs text-zinc-500 text-center py-4">No events yet</p>
|
<p className="text-xs text-ink-soft text-center py-4">No events yet</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{events.map((event) => (
|
{events.map((event) => (
|
||||||
<div key={event.id} className="bg-zinc-800 rounded border border-zinc-700">
|
<div key={event.id} className="bg-surface-card rounded border border-line">
|
||||||
<button
|
<button
|
||||||
onClick={() => setExpanded(expanded === event.id ? null : event.id)}
|
onClick={() => setExpanded(expanded === event.id ? null : event.id)}
|
||||||
className="w-full flex items-center gap-2 px-3 py-2 text-left"
|
className="w-full flex items-center gap-2 px-3 py-2 text-left"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`text-xs font-mono ${
|
className={`text-xs font-mono ${
|
||||||
EVENT_COLORS[event.event_type] || "text-zinc-300"
|
EVENT_COLORS[event.event_type] || "text-ink-mid"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{event.event_type}
|
{event.event_type}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[9px] text-zinc-500 ml-auto">
|
<span className="text-[9px] text-ink-soft ml-auto">
|
||||||
{formatTime(event.created_at)}
|
{formatTime(event.created_at)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-zinc-500">
|
<span className="text-[10px] text-ink-soft">
|
||||||
{expanded === event.id ? "▼" : "▶"}
|
{expanded === event.id ? "▼" : "▶"}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{expanded === event.id && (
|
{expanded === event.id && (
|
||||||
<div className="px-3 pb-2">
|
<div className="px-3 pb-2">
|
||||||
<pre className="text-[10px] text-zinc-300 bg-zinc-900 rounded p-2 overflow-x-auto max-h-40">
|
<pre className="text-[10px] text-ink-mid bg-surface-sunken rounded p-2 overflow-x-auto max-h-40">
|
||||||
{JSON.stringify(event.payload, null, 2)}
|
{JSON.stringify(event.payload, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
<div className="mt-1 text-[9px] text-zinc-500 font-mono">
|
<div className="mt-1 text-[9px] text-ink-soft font-mono">
|
||||||
ID: {event.id}
|
ID: {event.id}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -145,7 +145,7 @@ export function FilesTab({ workspaceId }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="p-4 text-xs text-zinc-500">Loading files...</div>;
|
return <div className="p-4 text-xs text-ink-soft">Loading files...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -163,34 +163,34 @@ export function FilesTab({ workspaceId }: Props) {
|
|||||||
|
|
||||||
{showDeleteAll && (
|
{showDeleteAll && (
|
||||||
<div className="mx-3 mt-2 px-3 py-2 bg-red-950/30 border border-red-800/40 rounded space-y-1.5">
|
<div className="mx-3 mt-2 px-3 py-2 bg-red-950/30 border border-red-800/40 rounded space-y-1.5">
|
||||||
<p className="text-xs text-red-300">Delete all {files.filter((f) => !f.dir).length} files? This cannot be undone.</p>
|
<p className="text-xs text-bad">Delete all {files.filter((f) => !f.dir).length} files? This cannot be undone.</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button type="button" onClick={() => { handleDeleteAll(); setShowDeleteAll(false); }} className="px-2 py-0.5 bg-red-600 hover:bg-red-500 text-[10px] rounded text-white">Delete All</button>
|
<button type="button" onClick={() => { handleDeleteAll(); setShowDeleteAll(false); }} className="px-2 py-0.5 bg-red-600 hover:bg-red-500 text-[10px] rounded text-ink">Delete All</button>
|
||||||
<button type="button" onClick={() => setShowDeleteAll(false)} className="px-2 py-0.5 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-300">Cancel</button>
|
<button type="button" onClick={() => setShowDeleteAll(false)} className="px-2 py-0.5 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink-mid">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mx-3 mt-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400">{error}</div>
|
<div className="mx-3 mt-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">{error}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{confirmDelete && (
|
{confirmDelete && (
|
||||||
<div className="mx-3 mt-2 px-3 py-2 bg-amber-950/30 border border-amber-800/40 rounded space-y-1.5">
|
<div className="mx-3 mt-2 px-3 py-2 bg-amber-950/30 border border-amber-800/40 rounded space-y-1.5">
|
||||||
<p className="text-xs text-amber-300">Delete <span className="font-mono">{confirmDelete}</span>{files.find((f) => f.path === confirmDelete && f.dir) ? " and all its contents" : ""}?</p>
|
<p className="text-xs text-warm">Delete <span className="font-mono">{confirmDelete}</span>{files.find((f) => f.path === confirmDelete && f.dir) ? " and all its contents" : ""}?</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button type="button" onClick={confirmDeleteFile} className="px-2 py-0.5 bg-red-600 hover:bg-red-500 text-[10px] rounded text-white">Delete</button>
|
<button type="button" onClick={confirmDeleteFile} className="px-2 py-0.5 bg-red-600 hover:bg-red-500 text-[10px] rounded text-ink">Delete</button>
|
||||||
<button type="button" onClick={() => setConfirmDelete(null)} className="px-2 py-0.5 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-300">Cancel</button>
|
<button type="button" onClick={() => setConfirmDelete(null)} className="px-2 py-0.5 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink-mid">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-1 min-h-0">
|
<div className="flex flex-1 min-h-0">
|
||||||
{/* File tree */}
|
{/* File tree */}
|
||||||
<div className="w-[180px] border-r border-zinc-800/40 overflow-y-auto shrink-0">
|
<div className="w-[180px] border-r border-line/40 overflow-y-auto shrink-0">
|
||||||
{/* New file input */}
|
{/* New file input */}
|
||||||
{showNewFile && (
|
{showNewFile && (
|
||||||
<div className="px-2 py-1 border-b border-zinc-800/40">
|
<div className="px-2 py-1 border-b border-line/40">
|
||||||
<input
|
<input
|
||||||
aria-label="New file path"
|
aria-label="New file path"
|
||||||
value={newFileName}
|
value={newFileName}
|
||||||
@ -198,13 +198,13 @@ export function FilesTab({ workspaceId }: Props) {
|
|||||||
onKeyDown={(e) => e.key === "Enter" && createFile()}
|
onKeyDown={(e) => e.key === "Enter" && createFile()}
|
||||||
placeholder="path/file.md"
|
placeholder="path/file.md"
|
||||||
autoFocus
|
autoFocus
|
||||||
className="w-full bg-zinc-800 border border-zinc-600 rounded px-1.5 py-0.5 text-[10px] text-zinc-100 font-mono focus:outline-none focus:border-blue-500"
|
className="w-full bg-surface-card border border-line rounded px-1.5 py-0.5 text-[10px] text-ink font-mono focus:outline-none focus:border-accent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{files.length === 0 ? (
|
{files.length === 0 ? (
|
||||||
<div className="px-3 py-4 text-[10px] text-zinc-600 text-center">
|
<div className="px-3 py-4 text-[10px] text-ink-soft text-center">
|
||||||
No config files yet
|
No config files yet
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export function FileEditor({
|
|||||||
<div className="flex-1 flex items-center justify-center">
|
<div className="flex-1 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl opacity-20 mb-2">📄</div>
|
<div className="text-2xl opacity-20 mb-2">📄</div>
|
||||||
<p className="text-[10px] text-zinc-600">Select a file to edit</p>
|
<p className="text-[10px] text-ink-soft">Select a file to edit</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -45,18 +45,18 @@ export function FileEditor({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* File header */}
|
{/* File header */}
|
||||||
<div className="flex items-center justify-between px-3 py-1.5 border-b border-zinc-800/40 bg-zinc-900/20">
|
<div className="flex items-center justify-between px-3 py-1.5 border-b border-line/40 bg-surface-sunken/20">
|
||||||
<div className="flex items-center gap-1.5 min-w-0">
|
<div className="flex items-center gap-1.5 min-w-0">
|
||||||
<span className="text-[10px] opacity-50">{getIcon(selectedFile, false)}</span>
|
<span className="text-[10px] opacity-50">{getIcon(selectedFile, false)}</span>
|
||||||
<span className="text-[10px] font-mono text-zinc-300 truncate">{selectedFile}</span>
|
<span className="text-[10px] font-mono text-ink-mid truncate">{selectedFile}</span>
|
||||||
{isDirty && <span className="text-[9px] text-amber-400">modified</span>}
|
{isDirty && <span className="text-[9px] text-warm">modified</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{success && <span className="text-[9px] text-emerald-400">{success}</span>}
|
{success && <span className="text-[9px] text-good">{success}</span>}
|
||||||
<button
|
<button
|
||||||
onClick={onDownload}
|
onClick={onDownload}
|
||||||
aria-label="Download file"
|
aria-label="Download file"
|
||||||
className="text-[10px] text-zinc-500 hover:text-zinc-300"
|
className="text-[10px] text-ink-soft hover:text-ink-mid"
|
||||||
>
|
>
|
||||||
↓
|
↓
|
||||||
</button>
|
</button>
|
||||||
@ -64,7 +64,7 @@ export function FileEditor({
|
|||||||
<button
|
<button
|
||||||
onClick={onSave}
|
onClick={onSave}
|
||||||
disabled={!isDirty || saving}
|
disabled={!isDirty || saving}
|
||||||
className="text-[10px] text-blue-400 hover:text-blue-300 disabled:opacity-30"
|
className="text-[10px] text-accent hover:text-accent disabled:opacity-30"
|
||||||
>
|
>
|
||||||
{saving ? "Saving..." : "Save"}
|
{saving ? "Saving..." : "Save"}
|
||||||
</button>
|
</button>
|
||||||
@ -74,7 +74,7 @@ export function FileEditor({
|
|||||||
|
|
||||||
{/* Editor area */}
|
{/* Editor area */}
|
||||||
{loadingFile ? (
|
{loadingFile ? (
|
||||||
<div className="p-4 text-xs text-zinc-500">Loading...</div>
|
<div className="p-4 text-xs text-ink-soft">Loading...</div>
|
||||||
) : (
|
) : (
|
||||||
<textarea
|
<textarea
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
@ -103,7 +103,7 @@ export function FileEditor({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="flex-1 w-full bg-zinc-950 p-3 text-[11px] font-mono text-zinc-200 leading-relaxed resize-none focus:outline-none"
|
className="flex-1 w-full bg-surface p-3 text-[11px] font-mono text-ink leading-relaxed resize-none focus:outline-none"
|
||||||
style={{ tabSize: 2 }}
|
style={{ tabSize: 2 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -58,20 +58,20 @@ function TreeItem({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className="group w-full flex items-center gap-1 px-2 py-0.5 text-left hover:bg-zinc-800/40 transition-colors cursor-pointer"
|
className="group w-full flex items-center gap-1 px-2 py-0.5 text-left hover:bg-surface-card/40 transition-colors cursor-pointer"
|
||||||
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
||||||
onClick={() => onToggleDir(node.path)}
|
onClick={() => onToggleDir(node.path)}
|
||||||
>
|
>
|
||||||
<span className="text-[9px] text-zinc-500 w-3">{isLoading ? "…" : expanded ? "▼" : "▶"}</span>
|
<span className="text-[9px] text-ink-soft w-3">{isLoading ? "…" : expanded ? "▼" : "▶"}</span>
|
||||||
<span className="text-[10px]">📁</span>
|
<span className="text-[10px]">📁</span>
|
||||||
<span className="text-[10px] text-zinc-400 flex-1">{node.name}</span>
|
<span className="text-[10px] text-ink-mid flex-1">{node.name}</span>
|
||||||
<button
|
<button
|
||||||
aria-label={`Delete ${node.name}`}
|
aria-label={`Delete ${node.name}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onDelete(node.path);
|
onDelete(node.path);
|
||||||
}}
|
}}
|
||||||
className="text-[9px] text-red-400/0 group-hover:text-red-400/60 hover:!text-red-400 transition-colors"
|
className="text-[9px] text-bad/0 group-hover:text-bad/60 hover:!text-bad transition-colors"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
@ -95,7 +95,7 @@ function TreeItem({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`group flex items-center gap-1 px-2 py-0.5 cursor-pointer transition-colors ${
|
className={`group flex items-center gap-1 px-2 py-0.5 cursor-pointer transition-colors ${
|
||||||
isSelected ? "bg-blue-900/30 text-zinc-100" : "hover:bg-zinc-800/40 text-zinc-400"
|
isSelected ? "bg-blue-900/30 text-ink" : "hover:bg-surface-card/40 text-ink-mid"
|
||||||
}`}
|
}`}
|
||||||
style={{ paddingLeft: `${depth * 12 + 20}px` }}
|
style={{ paddingLeft: `${depth * 12 + 20}px` }}
|
||||||
onClick={() => onSelect(node.path)}
|
onClick={() => onSelect(node.path)}
|
||||||
@ -108,7 +108,7 @@ function TreeItem({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onDelete(node.path);
|
onDelete(node.path);
|
||||||
}}
|
}}
|
||||||
className="text-[9px] text-red-400/0 group-hover:text-red-400/60 hover:!text-red-400 transition-colors"
|
className="text-[9px] text-bad/0 group-hover:text-bad/60 hover:!text-bad transition-colors"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -26,25 +26,25 @@ export function FilesToolbar({
|
|||||||
const uploadRef = useRef<HTMLInputElement>(null);
|
const uploadRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between px-3 py-2 border-b border-zinc-800/40 bg-zinc-900/30">
|
<div className="flex items-center justify-between px-3 py-2 border-b border-line/40 bg-surface-sunken/30">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<select
|
<select
|
||||||
value={root}
|
value={root}
|
||||||
onChange={(e) => setRoot(e.target.value)}
|
onChange={(e) => setRoot(e.target.value)}
|
||||||
aria-label="File root directory"
|
aria-label="File root directory"
|
||||||
className="text-[10px] bg-zinc-800 text-zinc-300 border border-zinc-700 rounded px-1.5 py-0.5 outline-none"
|
className="text-[10px] bg-surface-card text-ink-mid border border-line rounded px-1.5 py-0.5 outline-none"
|
||||||
>
|
>
|
||||||
<option value="/configs">/configs</option>
|
<option value="/configs">/configs</option>
|
||||||
<option value="/home">/home</option>
|
<option value="/home">/home</option>
|
||||||
<option value="/workspace">/workspace</option>
|
<option value="/workspace">/workspace</option>
|
||||||
<option value="/plugins">/plugins</option>
|
<option value="/plugins">/plugins</option>
|
||||||
</select>
|
</select>
|
||||||
<span className="text-[10px] text-zinc-500">{fileCount} files</span>
|
<span className="text-[10px] text-ink-soft">{fileCount} files</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
{root === "/configs" && (
|
{root === "/configs" && (
|
||||||
<>
|
<>
|
||||||
<button type="button" onClick={onNewFile} aria-label="Create new file" className="text-[10px] text-blue-400 hover:text-blue-300" title="Create new file">
|
<button type="button" onClick={onNewFile} aria-label="Create new file" className="text-[10px] text-accent hover:text-accent" title="Create new file">
|
||||||
+ New
|
+ New
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
@ -57,20 +57,20 @@ export function FilesToolbar({
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(e) => e.target.files && onUpload(e.target.files)}
|
onChange={(e) => e.target.files && onUpload(e.target.files)}
|
||||||
/>
|
/>
|
||||||
<button type="button" onClick={() => uploadRef.current?.click()} aria-label="Upload folder" className="text-[10px] text-blue-400 hover:text-blue-300" title="Upload folder">
|
<button type="button" onClick={() => uploadRef.current?.click()} aria-label="Upload folder" className="text-[10px] text-accent hover:text-accent" title="Upload folder">
|
||||||
Upload
|
Upload
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<button type="button" onClick={onDownloadAll} aria-label="Download all files" className="text-[10px] text-zinc-500 hover:text-zinc-300" title="Download all files">
|
<button type="button" onClick={onDownloadAll} aria-label="Download all files" className="text-[10px] text-ink-soft hover:text-ink-mid" title="Download all files">
|
||||||
Export
|
Export
|
||||||
</button>
|
</button>
|
||||||
{root === "/configs" && (
|
{root === "/configs" && (
|
||||||
<button type="button" onClick={onClearAll} aria-label="Delete all files" className="text-[10px] text-red-400/60 hover:text-red-400" title="Delete all files">
|
<button type="button" onClick={onClearAll} aria-label="Delete all files" className="text-[10px] text-bad/60 hover:text-bad" title="Delete all files">
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button type="button" onClick={onRefresh} aria-label="Refresh file list" className="text-[10px] text-zinc-500 hover:text-zinc-300" title="Refresh">
|
<button type="button" onClick={onRefresh} aria-label="Refresh file list" className="text-[10px] text-ink-soft hover:text-ink-mid" title="Refresh">
|
||||||
↻
|
↻
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -114,13 +114,13 @@ export function MemoryTab({ workspaceId }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="p-4 text-xs text-zinc-500">Loading memory...</div>;
|
return <div className="p-4 text-xs text-ink-soft">Loading memory...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
{error && !showAdd && (
|
{error && !showAdd && (
|
||||||
<div role="alert" className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400">
|
<div role="alert" className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -128,8 +128,8 @@ export function MemoryTab({ workspaceId }: Props) {
|
|||||||
<section className="space-y-3">
|
<section className="space-y-3">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-medium text-zinc-200">Awareness dashboard</div>
|
<div className="text-xs font-medium text-ink">Awareness dashboard</div>
|
||||||
<p className="text-[10px] text-zinc-500">
|
<p className="text-[10px] text-ink-soft">
|
||||||
Embedded view for the local Awareness memory UI. The current workspace id is appended to the URL for workspace-scoped routing or future filtering.
|
Embedded view for the local Awareness memory UI. The current workspace id is appended to the URL for workspace-scoped routing or future filtering.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -137,14 +137,14 @@ export function MemoryTab({ workspaceId }: Props) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowAwareness((prev) => !prev)}
|
onClick={() => setShowAwareness((prev) => !prev)}
|
||||||
className="shrink-0 px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-200"
|
className="shrink-0 px-2 py-1 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink"
|
||||||
>
|
>
|
||||||
{showAwareness ? "Collapse" : "Expand"}
|
{showAwareness ? "Collapse" : "Expand"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={openAwareness}
|
onClick={openAwareness}
|
||||||
className="shrink-0 px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-200"
|
className="shrink-0 px-2 py-1 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink"
|
||||||
>
|
>
|
||||||
Open
|
Open
|
||||||
</button>
|
</button>
|
||||||
@ -153,7 +153,7 @@ export function MemoryTab({ workspaceId }: Props) {
|
|||||||
|
|
||||||
{showAwareness ? (
|
{showAwareness ? (
|
||||||
AWARENESS_BASE_URL ? (
|
AWARENESS_BASE_URL ? (
|
||||||
<div className="overflow-hidden rounded-xl border border-zinc-800 bg-zinc-900/70 shadow-[0_0_0_1px_rgba(255,255,255,0.02)]">
|
<div className="overflow-hidden rounded-xl border border-line bg-surface-sunken/70 shadow-[0_0_0_1px_rgba(255,255,255,0.02)]">
|
||||||
<iframe
|
<iframe
|
||||||
title="Awareness dashboard"
|
title="Awareness dashboard"
|
||||||
src={awarenessUrl}
|
src={awarenessUrl}
|
||||||
@ -162,71 +162,71 @@ export function MemoryTab({ workspaceId }: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-xl border border-dashed border-zinc-800 bg-zinc-900/40 p-4 text-xs text-zinc-500">
|
<div className="rounded-xl border border-dashed border-line bg-surface-sunken/40 p-4 text-xs text-ink-soft">
|
||||||
Set <code className="font-mono text-zinc-300">NEXT_PUBLIC_AWARENESS_URL</code> to embed the Awareness dashboard here.
|
Set <code className="font-mono text-ink-mid">NEXT_PUBLIC_AWARENESS_URL</code> to embed the Awareness dashboard here.
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 px-4 py-3 flex items-center justify-between gap-3">
|
<div className="rounded-xl border border-line bg-surface-sunken/50 px-4 py-3 flex items-center justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-xs text-zinc-200">Awareness dashboard is collapsed</p>
|
<p className="text-xs text-ink">Awareness dashboard is collapsed</p>
|
||||||
<p className="text-[10px] text-zinc-500 truncate">
|
<p className="text-[10px] text-ink-soft truncate">
|
||||||
Workspace context stays linked through <span className="font-mono text-zinc-400">{workspaceId}</span>.
|
Workspace context stays linked through <span className="font-mono text-ink-mid">{workspaceId}</span>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowAwareness(true)}
|
onClick={() => setShowAwareness(true)}
|
||||||
className="shrink-0 px-2 py-1 bg-blue-600 hover:bg-blue-500 text-[10px] rounded text-white"
|
className="shrink-0 px-2 py-1 bg-accent-strong hover:bg-accent text-[10px] rounded text-ink"
|
||||||
>
|
>
|
||||||
Expand
|
Expand
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid gap-2 rounded-xl border border-zinc-800 bg-zinc-950/40 px-3 py-2 text-[10px] text-zinc-400 sm:grid-cols-3">
|
<div className="grid gap-2 rounded-xl border border-line bg-surface/40 px-3 py-2 text-[10px] text-ink-mid sm:grid-cols-3">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="uppercase tracking-[0.18em] text-zinc-500">Status</span>
|
<span className="uppercase tracking-[0.18em] text-ink-soft">Status</span>
|
||||||
<span className="font-medium text-emerald-300">Connected</span>
|
<span className="font-medium text-good">Connected</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="uppercase tracking-[0.18em] text-zinc-500">Mode</span>
|
<span className="uppercase tracking-[0.18em] text-ink-soft">Mode</span>
|
||||||
<span className="font-medium text-zinc-200">{awarenessStatus}</span>
|
<span className="font-medium text-ink">{awarenessStatus}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between gap-2 min-w-0">
|
<div className="flex items-center justify-between gap-2 min-w-0">
|
||||||
<span className="uppercase tracking-[0.18em] text-zinc-500">Workspace</span>
|
<span className="uppercase tracking-[0.18em] text-ink-soft">Workspace</span>
|
||||||
<span className="font-mono text-zinc-300 truncate">{workspaceId}</span>
|
<span className="font-mono text-ink-mid truncate">{workspaceId}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="space-y-3 border-t border-zinc-800/60 pt-4">
|
<section className="space-y-3 border-t border-line/60 pt-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-medium text-zinc-200">Workspace KV memory</div>
|
<div className="text-xs font-medium text-ink">Workspace KV memory</div>
|
||||||
<p className="text-[10px] text-zinc-500">
|
<p className="text-[10px] text-ink-soft">
|
||||||
Native platform key-value memory for workspace <span className="font-mono text-zinc-400">{workspaceId}</span>.
|
Native platform key-value memory for workspace <span className="font-mono text-ink-mid">{workspaceId}</span>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowAdvanced((prev) => !prev)}
|
onClick={() => setShowAdvanced((prev) => !prev)}
|
||||||
className="px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-300"
|
className="px-2 py-1 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink-mid"
|
||||||
>
|
>
|
||||||
{showAdvanced ? "Hide Advanced" : "Advanced"}
|
{showAdvanced ? "Hide Advanced" : "Advanced"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={loadMemory}
|
onClick={loadMemory}
|
||||||
className="px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-300"
|
className="px-2 py-1 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink-mid"
|
||||||
>
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setShowAdd(!showAdd); if (!showAdd) setShowAdvanced(true); }}
|
onClick={() => { setShowAdd(!showAdd); if (!showAdd) setShowAdvanced(true); }}
|
||||||
className="px-2 py-1 bg-blue-600 hover:bg-blue-500 text-[10px] rounded text-white"
|
className="px-2 py-1 bg-accent-strong hover:bg-accent text-[10px] rounded text-ink"
|
||||||
>
|
>
|
||||||
+ Add
|
+ Add
|
||||||
</button>
|
</button>
|
||||||
@ -234,13 +234,13 @@ export function MemoryTab({ workspaceId }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showAdvanced && showAdd && (
|
{showAdvanced && showAdd && (
|
||||||
<div className="bg-zinc-800 rounded p-3 space-y-2 border border-zinc-700">
|
<div className="bg-surface-card rounded p-3 space-y-2 border border-line">
|
||||||
<input
|
<input
|
||||||
value={newKey}
|
value={newKey}
|
||||||
onChange={(e) => setNewKey(e.target.value)}
|
onChange={(e) => setNewKey(e.target.value)}
|
||||||
placeholder="Key"
|
placeholder="Key"
|
||||||
aria-label="Memory key"
|
aria-label="Memory key"
|
||||||
className="w-full bg-zinc-900 border border-zinc-600 rounded px-2 py-1 text-xs text-zinc-100 focus:outline-none focus:border-blue-500"
|
className="w-full bg-surface-sunken border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent"
|
||||||
/>
|
/>
|
||||||
<textarea
|
<textarea
|
||||||
value={newValue}
|
value={newValue}
|
||||||
@ -248,21 +248,21 @@ export function MemoryTab({ workspaceId }: Props) {
|
|||||||
placeholder='Value (JSON or plain text)'
|
placeholder='Value (JSON or plain text)'
|
||||||
rows={3}
|
rows={3}
|
||||||
aria-label="Memory value (JSON or plain text)"
|
aria-label="Memory value (JSON or plain text)"
|
||||||
className="w-full bg-zinc-900 border border-zinc-600 rounded px-2 py-1 text-xs font-mono text-zinc-100 focus:outline-none focus:border-blue-500 resize-none"
|
className="w-full bg-surface-sunken border border-line rounded px-2 py-1 text-xs font-mono text-ink focus:outline-none focus:border-accent resize-none"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
value={newTTL}
|
value={newTTL}
|
||||||
onChange={(e) => setNewTTL(e.target.value)}
|
onChange={(e) => setNewTTL(e.target.value)}
|
||||||
placeholder="TTL in seconds (optional)"
|
placeholder="TTL in seconds (optional)"
|
||||||
aria-label="TTL in seconds (optional)"
|
aria-label="TTL in seconds (optional)"
|
||||||
className="w-full bg-zinc-900 border border-zinc-600 rounded px-2 py-1 text-xs text-zinc-100 focus:outline-none focus:border-blue-500"
|
className="w-full bg-surface-sunken border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent"
|
||||||
/>
|
/>
|
||||||
{error && <div role="alert" className="text-xs text-red-400">{error}</div>}
|
{error && <div role="alert" className="text-xs text-bad">{error}</div>}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
className="px-3 py-1 bg-blue-600 hover:bg-blue-500 text-xs rounded text-white"
|
className="px-3 py-1 bg-accent-strong hover:bg-accent text-xs rounded text-ink"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
@ -272,7 +272,7 @@ export function MemoryTab({ workspaceId }: Props) {
|
|||||||
setShowAdd(false);
|
setShowAdd(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
}}
|
}}
|
||||||
className="px-3 py-1 bg-zinc-700 hover:bg-zinc-600 text-xs rounded text-zinc-300"
|
className="px-3 py-1 bg-surface-card hover:bg-zinc-600 text-xs rounded text-ink-mid"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@ -282,25 +282,25 @@ export function MemoryTab({ workspaceId }: Props) {
|
|||||||
|
|
||||||
{showAdvanced ? (
|
{showAdvanced ? (
|
||||||
entries.length === 0 ? (
|
entries.length === 0 ? (
|
||||||
<p className="text-xs text-zinc-500 text-center py-4">No memory entries</p>
|
<p className="text-xs text-ink-soft text-center py-4">No memory entries</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{entries.map((entry) => (
|
{entries.map((entry) => (
|
||||||
<div key={entry.key} className="bg-zinc-800 rounded border border-zinc-700">
|
<div key={entry.key} className="bg-surface-card rounded border border-line">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setExpanded(expanded === entry.key ? null : entry.key)}
|
onClick={() => setExpanded(expanded === entry.key ? null : entry.key)}
|
||||||
className="w-full flex items-center justify-between px-3 py-2 text-left"
|
className="w-full flex items-center justify-between px-3 py-2 text-left"
|
||||||
aria-expanded={expanded === entry.key}
|
aria-expanded={expanded === entry.key}
|
||||||
>
|
>
|
||||||
<span className="text-xs font-mono text-blue-400">{entry.key}</span>
|
<span className="text-xs font-mono text-accent">{entry.key}</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{entry.expires_at && (
|
{entry.expires_at && (
|
||||||
<span className="text-[9px] text-zinc-500">
|
<span className="text-[9px] text-ink-soft">
|
||||||
TTL {new Date(entry.expires_at).toLocaleString()}
|
TTL {new Date(entry.expires_at).toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="text-[10px] text-zinc-500">
|
<span className="text-[10px] text-ink-soft">
|
||||||
{expanded === entry.key ? "▼" : "▶"}
|
{expanded === entry.key ? "▼" : "▶"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -308,17 +308,17 @@ export function MemoryTab({ workspaceId }: Props) {
|
|||||||
|
|
||||||
{expanded === entry.key && (
|
{expanded === entry.key && (
|
||||||
<div className="px-3 pb-2 space-y-2">
|
<div className="px-3 pb-2 space-y-2">
|
||||||
<pre className="text-[10px] text-zinc-300 bg-zinc-900 rounded p-2 overflow-x-auto max-h-40">
|
<pre className="text-[10px] text-ink-mid bg-surface-sunken rounded p-2 overflow-x-auto max-h-40">
|
||||||
{JSON.stringify(entry.value, null, 2)}
|
{JSON.stringify(entry.value, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-[9px] text-zinc-500">
|
<span className="text-[9px] text-ink-soft">
|
||||||
Updated: {new Date(entry.updated_at).toLocaleString()}
|
Updated: {new Date(entry.updated_at).toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleDelete(entry.key)}
|
onClick={() => handleDelete(entry.key)}
|
||||||
className="text-[10px] text-red-400 hover:text-red-300"
|
className="text-[10px] text-bad hover:text-bad"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
@ -330,17 +330,17 @@ export function MemoryTab({ workspaceId }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-xl border border-zinc-800 bg-zinc-950/30 px-4 py-3 flex items-center justify-between gap-3">
|
<div className="rounded-xl border border-line bg-surface/30 px-4 py-3 flex items-center justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-xs text-zinc-200">Advanced workspace memory is hidden</p>
|
<p className="text-xs text-ink">Advanced workspace memory is hidden</p>
|
||||||
<p className="text-[10px] text-zinc-500 truncate">
|
<p className="text-[10px] text-ink-soft truncate">
|
||||||
KV entries remain available if you need the raw platform store.
|
KV entries remain available if you need the raw platform store.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowAdvanced(true)}
|
onClick={() => setShowAdvanced(true)}
|
||||||
className="shrink-0 px-2 py-1 bg-blue-600 hover:bg-blue-500 text-[10px] rounded text-white"
|
className="shrink-0 px-2 py-1 bg-accent-strong hover:bg-accent text-[10px] rounded text-ink"
|
||||||
>
|
>
|
||||||
Show
|
Show
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -180,19 +180,19 @@ export function ScheduleTab({ workspaceId }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="p-4 text-[10px] text-zinc-500">Loading schedules...</div>;
|
return <div className="p-4 text-[10px] text-ink-soft">Loading schedules...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-3 py-2 border-b border-zinc-800/50">
|
<div className="flex items-center justify-between px-3 py-2 border-b border-line/50">
|
||||||
<span className="text-[10px] font-semibold text-zinc-400 uppercase tracking-wider">
|
<span className="text-[10px] font-semibold text-ink-mid uppercase tracking-wider">
|
||||||
Schedules
|
Schedules
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => { resetForm(); setShowForm(true); }}
|
onClick={() => { resetForm(); setShowForm(true); }}
|
||||||
className="text-[11px] px-2 py-0.5 bg-blue-600/20 text-blue-400 rounded hover:bg-blue-600/30 transition-colors"
|
className="text-[11px] px-2 py-0.5 bg-accent-strong/20 text-accent rounded hover:bg-accent-strong/30 transition-colors"
|
||||||
>
|
>
|
||||||
+ Add Schedule
|
+ Add Schedule
|
||||||
</button>
|
</button>
|
||||||
@ -200,36 +200,36 @@ export function ScheduleTab({ workspaceId }: Props) {
|
|||||||
|
|
||||||
{/* Create/Edit Form */}
|
{/* Create/Edit Form */}
|
||||||
{showForm && (
|
{showForm && (
|
||||||
<div className="p-3 border-b border-zinc-800/50 bg-zinc-900/50 space-y-2">
|
<div className="p-3 border-b border-line/50 bg-surface-sunken/50 space-y-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
aria-label="Schedule name"
|
aria-label="Schedule name"
|
||||||
placeholder="Schedule name (e.g., Daily security scan)"
|
placeholder="Schedule name (e.g., Daily security scan)"
|
||||||
value={formName}
|
value={formName}
|
||||||
onChange={(e) => setFormName(e.target.value)}
|
onChange={(e) => setFormName(e.target.value)}
|
||||||
className="w-full text-[10px] bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-200 placeholder:text-zinc-600"
|
className="w-full text-[10px] bg-surface-card border border-line rounded px-2 py-1 text-ink placeholder:text-ink-soft"
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<label htmlFor={cronId} className="text-[10px] text-zinc-500 block mb-0.5">Cron Expression</label>
|
<label htmlFor={cronId} className="text-[10px] text-ink-soft block mb-0.5">Cron Expression</label>
|
||||||
<input
|
<input
|
||||||
id={cronId}
|
id={cronId}
|
||||||
type="text"
|
type="text"
|
||||||
value={formCron}
|
value={formCron}
|
||||||
onChange={(e) => setFormCron(e.target.value)}
|
onChange={(e) => setFormCron(e.target.value)}
|
||||||
className="w-full text-[10px] bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-200 font-mono"
|
className="w-full text-[10px] bg-surface-card border border-line rounded px-2 py-1 text-ink font-mono"
|
||||||
/>
|
/>
|
||||||
<div className="text-[10px] text-zinc-600 mt-0.5">
|
<div className="text-[10px] text-ink-soft mt-0.5">
|
||||||
{cronToHuman(formCron)}
|
{cronToHuman(formCron)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-24">
|
<div className="w-24">
|
||||||
<label htmlFor={timezoneId} className="text-[10px] text-zinc-500 block mb-0.5">Timezone</label>
|
<label htmlFor={timezoneId} className="text-[10px] text-ink-soft block mb-0.5">Timezone</label>
|
||||||
<select
|
<select
|
||||||
id={timezoneId}
|
id={timezoneId}
|
||||||
value={formTimezone}
|
value={formTimezone}
|
||||||
onChange={(e) => setFormTimezone(e.target.value)}
|
onChange={(e) => setFormTimezone(e.target.value)}
|
||||||
className="w-full text-[10px] bg-zinc-800 border border-zinc-700 rounded px-1 py-1 text-zinc-200"
|
className="w-full text-[10px] bg-surface-card border border-line rounded px-1 py-1 text-ink"
|
||||||
>
|
>
|
||||||
<option value="UTC">UTC</option>
|
<option value="UTC">UTC</option>
|
||||||
<option value="America/New_York">US Eastern</option>
|
<option value="America/New_York">US Eastern</option>
|
||||||
@ -245,44 +245,44 @@ export function ScheduleTab({ workspaceId }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={promptId} className="text-[10px] text-zinc-500 block mb-0.5">Prompt / Task</label>
|
<label htmlFor={promptId} className="text-[10px] text-ink-soft block mb-0.5">Prompt / Task</label>
|
||||||
<textarea
|
<textarea
|
||||||
id={promptId}
|
id={promptId}
|
||||||
value={formPrompt}
|
value={formPrompt}
|
||||||
onChange={(e) => setFormPrompt(e.target.value)}
|
onChange={(e) => setFormPrompt(e.target.value)}
|
||||||
placeholder="What should the agent do on this schedule?"
|
placeholder="What should the agent do on this schedule?"
|
||||||
rows={3}
|
rows={3}
|
||||||
className="w-full text-[10px] bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-200 placeholder:text-zinc-600 resize-y"
|
className="w-full text-[10px] bg-surface-card border border-line rounded px-2 py-1 text-ink placeholder:text-ink-soft resize-y"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="flex items-center gap-1.5 text-[10px] text-zinc-400 cursor-pointer">
|
<label className="flex items-center gap-1.5 text-[10px] text-ink-mid cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={formEnabled}
|
checked={formEnabled}
|
||||||
onChange={(e) => setFormEnabled(e.target.checked)}
|
onChange={(e) => setFormEnabled(e.target.checked)}
|
||||||
className="rounded border-zinc-600"
|
className="rounded border-line"
|
||||||
/>
|
/>
|
||||||
Enabled
|
Enabled
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{error && <div className="text-[10px] text-red-400">{error}</div>}
|
{error && <div className="text-[10px] text-bad">{error}</div>}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={!formCron || !formPrompt}
|
disabled={!formCron || !formPrompt}
|
||||||
className="text-[11px] px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-500 disabled:opacity-40 transition-colors"
|
className="text-[11px] px-3 py-1 bg-accent-strong text-ink rounded hover:bg-accent disabled:opacity-40 transition-colors"
|
||||||
>
|
>
|
||||||
{editId ? "Update" : "Create"}
|
{editId ? "Update" : "Create"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={resetForm}
|
onClick={resetForm}
|
||||||
className="text-[11px] px-3 py-1 bg-zinc-800 text-zinc-400 rounded hover:bg-zinc-700 transition-colors"
|
className="text-[11px] px-3 py-1 bg-surface-card text-ink-mid rounded hover:bg-surface-card transition-colors"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] text-zinc-600 space-y-0.5">
|
<div className="text-[10px] text-ink-soft space-y-0.5">
|
||||||
<div>Common patterns:</div>
|
<div>Common patterns:</div>
|
||||||
<div className="font-mono">{"0 9 * * *"} — Daily at 9:00 AM</div>
|
<div className="font-mono">{"0 9 * * *"} — Daily at 9:00 AM</div>
|
||||||
<div className="font-mono">{"*/30 * * * *"} — Every 30 minutes</div>
|
<div className="font-mono">{"*/30 * * * *"} — Every 30 minutes</div>
|
||||||
@ -297,8 +297,8 @@ export function ScheduleTab({ workspaceId }: Props) {
|
|||||||
{schedules.length === 0 && !showForm ? (
|
{schedules.length === 0 && !showForm ? (
|
||||||
<div className="p-6 text-center">
|
<div className="p-6 text-center">
|
||||||
<div className="text-2xl mb-2">⏲</div>
|
<div className="text-2xl mb-2">⏲</div>
|
||||||
<div className="text-[10px] text-zinc-400 mb-1">No schedules yet</div>
|
<div className="text-[10px] text-ink-mid mb-1">No schedules yet</div>
|
||||||
<div className="text-[9px] text-zinc-500">
|
<div className="text-[9px] text-ink-soft">
|
||||||
Add a schedule to run tasks automatically — daily scans, periodic reports, standup reminders.
|
Add a schedule to run tasks automatically — daily scans, periodic reports, standup reminders.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -306,7 +306,7 @@ export function ScheduleTab({ workspaceId }: Props) {
|
|||||||
schedules.map((sched) => (
|
schedules.map((sched) => (
|
||||||
<div
|
<div
|
||||||
key={sched.id}
|
key={sched.id}
|
||||||
className={`px-3 py-2 border-b border-zinc-800/30 ${
|
className={`px-3 py-2 border-b border-line/30 ${
|
||||||
!sched.enabled ? "opacity-50" : ""
|
!sched.enabled ? "opacity-50" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -324,26 +324,26 @@ export function ScheduleTab({ workspaceId }: Props) {
|
|||||||
}`}
|
}`}
|
||||||
title={sched.enabled ? "Click to disable" : "Click to enable"}
|
title={sched.enabled ? "Click to disable" : "Click to enable"}
|
||||||
/>
|
/>
|
||||||
<span className="text-[10px] font-medium text-zinc-200 truncate">
|
<span className="text-[10px] font-medium text-ink truncate">
|
||||||
{sched.name || "Unnamed schedule"}
|
{sched.name || "Unnamed schedule"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[9px] text-zinc-500 mt-0.5 font-mono">
|
<div className="text-[9px] text-ink-soft mt-0.5 font-mono">
|
||||||
{cronToHuman(sched.cron_expr)}
|
{cronToHuman(sched.cron_expr)}
|
||||||
{sched.timezone !== "UTC" && (
|
{sched.timezone !== "UTC" && (
|
||||||
<span className="text-zinc-600"> ({sched.timezone})</span>
|
<span className="text-ink-soft"> ({sched.timezone})</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[9px] text-zinc-500 mt-0.5 truncate">
|
<div className="text-[9px] text-ink-soft mt-0.5 truncate">
|
||||||
{sched.prompt.slice(0, 80)}{sched.prompt.length > 80 ? "..." : ""}
|
{sched.prompt.slice(0, 80)}{sched.prompt.length > 80 ? "..." : ""}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 mt-1 text-[8px] text-zinc-500">
|
<div className="flex items-center gap-3 mt-1 text-[8px] text-ink-soft">
|
||||||
<span>Last: {relativeTime(sched.last_run_at)}</span>
|
<span>Last: {relativeTime(sched.last_run_at)}</span>
|
||||||
<span>Next: {relativeTime(sched.next_run_at)}</span>
|
<span>Next: {relativeTime(sched.next_run_at)}</span>
|
||||||
<span>Runs: {sched.run_count}</span>
|
<span>Runs: {sched.run_count}</span>
|
||||||
</div>
|
</div>
|
||||||
{sched.last_error && (
|
{sched.last_error && (
|
||||||
<div className="text-[8px] text-red-400/70 mt-0.5 truncate">
|
<div className="text-[8px] text-bad/70 mt-0.5 truncate">
|
||||||
Error: {sched.last_error}
|
Error: {sched.last_error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -352,7 +352,7 @@ export function ScheduleTab({ workspaceId }: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleRunNow(sched)}
|
onClick={() => handleRunNow(sched)}
|
||||||
aria-label={`Run schedule ${sched.name} now`}
|
aria-label={`Run schedule ${sched.name} now`}
|
||||||
className="text-[11px] px-1.5 py-0.5 text-blue-400 hover:bg-blue-600/20 rounded transition-colors"
|
className="text-[11px] px-1.5 py-0.5 text-accent hover:bg-accent-strong/20 rounded transition-colors"
|
||||||
title="Run now"
|
title="Run now"
|
||||||
>
|
>
|
||||||
▶
|
▶
|
||||||
@ -360,7 +360,7 @@ export function ScheduleTab({ workspaceId }: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleEdit(sched)}
|
onClick={() => handleEdit(sched)}
|
||||||
aria-label={`Edit schedule ${sched.name}`}
|
aria-label={`Edit schedule ${sched.name}`}
|
||||||
className="text-[11px] px-1.5 py-0.5 text-zinc-400 hover:bg-zinc-700 rounded transition-colors"
|
className="text-[11px] px-1.5 py-0.5 text-ink-mid hover:bg-surface-card rounded transition-colors"
|
||||||
title="Edit"
|
title="Edit"
|
||||||
>
|
>
|
||||||
✎
|
✎
|
||||||
@ -368,7 +368,7 @@ export function ScheduleTab({ workspaceId }: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setPendingDelete({ id: sched.id, name: sched.name })}
|
onClick={() => setPendingDelete({ id: sched.id, name: sched.name })}
|
||||||
aria-label={`Delete schedule ${sched.name}`}
|
aria-label={`Delete schedule ${sched.name}`}
|
||||||
className="text-[11px] px-1.5 py-0.5 text-red-400 hover:bg-red-600/20 rounded transition-colors"
|
className="text-[11px] px-1.5 py-0.5 text-bad hover:bg-red-600/20 rounded transition-colors"
|
||||||
title="Delete"
|
title="Delete"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
|
|||||||
@ -300,11 +300,11 @@ export function SkillsTab({ workspaceId, data }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
{/* Plugins section */}
|
{/* Plugins section */}
|
||||||
<div className="rounded-xl border border-zinc-800 bg-zinc-900/70 p-3">
|
<div className="rounded-xl border border-line bg-surface-sunken/70 p-3">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[10px] uppercase tracking-[0.22em] text-zinc-500">Plugins</div>
|
<div className="text-[10px] uppercase tracking-[0.22em] text-ink-soft">Plugins</div>
|
||||||
<h3 className="mt-1 text-sm font-semibold text-zinc-100">
|
<h3 className="mt-1 text-sm font-semibold text-ink">
|
||||||
{installed.length} installed
|
{installed.length} installed
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
@ -332,27 +332,27 @@ export function SkillsTab({ workspaceId, data }: Props) {
|
|||||||
className={`flex items-center justify-between gap-2 rounded-lg border px-3 py-2 ${
|
className={`flex items-center justify-between gap-2 rounded-lg border px-3 py-2 ${
|
||||||
inert
|
inert
|
||||||
? "border-amber-800/40 bg-amber-950/10 opacity-70"
|
? "border-amber-800/40 bg-amber-950/10 opacity-70"
|
||||||
: "border-zinc-800/60 bg-zinc-950/40"
|
: "border-line/60 bg-surface/40"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[11px] font-medium text-zinc-200">{p.name}</span>
|
<span className="text-[11px] font-medium text-ink">{p.name}</span>
|
||||||
{p.version && <span className="text-[10px] text-zinc-600">v{p.version}</span>}
|
{p.version && <span className="text-[10px] text-ink-soft">v{p.version}</span>}
|
||||||
{inert && (
|
{inert && (
|
||||||
<span className="rounded-full border border-amber-700/50 bg-amber-950/30 px-1.5 py-0.5 text-[10px] text-amber-300">
|
<span className="rounded-full border border-amber-700/50 bg-amber-950/30 px-1.5 py-0.5 text-[10px] text-warm">
|
||||||
inert on this runtime
|
inert on this runtime
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{p.description && <div className="text-[10px] text-zinc-500 truncate">{p.description}</div>}
|
{p.description && <div className="text-[10px] text-ink-soft truncate">{p.description}</div>}
|
||||||
{p.skills && p.skills.length > 0 && (
|
{p.skills && p.skills.length > 0 && (
|
||||||
<div className="mt-1 flex flex-wrap gap-1">
|
<div className="mt-1 flex flex-wrap gap-1">
|
||||||
{p.skills.slice(0, 4).map((s) => (
|
{p.skills.slice(0, 4).map((s) => (
|
||||||
<span key={s} className="rounded-full bg-zinc-800/60 px-1.5 py-0.5 text-[10px] text-zinc-400">{s}</span>
|
<span key={s} className="rounded-full bg-surface-card/60 px-1.5 py-0.5 text-[10px] text-ink-mid">{s}</span>
|
||||||
))}
|
))}
|
||||||
{p.skills.length > 4 && (
|
{p.skills.length > 4 && (
|
||||||
<span className="text-[10px] text-zinc-600">+{p.skills.length - 4}</span>
|
<span className="text-[10px] text-ink-soft">+{p.skills.length - 4}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -360,7 +360,7 @@ export function SkillsTab({ workspaceId, data }: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleUninstall(p.name)}
|
onClick={() => handleUninstall(p.name)}
|
||||||
disabled={uninstalling === p.name}
|
disabled={uninstalling === p.name}
|
||||||
className="shrink-0 rounded-full border border-red-800/40 bg-red-950/20 px-2 py-0.5 text-[11px] text-red-400 hover:bg-red-900/30 disabled:opacity-30"
|
className="shrink-0 rounded-full border border-red-800/40 bg-red-950/20 px-2 py-0.5 text-[11px] text-bad hover:bg-red-900/30 disabled:opacity-30"
|
||||||
>
|
>
|
||||||
{uninstalling === p.name ? "..." : "Remove"}
|
{uninstalling === p.name ? "..." : "Remove"}
|
||||||
</button>
|
</button>
|
||||||
@ -372,11 +372,11 @@ export function SkillsTab({ workspaceId, data }: Props) {
|
|||||||
|
|
||||||
{/* Plugin registry (expandable) */}
|
{/* Plugin registry (expandable) */}
|
||||||
{showRegistry && (
|
{showRegistry && (
|
||||||
<div className="mt-3 border-t border-zinc-800/40 pt-3">
|
<div className="mt-3 border-t border-line/40 pt-3">
|
||||||
{/* Install from any source (github://, clawhub://, …) */}
|
{/* Install from any source (github://, clawhub://, …) */}
|
||||||
<div className="mb-3 rounded-lg border border-zinc-800/60 bg-zinc-950/40 p-2.5">
|
<div className="mb-3 rounded-lg border border-line/60 bg-surface/40 p-2.5">
|
||||||
<div className="flex items-center justify-between gap-2 mb-1.5">
|
<div className="flex items-center justify-between gap-2 mb-1.5">
|
||||||
<div className="text-[10px] uppercase tracking-[0.2em] text-zinc-600">
|
<div className="text-[10px] uppercase tracking-[0.2em] text-ink-soft">
|
||||||
Install from source
|
Install from source
|
||||||
</div>
|
</div>
|
||||||
{sourceSchemes.length > 0 && (
|
{sourceSchemes.length > 0 && (
|
||||||
@ -384,7 +384,7 @@ export function SkillsTab({ workspaceId, data }: Props) {
|
|||||||
{sourceSchemes.map((s) => (
|
{sourceSchemes.map((s) => (
|
||||||
<span
|
<span
|
||||||
key={s}
|
key={s}
|
||||||
className="rounded-full border border-zinc-700/50 bg-zinc-900/50 px-1.5 py-0.5 text-[10px] text-zinc-500"
|
className="rounded-full border border-line/50 bg-surface-sunken/50 px-1.5 py-0.5 text-[10px] text-ink-soft"
|
||||||
>
|
>
|
||||||
{s}://
|
{s}://
|
||||||
</span>
|
</span>
|
||||||
@ -403,7 +403,7 @@ export function SkillsTab({ workspaceId, data }: Props) {
|
|||||||
}}
|
}}
|
||||||
placeholder="e.g. github://owner/repo#v1.0"
|
placeholder="e.g. github://owner/repo#v1.0"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="flex-1 rounded border border-zinc-700 bg-zinc-950 px-2 py-1 text-[10px] text-zinc-200 placeholder:text-zinc-600 focus:border-violet-600 focus:outline-none"
|
className="flex-1 rounded border border-line bg-surface px-2 py-1 text-[10px] text-ink placeholder:text-ink-soft focus:border-violet-600 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleInstallCustom}
|
onClick={handleInstallCustom}
|
||||||
@ -413,12 +413,12 @@ export function SkillsTab({ workspaceId, data }: Props) {
|
|||||||
{installing === customSource.trim() ? "Installing..." : "Install"}
|
{installing === customSource.trim() ? "Installing..." : "Install"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-[10px] text-zinc-600">
|
<div className="mt-1 text-[10px] text-ink-soft">
|
||||||
Local registry plugins below; paste any scheme URL above for GitHub or other sources.
|
Local registry plugins below; paste any scheme URL above for GitHub or other sources.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="text-[10px] uppercase tracking-[0.2em] text-zinc-600">Available plugins</div>
|
<div className="text-[10px] uppercase tracking-[0.2em] text-ink-soft">Available plugins</div>
|
||||||
{/* Retry visible whenever registry is empty — including
|
{/* Retry visible whenever registry is empty — including
|
||||||
the loading state — so a stuck fetch (Fast Refresh
|
the loading state — so a stuck fetch (Fast Refresh
|
||||||
stranded promise, slow server, browser quirk) has a
|
stranded promise, slow server, browser quirk) has a
|
||||||
@ -445,21 +445,21 @@ export function SkillsTab({ workspaceId, data }: Props) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{registryLoading && registry.length === 0 ? (
|
{registryLoading && registry.length === 0 ? (
|
||||||
<div className="text-[10px] text-zinc-500">Loading registry…</div>
|
<div className="text-[10px] text-ink-soft">Loading registry…</div>
|
||||||
) : registryError ? (
|
) : registryError ? (
|
||||||
<div className="rounded-lg border border-red-800/40 bg-red-950/20 px-2 py-1.5">
|
<div className="rounded-lg border border-red-800/40 bg-red-950/20 px-2 py-1.5">
|
||||||
<div className="text-[10px] text-red-300 font-semibold mb-0.5">
|
<div className="text-[10px] text-bad font-semibold mb-0.5">
|
||||||
Couldn't load the plugin registry
|
Couldn't load the plugin registry
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] text-red-400/80">{registryError}</div>
|
<div className="text-[10px] text-bad/80">{registryError}</div>
|
||||||
<div className="mt-1 text-[10px] text-zinc-500">
|
<div className="mt-1 text-[10px] text-ink-soft">
|
||||||
Check the platform server is reachable at /plugins. The Retry button is in the header above.
|
Check the platform server is reachable at /plugins. The Retry button is in the header above.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : registry.length === 0 ? (
|
) : registry.length === 0 ? (
|
||||||
<div className="rounded-lg border border-zinc-800/40 bg-zinc-950/40 px-2 py-1.5">
|
<div className="rounded-lg border border-line/40 bg-surface/40 px-2 py-1.5">
|
||||||
<div className="text-[10px] text-zinc-400 mb-0.5">Registry returned 0 plugins.</div>
|
<div className="text-[10px] text-ink-mid mb-0.5">Registry returned 0 plugins.</div>
|
||||||
<div className="text-[10px] text-zinc-600">
|
<div className="text-[10px] text-ink-soft">
|
||||||
This usually means the platform's plugins/ directory is empty.
|
This usually means the platform's plugins/ directory is empty.
|
||||||
Run scripts/clone-manifest.sh to populate it from the standalone repos.
|
Run scripts/clone-manifest.sh to populate it from the standalone repos.
|
||||||
</div>
|
</div>
|
||||||
@ -469,30 +469,30 @@ export function SkillsTab({ workspaceId, data }: Props) {
|
|||||||
{registry.map((p) => {
|
{registry.map((p) => {
|
||||||
const isInstalled = installedNames.has(p.name);
|
const isInstalled = installedNames.has(p.name);
|
||||||
return (
|
return (
|
||||||
<div key={p.name} className="flex items-center justify-between gap-2 rounded-lg border border-zinc-800/40 bg-zinc-950/30 px-3 py-2">
|
<div key={p.name} className="flex items-center justify-between gap-2 rounded-lg border border-line/40 bg-surface/30 px-3 py-2">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[11px] text-zinc-300">{p.name}</span>
|
<span className="text-[11px] text-ink-mid">{p.name}</span>
|
||||||
{p.version && <span className="text-[10px] text-zinc-600">v{p.version}</span>}
|
{p.version && <span className="text-[10px] text-ink-soft">v{p.version}</span>}
|
||||||
</div>
|
</div>
|
||||||
{p.description && <div className="text-[10px] text-zinc-500 truncate">{p.description}</div>}
|
{p.description && <div className="text-[10px] text-ink-soft truncate">{p.description}</div>}
|
||||||
{p.tags && p.tags.length > 0 && (
|
{p.tags && p.tags.length > 0 && (
|
||||||
<div className="mt-1 flex flex-wrap gap-1">
|
<div className="mt-1 flex flex-wrap gap-1">
|
||||||
{p.tags.map((t) => (
|
{p.tags.map((t) => (
|
||||||
<span key={t} className="rounded-full border border-zinc-700/40 px-1.5 py-0.5 text-[10px] text-zinc-500">{t}</span>
|
<span key={t} className="rounded-full border border-line/40 px-1.5 py-0.5 text-[10px] text-ink-soft">{t}</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{p.runtimes && p.runtimes.length > 0 && (
|
{p.runtimes && p.runtimes.length > 0 && (
|
||||||
<div className="mt-1 flex flex-wrap gap-1">
|
<div className="mt-1 flex flex-wrap gap-1">
|
||||||
{p.runtimes.map((r) => (
|
{p.runtimes.map((r) => (
|
||||||
<span key={r} className="rounded-full border border-blue-800/40 bg-blue-950/20 px-1.5 py-0.5 text-[10px] text-blue-300">{r}</span>
|
<span key={r} className="rounded-full border border-blue-800/40 bg-blue-950/20 px-1.5 py-0.5 text-[10px] text-accent">{r}</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isInstalled ? (
|
{isInstalled ? (
|
||||||
<span className="shrink-0 text-[10px] text-emerald-500">Installed</span>
|
<span className="shrink-0 text-[10px] text-good">Installed</span>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleInstall(p.name)}
|
onClick={() => handleInstall(p.name)}
|
||||||
@ -512,30 +512,30 @@ export function SkillsTab({ workspaceId, data }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Skills section */}
|
{/* Skills section */}
|
||||||
<div className="rounded-xl border border-zinc-800 bg-zinc-900/70 p-3">
|
<div className="rounded-xl border border-line bg-surface-sunken/70 p-3">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[10px] uppercase tracking-[0.22em] text-zinc-500">Workspace skills</div>
|
<div className="text-[10px] uppercase tracking-[0.22em] text-ink-soft">Workspace skills</div>
|
||||||
<h3 className="mt-1 text-sm font-semibold text-zinc-100">Installed skills</h3>
|
<h3 className="mt-1 text-sm font-semibold text-ink">Installed skills</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<MetaPill label="Count" value={String(capability.skillCount)} />
|
<MetaPill label="Count" value={String(capability.skillCount)} />
|
||||||
<MetaPill label="Runtime" value={capability.runtime || "unknown"} />
|
<MetaPill label="Runtime" value={capability.runtime || "unknown"} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-[11px] leading-5 text-zinc-500">
|
<p className="mt-2 text-[11px] leading-5 text-ink-soft">
|
||||||
Live skill directory from the Agent Card — updates when the workspace hot-reloads skills.
|
Live skill directory from the Agent Card — updates when the workspace hot-reloads skills.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-3 flex flex-wrap gap-2">
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setPanelTab("config")}
|
onClick={() => setPanelTab("config")}
|
||||||
className="rounded-full border border-zinc-700 bg-zinc-950 px-3 py-1 text-[10px] text-zinc-300 hover:bg-zinc-900"
|
className="rounded-full border border-line bg-surface px-3 py-1 text-[10px] text-ink-mid hover:bg-surface-sunken"
|
||||||
>
|
>
|
||||||
Open Config
|
Open Config
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPanelTab("files")}
|
onClick={() => setPanelTab("files")}
|
||||||
className="rounded-full border border-zinc-700 bg-zinc-950 px-3 py-1 text-[10px] text-zinc-300 hover:bg-zinc-900"
|
className="rounded-full border border-line bg-surface px-3 py-1 text-[10px] text-ink-mid hover:bg-surface-sunken"
|
||||||
>
|
>
|
||||||
Open Files
|
Open Files
|
||||||
</button>
|
</button>
|
||||||
@ -550,27 +550,27 @@ export function SkillsTab({ workspaceId, data }: Props) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{skills.length === 0 ? (
|
{skills.length === 0 ? (
|
||||||
<div className="rounded-xl border border-dashed border-zinc-800 bg-zinc-900/40 p-6 text-center">
|
<div className="rounded-xl border border-dashed border-line bg-surface-sunken/40 p-6 text-center">
|
||||||
<div className="text-sm text-zinc-100">No skills loaded</div>
|
<div className="text-sm text-ink">No skills loaded</div>
|
||||||
<p className="mt-2 text-[11px] leading-5 text-zinc-500">
|
<p className="mt-2 text-[11px] leading-5 text-ink-soft">
|
||||||
Add skills from the Config tab, install a plugin above, or let the runtime hot-load them.
|
Add skills from the Config tab, install a plugin above, or let the runtime hot-load them.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{skills.map((skill) => (
|
{skills.map((skill) => (
|
||||||
<div key={skill.id} className="rounded-xl border border-zinc-800 bg-zinc-900/60 p-3">
|
<div key={skill.id} className="rounded-xl border border-line bg-surface-sunken/60 p-3">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-semibold text-zinc-100">{skill.name}</div>
|
<div className="text-xs font-semibold text-ink">{skill.name}</div>
|
||||||
<div className="mt-0.5 text-[10px] font-mono text-zinc-500">{skill.id}</div>
|
<div className="mt-0.5 text-[10px] font-mono text-ink-soft">{skill.id}</div>
|
||||||
</div>
|
</div>
|
||||||
{skill.tags.length > 0 && (
|
{skill.tags.length > 0 && (
|
||||||
<div className="flex flex-wrap justify-end gap-1.5">
|
<div className="flex flex-wrap justify-end gap-1.5">
|
||||||
{skill.tags.slice(0, 4).map((tag) => (
|
{skill.tags.slice(0, 4).map((tag) => (
|
||||||
<span
|
<span
|
||||||
key={tag}
|
key={tag}
|
||||||
className="rounded-full border border-zinc-700 bg-zinc-900 px-2 py-0.5 text-[9px] text-zinc-400"
|
className="rounded-full border border-line bg-surface-sunken px-2 py-0.5 text-[9px] text-ink-mid"
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
@ -580,17 +580,17 @@ export function SkillsTab({ workspaceId, data }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{skill.description && (
|
{skill.description && (
|
||||||
<p className="mt-2 text-[11px] leading-5 text-zinc-400">{skill.description}</p>
|
<p className="mt-2 text-[11px] leading-5 text-ink-mid">{skill.description}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{skill.examples.length > 0 && (
|
{skill.examples.length > 0 && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<div className="text-[9px] uppercase tracking-[0.2em] text-zinc-500">Examples</div>
|
<div className="text-[9px] uppercase tracking-[0.2em] text-ink-soft">Examples</div>
|
||||||
<div className="mt-1 space-y-1">
|
<div className="mt-1 space-y-1">
|
||||||
{skill.examples.slice(0, 2).map((example, index) => (
|
{skill.examples.slice(0, 2).map((example, index) => (
|
||||||
<div
|
<div
|
||||||
key={`${skill.id}-${index}`}
|
key={`${skill.id}-${index}`}
|
||||||
className="rounded-md border border-zinc-800 bg-zinc-950/60 px-2 py-1 text-[10px] text-zinc-300"
|
className="rounded-md border border-line bg-surface/60 px-2 py-1 text-[10px] text-ink-mid"
|
||||||
>
|
>
|
||||||
{example}
|
{example}
|
||||||
</div>
|
</div>
|
||||||
@ -624,8 +624,8 @@ function extractSkills(agentCard: Record<string, unknown> | null): SkillEntry[]
|
|||||||
|
|
||||||
function MetaPill({ label, value }: { label: string; value: string }) {
|
function MetaPill({ label, value }: { label: string; value: string }) {
|
||||||
return (
|
return (
|
||||||
<span className="inline-flex items-center gap-1 rounded-full border border-zinc-700/60 bg-zinc-950/60 px-2 py-1 text-[9px] text-zinc-300">
|
<span className="inline-flex items-center gap-1 rounded-full border border-line/60 bg-surface/60 px-2 py-1 text-[9px] text-ink-mid">
|
||||||
<span className="uppercase tracking-[0.18em] text-[8px] text-zinc-500">{label}</span>
|
<span className="uppercase tracking-[0.18em] text-[8px] text-ink-soft">{label}</span>
|
||||||
<span className="font-medium">{value}</span>
|
<span className="font-medium">{value}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -48,20 +48,20 @@ export function TracesTab({ workspaceId }: Props) {
|
|||||||
}, [loadTraces]);
|
}, [loadTraces]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="p-4 text-xs text-zinc-500">Loading traces...</div>;
|
return <div className="p-4 text-xs text-ink-soft">Loading traces...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 space-y-2">
|
<div className="p-4 space-y-2">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-xs text-zinc-400">{traces.length} traces</span>
|
<span className="text-xs text-ink-mid">{traces.length} traces</span>
|
||||||
<button type="button" onClick={loadTraces} className="text-[10px] text-zinc-500 hover:text-zinc-300">
|
<button type="button" onClick={loadTraces} className="text-[10px] text-ink-soft hover:text-ink-mid">
|
||||||
Refresh
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400">
|
<div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -69,70 +69,70 @@ export function TracesTab({ workspaceId }: Props) {
|
|||||||
{traces.length === 0 && !error ? (
|
{traces.length === 0 && !error ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<div className="text-2xl opacity-20 mb-2" aria-hidden="true">--</div>
|
<div className="text-2xl opacity-20 mb-2" aria-hidden="true">--</div>
|
||||||
<p className="text-xs text-zinc-600">No traces yet</p>
|
<p className="text-xs text-ink-soft">No traces yet</p>
|
||||||
<details className="mt-2 text-[10px] text-zinc-700">
|
<details className="mt-2 text-[10px] text-zinc-700">
|
||||||
<summary className="cursor-pointer text-zinc-500 hover:text-zinc-400">How to enable tracing</summary>
|
<summary className="cursor-pointer text-ink-soft hover:text-ink-mid">How to enable tracing</summary>
|
||||||
<p className="mt-1">
|
<p className="mt-1">
|
||||||
Set <code className="font-mono text-zinc-400">LANGFUSE_HOST</code>, <code className="font-mono text-zinc-400">LANGFUSE_PUBLIC_KEY</code>, <code className="font-mono text-zinc-400">LANGFUSE_SECRET_KEY</code> as workspace secrets to enable tracing.
|
Set <code className="font-mono text-ink-mid">LANGFUSE_HOST</code>, <code className="font-mono text-ink-mid">LANGFUSE_PUBLIC_KEY</code>, <code className="font-mono text-ink-mid">LANGFUSE_SECRET_KEY</code> as workspace secrets to enable tracing.
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{traces.map((trace) => (
|
{traces.map((trace) => (
|
||||||
<div key={trace.id} className="bg-zinc-800/40 border border-zinc-700/40 rounded-lg overflow-hidden">
|
<div key={trace.id} className="bg-surface-card/40 border border-line/40 rounded-lg overflow-hidden">
|
||||||
<button
|
<button
|
||||||
onClick={() => setExpanded(expanded === trace.id ? null : trace.id)}
|
onClick={() => setExpanded(expanded === trace.id ? null : trace.id)}
|
||||||
className="w-full px-3 py-2 flex items-center gap-2 text-left hover:bg-zinc-800/60 transition-colors"
|
className="w-full px-3 py-2 flex items-center gap-2 text-left hover:bg-surface-card/60 transition-colors"
|
||||||
>
|
>
|
||||||
<div className={`w-1.5 h-1.5 rounded-full shrink-0 ${
|
<div className={`w-1.5 h-1.5 rounded-full shrink-0 ${
|
||||||
trace.status === "ERROR" ? "bg-red-400" : "bg-emerald-400"
|
trace.status === "ERROR" ? "bg-red-400" : "bg-emerald-400"
|
||||||
}`} />
|
}`} />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-[11px] text-zinc-200 truncate">{trace.name || "trace"}</div>
|
<div className="text-[11px] text-ink truncate">{trace.name || "trace"}</div>
|
||||||
<div className="text-[9px] text-zinc-500">{formatTime(trace.timestamp)}</div>
|
<div className="text-[9px] text-ink-soft">{formatTime(trace.timestamp)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
{trace.latency != null && (
|
{trace.latency != null && (
|
||||||
<span className="text-[9px] text-zinc-500 tabular-nums">
|
<span className="text-[9px] text-ink-soft tabular-nums">
|
||||||
{trace.latency > 1000 ? `${(trace.latency / 1000).toFixed(1)}s` : `${trace.latency}ms`}
|
{trace.latency > 1000 ? `${(trace.latency / 1000).toFixed(1)}s` : `${trace.latency}ms`}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{trace.usage?.total != null && (
|
{trace.usage?.total != null && (
|
||||||
<span className="text-[9px] text-zinc-500 tabular-nums">
|
<span className="text-[9px] text-ink-soft tabular-nums">
|
||||||
{trace.usage.total} tok
|
{trace.usage.total} tok
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="text-[9px] text-zinc-500">
|
<span className="text-[9px] text-ink-soft">
|
||||||
{expanded === trace.id ? "▼" : "▶"}
|
{expanded === trace.id ? "▼" : "▶"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{expanded === trace.id && (
|
{expanded === trace.id && (
|
||||||
<div className="px-3 pb-2 space-y-2 border-t border-zinc-700/30">
|
<div className="px-3 pb-2 space-y-2 border-t border-line/30">
|
||||||
{trace.input && (
|
{trace.input && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[9px] text-zinc-500 uppercase tracking-wider mt-2 mb-1">Input</div>
|
<div className="text-[9px] text-ink-soft uppercase tracking-wider mt-2 mb-1">Input</div>
|
||||||
<pre className="text-[9px] text-zinc-300 bg-zinc-900 rounded p-2 overflow-x-auto max-h-32">
|
<pre className="text-[9px] text-ink-mid bg-surface-sunken rounded p-2 overflow-x-auto max-h-32">
|
||||||
{String(typeof trace.input === "string" ? trace.input : JSON.stringify(trace.input, null, 2))}
|
{String(typeof trace.input === "string" ? trace.input : JSON.stringify(trace.input, null, 2))}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{trace.output && (
|
{trace.output && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[9px] text-zinc-500 uppercase tracking-wider mb-1">Output</div>
|
<div className="text-[9px] text-ink-soft uppercase tracking-wider mb-1">Output</div>
|
||||||
<pre className="text-[9px] text-zinc-300 bg-zinc-900 rounded p-2 overflow-x-auto max-h-32">
|
<pre className="text-[9px] text-ink-mid bg-surface-sunken rounded p-2 overflow-x-auto max-h-32">
|
||||||
{String(typeof trace.output === "string" ? trace.output : JSON.stringify(trace.output, null, 2))}
|
{String(typeof trace.output === "string" ? trace.output : JSON.stringify(trace.output, null, 2))}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{trace.totalCost != null && (
|
{trace.totalCost != null && (
|
||||||
<div className="text-[9px] text-zinc-500">
|
<div className="text-[9px] text-ink-soft">
|
||||||
Cost: ${trace.totalCost.toFixed(6)}
|
Cost: ${trace.totalCost.toFixed(6)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-[8px] text-zinc-500 font-mono select-all">
|
<div className="text-[8px] text-ink-soft font-mono select-all">
|
||||||
{trace.id}
|
{trace.id}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -363,15 +363,15 @@ export function AgentCommsPanel({ workspaceId }: { workspaceId: string }) {
|
|||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="text-xs text-zinc-500 text-center py-8">Loading agent communications...</div>;
|
return <div className="text-xs text-ink-soft text-center py-8">Loading agent communications...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="text-xs text-zinc-500 text-center py-8">
|
<div className="text-xs text-ink-soft text-center py-8">
|
||||||
No agent-to-agent communications yet.
|
No agent-to-agent communications yet.
|
||||||
<br />
|
<br />
|
||||||
<span className="text-zinc-600">Delegations and peer messages will appear here.</span>
|
<span className="text-ink-soft">Delegations and peer messages will appear here.</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -500,7 +500,7 @@ function PeerTabs({
|
|||||||
<div
|
<div
|
||||||
role="tablist"
|
role="tablist"
|
||||||
aria-label="Peer threads"
|
aria-label="Peer threads"
|
||||||
className="flex border-b border-zinc-800/40 bg-zinc-900/30 px-2 shrink-0 overflow-x-auto"
|
className="flex border-b border-line/40 bg-surface-sunken/30 px-2 shrink-0 overflow-x-auto"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
const idx = ids.indexOf(selectedPeerId);
|
const idx = ids.indexOf(selectedPeerId);
|
||||||
if (idx < 0) return;
|
if (idx < 0) return;
|
||||||
@ -552,10 +552,10 @@ function PeerTabButton({
|
|||||||
className={`shrink-0 px-3 py-1.5 text-[10px] font-medium transition-colors whitespace-nowrap ${
|
className={`shrink-0 px-3 py-1.5 text-[10px] font-medium transition-colors whitespace-nowrap ${
|
||||||
active
|
active
|
||||||
? "border-b-2 border-cyan-500 text-cyan-200"
|
? "border-b-2 border-cyan-500 text-cyan-200"
|
||||||
: "border-b-2 border-transparent text-zinc-500 hover:text-zinc-300"
|
: "border-b-2 border-transparent text-ink-soft hover:text-ink-mid"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{label} <span className="text-[9px] text-zinc-500">({count})</span>
|
{label} <span className="text-[9px] text-ink-soft">({count})</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -567,23 +567,23 @@ function NormalMessage({ msg }: { msg: CommMessage }) {
|
|||||||
className={`max-w-[85%] rounded-lg px-3 py-2 text-xs ${
|
className={`max-w-[85%] rounded-lg px-3 py-2 text-xs ${
|
||||||
msg.flow === "out"
|
msg.flow === "out"
|
||||||
? "bg-cyan-900/30 text-cyan-100 border border-cyan-700/20"
|
? "bg-cyan-900/30 text-cyan-100 border border-cyan-700/20"
|
||||||
: "bg-zinc-800/80 text-zinc-200 border border-zinc-700/30"
|
: "bg-surface-card/80 text-ink border border-line/30"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="text-[9px] text-zinc-500 mb-1">
|
<div className="text-[9px] text-ink-soft mb-1">
|
||||||
{msg.flow === "out" ? `→ To ${msg.peerName}` : `← From ${msg.peerName}`}
|
{msg.flow === "out" ? `→ To ${msg.peerName}` : `← From ${msg.peerName}`}
|
||||||
</div>
|
</div>
|
||||||
{msg.text ? (
|
{msg.text ? (
|
||||||
<MarkdownBody className="text-zinc-300">{msg.text}</MarkdownBody>
|
<MarkdownBody className="text-ink-mid">{msg.text}</MarkdownBody>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-zinc-300">(no message text)</div>
|
<div className="text-ink-mid">(no message text)</div>
|
||||||
)}
|
)}
|
||||||
{msg.responseText && (
|
{msg.responseText && (
|
||||||
<MarkdownBody className="mt-1.5 pt-1.5 border-t border-zinc-700/30 text-zinc-400">
|
<MarkdownBody className="mt-1.5 pt-1.5 border-t border-line/30 text-ink-mid">
|
||||||
{msg.responseText}
|
{msg.responseText}
|
||||||
</MarkdownBody>
|
</MarkdownBody>
|
||||||
)}
|
)}
|
||||||
<div className="text-[9px] text-zinc-500 mt-1">
|
<div className="text-[9px] text-ink-soft mt-1">
|
||||||
{new Date(msg.timestamp).toLocaleTimeString()}
|
{new Date(msg.timestamp).toLocaleTimeString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -648,7 +648,7 @@ function ErrorMessage({ msg }: { msg: CommMessage }) {
|
|||||||
return (
|
return (
|
||||||
<div className={`flex ${msg.flow === "out" ? "justify-end" : "justify-start"}`}>
|
<div className={`flex ${msg.flow === "out" ? "justify-end" : "justify-start"}`}>
|
||||||
<div className="max-w-[85%] rounded-lg border border-red-800/50 bg-red-950/30 px-3 py-2 text-xs">
|
<div className="max-w-[85%] rounded-lg border border-red-800/50 bg-red-950/30 px-3 py-2 text-xs">
|
||||||
<div className="flex items-center gap-1.5 text-[10px] text-red-300 font-semibold uppercase tracking-wide mb-1.5">
|
<div className="flex items-center gap-1.5 text-[10px] text-bad font-semibold uppercase tracking-wide mb-1.5">
|
||||||
<span aria-hidden="true">⚠</span>
|
<span aria-hidden="true">⚠</span>
|
||||||
{msg.flow === "out"
|
{msg.flow === "out"
|
||||||
? `Failed to deliver to ${msg.peerName}`
|
? `Failed to deliver to ${msg.peerName}`
|
||||||
@ -656,14 +656,14 @@ function ErrorMessage({ msg }: { msg: CommMessage }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{msg.text && (
|
{msg.text && (
|
||||||
<div className="text-[10px] text-zinc-500 mb-1.5">
|
<div className="text-[10px] text-ink-soft mb-1.5">
|
||||||
<span className="uppercase tracking-wide">Task</span>
|
<span className="uppercase tracking-wide">Task</span>
|
||||||
<MarkdownBody className="text-zinc-400">{msg.text}</MarkdownBody>
|
<MarkdownBody className="text-ink-mid">{msg.text}</MarkdownBody>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="rounded bg-zinc-950/60 border border-red-900/40 px-2 py-1.5 mb-1.5">
|
<div className="rounded bg-surface/60 border border-red-900/40 px-2 py-1.5 mb-1.5">
|
||||||
<div className="text-[9px] uppercase tracking-wide text-red-400 mb-0.5">
|
<div className="text-[9px] uppercase tracking-wide text-bad mb-0.5">
|
||||||
Underlying error
|
Underlying error
|
||||||
</div>
|
</div>
|
||||||
<code className="text-[11px] font-mono text-red-200 whitespace-pre-wrap break-words">
|
<code className="text-[11px] font-mono text-red-200 whitespace-pre-wrap break-words">
|
||||||
@ -671,7 +671,7 @@ function ErrorMessage({ msg }: { msg: CommMessage }) {
|
|||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-[10px] text-zinc-400 leading-snug mb-2">{hint}</p>
|
<p className="text-[10px] text-ink-mid leading-snug mb-2">{hint}</p>
|
||||||
|
|
||||||
{msg.peerId && (
|
{msg.peerId && (
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
@ -686,14 +686,14 @@ function ErrorMessage({ msg }: { msg: CommMessage }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
className="px-2 py-0.5 rounded bg-zinc-800 hover:bg-zinc-700 border border-zinc-700/50 text-[10px] text-zinc-300 transition-colors"
|
className="px-2 py-0.5 rounded bg-surface-card hover:bg-surface-card border border-line/50 text-[10px] text-ink-mid transition-colors"
|
||||||
>
|
>
|
||||||
Open {msg.peerName}
|
Open {msg.peerName}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="text-[9px] text-zinc-500 mt-1.5">
|
<div className="text-[9px] text-ink-soft mt-1.5">
|
||||||
{new Date(msg.timestamp).toLocaleTimeString()}
|
{new Date(msg.timestamp).toLocaleTimeString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -26,14 +26,14 @@ export function PendingAttachmentPill({
|
|||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1.5 rounded-md border border-zinc-700/60 bg-zinc-800/80 px-2 py-1 text-[10px] text-zinc-300 max-w-[200px]">
|
<div className="flex items-center gap-1.5 rounded-md border border-line/60 bg-surface-card/80 px-2 py-1 text-[10px] text-ink-mid max-w-[200px]">
|
||||||
<FileGlyph className="text-zinc-400 shrink-0" />
|
<FileGlyph className="text-ink-mid shrink-0" />
|
||||||
<span className="truncate" title={file.name}>{file.name}</span>
|
<span className="truncate" title={file.name}>{file.name}</span>
|
||||||
<span className="text-zinc-500 shrink-0 tabular-nums">{formatSize(file.size)}</span>
|
<span className="text-ink-soft shrink-0 tabular-nums">{formatSize(file.size)}</span>
|
||||||
<button
|
<button
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
aria-label={`Remove ${file.name}`}
|
aria-label={`Remove ${file.name}`}
|
||||||
className="ml-0.5 text-zinc-500 hover:text-zinc-200 transition-colors shrink-0"
|
className="ml-0.5 text-ink-soft hover:text-ink transition-colors shrink-0"
|
||||||
>
|
>
|
||||||
<svg width="10" height="10" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
<svg width="10" height="10" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||||
<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
||||||
@ -57,8 +57,8 @@ export function AttachmentChip({
|
|||||||
}) {
|
}) {
|
||||||
const toneClasses =
|
const toneClasses =
|
||||||
tone === "user"
|
tone === "user"
|
||||||
? "border-blue-400/30 bg-blue-600/20 hover:bg-blue-600/30 text-blue-100"
|
? "border-blue-400/30 bg-accent-strong/20 hover:bg-accent-strong/30 text-blue-100"
|
||||||
: "border-zinc-600/50 bg-zinc-700/40 hover:bg-zinc-600/50 text-zinc-100";
|
: "border-line/50 bg-surface-card/40 hover:bg-zinc-600/50 text-ink";
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => onDownload(attachment)}
|
onClick={() => onDownload(attachment)}
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export function TextInput({ label, value, onChange, placeholder, mono }: { label
|
|||||||
const id = `textinput-${label.toLowerCase().replace(/\s+/g, "-")}`;
|
const id = `textinput-${label.toLowerCase().replace(/\s+/g, "-")}`;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={id} className="text-[10px] text-zinc-500 block mb-1">{label}</label>
|
<label htmlFor={id} className="text-[10px] text-ink-soft block mb-1">{label}</label>
|
||||||
<input
|
<input
|
||||||
id={id}
|
id={id}
|
||||||
type="text"
|
type="text"
|
||||||
@ -60,7 +60,7 @@ export function TextInput({ label, value, onChange, placeholder, mono }: { label
|
|||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
className={`w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500 ${mono ? "font-mono" : ""}`}
|
className={`w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent ${mono ? "font-mono" : ""}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -70,7 +70,7 @@ export function NumberInput({ label, value, onChange, min, max }: { label: strin
|
|||||||
const id = `numberinput-${label.toLowerCase().replace(/\s+/g, "-")}`;
|
const id = `numberinput-${label.toLowerCase().replace(/\s+/g, "-")}`;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={id} className="text-[10px] text-zinc-500 block mb-1">{label}</label>
|
<label htmlFor={id} className="text-[10px] text-ink-soft block mb-1">{label}</label>
|
||||||
<input
|
<input
|
||||||
id={id}
|
id={id}
|
||||||
type="number"
|
type="number"
|
||||||
@ -79,7 +79,7 @@ export function NumberInput({ label, value, onChange, min, max }: { label: strin
|
|||||||
min={min}
|
min={min}
|
||||||
max={max}
|
max={max}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500 font-mono"
|
className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -89,7 +89,7 @@ export function Toggle({ label, checked, onChange }: { label: string; checked: b
|
|||||||
return (
|
return (
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} className="accent-blue-500" />
|
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} className="accent-blue-500" />
|
||||||
<span className="text-[10px] text-zinc-400">{label}</span>
|
<span className="text-[10px] text-ink-mid">{label}</span>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -99,12 +99,12 @@ export function TagList({ label, values, onChange, placeholder }: { label: strin
|
|||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={id} className="text-[10px] text-zinc-500 block mb-1">{label}</label>
|
<label htmlFor={id} className="text-[10px] text-ink-soft block mb-1">{label}</label>
|
||||||
<div className="flex flex-wrap gap-1 mb-1">
|
<div className="flex flex-wrap gap-1 mb-1">
|
||||||
{values.map((v, i) => (
|
{values.map((v, i) => (
|
||||||
<span key={i} className="inline-flex items-center gap-1 px-1.5 py-0.5 bg-zinc-800 border border-zinc-700 rounded text-[10px] text-zinc-300 font-mono">
|
<span key={i} className="inline-flex items-center gap-1 px-1.5 py-0.5 bg-surface-card border border-line rounded text-[10px] text-ink-mid font-mono">
|
||||||
{v}
|
{v}
|
||||||
<button type="button" aria-label={`Remove tag ${v}`} onClick={() => onChange(values.filter((_, j) => j !== i))} className="text-zinc-500 hover:text-red-400">×</button>
|
<button type="button" aria-label={`Remove tag ${v}`} onClick={() => onChange(values.filter((_, j) => j !== i))} className="text-ink-soft hover:text-bad">×</button>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -121,7 +121,7 @@ export function TagList({ label, values, onChange, placeholder }: { label: strin
|
|||||||
}}
|
}}
|
||||||
placeholder={placeholder || "Type and press Enter"}
|
placeholder={placeholder || "Type and press Enter"}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-[10px] text-zinc-200 focus:outline-none focus:border-blue-500 font-mono"
|
className="w-full bg-surface-card border border-line rounded px-2 py-1 text-[10px] text-ink focus:outline-none focus:border-accent font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -130,8 +130,8 @@ export function TagList({ label, values, onChange, placeholder }: { label: strin
|
|||||||
export function Section({ title, children, defaultOpen = true }: { title: string; children: React.ReactNode; defaultOpen?: boolean }) {
|
export function Section({ title, children, defaultOpen = true }: { title: string; children: React.ReactNode; defaultOpen?: boolean }) {
|
||||||
const [open, setOpen] = useState(defaultOpen);
|
const [open, setOpen] = useState(defaultOpen);
|
||||||
return (
|
return (
|
||||||
<div className="border border-zinc-800 rounded mb-2">
|
<div className="border border-line rounded mb-2">
|
||||||
<button type="button" onClick={() => setOpen(!open)} className="w-full flex items-center justify-between px-3 py-1.5 text-[10px] text-zinc-400 hover:text-zinc-200 bg-zinc-900/50">
|
<button type="button" onClick={() => setOpen(!open)} className="w-full flex items-center justify-between px-3 py-1.5 text-[10px] text-ink-mid hover:text-ink bg-surface-sunken/50">
|
||||||
<span className="font-medium uppercase tracking-wider">{title}</span>
|
<span className="font-medium uppercase tracking-wider">{title}</span>
|
||||||
<span>{open ? "▾" : "▸"}</span>
|
<span>{open ? "▾" : "▸"}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -68,7 +68,7 @@ function labelForKey(key: string): string {
|
|||||||
|
|
||||||
function ScopeBadge({ scope }: { scope: "global" | "workspace" | "override" }) {
|
function ScopeBadge({ scope }: { scope: "global" | "workspace" | "override" }) {
|
||||||
if (scope === "global") {
|
if (scope === "global") {
|
||||||
return <span className="text-[8px] text-amber-400 bg-amber-900/30 px-1.5 py-0.5 rounded" title="Inherited from global secrets">Global</span>;
|
return <span className="text-[8px] text-warm bg-amber-900/30 px-1.5 py-0.5 rounded" title="Inherited from global secrets">Global</span>;
|
||||||
}
|
}
|
||||||
if (scope === "override") {
|
if (scope === "override") {
|
||||||
return <span className="text-[8px] text-purple-400 bg-purple-900/30 px-1.5 py-0.5 rounded" title="Overrides global secret">Override</span>;
|
return <span className="text-[8px] text-purple-400 bg-purple-900/30 px-1.5 py-0.5 rounded" title="Overrides global secret">Override</span>;
|
||||||
@ -96,26 +96,26 @@ function SecretRow({ label, secretKey, isSet, scope, globalMode, onSave, onDelet
|
|||||||
const isPlaintext = secretKey === "MODEL_PROVIDER";
|
const isPlaintext = secretKey === "MODEL_PROVIDER";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-zinc-800/50 rounded px-3 py-2 border border-zinc-700/50">
|
<div className="bg-surface-card/50 rounded px-3 py-2 border border-line/50">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="text-[10px] text-zinc-300">{label}</div>
|
<div className="text-[10px] text-ink-mid">{label}</div>
|
||||||
<div className="flex items-center gap-2 mt-0.5">
|
<div className="flex items-center gap-2 mt-0.5">
|
||||||
<span className="text-[9px] font-mono text-zinc-500">{secretKey}</span>
|
<span className="text-[9px] font-mono text-ink-soft">{secretKey}</span>
|
||||||
{isSet && (
|
{isSet && (
|
||||||
<span className="text-[9px] font-mono text-zinc-500 tracking-widest" title="Value is set (encrypted)">
|
<span className="text-[9px] font-mono text-ink-soft tracking-widest" title="Value is set (encrypted)">
|
||||||
•••••
|
•••••
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
{isSet && <span className="text-[10px] text-green-500 bg-green-900/30 px-1.5 py-0.5 rounded">Set</span>}
|
{isSet && <span className="text-[10px] text-good bg-green-900/30 px-1.5 py-0.5 rounded">Set</span>}
|
||||||
{scope && <ScopeBadge scope={scope} />}
|
{scope && <ScopeBadge scope={scope} />}
|
||||||
{!editing && isSet && (globalMode || scope !== "global") && (
|
{!editing && isSet && (globalMode || scope !== "global") && (
|
||||||
<button type="button" onClick={onDelete} className="text-[11px] text-red-400 hover:text-red-300">Remove</button>
|
<button type="button" onClick={onDelete} className="text-[11px] text-bad hover:text-bad">Remove</button>
|
||||||
)}
|
)}
|
||||||
<button type="button" onClick={() => setEditing(!editing)} className="text-[11px] text-blue-400 hover:text-blue-300">
|
<button type="button" onClick={() => setEditing(!editing)} className="text-[11px] text-accent hover:text-accent">
|
||||||
{actionLabel()}
|
{actionLabel()}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -126,12 +126,12 @@ function SecretRow({ label, secretKey, isSet, scope, globalMode, onSave, onDelet
|
|||||||
value={value} onChange={(e) => setValue(e.target.value)}
|
value={value} onChange={(e) => setValue(e.target.value)}
|
||||||
placeholder={isPlaintext ? "anthropic:claude-sonnet-4-6" : "sk-..."}
|
placeholder={isPlaintext ? "anthropic:claude-sonnet-4-6" : "sk-..."}
|
||||||
type={isPlaintext ? "text" : "password"} autoFocus
|
type={isPlaintext ? "text" : "password"} autoFocus
|
||||||
className="flex-1 bg-zinc-900 border border-zinc-600 rounded px-2 py-1 text-[10px] text-zinc-100 font-mono focus:outline-none focus:border-blue-500"
|
className="flex-1 bg-surface-sunken border border-line rounded px-2 py-1 text-[10px] text-ink font-mono focus:outline-none focus:border-accent"
|
||||||
/>
|
/>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
onClick={() => { onSave(value); setEditing(false); setValue(""); }}
|
onClick={() => { onSave(value); setEditing(false); setValue(""); }}
|
||||||
disabled={!value}
|
disabled={!value}
|
||||||
className="px-2 py-1 bg-blue-600 hover:bg-blue-500 text-[10px] rounded text-white disabled:opacity-30"
|
className="px-2 py-1 bg-accent-strong hover:bg-accent text-[10px] rounded text-ink disabled:opacity-30"
|
||||||
>Save</button>
|
>Save</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -156,19 +156,19 @@ function CustomSecretRow({ secretKey, scope, globalMode, onSave, onDelete }: {
|
|||||||
<div className="py-1.5 px-2">
|
<div className="py-1.5 px-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<span className={`text-[10px] font-mono ${globalMode ? "text-amber-400" : scope === "global" ? "text-zinc-400" : "text-blue-400"}`}>
|
<span className={`text-[10px] font-mono ${globalMode ? "text-warm" : scope === "global" ? "text-ink-mid" : "text-accent"}`}>
|
||||||
{secretKey}
|
{secretKey}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[9px] font-mono text-zinc-500 tracking-widest ml-2">•••••</span>
|
<span className="text-[9px] font-mono text-ink-soft tracking-widest ml-2">•••••</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<span className="text-[10px] text-green-500">Set</span>
|
<span className="text-[10px] text-good">Set</span>
|
||||||
{!globalMode && <ScopeBadge scope={scope} />}
|
{!globalMode && <ScopeBadge scope={scope} />}
|
||||||
{canDelete && !editing && (
|
{canDelete && !editing && (
|
||||||
<button type="button" onClick={onDelete} className="text-[11px] text-red-400 hover:text-red-300">Remove</button>
|
<button type="button" onClick={onDelete} className="text-[11px] text-bad hover:text-bad">Remove</button>
|
||||||
)}
|
)}
|
||||||
{(canDelete || showOverride) && (
|
{(canDelete || showOverride) && (
|
||||||
<button type="button" onClick={() => setEditing(!editing)} className="text-[11px] text-blue-400 hover:text-blue-300">
|
<button type="button" onClick={() => setEditing(!editing)} className="text-[11px] text-accent hover:text-accent">
|
||||||
{editing ? "Cancel" : showOverride ? "Override" : "Update"}
|
{editing ? "Cancel" : showOverride ? "Override" : "Update"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@ -179,12 +179,12 @@ function CustomSecretRow({ secretKey, scope, globalMode, onSave, onDelete }: {
|
|||||||
<input
|
<input
|
||||||
value={value} onChange={(e) => setValue(e.target.value)}
|
value={value} onChange={(e) => setValue(e.target.value)}
|
||||||
placeholder="New value" type="password" autoFocus
|
placeholder="New value" type="password" autoFocus
|
||||||
className="flex-1 bg-zinc-900 border border-zinc-600 rounded px-2 py-1 text-[10px] text-zinc-100 font-mono focus:outline-none focus:border-blue-500"
|
className="flex-1 bg-surface-sunken border border-line rounded px-2 py-1 text-[10px] text-ink font-mono focus:outline-none focus:border-accent"
|
||||||
/>
|
/>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
onClick={() => { onSave(value); setEditing(false); setValue(""); }}
|
onClick={() => { onSave(value); setEditing(false); setValue(""); }}
|
||||||
disabled={!value}
|
disabled={!value}
|
||||||
className="px-2 py-1 bg-blue-600 hover:bg-blue-500 text-[10px] rounded text-white disabled:opacity-30"
|
className="px-2 py-1 bg-accent-strong hover:bg-accent text-[10px] rounded text-ink disabled:opacity-30"
|
||||||
>Save</button>
|
>Save</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -288,17 +288,17 @@ export function SecretsSection({ workspaceId, requiredEnv }: { workspaceId: stri
|
|||||||
return (
|
return (
|
||||||
<Section title="Secrets & API Keys" defaultOpen={false}>
|
<Section title="Secrets & API Keys" defaultOpen={false}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-[10px] text-zinc-500">Loading secrets...</div>
|
<div className="text-[10px] text-ink-soft">Loading secrets...</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{error && <div className="px-2 py-1 bg-red-900/30 border border-red-800 rounded text-[10px] text-red-400">{error}</div>}
|
{error && <div className="px-2 py-1 bg-red-900/30 border border-red-800 rounded text-[10px] text-bad">{error}</div>}
|
||||||
|
|
||||||
{/* Scope toggle */}
|
{/* Scope toggle */}
|
||||||
<div className="flex items-center gap-2 pb-1">
|
<div className="flex items-center gap-2 pb-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setGlobalMode(false)}
|
onClick={() => setGlobalMode(false)}
|
||||||
className={`text-[10px] px-2 py-0.5 rounded transition-colors ${
|
className={`text-[10px] px-2 py-0.5 rounded transition-colors ${
|
||||||
!globalMode ? "bg-blue-600/20 text-blue-300 border border-blue-500/30" : "text-zinc-500 hover:text-zinc-300"
|
!globalMode ? "bg-accent-strong/20 text-accent border border-accent/30" : "text-ink-soft hover:text-ink-mid"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
This Workspace
|
This Workspace
|
||||||
@ -306,7 +306,7 @@ export function SecretsSection({ workspaceId, requiredEnv }: { workspaceId: stri
|
|||||||
<button
|
<button
|
||||||
onClick={() => setGlobalMode(true)}
|
onClick={() => setGlobalMode(true)}
|
||||||
className={`text-[10px] px-2 py-0.5 rounded transition-colors ${
|
className={`text-[10px] px-2 py-0.5 rounded transition-colors ${
|
||||||
globalMode ? "bg-amber-600/20 text-amber-300 border border-amber-500/30" : "text-zinc-500 hover:text-zinc-300"
|
globalMode ? "bg-amber-600/20 text-warm border border-amber-500/30" : "text-ink-soft hover:text-ink-mid"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Global (All Workspaces)
|
Global (All Workspaces)
|
||||||
@ -314,7 +314,7 @@ export function SecretsSection({ workspaceId, requiredEnv }: { workspaceId: stri
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{globalMode && (
|
{globalMode && (
|
||||||
<div className="px-2 py-1.5 bg-amber-950/20 border border-amber-800/30 rounded text-[10px] text-amber-400/80 leading-relaxed">
|
<div className="px-2 py-1.5 bg-amber-950/20 border border-amber-800/30 rounded text-[10px] text-warm/80 leading-relaxed">
|
||||||
Global keys apply to all workspaces. Workspace-level keys override globals with the same name.
|
Global keys apply to all workspaces. Workspace-level keys override globals with the same name.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -349,27 +349,27 @@ export function SecretsSection({ workspaceId, requiredEnv }: { workspaceId: stri
|
|||||||
|
|
||||||
{/* Add new */}
|
{/* Add new */}
|
||||||
{showAdd ? (
|
{showAdd ? (
|
||||||
<div className="bg-zinc-800/50 rounded p-2 space-y-1.5 border border-zinc-700/50">
|
<div className="bg-surface-card/50 rounded p-2 space-y-1.5 border border-line/50">
|
||||||
<input value={newKey} onChange={(e) => setNewKey(e.target.value.toUpperCase())} placeholder="KEY_NAME"
|
<input value={newKey} onChange={(e) => setNewKey(e.target.value.toUpperCase())} placeholder="KEY_NAME"
|
||||||
className="w-full bg-zinc-900 border border-zinc-600 rounded px-2 py-1 text-[10px] font-mono text-zinc-100 focus:outline-none focus:border-blue-500" />
|
className="w-full bg-surface-sunken border border-line rounded px-2 py-1 text-[10px] font-mono text-ink focus:outline-none focus:border-accent" />
|
||||||
<input value={newValue} onChange={(e) => setNewValue(e.target.value)} placeholder="Value" type="password"
|
<input value={newValue} onChange={(e) => setNewValue(e.target.value)} placeholder="Value" type="password"
|
||||||
className="w-full bg-zinc-900 border border-zinc-600 rounded px-2 py-1 text-[10px] text-zinc-100 focus:outline-none focus:border-blue-500" />
|
className="w-full bg-surface-sunken border border-line rounded px-2 py-1 text-[10px] text-ink focus:outline-none focus:border-accent" />
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button type="button" onClick={() => { if (newKey && newValue) handleSave(newKey, newValue); }} disabled={!newKey || !newValue}
|
<button type="button" onClick={() => { if (newKey && newValue) handleSave(newKey, newValue); }} disabled={!newKey || !newValue}
|
||||||
className="px-2 py-1 bg-blue-600 hover:bg-blue-500 text-[10px] rounded text-white disabled:opacity-30">
|
className="px-2 py-1 bg-accent-strong hover:bg-accent text-[10px] rounded text-ink disabled:opacity-30">
|
||||||
Save{globalMode ? " (Global)" : ""}
|
Save{globalMode ? " (Global)" : ""}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => { setShowAdd(false); setNewKey(""); setNewValue(""); }}
|
<button type="button" onClick={() => { setShowAdd(false); setNewKey(""); setNewValue(""); }}
|
||||||
className="px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-300">Cancel</button>
|
className="px-2 py-1 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink-mid">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button type="button" onClick={() => setShowAdd(true)} className="text-[10px] text-blue-400 hover:text-blue-300">
|
<button type="button" onClick={() => setShowAdd(true)} className="text-[10px] text-accent hover:text-accent">
|
||||||
+ Add {globalMode ? "Global " : ""}Variable
|
+ Add {globalMode ? "Global " : ""}Variable
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="text-[9px] text-zinc-500 pt-1">
|
<div className="text-[9px] text-ink-soft pt-1">
|
||||||
Values are encrypted and never exposed to the browser.
|
Values are encrypted and never exposed to the browser.
|
||||||
{globalMode
|
{globalMode
|
||||||
? " Global keys are shared across all workspaces. Restart workspaces to apply changes."
|
? " Global keys are shared across all workspaces. Restart workspaces to apply changes."
|
||||||
|
|||||||
@ -14,8 +14,8 @@ describe("formatCredits", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("pillTone", () => {
|
describe("pillTone", () => {
|
||||||
it("zinc for healthy balance", () => {
|
it("neutral surface for healthy balance", () => {
|
||||||
expect(pillTone({ credits_balance: 5000, plan_monthly_credits: 9000 })).toContain("zinc");
|
expect(pillTone({ credits_balance: 5000, plan_monthly_credits: 9000 })).toContain("surface-card");
|
||||||
});
|
});
|
||||||
it("amber when under 10% of monthly", () => {
|
it("amber when under 10% of monthly", () => {
|
||||||
expect(pillTone({ credits_balance: 500, plan_monthly_credits: 9000 })).toContain("amber");
|
expect(pillTone({ credits_balance: 500, plan_monthly_credits: 9000 })).toContain("amber");
|
||||||
@ -26,7 +26,10 @@ describe("pillTone", () => {
|
|||||||
});
|
});
|
||||||
it("trial (monthly=0) is healthy until balance hits zero", () => {
|
it("trial (monthly=0) is healthy until balance hits zero", () => {
|
||||||
// No paid plan → no ratio reference; only "0" means empty.
|
// No paid plan → no ratio reference; only "0" means empty.
|
||||||
expect(pillTone({ credits_balance: 50, plan_monthly_credits: 0 })).toContain("zinc");
|
// Healthy pill resolves to the warm-paper neutral surface (was zinc
|
||||||
|
// pre-v4 migration); empty pill stays red because tinted state colours
|
||||||
|
// are kept literal so the warning reads in both themes.
|
||||||
|
expect(pillTone({ credits_balance: 50, plan_monthly_credits: 0 })).toContain("surface-card");
|
||||||
expect(pillTone({ credits_balance: 0, plan_monthly_credits: 0 })).toContain("red");
|
expect(pillTone({ credits_balance: 0, plan_monthly_credits: 0 })).toContain("red");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export function pillTone(fields: CreditsFields): string {
|
|||||||
if (balance <= 0) return "bg-red-950 text-red-200 border-red-800";
|
if (balance <= 0) return "bg-red-950 text-red-200 border-red-800";
|
||||||
const ratio = monthly > 0 ? balance / monthly : 1;
|
const ratio = monthly > 0 ? balance / monthly : 1;
|
||||||
if (ratio < 0.1) return "bg-amber-950 text-amber-200 border-amber-800";
|
if (ratio < 0.1) return "bg-amber-950 text-amber-200 border-amber-800";
|
||||||
return "bg-zinc-800 text-zinc-200 border-zinc-700";
|
return "bg-surface-card text-ink border-line";
|
||||||
}
|
}
|
||||||
|
|
||||||
// bannerKind picks which (if any) banner to show under the balance
|
// bannerKind picks which (if any) banner to show under the balance
|
||||||
|
|||||||
@ -12,10 +12,10 @@ export function statusDotClass(status: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TIER_CONFIG: Record<number, { label: string; color: string; border: string }> = {
|
export const TIER_CONFIG: Record<number, { label: string; color: string; border: string }> = {
|
||||||
1: { label: "T1", color: "text-zinc-500 bg-zinc-800/80", border: "text-zinc-400 border-zinc-700/60" },
|
1: { label: "T1", color: "text-ink-soft bg-surface-card/80", border: "text-ink-mid border-line/60" },
|
||||||
2: { label: "T2", color: "text-sky-400 bg-sky-950/50", border: "text-sky-400 border-sky-500/30" },
|
2: { label: "T2", color: "text-sky-400 bg-sky-950/50", border: "text-sky-400 border-sky-500/30" },
|
||||||
3: { label: "T3", color: "text-violet-400 bg-violet-950/50", border: "text-violet-400 border-violet-500/30" },
|
3: { label: "T3", color: "text-violet-400 bg-violet-950/50", border: "text-violet-400 border-violet-500/30" },
|
||||||
4: { label: "T4", color: "text-amber-400 bg-amber-950/50", border: "text-amber-400 border-amber-500/30" },
|
4: { label: "T4", color: "text-warm bg-amber-950/50", border: "text-warm border-amber-500/30" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const COMM_TYPE_LABELS: Record<string, string> = {
|
export const COMM_TYPE_LABELS: Record<string, string> = {
|
||||||
|
|||||||
40
canvas/src/lib/theme-cookie.ts
Normal file
40
canvas/src/lib/theme-cookie.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Theme cookie constants + boot script.
|
||||||
|
*
|
||||||
|
* No "use client" pragma — these are imported by both server components
|
||||||
|
* (app/layout.tsx, which calls cookies() during SSR) and client
|
||||||
|
* components (lib/theme-provider.tsx). Constants exported from a
|
||||||
|
* "use client" file get rewritten by Next.js as client-reference
|
||||||
|
* placeholders, so a server importer sees a Function instead of the
|
||||||
|
* underlying value. Keeping shared primitives here avoids that trap.
|
||||||
|
*
|
||||||
|
* Aligned with molecule-app's matching module — same cookie name, same
|
||||||
|
* three-value enum — so the preference follows the user across surfaces
|
||||||
|
* (app, market, landing, canvas) when the cookie is set with
|
||||||
|
* Domain=.moleculesai.app.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ThemePreference = "system" | "light" | "dark";
|
||||||
|
export type ResolvedTheme = "light" | "dark";
|
||||||
|
|
||||||
|
export const THEME_COOKIE = "mol_theme";
|
||||||
|
|
||||||
|
export function readThemeCookie(value: string | undefined): ThemePreference {
|
||||||
|
if (value === "light" || value === "dark" || value === "system") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return "system";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inline boot script. Stringified verbatim by app/layout.tsx so it runs
|
||||||
|
* synchronously before the body paints — preventing a flash of the wrong
|
||||||
|
* theme. Reads cookie via document.cookie regex (no parser available
|
||||||
|
* yet), falls back to matchMedia, and stamps data-theme on <html>.
|
||||||
|
*
|
||||||
|
* Must remain tiny and dependency-free — runs before hydration. The
|
||||||
|
* canvas's middleware sets a strict CSP with nonce-based script-src in
|
||||||
|
* production; the layout passes the nonce on the <script> tag so this
|
||||||
|
* passes the inline-script gate.
|
||||||
|
*/
|
||||||
|
export const themeBootScript = `(()=>{try{var m=document.cookie.match(/(?:^|;\\s*)${THEME_COOKIE}=(system|light|dark)/);var p=m?m[1]:"system";var r=p==="system"?(window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"):p;document.documentElement.dataset.theme=r;}catch(e){}})();`;
|
||||||
145
canvas/src/lib/theme-provider.tsx
Normal file
145
canvas/src/lib/theme-provider.tsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import {
|
||||||
|
THEME_COOKIE,
|
||||||
|
type ResolvedTheme,
|
||||||
|
type ThemePreference,
|
||||||
|
} from "@/lib/theme-cookie";
|
||||||
|
|
||||||
|
// Re-export so callers can keep `import { THEME_COOKIE, type ThemePreference } from "@/lib/theme-provider"`
|
||||||
|
// working — but for server-component imports, prefer the underlying module
|
||||||
|
// directly to dodge the "use client" serialization wrapper.
|
||||||
|
export { THEME_COOKIE, themeBootScript } from "@/lib/theme-cookie";
|
||||||
|
export type { ThemePreference, ResolvedTheme } from "@/lib/theme-cookie";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme system: System / Light / Dark.
|
||||||
|
*
|
||||||
|
* `theme` — what the user picked. Persisted in the `mol_theme`
|
||||||
|
* cookie so it survives reloads and (when set to a
|
||||||
|
* parent domain) follows the user across moleculesai.app
|
||||||
|
* surfaces (app, market, docs, landing, canvas).
|
||||||
|
* `resolvedTheme` — the mode actually rendered. Equal to `theme` when the
|
||||||
|
* user picked light or dark; equal to the OS preference
|
||||||
|
* when they picked system.
|
||||||
|
*
|
||||||
|
* No-flash on first paint is handled by the inline `<script>` in
|
||||||
|
* app/layout.tsx, which runs before hydration and stamps data-theme on
|
||||||
|
* <html> based on cookie + matchMedia. This provider then takes over on
|
||||||
|
* mount and keeps the attribute in sync with state changes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type ThemeContextValue = {
|
||||||
|
theme: ThemePreference;
|
||||||
|
resolvedTheme: ResolvedTheme;
|
||||||
|
setTheme: (next: ThemePreference) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cookie attributes:
|
||||||
|
* - `Domain=.moleculesai.app` so the preference follows the user across
|
||||||
|
* canvas.moleculesai.app, app.moleculesai.app, market.moleculesai.app,
|
||||||
|
* docs.moleculesai.app, AND tenant subdomains (acme.moleculesai.app,
|
||||||
|
* acme.staging.moleculesai.app, ...). All match `endsWith(".moleculesai.app")`.
|
||||||
|
* Skipped on localhost (browser would reject Domain= for a
|
||||||
|
* non-public-suffix host).
|
||||||
|
* - `Max-Age=1y` — long-lived; users rarely change theme.
|
||||||
|
* - `SameSite=Lax` — fine for a UI preference; not security-sensitive.
|
||||||
|
* - `Secure` only in production HTTPS contexts.
|
||||||
|
*/
|
||||||
|
function writeThemeCookie(value: ThemePreference): void {
|
||||||
|
if (typeof document === "undefined") return;
|
||||||
|
const isProdHost =
|
||||||
|
typeof window !== "undefined" &&
|
||||||
|
window.location.hostname.endsWith(".moleculesai.app");
|
||||||
|
const parts = [
|
||||||
|
`${THEME_COOKIE}=${value}`,
|
||||||
|
"Path=/",
|
||||||
|
"Max-Age=31536000",
|
||||||
|
"SameSite=Lax",
|
||||||
|
];
|
||||||
|
if (isProdHost) {
|
||||||
|
parts.push("Domain=.moleculesai.app");
|
||||||
|
parts.push("Secure");
|
||||||
|
}
|
||||||
|
document.cookie = parts.join("; ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyResolvedTheme(resolved: ResolvedTheme): void {
|
||||||
|
if (typeof document === "undefined") return;
|
||||||
|
document.documentElement.dataset.theme = resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
initialTheme,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
initialTheme: ThemePreference;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const [theme, setThemeState] = useState<ThemePreference>(initialTheme);
|
||||||
|
const [systemPref, setSystemPref] = useState<ResolvedTheme>("light");
|
||||||
|
|
||||||
|
// Track OS preference when the user is on "system". Only registers a
|
||||||
|
// listener while theme === "system" so we don't pay listener cost in
|
||||||
|
// explicit modes.
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
const mql = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
setSystemPref(mql.matches ? "dark" : "light");
|
||||||
|
if (theme !== "system") return;
|
||||||
|
const onChange = (e: MediaQueryListEvent) =>
|
||||||
|
setSystemPref(e.matches ? "dark" : "light");
|
||||||
|
mql.addEventListener("change", onChange);
|
||||||
|
return () => mql.removeEventListener("change", onChange);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const resolvedTheme: ResolvedTheme =
|
||||||
|
theme === "system" ? systemPref : theme;
|
||||||
|
|
||||||
|
// Reflect resolvedTheme onto <html data-theme>. The inline boot script
|
||||||
|
// already did this once before hydration; this keeps it in sync after.
|
||||||
|
useEffect(() => {
|
||||||
|
applyResolvedTheme(resolvedTheme);
|
||||||
|
}, [resolvedTheme]);
|
||||||
|
|
||||||
|
const setTheme = useCallback((next: ThemePreference) => {
|
||||||
|
setThemeState(next);
|
||||||
|
writeThemeCookie(next);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value = useMemo<ThemeContextValue>(
|
||||||
|
() => ({ theme, resolvedTheme, setTheme }),
|
||||||
|
[theme, resolvedTheme, setTheme],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults returned when no <ThemeProvider> is in the tree. Real app
|
||||||
|
// always wraps via app/layout.tsx; this fallback exists so unit tests
|
||||||
|
// rendering components in isolation don't have to know about theme.
|
||||||
|
// setTheme is a no-op — there's no state to mutate without a provider —
|
||||||
|
// and the noopTheme reference is stable so consumers using it in deps
|
||||||
|
// arrays don't churn.
|
||||||
|
const noopTheme: ThemeContextValue = {
|
||||||
|
theme: "system",
|
||||||
|
resolvedTheme: "light",
|
||||||
|
setTheme: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useTheme(): ThemeContextValue {
|
||||||
|
return useContext(ThemeContext) ?? noopTheme;
|
||||||
|
}
|
||||||
43
canvas/src/lib/theme.ts
Normal file
43
canvas/src/lib/theme.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Theme tokens — semantic, light + dark.
|
||||||
|
*
|
||||||
|
* Source of truth for colours lives in app/globals.css as CSS custom
|
||||||
|
* properties (`--color-surface`, `--color-ink`, etc.). Tailwind v4
|
||||||
|
* generates utilities (`bg-surface`, `text-ink`, ...) from those tokens
|
||||||
|
* automatically; that's the preferred consumption path.
|
||||||
|
*
|
||||||
|
* This module exports `cssVar()` for the rare case where an inline
|
||||||
|
* `style={{}}` prop or SVG fill needs a token value — the returned
|
||||||
|
* `var(--color-foo)` string follows the live theme without re-renders.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ColorToken =
|
||||||
|
// Warm-paper surface (light-flippable)
|
||||||
|
| "surface"
|
||||||
|
| "surface-elevated"
|
||||||
|
| "surface-sunken"
|
||||||
|
| "surface-card"
|
||||||
|
| "line"
|
||||||
|
| "line-soft"
|
||||||
|
| "ink"
|
||||||
|
| "ink-mid"
|
||||||
|
| "ink-soft"
|
||||||
|
| "accent"
|
||||||
|
| "accent-strong"
|
||||||
|
| "warm"
|
||||||
|
| "good"
|
||||||
|
| "bad"
|
||||||
|
// Always-dark (terminal / console / log surfaces)
|
||||||
|
| "bg"
|
||||||
|
| "bg-elev"
|
||||||
|
| "bg-card"
|
||||||
|
| "line-strong"
|
||||||
|
| "ink-mute"
|
||||||
|
| "ink-dim"
|
||||||
|
| "accent-dim"
|
||||||
|
| "plasma"
|
||||||
|
| "warn";
|
||||||
|
|
||||||
|
export function cssVar(token: ColorToken): string {
|
||||||
|
return `var(--color-${token})`;
|
||||||
|
}
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import type { Config } from "tailwindcss";
|
|
||||||
import typography from "@tailwindcss/typography";
|
|
||||||
|
|
||||||
const config: Config = {
|
|
||||||
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
|
|
||||||
theme: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [typography],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
Loading…
Reference in New Issue
Block a user