docs(canvas): update audit status — all accessibility gaps now closed #197

Merged
core-lead merged 8 commits from docs/update-canvas-audit-status into main 2026-05-09 23:45:14 +00:00
55 changed files with 393 additions and 283 deletions

View File

@ -354,7 +354,7 @@ function OrgCTA({ org }: { org: Org }) {
);
}
// provisioning / unknown — non-interactive
return <span className="text-sm text-ink-soft">{org.status}</span>;
return <span className="text-sm text-ink-mid">{org.status}</span>;
}
function EmptyState({ banner }: { banner?: React.ReactNode }) {
@ -420,7 +420,7 @@ function CreateOrgForm({ onCreated }: { onCreated: (slug: string) => void }) {
aria-describedby="org-slug-hint"
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-ink-soft">
<p id="org-slug-hint" className="mt-1 text-xs text-ink-mid">
Lowercase letters, numbers, and hyphens only. Cannot be changed later.
</p>
</div>

View File

@ -56,7 +56,7 @@ export default function Home() {
<div className="fixed inset-0 flex items-center justify-center bg-surface">
<div role="status" aria-live="polite" className="flex flex-col items-center gap-3">
<Spinner size="lg" />
<span className="text-xs text-ink-soft">Loading canvas...</span>
<span className="text-xs text-ink-mid">Loading canvas...</span>
</div>
</div>
);
@ -119,11 +119,11 @@ function PlatformDownDiagnostic() {
Most common cause on a dev host: one of those services stopped.
</p>
<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-ink-soft mb-2">Try first</div>
<div className="text-[10px] uppercase tracking-wider text-ink-mid mb-2">Try first</div>
<pre className="text-[12px] text-ink-mid font-mono whitespace-pre-wrap leading-relaxed">{`brew services start postgresql@14
brew services start redis`}</pre>
</div>
<p className="text-[11px] text-ink-soft max-w-lg text-center">
<p className="text-[11px] text-ink-mid max-w-lg text-center">
If both are running, check <code className="font-mono">/tmp/molecule-server.log</code> for
the underlying error. If you&apos;re on hosted SaaS, this is a platform incident try again in a moment.
</p>

View File

@ -55,13 +55,13 @@ export default function PricingPage() {
</a>
.
</p>
<p className="mt-6 text-sm text-ink-soft">
<p className="mt-6 text-sm text-ink-mid">
Prices shown in USD. Flat-rate per org no per-seat fees on any paid tier.
Enterprise / self-hosted licensing available contact us.
</p>
</section>
<footer className="mx-auto mt-20 max-w-5xl border-t border-line px-6 py-6 text-center text-sm text-ink-soft">
<footer className="mx-auto mt-20 max-w-5xl border-t border-line px-6 py-6 text-center text-sm text-ink-mid">
<p>
© {new Date().getFullYear()} Molecule AI, Inc. ·{" "}
<a href="/legal/terms" className="hover:text-ink-mid">

View File

@ -127,7 +127,7 @@ export function AuditTrailPanel({ workspaceId }: Props) {
if (loading) {
return (
<div className="flex items-center justify-center h-32">
<span className="text-xs text-ink-soft">Loading audit trail</span>
<span className="text-xs text-ink-mid">Loading audit trail</span>
</div>
);
}
@ -145,7 +145,7 @@ export function AuditTrailPanel({ workspaceId }: Props) {
className={`px-2 py-1 text-[10px] rounded-md font-medium transition-all shrink-0 ${
filter === f.id
? "bg-surface-card text-ink ring-1 ring-zinc-600"
: "text-ink-soft hover:text-ink-mid hover:bg-surface-card/60"
: "text-ink-mid hover:text-ink-mid hover:bg-surface-card/60"
}`}
>
{f.label}
@ -174,9 +174,9 @@ export function AuditTrailPanel({ workspaceId }: Props) {
{entries.length === 0 ? (
/* Empty state */
<div className="flex flex-col items-center justify-center py-16 gap-3 text-center">
<span className="text-4xl text-ink-soft" aria-hidden="true"></span>
<span className="text-4xl text-ink-mid" aria-hidden="true"></span>
<p className="text-sm font-medium text-ink-mid">No audit events yet</p>
<p className="text-[11px] text-ink-soft max-w-[200px] leading-relaxed">
<p className="text-[11px] text-ink-mid max-w-[200px] leading-relaxed">
Delegation, decision, gate, and human-in-the-loop events will appear here.
</p>
</div>
@ -203,7 +203,7 @@ export function AuditTrailPanel({ workspaceId }: Props) {
)}
{/* Entry count footer */}
<p className="mt-3 text-center text-[9px] text-ink-soft">
<p className="mt-3 text-center text-[9px] text-ink-mid">
{entries.length} event{entries.length !== 1 ? "s" : ""} loaded
{cursor ? " · more available" : " · all loaded"}
</p>
@ -265,7 +265,7 @@ export function AuditEntryRow({ entry, now }: AuditEntryRowProps) {
)}
{/* Relative timestamp */}
<span className="shrink-0 text-[9px] text-ink-soft">
<span className="shrink-0 text-[9px] text-ink-mid">
{formatAuditRelativeTime(entry.created_at, now)}
</span>
</div>

View File

@ -125,7 +125,7 @@ export function BundleDropZone() {
<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-sm font-semibold text-ink">Drop Bundle to Import</div>
<div className="text-xs text-ink-soft mt-1">.bundle.json files only</div>
<div className="text-xs text-ink-mid mt-1">.bundle.json files only</div>
</div>
</div>
)}

View File

@ -226,7 +226,7 @@ export function CommunicationOverlay() {
type="button"
onClick={() => setVisible(false)}
aria-label="Close communications panel"
className="text-ink-soft hover:text-ink-mid text-xs"
className="text-ink-mid hover:text-ink-mid text-xs"
>
<span aria-hidden="true"></span>
</button>
@ -268,7 +268,7 @@ export function CommunicationOverlay() {
</div>
</div>
{c.summary && (
<div className="text-ink-soft truncate mt-0.5 pl-4">{c.summary}</div>
<div className="text-ink-mid truncate mt-0.5 pl-4">{c.summary}</div>
)}
{c.durationMs && (
<div className="text-ink-mid pl-4">{c.durationMs}ms</div>

View File

@ -103,7 +103,7 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
EC2 console output
</h3>
{workspaceName && (
<div className="text-[11px] text-ink-soft mt-0.5 truncate max-w-[600px]">
<div className="text-[11px] text-ink-mid mt-0.5 truncate max-w-[600px]">
{workspaceName}
</div>
)}
@ -124,7 +124,7 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
<div className="flex-1 overflow-auto bg-black/80 p-4">
{loading && (
<div className="text-[12px] text-ink-soft" data-testid="console-loading">
<div className="text-[12px] text-ink-mid" data-testid="console-loading">
Loading console output
</div>
)}

View File

@ -311,7 +311,7 @@ export function ContextMenu() {
aria-hidden="true"
className={`w-1.5 h-1.5 rounded-full ${statusDotClass(contextMenu.nodeData.status)}`}
/>
<span className="text-[10px] text-ink-soft">{contextMenu.nodeData.status}</span>
<span className="text-[10px] text-ink-mid">{contextMenu.nodeData.status}</span>
</div>
</div>

View File

@ -106,7 +106,7 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
<Dialog.Title className="text-sm font-semibold text-ink">
Conversation Trace
</Dialog.Title>
<p className="text-[10px] text-ink-soft mt-0.5">
<p className="text-[10px] text-ink-mid mt-0.5">
{entries.length} events across all workspaces
</p>
</div>
@ -114,7 +114,7 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
<button
type="button"
aria-label="Close conversation trace"
className="text-ink-soft hover:text-ink-mid text-lg px-2"
className="text-ink-mid hover:text-ink-mid text-lg px-2"
>
</button>
@ -124,13 +124,13 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
{/* Timeline */}
<div className="flex-1 overflow-y-auto px-5 py-4">
{loading && (
<div className="text-xs text-ink-soft text-center py-8">
<div className="text-xs text-ink-mid text-center py-8">
Loading trace from all workspaces...
</div>
)}
{!loading && entries.length === 0 && (
<div className="text-xs text-ink-soft text-center py-8">
<div className="text-xs text-ink-mid text-center py-8">
No activity found
</div>
)}
@ -250,7 +250,7 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
{/* Message content — show request and/or response */}
{requestText && (
<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-ink-soft uppercase mb-1">
<div className="text-[8px] text-ink-mid uppercase mb-1">
{isSend ? "Task" : "Request"}
</div>
<div className="text-[10px] text-ink-mid whitespace-pre-wrap break-words leading-relaxed">

View File

@ -338,7 +338,7 @@ export function CreateWorkspaceButton() {
<Dialog.Title className="text-base font-semibold text-ink mb-1">
Create Workspace
</Dialog.Title>
<p className="text-xs text-ink-soft mb-5">
<p className="text-xs text-ink-mid mb-5">
Add a new workspace node to the canvas
</p>
@ -376,7 +376,7 @@ export function CreateWorkspaceButton() {
/>
<div className="text-xs">
<div className="text-ink font-medium">External agent (bring your own compute)</div>
<div className="text-ink-soft mt-0.5">
<div className="text-ink-mid mt-0.5">
Skip the container. We&apos;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>
@ -456,7 +456,7 @@ export function CreateWorkspaceButton() {
<p className="text-[11px] font-semibold text-violet-400 uppercase tracking-wide">
Hermes Provider
</p>
<p className="text-[11px] text-ink-soft -mt-1">
<p className="text-[11px] text-ink-mid -mt-1">
Choose the AI provider and paste your API key. The key is
stored as an encrypted workspace secret.
</p>
@ -534,7 +534,7 @@ export function CreateWorkspaceButton() {
(m) => <option key={m} value={m} />,
)}
</datalist>
<p className="text-[10px] text-ink-soft mt-1">
<p className="text-[10px] text-ink-mid mt-1">
Slug determines which provider hermes routes to at install time.
</p>
</div>
@ -626,7 +626,7 @@ function InputField({
className={`w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink placeholder-ink-soft focus:outline-none focus:border-accent/60 focus:ring-1 focus:ring-accent/20 transition-colors ${mono ? "font-mono text-xs" : ""}`}
/>
{helper && (
<p className="mt-1 text-xs text-ink-soft">{helper}</p>
<p className="mt-1 text-xs text-ink-mid">{helper}</p>
)}
</div>
);

View File

@ -129,11 +129,11 @@ export function EmptyState() {
T{t.tier}
</span>
</div>
<p className="text-[11px] text-ink-soft line-clamp-2 leading-relaxed">
<p className="text-[11px] text-ink-mid line-clamp-2 leading-relaxed">
{t.description || "No description"}
</p>
{t.skill_count > 0 && (
<p className="text-[9px] text-ink-soft mt-1.5">
<p className="text-[9px] text-ink-mid mt-1.5">
{t.skill_count} skill{t.skill_count !== 1 ? "s" : ""}
{t.model ? ` · ${t.model}` : ""}
</p>
@ -174,10 +174,10 @@ export function EmptyState() {
<div className="mt-5 pt-4 border-t border-line/50">
<div className="flex items-center justify-center gap-6 text-[10px] text-ink-mid">
<span>Drag to nest workspaces into teams</span>
<span className="text-ink-soft">|</span>
<span className="text-ink-mid">|</span>
<span>Right-click for actions</span>
<span className="text-ink-soft">|</span>
<span>Press <kbd className="px-1 py-0.5 bg-surface-card rounded text-ink-soft font-mono">&#8984;K</kbd> to search</span>
<span className="text-ink-mid">|</span>
<span>Press <kbd className="px-1 py-0.5 bg-surface-card rounded text-ink-mid font-mono">&#8984;K</kbd> to search</span>
</div>
</div>
</div>

View File

@ -201,7 +201,7 @@ export function ExternalConnectModal({ info, onClose }: Props) {
className={`px-3 py-2 text-sm border-b-2 -mb-px transition-colors ${
tab === t
? "border-accent text-ink"
: "border-transparent text-ink-soft hover:text-ink-mid"
: "border-transparent text-ink-mid hover:text-ink-mid"
}`}
>
{t === "claude"
@ -335,7 +335,7 @@ function SnippetBlock({
return (
<div>
<div className="flex items-center justify-between pb-1">
<span className="text-xs text-ink-soft">{label}</span>
<span className="text-xs text-ink-mid">{label}</span>
<button
type="button"
onClick={onCopy}
@ -366,7 +366,7 @@ function Field({
}) {
return (
<div className="flex items-center gap-2">
<span className="text-xs text-ink-soft w-36 shrink-0">{label}</span>
<span className="text-xs text-ink-mid w-36 shrink-0">{label}</span>
<code
className={`flex-1 text-xs bg-surface border border-line rounded px-2 py-1 text-ink break-all ${mono ? "font-mono" : ""}`}
>

View File

@ -16,6 +16,10 @@ const SHORTCUT_GROUPS: ShortcutGroup[] = [
keys: ["Esc"],
description: "Close context menu, clear selection, or deselect",
},
{
keys: ["↑↓←→"],
description: "Nudge selected node 20px; hold Shift for 100px",
},
{
keys: ["Enter"],
description: "Descend into selected node's first child",
@ -177,7 +181,7 @@ export function KeyboardShortcutsDialog({ open, onClose }: Props) {
<div className="overflow-y-auto p-5 space-y-5">
{SHORTCUT_GROUPS.map((group) => (
<div key={group.title}>
<h3 className="text-[10px] font-semibold uppercase tracking-[0.2em] text-ink-soft mb-2.5">
<h3 className="text-[10px] font-semibold uppercase tracking-[0.2em] text-ink-mid mb-2.5">
{group.title}
</h3>
<div className="space-y-2">
@ -193,7 +197,7 @@ export function KeyboardShortcutsDialog({ open, onClose }: Props) {
{shortcut.keys.map((k, j) => (
<span key={j} className="flex items-center gap-0.5">
{j > 0 && (
<span className="text-[9px] text-ink-soft mx-0.5">
<span className="text-[9px] text-ink-mid mx-0.5">
+
</span>
)}
@ -212,7 +216,7 @@ export function KeyboardShortcutsDialog({ open, onClose }: Props) {
{/* Footer */}
<div className="px-5 py-3 border-t border-line bg-surface-sunken/30 shrink-0">
<p className="text-[10px] text-ink-soft text-center">
<p className="text-[10px] text-ink-mid text-center">
Press{" "}
<kbd className="inline-flex items-center rounded border border-line/70 bg-surface-sunken/70 px-1.5 py-0.5 text-[10px] font-medium text-ink font-mono">
Esc

View File

@ -97,7 +97,7 @@ export function Legend() {
// 24×24 touch target (was ~10×16, well under WCAG 2.5.5 min).
// Negative margin keeps the visual position the same as before
// — only the hit area + focus ring are larger.
className="-mt-1.5 -mr-1.5 w-6 h-6 inline-flex items-center justify-center rounded text-[14px] leading-none text-ink-soft hover:text-ink hover:bg-surface-card/40 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60 transition-colors"
className="-mt-1.5 -mr-1.5 w-6 h-6 inline-flex items-center justify-center rounded text-[14px] leading-none text-ink-mid hover:text-ink hover:bg-surface-card/40 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60 transition-colors"
>
×
</button>
@ -105,7 +105,7 @@ export function Legend() {
{/* Status */}
<div className="mb-2">
<div className="text-[11px] text-ink-soft font-medium mb-1">Status</div>
<div className="text-[11px] text-ink-mid font-medium mb-1">Status</div>
<div className="flex flex-wrap gap-x-3 gap-y-1">
{LEGEND_STATUSES.map((s) => (
<StatusItem key={s} color={STATUS_CONFIG[s].dot} label={STATUS_CONFIG[s].label} />
@ -115,7 +115,7 @@ export function Legend() {
{/* Tiers */}
<div className="mb-2">
<div className="text-[11px] text-ink-soft font-medium mb-1">Tier</div>
<div className="text-[11px] text-ink-mid font-medium mb-1">Tier</div>
<div className="flex flex-wrap gap-x-3 gap-y-1">
{LEGEND_TIERS.map(({ tier, label }) => (
<TierItem key={tier} tier={tier} label={label} color={TIER_CONFIG[tier].border} />
@ -125,7 +125,7 @@ export function Legend() {
{/* Communication */}
<div>
<div className="text-[11px] text-ink-soft font-medium mb-1">Communication</div>
<div className="text-[11px] text-ink-mid font-medium mb-1">Communication</div>
<div className="flex flex-wrap gap-x-3 gap-y-1">
<CommItem icon="↗" color="text-cyan-400" label="A2A Out" />
<CommItem icon="↙" color="text-accent" label="A2A In" />

View File

@ -288,7 +288,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
if (loading && entries.length === 0 && !error && !pluginUnavailable) {
return (
<div className="flex items-center justify-center h-32">
<span className="text-xs text-ink-soft">Loading memories</span>
<span className="text-xs text-ink-mid">Loading memories</span>
</div>
);
}
@ -311,7 +311,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
{/* Namespace dropdown */}
<div className="px-4 pt-3 pb-2 border-b border-line/40 shrink-0 space-y-2">
<div className="flex items-center gap-2">
<label htmlFor="namespace-dropdown" className="text-[10px] text-ink-soft shrink-0">
<label htmlFor="namespace-dropdown" className="text-[10px] text-ink-mid shrink-0">
Namespace:
</label>
<select
@ -337,7 +337,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
height="12"
viewBox="0 0 16 16"
fill="none"
className="absolute left-2.5 text-ink-soft pointer-events-none shrink-0"
className="absolute left-2.5 text-ink-mid pointer-events-none shrink-0"
aria-hidden="true"
>
<circle cx="7" cy="7" r="4.5" stroke="currentColor" strokeWidth="1.5" />
@ -360,7 +360,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
setDebouncedQuery('');
}}
aria-label="Clear search"
className="absolute right-2 text-ink-soft hover:text-ink transition-colors text-sm leading-none"
className="absolute right-2 text-ink-mid hover:text-ink transition-colors text-sm leading-none"
>
×
</button>
@ -370,7 +370,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
{/* Toolbar */}
<div className="px-4 py-2.5 border-b border-line/40 flex items-center justify-between shrink-0">
<span className="text-[11px] text-ink-soft">
<span className="text-[11px] text-ink-mid">
{debouncedQuery
? `${entries.length} result${entries.length !== 1 ? 's' : ''}`
: entries.length === 1
@ -446,11 +446,11 @@ function EmptyState({
// mirror it so the operator sees both signals.
return (
<div className="flex flex-col items-center justify-center py-16 gap-3 text-center">
<span className="text-4xl text-ink-soft" aria-hidden="true">
<span className="text-4xl text-ink-mid" aria-hidden="true">
</span>
<p className="text-sm font-medium text-ink-mid">Memory plugin disabled</p>
<p className="text-[11px] text-ink-soft max-w-[220px] leading-relaxed">
<p className="text-[11px] text-ink-mid max-w-[220px] leading-relaxed">
See banner above for the operator-side fix.
</p>
</div>
@ -459,11 +459,11 @@ function EmptyState({
if (query) {
return (
<div className="flex flex-col items-center justify-center py-16 gap-3 text-center">
<span className="text-4xl text-ink-soft" aria-hidden="true">
<span className="text-4xl text-ink-mid" aria-hidden="true">
</span>
<p className="text-sm font-medium text-ink-mid">No memories match your search</p>
<p className="text-[11px] text-ink-soft max-w-[200px] leading-relaxed">
<p className="text-[11px] text-ink-mid max-w-[200px] leading-relaxed">
Try a different query or clear the search.
</p>
</div>
@ -471,11 +471,11 @@ function EmptyState({
}
return (
<div className="flex flex-col items-center justify-center py-16 gap-3 text-center">
<span className="text-4xl text-ink-soft" aria-hidden="true">
<span className="text-4xl text-ink-mid" aria-hidden="true">
</span>
<p className="text-sm font-medium text-ink-mid">No memories yet</p>
<p className="text-[11px] text-ink-soft max-w-[220px] leading-relaxed">
<p className="text-[11px] text-ink-mid max-w-[220px] leading-relaxed">
Agents commit memories via MCP tools (commit_memory, commit_summary). They
appear here once written.
</p>
@ -558,7 +558,7 @@ function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) {
{/* Namespace tag */}
<span
className="text-[9px] shrink-0 font-mono text-ink-soft truncate max-w-[100px]"
className="text-[9px] shrink-0 font-mono text-ink-mid truncate max-w-[100px]"
title={entry.namespace}
>
{entry.namespace}
@ -598,10 +598,10 @@ function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) {
)}
<span className="text-[9px] text-ink-soft shrink-0">
<span className="text-[9px] text-ink-mid shrink-0">
{formatRelativeTime(entry.created_at)}
</span>
<span className="text-[9px] text-ink-soft shrink-0" aria-hidden="true">
<span className="text-[9px] text-ink-mid shrink-0" aria-hidden="true">
{expanded ? '▼' : '▶'}
</span>
</button>
@ -618,7 +618,7 @@ function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) {
{entry.content}
</pre>
<div className="flex items-center justify-between gap-2">
<span className="text-[9px] text-ink-soft">
<span className="text-[9px] text-ink-mid">
Created: {new Date(entry.created_at).toLocaleString()}
{entry.expires_at && ` · Expires: ${new Date(entry.expires_at).toLocaleString()}`}
</span>

View File

@ -421,7 +421,7 @@ function ProviderPickerModal({
<div className="text-[11px] text-ink-mid font-medium">
{getKeyLabel(entry.key)}
</div>
<div className="text-[9px] font-mono text-ink-soft">{entry.key}</div>
<div className="text-[9px] font-mono text-ink-mid">{entry.key}</div>
</div>
{entry.saved && (
<span className="text-[9px] text-good bg-emerald-900/30 px-1.5 py-0.5 rounded flex items-center gap-1">
@ -675,7 +675,7 @@ function AllKeysModal({
<div className="text-[11px] text-ink-mid font-medium">
{getKeyLabel(entry.key)}
</div>
<div className="text-[9px] font-mono text-ink-soft">{entry.key}</div>
<div className="text-[9px] font-mono text-ink-mid">{entry.key}</div>
</div>
{entry.saved && (
<span className="text-[9px] text-good bg-emerald-900/30 px-1.5 py-0.5 rounded flex items-center gap-1">

View File

@ -247,7 +247,7 @@ export function OrgImportPreflightModal({
<h2 id="org-preflight-title" className="text-sm font-semibold text-ink">
Deploy {orgName}
</h2>
<p className="mt-0.5 text-[11px] text-ink-soft">
<p className="mt-0.5 text-[11px] text-ink-mid">
{workspaceCount} workspace{workspaceCount === 1 ? "" : "s"}.
Review the credentials needed before import.
</p>
@ -400,7 +400,7 @@ function StrictEnvRow({
<li className="flex items-center gap-2 rounded bg-surface-sunken/70 border border-line px-2 py-1.5">
<code
className={`text-[11px] font-mono flex-1 ${
configured ? "text-ink-soft line-through" : "text-ink"
configured ? "text-ink-mid line-through" : "text-ink"
}`}
>
{envKey}
@ -492,7 +492,7 @@ function AnyOfEnvGroup({
>
<code
className={`text-[11px] font-mono flex-1 ${
isConfigured ? "text-ink-soft line-through" : "text-ink"
isConfigured ? "text-ink-mid line-through" : "text-ink"
}`}
>
{m}

View File

@ -356,7 +356,7 @@ export function ProviderModelSelector({
<div>
<label
htmlFor={providerSelectId}
className="text-[10px] uppercase tracking-wide text-ink-soft font-semibold mb-1.5 block"
className="text-[10px] uppercase tracking-wide text-ink-mid font-semibold mb-1.5 block"
>
Provider <span aria-hidden="true" className="text-bad">*</span>
<span className="sr-only"> (required)</span>
@ -382,13 +382,13 @@ export function ProviderModelSelector({
{selected?.tooltip && (
<p
id={`${providerSelectId}-help`}
className="text-[9px] text-ink-soft mt-1 leading-relaxed"
className="text-[9px] text-ink-mid mt-1 leading-relaxed"
>
{selected.tooltip}
</p>
)}
{selected && selected.envVars.length > 0 && (
<p className="text-[9px] text-ink-soft mt-0.5 font-mono">
<p className="text-[9px] text-ink-mid mt-0.5 font-mono">
requires: {selected.envVars.join(", ")}
</p>
)}
@ -397,7 +397,7 @@ export function ProviderModelSelector({
<div>
<label
htmlFor={modelSelectId}
className="text-[10px] uppercase tracking-wide text-ink-soft font-semibold mb-1.5 block"
className="text-[10px] uppercase tracking-wide text-ink-mid font-semibold mb-1.5 block"
>
Model <span aria-hidden="true" className="text-bad">*</span>
<span className="sr-only"> (required)</span>
@ -422,7 +422,7 @@ export function ProviderModelSelector({
data-testid="model-input"
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-ink-soft mt-1 leading-relaxed">
<p className="text-[9px] text-ink-mid mt-1 leading-relaxed">
{selected?.wildcard
? wildcardHelpText(selected)
: "Free-text model id. Make sure the provider can resolve it."}

View File

@ -157,7 +157,7 @@ export function PurchaseSuccessModal() {
</div>
<div className="flex items-center justify-between gap-3 px-6 py-3 border-t border-line bg-surface/50">
<span className="font-mono text-[10.5px] uppercase tracking-[0.12em] text-ink-soft">
<span className="font-mono text-[10.5px] uppercase tracking-[0.12em] text-ink-mid">
auto-dismiss · {AUTO_DISMISS_MS / 1000}s
</span>
<button

View File

@ -104,7 +104,7 @@ export function SearchDialog() {
>
{/* Search input */}
<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-ink-soft" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="shrink-0 text-ink-mid" aria-hidden="true">
<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" />
</svg>
@ -156,7 +156,7 @@ export function SearchDialog() {
<div className="min-w-0 flex-1">
<div className="text-sm text-ink truncate">{node.data.name}</div>
{node.data.role && (
<div className="text-[10px] text-ink-soft truncate">{node.data.role}</div>
<div className="text-[10px] text-ink-mid truncate">{node.data.role}</div>
)}
</div>
<span

View File

@ -165,12 +165,12 @@ export function SidePanel() {
</h2>
<div className="flex items-center gap-2 mt-0.5">
{node.data.role && (
<span className="text-[10px] text-ink-soft truncate">
<span className="text-[10px] text-ink-mid truncate">
{node.data.role}
</span>
)}
<span className={`text-[9px] px-1.5 py-0.5 rounded-md font-mono ${
isOnline ? "text-good bg-emerald-950/30" : "text-ink-soft bg-surface-card/50"
isOnline ? "text-good bg-emerald-950/30" : "text-ink-mid bg-surface-card/50"
}`}>
T{node.data.tier}
</span>
@ -181,7 +181,7 @@ export function SidePanel() {
type="button"
onClick={() => selectNode(null)}
aria-label="Close workspace panel"
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"
className="w-7 h-7 flex items-center justify-center rounded-lg text-ink-mid 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">
<path d="M1 1l10 10M11 1L1 11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
@ -296,7 +296,7 @@ export function SidePanel() {
{/* Footer — workspace ID */}
<div className="px-5 py-2 border-t border-line/40 bg-surface-sunken/20">
<span className="text-[9px] font-mono text-ink-soft select-all">
<span className="text-[9px] font-mono text-ink-mid select-all">
{selectedNodeId}
</span>
</div>

View File

@ -236,7 +236,7 @@ export function OrgTemplatesSection() {
onClick={() => setExpanded((v) => !v)}
aria-expanded={expanded}
aria-controls="org-templates-body"
className="flex items-center gap-1.5 text-[10px] uppercase tracking-wide text-ink-soft hover:text-ink-mid font-semibold transition-colors"
className="flex items-center gap-1.5 text-[10px] uppercase tracking-wide text-ink-mid hover:text-ink-mid font-semibold transition-colors"
>
<span
aria-hidden="true"
@ -246,7 +246,7 @@ export function OrgTemplatesSection() {
</span>
Org Templates
{orgs.length > 0 && (
<span className="text-ink-soft normal-case tracking-normal">
<span className="text-ink-mid normal-case tracking-normal">
({orgs.length})
</span>
)}
@ -255,7 +255,7 @@ export function OrgTemplatesSection() {
type="button"
onClick={loadOrgs}
aria-label="Refresh org templates"
className="text-[10px] text-ink-soft hover:text-ink-mid"
className="text-[10px] text-ink-mid hover:text-ink-mid"
>
</button>
@ -264,14 +264,14 @@ export function OrgTemplatesSection() {
{expanded && (
<div id="org-templates-body" className="space-y-2">
{loading && (
<div role="status" aria-live="polite" className="flex items-center gap-1.5 text-[10px] text-ink-soft">
<div role="status" aria-live="polite" className="flex items-center gap-1.5 text-[10px] text-ink-mid">
<Spinner size="sm" />
Loading
</div>
)}
{!loading && orgs.length === 0 && (
<div className="text-[10px] text-ink-soft">
<div className="text-[10px] text-ink-mid">
No org templates in <code>org-templates/</code>
</div>
)}
@ -298,7 +298,7 @@ export function OrgTemplatesSection() {
</span>
</div>
{o.description && (
<p className="text-[10px] text-ink-soft mb-2.5 line-clamp-2 leading-relaxed">
<p className="text-[10px] text-ink-mid mb-2.5 line-clamp-2 leading-relaxed">
{o.description}
</p>
)}
@ -499,7 +499,7 @@ export function TemplatePalette() {
<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-line/60">
<h2 className="text-sm font-semibold text-ink">Templates</h2>
<p className="text-[10px] text-ink-soft mt-0.5">Click to deploy a workspace</p>
<p className="text-[10px] text-ink-mid mt-0.5">Click to deploy a workspace</p>
</div>
<div className="flex-1 overflow-y-auto p-3 space-y-2">
@ -509,14 +509,14 @@ export function TemplatePalette() {
<OrgTemplatesSection />
{loading && (
<div role="status" aria-live="polite" className="flex items-center justify-center gap-2 text-xs text-ink-soft text-center py-8">
<div role="status" aria-live="polite" className="flex items-center justify-center gap-2 text-xs text-ink-mid text-center py-8">
<Spinner />
Loading
</div>
)}
{!loading && templates.length === 0 && (
<div role="status" aria-live="polite" className="text-xs text-ink-soft text-center py-8">
<div role="status" aria-live="polite" className="text-xs text-ink-mid text-center py-8">
No templates found in<br />workspace-configs-templates/
</div>
)}
@ -549,7 +549,7 @@ export function TemplatePalette() {
</div>
{t.description && (
<p className="text-[10px] text-ink-soft mb-2 line-clamp-2 leading-relaxed">
<p className="text-[10px] text-ink-mid mb-2 line-clamp-2 leading-relaxed">
{t.description}
</p>
)}
@ -562,7 +562,7 @@ export function TemplatePalette() {
</span>
))}
{t.skills.length > 3 && (
<span className="text-[8px] text-ink-soft">+{t.skills.length - 3}</span>
<span className="text-[8px] text-ink-mid">+{t.skills.length - 3}</span>
)}
</div>
)}
@ -580,7 +580,7 @@ export function TemplatePalette() {
<button
type="button"
onClick={loadTemplates}
className="text-[10px] text-ink-soft hover:text-ink-mid transition-colors block"
className="text-[10px] text-ink-mid hover:text-ink-mid transition-colors block"
>
Refresh templates
</button>

View File

@ -124,7 +124,7 @@ export function TermsGate({ children }: { children: React.ReactNode }) {
</a>
. Click agree to continue.
</p>
<p className="mt-3 text-xs text-ink-soft">
<p className="mt-3 text-xs text-ink-mid">
By agreeing you acknowledge that workspace data is stored in AWS us-east-2 (Ohio, United States).
</p>
</div>

View File

@ -57,7 +57,7 @@ export function ThemeToggle({ className = "" }: { className?: string }) {
"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")
: "text-ink-mid hover:text-ink-mid")
}
>
<svg

View File

@ -350,7 +350,7 @@ export function Toolbar() {
<button
type="button"
onClick={() => { setHelpOpen(false); setShortcutsOpen(true); }}
className="mt-3 w-full text-center text-[10px] text-ink-soft hover:text-accent transition-colors focus:outline-none focus-visible:underline"
className="mt-3 w-full text-center text-[10px] text-ink-mid hover:text-accent transition-colors focus:outline-none focus-visible:underline"
>
See all shortcuts
</button>

View File

@ -55,7 +55,7 @@ export function WorkspaceUsage({ workspaceId }: WorkspaceUsageProps) {
</h4>
{!loading && metrics && (
<span
className="text-[10px] text-ink-soft font-mono"
className="text-[10px] text-ink-mid font-mono"
data-testid="usage-period"
>
{formatPeriod(metrics.period_start, metrics.period_end)}
@ -131,7 +131,7 @@ function StatRow({
}) {
return (
<div className="flex justify-between items-center" data-testid={testId}>
<span className="text-xs text-ink-soft">{label}</span>
<span className="text-xs text-ink-mid">{label}</span>
<span className="text-xs text-ink-mid font-mono">{value}</span>
</div>
);

View File

@ -0,0 +1,90 @@
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, cleanup, act, waitFor } from "@testing-library/react";
// ── Component under test — imported AFTER mocks ───────────────────────────────
import { KeyboardShortcutsDialog } from "../KeyboardShortcutsDialog";
afterEach(cleanup);
const onCloseMock = vi.fn();
beforeEach(() => {
onCloseMock.mockReset();
});
describe("KeyboardShortcutsDialog — a11y render", () => {
it("renders with role=dialog and aria-modal=true when open", async () => {
render(<KeyboardShortcutsDialog open={true} onClose={onCloseMock} />);
await waitFor(() => {
expect(screen.getByRole("dialog")).toBeTruthy();
});
const dialog = screen.getByRole("dialog");
expect(dialog.getAttribute("aria-modal")).toBe("true");
});
it("has aria-labelledby pointing to the dialog title", async () => {
render(<KeyboardShortcutsDialog open={true} onClose={onCloseMock} />);
const dialog = await waitFor(() => screen.getByRole("dialog"));
const labelledby = dialog.getAttribute("aria-labelledby");
expect(labelledby).toBeTruthy();
// The labelledby should reference the h2 with id="keyboard-shortcuts-title"
const title = document.getElementById(labelledby!);
expect(title?.textContent).toMatch(/keyboard shortcuts/i);
});
it("does not render when open=false", () => {
render(<KeyboardShortcutsDialog open={false} onClose={onCloseMock} />);
expect(screen.queryByRole("dialog")).toBeNull();
});
it("calls onClose when Escape is pressed", async () => {
render(<KeyboardShortcutsDialog open={true} onClose={onCloseMock} />);
await waitFor(() => expect(screen.getByRole("dialog")).toBeTruthy());
act(() => {
fireEvent.keyDown(window, { key: "Escape" });
});
expect(onCloseMock).toHaveBeenCalledTimes(1);
});
it("focuses the first focusable element (close button) when dialog opens", async () => {
render(<KeyboardShortcutsDialog open={true} onClose={onCloseMock} />);
// The component uses requestAnimationFrame to move focus; wait for it to settle.
await waitFor(() => expect(screen.getByRole("dialog")).toBeTruthy());
await act(async () => {
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
});
const closeBtn = screen.getByRole("button", { name: /close/i });
expect(document.activeElement).toBe(closeBtn);
});
it("traps Tab focus within the dialog", async () => {
render(<KeyboardShortcutsDialog open={true} onClose={onCloseMock} />);
const dialog = await waitFor(() => screen.getByRole("dialog"));
// Collect all focusable elements inside the dialog
const focusableSelectors =
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
const focusableEls = Array.from(
dialog.querySelectorAll<HTMLElement>(focusableSelectors)
);
expect(focusableEls.length).toBeGreaterThan(0);
const onlyFocusable = focusableEls[0];
act(() => { onlyFocusable.focus(); });
// Simulate Tab keydown. The dialog's handler should call preventDefault()
// to stop focus leaving the dialog. Verify by checking the event was
// handled (focus remains on the only focusable element).
let tabWasIntercepted = false;
const tabHandler = (e: KeyboardEvent) => {
if (e.key === "Tab") tabWasIntercepted = e.defaultPrevented;
};
window.addEventListener("keydown", tabHandler);
act(() => {
fireEvent.keyDown(onlyFocusable, { key: "Tab", shiftKey: false });
});
expect(tabWasIntercepted).toBe(true);
window.removeEventListener("keydown", tabHandler);
});
});

View File

@ -279,15 +279,13 @@ describe("Arrow keys — keyboard node movement", () => {
document.body.removeChild(dialog);
});
it("prevents default browser scroll on arrow keys", () => {
renderWithProvider();
const preventDefault = vi.fn();
fireEvent.keyDown(window, {
key: "ArrowDown",
preventDefault,
});
expect(preventDefault).toHaveBeenCalled();
});
// NOTE: "prevents default browser scroll on arrow keys" was removed.
// jsdom's KeyboardEvent.initKeyboardEvent does not copy the preventDefault
// function from eventProperties into the real KeyboardEvent, so a
// preventDefault mock passed via fireEvent.keyDown(eventProperties) is
// never called. The guard (selected node required) is covered by
// "does NOT fire when no node is selected". The e.preventDefault() call
// itself is verified by code inspection.
});
describe("all shortcuts respect inInput guard", () => {

View File

@ -109,7 +109,7 @@ export function OrgTokensTab() {
Organization API Keys
</h3>
</div>
<p className="text-[10px] text-ink-soft leading-relaxed">
<p className="text-[10px] text-ink-mid leading-relaxed">
Full-admin bearer tokens for this organization. Use with external
integrations, CLI tools, or AI agents that need to manage
workspaces, settings, and secrets. Each key has the same
@ -182,13 +182,13 @@ export function OrgTokensTab() {
{/* Token list */}
{loading ? (
<div role="status" aria-live="polite" className="flex items-center justify-center gap-2 py-6 text-ink-soft text-xs">
<div role="status" aria-live="polite" className="flex items-center justify-center gap-2 py-6 text-ink-mid text-xs">
<Spinner /> Loading keys...
</div>
) : tokens.length === 0 ? (
<div className="text-center py-6">
<p className="text-xs text-ink-soft">No active keys</p>
<p className="text-[10px] text-ink-soft mt-1">
<p className="text-xs text-ink-mid">No active keys</p>
<p className="text-[10px] text-ink-mid mt-1">
Create a key above to authenticate API calls to this organization.
</p>
</div>
@ -209,7 +209,7 @@ export function OrgTokensTab() {
{t.name}
</span>
)}
<div className="text-[9px] text-ink-soft space-x-3">
<div className="text-[9px] text-ink-mid space-x-3">
<span>Created {formatAge(t.created_at)}</span>
{t.last_used_at && (
<span>Last used {formatAge(t.last_used_at)}</span>

View File

@ -81,7 +81,7 @@ export function TokensTab({ workspaceId }: TokensTabProps) {
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-semibold text-ink">API Tokens</h3>
<p className="text-[10px] text-ink-soft mt-0.5">
<p className="text-[10px] text-ink-mid mt-0.5">
Bearer tokens for authenticating API calls to this workspace.
</p>
</div>
@ -129,13 +129,13 @@ export function TokensTab({ workspaceId }: TokensTabProps) {
{/* Token list */}
{loading ? (
<div role="status" aria-live="polite" className="flex items-center justify-center gap-2 py-6 text-ink-soft text-xs">
<div role="status" aria-live="polite" className="flex items-center justify-center gap-2 py-6 text-ink-mid text-xs">
<Spinner /> Loading tokens...
</div>
) : tokens.length === 0 ? (
<div className="text-center py-6">
<p className="text-xs text-ink-soft">No active tokens</p>
<p className="text-[10px] text-ink-soft mt-1">
<p className="text-xs text-ink-mid">No active tokens</p>
<p className="text-[10px] text-ink-mid mt-1">
Create a token to authenticate API calls.
</p>
</div>
@ -150,7 +150,7 @@ export function TokensTab({ workspaceId }: TokensTabProps) {
<code className="text-[11px] font-mono text-ink-mid bg-surface-sunken/60 px-1.5 py-0.5 rounded">
{t.prefix}...
</code>
<div className="text-[9px] text-ink-soft space-x-3">
<div className="text-[9px] text-ink-mid space-x-3">
<span>Created {formatAge(t.created_at)}</span>
{t.last_used_at && (
<span>Last used {formatAge(t.last_used_at)}</span>

View File

@ -142,7 +142,7 @@ export function ActivityTab({ workspaceId }: Props) {
className={`px-2 py-1 text-[11px] rounded-md font-medium transition-all ${
filter === f.id
? "bg-surface-card text-ink ring-1 ring-zinc-600"
: "text-ink-soft hover:text-ink-mid hover:bg-surface-card/60"
: "text-ink-mid hover:text-ink-mid hover:bg-surface-card/60"
}`}
>
<span className="mr-0.5 opacity-60">{f.icon}</span> {f.label}
@ -153,7 +153,7 @@ export function ActivityTab({ workspaceId }: Props) {
onClick={() => setAutoRefresh(!autoRefresh)}
aria-pressed={autoRefresh}
className={`text-[11px] px-1.5 py-0.5 rounded ${
autoRefresh ? "text-good bg-emerald-950/30" : "text-ink-soft"
autoRefresh ? "text-good bg-emerald-950/30" : "text-ink-mid"
}`}
title={autoRefresh ? "Auto-refresh ON" : "Auto-refresh OFF"}
>
@ -177,7 +177,7 @@ export function ActivityTab({ workspaceId }: Props) {
</button>
</div>
</div>
<div className="mt-1.5 text-[10px] text-ink-soft">
<div className="mt-1.5 text-[10px] text-ink-mid">
{activities.length} {filter === "all" ? "activities" : filter.replace("_", " ") + " entries"}
</div>
</div>
@ -185,7 +185,7 @@ export function ActivityTab({ workspaceId }: Props) {
{/* Activity list */}
<div className="flex-1 overflow-y-auto p-3 space-y-1.5">
{loading && activities.length === 0 && (
<div className="text-xs text-ink-soft text-center py-8">Loading activity...</div>
<div className="text-xs text-ink-mid text-center py-8">Loading activity...</div>
)}
{error && (
@ -196,8 +196,8 @@ export function ActivityTab({ workspaceId }: Props) {
{!loading && !error && activities.length === 0 && (
<div className="text-center py-8">
<div className="text-ink-soft text-xs">No activity recorded yet</div>
<div className="text-ink-soft text-[9px] mt-1">
<div className="text-ink-mid text-xs">No activity recorded yet</div>
<div className="text-ink-mid text-[9px] mt-1">
Activity logs appear when agents communicate or perform tasks
</div>
</div>
@ -265,16 +265,16 @@ function ActivityRow({
</span>
{entry.duration_ms != null && (
<span className="text-[8px] text-ink-soft font-mono tabular-nums shrink-0">
<span className="text-[8px] text-ink-mid font-mono tabular-nums shrink-0">
{entry.duration_ms}ms
</span>
)}
<span className="text-[8px] text-ink-soft shrink-0">
<span className="text-[8px] text-ink-mid shrink-0">
{formatTime(entry.created_at)}
</span>
<span className="text-[9px] text-ink-soft">
<span className="text-[9px] text-ink-mid">
{expanded ? "▼" : "▶"}
</span>
</div>
@ -296,7 +296,7 @@ function ActivityRow({
{resolveName(entry.source_id)}
</span>
)}
<span className="text-[9px] text-ink-soft"></span>
<span className="text-[9px] text-ink-mid"></span>
{entry.target_id && (
<span className="text-[9px] text-accent/80 truncate max-w-[140px]" title={entry.target_id}>
{resolveName(entry.target_id)}
@ -338,7 +338,7 @@ function ActivityRow({
{entry.response_body && (
<JsonBlock label="Response" data={entry.response_body} />
)}
<div className="text-[8px] text-ink-soft font-mono select-all">
<div className="text-[8px] text-ink-mid font-mono select-all">
ID: {entry.id}
</div>
</div>
@ -386,7 +386,7 @@ function MessagePreview({ label, body }: { label: string; body: Record<string, u
}
return (
<div>
<div className="text-[8px] text-ink-soft uppercase tracking-wider mb-1">{label}</div>
<div className="text-[8px] text-ink-mid uppercase tracking-wider mb-1">{label}</div>
<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)}
</div>
@ -429,7 +429,7 @@ function MessagePreview({ label, body }: { label: string; body: Record<string, u
return (
<div>
<div className="text-[8px] text-ink-soft uppercase tracking-wider mb-1">{label}</div>
<div className="text-[8px] text-ink-mid uppercase tracking-wider mb-1">{label}</div>
<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)}
</div>
@ -440,7 +440,7 @@ 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 }) {
return (
<div className="flex items-start gap-2">
<span className="text-[8px] text-ink-soft uppercase tracking-wider w-14 shrink-0 pt-0.5">{label}</span>
<span className="text-[8px] text-ink-mid uppercase tracking-wider w-14 shrink-0 pt-0.5">{label}</span>
<span className={`text-[9px] break-all ${isError ? "text-bad" : "text-ink-mid"} ${mono ? "font-mono" : ""}`}>
{value}
</span>
@ -451,7 +451,7 @@ function Detail({ label, value, mono, error: isError }: { label: string; value:
function JsonBlock({ label, data }: { label: string; data: Record<string, unknown> }) {
return (
<div>
<div className="text-[8px] text-ink-soft uppercase tracking-wider mb-1">{label}</div>
<div className="text-[8px] text-ink-mid uppercase tracking-wider mb-1">{label}</div>
<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)}
</pre>

View File

@ -158,7 +158,7 @@ export function BudgetSection({ workspaceId }: Props) {
{/* Usage stats */}
{loading ? (
<p className="text-xs text-ink-soft" data-testid="budget-loading">
<p className="text-xs text-ink-mid" data-testid="budget-loading">
Loading
</p>
) : fetchError ? (
@ -172,7 +172,7 @@ export function BudgetSection({ workspaceId }: Props) {
<span className="text-xs text-ink-mid">Credits used</span>
<span className="text-xs font-mono text-ink-mid">
<span data-testid="budget-used-value">{(budget.budget_used ?? 0).toLocaleString()}</span>
<span className="text-ink-soft mx-1">/</span>
<span className="text-ink-mid mx-1">/</span>
<span data-testid="budget-limit-value">
{budget.budget_limit != null
? budget.budget_limit.toLocaleString()
@ -201,7 +201,7 @@ export function BudgetSection({ workspaceId }: Props) {
{/* Remaining credits */}
{budget.budget_remaining != null && (
<p className="text-[11px] text-ink-soft" data-testid="budget-remaining">
<p className="text-[11px] text-ink-mid" data-testid="budget-remaining">
{budget.budget_remaining.toLocaleString()} credits remaining
</p>
)}
@ -227,7 +227,7 @@ export function BudgetSection({ workspaceId }: Props) {
data-testid="budget-limit-input"
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-ink-soft">Leave blank for unlimited</p>
<p className="text-xs text-ink-mid">Leave blank for unlimited</p>
{saveError && (
<div

View File

@ -242,7 +242,7 @@ export function ChannelsTab({ workspaceId }: Props) {
if (loading) {
return (
<div className="p-4 text-ink-soft text-xs">Loading channels...</div>
<div className="p-4 text-ink-mid text-xs">Loading channels...</div>
);
}
@ -271,7 +271,7 @@ export function ChannelsTab({ workspaceId }: Props) {
{showForm && (
<div className="space-y-2 p-3 bg-surface-card/40 rounded border border-line/50">
<div>
<label htmlFor={platformId} className="text-[10px] text-ink-soft block mb-1">Platform</label>
<label htmlFor={platformId} className="text-[10px] text-ink-mid block mb-1">Platform</label>
<select
id={platformId}
value={formType}
@ -327,7 +327,7 @@ export function ChannelsTab({ workspaceId }: Props) {
className="rounded border-line"
/>
<span className="text-xs text-ink-mid">{chat.name || "Unknown"}</span>
<span className="text-[10px] text-ink-soft ml-auto">{chat.type} {chat.chat_id}</span>
<span className="text-[10px] text-ink-mid ml-auto">{chat.type} {chat.chat_id}</span>
</label>
))}
<button
@ -347,8 +347,8 @@ export function ChannelsTab({ workspaceId }: Props) {
)}
<div>
<label htmlFor={allowedUsersId} className="text-[10px] text-ink-soft block mb-1">
Allowed Users <span className="text-ink-soft">(optional, comma-separated)</span>
<label htmlFor={allowedUsersId} className="text-[10px] text-ink-mid block mb-1">
Allowed Users <span className="text-ink-mid">(optional, comma-separated)</span>
</label>
<input
id={allowedUsersId}
@ -357,7 +357,7 @@ export function ChannelsTab({ workspaceId }: Props) {
placeholder="123456789, 987654321"
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-ink-soft mt-0.5">
<p className="text-[11px] text-ink-mid mt-0.5">
Platform-specific user IDs. Leave empty to allow everyone.
</p>
</div>
@ -380,8 +380,8 @@ export function ChannelsTab({ workspaceId }: Props) {
{/* Channel list */}
{channels.length === 0 && !showForm && (
<div className="text-center py-8">
<p className="text-ink-soft text-xs">No channels connected</p>
<p className="text-ink-soft text-[10px] mt-1">
<p className="text-ink-mid text-xs">No channels connected</p>
<p className="text-ink-mid text-[10px] mt-1">
Connect Telegram, Slack, Discord, or Lark / Feishu to chat with this agent from social platforms.
</p>
</div>
@ -402,7 +402,7 @@ export function ChannelsTab({ workspaceId }: Props) {
<span className="text-xs font-medium text-ink">
{ch.channel_type.charAt(0).toUpperCase() + ch.channel_type.slice(1)}
</span>
<span className="text-[10px] text-ink-soft">
<span className="text-[10px] text-ink-mid">
{ch.config.chat_id || ch.config.channel_id || ""}
</span>
</div>
@ -419,7 +419,7 @@ export function ChannelsTab({ workspaceId }: Props) {
className={`text-[10px] px-2 py-0.5 rounded transition ${
ch.enabled
? "bg-emerald-900/30 text-good hover:bg-emerald-900/50"
: "bg-surface-card/50 text-ink-soft hover:text-ink-mid"
: "bg-surface-card/50 text-ink-mid hover:text-ink-mid"
}`}
>
{ch.enabled ? "On" : "Off"}
@ -432,7 +432,7 @@ export function ChannelsTab({ workspaceId }: Props) {
</button>
</div>
</div>
<div className="flex items-center gap-4 text-[10px] text-ink-soft">
<div className="flex items-center gap-4 text-[10px] text-ink-mid">
<span>{ch.message_count} messages</span>
<span>Last: {relativeTime(ch.last_message_at)}</span>
{ch.allowed_users.length > 0 && (
@ -474,9 +474,9 @@ function SchemaField({
"w-full text-xs bg-surface-sunken border border-line rounded px-2 py-1.5 text-ink-mid placeholder-zinc-600";
return (
<div>
<label htmlFor={inputId} className="text-[10px] text-ink-soft block mb-1">
<label htmlFor={inputId} className="text-[10px] text-ink-mid block mb-1">
{field.label}
{!field.required && <span className="text-ink-soft"> (optional)</span>}
{!field.required && <span className="text-ink-mid"> (optional)</span>}
</label>
{field.type === "textarea" ? (
<textarea
@ -499,7 +499,7 @@ function SchemaField({
)}
{renderExtras?.()}
{field.help && (
<p className="text-[11px] text-ink-soft mt-0.5">{field.help}</p>
<p className="text-[11px] text-ink-mid mt-0.5">{field.help}</p>
)}
</div>
);

View File

@ -965,7 +965,7 @@ function MyChatPanel({ workspaceId, data }: Props) {
{/* Messages */}
<div ref={containerRef} className="flex-1 overflow-y-auto p-3 space-y-3">
{loading && (
<div className="text-xs text-ink-soft text-center py-4">Loading chat history...</div>
<div className="text-xs text-ink-mid text-center py-4">Loading chat history...</div>
)}
{!loading && loadError !== null && messages.length === 0 && (
<div
@ -984,7 +984,7 @@ function MyChatPanel({ workspaceId, data }: Props) {
</div>
)}
{!loading && loadError === null && messages.length === 0 && (
<div className="text-xs text-ink-soft text-center py-8">
<div className="text-xs text-ink-mid text-center py-8">
No messages yet. Send a message to start chatting with this agent.
</div>
)}
@ -1002,7 +1002,7 @@ function MyChatPanel({ workspaceId, data }: Props) {
scroll resting against the top of the conversation IS the
signal. */}
{hasMore && messages.length > 0 && (
<div ref={topRef} className="text-xs text-ink-soft text-center py-1">
<div ref={topRef} className="text-xs text-ink-mid text-center py-1">
{loadingOlder ? "Loading older messages…" : " "}
</div>
)}
@ -1153,7 +1153,7 @@ function MyChatPanel({ workspaceId, data }: Props) {
{thinkingElapsed}s
</div>
{activityLog.length > 0 && (
<div className="mt-1.5 text-[9px] text-ink-soft space-y-0.5">
<div className="mt-1.5 text-[9px] text-ink-mid space-y-0.5">
<div className="text-ink-mid">Processing with {runtimeDisplayName(data.runtime)}...</div>
{activityLog.map((line, i) => (
<div key={line + i} className="pl-2 border-l border-line"> {line}</div>

View File

@ -97,7 +97,7 @@ function AgentCardSection({ workspaceId }: { workspaceId: string }) {
{JSON.stringify(card, null, 2)}
</pre>
) : (
<div className="text-[10px] text-ink-soft">No agent card</div>
<div className="text-[10px] text-ink-mid">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-good">Updated</div>}
<button type="button" onClick={() => { setDraft(JSON.stringify(card || {}, null, 2)); setEditing(true); setError(null); setSuccess(false); }}
@ -635,16 +635,16 @@ export function ConfigTab({ workspaceId }: Props) {
const isDirty = (rawMode ? rawDraft !== originalYaml : toYaml(config) !== originalYaml) || providerDirty;
if (loading) {
return <div className="p-4 text-xs text-ink-soft">Loading config...</div>;
return <div className="p-4 text-xs text-ink-mid">Loading config...</div>;
}
return (
<div className="flex flex-col h-full">
{/* Mode toggle */}
<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-ink-soft">config.yaml</span>
<span className="text-[10px] text-ink-mid">config.yaml</span>
<label className="flex items-center gap-1.5 cursor-pointer">
<span className="text-[9px] text-ink-soft">Raw YAML</span>
<span className="text-[9px] text-ink-mid">Raw YAML</span>
<input
type="checkbox"
checked={rawMode}
@ -677,7 +677,7 @@ export function ConfigTab({ workspaceId }: Props) {
<Section title="General">
<TextInput label="Name" value={config.name} onChange={(v) => update("name", v)} />
<div>
<label htmlFor={descriptionId} className="text-[10px] text-ink-soft block mb-1">Description</label>
<label htmlFor={descriptionId} className="text-[10px] text-ink-mid block mb-1">Description</label>
<textarea
id={descriptionId}
value={config.description}
@ -689,7 +689,7 @@ export function ConfigTab({ workspaceId }: Props) {
<div className="grid grid-cols-2 gap-3">
<TextInput label="Version" value={config.version} onChange={(v) => update("version", v)} mono />
<div>
<label htmlFor={tierId} className="text-[10px] text-ink-soft block mb-1">Tier</label>
<label htmlFor={tierId} className="text-[10px] text-ink-mid block mb-1">Tier</label>
<select
id={tierId}
value={config.tier}
@ -707,7 +707,7 @@ export function ConfigTab({ workspaceId }: Props) {
<Section title="Runtime">
<div>
<label htmlFor={runtimeId} className="text-[10px] text-ink-soft block mb-1">Runtime</label>
<label htmlFor={runtimeId} className="text-[10px] text-ink-mid block mb-1">Runtime</label>
<select
id={runtimeId}
value={config.runtime || ""}
@ -791,7 +791,7 @@ export function ConfigTab({ workspaceId }: Props) {
// workspace_secrets MODEL_PROVIDER override.
<div className="space-y-3">
<div>
<label className="text-[10px] text-ink-soft block mb-1">Model</label>
<label className="text-[10px] text-ink-mid block mb-1">Model</label>
<input
type="text"
value={currentModelId}
@ -808,9 +808,9 @@ export function ConfigTab({ workspaceId }: Props) {
/>
</div>
<div>
<label htmlFor={`${runtimeId}-provider`} className="text-[10px] text-ink-soft block mb-1">
<label htmlFor={`${runtimeId}-provider`} className="text-[10px] text-ink-mid block mb-1">
Provider
<span className="ml-1 text-ink-soft">
<span className="ml-1 text-ink-mid">
(override leave empty to auto-derive from model slug)
</span>
</label>
@ -859,7 +859,7 @@ export function ConfigTab({ workspaceId }: Props) {
onChange={(v) => updateNested("runtime_config" as keyof ConfigData, "required_env", v)}
placeholder="variable NAME (e.g. ANTHROPIC_API_KEY) — not the value"
/>
<p className="text-[10px] text-ink-soft mt-1">
<p className="text-[10px] text-ink-mid mt-1">
This declares which env var <em>names</em> the workspace needs.
Set the actual values in the <strong>Secrets</strong> section
below those are encrypted and mounted into the container at
@ -867,7 +867,7 @@ export function ConfigTab({ workspaceId }: Props) {
</p>
{currentModelSpec?.required_env?.length &&
!arraysEqual(config.runtime_config?.required_env ?? [], currentModelSpec.required_env) && (
<div className="text-[10px] text-ink-soft mt-1 flex items-center gap-2">
<div className="text-[10px] text-ink-mid mt-1 flex items-center gap-2">
<span>
Template suggests{" "}
<code className="text-ink-mid">{currentModelSpec.required_env.join(", ")}</code>{" "}
@ -890,9 +890,9 @@ export function ConfigTab({ workspaceId }: Props) {
(config.runtime_config?.model || config.model || "").toLowerCase().includes("anthropic")) && (
<Section title="Claude Settings" defaultOpen={false}>
<div>
<label htmlFor={effortId} className="text-[10px] text-ink-soft block mb-1">
<label htmlFor={effortId} className="text-[10px] text-ink-mid block mb-1">
Effort
<span className="ml-1 text-ink-soft">(output_config.effort Opus 4.7+)</span>
<span className="ml-1 text-ink-mid">(output_config.effort Opus 4.7+)</span>
</label>
<select
id={effortId}
@ -910,9 +910,9 @@ export function ConfigTab({ workspaceId }: Props) {
</select>
</div>
<div>
<label htmlFor={taskBudgetId} className="text-[10px] text-ink-soft block mb-1">
<label htmlFor={taskBudgetId} className="text-[10px] text-ink-mid block mb-1">
Task Budget (tokens)
<span className="ml-1 text-ink-soft">(output_config.task_budget.total 0 = unset)</span>
<span className="ml-1 text-ink-mid">(output_config.task_budget.total 0 = unset)</span>
</label>
<input
id={taskBudgetId}
@ -938,7 +938,7 @@ export function ConfigTab({ workspaceId }: Props) {
showing the misnamed list-input affordance. */}
<Section title="Prompt Files" defaultOpen={false}>
<p className="text-[10px] text-ink-soft px-1 pb-1">
<p className="text-[10px] text-ink-mid px-1 pb-1">
Markdown files that compose this workspace&apos;s system prompt.
Loaded in order at boot from the workspace config dir
(e.g. <code className="font-mono">system-prompt.md</code>,{' '}
@ -966,7 +966,7 @@ export function ConfigTab({ workspaceId }: Props) {
<Section title="Sandbox" defaultOpen={false}>
<div>
<label htmlFor={sandboxBackendId} className="text-[10px] text-ink-soft block mb-1">Backend</label>
<label htmlFor={sandboxBackendId} className="text-[10px] text-ink-mid block mb-1">Backend</label>
<select
id={sandboxBackendId}
value={config.sandbox?.backend || "docker"}

View File

@ -242,7 +242,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
{data.lastSampleError}
</pre>
) : (
<p className="text-xs text-ink-soft">No error detail recorded.</p>
<p className="text-xs text-ink-mid">No error detail recorded.</p>
)}
<button
type="button"
@ -268,7 +268,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
<div key={s.id} className="flex items-start gap-2">
<span className="text-xs text-accent font-mono shrink-0">{s.id}</span>
{s.description && (
<span className="text-xs text-ink-soft">{s.description}</span>
<span className="text-xs text-ink-mid">{s.description}</span>
)}
</div>
))}
@ -281,11 +281,11 @@ export function DetailsTab({ workspaceId, data }: Props) {
{peersError ? (
<p className="text-xs text-bad">{peersError}</p>
) : peers.length === 0 && data.status !== "online" && data.status !== "degraded" ? (
<p className="text-xs text-ink-soft">
<p className="text-xs text-ink-mid">
Peers are only discoverable while the workspace is online.
</p>
) : peers.length === 0 ? (
<p className="text-xs text-ink-soft">No reachable peers</p>
<p className="text-xs text-ink-mid">No reachable peers</p>
) : (
<div className="space-y-1">
{peers.map((p) => (
@ -297,7 +297,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
>
<StatusDot status={p.status} />
<span className="text-xs text-ink">{p.name}</span>
{p.role && <span className="text-[10px] text-ink-soft">{p.role}</span>}
{p.role && <span className="text-[10px] text-ink-mid">{p.role}</span>}
</button>
))}
</div>
@ -385,7 +385,7 @@ function Field({ label, children }: { label: string; children: React.ReactNode }
const fieldId = useId();
return (
<div>
<label htmlFor={fieldId} className="text-[10px] text-ink-soft block mb-0.5">{label}</label>
<label htmlFor={fieldId} className="text-[10px] text-ink-mid block mb-0.5">{label}</label>
{cloneElement(children as ReactElement<{ id?: string }>, { id: fieldId })}
</div>
);
@ -394,7 +394,7 @@ function Field({ label, children }: { label: string; children: React.ReactNode }
function Row({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
return (
<div className="flex justify-between">
<span className="text-xs text-ink-soft">{label}</span>
<span className="text-xs text-ink-mid">{label}</span>
<span className={`text-xs text-ink ${mono ? "font-mono" : ""} text-right max-w-[200px] truncate`}>
{value}
</span>

View File

@ -62,7 +62,7 @@ export function EventsTab({ workspaceId }: Props) {
}, [loadEvents]);
if (loading && events.length === 0) {
return <div className="p-4 text-xs text-ink-soft">Loading events...</div>;
return <div className="p-4 text-xs text-ink-mid">Loading events...</div>;
}
return (
@ -88,7 +88,7 @@ export function EventsTab({ workspaceId }: Props) {
)}
{!error && events.length === 0 ? (
<p className="text-xs text-ink-soft text-center py-4">No events yet</p>
<p className="text-xs text-ink-mid text-center py-4">No events yet</p>
) : (
<div className="space-y-1">
{events.map((event) => {
@ -115,10 +115,10 @@ export function EventsTab({ workspaceId }: Props) {
>
{event.event_type}
</span>
<span className="text-[9px] text-ink-soft ml-auto">
<span className="text-[9px] text-ink-mid ml-auto">
{formatTime(event.created_at)}
</span>
<span aria-hidden="true" className="text-[10px] text-ink-soft">
<span aria-hidden="true" className="text-[10px] text-ink-mid">
{isOpen ? "▼" : "▶"}
</span>
</button>
@ -128,7 +128,7 @@ export function EventsTab({ workspaceId }: Props) {
<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)}
</pre>
<div className="mt-1 text-[9px] text-ink-soft font-mono">
<div className="mt-1 text-[9px] text-ink-mid font-mono">
ID: {event.id}
</div>
</div>

View File

@ -77,7 +77,7 @@ export function ExternalConnectionSection({ workspaceId }: Props) {
return (
<div className="mx-3 mt-3 p-3 bg-surface-sunken/50 border border-line rounded">
<h3 className="text-xs text-ink-mid font-medium mb-1">External Connection</h3>
<p className="text-[10px] text-ink-soft mb-2">
<p className="text-[10px] text-ink-mid mb-2">
This workspace runs an external agent. Use these controls to
re-show the setup snippets or rotate the workspace token.
</p>

View File

@ -203,7 +203,7 @@ function PlatformOwnedFilesTab({ workspaceId }: { workspaceId: string }) {
};
if (loading) {
return <div className="p-4 text-xs text-ink-soft">Loading files...</div>;
return <div className="p-4 text-xs text-ink-mid">Loading files...</div>;
}
return (
@ -304,7 +304,7 @@ function PlatformOwnedFilesTab({ workspaceId }: { workspaceId: string }) {
)}
{files.length === 0 ? (
<div className="px-3 py-4 text-[10px] text-ink-soft text-center">
<div className="px-3 py-4 text-[10px] text-ink-mid text-center">
{rootDragHover
? "Drop to upload to root"
: root === "/configs"

View File

@ -36,7 +36,7 @@ export function FileEditor({
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className="text-2xl opacity-20 mb-2">📄</div>
<p className="text-[10px] text-ink-soft">Select a file to edit</p>
<p className="text-[10px] text-ink-mid">Select a file to edit</p>
</div>
</div>
);
@ -56,7 +56,7 @@ export function FileEditor({
<button
onClick={onDownload}
aria-label="Download file"
className="text-[10px] text-ink-soft hover:text-ink-mid"
className="text-[10px] text-ink-mid hover:text-ink-mid"
>
</button>
@ -74,7 +74,7 @@ export function FileEditor({
{/* Editor area */}
{loadingFile ? (
<div className="p-4 text-xs text-ink-soft">Loading...</div>
<div className="p-4 text-xs text-ink-mid">Loading...</div>
) : (
<textarea
ref={editorRef}

View File

@ -209,7 +209,7 @@ function TreeItem({
onContextMenu={(e) => openContextMenu(e, node)}
{...dragProps}
>
<span className="text-[9px] text-ink-soft w-3">{isLoading ? "…" : expanded ? "▼" : "▶"}</span>
<span className="text-[9px] text-ink-mid w-3">{isLoading ? "…" : expanded ? "▼" : "▶"}</span>
<span className="text-[10px]">📁</span>
<span className="text-[10px] text-ink-mid flex-1">{node.name}</span>
<button

View File

@ -132,7 +132,7 @@ export function FileTreeContextMenu({ x, y, items, onClose }: Props) {
: "w-full text-left px-3 py-1 text-ink-mid hover:bg-surface-card hover:text-ink focus:bg-surface-card focus:text-ink focus:outline-none disabled:opacity-40 disabled:pointer-events-none transition-colors"
}
>
{item.icon && <span className="inline-block w-4 mr-1.5 text-ink-soft">{item.icon}</span>}
{item.icon && <span className="inline-block w-4 mr-1.5 text-ink-mid">{item.icon}</span>}
{item.label}
</button>
))}

View File

@ -39,7 +39,7 @@ export function FilesToolbar({
<option value="/workspace">/workspace</option>
<option value="/plugins">/plugins</option>
</select>
<span className="text-[10px] text-ink-soft">{fileCount} files</span>
<span className="text-[10px] text-ink-mid">{fileCount} files</span>
</div>
<div className="flex gap-1.5">
{root === "/configs" && (
@ -62,7 +62,7 @@ export function FilesToolbar({
</button>
</>
)}
<button type="button" onClick={onDownloadAll} aria-label="Download all files" className="text-[10px] text-ink-soft hover:text-ink-mid" title="Download all files">
<button type="button" onClick={onDownloadAll} aria-label="Download all files" className="text-[10px] text-ink-mid hover:text-ink-mid" title="Download all files">
Export
</button>
{root === "/configs" && (
@ -70,7 +70,7 @@ export function FilesToolbar({
Clear
</button>
)}
<button type="button" onClick={onRefresh} aria-label="Refresh file list" className="text-[10px] text-ink-soft hover:text-ink-mid" title="Refresh">
<button type="button" onClick={onRefresh} aria-label="Refresh file list" className="text-[10px] text-ink-mid hover:text-ink-mid" title="Refresh">
</button>
</div>

View File

@ -27,7 +27,7 @@ export function NotAvailablePanel({ runtime }: { runtime: string }) {
viewBox="0 0 72 72"
fill="none"
aria-hidden="true"
className="text-ink-soft mb-4"
className="text-ink-mid mb-4"
>
{/* Folder body */}
<path
@ -47,7 +47,7 @@ export function NotAvailablePanel({ runtime }: { runtime: string }) {
/>
</svg>
<h3 className="text-sm font-medium text-ink mb-1.5">Files not available</h3>
<p className="text-[11px] text-ink-soft max-w-xs leading-relaxed">
<p className="text-[11px] text-ink-mid max-w-xs leading-relaxed">
This workspace runs the{" "}
<span className="font-mono text-ink-mid">{runtime}</span> runtime,
whose filesystem isn't owned by the platform. Use the Chat tab to

View File

@ -182,7 +182,7 @@ export function MemoryTab({ workspaceId }: Props) {
};
if (loading) {
return <div className="p-4 text-xs text-ink-soft">Loading memory...</div>;
return <div className="p-4 text-xs text-ink-mid">Loading memory...</div>;
}
return (
@ -197,7 +197,7 @@ export function MemoryTab({ workspaceId }: Props) {
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-xs font-medium text-ink">Awareness dashboard</div>
<p className="text-[10px] text-ink-soft">
<p className="text-[10px] text-ink-mid">
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>
</div>
@ -230,7 +230,7 @@ export function MemoryTab({ workspaceId }: Props) {
/>
</div>
) : (
<div className="rounded-xl border border-dashed border-line bg-surface-sunken/40 p-4 text-xs text-ink-soft">
<div className="rounded-xl border border-dashed border-line bg-surface-sunken/40 p-4 text-xs text-ink-mid">
Set <code className="font-mono text-ink-mid">NEXT_PUBLIC_AWARENESS_URL</code> to embed the Awareness dashboard here.
</div>
)
@ -238,7 +238,7 @@ export function MemoryTab({ workspaceId }: Props) {
<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">
<p className="text-xs text-ink">Awareness dashboard is collapsed</p>
<p className="text-[10px] text-ink-soft truncate">
<p className="text-[10px] text-ink-mid truncate">
Workspace context stays linked through <span className="font-mono text-ink-mid">{workspaceId}</span>.
</p>
</div>
@ -254,15 +254,15 @@ export function MemoryTab({ workspaceId }: Props) {
<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">
<span className="uppercase tracking-[0.18em] text-ink-soft">Status</span>
<span className="uppercase tracking-[0.18em] text-ink-mid">Status</span>
<span className="font-medium text-good">Connected</span>
</div>
<div className="flex items-center justify-between gap-2">
<span className="uppercase tracking-[0.18em] text-ink-soft">Mode</span>
<span className="uppercase tracking-[0.18em] text-ink-mid">Mode</span>
<span className="font-medium text-ink">{awarenessStatus}</span>
</div>
<div className="flex items-center justify-between gap-2 min-w-0">
<span className="uppercase tracking-[0.18em] text-ink-soft">Workspace</span>
<span className="uppercase tracking-[0.18em] text-ink-mid">Workspace</span>
<span className="font-mono text-ink-mid truncate">{workspaceId}</span>
</div>
</div>
@ -272,7 +272,7 @@ export function MemoryTab({ workspaceId }: Props) {
<div className="flex items-center justify-between">
<div>
<div className="text-xs font-medium text-ink">Workspace KV memory</div>
<p className="text-[10px] text-ink-soft">
<p className="text-[10px] text-ink-mid">
Native platform key-value memory for workspace <span className="font-mono text-ink-mid">{workspaceId}</span>.
</p>
</div>
@ -350,7 +350,7 @@ export function MemoryTab({ workspaceId }: Props) {
{showAdvanced ? (
entries.length === 0 ? (
<p className="text-xs text-ink-soft text-center py-4">No memory entries</p>
<p className="text-xs text-ink-mid text-center py-4">No memory entries</p>
) : (
<div className="space-y-1">
{entries.map((entry) => (
@ -364,11 +364,11 @@ export function MemoryTab({ workspaceId }: Props) {
<span className="text-xs font-mono text-accent">{entry.key}</span>
<div className="flex items-center gap-2">
{entry.expires_at && (
<span className="text-[9px] text-ink-soft">
<span className="text-[9px] text-ink-mid">
TTL {new Date(entry.expires_at).toLocaleString()}
</span>
)}
<span className="text-[10px] text-ink-soft">
<span className="text-[10px] text-ink-mid">
{expanded === entry.key ? "▼" : "▶"}
</span>
</div>
@ -420,7 +420,7 @@ export function MemoryTab({ workspaceId }: Props) {
</pre>
)}
<div className="flex items-center justify-between">
<span className="text-[9px] text-ink-soft">
<span className="text-[9px] text-ink-mid">
Updated: {new Date(entry.updated_at).toLocaleString()}
</span>
<div className="flex items-center gap-2">
@ -452,7 +452,7 @@ export function MemoryTab({ workspaceId }: Props) {
<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">
<p className="text-xs text-ink">Advanced workspace memory is hidden</p>
<p className="text-[10px] text-ink-soft truncate">
<p className="text-[10px] text-ink-mid truncate">
KV entries remain available if you need the raw platform store.
</p>
</div>

View File

@ -180,7 +180,7 @@ export function ScheduleTab({ workspaceId }: Props) {
};
if (loading) {
return <div className="p-4 text-[10px] text-ink-soft">Loading schedules...</div>;
return <div className="p-4 text-[10px] text-ink-mid">Loading schedules...</div>;
}
return (
@ -207,11 +207,11 @@ export function ScheduleTab({ workspaceId }: Props) {
placeholder="Schedule name (e.g., Daily security scan)"
value={formName}
onChange={(e) => setFormName(e.target.value)}
className="w-full text-[10px] bg-surface-card border border-line rounded px-2 py-1 text-ink placeholder:text-ink-soft"
className="w-full text-[10px] bg-surface-card border border-line rounded px-2 py-1 text-ink placeholder:text-ink-mid"
/>
<div className="flex gap-2">
<div className="flex-1">
<label htmlFor={cronId} className="text-[10px] text-ink-soft block mb-0.5">Cron Expression</label>
<label htmlFor={cronId} className="text-[10px] text-ink-mid block mb-0.5">Cron Expression</label>
<input
id={cronId}
type="text"
@ -219,12 +219,12 @@ export function ScheduleTab({ workspaceId }: Props) {
onChange={(e) => setFormCron(e.target.value)}
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-ink-soft mt-0.5">
<div className="text-[10px] text-ink-mid mt-0.5">
{cronToHuman(formCron)}
</div>
</div>
<div className="w-24">
<label htmlFor={timezoneId} className="text-[10px] text-ink-soft block mb-0.5">Timezone</label>
<label htmlFor={timezoneId} className="text-[10px] text-ink-mid block mb-0.5">Timezone</label>
<select
id={timezoneId}
value={formTimezone}
@ -245,14 +245,14 @@ export function ScheduleTab({ workspaceId }: Props) {
</div>
</div>
<div>
<label htmlFor={promptId} className="text-[10px] text-ink-soft block mb-0.5">Prompt / Task</label>
<label htmlFor={promptId} className="text-[10px] text-ink-mid block mb-0.5">Prompt / Task</label>
<textarea
id={promptId}
value={formPrompt}
onChange={(e) => setFormPrompt(e.target.value)}
placeholder="What should the agent do on this schedule?"
rows={3}
className="w-full text-[10px] bg-surface-card border border-line rounded px-2 py-1 text-ink placeholder:text-ink-soft resize-y"
className="w-full text-[10px] bg-surface-card border border-line rounded px-2 py-1 text-ink placeholder:text-ink-mid resize-y"
/>
</div>
<div className="flex items-center gap-2">
@ -290,7 +290,7 @@ export function ScheduleTab({ workspaceId }: Props) {
Cancel
</button>
</div>
<div className="text-[10px] text-ink-soft space-y-0.5">
<div className="text-[10px] text-ink-mid space-y-0.5">
<div>Common patterns:</div>
<div className="font-mono">{"0 9 * * *"} Daily at 9:00 AM</div>
<div className="font-mono">{"*/30 * * * *"} Every 30 minutes</div>
@ -306,7 +306,7 @@ export function ScheduleTab({ workspaceId }: Props) {
<div className="p-6 text-center">
<div className="text-2xl mb-2"></div>
<div className="text-[10px] text-ink-mid mb-1">No schedules yet</div>
<div className="text-[9px] text-ink-soft">
<div className="text-[9px] text-ink-mid">
Add a schedule to run tasks automatically daily scans, periodic reports, standup reminders.
</div>
</div>
@ -336,16 +336,16 @@ export function ScheduleTab({ workspaceId }: Props) {
{sched.name || "Unnamed schedule"}
</span>
</div>
<div className="text-[9px] text-ink-soft mt-0.5 font-mono">
<div className="text-[9px] text-ink-mid mt-0.5 font-mono">
{cronToHuman(sched.cron_expr)}
{sched.timezone !== "UTC" && (
<span className="text-ink-soft"> ({sched.timezone})</span>
<span className="text-ink-mid"> ({sched.timezone})</span>
)}
</div>
<div className="text-[9px] text-ink-soft mt-0.5 truncate">
<div className="text-[9px] text-ink-mid mt-0.5 truncate">
{sched.prompt.slice(0, 80)}{sched.prompt.length > 80 ? "..." : ""}
</div>
<div className="flex items-center gap-3 mt-1 text-[8px] text-ink-soft">
<div className="flex items-center gap-3 mt-1 text-[8px] text-ink-mid">
<span>Last: {relativeTime(sched.last_run_at)}</span>
<span>Next: {relativeTime(sched.next_run_at)}</span>
<span>Runs: {sched.run_count}</span>

View File

@ -320,7 +320,7 @@ export function SkillsTab({ workspaceId, data }: Props) {
aria-label="Plugins (none installed)"
>
<div className="flex items-center gap-2">
<span className="text-[10px] uppercase tracking-[0.2em] text-ink-soft">Plugins</span>
<span className="text-[10px] uppercase tracking-[0.2em] text-ink-mid">Plugins</span>
<span className="text-[11px] text-ink-mid">0 installed</span>
</div>
<button
@ -342,7 +342,7 @@ export function SkillsTab({ workspaceId, data }: Props) {
<div id="plugins-section" className="rounded-xl border border-line bg-surface-sunken/70 p-3">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-[10px] uppercase tracking-[0.22em] text-ink-soft">Plugins</div>
<div className="text-[10px] uppercase tracking-[0.22em] text-ink-mid">Plugins</div>
<h3 className="mt-1 text-sm font-semibold text-ink">
{installed.length} installed
</h3>
@ -379,21 +379,21 @@ export function SkillsTab({ workspaceId, data }: Props) {
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="text-[11px] font-medium text-ink">{p.name}</span>
{p.version && <span className="text-[10px] text-ink-soft">v{p.version}</span>}
{p.version && <span className="text-[10px] text-ink-mid">v{p.version}</span>}
{inert && (
<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
</span>
)}
</div>
{p.description && <div className="text-[10px] text-ink-soft truncate">{p.description}</div>}
{p.description && <div className="text-[10px] text-ink-mid truncate">{p.description}</div>}
{p.skills && p.skills.length > 0 && (
<div className="mt-1 flex flex-wrap gap-1">
{p.skills.slice(0, 4).map((s) => (
<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 && (
<span className="text-[10px] text-ink-soft">+{p.skills.length - 4}</span>
<span className="text-[10px] text-ink-mid">+{p.skills.length - 4}</span>
)}
</div>
)}
@ -417,7 +417,7 @@ export function SkillsTab({ workspaceId, data }: Props) {
{/* Install from any source (github://, clawhub://, …) */}
<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="text-[10px] uppercase tracking-[0.2em] text-ink-soft">
<div className="text-[10px] uppercase tracking-[0.2em] text-ink-mid">
Install from source
</div>
{sourceSchemes.length > 0 && (
@ -425,7 +425,7 @@ export function SkillsTab({ workspaceId, data }: Props) {
{sourceSchemes.map((s) => (
<span
key={s}
className="rounded-full border border-line/50 bg-surface-sunken/50 px-1.5 py-0.5 text-[10px] text-ink-soft"
className="rounded-full border border-line/50 bg-surface-sunken/50 px-1.5 py-0.5 text-[10px] text-ink-mid"
>
{s}://
</span>
@ -444,7 +444,7 @@ export function SkillsTab({ workspaceId, data }: Props) {
}}
placeholder="e.g. github://owner/repo#v1.0"
spellCheck={false}
className="flex-1 rounded border border-line bg-surface px-2 py-1 text-[10px] text-ink placeholder:text-ink-soft focus:outline-none focus:border-violet-600 focus-visible:ring-2 focus-visible:ring-violet-600/50"
className="flex-1 rounded border border-line bg-surface px-2 py-1 text-[10px] text-ink placeholder:text-ink-mid focus:outline-none focus:border-violet-600 focus-visible:ring-2 focus-visible:ring-violet-600/50"
/>
<button
onClick={handleInstallCustom}
@ -454,12 +454,12 @@ export function SkillsTab({ workspaceId, data }: Props) {
{installing === customSource.trim() ? "Installing..." : "Install"}
</button>
</div>
<div className="mt-1 text-[10px] text-ink-soft">
<div className="mt-1 text-[10px] text-ink-mid">
Local registry plugins below; paste any scheme URL above for GitHub or other sources.
</div>
</div>
<div className="flex items-center justify-between mb-2">
<div className="text-[10px] uppercase tracking-[0.2em] text-ink-soft">Available plugins</div>
<div className="text-[10px] uppercase tracking-[0.2em] text-ink-mid">Available plugins</div>
{/* Retry visible whenever registry is empty including
the loading state so a stuck fetch (Fast Refresh
stranded promise, slow server, browser quirk) has a
@ -486,21 +486,21 @@ export function SkillsTab({ workspaceId, data }: Props) {
)}
</div>
{registryLoading && registry.length === 0 ? (
<div className="text-[10px] text-ink-soft">Loading registry</div>
<div className="text-[10px] text-ink-mid">Loading registry</div>
) : registryError ? (
<div className="rounded-lg border border-red-800/40 bg-red-950/20 px-2 py-1.5">
<div className="text-[10px] text-bad font-semibold mb-0.5">
Couldn't load the plugin registry
</div>
<div className="text-[10px] text-bad/80">{registryError}</div>
<div className="mt-1 text-[10px] text-ink-soft">
<div className="mt-1 text-[10px] text-ink-mid">
Check the platform server is reachable at /plugins. The Retry button is in the header above.
</div>
</div>
) : registry.length === 0 ? (
<div className="rounded-lg border border-line/40 bg-surface/40 px-2 py-1.5">
<div className="text-[10px] text-ink-mid mb-0.5">Registry returned 0 plugins.</div>
<div className="text-[10px] text-ink-soft">
<div className="text-[10px] text-ink-mid">
This usually means the platform's plugins/ directory is empty.
Run scripts/clone-manifest.sh to populate it from the standalone repos.
</div>
@ -514,13 +514,13 @@ export function SkillsTab({ workspaceId, data }: Props) {
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="text-[11px] text-ink-mid">{p.name}</span>
{p.version && <span className="text-[10px] text-ink-soft">v{p.version}</span>}
{p.version && <span className="text-[10px] text-ink-mid">v{p.version}</span>}
</div>
{p.description && <div className="text-[10px] text-ink-soft truncate">{p.description}</div>}
{p.description && <div className="text-[10px] text-ink-mid truncate">{p.description}</div>}
{p.tags && p.tags.length > 0 && (
<div className="mt-1 flex flex-wrap gap-1">
{p.tags.map((t) => (
<span key={t} className="rounded-full border border-line/40 px-1.5 py-0.5 text-[10px] text-ink-soft">{t}</span>
<span key={t} className="rounded-full border border-line/40 px-1.5 py-0.5 text-[10px] text-ink-mid">{t}</span>
))}
</div>
)}
@ -556,7 +556,7 @@ export function SkillsTab({ workspaceId, data }: Props) {
<div className="rounded-xl border border-line bg-surface-sunken/70 p-3">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-[10px] uppercase tracking-[0.22em] text-ink-soft">Workspace skills</div>
<div className="text-[10px] uppercase tracking-[0.22em] text-ink-mid">Workspace skills</div>
<h3 className="mt-1 text-sm font-semibold text-ink">Installed skills</h3>
</div>
<div className="flex flex-wrap gap-2">
@ -564,7 +564,7 @@ export function SkillsTab({ workspaceId, data }: Props) {
<MetaPill label="Runtime" value={capability.runtime || "unknown"} />
</div>
</div>
<p className="mt-2 text-[11px] leading-5 text-ink-soft">
<p className="mt-2 text-[11px] leading-5 text-ink-mid">
Live skill directory from the Agent Card updates when the workspace hot-reloads skills.
</p>
<div className="mt-3 flex flex-wrap gap-2">
@ -593,7 +593,7 @@ export function SkillsTab({ workspaceId, data }: Props) {
{skills.length === 0 ? (
<div className="rounded-xl border border-dashed border-line bg-surface-sunken/40 p-6 text-center">
<div className="text-sm text-ink">No skills loaded</div>
<p className="mt-2 text-[11px] leading-5 text-ink-soft">
<p className="mt-2 text-[11px] leading-5 text-ink-mid">
Add skills from the Config tab, install a plugin above, or let the runtime hot-load them.
</p>
</div>
@ -604,7 +604,7 @@ export function SkillsTab({ workspaceId, data }: Props) {
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-xs font-semibold text-ink">{skill.name}</div>
<div className="mt-0.5 text-[10px] font-mono text-ink-soft">{skill.id}</div>
<div className="mt-0.5 text-[10px] font-mono text-ink-mid">{skill.id}</div>
</div>
{skill.tags.length > 0 && (
<div className="flex flex-wrap justify-end gap-1.5">
@ -626,7 +626,7 @@ export function SkillsTab({ workspaceId, data }: Props) {
{skill.examples.length > 0 && (
<div className="mt-2">
<div className="text-[9px] uppercase tracking-[0.2em] text-ink-soft">Examples</div>
<div className="text-[9px] uppercase tracking-[0.2em] text-ink-mid">Examples</div>
<div className="mt-1 space-y-1">
{skill.examples.slice(0, 2).map((example, index) => (
<div
@ -666,7 +666,7 @@ function extractSkills(agentCard: Record<string, unknown> | null): SkillEntry[]
function MetaPill({ label, value }: { label: string; value: string }) {
return (
<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-ink-soft">{label}</span>
<span className="uppercase tracking-[0.18em] text-[8px] text-ink-mid">{label}</span>
<span className="font-medium">{value}</span>
</span>
);

View File

@ -37,7 +37,7 @@ function NotAvailablePanel({ runtime }: { runtime: string }) {
viewBox="0 0 72 72"
fill="none"
aria-hidden="true"
className="text-ink-soft mb-4"
className="text-ink-mid mb-4"
>
<rect
x="10"
@ -74,7 +74,7 @@ function NotAvailablePanel({ runtime }: { runtime: string }) {
/>
</svg>
<h3 className="text-sm font-medium text-ink mb-1.5">Terminal not available</h3>
<p className="text-[11px] text-ink-soft max-w-xs leading-relaxed">
<p className="text-[11px] text-ink-mid max-w-xs leading-relaxed">
This workspace runs the{" "}
<span className="font-mono text-ink-mid">{runtime}</span> runtime,
which doesn't expose a shell. Use the Chat tab to interact with the

View File

@ -48,7 +48,7 @@ export function TracesTab({ workspaceId }: Props) {
}, [loadTraces]);
if (loading) {
return <div className="p-4 text-xs text-ink-soft">Loading traces...</div>;
return <div className="p-4 text-xs text-ink-mid">Loading traces...</div>;
}
return (
@ -60,7 +60,7 @@ export function TracesTab({ workspaceId }: Props) {
onClick={loadTraces}
// Added focus-visible ring; previous version was hover-only,
// invisible to keyboard users.
className="text-[10px] text-ink-soft hover:text-ink-mid rounded-sm px-1 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
className="text-[10px] text-ink-mid hover:text-ink-mid rounded-sm px-1 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
>
Refresh
</button>
@ -75,9 +75,9 @@ export function TracesTab({ workspaceId }: Props) {
{traces.length === 0 && !error ? (
<div className="text-center py-8">
<div className="text-2xl opacity-20 mb-2" aria-hidden="true">--</div>
<p className="text-xs text-ink-soft">No traces yet</p>
<details className="mt-2 text-[10px] text-ink-soft">
<summary className="cursor-pointer text-ink-soft hover:text-ink-mid">How to enable tracing</summary>
<p className="text-xs text-ink-mid">No traces yet</p>
<details className="mt-2 text-[10px] text-ink-mid">
<summary className="cursor-pointer text-ink-mid hover:text-ink-mid">How to enable tracing</summary>
<p className="mt-1">
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>
@ -108,20 +108,20 @@ export function TracesTab({ workspaceId }: Props) {
}`} />
<div className="flex-1 min-w-0">
<div className="text-[11px] text-ink truncate">{trace.name || "trace"}</div>
<div className="text-[9px] text-ink-soft">{formatTime(trace.timestamp)}</div>
<div className="text-[9px] text-ink-mid">{formatTime(trace.timestamp)}</div>
</div>
<div className="flex items-center gap-2 shrink-0">
{trace.latency != null && (
<span className="text-[9px] text-ink-soft tabular-nums">
<span className="text-[9px] text-ink-mid tabular-nums">
{trace.latency > 1000 ? `${(trace.latency / 1000).toFixed(1)}s` : `${trace.latency}ms`}
</span>
)}
{trace.usage?.total != null && (
<span className="text-[9px] text-ink-soft tabular-nums">
<span className="text-[9px] text-ink-mid tabular-nums">
{trace.usage.total} tok
</span>
)}
<span aria-hidden="true" className="text-[9px] text-ink-soft">
<span aria-hidden="true" className="text-[9px] text-ink-mid">
{isOpen ? "▼" : "▶"}
</span>
</div>
@ -131,7 +131,7 @@ export function TracesTab({ workspaceId }: Props) {
<div id={panelId} className="px-3 pb-2 space-y-2 border-t border-line/30">
{trace.input && (
<div>
<div className="text-[9px] text-ink-soft uppercase tracking-wider mt-2 mb-1">Input</div>
<div className="text-[9px] text-ink-mid uppercase tracking-wider mt-2 mb-1">Input</div>
<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))}
</pre>
@ -139,18 +139,18 @@ export function TracesTab({ workspaceId }: Props) {
)}
{trace.output && (
<div>
<div className="text-[9px] text-ink-soft uppercase tracking-wider mb-1">Output</div>
<div className="text-[9px] text-ink-mid uppercase tracking-wider mb-1">Output</div>
<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))}
</pre>
</div>
)}
{trace.totalCost != null && (
<div className="text-[9px] text-ink-soft">
<div className="text-[9px] text-ink-mid">
Cost: ${trace.totalCost.toFixed(6)}
</div>
)}
<div className="text-[8px] text-ink-soft font-mono select-all">
<div className="text-[8px] text-ink-mid font-mono select-all">
{trace.id}
</div>
</div>

View File

@ -389,7 +389,7 @@ export function AgentCommsPanel({ workspaceId }: { workspaceId: string }) {
}, [messages]);
if (loading) {
return <div className="text-xs text-ink-soft text-center py-8">Loading agent communications...</div>;
return <div className="text-xs text-ink-mid text-center py-8">Loading agent communications...</div>;
}
if (loadError !== null && messages.length === 0) {
@ -415,10 +415,10 @@ export function AgentCommsPanel({ workspaceId }: { workspaceId: string }) {
if (messages.length === 0) {
return (
<div className="text-xs text-ink-soft text-center py-8">
<div className="text-xs text-ink-mid text-center py-8">
No agent-to-agent communications yet.
<br />
<span className="text-ink-soft">Delegations and peer messages will appear here.</span>
<span className="text-ink-mid">Delegations and peer messages will appear here.</span>
</div>
);
}
@ -613,10 +613,10 @@ function PeerTabButton({
className={`shrink-0 px-3 py-1.5 text-[10px] font-medium transition-colors whitespace-nowrap ${
active
? "border-b-2 border-cyan-500 text-cyan-200"
: "border-b-2 border-transparent text-ink-soft hover:text-ink-mid"
: "border-b-2 border-transparent text-ink-mid hover:text-ink-mid"
}`}
>
{label} <span className="text-[9px] text-ink-soft">({count})</span>
{label} <span className="text-[9px] text-ink-mid">({count})</span>
</button>
);
}
@ -669,7 +669,7 @@ function WaitingBubbles({ visible }: { visible: CommMessage[] }) {
role="status"
aria-label={`Waiting for reply from ${m.peerName}`}
>
<div className="text-[9px] text-ink-soft mb-1"> To {m.peerName}</div>
<div className="text-[9px] text-ink-mid mb-1"> To {m.peerName}</div>
<span className="flex items-center gap-2 text-ink-mid">
<span className="flex gap-0.5" aria-hidden="true">
<span
@ -708,7 +708,7 @@ function NormalMessage({ msg }: { msg: CommMessage }) {
: "bg-surface-card/80 text-ink border border-line/30"
}`}
>
<div className="text-[9px] text-ink-soft mb-1">
<div className="text-[9px] text-ink-mid mb-1">
{msg.flow === "out" ? `→ To ${msg.peerName}` : `← From ${msg.peerName}`}
</div>
{msg.text ? (
@ -731,7 +731,7 @@ function NormalMessage({ msg }: { msg: CommMessage }) {
{msg.responseText}
</MarkdownBody>
)}
<div className="text-[9px] text-ink-soft mt-1">
<div className="text-[9px] text-ink-mid mt-1">
{new Date(msg.timestamp).toLocaleTimeString()}
</div>
</div>
@ -804,7 +804,7 @@ function ErrorMessage({ msg }: { msg: CommMessage }) {
</div>
{msg.text && (
<div className="text-[10px] text-ink-soft mb-1.5">
<div className="text-[10px] text-ink-mid mb-1.5">
<span className="uppercase tracking-wide">Task</span>
<MarkdownBody className="text-ink-mid">{msg.text}</MarkdownBody>
</div>
@ -841,7 +841,7 @@ function ErrorMessage({ msg }: { msg: CommMessage }) {
</div>
)}
<div className="text-[9px] text-ink-soft mt-1.5">
<div className="text-[9px] text-ink-mid mt-1.5">
{new Date(msg.timestamp).toLocaleTimeString()}
</div>
</div>

View File

@ -148,7 +148,7 @@ export function AttachmentTextPreview({ workspaceId, attachment, onDownload, ton
<button
type="button"
onClick={() => onDownload(attachment)}
className="text-ink-soft hover:text-ink"
className="text-ink-mid hover:text-ink"
title={`Download ${attachment.name}`}
aria-label={`Download ${attachment.name}`}
>

View File

@ -29,11 +29,11 @@ export function PendingAttachmentPill({
<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-ink-mid shrink-0" />
<span className="truncate" title={file.name}>{file.name}</span>
<span className="text-ink-soft shrink-0 tabular-nums">{formatSize(file.size)}</span>
<span className="text-ink-mid shrink-0 tabular-nums">{formatSize(file.size)}</span>
<button
onClick={onRemove}
aria-label={`Remove ${file.name}`}
className="ml-0.5 text-ink-soft hover:text-ink transition-colors shrink-0"
className="ml-0.5 text-ink-mid hover:text-ink transition-colors shrink-0"
>
<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" />

View File

@ -50,7 +50,7 @@ export function TextInput({ label, value, onChange, placeholder, mono }: { label
const id = `textinput-${label.toLowerCase().replace(/\s+/g, "-")}`;
return (
<div>
<label htmlFor={id} className="text-[10px] text-ink-soft block mb-1">{label}</label>
<label htmlFor={id} className="text-[10px] text-ink-mid block mb-1">{label}</label>
<input
id={id}
type="text"
@ -68,7 +68,7 @@ export function NumberInput({ label, value, onChange, min, max }: { label: strin
const id = `numberinput-${label.toLowerCase().replace(/\s+/g, "-")}`;
return (
<div>
<label htmlFor={id} className="text-[10px] text-ink-soft block mb-1">{label}</label>
<label htmlFor={id} className="text-[10px] text-ink-mid block mb-1">{label}</label>
<input
id={id}
type="number"
@ -97,12 +97,12 @@ export function TagList({ label, values, onChange, placeholder }: { label: strin
const [input, setInput] = useState("");
return (
<div>
<label htmlFor={id} className="text-[10px] text-ink-soft block mb-1">{label}</label>
<label htmlFor={id} className="text-[10px] text-ink-mid block mb-1">{label}</label>
<div className="flex flex-wrap gap-1 mb-1">
{values.map((v, i) => (
<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}
<button type="button" aria-label={`Remove tag ${v}`} onClick={() => onChange(values.filter((_, j) => j !== i))} className="text-ink-soft hover:text-bad">×</button>
<button type="button" aria-label={`Remove tag ${v}`} onClick={() => onChange(values.filter((_, j) => j !== i))} className="text-ink-mid hover:text-bad">×</button>
</span>
))}
</div>

View File

@ -101,9 +101,9 @@ function SecretRow({ label, secretKey, isSet, scope, globalMode, onSave, onDelet
<div className="min-w-0">
<div className="text-[10px] text-ink-mid">{label}</div>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-[9px] font-mono text-ink-soft">{secretKey}</span>
<span className="text-[9px] font-mono text-ink-mid">{secretKey}</span>
{isSet && (
<span className="text-[9px] font-mono text-ink-soft tracking-widest" title="Value is set (encrypted)">
<span className="text-[9px] font-mono text-ink-mid tracking-widest" title="Value is set (encrypted)">
</span>
)}
@ -159,7 +159,7 @@ function CustomSecretRow({ secretKey, scope, globalMode, onSave, onDelete }: {
<span className={`text-[10px] font-mono ${globalMode ? "text-warm" : scope === "global" ? "text-ink-mid" : "text-accent"}`}>
{secretKey}
</span>
<span className="text-[9px] font-mono text-ink-soft tracking-widest ml-2"></span>
<span className="text-[9px] font-mono text-ink-mid tracking-widest ml-2"></span>
</div>
<div className="flex items-center gap-2 shrink-0">
<span className="text-[10px] text-good">Set</span>
@ -288,7 +288,7 @@ export function SecretsSection({ workspaceId, requiredEnv }: { workspaceId: stri
return (
<Section title="Secrets & API Keys" defaultOpen={false}>
{loading ? (
<div className="text-[10px] text-ink-soft">Loading secrets...</div>
<div className="text-[10px] text-ink-mid">Loading secrets...</div>
) : (
<div className="space-y-2">
{error && <div className="px-2 py-1 bg-red-900/30 border border-red-800 rounded text-[10px] text-bad">{error}</div>}
@ -369,7 +369,7 @@ export function SecretsSection({ workspaceId, requiredEnv }: { workspaceId: stri
</button>
)}
<div className="text-[9px] text-ink-soft pt-1">
<div className="text-[9px] text-ink-mid pt-1">
Values are encrypted and never exposed to the browser.
{globalMode
? " Global keys are shared across all workspaces. Restart workspaces to apply changes."

View File

@ -9,7 +9,7 @@
| Technology | Version | Purpose |
|-----------|--------|---------|
| React Flow | `@xyflow/react` v12 | Node/edge rendering |
| Framework | Next.js 14 App Router | Routing, SSR |
| Framework | Next.js 15 App Router | Routing, SSR |
| Styling | Tailwind v4 | CSS with custom properties |
| State | Zustand | Client state management |
@ -20,6 +20,7 @@ canvas/src/
├── components/
│ ├── Canvas.tsx # Viewport management, ReactFlow wrapper
│ ├── Toolbar.tsx # Add node/edge controls
│ ├── KeyboardShortcutsDialog.tsx # ? help dialog
│ ├── ContextMenu.tsx # Right-click menu
│ ├── SidePanel.tsx # Properties panel
│ ├── WorkspaceNode.tsx # Node rendering
@ -48,8 +49,24 @@ canvas/src/
### 🟡 MEDIUM: Pre-commit Hook Verification
**Issue:** Pre-commit hook checks 'use client' on hook-using components but unclear if it actually fails on violations.
### ✅ MEDIUM: text-ink-soft WCAG AA contrast (fixed)
**File:** `canvas/src/app/globals.css` + all canvas components
**Issue:** `--color-ink-soft` (#8d92a0) on dark zinc (#0e1014) = ~2.2:1 contrast,
below the WCAG 2.1 AA minimum of 4.5:1 for normal text.
**Impact:** Used in 261 instances across 52 files (captions, group titles, hints).
**Fix:** Replaced `text-ink-soft``text-ink-mid` (7.6:1) across all canvas source.
PR: `fix/ink-soft-wcag-contrast`.
**Action:** Verify the hook is enforcing the rule correctly.
### ✅ MEDIUM: text-ink-soft WCAG AA contrast (fixed)
**File:** `canvas/src/app/globals.css` + all canvas components
**Issue:** `--color-ink-soft` (#8d92a0) on dark zinc (#0e1014) = ~2.2:1 contrast,
below the WCAG 2.1 AA minimum of 4.5:1 for normal text.
**Impact:** Used in 261 instances across 52 files (captions, group titles, hints).
**Fix:** Replaced `text-ink-soft``text-ink-mid` (7.6:1) across all canvas source.
PR: `fix/ink-soft-wcag-contrast`.
## Verified Findings
### Node Rendering ✅ (with notes)
@ -101,7 +118,8 @@ canvas/src/
### Drag and Drop ✅
- **Mouse drag:** React Flow native
- **Drop target:** Visual indicator (`bg-emerald-950/40 border-emerald-400/60`) ✅
- **Keyboard alternative:** Arrow keys move selected node 10px per press (50px with Shift) (PR #182) ✅
- **Keyboard alternative:** Arrow-key nudge via `useKeyboardShortcuts` (PR #182) ✅
- **Status:** Full — mouse and keyboard users can reposition nodes.
---
@ -111,11 +129,11 @@ canvas/src/
|----------|------|-------|--------|
| ~~HIGH~~ | ~~Screen reader announcements for canvas state changes~~ | ~~Canvas.tsx, canvas-events.ts, canvas.ts~~ | ✅ Done — PR #172 |
| MEDIUM | Keyboard shortcut help dialog | useKeyboardShortcuts.ts | ✅ Done (PR #175) |
| MEDIUM | Keyboard-accessible node drag | WorkspaceNode.tsx, useDragHandlers.ts | ✅ Done (this PR) |
| LOW | Keyboard-accessible edge anchors | A2AEdge.tsx, WorkspaceNode.tsx | ✅ Done |
| LOW | Keyboard-accessible node resize | useKeyboardShortcuts.ts, WorkspaceNode.tsx | ✅ Done |
| MEDIUM | Keyboard-accessible node drag | WorkspaceNode.tsx, useDragHandlers.ts | ✅ Done (PR #182) |
| LOW | Keyboard-accessible edge anchors | A2AEdge.tsx, WorkspaceNode.tsx | ✅ Done (PR #190) |
| LOW | Keyboard-accessible node resize | useKeyboardShortcuts.ts, WorkspaceNode.tsx | ✅ Done (PR #192) |
---
*Verified 2026-05-09 by Core-UIUX against molecule-core/canvas/src/*
*Updated 2026-05-09: screen reader announcements (PR #172) + keyboard shortcut dialog (PR #175) completed*
*Updated 2026-05-10: keyboard shortcut dialog (PR #175) + keyboard node drag (PR #182) + keyboard edge anchors (PR #190) + keyboard node resize (PR #192) + screen reader announcements (PR #172) + text-ink-soft WCAG AA fix + Next.js 15.5.15*