From 2b10290393af60bbd3860aa599d4f9c633ff5577 Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Sun, 25 Oct 2026 01:03:22 +0300 Subject: [PATCH] refactor(studio): split layout.css by screen Co-Authored-By: Claude Opus 5.5 --- apps/agent-forge/src/App.tsx | 4 + apps/agent-forge/src/canvas/AgentNode.tsx | 2 +- apps/agent-forge/src/canvas/paletteActions.ts | 2 +- apps/agent-forge/src/components/drawer.css | 445 ++++++ apps/agent-forge/src/components/layout.css | 1300 +---------------- .../src/components/topbarNarrow.css | 82 ++ .../src/components/useMediaQuery.ts | 2 +- apps/agent-forge/src/observe/observe.css | 2 +- .../agent-forge/src/screens/agents/agents.css | 549 +++++++ apps/agent-forge/src/ui/base.css | 235 +++ apps/agent-forge/src/ui/primitives.css | 2 +- 11 files changed, 1324 insertions(+), 1301 deletions(-) create mode 100644 apps/agent-forge/src/components/drawer.css create mode 100644 apps/agent-forge/src/components/topbarNarrow.css create mode 100644 apps/agent-forge/src/screens/agents/agents.css create mode 100644 apps/agent-forge/src/ui/base.css diff --git a/apps/agent-forge/src/App.tsx b/apps/agent-forge/src/App.tsx index 1421a3e3d..670be3b5d 100644 --- a/apps/agent-forge/src/App.tsx +++ b/apps/agent-forge/src/App.tsx @@ -1,4 +1,8 @@ +import './ui/base.css'; import './components/layout.css'; +import './components/topbarNarrow.css'; +import './screens/agents/agents.css'; +import './components/drawer.css'; import { ReactFlowProvider } from '@xyflow/react'; import { AppStateProvider } from './state/AppState'; import { ErrorBoundary } from './components/ErrorBoundary'; diff --git a/apps/agent-forge/src/canvas/AgentNode.tsx b/apps/agent-forge/src/canvas/AgentNode.tsx index 49374180a..f88cfa091 100644 --- a/apps/agent-forge/src/canvas/AgentNode.tsx +++ b/apps/agent-forge/src/canvas/AgentNode.tsx @@ -210,7 +210,7 @@ export const NODE_TYPES: Record = Object.fromEntries( GRAPH_NODE_TYPES.map((type) => [flowNodeType(type), AgentNode]) ); -/** The MiniMap draws each node in its type's swatch colour (see `.minimap-node-*` in layout.css). */ +/** The MiniMap draws each node in its type's swatch colour (see `.minimap-node-*` in screens/agents/agents.css). */ export function miniMapNodeClassName(node: { data: Record }): string { const graphNode = (node.data as Partial).graphNode; return graphNode ? `minimap-node-${graphNode.type}` : ''; diff --git a/apps/agent-forge/src/canvas/paletteActions.ts b/apps/agent-forge/src/canvas/paletteActions.ts index dae6cc11e..02758b7b1 100644 --- a/apps/agent-forge/src/canvas/paletteActions.ts +++ b/apps/agent-forge/src/canvas/paletteActions.ts @@ -1,7 +1,7 @@ import { HOOK_TEMPLATES, type HookTemplate } from '../hooks/hookTemplates'; import type { AgentNodeHookPhase, GraphPosition } from '../graph/types'; -/** Approximate rendered size of an `.rf-node` (layout.css), used to centre a newly added node. */ +/** Approximate rendered size of an `.rf-node` (screens/agents/agents.css), used to centre a newly added node. */ const NODE_SIZE = { width: 190, height: 80 }; /** diff --git a/apps/agent-forge/src/components/drawer.css b/apps/agent-forge/src/components/drawer.css new file mode 100644 index 000000000..375aef96b --- /dev/null +++ b/apps/agent-forge/src/components/drawer.css @@ -0,0 +1,445 @@ +/* Run drawer: tab bar, Logs, Trace, History, Output, debugger and Chat. */ + +/* ---------- Bottom drawer ---------- */ +.drawer { + grid-column: 1 / -1; + border-top: 1px solid var(--border); + background: var(--surface); + display: flex; + flex-direction: column; + min-height: 0; +} +.drawer-tabs { + display: flex; + align-items: center; + border-bottom: 1px solid var(--border); + flex: none; + padding: 0 6px; +} +.drawer-tab { + padding: 8px 12px; + font-size: 11.5px; + font-weight: 600; + color: var(--text-muted); + cursor: pointer; + border-bottom: 2px solid transparent; + display: flex; + align-items: center; + gap: 6px; + background: none; + border-left: none; + border-right: none; + border-top: none; +} +.drawer-tab.active { + color: var(--text); + border-bottom-color: var(--accent); +} +.drawer-tab .count { + background: var(--surface-2); + border-radius: 10px; + padding: 0 6px; + font-size: 10px; + font-family: var(--font-mono); + color: var(--text-faint); +} +.drawer-spacer { + flex: 1; +} +.drawer-body { + flex: 1; + overflow-y: auto; + font-family: var(--font-mono); + font-size: 11.5px; + padding: 8px 12px; + color: var(--text-muted); +} + +/* ---------- Logs tab ---------- */ +.logs-panel { + display: flex; + flex-direction: column; + height: 100%; +} +.logs-filter-bar { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + border-bottom: 1px solid var(--border); + flex: none; + font-family: var(--font-ui); +} +.logs-filter-bar input[type='search'] { + flex: 1; + min-width: 100px; + background: var(--surface-2); + border: 1px solid var(--border); + color: var(--text); + border-radius: 6px; + padding: 4px 8px; + font-size: 11px; +} +.logs-filter-toggle { + border: 1px solid var(--border); + background: var(--surface-2); + color: var(--text-muted); + border-radius: 12px; + padding: 2px 8px; + font-size: 10.5px; + font-weight: 600; + cursor: pointer; +} +.logs-filter-toggle.on { + border-color: var(--accent); + color: var(--accent); + background: var(--accent-soft); +} +.logs-empty { + padding: 16px; + color: var(--text-faint); + font-family: var(--font-ui); +} +.logs-viewport { + flex: 1; + overflow-y: auto; + position: relative; +} +.log-line { display: grid; grid-template-columns: 64px 60px 1fr; gap: 10px; padding: 3px 12px; border-radius: 5px; align-items: start; cursor: pointer; } +.log-line:hover { background: var(--surface-2); } +.log-line.highlighted { background: var(--accent-soft); } +.log-time { color: var(--text-faint); } +.log-level { font-weight: 700; } +.lvl-info { color: var(--info); } +.lvl-warn { color: var(--warning); } +.lvl-error { color: var(--danger); } +.lvl-tool { color: var(--accent); } +.log-msg { color: var(--text); white-space: pre-wrap; word-break: break-word; } +.log-msg b { color: var(--text-muted); font-weight: 600; } + +/* ---------- Trace tab ---------- */ +.trace-panel { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + height: 100%; + overflow-y: auto; +} +.trace-empty { + padding: 16px; + color: var(--text-faint); + font-family: var(--font-ui); +} +.trace-row { display: flex; align-items: center; gap: 8px; padding: 5px 12px; cursor: pointer; border-radius: 5px; } +.trace-row:hover, .trace-row.selected { background: var(--surface-2); } +.trace-bar-track { position: relative; flex: 1; height: 16px; background: var(--surface-2); border-radius: 4px; overflow: hidden; } +.trace-bar { position: absolute; top: 0; bottom: 0; border-radius: 4px; background: var(--accent); } +.trace-bar.pending { background: repeating-linear-gradient(45deg, var(--accent), var(--accent) 4px, transparent 4px, transparent 8px); } +.trace-name { width: 150px; flex: none; color: var(--text); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.trace-dur { width: 56px; flex: none; text-align: right; color: var(--text-muted); } + +/* ---------- History tab (time travel) ---------- */ +.history-panel { display: flex; flex-direction: column; gap: 10px; } +.history-table { width: 100%; border-collapse: collapse; } +.history-table th { text-align: left; color: var(--text-faint); font-weight: 600; padding: 3px 8px; border-bottom: 1px solid var(--border); } +.history-table td { color: var(--text); padding: 3px 8px; border-bottom: 1px solid var(--border); } +.replay-editor { display: flex; flex-direction: column; gap: 6px; padding: 8px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface-2); } +.replay-actions { display: flex; gap: 6px; } +.traj-row { display: grid; grid-template-columns: 90px 1fr 1fr; gap: 10px; padding: 3px 8px; border-radius: 5px; } +.traj-row.traj-head { color: var(--text-faint); } +.traj-row.diverged { background: var(--warning-soft); } +.traj-row.diverged > span { color: var(--warning); font-weight: 700; } +.traj-cell { color: var(--text); white-space: pre-wrap; word-break: break-word; } +.traj-missing { color: var(--text-faint); } +.traj-tool { color: var(--accent); } +.traj-drift { margin: 6px 0 0; padding-left: 18px; color: var(--warning); } + +/* ---------- Output tab ---------- */ +.output-json { padding: 10px 14px; white-space: pre-wrap; word-break: break-word; font-family: var(--font-mono); font-size: 11.5px; } +.json-key { color: var(--info); } +.json-str { color: var(--success); } +.json-num { color: var(--warning); } +.json-bool { color: var(--accent); } +.json-null { color: var(--text-faint); } +.json-node { display: block; } +.json-toggle { + background: none; + border: none; + color: var(--text-faint); + cursor: pointer; + font-family: var(--font-mono); + padding: 0 3px 0 0; + font-size: 11px; +} +.json-children { + margin-left: 14px; + border-left: 1px dashed var(--border); + padding-left: 8px; +} + +/* ---------- Step-through debugger ---------- */ +.debug-bar { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + background: var(--warning-soft); + border-bottom: 1px solid var(--border); + font-size: 11px; + font-family: var(--font-ui); +} +.debug-bar .step-count { + color: var(--text-muted); + font-family: var(--font-mono); +} +.breakpoint-toggle { + display: inline-flex; + align-items: center; + gap: 5px; + border: 1px solid var(--border); + background: var(--surface-2); + border-radius: 20px; + padding: 4px 9px; + font-size: 11px; + cursor: pointer; +} +.breakpoint-toggle.on { + border-color: var(--danger); + color: var(--danger); + background: var(--danger-soft); +} +.rf-node.has-breakpoint { + box-shadow: 0 0 0 2px var(--danger); +} +.rf-node.highlighted { + box-shadow: 0 0 0 3px var(--accent), var(--shadow); +} + +@media (max-width: 860px) { + .drawer-tabs { + overflow-x: auto; + } + .drawer-body { + overflow-x: auto; + } + .chat-msg { + max-width: 92%; + } +} + +/* ---------- Chat ---------- + Ported from .design-ref/agent-forge-mockup.html's `.chat-*` classes - + user/agent bubbles, avatars, timestamps, a typing indicator, and inline + tool-call/approval cards under an agent bubble. See ChatPanel.tsx. */ +.chat-panel { + display: flex; + flex-direction: column; + height: 100%; + font-family: var(--font-ui); +} +.chat-session-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + border-bottom: 1px solid var(--border); + font-size: 11.5px; + color: var(--text-muted); +} +.chat-session-bar select { + font-size: 11.5px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--surface-2); + color: var(--text); + padding: 3px 6px; + max-width: 260px; +} +.chat-messages { + flex: 1; + overflow-y: auto; + padding: 10px 14px; + display: flex; + flex-direction: column; + gap: 10px; +} +.chat-empty { + color: var(--text-faint); + font-size: 12.5px; + margin: auto; +} +.chat-msg { + display: flex; + gap: 8px; + max-width: 72%; +} +.chat-msg.user { + align-self: flex-end; + flex-direction: row-reverse; +} +.chat-avatar { + flex: none; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 9.5px; + font-weight: 700; + color: #fff; +} +.chat-msg.agent .chat-avatar { + background: var(--accent); +} +.chat-msg.user .chat-avatar { + background: var(--text-faint); +} +.chat-bubble { + background: var(--surface-2); + border: 1px solid var(--border); + border-radius: 12px; + padding: 7px 10px; + font-size: 12.5px; + line-height: 1.4; + white-space: pre-wrap; +} +.chat-msg.user .chat-bubble { + background: var(--accent); + border-color: var(--accent); + color: #fff; +} +.chat-msg.agent .chat-bubble { + border-top-left-radius: 3px; +} +.chat-msg.user .chat-bubble { + border-top-right-radius: 3px; +} +.chat-bubble .chat-tool-call { + margin-top: 4px; + font-family: var(--font-mono); + font-size: 11px; + opacity: 0.85; +} +.chat-tool-result { + margin-top: 2px; + opacity: 0.75; +} +.chat-time { + font-size: 9.5px; + color: var(--text-faint); + margin-top: 3px; + font-family: var(--font-mono); +} +.chat-msg.user .chat-time { + text-align: right; +} +/* the top bar's Run input, kept apart from typed messages. */ +.chat-msg.run-input .chat-bubble { + background: var(--surface-2); + border: 1px dashed var(--border); + color: var(--text-muted); + font-family: var(--font-mono); + font-size: 11.5px; +} +.chat-msg.run-input .chat-avatar { + background: var(--text-muted); + font-size: 8.5px; +} +.chat-run-label { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.4px; + color: var(--text-faint); + text-align: right; + margin-bottom: 3px; +} +.chat-typing { + display: flex; + gap: 3px; + align-items: center; + padding: 2px 2px; +} +.chat-typing span { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--text-faint); + animation: typing-bounce 1.1s infinite ease-in-out; +} +.chat-typing span:nth-child(2) { + animation-delay: 0.15s; +} +.chat-typing span:nth-child(3) { + animation-delay: 0.3s; +} +@keyframes typing-bounce { + 0%, + 60%, + 100% { + transform: translateY(0); + opacity: 0.5; + } + 30% { + transform: translateY(-3px); + opacity: 1; + } +} +/* Inline-in-thread approval card - reuses .approval-card's base look + (Topbar.tsx's compact version) with a couple of thread-specific tweaks. */ +.chat-approval-card { + flex-wrap: wrap; +} +.chat-action-error { + padding: 4px 12px; + font-size: 11.5px; + color: var(--danger); +} +.chat-input-row { + display: flex; + gap: 8px; + align-items: flex-end; + padding: 8px 10px; + border-top: 1px solid var(--border); +} +.chat-input { + flex: 1; + resize: none; + max-height: 90px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface-2); + color: var(--text); + font-family: var(--font-ui); + font-size: 12.5px; + padding: 7px 9px; +} +.chat-input:focus { + outline: 2px solid var(--accent); + outline-offset: -1px; +} +.chat-input:disabled { + opacity: 0.6; +} + +/* Persisted trace history next to the waterfall */ +.trace-layout { display: flex; height: 100%; min-height: 0; } +.trace-main { flex: 1; min-width: 0; height: 100%; } +.trace-history { display: flex; flex-direction: column; gap: 4px; width: 240px; flex: none; padding: 8px; overflow-y: auto; border-right: 1px solid var(--border); } +.trace-item { display: flex; flex-direction: column; gap: 2px; text-align: left; padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px; background: transparent; color: var(--text); font: inherit; font-size: 12px; cursor: pointer; } +.trace-item:hover, .trace-item.selected { background: var(--surface-2); } +.trace-item.selected { border-color: var(--accent); } +.trace-item-head { display: flex; justify-content: space-between; gap: 8px; } +.trace-item-meta { color: var(--text-muted); } +.trace-item-error .trace-item-status, .trace-history-error { color: var(--danger); } +.trace-history-note { padding: 4px; color: var(--text-faint); font-size: 12px; } +.trace-kind { flex: none; color: var(--text-faint); font-size: 11px; } +.trace-bar.error { background: var(--danger); } +.trace-row-error .trace-name { color: var(--danger); } +@media (max-width: 700px) { + .trace-layout { flex-direction: column; } + .trace-history { width: auto; flex-direction: row; overflow-x: auto; overflow-y: hidden; border-right: 0; border-bottom: 1px solid var(--border); } + .trace-item { min-width: 200px; } +} diff --git a/apps/agent-forge/src/components/layout.css b/apps/agent-forge/src/components/layout.css index b5cbff033..091acd4d1 100644 --- a/apps/agent-forge/src/components/layout.css +++ b/apps/agent-forge/src/components/layout.css @@ -1,9 +1,7 @@ /* - * App shell chrome, ported from .design-ref/agent-forge-mockup.html's - * topbar / rail / canvas-placeholder / inspector / drawer CSS. - * Node/edge/flow-surface styling is intentionally NOT ported here - the - * canvas is a placeholder until real ReactFlow is wired up, so there is - * nothing to draw nodes/edges onto yet. + * App chrome: screen frame, top bar and the error boundary (narrow top bar: topbarNarrow.css). + * The Agents screen lives in screens/agents/agents.css, the run drawer in + * components/drawer.css, shared primitives in ui/base.css. */ .app { @@ -44,18 +42,6 @@ color: var(--accent); } -/* read by screen readers, not drawn (the page h1, status prefixes). */ -.visually-hidden { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} /* ---------- Top bar ---------- */ .topbar { @@ -129,13 +115,6 @@ border-radius: 4px; padding: 1px 5px; } -.dot { - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--success); - box-shadow: 0 0 0 3px var(--success-soft); -} /* inline human-in-the-loop approval card, shown in the Topbar while a run is paused awaiting a tool-call decision (see AppState.runStatus / @@ -165,1281 +144,10 @@ white-space: nowrap; } -.btn { - border: 1px solid var(--border); - background: var(--surface-2); - color: var(--text); - border-radius: 7px; - padding: 6px 12px; - font-size: 12.5px; - font-weight: 600; - cursor: pointer; - display: inline-flex; - align-items: center; - gap: 6px; - transition: filter 0.12s, transform 0.04s; -} -.btn:hover { - filter: brightness(1.05); -} -.btn:active { - transform: translateY(1px); -} -.btn:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 1px; -} -.btn-primary { - background: var(--accent-solid); - border-color: var(--accent-solid); - color: var(--on-accent); -} -.btn-success { - background: var(--success-soft); - border-color: var(--success); - color: var(--success); -} -.btn-danger { - background: var(--danger-soft); - border-color: var(--danger); - color: var(--danger); -} -.btn-ghost { - background: transparent; -} -.btn[disabled] { - opacity: 0.4; - cursor: not-allowed; -} - -/* ---------- Shell ---------- */ -.shell { - display: grid; - grid-template-columns: 236px 1fr 300px; - min-height: 0; -} -.rail, -.inspector { - background: var(--surface); - border-color: var(--border); - min-height: 0; - display: flex; - flex-direction: column; - overflow: hidden; -} -.rail { - border-right: 1px solid var(--border); -} -.inspector { - border-left: 1px solid var(--border); -} - -.rail-tabs { - display: flex; - border-bottom: 1px solid var(--border); - flex: none; -} -.rail-tab { - flex: 1; - text-align: center; - padding: 9px 4px; - font-size: 11.5px; - font-weight: 600; - color: var(--text-muted); - cursor: pointer; - border-bottom: 2px solid transparent; - background: none; - border-left: none; - border-right: none; - border-top: none; -} -.rail-tab.active { - color: var(--accent); - border-bottom-color: var(--accent); -} -.rail-body { - overflow-y: auto; - padding: 10px; - flex: 1; -} -.rail-section-title { - font-size: 10.5px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.6px; - color: var(--text-faint); - margin: 12px 4px 6px; -} -.rail-section-title:first-child { - margin-top: 2px; -} - -.agent-card { - border: 1px solid var(--border); - border-radius: 9px; - padding: 9px 10px; - margin-bottom: 7px; - cursor: pointer; - background: var(--surface); -} -.agent-card.selected { - border-color: var(--accent); - background: var(--accent-soft); -} -.agent-card-top { - display: flex; - align-items: center; - justify-content: space-between; - gap: 6px; -} -.agent-name { - font-weight: 600; - font-size: 12.5px; -} -.agent-meta { - color: var(--text-muted); - font-size: 11px; - margin-top: 3px; - font-family: var(--font-mono); - display: block; -} -.status-pill { - font-size: 10px; - font-weight: 700; - letter-spacing: 0.3px; - text-transform: uppercase; - padding: 2px 6px; - border-radius: 20px; - display: inline-flex; - align-items: center; - gap: 4px; - flex: none; -} -.status-pill .dot { - width: 5px; - height: 5px; - box-shadow: none; - /* match each status pill's own text color (success/danger/ - warning/faint, set by .status-running/.status-error/.status-paused/ - .status-stopped above) instead of always rendering the base `.dot` - rule's green - a status pill's dot should reflect that status. */ - background: currentColor; -} -.status-running, -.status-done { - background: var(--success-soft); - color: var(--success); -} -/* an unsaved agent is a warning, not a green "running". */ -.status-unsaved { - background: var(--warning-soft); - color: var(--warning); -} -.agent-card-main { - display: block; - width: 100%; - background: none; - border: none; - padding: 0; - margin: 0; - color: inherit; - font: inherit; - text-align: left; - cursor: pointer; -} -.agent-card-actions { - display: flex; - gap: 6px; - margin-top: 7px; -} -.agent-card-actions .btn { - padding: 3px 9px; - font-size: 11.5px; -} -.btn-danger-text { - color: var(--danger); -} -/* shown while the live stream to the studio server is down. */ -.connection-banner { - position: fixed; - top: 56px; - left: 50%; - transform: translateX(-50%); - z-index: 50; - max-width: calc(100vw - 32px); - background: var(--danger-soft); - color: var(--danger); - border: 1px solid var(--danger); - border-radius: 8px; - padding: 8px 14px; - font-size: 12.5px; - font-weight: 600; - box-shadow: var(--shadow); -} -.status-stopped, -.status-idle { - background: var(--surface-2); - color: var(--text-faint); -} -.status-error { - background: var(--danger-soft); - color: var(--danger); -} -.status-paused { - background: var(--warning-soft); - color: var(--warning); -} - -.node-palette-item { - display: flex; - align-items: center; - gap: 8px; - padding: 7px 8px; - border-radius: 8px; - border: 1px solid var(--border); - margin-bottom: 6px; - cursor: grab; - background: var(--surface); - font-size: 12px; - font-weight: 500; -} -/* palette items, hook switches/chips and toggles are native buttons. */ -button.node-palette-item { - width: 100%; - text-align: left; - color: var(--text); - font-family: inherit; -} -.rail-panel { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; -} -.palette-notice { - font-size: 10.5px; - color: var(--text-muted); - margin: 8px 4px 0; -} -button.switch { - padding: 0; -} -button.chip, -button.breakpoint-toggle { - color: var(--text); - cursor: pointer; -} -button.breakpoint-toggle { - font-family: inherit; -} -.hook-chip-select { - flex: 1; - min-width: 0; - display: flex; - align-items: center; - gap: 7px; - background: none; - border: none; - padding: 0; - color: inherit; - font: inherit; - text-align: left; - cursor: pointer; -} -.hook-chip-select .hook-chip-name { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.node-palette-item:focus-visible, -.switch:focus-visible, -.chip:focus-visible, -.breakpoint-toggle:focus-visible, -.hook-chip-select:focus-visible, -.rail-tab:focus-visible, -.drawer-tab:focus-visible, -.agent-card-main:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 1px; -} - -.node-swatch { - width: 9px; - height: 9px; - border-radius: 3px; - flex: none; -} - -/* ---------- Canvas (real ReactFlow / @xyflow/react) ---------- */ -.canvas-wrap { - position: relative; - overflow: hidden; - background: var(--bg); -} -.canvas-wrap .react-flow__background { - background: var(--bg); -} -.canvas-toolbar { - position: absolute; - left: 12px; - top: 12px; - z-index: 5; - display: flex; - gap: 6px; - background: var(--surface); - border: 1px solid var(--border); - border-radius: 9px; - padding: 4px; - box-shadow: var(--shadow); -} -.canvas-toolbar .btn { - padding: 5px 8px; -} -.canvas-connect-error { - position: absolute; - left: 50%; - top: 12px; - transform: translateX(-50%); - z-index: 6; - background: var(--danger-soft); - color: var(--danger); - border: 1px solid var(--danger); - border-radius: 8px; - padding: 6px 12px; - font-size: 11.5px; - font-weight: 600; - box-shadow: var(--shadow); -} - -/* ReactFlow's own MiniMap/Controls chrome, nudged to match the mockup - (minimap top-right, controls bottom-right) and re-themed with our tokens - rather than reimplementing either widget. */ -.react-flow__minimap { - background: var(--surface) !important; - border: 1px solid var(--border); - border-radius: 9px; - box-shadow: var(--shadow); -} -/* minimap nodes in their type's swatch colour (AgentNode.tsx's - miniMapNodeClassName), and a mask that is visible on --surface in both - schemes - the default light-grey nodes and mask vanished on it. */ -.react-flow__minimap-mask { - fill: color-mix(in srgb, var(--text) 10%, transparent); -} -.react-flow__minimap-node { - fill: var(--text-faint); - stroke: none; -} -.react-flow__minimap-node.minimap-node-trigger { - fill: var(--info); -} -.react-flow__minimap-node.minimap-node-llm { - fill: var(--accent); -} -.react-flow__minimap-node.minimap-node-tool, -.react-flow__minimap-node.minimap-node-approval { - fill: var(--warning); -} -.react-flow__minimap-node.minimap-node-router { - fill: var(--danger); -} -.react-flow__minimap-node.minimap-node-output { - fill: var(--success); -} -.react-flow__controls { - box-shadow: var(--shadow); - border-radius: 9px; - overflow: hidden; - border: 1px solid var(--border); -} -.react-flow__controls-button { - background: var(--surface); - border-bottom: 1px solid var(--border); - color: var(--text-muted); -} -.react-flow__controls-button:hover { - background: var(--surface-2); -} -.react-flow__controls-button svg { - fill: currentColor; -} -.react-flow__edge-path { - stroke: var(--text-faint); - stroke-width: 1.6; -} -.react-flow__edge.selected .react-flow__edge-path { - stroke: var(--accent); - stroke-width: 2; -} -.react-flow__attribution { +.shell-scrim { display: none; } -/* Custom node chrome (AgentNode.tsx), matching .node in the mockup. */ -.rf-node { - width: 190px; - border-radius: 11px; - background: var(--surface); - border: 1.5px solid var(--border); - box-shadow: var(--shadow); - font-family: var(--font-ui); -} -.rf-node.selected { - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-soft), var(--shadow); -} -.rf-node-head { - display: flex; - align-items: center; - gap: 7px; - padding: 8px 10px; - font-weight: 600; - font-size: 12px; - border-radius: 11px 11px 0 0; -} -.rf-node-head .node-swatch { - display: flex; - align-items: center; - justify-content: center; - font-size: 9px; -} -.rf-node-title { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.rf-node-body { - padding: 9px 10px; - font-size: 11px; - color: var(--text-muted); -} -.rf-node-body code { - font-family: var(--font-mono); - background: var(--surface-2); - padding: 1px 4px; - border-radius: 4px; - color: var(--text); -} -.rf-node .node-row { - display: flex; - justify-content: space-between; - gap: 6px; - margin-top: 3px; -} -.rf-node .node-row:first-child { - margin-top: 0; -} -.rf-port.rf-port-in, -.rf-port.rf-port-out { - width: 9px; - height: 9px; - border-radius: 50%; - background: var(--surface); - border: 2px solid var(--text-faint); -} -.rf-port.rf-port-out { - border-color: var(--accent); -} - -/* ---------- Inspector ---------- */ -.inspector-head { - padding: 11px 12px; - border-bottom: 1px solid var(--border); - flex: none; -} -.inspector-head .k { - font-size: 10.5px; - text-transform: uppercase; - letter-spacing: 0.5px; - color: var(--text-faint); - font-weight: 700; -} -.inspector-head .v { - font-weight: 700; - font-size: 13.5px; - margin-top: 2px; -} -.inspector-body { - overflow-y: auto; - padding: 12px; - flex: 1; -} -.field { - margin-bottom: 13px; -} -.field label { - display: block; - font-size: 11px; - font-weight: 600; - color: var(--text-muted); - margin-bottom: 5px; -} -.field .hint { - font-size: 10.5px; - color: var(--text-faint); - margin-top: 4px; -} -.field .hint.field-error { - color: var(--danger); -} -.input, -.select, -.textarea { - width: 100%; - background: var(--surface-2); - border: 1px solid var(--border); - color: var(--text); - border-radius: 7px; - padding: 7px 9px; - font-size: 12px; - font-family: var(--font-ui); -} -.textarea { - font-family: var(--font-mono); - font-size: 11.5px; - min-height: 64px; - resize: vertical; -} -.input:focus, -.select:focus, -.textarea:focus { - outline: 2px solid var(--accent); - outline-offset: -1px; -} -.row2 { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 8px; -} -.chip-list { - display: flex; - flex-wrap: wrap; - gap: 6px; -} -.chip { - display: inline-flex; - align-items: center; - gap: 5px; - border: 1px solid var(--border); - background: var(--surface-2); - border-radius: 20px; - padding: 4px 9px 4px 8px; - font-size: 11px; - font-family: var(--font-mono); -} -.chip.on { - border-color: var(--accent); - color: var(--accent); - background: var(--accent-soft); -} - -/* ---------- Hooks ---------- */ -.node-hooks { - position: absolute; - top: -7px; - left: 10px; - display: flex; - gap: 4px; -} -.hook-badge { - font-size: 8.5px; - font-weight: 700; - letter-spacing: 0.2px; - padding: 1px 5px; - border-radius: 20px; - font-family: var(--font-mono); - border: 1px solid var(--border); - background: var(--surface); - color: var(--text-muted); - display: inline-flex; - align-items: center; - gap: 2px; -} -.hook-badge.pre { - color: var(--info); - border-color: var(--info); -} -.hook-badge.post { - color: var(--accent); - border-color: var(--accent); -} -.switch { - width: 30px; - height: 17px; - border-radius: 20px; - background: var(--surface-2); - border: 1px solid var(--border); - position: relative; - cursor: pointer; - flex: none; -} -.switch.on { - background: var(--accent); - border-color: var(--accent); -} -.switch::after { - content: ''; - position: absolute; - width: 12px; - height: 12px; - border-radius: 50%; - background: #fff; - top: 1.5px; - left: 2px; - transition: left 0.12s; -} -.switch.on::after { - left: 15px; -} -.toggle-row { - display: flex; - align-items: center; - justify-content: space-between; - padding: 2px 0; -} -.hook-group { - margin-bottom: 10px; -} -.hook-group-label { - display: flex; - align-items: center; - gap: 6px; - font-size: 10.5px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.4px; - color: var(--text-faint); - margin-bottom: 6px; -} -.hook-group-label .hook-count { - font-family: var(--font-mono); - font-weight: 600; - color: var(--text-muted); - text-transform: none; - letter-spacing: 0; -} -.hook-chip { - display: flex; - align-items: center; - gap: 7px; - border: 1px solid var(--border); - background: var(--surface); - border-radius: 8px; - padding: 6px 8px; - margin-bottom: 5px; - cursor: pointer; - font-size: 11.5px; -} -.hook-chip.selected { - border-color: var(--accent); - background: var(--accent-soft); -} -.hook-chip .switch { - width: 24px; - height: 14px; -} -.hook-chip .switch::after { - width: 9px; - height: 9px; -} -.hook-chip .switch.on::after { - left: 12px; -} -.hook-chip-name { - font-family: var(--font-mono); - font-weight: 600; - flex: 1; -} -.hook-chip-when { - color: var(--text-faint); - font-size: 9.5px; - font-family: var(--font-mono); -} -.hook-code { - font-family: var(--font-mono); - font-size: 10.5px; - border: 1px solid var(--border); - border-radius: 7px; - overflow: hidden; - margin-top: 6px; -} -.btn-add-hook { - justify-content: center; - padding: 5px 9px; - font-size: 11px; -} - -/* ---------- Bottom drawer ---------- */ -.drawer { - grid-column: 1 / -1; - border-top: 1px solid var(--border); - background: var(--surface); - display: flex; - flex-direction: column; - min-height: 0; -} -.drawer-tabs { - display: flex; - align-items: center; - border-bottom: 1px solid var(--border); - flex: none; - padding: 0 6px; -} -.drawer-tab { - padding: 8px 12px; - font-size: 11.5px; - font-weight: 600; - color: var(--text-muted); - cursor: pointer; - border-bottom: 2px solid transparent; - display: flex; - align-items: center; - gap: 6px; - background: none; - border-left: none; - border-right: none; - border-top: none; -} -.drawer-tab.active { - color: var(--text); - border-bottom-color: var(--accent); -} -.drawer-tab .count { - background: var(--surface-2); - border-radius: 10px; - padding: 0 6px; - font-size: 10px; - font-family: var(--font-mono); - color: var(--text-faint); -} -.drawer-spacer { - flex: 1; -} -.drawer-body { - flex: 1; - overflow-y: auto; - font-family: var(--font-mono); - font-size: 11.5px; - padding: 8px 12px; - color: var(--text-muted); -} - -/* ---------- Logs tab ---------- */ -.logs-panel { - display: flex; - flex-direction: column; - height: 100%; -} -.logs-filter-bar { - display: flex; - align-items: center; - gap: 6px; - padding: 6px 8px; - border-bottom: 1px solid var(--border); - flex: none; - font-family: var(--font-ui); -} -.logs-filter-bar input[type='search'] { - flex: 1; - min-width: 100px; - background: var(--surface-2); - border: 1px solid var(--border); - color: var(--text); - border-radius: 6px; - padding: 4px 8px; - font-size: 11px; -} -.logs-filter-toggle { - border: 1px solid var(--border); - background: var(--surface-2); - color: var(--text-muted); - border-radius: 12px; - padding: 2px 8px; - font-size: 10.5px; - font-weight: 600; - cursor: pointer; -} -.logs-filter-toggle.on { - border-color: var(--accent); - color: var(--accent); - background: var(--accent-soft); -} -.logs-empty { - padding: 16px; - color: var(--text-faint); - font-family: var(--font-ui); -} -.logs-viewport { - flex: 1; - overflow-y: auto; - position: relative; -} -.log-line { display: grid; grid-template-columns: 64px 60px 1fr; gap: 10px; padding: 3px 12px; border-radius: 5px; align-items: start; cursor: pointer; } -.log-line:hover { background: var(--surface-2); } -.log-line.highlighted { background: var(--accent-soft); } -.log-time { color: var(--text-faint); } -.log-level { font-weight: 700; } -.lvl-info { color: var(--info); } -.lvl-warn { color: var(--warning); } -.lvl-error { color: var(--danger); } -.lvl-tool { color: var(--accent); } -.log-msg { color: var(--text); white-space: pre-wrap; word-break: break-word; } -.log-msg b { color: var(--text-muted); font-weight: 600; } - -/* ---------- Trace tab ---------- */ -.trace-panel { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - height: 100%; - overflow-y: auto; -} -.trace-empty { - padding: 16px; - color: var(--text-faint); - font-family: var(--font-ui); -} -.trace-row { display: flex; align-items: center; gap: 8px; padding: 5px 12px; cursor: pointer; border-radius: 5px; } -.trace-row:hover, .trace-row.selected { background: var(--surface-2); } -.trace-bar-track { position: relative; flex: 1; height: 16px; background: var(--surface-2); border-radius: 4px; overflow: hidden; } -.trace-bar { position: absolute; top: 0; bottom: 0; border-radius: 4px; background: var(--accent); } -.trace-bar.pending { background: repeating-linear-gradient(45deg, var(--accent), var(--accent) 4px, transparent 4px, transparent 8px); } -.trace-name { width: 150px; flex: none; color: var(--text); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.trace-dur { width: 56px; flex: none; text-align: right; color: var(--text-muted); } - -/* ---------- History tab (time travel) ---------- */ -.history-panel { display: flex; flex-direction: column; gap: 10px; } -.history-table { width: 100%; border-collapse: collapse; } -.history-table th { text-align: left; color: var(--text-faint); font-weight: 600; padding: 3px 8px; border-bottom: 1px solid var(--border); } -.history-table td { color: var(--text); padding: 3px 8px; border-bottom: 1px solid var(--border); } -.replay-editor { display: flex; flex-direction: column; gap: 6px; padding: 8px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface-2); } -.replay-actions { display: flex; gap: 6px; } -.traj-row { display: grid; grid-template-columns: 90px 1fr 1fr; gap: 10px; padding: 3px 8px; border-radius: 5px; } -.traj-row.traj-head { color: var(--text-faint); } -.traj-row.diverged { background: var(--warning-soft); } -.traj-row.diverged > span { color: var(--warning); font-weight: 700; } -.traj-cell { color: var(--text); white-space: pre-wrap; word-break: break-word; } -.traj-missing { color: var(--text-faint); } -.traj-tool { color: var(--accent); } -.traj-drift { margin: 6px 0 0; padding-left: 18px; color: var(--warning); } - -/* ---------- Output tab ---------- */ -.output-json { padding: 10px 14px; white-space: pre-wrap; word-break: break-word; font-family: var(--font-mono); font-size: 11.5px; } -.json-key { color: var(--info); } -.json-str { color: var(--success); } -.json-num { color: var(--warning); } -.json-bool { color: var(--accent); } -.json-null { color: var(--text-faint); } -.json-node { display: block; } -.json-toggle { - background: none; - border: none; - color: var(--text-faint); - cursor: pointer; - font-family: var(--font-mono); - padding: 0 3px 0 0; - font-size: 11px; -} -.json-children { - margin-left: 14px; - border-left: 1px dashed var(--border); - padding-left: 8px; -} - -/* ---------- Step-through debugger ---------- */ -.debug-bar { - display: flex; - align-items: center; - gap: 6px; - padding: 6px 10px; - background: var(--warning-soft); - border-bottom: 1px solid var(--border); - font-size: 11px; - font-family: var(--font-ui); -} -.debug-bar .step-count { - color: var(--text-muted); - font-family: var(--font-mono); -} -.breakpoint-toggle { - display: inline-flex; - align-items: center; - gap: 5px; - border: 1px solid var(--border); - background: var(--surface-2); - border-radius: 20px; - padding: 4px 9px; - font-size: 11px; - cursor: pointer; -} -.breakpoint-toggle.on { - border-color: var(--danger); - color: var(--danger); - background: var(--danger-soft); -} -.rf-node.has-breakpoint { - box-shadow: 0 0 0 2px var(--danger); -} -.rf-node.highlighted { - box-shadow: 0 0 0 3px var(--accent), var(--shadow); -} - -/* ---------- narrow layout (<= 860px) ---------- - The rail and the Inspector become slide-over drawers toggled from the top - bar (App.tsx's useSidePanels, Topbar.tsx's NarrowTopbar); Debug, Import, - Export and Save move into the top bar's "More" menu. Keep the breakpoint in - sync with useMediaQuery.ts's NARROW_QUERY. */ -.overflow-menu { - position: relative; - flex: none; -} -.overflow-menu-list { - position: absolute; - right: 0; - top: calc(100% + 6px); - z-index: 30; - min-width: 210px; - background: var(--surface); - border: 1px solid var(--border); - border-radius: 9px; - box-shadow: var(--shadow); - padding: 4px; - display: flex; - flex-direction: column; -} -.overflow-menu-env { - padding: 4px 4px 6px; - border-bottom: 1px solid var(--border); - margin-bottom: 4px; -} -.overflow-menu-env .env-select { - flex-wrap: wrap; -} -.overflow-menu-item { - text-align: left; - background: none; - border: none; - border-radius: 6px; - padding: 8px 10px; - font-size: 12.5px; - font-weight: 600; - color: var(--text); - cursor: pointer; -} -.overflow-menu-item:hover, -.overflow-menu-item:focus-visible { - background: var(--surface-2); - outline: 2px solid var(--accent); - outline-offset: -2px; -} -.panel-toggle { - flex: none; - padding: 6px 9px; - font-size: 15px; - line-height: 1; -} -.panel-toggle.active { - background: var(--accent-soft); - color: var(--accent); -} -.topbar-narrow { - gap: 6px; - padding: 0 8px; -} -.topbar-narrow .crumbs { - flex: 1; -} -.topbar-narrow .crumbs b { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - min-width: 0; -} -.topbar-narrow .crumbs .mock-badge, -.topbar-narrow .crumbs .dirty-dot { - flex: none; -} -.topbar-narrow .run-error { - max-width: 90px; -} -/* The Chat tab shows the same approval card inline. */ -.topbar-narrow .approval-card { - display: none; -} -.shell-scrim { - display: none; -} - -@media (max-width: 860px) { - .shell { - grid-template-columns: minmax(0, 1fr); - overflow: hidden; - position: relative; - } - .rail, - .inspector { - position: absolute; - top: 0; - bottom: 0; - z-index: 20; - width: min(300px, 86vw); - box-shadow: var(--shadow); - visibility: hidden; - transition: transform 0.18s ease, visibility 0s linear 0.18s; - } - .rail { - left: 0; - transform: translateX(-100%); - } - .inspector { - right: 0; - transform: translateX(100%); - } - .shell.rail-open .rail, - .shell.inspector-open .inspector { - transform: none; - visibility: visible; - transition: transform 0.18s ease; - } - .shell-scrim { - display: block; - position: absolute; - inset: 0; - z-index: 15; - background: rgba(10, 12, 18, 0.35); - } - .canvas-wrap .react-flow__minimap { - display: none; - } - .drawer-tabs { - overflow-x: auto; - } - .drawer-body { - overflow-x: auto; - } - .chat-msg { - max-width: 92%; - } -} - -/* ---------- Chat ---------- - Ported from .design-ref/agent-forge-mockup.html's `.chat-*` classes - - user/agent bubbles, avatars, timestamps, a typing indicator, and inline - tool-call/approval cards under an agent bubble. See ChatPanel.tsx. */ -.chat-panel { - display: flex; - flex-direction: column; - height: 100%; - font-family: var(--font-ui); -} -.chat-session-bar { - display: flex; - align-items: center; - gap: 8px; - padding: 6px 10px; - border-bottom: 1px solid var(--border); - font-size: 11.5px; - color: var(--text-muted); -} -.chat-session-bar select { - font-size: 11.5px; - border: 1px solid var(--border); - border-radius: 6px; - background: var(--surface-2); - color: var(--text); - padding: 3px 6px; - max-width: 260px; -} -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 10px 14px; - display: flex; - flex-direction: column; - gap: 10px; -} -.chat-empty { - color: var(--text-faint); - font-size: 12.5px; - margin: auto; -} -.chat-msg { - display: flex; - gap: 8px; - max-width: 72%; -} -.chat-msg.user { - align-self: flex-end; - flex-direction: row-reverse; -} -.chat-avatar { - flex: none; - width: 24px; - height: 24px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: 9.5px; - font-weight: 700; - color: #fff; -} -.chat-msg.agent .chat-avatar { - background: var(--accent); -} -.chat-msg.user .chat-avatar { - background: var(--text-faint); -} -.chat-bubble { - background: var(--surface-2); - border: 1px solid var(--border); - border-radius: 12px; - padding: 7px 10px; - font-size: 12.5px; - line-height: 1.4; - white-space: pre-wrap; -} -.chat-msg.user .chat-bubble { - background: var(--accent); - border-color: var(--accent); - color: #fff; -} -.chat-msg.agent .chat-bubble { - border-top-left-radius: 3px; -} -.chat-msg.user .chat-bubble { - border-top-right-radius: 3px; -} -.chat-bubble .chat-tool-call { - margin-top: 4px; - font-family: var(--font-mono); - font-size: 11px; - opacity: 0.85; -} -.chat-tool-result { - margin-top: 2px; - opacity: 0.75; -} -.chat-time { - font-size: 9.5px; - color: var(--text-faint); - margin-top: 3px; - font-family: var(--font-mono); -} -.chat-msg.user .chat-time { - text-align: right; -} -/* the top bar's Run input, kept apart from typed messages. */ -.chat-msg.run-input .chat-bubble { - background: var(--surface-2); - border: 1px dashed var(--border); - color: var(--text-muted); - font-family: var(--font-mono); - font-size: 11.5px; -} -.chat-msg.run-input .chat-avatar { - background: var(--text-muted); - font-size: 8.5px; -} -.chat-run-label { - font-size: 10px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.4px; - color: var(--text-faint); - text-align: right; - margin-bottom: 3px; -} -.chat-typing { - display: flex; - gap: 3px; - align-items: center; - padding: 2px 2px; -} -.chat-typing span { - width: 5px; - height: 5px; - border-radius: 50%; - background: var(--text-faint); - animation: typing-bounce 1.1s infinite ease-in-out; -} -.chat-typing span:nth-child(2) { - animation-delay: 0.15s; -} -.chat-typing span:nth-child(3) { - animation-delay: 0.3s; -} -@keyframes typing-bounce { - 0%, - 60%, - 100% { - transform: translateY(0); - opacity: 0.5; - } - 30% { - transform: translateY(-3px); - opacity: 1; - } -} -/* Inline-in-thread approval card - reuses .approval-card's base look - (Topbar.tsx's compact version) with a couple of thread-specific tweaks. */ -.chat-approval-card { - flex-wrap: wrap; -} -.chat-action-error { - padding: 4px 12px; - font-size: 11.5px; - color: var(--danger); -} -.chat-input-row { - display: flex; - gap: 8px; - align-items: flex-end; - padding: 8px 10px; - border-top: 1px solid var(--border); -} -.chat-input { - flex: 1; - resize: none; - max-height: 90px; - border: 1px solid var(--border); - border-radius: 8px; - background: var(--surface-2); - color: var(--text); - font-family: var(--font-ui); - font-size: 12.5px; - padding: 7px 9px; -} -.chat-input:focus { - outline: 2px solid var(--accent); - outline-offset: -1px; -} -.chat-input:disabled { - opacity: 0.6; -} - -/* Persisted trace history next to the waterfall */ -.trace-layout { display: flex; height: 100%; min-height: 0; } -.trace-main { flex: 1; min-width: 0; height: 100%; } -.trace-history { display: flex; flex-direction: column; gap: 4px; width: 240px; flex: none; padding: 8px; overflow-y: auto; border-right: 1px solid var(--border); } -.trace-item { display: flex; flex-direction: column; gap: 2px; text-align: left; padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px; background: transparent; color: var(--text); font: inherit; font-size: 12px; cursor: pointer; } -.trace-item:hover, .trace-item.selected { background: var(--surface-2); } -.trace-item.selected { border-color: var(--accent); } -.trace-item-head { display: flex; justify-content: space-between; gap: 8px; } -.trace-item-meta { color: var(--text-muted); } -.trace-item-error .trace-item-status, .trace-history-error { color: var(--danger); } -.trace-history-note { padding: 4px; color: var(--text-faint); font-size: 12px; } -.trace-kind { flex: none; color: var(--text-faint); font-size: 11px; } -.trace-bar.error { background: var(--danger); } -.trace-row-error .trace-name { color: var(--danger); } -@media (max-width: 700px) { - .trace-layout { flex-direction: column; } - .trace-history { width: auto; flex-direction: row; overflow-x: auto; overflow-y: hidden; border-right: 0; border-bottom: 1px solid var(--border); } - .trace-item { min-width: 200px; } -} - /* top-level error boundary fallback. */ .error-boundary { max-width: 640px; diff --git a/apps/agent-forge/src/components/topbarNarrow.css b/apps/agent-forge/src/components/topbarNarrow.css new file mode 100644 index 000000000..9910d87e0 --- /dev/null +++ b/apps/agent-forge/src/components/topbarNarrow.css @@ -0,0 +1,82 @@ +/* ---------- narrow layout (<= 860px) ---------- + The rail and the Inspector become slide-over drawers toggled from the top + bar (App.tsx's useSidePanels, Topbar.tsx's NarrowTopbar); Debug, Import, + Export and Save move into the top bar's "More" menu. Keep the breakpoint in + sync with useMediaQuery.ts's NARROW_QUERY. */ +.overflow-menu { + position: relative; + flex: none; +} +.overflow-menu-list { + position: absolute; + right: 0; + top: calc(100% + 6px); + z-index: 30; + min-width: 210px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 9px; + box-shadow: var(--shadow); + padding: 4px; + display: flex; + flex-direction: column; +} +.overflow-menu-env { + padding: 4px 4px 6px; + border-bottom: 1px solid var(--border); + margin-bottom: 4px; +} +.overflow-menu-env .env-select { + flex-wrap: wrap; +} +.overflow-menu-item { + text-align: left; + background: none; + border: none; + border-radius: 6px; + padding: 8px 10px; + font-size: 12.5px; + font-weight: 600; + color: var(--text); + cursor: pointer; +} +.overflow-menu-item:hover, +.overflow-menu-item:focus-visible { + background: var(--surface-2); + outline: 2px solid var(--accent); + outline-offset: -2px; +} +.panel-toggle { + flex: none; + padding: 6px 9px; + font-size: 15px; + line-height: 1; +} +.panel-toggle.active { + background: var(--accent-soft); + color: var(--accent); +} +.topbar-narrow { + gap: 6px; + padding: 0 8px; +} +.topbar-narrow .crumbs { + flex: 1; +} +.topbar-narrow .crumbs b { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} +.topbar-narrow .crumbs .mock-badge, +.topbar-narrow .crumbs .dirty-dot { + flex: none; +} +.topbar-narrow .run-error { + max-width: 90px; +} +/* The Chat tab shows the same approval card inline. */ +.topbar-narrow .approval-card { + display: none; +} diff --git a/apps/agent-forge/src/components/useMediaQuery.ts b/apps/agent-forge/src/components/useMediaQuery.ts index 93a892a20..9719f39fe 100644 --- a/apps/agent-forge/src/components/useMediaQuery.ts +++ b/apps/agent-forge/src/components/useMediaQuery.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; -/** Below this width the rail and Inspector become slide-over drawers. Keep in sync with layout.css. */ +/** Below this width the rail and Inspector become slide-over drawers. Keep in sync with the 860px blocks in screens/agents/agents.css and components/drawer.css. */ export const NARROW_QUERY = '(max-width: 860px)'; function matches(query: string): boolean { diff --git a/apps/agent-forge/src/observe/observe.css b/apps/agent-forge/src/observe/observe.css index 060b319a3..4bc5075ad 100644 --- a/apps/agent-forge/src/observe/observe.css +++ b/apps/agent-forge/src/observe/observe.css @@ -1,6 +1,6 @@ /* * Observe view (Traces / Insights / Monitoring / Scores) - styles for the - * panels reading the observe daemon. Reuses layout.css's primitives (.btn, + * panels reading the observe daemon. Reuses ui/base.css's primitives (.btn, * .select, .status-pill, .trace-row/.trace-bar waterfall, .agent-card) and * only adds what the panels themselves need. */ diff --git a/apps/agent-forge/src/screens/agents/agents.css b/apps/agent-forge/src/screens/agents/agents.css new file mode 100644 index 000000000..96b1f14be --- /dev/null +++ b/apps/agent-forge/src/screens/agents/agents.css @@ -0,0 +1,549 @@ +/* Agents screen: shell, rail, canvas, Inspector, hooks and the narrow slide-over layout. */ + +/* ---------- Shell ---------- */ +.shell { + display: grid; + grid-template-columns: 236px 1fr 300px; + min-height: 0; +} +.rail, +.inspector { + background: var(--surface); + border-color: var(--border); + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} +.rail { + border-right: 1px solid var(--border); +} +.inspector { + border-left: 1px solid var(--border); +} + +.rail-tabs { + display: flex; + border-bottom: 1px solid var(--border); + flex: none; +} +.rail-tab { + flex: 1; + text-align: center; + padding: 9px 4px; + font-size: 11.5px; + font-weight: 600; + color: var(--text-muted); + cursor: pointer; + border-bottom: 2px solid transparent; + background: none; + border-left: none; + border-right: none; + border-top: none; +} +.rail-tab.active { + color: var(--accent); + border-bottom-color: var(--accent); +} +.rail-body { + overflow-y: auto; + padding: 10px; + flex: 1; +} +.rail-section-title { + font-size: 10.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.6px; + color: var(--text-faint); + margin: 12px 4px 6px; +} +.rail-section-title:first-child { + margin-top: 2px; +} + +.agent-card { + border: 1px solid var(--border); + border-radius: 9px; + padding: 9px 10px; + margin-bottom: 7px; + cursor: pointer; + background: var(--surface); +} +.agent-card.selected { + border-color: var(--accent); + background: var(--accent-soft); +} +.agent-card-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; +} +.agent-name { + font-weight: 600; + font-size: 12.5px; +} +.agent-meta { + color: var(--text-muted); + font-size: 11px; + margin-top: 3px; + font-family: var(--font-mono); + display: block; +} + +.agent-card-main { + display: block; + width: 100%; + background: none; + border: none; + padding: 0; + margin: 0; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.agent-card-actions { + display: flex; + gap: 6px; + margin-top: 7px; +} +.agent-card-actions .btn { + padding: 3px 9px; + font-size: 11.5px; +} + +/* shown while the live stream to the studio server is down. */ +.connection-banner { + position: fixed; + top: 56px; + left: 50%; + transform: translateX(-50%); + z-index: 50; + max-width: calc(100vw - 32px); + background: var(--danger-soft); + color: var(--danger); + border: 1px solid var(--danger); + border-radius: 8px; + padding: 8px 14px; + font-size: 12.5px; + font-weight: 600; + box-shadow: var(--shadow); +} + +.node-palette-item { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 8px; + border-radius: 8px; + border: 1px solid var(--border); + margin-bottom: 6px; + cursor: grab; + background: var(--surface); + font-size: 12px; + font-weight: 500; +} +/* palette items, hook switches/chips and toggles are native buttons. */ +button.node-palette-item { + width: 100%; + text-align: left; + color: var(--text); + font-family: inherit; +} +.rail-panel { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; +} +.palette-notice { + font-size: 10.5px; + color: var(--text-muted); + margin: 8px 4px 0; +} +button.switch { + padding: 0; +} +button.chip, +button.breakpoint-toggle { + color: var(--text); + cursor: pointer; +} +button.breakpoint-toggle { + font-family: inherit; +} +.hook-chip-select { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 7px; + background: none; + border: none; + padding: 0; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.hook-chip-select .hook-chip-name { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.node-palette-item:focus-visible, +.switch:focus-visible, +.chip:focus-visible, +.breakpoint-toggle:focus-visible, +.hook-chip-select:focus-visible, +.rail-tab:focus-visible, +.drawer-tab:focus-visible, +.agent-card-main:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 1px; +} + + +/* ---------- Canvas (real ReactFlow / @xyflow/react) ---------- */ +.canvas-wrap { + position: relative; + overflow: hidden; + background: var(--bg); +} +.canvas-wrap .react-flow__background { + background: var(--bg); +} +.canvas-toolbar { + position: absolute; + left: 12px; + top: 12px; + z-index: 5; + display: flex; + gap: 6px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 9px; + padding: 4px; + box-shadow: var(--shadow); +} +.canvas-toolbar .btn { + padding: 5px 8px; +} +.canvas-connect-error { + position: absolute; + left: 50%; + top: 12px; + transform: translateX(-50%); + z-index: 6; + background: var(--danger-soft); + color: var(--danger); + border: 1px solid var(--danger); + border-radius: 8px; + padding: 6px 12px; + font-size: 11.5px; + font-weight: 600; + box-shadow: var(--shadow); +} + +/* ReactFlow's own MiniMap/Controls chrome, nudged to match the mockup + (minimap top-right, controls bottom-right) and re-themed with our tokens + rather than reimplementing either widget. */ +.react-flow__minimap { + background: var(--surface) !important; + border: 1px solid var(--border); + border-radius: 9px; + box-shadow: var(--shadow); +} +/* minimap nodes in their type's swatch colour (AgentNode.tsx's + miniMapNodeClassName), and a mask that is visible on --surface in both + schemes - the default light-grey nodes and mask vanished on it. */ +.react-flow__minimap-mask { + fill: color-mix(in srgb, var(--text) 10%, transparent); +} +.react-flow__minimap-node { + fill: var(--text-faint); + stroke: none; +} +.react-flow__minimap-node.minimap-node-trigger { + fill: var(--info); +} +.react-flow__minimap-node.minimap-node-llm { + fill: var(--accent); +} +.react-flow__minimap-node.minimap-node-tool, +.react-flow__minimap-node.minimap-node-approval { + fill: var(--warning); +} +.react-flow__minimap-node.minimap-node-router { + fill: var(--danger); +} +.react-flow__minimap-node.minimap-node-output { + fill: var(--success); +} +.react-flow__controls { + box-shadow: var(--shadow); + border-radius: 9px; + overflow: hidden; + border: 1px solid var(--border); +} +.react-flow__controls-button { + background: var(--surface); + border-bottom: 1px solid var(--border); + color: var(--text-muted); +} +.react-flow__controls-button:hover { + background: var(--surface-2); +} +.react-flow__controls-button svg { + fill: currentColor; +} +.react-flow__edge-path { + stroke: var(--text-faint); + stroke-width: 1.6; +} +.react-flow__edge.selected .react-flow__edge-path { + stroke: var(--accent); + stroke-width: 2; +} +.react-flow__attribution { + display: none; +} + +/* Custom node chrome (AgentNode.tsx), matching .node in the mockup. */ +.rf-node { + width: 190px; + border-radius: 11px; + background: var(--surface); + border: 1.5px solid var(--border); + box-shadow: var(--shadow); + font-family: var(--font-ui); +} +.rf-node.selected { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft), var(--shadow); +} +.rf-node-head { + display: flex; + align-items: center; + gap: 7px; + padding: 8px 10px; + font-weight: 600; + font-size: 12px; + border-radius: 11px 11px 0 0; +} +.rf-node-head .node-swatch { + display: flex; + align-items: center; + justify-content: center; + font-size: 9px; +} +.rf-node-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.rf-node-body { + padding: 9px 10px; + font-size: 11px; + color: var(--text-muted); +} +.rf-node-body code { + font-family: var(--font-mono); + background: var(--surface-2); + padding: 1px 4px; + border-radius: 4px; + color: var(--text); +} +.rf-node .node-row { + display: flex; + justify-content: space-between; + gap: 6px; + margin-top: 3px; +} +.rf-node .node-row:first-child { + margin-top: 0; +} +.rf-port.rf-port-in, +.rf-port.rf-port-out { + width: 9px; + height: 9px; + border-radius: 50%; + background: var(--surface); + border: 2px solid var(--text-faint); +} +.rf-port.rf-port-out { + border-color: var(--accent); +} + +/* ---------- Inspector ---------- */ +.inspector-head { + padding: 11px 12px; + border-bottom: 1px solid var(--border); + flex: none; +} +.inspector-head .k { + font-size: 10.5px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-faint); + font-weight: 700; +} +.inspector-head .v { + font-weight: 700; + font-size: 13.5px; + margin-top: 2px; +} +.inspector-body { + overflow-y: auto; + padding: 12px; + flex: 1; +} + +/* ---------- Hooks ---------- */ +.node-hooks { + position: absolute; + top: -7px; + left: 10px; + display: flex; + gap: 4px; +} +.hook-badge { + font-size: 8.5px; + font-weight: 700; + letter-spacing: 0.2px; + padding: 1px 5px; + border-radius: 20px; + font-family: var(--font-mono); + border: 1px solid var(--border); + background: var(--surface); + color: var(--text-muted); + display: inline-flex; + align-items: center; + gap: 2px; +} +.hook-badge.pre { + color: var(--info); + border-color: var(--info); +} +.hook-badge.post { + color: var(--accent); + border-color: var(--accent); +} + +.hook-group { + margin-bottom: 10px; +} +.hook-group-label { + display: flex; + align-items: center; + gap: 6px; + font-size: 10.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.4px; + color: var(--text-faint); + margin-bottom: 6px; +} +.hook-group-label .hook-count { + font-family: var(--font-mono); + font-weight: 600; + color: var(--text-muted); + text-transform: none; + letter-spacing: 0; +} +.hook-chip { + display: flex; + align-items: center; + gap: 7px; + border: 1px solid var(--border); + background: var(--surface); + border-radius: 8px; + padding: 6px 8px; + margin-bottom: 5px; + cursor: pointer; + font-size: 11.5px; +} +.hook-chip.selected { + border-color: var(--accent); + background: var(--accent-soft); +} +.hook-chip .switch { + width: 24px; + height: 14px; +} +.hook-chip .switch::after { + width: 9px; + height: 9px; +} +.hook-chip .switch.on::after { + left: 12px; +} +.hook-chip-name { + font-family: var(--font-mono); + font-weight: 600; + flex: 1; +} +.hook-chip-when { + color: var(--text-faint); + font-size: 9.5px; + font-family: var(--font-mono); +} +.hook-code { + font-family: var(--font-mono); + font-size: 10.5px; + border: 1px solid var(--border); + border-radius: 7px; + overflow: hidden; + margin-top: 6px; +} +.btn-add-hook { + justify-content: center; + padding: 5px 9px; + font-size: 11px; +} + +@media (max-width: 860px) { + .shell { + grid-template-columns: minmax(0, 1fr); + overflow: hidden; + position: relative; + } + .rail, + .inspector { + position: absolute; + top: 0; + bottom: 0; + z-index: 20; + width: min(300px, 86vw); + box-shadow: var(--shadow); + visibility: hidden; + transition: transform 0.18s ease, visibility 0s linear 0.18s; + } + .rail { + left: 0; + transform: translateX(-100%); + } + .inspector { + right: 0; + transform: translateX(100%); + } + .shell.rail-open .rail, + .shell.inspector-open .inspector { + transform: none; + visibility: visible; + transition: transform 0.18s ease; + } + .shell-scrim { + display: block; + position: absolute; + inset: 0; + z-index: 15; + background: rgba(10, 12, 18, 0.35); + } + .canvas-wrap .react-flow__minimap { + display: none; + } +} diff --git a/apps/agent-forge/src/ui/base.css b/apps/agent-forge/src/ui/base.css new file mode 100644 index 000000000..81bddf4d5 --- /dev/null +++ b/apps/agent-forge/src/ui/base.css @@ -0,0 +1,235 @@ +/* + * Shared primitives used by more than one screen (buttons, status pills, form + * fields, chips, switches). Moved out of layout.css unchanged; imported before + * layout.css so the chrome, Agents and drawer rules keep their cascade position. + */ + +/* read by screen readers, not drawn (the page h1, status prefixes). */ +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--success); + box-shadow: 0 0 0 3px var(--success-soft); +} + +.btn { + border: 1px solid var(--border); + background: var(--surface-2); + color: var(--text); + border-radius: 7px; + padding: 6px 12px; + font-size: 12.5px; + font-weight: 600; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 6px; + transition: filter 0.12s, transform 0.04s; +} +.btn:hover { + filter: brightness(1.05); +} +.btn:active { + transform: translateY(1px); +} +.btn:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 1px; +} +.btn-primary { + background: var(--accent-solid); + border-color: var(--accent-solid); + color: var(--on-accent); +} +.btn-success { + background: var(--success-soft); + border-color: var(--success); + color: var(--success); +} +.btn-danger { + background: var(--danger-soft); + border-color: var(--danger); + color: var(--danger); +} +.btn-ghost { + background: transparent; +} +.btn[disabled] { + opacity: 0.4; + cursor: not-allowed; +} + +.status-pill { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.3px; + text-transform: uppercase; + padding: 2px 6px; + border-radius: 20px; + display: inline-flex; + align-items: center; + gap: 4px; + flex: none; +} +.status-pill .dot { + width: 5px; + height: 5px; + box-shadow: none; + /* match each status pill's own text color (success/danger/ + warning/faint, set by .status-running/.status-error/.status-paused/ + .status-stopped above) instead of always rendering the base `.dot` + rule's green - a status pill's dot should reflect that status. */ + background: currentColor; +} +.status-running, +.status-done { + background: var(--success-soft); + color: var(--success); +} +/* an unsaved agent is a warning, not a green "running". */ +.status-unsaved { + background: var(--warning-soft); + color: var(--warning); +} + +.btn-danger-text { + color: var(--danger); +} + +.status-stopped, +.status-idle { + background: var(--surface-2); + color: var(--text-faint); +} +.status-error { + background: var(--danger-soft); + color: var(--danger); +} +.status-paused { + background: var(--warning-soft); + color: var(--warning); +} + +.node-swatch { + width: 9px; + height: 9px; + border-radius: 3px; + flex: none; +} + +.field { + margin-bottom: 13px; +} +.field label { + display: block; + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + margin-bottom: 5px; +} +.field .hint { + font-size: 10.5px; + color: var(--text-faint); + margin-top: 4px; +} +.field .hint.field-error { + color: var(--danger); +} +.input, +.select, +.textarea { + width: 100%; + background: var(--surface-2); + border: 1px solid var(--border); + color: var(--text); + border-radius: 7px; + padding: 7px 9px; + font-size: 12px; + font-family: var(--font-ui); +} +.textarea { + font-family: var(--font-mono); + font-size: 11.5px; + min-height: 64px; + resize: vertical; +} +.input:focus, +.select:focus, +.textarea:focus { + outline: 2px solid var(--accent); + outline-offset: -1px; +} +.row2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} +.chip-list { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.chip { + display: inline-flex; + align-items: center; + gap: 5px; + border: 1px solid var(--border); + background: var(--surface-2); + border-radius: 20px; + padding: 4px 9px 4px 8px; + font-size: 11px; + font-family: var(--font-mono); +} +.chip.on { + border-color: var(--accent); + color: var(--accent); + background: var(--accent-soft); +} + +.switch { + width: 30px; + height: 17px; + border-radius: 20px; + background: var(--surface-2); + border: 1px solid var(--border); + position: relative; + cursor: pointer; + flex: none; +} +.switch.on { + background: var(--accent); + border-color: var(--accent); +} +.switch::after { + content: ''; + position: absolute; + width: 12px; + height: 12px; + border-radius: 50%; + background: #fff; + top: 1.5px; + left: 2px; + transition: left 0.12s; +} +.switch.on::after { + left: 15px; +} +.toggle-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 2px 0; +} diff --git a/apps/agent-forge/src/ui/primitives.css b/apps/agent-forge/src/ui/primitives.css index 2001d2594..85fd0530a 100644 --- a/apps/agent-forge/src/ui/primitives.css +++ b/apps/agent-forge/src/ui/primitives.css @@ -1,6 +1,6 @@ /* * Studio kit primitives: StatusPill tones, EmptyState, Toast. - * Base pill shape (.status-pill, .dot) lives in components/layout.css; the + * Base pill shape (.status-pill, .dot) lives in ui/base.css; the * tone classes below decide its colour. Two-class selectors so they beat the * single-class legacy .status-* rules regardless of load order. */