Merge pull request #856 from ComposioHQ/ashish921998/design-sync-pr

Align web UI to design system
This commit is contained in:
Ashish Huddar 2026-04-01 21:57:57 +05:30 committed by GitHub
commit 508e7b381e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 440 additions and 121 deletions

View File

@ -520,11 +520,13 @@ describe("SessionCard", () => {
expect(onSend).toHaveBeenCalledTimes(1);
});
expect(screen.queryByRole("button", { name: "Sent" })).not.toBeInTheDocument();
expect(screen.getByRole("textbox", { name: /type a reply to the agent/i })).toHaveValue(
"please continue",
);
expect(screen.getByRole("textbox", { name: /type a reply to the agent/i })).not.toBeDisabled();
await waitFor(() => {
expect(screen.queryByRole("button", { name: "Sent" })).not.toBeInTheDocument();
expect(screen.getByRole("textbox", { name: /type a reply to the agent/i })).toHaveValue(
"please continue",
);
expect(screen.getByRole("textbox", { name: /type a reply to the agent/i })).not.toBeDisabled();
});
});
it("shows a temporary failed state when an alert action send is rejected", async () => {

View File

@ -0,0 +1,70 @@
import { render, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
let searchParams = new URLSearchParams();
vi.mock("next/navigation", () => ({
useSearchParams: () => searchParams,
}));
vi.mock("@/components/DirectTerminal", () => ({
DirectTerminal: ({ sessionId }: { sessionId: string }) => (
<div data-testid={`direct-terminal-${sessionId}`}>{sessionId}</div>
),
}));
vi.mock("@/components/Terminal", () => ({
Terminal: ({ sessionId }: { sessionId: string }) => (
<div data-testid={`legacy-terminal-${sessionId}`}>{sessionId}</div>
),
}));
describe("TerminalTestPage", () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it("renders the terminal comparison and documentation content", async () => {
searchParams = new URLSearchParams();
vi.stubGlobal(
"fetch",
vi.fn(async () => ({
json: async () => ({
sessions: [{ id: "ao-orchestrator" }, { id: "ao-20" }],
}),
})),
);
const { default: TerminalTestPage } = await import("./page");
render(<TerminalTestPage />);
expect(screen.getByText("Terminal Implementation Test & Documentation")).toBeInTheDocument();
expect(screen.getByText(/Root Cause Analysis/i)).toBeInTheDocument();
expect(screen.getByText(/The Debugging Journey/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /hide side-by-side comparison/i })).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByTestId("legacy-terminal-ao-orchestrator")).toBeInTheDocument();
expect(screen.getByTestId("direct-terminal-ao-20")).toBeInTheDocument();
});
});
it("shows the same-session warning when both panes target one session", async () => {
searchParams = new URLSearchParams("session=ao-20");
vi.stubGlobal(
"fetch",
vi.fn(async () => ({
json: async () => ({
sessions: [{ id: "ao-20" }],
}),
})),
);
const { default: TerminalTestPage } = await import("./page");
render(<TerminalTestPage />);
expect(screen.getByText(/Using same session for both terminals/i)).toBeInTheDocument();
expect(screen.getByText("?old_session=ao-orchestrator&new_session=ao-20")).toBeInTheDocument();
});
});

View File

@ -76,7 +76,7 @@ function TerminalTestPageContent() {
</div>
{/* The Problem */}
<section className="mb-8 rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-6">
<section className="mb-8 rounded-[2px] border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-6">
<h2 className="mb-4 text-xl font-bold text-[var(--color-text-primary)]">
🐛 The Problem
</h2>
@ -97,7 +97,7 @@ function TerminalTestPageContent() {
</section>
{/* Root Cause */}
<section className="mb-8 rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-6">
<section className="mb-8 rounded-[2px] border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-6">
<h2 className="mb-4 text-xl font-bold text-[var(--color-text-primary)]">
🔍 Root Cause Analysis
</h2>
@ -110,7 +110,7 @@ function TerminalTestPageContent() {
<li>tmux uses OSC 52 escape sequences to synchronize clipboard with terminals</li>
<li>
Format:{" "}
<code className="rounded bg-black px-1 py-0.5">\x1b]52;c;&lt;base64&gt;\x07</code>
<code className="rounded-[2px] bg-black px-1 py-0.5">\x1b]52;c;&lt;base64&gt;\x07</code>
</li>
<li>Terminal must support OSC 52 and have proper capabilities declared</li>
</ul>
@ -123,7 +123,7 @@ function TerminalTestPageContent() {
<ul className="ml-6 list-disc space-y-1 text-[var(--color-text-secondary)]">
<li>tmux queries terminal capabilities using Device Attributes (DA/XDA)</li>
<li>
XDA query: <code className="rounded bg-black px-1 py-0.5">CSI &gt; q</code> (also
XDA query: <code className="rounded-[2px] bg-black px-1 py-0.5">CSI &gt; q</code> (also
called XTVERSION)
</li>
<li>
@ -138,14 +138,14 @@ function TerminalTestPageContent() {
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">
3. The Missing Piece
</h3>
<div className="rounded-lg border border-[var(--color-accent-red)] bg-[var(--color-bg-tertiary)] p-4">
<div className="rounded-[2px] border border-[var(--color-accent-red)] bg-[var(--color-bg-tertiary)] p-4">
<p className="font-semibold text-[var(--color-accent-red)]">
xterm.js does NOT implement XDA (Extended Device Attributes)
</p>
<ul className="ml-6 mt-2 list-disc space-y-1 text-[var(--color-text-secondary)]">
<li>
XDA is marked as TODO in xterm.js codebase:{" "}
<code className="rounded bg-black px-1 py-0.5">
<code className="rounded-[2px] bg-black px-1 py-0.5">
test.skip('CSI &gt; Ps q - Report xterm name and version (XTVERSION)')
</code>
</li>
@ -164,7 +164,7 @@ function TerminalTestPageContent() {
<ul className="ml-6 list-disc space-y-1 text-[var(--color-text-secondary)]">
<li>
iTerm2 sends proper XDA response identifying itself as{" "}
<code className="rounded bg-black px-1 py-0.5">"iTerm2 "</code>
<code className="rounded-[2px] bg-black px-1 py-0.5">"iTerm2 "</code>
</li>
<li>tmux detects this and enables clipboard for the entire session</li>
<li>
@ -177,7 +177,7 @@ function TerminalTestPageContent() {
</section>
{/* The Solution */}
<section className="mb-8 rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-6">
<section className="mb-8 rounded-[2px] border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-6">
<h2 className="mb-4 text-xl font-bold text-[var(--color-text-primary)]">
The Solution
</h2>
@ -190,7 +190,7 @@ function TerminalTestPageContent() {
Created custom terminal component that registers an XDA handler using xterm.js
parser API:
</p>
<pre className="overflow-x-auto rounded-lg bg-black p-4 text-xs">
<pre className="overflow-x-auto rounded-[2px] bg-black p-4 text-xs">
<code className="text-[var(--color-accent-green)]">
{`terminal.parser.registerCsiHandler(
{ prefix: ">", final: "q" }, // CSI > q is XDA query
@ -211,7 +211,7 @@ function TerminalTestPageContent() {
<ol className="ml-6 list-decimal space-y-1 text-[var(--color-text-secondary)]">
<li>Intercepts XDA queries from tmux</li>
<li>
Responds with <code className="rounded bg-black px-1 py-0.5">XTerm(370)</code>{" "}
Responds with <code className="rounded-[2px] bg-black px-1 py-0.5">XTerm(370)</code>{" "}
identification
</li>
<li>tmux detects "XTerm(" in response and enables TTYC_MS capability</li>
@ -245,12 +245,12 @@ function TerminalTestPageContent() {
</section>
{/* Node Version Requirement */}
<section className="mb-8 rounded-lg border border-[var(--color-accent-orange)] bg-[var(--color-bg-secondary)] p-6">
<section className="mb-8 rounded-[2px] border border-[var(--color-accent-orange)] bg-[var(--color-bg-secondary)] p-6">
<h2 className="mb-4 text-xl font-bold text-[var(--color-text-primary)]">
Node Version Requirement
</h2>
<div className="space-y-3 text-sm">
<div className="rounded-lg border border-[var(--color-accent-orange)] bg-[var(--color-bg-tertiary)] p-4">
<div className="rounded-[2px] border border-[var(--color-accent-orange)] bg-[var(--color-bg-tertiary)] p-4">
<p className="font-semibold text-[var(--color-accent-orange)]">
CRITICAL: This implementation requires Node 20.x (currently 20.20.0)
</p>
@ -264,7 +264,7 @@ function TerminalTestPageContent() {
</li>
<li>
Error on Node 25.6.1:{" "}
<code className="rounded bg-black px-1 py-0.5">posix_spawnp failed</code>
<code className="rounded-[2px] bg-black px-1 py-0.5">posix_spawnp failed</code>
</li>
<li>
Root cause: node-pty's native module (darwin-arm64 prebuild) fails to spawn
@ -294,7 +294,7 @@ function TerminalTestPageContent() {
</ul>
</div>
<div className="rounded-lg bg-[var(--color-bg-tertiary)] p-4">
<div className="rounded-[2px] bg-[var(--color-bg-tertiary)] p-4">
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">
Testing Instructions for Upgrades
</h3>
@ -304,13 +304,13 @@ function TerminalTestPageContent() {
<ol className="ml-6 list-decimal space-y-1 text-xs text-[var(--color-text-secondary)]">
<li>
Test node-pty directly:{" "}
<code className="rounded bg-black px-1 py-0.5">
<code className="rounded-[2px] bg-black px-1 py-0.5">
node -e "const pty = require('node-pty'); pty.spawn('/bin/bash', [],
&#123;&#125;)"
</code>
</li>
<li>
If no <code className="rounded bg-black px-1 py-0.5">posix_spawnp failed</code>{" "}
If no <code className="rounded-[2px] bg-black px-1 py-0.5">posix_spawnp failed</code>{" "}
error, proceed
</li>
<li>Start dev servers and open this page</li>
@ -327,7 +327,7 @@ function TerminalTestPageContent() {
<div className="flex items-center gap-4">
<button
onClick={() => setShowComparison(!showComparison)}
className="rounded-lg bg-[var(--color-accent-blue)] px-4 py-2 text-sm font-semibold text-white transition-opacity hover:opacity-80"
className="rounded-[2px] bg-[var(--color-accent-blue)] px-4 py-2 text-sm font-semibold text-white transition-opacity hover:opacity-80"
>
{showComparison ? "Hide" : "Show"} Side-by-Side Comparison
</button>
@ -336,10 +336,10 @@ function TerminalTestPageContent() {
</span>
</div>
{oldSessionId === newSessionId && (
<div className="mt-2 rounded border border-[var(--color-accent-orange)] bg-[var(--color-bg-tertiary)] p-2 text-xs text-[var(--color-text-secondary)]">
<div className="mt-2 rounded-[2px] border border-[var(--color-accent-orange)] bg-[var(--color-bg-tertiary)] p-2 text-xs text-[var(--color-text-secondary)]">
Using same session for both terminals. To avoid port conflicts, use different
sessions:
<code className="ml-1 rounded bg-black px-1">
<code className="ml-1 rounded-[2px] bg-black px-1">
?old_session=ao-orchestrator&new_session=ao-20
</code>
</div>
@ -351,9 +351,9 @@ function TerminalTestPageContent() {
<section className="mb-8">
<div className="grid gap-6 lg:grid-cols-2">
{/* Old Implementation */}
<div className="rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-4">
<div className="rounded-[2px] border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-4">
<div className="mb-3 flex items-center gap-2">
<span className="rounded bg-[var(--color-accent-red)] px-2 py-1 text-xs font-semibold text-white">
<span className="rounded-[2px] bg-[var(--color-accent-red)] px-2 py-1 text-xs font-semibold text-white">
OLD
</span>
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
@ -370,9 +370,9 @@ function TerminalTestPageContent() {
</div>
{/* New Implementation */}
<div className="rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-4">
<div className="rounded-[2px] border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-4">
<div className="mb-3 flex items-center gap-2">
<span className="rounded bg-[var(--color-accent-green)] px-2 py-1 text-xs font-semibold text-white">
<span className="rounded-[2px] bg-[var(--color-accent-green)] px-2 py-1 text-xs font-semibold text-white">
NEW
</span>
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
@ -390,7 +390,7 @@ function TerminalTestPageContent() {
</div>
</div>
<div className="mt-4 rounded-lg border border-[var(--color-accent-blue)] bg-[var(--color-bg-secondary)] p-4">
<div className="mt-4 rounded-[2px] border border-[var(--color-accent-blue)] bg-[var(--color-bg-secondary)] p-4">
<h3 className="mb-2 text-sm font-semibold text-[var(--color-text-primary)]">
🧪 How to Test Clipboard
</h3>
@ -409,13 +409,13 @@ function TerminalTestPageContent() {
)}
{/* Debugging Journey */}
<section className="rounded-lg border border-[var(--color-accent-purple)] bg-[var(--color-bg-secondary)] p-6">
<section className="rounded-[2px] border border-[var(--color-accent)] bg-[var(--color-bg-secondary)] p-6">
<h2 className="mb-4 text-xl font-bold text-[var(--color-text-primary)]">
🔬 The Debugging Journey
</h2>
<div className="space-y-4 text-sm text-[var(--color-text-secondary)]">
<div className="rounded-lg bg-[var(--color-bg-tertiary)] p-4">
<div className="rounded-[2px] bg-[var(--color-bg-tertiary)] p-4">
<p className="font-semibold text-[var(--color-accent-red)]">
Total Time Wasted: 12+ hours across Feb 15-16, 2026
</p>
@ -446,7 +446,7 @@ function TerminalTestPageContent() {
</li>
<li>
<strong>Tried force-enabling tmux clipboard</strong> - Used{" "}
<code className="rounded bg-black px-1 py-0.5">
<code className="rounded-[2px] bg-black px-1 py-0.5">
set-option -s set-clipboard on
</code>{" "}
in tmux.conf. Didn't help - tmux needs the terminal to declare support.
@ -462,11 +462,11 @@ function TerminalTestPageContent() {
<p>
<strong>What finally worked:</strong> Registering an XDA (Extended Device
Attributes) handler in xterm.js using{" "}
<code className="rounded bg-black px-1 py-0.5">
<code className="rounded-[2px] bg-black px-1 py-0.5">
terminal.parser.registerCsiHandler()
</code>
</p>
<pre className="mt-2 overflow-x-auto rounded-lg bg-black p-3 text-xs">
<pre className="mt-2 overflow-x-auto rounded-[2px] bg-black p-3 text-xs">
<code className="text-[var(--color-accent-green)]">
{`terminal.parser.registerCsiHandler(
{ prefix: ">", final: "q" },
@ -492,12 +492,12 @@ function TerminalTestPageContent() {
<li>
<strong>Deep-dive into tmux source code</strong> - Used DeepWiki.com to analyze
tmux's terminal capability detection logic in{" "}
<code className="rounded bg-black px-1 py-0.5">tty-keys.c</code> and{" "}
<code className="rounded bg-black px-1 py-0.5">tty.c</code>
<code className="rounded-[2px] bg-black px-1 py-0.5">tty-keys.c</code> and{" "}
<code className="rounded-[2px] bg-black px-1 py-0.5">tty.c</code>
</li>
<li>
<strong>Discovered XDA queries</strong> - Found that tmux sends{" "}
<code className="rounded bg-black px-1 py-0.5">CSI &gt; q</code> (XTVERSION) to
<code className="rounded-[2px] bg-black px-1 py-0.5">CSI &gt; q</code> (XTVERSION) to
detect terminal type
</li>
<li>
@ -508,7 +508,7 @@ function TerminalTestPageContent() {
<li>
<strong>Checked xterm.js implementation</strong> - Found that XDA is marked as
TODO in xterm.js tests:{" "}
<code className="rounded bg-black px-1 py-0.5">
<code className="rounded-[2px] bg-black px-1 py-0.5">
test.skip('CSI &gt; Ps q - Report xterm name and version (XTVERSION)')
</code>
</li>
@ -519,7 +519,7 @@ function TerminalTestPageContent() {
</ol>
</div>
<div className="rounded-lg border border-[var(--color-accent-orange)] bg-[var(--color-bg-tertiary)] p-4">
<div className="rounded-[2px] border border-[var(--color-accent-orange)] bg-[var(--color-bg-tertiary)] p-4">
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">
How We Could Have Figured It Out Faster
</h3>
@ -531,7 +531,7 @@ function TerminalTestPageContent() {
</li>
<li>
<strong>Monitor escape sequences</strong> - Running{" "}
<code className="rounded bg-black px-1 py-0.5">tmux -vvv</code> or using a
<code className="rounded-[2px] bg-black px-1 py-0.5">tmux -vvv</code> or using a
terminal protocol analyzer would have revealed the XDA queries being sent
</li>
<li>
@ -560,7 +560,7 @@ function TerminalTestPageContent() {
<li>xterm.js parser API documentation</li>
<li>XTerm Control Sequences: XTVERSION / Device Attributes</li>
<li>
tmux <code className="rounded bg-black px-1 py-0.5">tty-keys.c</code>: Terminal
tmux <code className="rounded-[2px] bg-black px-1 py-0.5">tty-keys.c</code>: Terminal
type detection logic
</li>
</ul>
@ -569,25 +569,25 @@ function TerminalTestPageContent() {
</section>
{/* Implementation Files */}
<section className="mt-6 rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-6">
<section className="mt-6 rounded-[2px] border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-6">
<h2 className="mb-4 text-xl font-bold text-[var(--color-text-primary)]">
📁 Implementation Files
</h2>
<ul className="ml-6 list-disc space-y-1 text-sm text-[var(--color-text-secondary)]">
<li>
<code className="rounded bg-black px-1 py-0.5">
<code className="rounded-[2px] bg-black px-1 py-0.5">
packages/web/src/components/DirectTerminal.tsx
</code>{" "}
- Main component with XDA handler
</li>
<li>
<code className="rounded bg-black px-1 py-0.5">
<code className="rounded-[2px] bg-black px-1 py-0.5">
packages/web/server/direct-terminal-ws.ts
</code>{" "}
- WebSocket server using node-pty
</li>
<li>
<code className="rounded bg-black px-1 py-0.5">
<code className="rounded-[2px] bg-black px-1 py-0.5">
packages/web/src/app/dev/terminal-test/page.tsx
</code>{" "}
- This test page

View File

@ -26,12 +26,12 @@
/* ── Light mode (default) ─────────────────────────────────────────── */
:root {
/* Base surfaces — Linear-standard light palette */
--color-bg-base: #ffffff;
/* Base surfaces — design-system light palette */
--color-bg-base: #f5f5f7;
--color-bg-surface: #ffffff;
--color-bg-elevated: #ffffff;
--color-bg-elevated-hover: #f7f7f8;
--color-bg-subtle: #f2f2f2;
--color-bg-subtle: #f0f0f2;
/* Backward-compat aliases */
--color-bg-primary: var(--color-bg-base);
@ -39,9 +39,9 @@
--color-bg-tertiary: var(--color-bg-elevated);
/* Borders */
--color-border-subtle: #e8e8ec;
--color-border-subtle: #e7e7ec;
--color-border-default: #d9d9de;
--color-border-strong: #c1c1c6;
--color-border-strong: #c9c9d2;
/* Backward-compat aliases */
--color-border-muted: var(--color-border-subtle);
@ -50,8 +50,8 @@
/* Text */
--color-text-primary: #1b1b1f;
--color-text-secondary: #5e5e66;
--color-text-tertiary: #8b8b93;
--color-text-muted: #8b8b93;
--color-text-tertiary: #8e8e96;
--color-text-muted: #8e8e96;
--color-text-inverse: #ffffff;
/* Interactive accent */
@ -75,15 +75,15 @@
--color-accent-yellow: #9a6700;
--color-accent-orange: #bc4c00;
--color-accent-red: #dc2626;
--color-accent-violet: #8250df;
--color-accent-purple: #8250df;
--color-accent-violet: #5B7EF8;
--color-accent-purple: #5B7EF8;
/* Theme-adaptive tints (bg tints behind status pills) */
--color-tint-blue: rgba(91, 126, 248, 0.08);
--color-tint-green: rgba(22, 163, 74, 0.08);
--color-tint-yellow: rgba(154, 103, 0, 0.08);
--color-tint-red: rgba(220, 38, 38, 0.08);
--color-tint-violet: rgba(130, 80, 223, 0.08);
--color-tint-violet: rgba(91, 126, 248, 0.08);
--color-tint-orange: rgba(188, 76, 0, 0.08);
--color-tint-neutral: rgba(0, 0, 0, 0.04);
@ -151,16 +151,13 @@
--color-alert-ci-unknown: #9a6700;
--color-alert-review: #1a7f37;
--color-alert-review-bg: #1a7f37;
--color-alert-changes: #8250df;
--color-alert-changes-bg: #7c3aed;
--color-alert-changes: #5B7EF8;
--color-alert-changes-bg: #5B7EF8;
--color-alert-conflict: #9a6700;
--color-alert-conflict-bg: #92400e;
--color-alert-comment: #bc4c00;
--color-alert-comment-bg: #c2410c;
/* Activity pulse glow */
--color-activity-pulse: rgba(22, 163, 74, 0.45);
--color-activity-pulse-end: rgba(22, 163, 74, 0);
}
/* ── Dark mode ────────────────────────────────────────────────────── */
@ -199,7 +196,7 @@
--color-status-review: #06b6d4;
--color-status-attention: #f1be64;
--color-status-idle: #293142;
--color-status-done: #202838;
--color-status-done: #3a4252;
--color-status-error: #ef4444;
/* Semantic aliases */
@ -208,15 +205,15 @@
--color-accent-yellow: #f1be64;
--color-accent-orange: #ff9d57;
--color-accent-red: #ef4444;
--color-accent-violet: #b59cff;
--color-accent-purple: #b59cff;
--color-accent-violet: #5B7EF8;
--color-accent-purple: #5B7EF8;
/* Theme-adaptive tints */
--color-tint-blue: rgba(91, 126, 248, 0.12);
--color-tint-green: rgba(34, 197, 94, 0.12);
--color-tint-yellow: rgba(241, 190, 100, 0.12);
--color-tint-red: rgba(239, 68, 68, 0.12);
--color-tint-violet: rgba(181, 156, 255, 0.12);
--color-tint-violet: rgba(91, 126, 248, 0.12);
--color-tint-orange: rgba(255, 157, 87, 0.12);
--color-tint-neutral: rgba(255, 255, 255, 0.05);
@ -233,7 +230,7 @@
/* Body */
--color-body-gradient-blue: rgba(110, 143, 255, 0.2);
--color-body-gradient-violet: rgba(91, 208, 177, 0.08);
--color-body-gradient-violet: rgba(91, 126, 248, 0.08);
/* Nav glass */
--color-nav-bg: rgba(10, 13, 18, 0.82);
@ -288,16 +285,13 @@
--color-alert-ci-unknown: #d4a72c;
--color-alert-review: #4dab6e;
--color-alert-review-bg: rgba(77, 171, 110, 0.25);
--color-alert-changes: #9b8afb;
--color-alert-changes-bg: rgba(155, 138, 251, 0.25);
--color-alert-changes: #5B7EF8;
--color-alert-changes-bg: rgba(91, 126, 248, 0.25);
--color-alert-conflict: #d4a72c;
--color-alert-conflict-bg: rgba(212, 167, 44, 0.25);
--color-alert-comment: #c88a2e;
--color-alert-comment-bg: rgba(200, 138, 46, 0.25);
/* Activity pulse glow */
--color-activity-pulse: rgba(34, 197, 94, 0.45);
--color-activity-pulse-end: rgba(34, 197, 94, 0);
}
@theme {
@ -412,7 +406,7 @@ body {
/* Light mode: solid white, NO gradient */
html:not(.dark) body {
background: #ffffff;
background: var(--color-bg-base);
}
/* Radial gradient only for dark mode */
@ -515,10 +509,12 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active {
@keyframes activity-pulse {
0%,
100% {
box-shadow: 0 0 0 0 var(--color-activity-pulse);
transform: scale(1);
opacity: 1;
}
50% {
box-shadow: 0 0 0 4px var(--color-activity-pulse-end);
transform: scale(1.16);
opacity: 0.72;
}
}
@ -617,10 +613,6 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active {
border-color 0.12s ease;
}
.orchestrator-btn:active {
transform: scale(0.97);
}
.orchestrator-btn:hover {
transform: translateY(-1px);
background: linear-gradient(
@ -632,6 +624,11 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active {
box-shadow: var(--btn-shadow-hover);
}
.orchestrator-btn:active,
.orchestrator-btn:hover:active {
transform: scale(0.97);
}
/* ── Glass nav ────────────────────────────────────────────────────────── */
.nav-glass {
@ -663,9 +660,9 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active {
position: relative;
overflow: visible;
border: 1px solid var(--color-border-default);
border-radius: 0;
border-radius: 2px;
background: var(--color-bg-surface);
box-shadow: 0 10px 24px rgba(2, 6, 12, 0.06);
box-shadow: var(--card-shadow);
}
.dashboard-hero__backdrop {
@ -718,7 +715,6 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active {
height: 6px;
border-radius: 999px;
background: currentColor;
box-shadow: 0 0 18px currentColor;
}
.dashboard-title {
@ -796,8 +792,8 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active {
}
.dashboard-alert {
border-radius: 0;
box-shadow: 0 16px 34px rgba(0, 0, 0, 0.08);
border-radius: 2px;
box-shadow: var(--detail-card-shadow);
}
/* ── Detail page cards — subtle depth, no hover lift ─────────────────── */
@ -805,7 +801,7 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active {
.detail-card {
background: var(--detail-card-bg);
box-shadow: var(--detail-card-shadow);
border-radius: 0;
border-radius: 2px;
}
.session-detail-page {
@ -826,6 +822,7 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active {
padding: 14px 16px;
background: var(--detail-card-bg);
box-shadow: var(--detail-card-shadow);
border-radius: 2px;
}
@media (min-width: 900px) {
@ -893,7 +890,7 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active {
min-height: 28px;
padding: 0 10px;
border: 1px solid var(--color-border-subtle);
border-radius: 0;
border-radius: 2px;
background: color-mix(in srgb, var(--color-bg-elevated) 88%, transparent);
color: var(--color-text-secondary);
font-size: 11px;
@ -948,6 +945,11 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active {
position: relative;
overflow: hidden;
box-shadow: inset 0 1px 0 color-mix(in srgb, white 16%, transparent);
transition:
transform 0.16s ease-out,
opacity 0.16s ease-out,
background 0.16s ease-out,
border-color 0.16s ease-out;
}
.session-detail-status-pill__dot {
@ -992,40 +994,34 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active {
@keyframes session-status-active-breathe {
0%, 100% {
box-shadow:
inset 0 1px 0 color-mix(in srgb, white 16%, transparent),
0 0 0 0 color-mix(in srgb, var(--color-status-working) 0%, transparent);
transform: translateY(0);
opacity: 0.96;
}
50% {
box-shadow:
inset 0 1px 0 color-mix(in srgb, white 18%, transparent),
0 0 0 3px color-mix(in srgb, var(--color-status-working) 10%, transparent);
transform: translateY(-1px);
opacity: 1;
}
}
@keyframes session-status-ready-breathe {
0%, 100% {
box-shadow:
inset 0 1px 0 color-mix(in srgb, white 16%, transparent),
0 0 0 0 color-mix(in srgb, var(--color-status-ready) 0%, transparent);
transform: translateY(0);
opacity: 0.96;
}
50% {
box-shadow:
inset 0 1px 0 color-mix(in srgb, white 20%, transparent),
0 0 0 3px color-mix(in srgb, var(--color-status-ready) 12%, transparent);
transform: translateY(-1px);
opacity: 1;
}
}
@keyframes session-status-waiting-breathe {
0%, 100% {
box-shadow:
inset 0 1px 0 color-mix(in srgb, white 16%, transparent),
0 0 0 0 color-mix(in srgb, var(--color-status-attention) 0%, transparent);
transform: translateY(0);
opacity: 0.95;
}
50% {
box-shadow:
inset 0 1px 0 color-mix(in srgb, white 18%, transparent),
0 0 0 3px color-mix(in srgb, var(--color-status-attention) 10%, transparent);
transform: translateY(-1px);
opacity: 1;
}
}
@ -3129,7 +3125,7 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active {
}
.toast--success .toast__icon {
background: var(--color-status-ready);
background: var(--color-status-working);
}
.toast--error .toast__icon {

View File

@ -0,0 +1,50 @@
import { render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
let searchParams = new URLSearchParams();
vi.mock("next/navigation", () => ({
useSearchParams: () => searchParams,
}));
vi.mock("@/components/DirectTerminal", () => ({
DirectTerminal: ({
sessionId,
startFullscreen,
}: {
sessionId: string;
startFullscreen: boolean;
}) => (
<div data-testid="direct-terminal">
{sessionId}:{String(startFullscreen)}
</div>
),
}));
describe("TestDirectPage", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("renders the default direct terminal clipboard instructions", async () => {
searchParams = new URLSearchParams();
const { default: TestDirectPage } = await import("./page");
render(<TestDirectPage />);
expect(screen.getByText("DirectTerminal Test - XDA Clipboard Support")).toBeInTheDocument();
expect(screen.getByText("Testing:")).toBeInTheDocument();
expect(screen.getByText("ao-orchestrator")).toBeInTheDocument();
expect(screen.getByTestId("direct-terminal")).toHaveTextContent("ao-orchestrator:false");
});
it("passes session and fullscreen params to the terminal", async () => {
searchParams = new URLSearchParams("session=ao-20&fullscreen=true");
const { default: TestDirectPage } = await import("./page");
render(<TestDirectPage />);
expect(screen.getByTestId("direct-terminal")).toHaveTextContent("ao-20:true");
expect(screen.getByText(/clipboard works without iTerm2 attachment/i)).toBeInTheDocument();
});
});

View File

@ -35,7 +35,7 @@ function TestDirectPageContent() {
<br />
tmux should recognize it as XTerm and enable clipboard support (OSC 52).
</p>
<div className="mt-4 rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-4">
<div className="mt-4 rounded-[2px] border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-4">
<h2 className="mb-2 text-sm font-semibold text-[var(--color-text-primary)]">
Testing: <span className="text-[var(--color-accent-green)]">{sessionId}</span>
</h2>
@ -56,7 +56,7 @@ function TestDirectPageContent() {
</li>
</ol>
</div>
<div className="mt-4 rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-4">
<div className="mt-4 rounded-[2px] border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-4">
<h2 className="mb-2 text-sm font-semibold text-[var(--color-text-primary)]">
Technical Details:
</h2>

View File

@ -15,7 +15,7 @@ import type { FitAddon as FitAddonType } from "@xterm/addon-fit";
interface DirectTerminalProps {
sessionId: string;
startFullscreen?: boolean;
/** Visual variant. "orchestrator" uses violet accent; "agent" (default) uses blue. */
/** Visual variant. Orchestrator keeps the same design-system blue accent as the rest of the app. */
variant?: "agent" | "orchestrator";
/** CSS height for the terminal container in normal (non-fullscreen) mode.
* Defaults to "max(440px, calc(100vh - 440px))". */
@ -78,11 +78,7 @@ export function buildTerminalThemes(variant: TerminalVariant): { dark: ITheme; l
selDark: "rgba(91, 126, 248, 0.30)",
selLight: "rgba(91, 126, 248, 0.25)",
};
const orchAccent = {
cursor: "#a371f7",
selDark: "rgba(163, 113, 247, 0.25)",
selLight: "rgba(130, 80, 223, 0.20)",
};
const orchAccent = agentAccent;
const accent = variant === "orchestrator" ? orchAccent : agentAccent;
const dark: ITheme = {
@ -682,8 +678,7 @@ export function DirectTerminal({
};
}, [fullscreen]);
const accentColor =
variant === "orchestrator" ? "var(--color-accent-violet)" : "var(--color-accent)";
const accentColor = "var(--color-accent)";
const statusDotClass =
status === "connected"

View File

@ -38,7 +38,7 @@ export function PRStatus({ pr }: PRStatusProps) {
{/* Merged badge */}
{pr.state === "merged" && (
<span className="inline-flex items-center rounded-full bg-[rgba(163,113,247,0.1)] px-2 py-0.5 text-[10px] font-semibold text-[var(--color-accent-violet)]">
<span className="inline-flex items-center rounded-full bg-[var(--color-chip-bg)] px-2 py-0.5 text-[10px] font-semibold text-[var(--color-text-secondary)]">
merged
</span>
)}

View File

@ -538,7 +538,10 @@ function SessionDetailPRCard({ pr, sessionId }: { pr: DashboardPR; sessionId: st
<span className="text-[var(--color-text-tertiary)]">&middot;</span>
<span
className="px-2 py-0.5 text-[10px] font-semibold"
style={{ color: "#a371f7", background: "rgba(163,113,247,0.12)" }}
style={{
color: "var(--color-text-secondary)",
background: "var(--color-chip-bg)",
}}
>
Merged
</span>
@ -631,7 +634,7 @@ function SessionDetailPRCard({ pr, sessionId }: { pr: DashboardPR; sessionId: st
onClick={() => handleAskAgentToFix(c)}
disabled={sendingComments.has(c.url)}
className={cn(
"mt-1.5 px-3 py-1 text-[11px] font-semibold transition-all",
"mt-1.5 px-3 py-1 text-[11px] font-semibold transition-colors duration-150",
sentComments.has(c.url)
? "bg-[var(--color-status-ready)] text-white"
: errorComments.has(c.url)

View File

@ -40,7 +40,7 @@ export function Terminal({ sessionId }: TerminalProps) {
return (
<div
className={cn(
"overflow-hidden rounded-lg border border-[var(--color-border-default)] bg-black",
"overflow-hidden rounded-[2px] border border-[var(--color-border-default)] bg-black",
fullscreen && "fixed inset-0 z-50 rounded-none border-0",
)}
>
@ -68,7 +68,7 @@ export function Terminal({ sessionId }: TerminalProps) {
</span>
<button
onClick={() => setFullscreen(!fullscreen)}
className="ml-auto rounded px-2 py-0.5 text-[11px] text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-bg-secondary)] hover:text-[var(--color-text-primary)]"
className="ml-auto rounded-[2px] px-2 py-0.5 text-[11px] text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-bg-secondary)] hover:text-[var(--color-text-primary)]"
>
{fullscreen ? "exit fullscreen" : "fullscreen"}
</button>

View File

@ -0,0 +1,129 @@
import { render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { DirectTerminal } from "../DirectTerminal";
const replaceMock = vi.fn();
let searchParams = new URLSearchParams();
vi.mock("next/navigation", () => ({
useRouter: () => ({ replace: replaceMock }),
usePathname: () => "/test-direct",
useSearchParams: () => searchParams,
}));
vi.mock("next-themes", () => ({
useTheme: () => ({ resolvedTheme: "dark" }),
}));
class MockTerminal {
options: Record<string, unknown>;
parser = {
registerCsiHandler: vi.fn(),
registerOscHandler: vi.fn(),
};
cols = 80;
rows = 24;
constructor(options: Record<string, unknown>) {
this.options = options;
}
loadAddon() {}
open() {}
write() {}
refresh() {}
dispose() {}
hasSelection() {
return false;
}
getSelection() {
return "";
}
clearSelection() {}
onSelectionChange() {
return { dispose() {} };
}
attachCustomKeyEventHandler() {}
onData() {
return { dispose() {} };
}
}
class MockFitAddon {
fit() {}
}
function MockWebLinksAddon() {
return undefined;
}
class MockWebSocket {
static OPEN = 1;
static instances: MockWebSocket[] = [];
readyState = MockWebSocket.OPEN;
binaryType = "arraybuffer";
onopen: (() => void) | null = null;
onmessage: ((event: { data: string }) => void) | null = null;
onerror: ((event: unknown) => void) | null = null;
onclose: ((event: { code: number; reason: string }) => void) | null = null;
constructor(public url: string) {
MockWebSocket.instances.push(this);
setTimeout(() => this.onopen?.(), 0);
}
send() {}
close() {}
}
vi.mock("xterm", () => ({
Terminal: MockTerminal,
}));
vi.mock("@xterm/addon-fit", () => ({
FitAddon: MockFitAddon,
}));
vi.mock("@xterm/addon-web-links", () => ({
WebLinksAddon: MockWebLinksAddon,
}));
describe("DirectTerminal render", () => {
beforeEach(() => {
searchParams = new URLSearchParams();
replaceMock.mockReset();
MockWebSocket.instances = [];
Object.defineProperty(document, "fonts", {
configurable: true,
value: { ready: Promise.resolve() },
});
vi.stubGlobal("WebSocket", MockWebSocket);
vi.stubGlobal(
"fetch",
vi.fn(async () => ({
ok: true,
json: async () => ({
proxyWsPath: "/ao-terminal-ws",
}),
})),
);
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it("renders the shared accent chrome for orchestrator terminals", async () => {
render(<DirectTerminal sessionId="ao-orchestrator" variant="orchestrator" />);
await waitFor(() => expect(fetch).toHaveBeenCalledWith("/api/runtime/terminal", expect.any(Object)));
await waitFor(() =>
expect(screen.getByText("Connected")).toBeInTheDocument(),
);
expect(screen.getByText("ao-orchestrator")).toHaveStyle({ color: "var(--color-accent)" });
expect(screen.getByText("XDA")).toHaveStyle({ color: "var(--color-accent)" });
expect(MockWebSocket.instances[0]?.url).toContain("/ao-terminal-ws?session=ao-orchestrator");
});
});

View File

@ -125,11 +125,21 @@ describe("buildTerminalThemes", () => {
expect(dark.background).toBe("#0a0a0f");
});
it("variant changes cursor color between agent and orchestrator", () => {
it("orchestrator variant reuses the shared design-system accent", () => {
const agent = buildTerminalThemes("agent");
const orch = buildTerminalThemes("orchestrator");
expect(agent.dark.cursor).not.toBe(orch.dark.cursor);
expect(agent.light.cursor).not.toBe(orch.light.cursor);
expect(agent.dark.cursor).toBe(orch.dark.cursor);
expect(agent.light.cursor).toBe(orch.light.cursor);
expect(agent.dark.selectionBackground).toBe(orch.dark.selectionBackground);
expect(agent.light.selectionBackground).toBe(orch.light.selectionBackground);
});
it("keeps ANSI magenta distinct from ANSI blue", () => {
const { dark, light } = buildTerminalThemes("agent");
expect(dark.magenta).not.toBe(dark.blue);
expect(dark.brightMagenta).not.toBe(dark.brightBlue);
expect(light.magenta).not.toBe(light.blue);
expect(light.brightMagenta).not.toBe(light.brightBlue);
});
it("selection colors differ between dark and light themes", () => {

View File

@ -180,4 +180,29 @@ describe("SessionDetail mobile navbar", () => {
expect(screen.getByText("lint")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Ask Agent to Fix" })).toBeInTheDocument();
});
it("shows the merged badge styling for merged PR sessions", () => {
render(
<SessionDetail
session={makeSession({
id: "worker-merged",
projectId: "my-app",
summary: "Merged session",
pr: makePR({
number: 89,
state: "merged",
title: "Preserve merged badge styling",
}),
})}
projectOrchestratorId="my-app-orchestrator"
/>,
);
const mergedBadge = screen.getByText("Merged");
expect(mergedBadge).toBeInTheDocument();
expect(mergedBadge).toHaveStyle({
color: "var(--color-text-secondary)",
background: "var(--color-chip-bg)",
});
});
});

View File

@ -0,0 +1,39 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Terminal } from "../Terminal";
describe("Terminal", () => {
beforeEach(() => {
vi.stubGlobal(
"fetch",
vi.fn(async () => ({
ok: true,
json: async () => ({ url: "http://localhost:14800/session/demo" }),
})),
);
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it("loads the iframe terminal URL and supports fullscreen toggling", async () => {
const { container } = render(<Terminal sessionId="ao-77" />);
await waitFor(() =>
expect(screen.getByTitle("Terminal: ao-77")).toHaveAttribute(
"src",
"http://localhost:14800/session/demo",
),
);
expect(fetch).toHaveBeenCalledWith(
`${window.location.protocol}//${window.location.hostname}:14800/terminal?session=ao-77`,
);
fireEvent.click(screen.getByRole("button", { name: "fullscreen" }));
expect(container.firstChild).toHaveClass("fixed", "inset-0");
expect(screen.getByRole("button", { name: "exit fullscreen" })).toBeInTheDocument();
});
});