diff --git a/packages/web/src/app/dev/terminal-test/page.tsx b/packages/web/src/app/dev/terminal-test/page.tsx index e9047a20b..8323ae23e 100644 --- a/packages/web/src/app/dev/terminal-test/page.tsx +++ b/packages/web/src/app/dev/terminal-test/page.tsx @@ -76,7 +76,7 @@ function TerminalTestPageContent() { {/* The Problem */} -
+

๐Ÿ› The Problem

@@ -97,7 +97,7 @@ function TerminalTestPageContent() {
{/* Root Cause */} -
+

๐Ÿ” Root Cause Analysis

@@ -110,7 +110,7 @@ function TerminalTestPageContent() {
  • tmux uses OSC 52 escape sequences to synchronize clipboard with terminals
  • Format:{" "} - \x1b]52;c;<base64>\x07 + \x1b]52;c;<base64>\x07
  • Terminal must support OSC 52 and have proper capabilities declared
  • @@ -123,7 +123,7 @@ function TerminalTestPageContent() {
    • tmux queries terminal capabilities using Device Attributes (DA/XDA)
    • - XDA query: CSI > q (also + XDA query: CSI > q (also called XTVERSION)
    • @@ -138,14 +138,14 @@ function TerminalTestPageContent() {

      3. The Missing Piece

      -
      +

      xterm.js does NOT implement XDA (Extended Device Attributes)

      • XDA is marked as TODO in xterm.js codebase:{" "} - + test.skip('CSI > Ps q - Report xterm name and version (XTVERSION)')
      • @@ -164,7 +164,7 @@ function TerminalTestPageContent() {
        • iTerm2 sends proper XDA response identifying itself as{" "} - "iTerm2 " + "iTerm2 "
        • tmux detects this and enables clipboard for the entire session
        • @@ -177,7 +177,7 @@ function TerminalTestPageContent() {
    {/* The Solution */} -
    +

    โœ… The Solution

    @@ -190,7 +190,7 @@ function TerminalTestPageContent() { Created custom terminal component that registers an XDA handler using xterm.js parser API:

    -
    +              
                     
                       {`terminal.parser.registerCsiHandler(
       { prefix: ">", final: "q" }, // CSI > q is XDA query
    @@ -211,7 +211,7 @@ function TerminalTestPageContent() {
                   
    1. Intercepts XDA queries from tmux
    2. - Responds with XTerm(370){" "} + Responds with XTerm(370){" "} identification
    3. tmux detects "XTerm(" in response and enables TTYC_MS capability
    4. @@ -245,12 +245,12 @@ function TerminalTestPageContent() {
    {/* Node Version Requirement */} -
    +

    โš ๏ธ Node Version Requirement

    -
    +

    CRITICAL: This implementation requires Node 20.x (currently 20.20.0)

    @@ -264,7 +264,7 @@ function TerminalTestPageContent() {
  • Error on Node 25.6.1:{" "} - posix_spawnp failed + posix_spawnp failed
  • Root cause: node-pty's native module (darwin-arm64 prebuild) fails to spawn @@ -294,7 +294,7 @@ function TerminalTestPageContent() {
  • -
    +

    โšก Testing Instructions for Upgrades

    @@ -304,13 +304,13 @@ function TerminalTestPageContent() {
    1. Test node-pty directly:{" "} - + node -e "const pty = require('node-pty'); pty.spawn('/bin/bash', [], {})"
    2. - If no posix_spawnp failed{" "} + If no posix_spawnp failed{" "} error, proceed
    3. Start dev servers and open this page
    4. @@ -327,7 +327,7 @@ function TerminalTestPageContent() {
      @@ -336,10 +336,10 @@ function TerminalTestPageContent() {
      {oldSessionId === newSessionId && ( -
      +
      โš ๏ธ Using same session for both terminals. To avoid port conflicts, use different sessions: - + ?old_session=ao-orchestrator&new_session=ao-20
      @@ -351,9 +351,9 @@ function TerminalTestPageContent() {
      {/* Old Implementation */} -
      +
      - + OLD

      @@ -370,9 +370,9 @@ function TerminalTestPageContent() {

      {/* New Implementation */} -
      +
      - + NEW

      @@ -390,7 +390,7 @@ function TerminalTestPageContent() {

      -
      +

      ๐Ÿงช How to Test Clipboard

      @@ -409,13 +409,13 @@ function TerminalTestPageContent() { )} {/* Debugging Journey */} -
      +

      ๐Ÿ”ฌ The Debugging Journey

      -
      +

      Total Time Wasted: 12+ hours across Feb 15-16, 2026

      @@ -446,7 +446,7 @@ function TerminalTestPageContent() {
    5. Tried force-enabling tmux clipboard - Used{" "} - + set-option -s set-clipboard on {" "} in tmux.conf. Didn't help - tmux needs the terminal to declare support. @@ -462,11 +462,11 @@ function TerminalTestPageContent() {

      What finally worked: Registering an XDA (Extended Device Attributes) handler in xterm.js using{" "} - + terminal.parser.registerCsiHandler()

      -
      +                
                         
                           {`terminal.parser.registerCsiHandler(
         { prefix: ">", final: "q" },
      @@ -492,12 +492,12 @@ function TerminalTestPageContent() {
                       
    6. Deep-dive into tmux source code - Used DeepWiki.com to analyze tmux's terminal capability detection logic in{" "} - tty-keys.c and{" "} - tty.c + tty-keys.c and{" "} + tty.c
    7. Discovered XDA queries - Found that tmux sends{" "} - CSI > q (XTVERSION) to + CSI > q (XTVERSION) to detect terminal type
    8. @@ -508,7 +508,7 @@ function TerminalTestPageContent() {
    9. Checked xterm.js implementation - Found that XDA is marked as TODO in xterm.js tests:{" "} - + test.skip('CSI > Ps q - Report xterm name and version (XTVERSION)')
    10. @@ -519,7 +519,7 @@ function TerminalTestPageContent() {
    -
    +

    โšก How We Could Have Figured It Out Faster

    @@ -531,7 +531,7 @@ function TerminalTestPageContent() {
  • Monitor escape sequences - Running{" "} - tmux -vvv or using a + tmux -vvv or using a terminal protocol analyzer would have revealed the XDA queries being sent
  • @@ -560,7 +560,7 @@ function TerminalTestPageContent() {
  • xterm.js parser API documentation
  • XTerm Control Sequences: XTVERSION / Device Attributes
  • - tmux tty-keys.c: Terminal + tmux tty-keys.c: Terminal type detection logic
  • @@ -569,25 +569,25 @@ function TerminalTestPageContent() {
    {/* Implementation Files */} -
    +

    ๐Ÿ“ Implementation Files

    • - + packages/web/src/components/DirectTerminal.tsx {" "} - Main component with XDA handler
    • - + packages/web/server/direct-terminal-ws.ts {" "} - WebSocket server using node-pty
    • - + packages/web/src/app/dev/terminal-test/page.tsx {" "} - This test page diff --git a/packages/web/src/app/globals.css b/packages/web/src/app/globals.css index 9b8bb5b68..c80b28305 100644 --- a/packages/web/src/app/globals.css +++ b/packages/web/src/app/globals.css @@ -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,8 +151,8 @@ --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; @@ -199,7 +199,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 +208,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 +233,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,8 +288,8 @@ --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; @@ -412,7 +412,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 +515,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 +619,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 +630,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 +666,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 +721,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 +798,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 +807,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 +828,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 +896,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 +951,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 +1000,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 +3131,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 { diff --git a/packages/web/src/app/test-direct/page.tsx b/packages/web/src/app/test-direct/page.tsx index 3d6c43672..9a8b14911 100644 --- a/packages/web/src/app/test-direct/page.tsx +++ b/packages/web/src/app/test-direct/page.tsx @@ -35,7 +35,7 @@ function TestDirectPageContent() {
      tmux should recognize it as XTerm and enable clipboard support (OSC 52).

      -
      +

      Testing: {sessionId}

      @@ -56,7 +56,7 @@ function TestDirectPageContent() {
    • -
      +

      Technical Details:

      diff --git a/packages/web/src/components/DirectTerminal.tsx b/packages/web/src/components/DirectTerminal.tsx index 7191b7294..761bfe68d 100644 --- a/packages/web/src/components/DirectTerminal.tsx +++ b/packages/web/src/components/DirectTerminal.tsx @@ -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))". */ @@ -46,11 +46,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 = { @@ -66,7 +62,7 @@ export function buildTerminalThemes(variant: TerminalVariant): { dark: ITheme; l green: "#22c55e", yellow: "#f59e0b", blue: "#5b7ef8", - magenta: "#a371f7", + magenta: "#5b7ef8", cyan: "#22d3ee", white: "#d4d4d8", brightBlack: "#50506a", @@ -74,7 +70,7 @@ export function buildTerminalThemes(variant: TerminalVariant): { dark: ITheme; l brightGreen: "#4ade80", brightYellow: "#fbbf24", brightBlue: "#7b9cfb", - brightMagenta: "#c084fc", + brightMagenta: "#7b9cfb", brightCyan: "#67e8f9", brightWhite: "#eeeef5", }; @@ -92,7 +88,7 @@ export function buildTerminalThemes(variant: TerminalVariant): { dark: ITheme; l green: "#1f7a3d", yellow: "#8a5a00", blue: "#175cd3", - magenta: "#8e24aa", + magenta: "#175cd3", cyan: "#0b7285", white: "#4b5563", brightBlack: "#374151", @@ -100,7 +96,7 @@ export function buildTerminalThemes(variant: TerminalVariant): { dark: ITheme; l brightGreen: "#176639", brightYellow: "#6f4a00", brightBlue: "#1d4ed8", - brightMagenta: "#7b1fa2", + brightMagenta: "#1d4ed8", brightCyan: "#155e75", brightWhite: "#374151", }; @@ -609,8 +605,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" diff --git a/packages/web/src/components/PRStatus.tsx b/packages/web/src/components/PRStatus.tsx index bbe33c792..26242c32a 100644 --- a/packages/web/src/components/PRStatus.tsx +++ b/packages/web/src/components/PRStatus.tsx @@ -38,7 +38,7 @@ export function PRStatus({ pr }: PRStatusProps) { {/* Merged badge */} {pr.state === "merged" && ( - + merged )} diff --git a/packages/web/src/components/SessionDetail.tsx b/packages/web/src/components/SessionDetail.tsx index 9947bd08e..7960b7899 100644 --- a/packages/web/src/components/SessionDetail.tsx +++ b/packages/web/src/components/SessionDetail.tsx @@ -538,7 +538,10 @@ function SessionDetailPRCard({ pr, sessionId }: { pr: DashboardPR; sessionId: st · Merged @@ -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) diff --git a/packages/web/src/components/Terminal.tsx b/packages/web/src/components/Terminal.tsx index 3a9a130a6..cffc58aa4 100644 --- a/packages/web/src/components/Terminal.tsx +++ b/packages/web/src/components/Terminal.tsx @@ -40,7 +40,7 @@ export function Terminal({ sessionId }: TerminalProps) { return (
      @@ -68,7 +68,7 @@ export function Terminal({ sessionId }: TerminalProps) {