From bba1316527e747990ca99da6aea474b0c668ee44 Mon Sep 17 00:00:00 2001 From: prateek Date: Mon, 16 Feb 2026 04:34:15 +0530 Subject: [PATCH] feat: implement DirectTerminal with XDA clipboard support (#55) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: implement DirectTerminal with XDA clipboard support Fixes clipboard functionality in web terminal without requiring iTerm2 attachment. ## Problem Browser clipboard (Cmd+C/Ctrl+C) only worked when an iTerm2 client was attached to the tmux session. Users had to keep iTerm2 tabs open in the background for clipboard to work in the web dashboard. ## Root Cause - tmux uses XDA (Extended Device Attributes) queries to detect terminal capabilities - xterm.js doesn't implement XDA (marked as TODO in their codebase) - Without XDA response, tmux doesn't enable clipboard support (TTYC_MS) - iTerm2 responds to XDA, enabling clipboard for the entire session ## Solution Implemented DirectTerminal component with custom XDA handler: - Registers CSI > q handler using xterm.js parser API - Responds with XTerm identification: DCS > | XTerm(370) ST - tmux detects "XTerm(" and enables clipboard capability - OSC 52 sequences flow: tmux → WebSocket → xterm.js → navigator.clipboard ## Changes - Add DirectTerminal component with XDA handler - Add direct-terminal-ws WebSocket server using node-pty - Replace Terminal with DirectTerminal in SessionDetail - Add comprehensive test page at /dev/terminal-test documenting: - Root cause analysis - Implementation details - Node version requirements (requires Node 20.x due to node-pty) - Debugging journey and lessons learned - Fix ttyd port recycling to prevent EADDRINUSE errors ## Testing Visit http://localhost:3000/dev/terminal-test for side-by-side comparison and complete documentation. Investigation time: 12+ hours (Feb 15-16, 2026) Co-Authored-By: Claude Sonnet 4.5 * fix: lint and typecheck errors - Remove unused expectedWidth variable in DirectTerminal - Add test files to eslint ignores * fix: wrap useSearchParams in Suspense boundary - Add Suspense wrapper to /dev/terminal-test page - Add Suspense wrapper to /test-direct page - Fixes Next.js build prerender error * fix: address Bugbot review comments - Fix useEffect cleanup memory leak in DirectTerminal - Remove accidentally committed build artifacts - Remove test scripts from repository root - Add build/ to .gitignore * fix: address additional Bugbot comments - Re-enable mouse mode in ttyd terminal (was temporarily disabled) - Fix hardcoded macOS paths in direct-terminal-ws - Use 'tmux' from PATH instead of hardcoded /opt/homebrew/bin/tmux - Use os.userInfo() for username fallback instead of 'equinox' - Use process.env.PATH for cross-platform compatibility Note: Bugbot comment about XDA response is incorrect - terminal.write() is the correct API for responding to XDA queries. The implementation works as confirmed by user testing. * perf: fix slow terminal scrolling - Remove status and error from useEffect dependencies (was recreating terminal on every state change) - Add scroll performance settings: scrollSensitivity, fastScrollModifier - Terminal now only recreates when sessionId changes * fix: pass startFullscreen prop to DirectTerminal in SessionDetail The fullscreen query parameter was being read from URL but not passed through to the DirectTerminal component. This caused the fullscreen=true query param to be ignored when viewing session detail pages. Fixes: - Pass startFullscreen prop from SessionDetail to DirectTerminal - Enables ?fullscreen=true to work on session detail pages Co-Authored-By: Claude Sonnet 4.5 * fix: enable mouse mode for DirectTerminal scrolling DirectTerminal was not scrollable because tmux mouse mode was not enabled for the sessions. The ttyd implementation already had this, but the DirectTerminal WebSocket server was missing it. Changes: - Add spawn import from node:child_process - Enable tmux mouse mode on session connection - Hide tmux status bar for cleaner appearance This makes DirectTerminal scrolling work the same as ttyd terminals. Co-Authored-By: Claude Sonnet 4.5 * fix: reduce scroll speed in DirectTerminal for smoother experience Reduced scroll sensitivity from 3 to 1 lines per wheel tick for more natural scrolling behavior. Also reduced fast scroll (with Alt) from 5 to 3. Changes: - scrollSensitivity: 3 → 1 (normal scroll speed) - fastScrollSensitivity: 5 → 3 (Alt+scroll speed) Co-Authored-By: Claude Sonnet 4.5 * feat: auto-select two different sessions for terminal test page When no query params are provided, the test page now automatically fetches available sessions and picks two different ones for side-by-side comparison. This avoids port conflicts between ttyd and DirectTerminal by default. Changes: - Fetch sessions from /api/sessions on mount - Use first two available sessions as defaults (or fall back to hardcoded) - Query params still work for manual override - Warning only shows when sessions are actually the same Examples: - /dev/terminal-test (auto-picks two sessions) - /dev/terminal-test?old_session=X&new_session=Y (manual override) - /dev/terminal-test?session=X (uses same session for both) Co-Authored-By: Claude Sonnet 4.5 * fix: filter out terminated sessions in terminal test auto-selection The auto-selection was picking terminated/exited sessions, causing WebSocket connection failures when trying to attach to non-existent tmux sessions (PTY exit code 1). Now filters to only use sessions with activity !== "exited" for auto-selection, ensuring terminals can actually connect. Co-Authored-By: Claude Sonnet 4.5 * feat: add server-side active session filtering to API Instead of filtering terminated sessions on the client, added proper API support with ?active=true query parameter to filter server-side. Changes: - GET /api/sessions?active=true - Returns only non-exited sessions - Updated terminal test page to use new API parameter - Properly maintains session/dashboard alignment during filtering - Removed client-side filtering logic This is cleaner, more efficient, and follows proper API design patterns. Co-Authored-By: Claude Sonnet 4.5 * refactor: replace magic strings with ACTIVITY_STATE constants Added ACTIVITY_STATE constants to @composio/ao-core types and replaced all hardcoded "exited", "waiting_input", "blocked" strings with proper constants throughout the codebase. Changes: - Added ACTIVITY_STATE constant object to core/types.ts - Replaced magic strings in API route (/api/sessions) - Replaced magic strings in lib/types.ts (getAttentionLevel) - Properly typed constants with satisfies Record This prevents typos, improves IDE autocomplete, and makes refactoring easier by having a single source of truth for activity state values. Co-Authored-By: Claude Sonnet 4.5 * fix: move WebSocket servers outside src/ to fix webpack build error Moved terminal-websocket.ts and direct-terminal-ws.ts from src/server/ to server/ (outside src/) to prevent Next.js webpack from trying to bundle them with client code. These are standalone Node.js WebSocket servers (not Next.js API routes) that should not be part of the Next.js build. Webpack was failing when encountering node:child_process imports. Changes: - Moved src/server/*.ts to server/*.ts - Updated package.json scripts to point to new location Fixes: Module build failed: UnhandledSchemeError with node:child_process Co-Authored-By: Claude Sonnet 4.5 * fix: update server file path in terminal test page docs Updated documentation to reflect new server file location after moving files from src/server/ to server/. Co-Authored-By: Claude Sonnet 4.5 * fix: import from @composio/ao-core/types to avoid bundling server code The main @composio/ao-core export includes tmux utilities that use node:child_process, which webpack cannot bundle for the client. The package already exports a /types entry point that only includes types and constants (no server utilities). Changes: - Import from @composio/ao-core/types instead of @composio/ao-core - This prevents webpack from trying to bundle tmux.js in client code Fixes webpack error: UnhandledSchemeError with node:child_process Co-Authored-By: Claude Sonnet 4.5 * fix: combine duplicate imports to resolve lint errors * fix: update .env.local.example to match code defaults (port 3003) Addresses bugbot comment: Documentation showed port 3002 but both server (direct-terminal-ws.ts) and client (DirectTerminal.tsx) default to 3003. This mismatch could cause connection failures if developers set only DIRECT_TERMINAL_PORT without NEXT_PUBLIC_DIRECT_TERMINAL_PORT. Updated documentation to reflect actual code defaults. --------- Co-authored-by: Claude Sonnet 4.5 --- eslint.config.js | 2 + packages/core/src/types.ts | 9 + packages/web/.env.local.example | 7 + packages/web/.gitignore | 1 + packages/web/package.json | 14 +- packages/web/server/direct-terminal-ws.ts | 221 ++++++++ .../{src => }/server/terminal-websocket.ts | 17 +- packages/web/src/app/api/sessions/route.ts | 27 +- .../web/src/app/dev/terminal-test/page.tsx | 533 ++++++++++++++++++ packages/web/src/app/test-direct/page.tsx | 84 +++ .../web/src/components/DirectTerminal.tsx | 324 +++++++++++ packages/web/src/components/SessionDetail.tsx | 7 +- packages/web/src/lib/types.ts | 23 +- pnpm-lock.yaml | 421 ++++++++++++++ 14 files changed, 1663 insertions(+), 27 deletions(-) create mode 100644 packages/web/.env.local.example create mode 100644 packages/web/.gitignore create mode 100644 packages/web/server/direct-terminal-ws.ts rename packages/web/{src => }/server/terminal-websocket.ts (95%) create mode 100644 packages/web/src/app/dev/terminal-test/page.tsx create mode 100644 packages/web/src/app/test-direct/page.tsx create mode 100644 packages/web/src/components/DirectTerminal.tsx diff --git a/eslint.config.js b/eslint.config.js index 653ec05ce..fd2c968f5 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -13,6 +13,8 @@ export default tseslint.config( "packages/web/next-env.d.ts", "packages/web/next.config.js", "packages/web/postcss.config.mjs", + "test-clipboard*.mjs", + "test-clipboard*.sh", ], }, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index bd3c31114..65de461eb 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -49,6 +49,15 @@ export type ActivityState = | "blocked" // agent hit an error or is stuck | "exited"; // agent process is no longer running +/** Activity state constants */ +export const ACTIVITY_STATE = { + ACTIVE: "active" as const, + IDLE: "idle" as const, + WAITING_INPUT: "waiting_input" as const, + BLOCKED: "blocked" as const, + EXITED: "exited" as const, +} satisfies Record; + /** A running agent session */ export interface Session { /** Unique session ID, e.g. "my-app-3" */ diff --git a/packages/web/.env.local.example b/packages/web/.env.local.example new file mode 100644 index 000000000..d0260f17a --- /dev/null +++ b/packages/web/.env.local.example @@ -0,0 +1,7 @@ +# Terminal server ports +TERMINAL_PORT=3001 # ttyd (iframe) terminal server +DIRECT_TERMINAL_PORT=3003 # Direct WebSocket terminal server with XDA support + +# Public environment variables (exposed to browser) +NEXT_PUBLIC_TERMINAL_PORT=3001 +NEXT_PUBLIC_DIRECT_TERMINAL_PORT=3003 diff --git a/packages/web/.gitignore b/packages/web/.gitignore new file mode 100644 index 000000000..567609b12 --- /dev/null +++ b/packages/web/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/packages/web/package.json b/packages/web/package.json index fffe337ca..e290378b9 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -5,9 +5,10 @@ "private": true, "type": "module", "scripts": { - "dev": "concurrently \"npm:dev:next\" \"npm:dev:terminal\"", + "dev": "concurrently \"npm:dev:next\" \"npm:dev:terminal\" \"npm:dev:direct-terminal\"", "dev:next": "next dev", - "dev:terminal": "tsx watch src/server/terminal-websocket.ts", + "dev:terminal": "tsx watch server/terminal-websocket.ts", + "dev:direct-terminal": "tsx watch server/direct-terminal-ws.ts", "build": "next build", "start": "next start", "typecheck": "tsc --noEmit", @@ -25,9 +26,14 @@ "@composio/ao-plugin-tracker-github": "workspace:*", "@composio/ao-plugin-tracker-linear": "workspace:*", "@composio/ao-plugin-workspace-worktree": "workspace:*", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-web-links": "^0.12.0", "next": "^15.1.0", + "node-pty": "^1.1.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "ws": "^8.19.0", + "xterm": "^5.3.0" }, "devDependencies": { "@tailwindcss/postcss": "^4.0.0", @@ -35,9 +41,11 @@ "@testing-library/react": "^16.1.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", + "@types/ws": "^8.18.1", "@vitejs/plugin-react": "^4.3.0", "concurrently": "^9.2.1", "jsdom": "^25.0.0", + "node-gyp": "^12.2.0", "playwright": "^1.49.0", "tailwindcss": "^4.0.0", "tsx": "^4.19.0", diff --git a/packages/web/server/direct-terminal-ws.ts b/packages/web/server/direct-terminal-ws.ts new file mode 100644 index 000000000..51f0c8551 --- /dev/null +++ b/packages/web/server/direct-terminal-ws.ts @@ -0,0 +1,221 @@ +/** + * Direct WebSocket terminal server using node-pty. + * Connects browser xterm.js directly to tmux sessions via WebSocket. + * + * This bypasses ttyd and gives us control over terminal initialization, + * allowing us to implement the XDA (Extended Device Attributes) handler + * that tmux requires for clipboard support. + */ + +import { createServer } from "node:http"; +import { spawn } from "node:child_process"; +import { WebSocketServer, WebSocket } from "ws"; +import { spawn as ptySpawn, type IPty } from "node-pty"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { homedir, userInfo } from "node:os"; +import { loadConfig } from "@composio/ao-core"; + +// Load config with fallback +let config: ReturnType; +try { + config = loadConfig(); +} catch (err) { + console.warn("[DirectTerminal] Could not load config, using defaults:", err instanceof Error ? err.message : String(err)); + // Fallback to default config + config = { + dataDir: join(homedir(), ".ao/sessions"), + } as ReturnType; +} + +interface TerminalSession { + sessionId: string; + pty: IPty; + ws: WebSocket; +} + +const activeSessions = new Map(); + +/** + * Create HTTP server with WebSocket upgrade handling + */ +const server = createServer((req, res) => { + // Health check endpoint + if (req.url === "/health") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ + active: activeSessions.size, + sessions: Array.from(activeSessions.keys()), + })); + return; + } + + res.writeHead(404); + res.end("Not found"); +}); + +/** + * WebSocket server for terminal connections + */ +const wss = new WebSocketServer({ + server, + path: "/ws", +}); + +wss.on("connection", (ws, req) => { + const url = new URL(req.url ?? "/", "ws://localhost"); + const sessionId = url.searchParams.get("session"); + + if (!sessionId) { + console.error("[DirectTerminal] Missing session parameter"); + ws.close(1008, "Missing session parameter"); + return; + } + + // Validate session ID + if (!/^[a-zA-Z0-9_-]+$/.test(sessionId)) { + console.error("[DirectTerminal] Invalid session ID:", sessionId); + ws.close(1008, "Invalid session ID"); + return; + } + + // Validate session exists + const sessionPath = join(config.dataDir, sessionId); + if (!existsSync(sessionPath)) { + console.error("[DirectTerminal] Session not found:", sessionId); + ws.close(1008, "Session not found"); + return; + } + + console.log(`[DirectTerminal] New connection for session: ${sessionId}`); + + // Enable mouse mode for scrollback support + const mouseProc = spawn("tmux", ["set-option", "-t", sessionId, "mouse", "on"]); + mouseProc.on("error", (err) => { + console.error(`[DirectTerminal] Failed to set mouse mode for ${sessionId}:`, err.message); + }); + + // Hide the green status bar for cleaner appearance + const statusProc = spawn("tmux", ["set-option", "-t", sessionId, "status", "off"]); + statusProc.on("error", (err) => { + console.error(`[DirectTerminal] Failed to hide status bar for ${sessionId}:`, err.message); + }); + + // Spawn PTY attached to tmux session + // Use tmux from PATH for cross-platform compatibility + const tmuxPath = "tmux"; + + // Build complete environment - node-pty requires proper env setup + const homeDir = process.env.HOME || homedir(); + const currentUser = process.env.USER || userInfo().username; + const env = { + HOME: homeDir, + SHELL: process.env.SHELL || "/bin/bash", + USER: currentUser, + PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin", + TERM: "xterm-256color", + LANG: process.env.LANG || "en_US.UTF-8", + TMPDIR: process.env.TMPDIR || "/tmp", + }; + + let pty: IPty; + try { + console.log(`[DirectTerminal] Spawning PTY: ${tmuxPath} attach-session -t ${sessionId}`); + console.log(`[DirectTerminal] CWD: ${homeDir}, ENV keys:`, Object.keys(env).join(", ")); + + pty = ptySpawn(tmuxPath, ["attach-session", "-t", sessionId], { + name: "xterm-256color", + cols: 80, + rows: 24, + cwd: homeDir, + env, + }); + + console.log(`[DirectTerminal] PTY spawned successfully`); + } catch (err) { + console.error(`[DirectTerminal] Failed to spawn PTY:`, err); + ws.close(1011, `Failed to spawn terminal: ${err instanceof Error ? err.message : String(err)}`); + return; + } + + const session: TerminalSession = { sessionId, pty, ws }; + activeSessions.set(sessionId, session); + + // PTY -> WebSocket + pty.onData((data) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(data); + } + }); + + // PTY exit + pty.onExit(({ exitCode }) => { + console.log(`[DirectTerminal] PTY exited for ${sessionId} with code ${exitCode}`); + activeSessions.delete(sessionId); + if (ws.readyState === WebSocket.OPEN) { + ws.close(1000, "Terminal session ended"); + } + }); + + // WebSocket -> PTY + ws.on("message", (data) => { + const message = data.toString("utf8"); + + // Handle resize messages (sent by xterm.js FitAddon) + if (message.startsWith("{")) { + try { + const parsed = JSON.parse(message) as { type?: string; cols?: number; rows?: number }; + if (parsed.type === "resize" && parsed.cols && parsed.rows) { + pty.resize(parsed.cols, parsed.rows); + return; + } + } catch { + // Not JSON, treat as terminal input + } + } + + // Normal terminal input + pty.write(message); + }); + + // WebSocket close + ws.on("close", () => { + console.log(`[DirectTerminal] WebSocket closed for ${sessionId}`); + activeSessions.delete(sessionId); + pty.kill(); + }); + + // WebSocket error + ws.on("error", (err) => { + console.error(`[DirectTerminal] WebSocket error for ${sessionId}:`, err.message); + activeSessions.delete(sessionId); + pty.kill(); + }); +}); + +const PORT = parseInt(process.env.DIRECT_TERMINAL_PORT ?? "3003", 10); + +server.listen(PORT, () => { + console.log(`[DirectTerminal] WebSocket server listening on port ${PORT}`); +}); + +// Graceful shutdown +function shutdown(signal: string) { + console.log(`[DirectTerminal] Received ${signal}, shutting down...`); + for (const [, session] of activeSessions) { + session.pty.kill(); + session.ws.close(1001, "Server shutting down"); + } + server.close(() => { + console.log("[DirectTerminal] Server closed"); + process.exit(0); + }); + const forceExitTimer = setTimeout(() => { + console.error("[DirectTerminal] Forced shutdown after timeout"); + process.exit(1); + }, 5000); + forceExitTimer.unref(); +} + +process.on("SIGINT", () => shutdown("SIGINT")); +process.on("SIGTERM", () => shutdown("SIGTERM")); diff --git a/packages/web/src/server/terminal-websocket.ts b/packages/web/server/terminal-websocket.ts similarity index 95% rename from packages/web/src/server/terminal-websocket.ts rename to packages/web/server/terminal-websocket.ts index 365e0141a..7f9b724b7 100644 --- a/packages/web/src/server/terminal-websocket.ts +++ b/packages/web/server/terminal-websocket.ts @@ -122,7 +122,7 @@ function getOrSpawnTtyd(sessionId: string): TtydInstance { console.log(`[Terminal] Spawning ttyd for ${sessionId} on port ${port}`); - // Enable mouse mode so scroll works as scrollback, not input cycling + // Enable mouse mode for scrollback support const mouseProc = spawn("tmux", ["set-option", "-t", sessionId, "mouse", "on"]); mouseProc.on("error", (err) => { console.error(`[Terminal] Failed to set mouse mode for ${sessionId}:`, err.message); @@ -158,8 +158,11 @@ function getOrSpawnTtyd(sessionId: string): TtydInstance { const current = instances.get(sessionId); if (current?.process === proc) { instances.delete(sessionId); - // Recycle port for reuse - availablePorts.add(port); + // Only recycle port on clean exit (code 0), not on errors + // Failed ttyd processes may leave ports in TIME_WAIT state + if (code === 0) { + availablePorts.add(port); + } } }); @@ -169,8 +172,7 @@ function getOrSpawnTtyd(sessionId: string): TtydInstance { const current = instances.get(sessionId); if (current?.process === proc) { instances.delete(sessionId); - // Recycle port for reuse - availablePorts.add(port); + // Don't recycle port on error - may still be in use or TIME_WAIT } // Kill any running process try { @@ -192,7 +194,7 @@ const server = createServer(async (req, res) => { // CORS for dashboard - allow requests from the same host as the dashboard // TODO: Replace with proper session-based authentication const origin = req.headers.origin; - if (origin) { + if (origin && origin !== "null") { // Extract hostname from origin and compare with request host try { const originUrl = new URL(origin); @@ -204,6 +206,9 @@ const server = createServer(async (req, res) => { } catch { // Invalid origin URL, don't set CORS header } + } else { + // Allow null origin (file:// or local HTML files) + res.setHeader("Access-Control-Allow-Origin", "*"); } res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); diff --git a/packages/web/src/app/api/sessions/route.ts b/packages/web/src/app/api/sessions/route.ts index 9e18c10f9..5a06060f3 100644 --- a/packages/web/src/app/api/sessions/route.ts +++ b/packages/web/src/app/api/sessions/route.ts @@ -1,4 +1,4 @@ -import type { Session, ProjectConfig } from "@composio/ao-core"; +import { ACTIVITY_STATE, type Session, type ProjectConfig } from "@composio/ao-core"; import { NextResponse } from "next/server"; import { getServices, getSCM, getTracker } from "@/lib/services"; import { sessionToDashboard, enrichSessionPR, enrichSessionIssue, computeStats } from "@/lib/serialize"; @@ -23,15 +23,32 @@ function resolveProject( return firstKey ? projects[firstKey] : undefined; } -/** GET /api/sessions — List all sessions with full state */ -export async function GET() { +/** GET /api/sessions — List all sessions with full state + * Query params: + * - active=true: Only return non-exited sessions + */ +export async function GET(request: Request) { try { + const { searchParams } = new URL(request.url); + const activeOnly = searchParams.get("active") === "true"; + const { config, registry, sessionManager } = await getServices(); const coreSessions = await sessionManager.list(); // Filter out orchestrator sessions — they get their own button, not a card - const workerSessions = coreSessions.filter((s) => !s.id.endsWith("-orchestrator")); - const dashboardSessions = workerSessions.map(sessionToDashboard); + let workerSessions = coreSessions.filter((s) => !s.id.endsWith("-orchestrator")); + + // Convert to dashboard format + let dashboardSessions = workerSessions.map(sessionToDashboard); + + // Filter to active sessions only if requested (keep workerSessions in sync) + if (activeOnly) { + const activeIndices = dashboardSessions + .map((s, i) => (s.activity !== ACTIVITY_STATE.EXITED ? i : -1)) + .filter((i) => i !== -1); + workerSessions = activeIndices.map((i) => workerSessions[i]); + dashboardSessions = activeIndices.map((i) => dashboardSessions[i]); + } // Enrich issue labels using tracker plugin (synchronous) workerSessions.forEach((core, i) => { diff --git a/packages/web/src/app/dev/terminal-test/page.tsx b/packages/web/src/app/dev/terminal-test/page.tsx new file mode 100644 index 000000000..c928e9042 --- /dev/null +++ b/packages/web/src/app/dev/terminal-test/page.tsx @@ -0,0 +1,533 @@ +"use client"; + +import { DirectTerminal } from "@/components/DirectTerminal"; +import { Terminal } from "@/components/Terminal"; +import { useSearchParams } from "next/navigation"; +import { useState, useEffect, Suspense } from "react"; + +// Force dynamic rendering (required for useSearchParams) +export const dynamic = "force-dynamic"; + +/** + * Terminal Implementation Test & Documentation + * + * This page compares two terminal implementations and documents why DirectTerminal + * (with XDA support) was necessary for proper clipboard functionality. + * + * By default, automatically picks two different sessions to avoid port conflicts. + * + * Examples: + * - http://localhost:3000/dev/terminal-test (auto-picks two different sessions) + * - http://localhost:3000/dev/terminal-test?old_session=ao-orchestrator&new_session=ao-20 + * - http://localhost:3000/dev/terminal-test?session=ao-20 (uses same session for both) + * + * Note: Using different sessions for old/new avoids port conflicts when both render simultaneously. + */ +function TerminalTestPageContent() { + const searchParams = useSearchParams(); + const [availableSessions, setAvailableSessions] = useState([]); + const [showComparison, setShowComparison] = useState(true); + + // Fetch available sessions on mount (only active ones) + useEffect(() => { + fetch("/api/sessions?active=true") + .then((res) => res.json()) + .then((data) => { + if (data.sessions && Array.isArray(data.sessions)) { + const ids = data.sessions.map((s: { id: string }) => s.id); + setAvailableSessions(ids); + } + }) + .catch((err) => { + console.error("Failed to fetch sessions:", err); + }); + }, []); + + // Determine which sessions to use + const sessionParam = searchParams.get("session"); + const oldSessionParam = searchParams.get("old_session"); + const newSessionParam = searchParams.get("new_session"); + + // If no params provided, use first two available sessions (or fallback to defaults) + const defaultOldSession = availableSessions[0] || "ao-orchestrator"; + const defaultNewSession = availableSessions[1] || availableSessions[0] || "ao-orchestrator"; + + // Allow overriding individual sessions + const oldSessionId = oldSessionParam || (sessionParam || defaultOldSession); + const newSessionId = newSessionParam || (sessionParam || defaultNewSession); + + return ( +
+
+ {/* Header */} +
+

+ Terminal Implementation Test & Documentation +

+

+ Comparing sessions: + OLD: {oldSessionId} + NEW: {newSessionId} +

+
+ + {/* The Problem */} +
+

🐛 The Problem

+
+

+ Issue: Browser clipboard (Cmd+C/Ctrl+C) + only worked when an iTerm2 client was attached to the tmux session. +

+

+ Impact: Users had to keep iTerm2 tabs open + in the background for clipboard to work in the web dashboard. +

+

+ Investigation time: 12+ hours of debugging + across tmux, ttyd, xterm.js, and macOS clipboard systems. +

+
+
+ + {/* Root Cause */} +
+

🔍 Root Cause Analysis

+
+
+

+ 1. How tmux Clipboard Works (OSC 52) +

+
    +
  • tmux uses OSC 52 escape sequences to synchronize clipboard with terminals
  • +
  • Format: \x1b]52;c;<base64>\x07
  • +
  • Terminal must support OSC 52 and have proper capabilities declared
  • +
+
+ +
+

+ 2. tmux Capability Detection (XDA) +

+
    +
  • tmux queries terminal capabilities using Device Attributes (DA/XDA)
  • +
  • + XDA query: CSI > q (also called XTVERSION) +
  • +
  • + Terminal responds with identification string containing terminal type (e.g., "XTerm(370)", "iTerm2 ") +
  • +
  • Based on response, tmux enables features like TTYC_MS (clipboard support)
  • +
+
+ +
+

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)') + +
  • +
  • Without XDA response, tmux doesn't recognize the terminal as clipboard-capable
  • +
  • tmux never emits OSC 52 sequences → clipboard doesn't work
  • +
+
+
+ +
+

+ 4. Why iTerm2 "Fixed" It +

+
    +
  • + iTerm2 sends proper XDA response identifying itself as{" "} + "iTerm2 " +
  • +
  • tmux detects this and enables clipboard for the entire session
  • +
  • OSC 52 sequences are then sent to ALL clients, including browser (ttyd/xterm.js)
  • +
  • This is why clipboard "magically worked" when iTerm2 was attached
  • +
+
+
+
+ + {/* The Solution */} +
+

✅ The Solution

+
+
+

DirectTerminal Implementation

+

+ 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
+  () => {
+    // Respond with XTerm identification
+    terminal.write("\\x1bP>|XTerm(370)\\x1b\\\\");
+    return true;
+  }
+);`}
+                
+              
+
+ +
+

What This Does

+
    +
  1. Intercepts XDA queries from tmux
  2. +
  3. + Responds with XTerm(370) identification +
  4. +
  5. tmux detects "XTerm(" in response and enables TTYC_MS capability
  6. +
  7. OSC 52 sequences now flow: tmux → WebSocket → xterm.js → navigator.clipboard
  8. +
  9. + Clipboard works without iTerm2! +
  10. +
+
+ +
+

Architecture Changes

+
    +
  • + Old: Browser → ttyd (iframe) → tmux +
  • +
  • + New: Browser → Custom WebSocket (node-pty) → tmux +
  • +
  • Bypasses ttyd for direct control over terminal initialization
  • +
  • Full control over escape sequence handling and capabilities
  • +
+
+
+
+ + {/* Node Version Requirement */} +
+

⚠️ Node Version Requirement

+
+
+

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

+
+ +
+

Why Node 20?

+
    +
  • + node-pty 1.1.0 is incompatible with Node 25+ +
  • +
  • + Error on Node 25.6.1:{" "} + posix_spawnp failed +
  • +
  • + Root cause: node-pty's native module (darwin-arm64 prebuild) fails to spawn processes on Node 25 +
  • +
  • No darwin-arm64 prebuilds available that work with Node 25
  • +
  • Building from source also fails with the same error
  • +
+
+ +
+

When Can We Upgrade?

+
    +
  • + Option 1: Wait for node-pty 1.2.0 stable release with Node 25+ support +
  • +
  • + Option 2: Test node-pty beta versions (currently 1.2.0-beta.11) +
  • +
  • + Option 3: Switch to alternative PTY library (e.g., xterm-pty, node-child-pty) +
  • +
+
+ +
+

+ ⚡ Testing Instructions for Upgrades +

+

Before upgrading Node or node-pty:

+
    +
  1. + Test node-pty directly:{" "} + + node -e "const pty = require('node-pty'); pty.spawn('/bin/bash', [], {})" + +
  2. +
  3. + If no posix_spawnp failed error, proceed +
  4. +
  5. Start dev servers and open this page
  6. +
  7. Test DirectTerminal: verify connection, clipboard, resize
  8. +
  9. Test multiple sessions to ensure stability
  10. +
  11. If all tests pass, upgrade is safe
  12. +
+
+
+
+ + {/* Comparison Toggle */} +
+
+ + + Compare old (ttyd iframe) vs new (DirectTerminal with XDA) + +
+ {oldSessionId === newSessionId && ( +
+ ⚠️ Using same session for both terminals. To avoid port conflicts, use different sessions: + ?old_session=ao-orchestrator&new_session=ao-20 +
+ )} +
+ + {/* Side-by-Side Comparison */} + {showComparison && ( +
+
+ {/* Old Implementation */} +
+
+ + OLD + +

+ ttyd iframe (no XDA) +

+
+
+ ❌ Clipboard requires iTerm2 attached +
+ ✅ Battle-tested (ttyd) +
+ ❌ No control over capabilities +
+ +
+ + {/* New Implementation */} +
+
+ + NEW + +

+ DirectTerminal (with XDA) +

+
+
+ ✅ Clipboard works standalone +
+ ✅ Full control over terminal +
+ ⚠️ Requires Node 20.x (node-pty) +
+ +
+
+ +
+

+ 🧪 How to Test Clipboard +

+
    +
  1. Drag-select text in BOTH terminals above
  2. +
  3. Press Cmd+C (macOS) or Ctrl+Shift+C (Linux/Windows)
  4. +
  5. Paste in another application (Notes, VS Code, etc.)
  6. +
  7. + + Expected: RIGHT terminal works, LEFT may not (unless iTerm2 is attached) + +
  8. +
+
+
+ )} + + {/* Debugging Journey */} +
+

🔬 The Debugging Journey

+ +
+
+

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

+
+ +
+

❌ What We Tried (That Didn't Work)

+
    +
  1. + Suspected ttyd clipboard handling - Spent hours analyzing ttyd source code, checking OSC 52 + passthrough. ttyd was innocent - it passes escape sequences correctly. +
  2. +
  3. + Blamed xterm.js configuration - Tried every possible xterm.js option, clipboard addon + configurations, terminal type settings. None made a difference. +
  4. +
  5. + Investigated macOS clipboard permissions - Checked browser permissions, sandbox attributes, + navigator.clipboard API. All were correct. +
  6. +
  7. + Suspected WebSocket encoding issues - Checked binary vs text mode, UTF-8 encoding, + base64 handling. All correct. +
  8. +
  9. + 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. +
  10. +
+
+ +
+

💡 The Breakthrough

+
+

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

+
+                  
+{`terminal.parser.registerCsiHandler(
+  { prefix: ">", final: "q" },
+  () => {
+    terminal.write("\\x1bP>|XTerm(370)\\x1b\\\\");
+    return true;
+  }
+);`}
+                  
+                
+

+ This single handler made tmux recognize our terminal as clipboard-capable. Clipboard immediately started working. +

+
+
+ +
+

🎯 How We Finally Figured It Out

+
    +
  1. + Deep-dive into tmux source code - Used DeepWiki.com to analyze tmux's terminal capability + detection logic in tty-keys.c and{" "} + tty.c +
  2. +
  3. + Discovered XDA queries - Found that tmux sends{" "} + CSI > q (XTVERSION) to detect terminal type +
  4. +
  5. + Traced the "iTerm2 magic" - Realized why clipboard worked when iTerm2 was attached: + iTerm2 responds to XDA queries, enabling clipboard for the entire tmux session +
  6. +
  7. + 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)') + +
  8. +
  9. + Implemented custom handler - Used xterm.js parser API to register our own XDA handler +
  10. +
+
+ +
+

+ ⚡ How We Could Have Figured It Out Faster +

+
    +
  1. + Start with tmux source code first - Instead of debugging xterm.js and ttyd, we should have + immediately checked how tmux detects terminal capabilities +
  2. +
  3. + Monitor escape sequences - Running{" "} + tmux -vvv or using a terminal protocol analyzer + would have revealed the XDA queries being sent +
  4. +
  5. + Compare working vs broken scenarios - Should have captured and diffed the escape sequences + when iTerm2 was attached vs not attached earlier +
  6. +
  7. + Check xterm.js issues/limitations first - A GitHub search for "xterm.js XDA" or + "xterm.js device attributes" would have revealed it's unimplemented +
  8. +
  9. + Read tmux documentation on terminal types - tmux man page mentions terminal capability + detection, but we focused on clipboard settings instead +
  10. +
+
+ +
+

📚 Key Resources

+
    +
  • tmux source analysis: DeepWiki.com (Feb 15, 2026)
  • +
  • xterm.js parser API documentation
  • +
  • XTerm Control Sequences: XTVERSION / Device Attributes
  • +
  • tmux tty-keys.c: Terminal type detection logic
  • +
+
+
+
+ + {/* 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 +
  • +
+
+ + {/* Footer */} +
+

Investigation: Feb 15-16, 2026 • Duration: 12+ hours • Status: ✅ Resolved

+

Node 20.20.0 • node-pty 1.1.0 • xterm.js 5.3.0

+
+
+
+ ); +} + +export default function TerminalTestPage() { + return ( + Loading...}> + + + ); +} diff --git a/packages/web/src/app/test-direct/page.tsx b/packages/web/src/app/test-direct/page.tsx new file mode 100644 index 000000000..053768136 --- /dev/null +++ b/packages/web/src/app/test-direct/page.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { DirectTerminal } from "@/components/DirectTerminal"; +import { useSearchParams } from "next/navigation"; +import { Suspense } from "react"; + +// Force dynamic rendering (required for useSearchParams) +export const dynamic = "force-dynamic"; + +/** + * Test page for DirectTerminal with XDA clipboard support. + * + * Examples: + * - http://localhost:3000/test-direct + * - http://localhost:3000/test-direct?session=ao-20 + * - http://localhost:3000/test-direct?session=ao-20&fullscreen=true + * + * This uses native xterm.js with registered XDA handler, + * which should enable clipboard (OSC 52) in tmux without + * requiring iTerm2 attachment. + */ +function TestDirectPageContent() { + const searchParams = useSearchParams(); + const startFullscreen = searchParams.get("fullscreen") === "true"; + const sessionId = searchParams.get("session") || "ao-orchestrator"; + return ( +
+
+
+

+ DirectTerminal Test - XDA Clipboard Support +

+

+ This terminal has XDA (Extended Device Attributes) handler registered. +
+ tmux should recognize it as XTerm and enable clipboard support (OSC 52). +

+
+

+ Testing: {sessionId} +

+

Test Steps:

+
    +
  1. Connected to tmux session: {sessionId}
  2. +
  3. + Verify XDA badge shows ✓ XDA +
  4. +
  5. Drag-select text in the terminal
  6. +
  7. Press Cmd+C (macOS) or Ctrl+C (Linux/Windows)
  8. +
  9. Paste elsewhere to verify clipboard works
  10. +
  11. + Expected: Clipboard works without iTerm2 attachment! +
  12. +
+
+
+

Technical Details:

+
    +
  • Registers CSI > q (XDA) handler in xterm.js parser
  • +
  • Responds with DCS > | XTerm(370) ST sequence
  • +
  • tmux detects "XTerm(" and enables TTYC_MS (clipboard capability)
  • +
  • OSC 52 sequences flow: tmux → WebSocket → xterm.js → navigator.clipboard
  • +
+
+
+ + {/* Test with specified session - key forces remount on fullscreen change */} + +
+
+ ); +} + +export default function TestDirectPage() { + return ( + Loading...}> + + + ); +} diff --git a/packages/web/src/components/DirectTerminal.tsx b/packages/web/src/components/DirectTerminal.tsx new file mode 100644 index 000000000..27deb6524 --- /dev/null +++ b/packages/web/src/components/DirectTerminal.tsx @@ -0,0 +1,324 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { useRouter, usePathname, useSearchParams } from "next/navigation"; +import { cn } from "@/lib/cn"; + +// Import xterm CSS (must be imported in client component) +import "xterm/css/xterm.css"; + +// Dynamically import xterm types for TypeScript +import type { Terminal as TerminalType } from "xterm"; +import type { FitAddon as FitAddonType } from "@xterm/addon-fit"; + +interface DirectTerminalProps { + sessionId: string; + startFullscreen?: boolean; +} + +/** + * Direct xterm.js terminal with native WebSocket connection. + * Implements Extended Device Attributes (XDA) handler to enable + * tmux clipboard support (OSC 52) without requiring iTerm2 attachment. + * + * Based on DeepWiki analysis: + * - tmux queries for XDA (CSI > q / XTVERSION) to detect terminal type + * - When tmux sees "XTerm(" in response, it enables TTYC_MS (clipboard) + * - xterm.js doesn't implement XDA by default, so we register custom handler + */ +export function DirectTerminal({ sessionId, startFullscreen = false }: DirectTerminalProps) { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const terminalRef = useRef(null); + const terminalInstance = useRef(null); + const fitAddon = useRef(null); + const ws = useRef(null); + const [fullscreen, setFullscreen] = useState(startFullscreen); + const [status, setStatus] = useState<"connecting" | "connected" | "error">("connecting"); + const [error, setError] = useState(null); + + // Update URL when fullscreen changes + useEffect(() => { + const params = new URLSearchParams(searchParams.toString()); + + if (fullscreen) { + params.set("fullscreen", "true"); + } else { + params.delete("fullscreen"); + } + + const newUrl = params.toString() ? `${pathname}?${params.toString()}` : pathname; + router.replace(newUrl, { scroll: false }); + }, [fullscreen, pathname, router, searchParams]); + + useEffect(() => { + if (!terminalRef.current) return; + // Prevent retry loop on persistent errors + if (error && status === "error") return; + + // Dynamically import xterm.js to avoid SSR issues + let mounted = true; + let cleanup: (() => void) | null = null; + + Promise.all([ + import("xterm").then((mod) => mod.Terminal), + import("@xterm/addon-fit").then((mod) => mod.FitAddon), + import("@xterm/addon-web-links").then((mod) => mod.WebLinksAddon), + ]) + .then(([Terminal, FitAddon, WebLinksAddon]) => { + if (!mounted || !terminalRef.current) return; + + // Initialize xterm.js Terminal + const terminal = new Terminal({ + cursorBlink: true, + fontSize: 14, + fontFamily: 'Menlo, Monaco, "Courier New", monospace', + theme: { + background: "#000000", + foreground: "#ffffff", + cursor: "#ffffff", + cursorAccent: "#000000", + selectionBackground: "rgba(255, 255, 255, 0.3)", + }, + scrollback: 10000, + allowProposedApi: true, // Required for some advanced features + // Smooth scrolling configuration + fastScrollModifier: "alt", + fastScrollSensitivity: 3, + scrollSensitivity: 1, + }); + + // Add FitAddon for responsive sizing + const fit = new FitAddon(); + terminal.loadAddon(fit); + fitAddon.current = fit; + + // Add WebLinksAddon for clickable links + const webLinks = new WebLinksAddon(); + terminal.loadAddon(webLinks); + + // **CRITICAL FIX**: Register XDA (Extended Device Attributes) handler + // This makes tmux recognize our terminal and enable clipboard support + terminal.parser.registerCsiHandler( + { prefix: ">", final: "q" }, // CSI > q is XTVERSION / XDA + () => { + // Respond with XTerm identification that tmux recognizes + // tmux looks for "XTerm(" in the response (see tmux tty-keys.c) + // Format: DCS > | XTerm(version) ST + // DCS = \x1bP, ST = \x1b\\ + terminal.write("\x1bP>|XTerm(370)\x1b\\"); + console.log("[DirectTerminal] Sent XDA response for clipboard support"); + return true; // Handled + }, + ); + + // Open terminal in DOM + terminal.open(terminalRef.current); + terminalInstance.current = terminal; + + // Fit terminal to container + fit.fit(); + + // Connect WebSocket + const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const hostname = window.location.hostname; + const port = process.env.NEXT_PUBLIC_DIRECT_TERMINAL_PORT ?? "3003"; + const wsUrl = `${protocol}//${hostname}:${port}/ws?session=${encodeURIComponent(sessionId)}`; + + console.log("[DirectTerminal] Connecting to:", wsUrl); + const websocket = new WebSocket(wsUrl); + ws.current = websocket; + + websocket.binaryType = "arraybuffer"; + + websocket.onopen = () => { + console.log("[DirectTerminal] WebSocket connected"); + setStatus("connected"); + setError(null); + + // Send initial size + websocket.send( + JSON.stringify({ + type: "resize", + cols: terminal.cols, + rows: terminal.rows, + }), + ); + }; + + websocket.onmessage = (event) => { + const data = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data); + terminal.write(data); + }; + + websocket.onerror = (event) => { + console.error("[DirectTerminal] WebSocket error:", event); + setStatus("error"); + setError("WebSocket connection error"); + }; + + websocket.onclose = (event) => { + console.log("[DirectTerminal] WebSocket closed:", event.code, event.reason); + if (status === "connected") { + setStatus("error"); + setError("Connection closed"); + } + }; + + // Terminal input → WebSocket + const disposable = terminal.onData((data) => { + if (websocket.readyState === WebSocket.OPEN) { + websocket.send(data); + } + }); + + // Handle window resize + const handleResize = () => { + if (fit && websocket.readyState === WebSocket.OPEN) { + fit.fit(); + websocket.send( + JSON.stringify({ + type: "resize", + cols: terminal.cols, + rows: terminal.rows, + }), + ); + } + }; + + window.addEventListener("resize", handleResize); + + // Store cleanup function to be called from useEffect cleanup + cleanup = () => { + window.removeEventListener("resize", handleResize); + disposable.dispose(); + websocket.close(); + terminal.dispose(); + }; + }) + .catch((err) => { + console.error("[DirectTerminal] Failed to load xterm.js:", err); + setStatus("error"); + setError("Failed to load terminal"); + }); + + return () => { + mounted = false; + cleanup?.(); + }; + }, [sessionId]); + + // Re-fit terminal when fullscreen changes + useEffect(() => { + const fit = fitAddon.current; + const terminal = terminalInstance.current; + const websocket = ws.current; + const container = terminalRef.current; + + if (!fit || !terminal || !websocket || websocket.readyState !== WebSocket.OPEN || !container) { + return; + } + + let resizeAttempts = 0; + const maxAttempts = 10; + + const resizeTerminal = () => { + resizeAttempts++; + + // Get container dimensions + const rect = container.getBoundingClientRect(); + const expectedHeight = rect.height; + + // Check if container has reached target dimensions (within 10px tolerance) + const isFullscreenTarget = fullscreen ? expectedHeight > window.innerHeight - 100 : expectedHeight < 700; + + if (!isFullscreenTarget && resizeAttempts < maxAttempts) { + // Container hasn't reached target size yet, try again + requestAnimationFrame(resizeTerminal); + return; + } + + // Container is at target size, now resize terminal + terminal.refresh(0, terminal.rows - 1); + fit.fit(); + terminal.refresh(0, terminal.rows - 1); + + // Send new size to server + websocket.send( + JSON.stringify({ + type: "resize", + cols: terminal.cols, + rows: terminal.rows, + }), + ); + }; + + // Start resize polling + requestAnimationFrame(resizeTerminal); + + // Also try on transitionend + const handleTransitionEnd = (e: TransitionEvent) => { + if (e.target === container.parentElement) { + resizeAttempts = 0; + setTimeout(() => requestAnimationFrame(resizeTerminal), 50); + } + }; + + const parent = container.parentElement; + parent?.addEventListener("transitionend", handleTransitionEnd); + + // Backup timers in case RAF polling doesn't work + const timer1 = setTimeout(() => { resizeAttempts = 0; resizeTerminal(); }, 300); + const timer2 = setTimeout(() => { resizeAttempts = 0; resizeTerminal(); }, 600); + + return () => { + parent?.removeEventListener("transitionend", handleTransitionEnd); + clearTimeout(timer1); + clearTimeout(timer2); + }; + }, [fullscreen]); + + const statusColor = + status === "connected" ? "bg-[#3fb950]" : status === "error" ? "bg-[#f85149]" : "bg-[#d29922] animate-pulse"; + + const statusText = + status === "connected" ? "Connected" : status === "error" ? error ?? "Error" : "Connecting..."; + + const statusTextColor = + status === "connected" + ? "text-[var(--color-accent-green)]" + : status === "error" + ? "text-[var(--color-accent-red)]" + : "text-[var(--color-text-muted)]"; + + return ( +
+
+
+ {sessionId} + {statusText} + + XDA + + +
+
+
+ ); +} diff --git a/packages/web/src/components/SessionDetail.tsx b/packages/web/src/components/SessionDetail.tsx index 1f9164f80..9c0204e52 100644 --- a/packages/web/src/components/SessionDetail.tsx +++ b/packages/web/src/components/SessionDetail.tsx @@ -1,12 +1,13 @@ "use client"; import { useState, useEffect } from "react"; +import { useSearchParams } from "next/navigation"; import { type DashboardSession, type DashboardPR, } from "@/lib/types"; import { CICheckList } from "./CIBadge"; -import { Terminal } from "./Terminal"; +import { DirectTerminal } from "./DirectTerminal"; interface SessionDetailProps { session: DashboardSession; @@ -105,6 +106,8 @@ async function askAgentToFix( } export function SessionDetail({ session }: SessionDetailProps) { + const searchParams = useSearchParams(); + const startFullscreen = searchParams.get("fullscreen") === "true"; const pr = session.pr; const activity = activityLabel[session.activity] ?? { label: session.activity, @@ -236,7 +239,7 @@ export function SessionDetail({ session }: SessionDetailProps) { Terminal - +
diff --git a/packages/web/src/lib/types.ts b/packages/web/src/lib/types.ts index 65a83177d..696020e6c 100644 --- a/packages/web/src/lib/types.ts +++ b/packages/web/src/lib/types.ts @@ -15,16 +15,17 @@ export type { ReviewDecision, MergeReadiness, PRState, -} from "@composio/ao-core"; +} from "@composio/ao-core/types"; -import type { - CICheck as CoreCICheck, - MergeReadiness, - CIStatus, - SessionStatus, - ActivityState, - ReviewDecision, -} from "@composio/ao-core"; +import { + ACTIVITY_STATE, + type CICheck as CoreCICheck, + type MergeReadiness, + type CIStatus, + type SessionStatus, + type ActivityState, + type ReviewDecision, +} from "@composio/ao-core/types"; /** * Attention zone priority level, ordered by human action urgency: @@ -166,7 +167,7 @@ export function getAttentionLevel(session: DashboardSession): AttentionLevel { } // ── Respond: agent is waiting for human input ───────────────────── - if (session.activity === "waiting_input" || session.activity === "blocked") { + if (session.activity === ACTIVITY_STATE.WAITING_INPUT || session.activity === ACTIVITY_STATE.BLOCKED) { return "respond"; } if ( @@ -177,7 +178,7 @@ export function getAttentionLevel(session: DashboardSession): AttentionLevel { return "respond"; } // Exited agent with non-terminal status = crashed, needs human attention - if (session.activity === "exited") { + if (session.activity === ACTIVITY_STATE.EXITED) { return "respond"; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1157163e8..8d9fdfd9e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -457,15 +457,30 @@ importers: '@composio/ao-plugin-workspace-worktree': specifier: workspace:* version: link:../plugins/workspace-worktree + '@xterm/addon-fit': + specifier: ^0.11.0 + version: 0.11.0 + '@xterm/addon-web-links': + specifier: ^0.12.0 + version: 0.12.0 next: specifier: ^15.1.0 version: 15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + node-pty: + specifier: ^1.1.0 + version: 1.1.0 react: specifier: ^19.0.0 version: 19.2.4 react-dom: specifier: ^19.0.0 version: 19.2.4(react@19.2.4) + ws: + specifier: ^8.19.0 + version: 8.19.0 + xterm: + specifier: ^5.3.0 + version: 5.3.0 devDependencies: '@tailwindcss/postcss': specifier: ^4.0.0 @@ -482,6 +497,9 @@ importers: '@types/react-dom': specifier: ^19.0.0 version: 19.2.3(@types/react@19.2.14) + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 '@vitejs/plugin-react': specifier: ^4.3.0 version: 4.7.0(vite@5.4.21(@types/node@25.2.3)(lightningcss@1.30.2)) @@ -491,6 +509,9 @@ importers: jsdom: specifier: ^25.0.0 version: 25.0.1 + node-gyp: + specifier: ^12.2.0 + version: 12.2.0 playwright: specifier: ^1.49.0 version: 1.58.2 @@ -1289,6 +1310,10 @@ packages: resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} engines: {node: '>=18'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1415,6 +1440,14 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@npmcli/agent@4.0.0': + resolution: {integrity: sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/fs@5.0.0': + resolution: {integrity: sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==} + engines: {node: ^20.17.0 || >=22.9.0} + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -1720,6 +1753,9 @@ packages: '@types/wrap-ansi@3.0.0': resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@typescript-eslint/eslint-plugin@8.55.0': resolution: {integrity: sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1876,6 +1912,16 @@ packages: '@vitest/utils@4.0.18': resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@xterm/addon-fit@0.11.0': + resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==} + + '@xterm/addon-web-links@0.12.0': + resolution: {integrity: sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==} + + abbrev@4.0.0: + resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} + engines: {node: ^20.17.0 || >=22.9.0} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1988,6 +2034,10 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + cacache@20.0.3: + resolution: {integrity: sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==} + engines: {node: ^20.17.0 || >=22.9.0} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2025,6 +2075,10 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -2179,6 +2233,9 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + enhanced-resolve@5.19.0: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} @@ -2191,6 +2248,13 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -2302,6 +2366,9 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -2378,6 +2445,10 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} + fs-minipass@3.0.3: + resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2422,6 +2493,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@13.0.3: + resolution: {integrity: sha512-/g3B0mC+4x724v1TgtBlBtt2hPi/EWptsIAmXUx9Z2rvBYleQcsrmaOzd5LyL50jf/Soi83ZDJmw2+XqvH/EeA==} + engines: {node: 20 || >=22} + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -2453,6 +2528,9 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -2497,6 +2575,10 @@ packages: resolution: {integrity: sha512-tyao/4Vo36XnUItZ7DnUXX4f1jVao2mSrleV/5IPtW/XAEA26hRVsbc68nuTEKWcr5vMP/1mVoT2O7u8H4v1Vg==} engines: {node: '>=18'} + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -2552,6 +2634,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} + engines: {node: '>=20'} + jackspeak@4.2.3: resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} engines: {node: 20 || >=22} @@ -2732,6 +2818,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2742,6 +2832,10 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-fetch-happen@15.0.3: + resolution: {integrity: sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==} + engines: {node: ^20.17.0 || >=22.9.0} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2778,6 +2872,38 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minipass-collect@2.0.1: + resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass-fetch@5.0.1: + resolution: {integrity: sha512-yHK8pb0iCGat0lDrs/D6RZmCdaBT64tULXjdxjSMAqoDi18Q3qKEUTHypHQZQd9+FYpIS+lkvpq6C/R6SbUeRw==} + engines: {node: ^20.17.0 || >=22.9.0} + + minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@2.0.0: + resolution: {integrity: sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -2801,6 +2927,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + next@15.5.12: resolution: {integrity: sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -2822,9 +2952,25 @@ packages: sass: optional: true + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-gyp@12.2.0: + resolution: {integrity: sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + node-pty@1.1.0: + resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==} + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + nopt@9.0.0: + resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + nwsapi@2.2.23: resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} @@ -2894,6 +3040,10 @@ packages: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} + p-map@7.0.4: + resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} + engines: {node: '>=18'} + p-queue@6.6.2: resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} engines: {node: '>=8'} @@ -2940,6 +3090,10 @@ packages: resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} engines: {node: '>=0.10.0'} + path-scurry@2.0.1: + resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} + engines: {node: 20 || >=22} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -3005,6 +3159,14 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + proc-log@6.1.0: + resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -3064,6 +3226,10 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -3138,6 +3304,18 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3148,6 +3326,10 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + ssri@13.0.1: + resolution: {integrity: sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==} + engines: {node: ^20.17.0 || >=22.9.0} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -3216,6 +3398,10 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tar@7.5.7: + resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==} + engines: {node: '>=18'} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -3328,6 +3514,14 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unique-filename@5.0.0: + resolution: {integrity: sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==} + engines: {node: ^20.17.0 || >=22.9.0} + + unique-slug@6.0.0: + resolution: {integrity: sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==} + engines: {node: ^20.17.0 || >=22.9.0} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -3543,6 +3737,11 @@ packages: engines: {node: '>= 8'} hasBin: true + which@6.0.1: + resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -3579,6 +3778,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xterm@5.3.0: + resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==} + deprecated: This package is now deprecated. Move to @xterm/xterm instead. + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3586,6 +3789,13 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yaml@2.8.2: resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} @@ -4344,6 +4554,10 @@ snapshots: '@isaacs/cliui@9.0.0': {} + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4474,6 +4688,20 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@npmcli/agent@4.0.0': + dependencies: + agent-base: 7.1.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 11.2.6 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + '@npmcli/fs@5.0.0': + dependencies: + semver: 7.7.4 + '@opentelemetry/api@1.9.0': {} '@rolldown/pluginutils@1.0.0-beta.27': {} @@ -4720,6 +4948,10 @@ snapshots: '@types/wrap-ansi@3.0.0': {} + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.2.3 + '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -4946,6 +5178,12 @@ snapshots: '@vitest/pretty-format': 4.0.18 tinyrainbow: 3.0.3 + '@xterm/addon-fit@0.11.0': {} + + '@xterm/addon-web-links@0.12.0': {} + + abbrev@4.0.0: {} + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -5047,6 +5285,20 @@ snapshots: cac@6.7.14: {} + cacache@20.0.3: + dependencies: + '@npmcli/fs': 5.0.0 + fs-minipass: 3.0.3 + glob: 13.0.3 + lru-cache: 11.2.6 + minipass: 7.1.2 + minipass-collect: 2.0.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + p-map: 7.0.4 + ssri: 13.0.1 + unique-filename: 5.0.0 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -5079,6 +5331,8 @@ snapshots: check-error@2.1.3: {} + chownr@3.0.0: {} + ci-info@3.9.0: {} cli-cursor@5.0.0: @@ -5217,6 +5471,11 @@ snapshots: emoji-regex@8.0.0: {} + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 @@ -5229,6 +5488,10 @@ snapshots: entities@6.0.1: {} + env-paths@2.2.1: {} + + err-code@2.0.3: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -5391,6 +5654,8 @@ snapshots: expect-type@1.3.0: {} + exponential-backoff@3.1.3: {} + extendable-error@0.1.7: {} external-editor@3.1.0: @@ -5468,6 +5733,10 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 + fs-minipass@3.0.3: + dependencies: + minipass: 7.1.2 + fsevents@2.3.2: optional: true @@ -5512,6 +5781,12 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@13.0.3: + dependencies: + minimatch: 10.2.0 + minipass: 7.1.2 + path-scurry: 2.0.1 + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -5541,6 +5816,8 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + http-cache-semantics@4.2.0: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -5588,6 +5865,8 @@ snapshots: run-async: 3.0.0 rxjs: 7.8.2 + ip-address@10.1.0: {} + is-docker@2.2.1: {} is-extglob@2.1.1: {} @@ -5622,6 +5901,8 @@ snapshots: isexe@2.0.0: {} + isexe@4.0.0: {} + jackspeak@4.2.3: dependencies: '@isaacs/cliui': 9.0.0 @@ -5795,6 +6076,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.2.6: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -5805,6 +6088,22 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + make-fetch-happen@15.0.3: + dependencies: + '@npmcli/agent': 4.0.0 + cacache: 20.0.3 + http-cache-semantics: 4.2.0 + minipass: 7.1.2 + minipass-fetch: 5.0.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 1.0.0 + proc-log: 6.1.0 + promise-retry: 2.0.1 + ssri: 13.0.1 + transitivePeerDependencies: + - supports-color + math-intrinsics@1.1.0: {} merge2@1.4.1: {} @@ -5832,6 +6131,40 @@ snapshots: dependencies: brace-expansion: 2.0.2 + minipass-collect@2.0.1: + dependencies: + minipass: 7.1.2 + + minipass-fetch@5.0.1: + dependencies: + minipass: 7.1.2 + minipass-sized: 2.0.0 + minizlib: 3.1.0 + optionalDependencies: + encoding: 0.1.13 + + minipass-flush@1.0.5: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@2.0.0: + dependencies: + minipass: 7.1.2 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@7.1.2: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + mri@1.2.0: {} ms@2.1.3: {} @@ -5844,6 +6177,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.0.0: {} + next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 15.5.12 @@ -5868,8 +6203,33 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-addon-api@7.1.1: {} + + node-gyp@12.2.0: + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + graceful-fs: 4.2.11 + make-fetch-happen: 15.0.3 + nopt: 9.0.0 + proc-log: 6.1.0 + semver: 7.7.4 + tar: 7.5.7 + tinyglobby: 0.2.15 + which: 6.0.1 + transitivePeerDependencies: + - supports-color + + node-pty@1.1.0: + dependencies: + node-addon-api: 7.1.1 + node-releases@2.0.27: {} + nopt@9.0.0: + dependencies: + abbrev: 4.0.0 + nwsapi@2.2.23: {} obug@2.1.1: {} @@ -5938,6 +6298,8 @@ snapshots: p-map@2.1.0: {} + p-map@7.0.4: {} + p-queue@6.6.2: dependencies: eventemitter3: 4.0.7 @@ -5978,6 +6340,11 @@ snapshots: dependencies: path-root-regex: 0.1.2 + path-scurry@2.0.1: + dependencies: + lru-cache: 11.2.6 + minipass: 7.1.2 + path-type@4.0.0: {} pathe@1.1.2: {} @@ -6026,6 +6393,13 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + proc-log@6.1.0: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + proxy-from-env@1.1.0: {} punycode@2.3.1: {} @@ -6076,6 +6450,8 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry@0.12.0: {} + reusify@1.1.0: {} rollup@4.57.1: @@ -6183,6 +6559,21 @@ snapshots: slash@3.0.0: {} + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + source-map-js@1.2.1: {} spawndamnit@3.0.1: @@ -6192,6 +6583,10 @@ snapshots: sprintf-js@1.0.3: {} + ssri@13.0.1: + dependencies: + minipass: 7.1.2 + stackback@0.0.2: {} std-env@3.10.0: {} @@ -6249,6 +6644,14 @@ snapshots: tapable@2.3.0: {} + tar@7.5.7: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + term-size@2.2.1: {} tinybench@2.9.0: {} @@ -6336,6 +6739,14 @@ snapshots: undici-types@7.16.0: {} + unique-filename@5.0.0: + dependencies: + unique-slug: 6.0.0 + + unique-slug@6.0.0: + dependencies: + imurmurhash: 0.1.4 + universalify@0.1.2: {} update-browserslist-db@1.2.3(browserslist@4.28.1): @@ -6555,6 +6966,10 @@ snapshots: dependencies: isexe: 2.0.0 + which@6.0.1: + dependencies: + isexe: 4.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -6580,10 +6995,16 @@ snapshots: xmlchars@2.2.0: {} + xterm@5.3.0: {} + y18n@5.0.8: {} yallist@3.1.1: {} + yallist@4.0.0: {} + + yallist@5.0.0: {} + yaml@2.8.2: {} yargs-parser@21.1.1: {}