Merge pull request #856 from ComposioHQ/ashish921998/design-sync-pr
Align web UI to design system
This commit is contained in:
commit
508e7b381e
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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;<base64>\x07</code>
|
||||
<code className="rounded-[2px] bg-black px-1 py-0.5">\x1b]52;c;<base64>\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 > q</code> (also
|
||||
XDA query: <code className="rounded-[2px] bg-black px-1 py-0.5">CSI > 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 > 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', [],
|
||||
{})"
|
||||
</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 > q</code> (XTVERSION) to
|
||||
<code className="rounded-[2px] bg-black px-1 py-0.5">CSI > 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 > 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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -538,7 +538,10 @@ function SessionDetailPRCard({ pr, sessionId }: { pr: DashboardPR; sessionId: st
|
|||
<span className="text-[var(--color-text-tertiary)]">·</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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue