feat: implement DirectTerminal with XDA clipboard support (#55)
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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<string, ActivityState>
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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
This commit is contained in:
parent
2a3723be48
commit
bba1316527
|
|
@ -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",
|
||||
],
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, ActivityState>;
|
||||
|
||||
/** A running agent session */
|
||||
export interface Session {
|
||||
/** Unique session ID, e.g. "my-app-3" */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
build/
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<typeof loadConfig>;
|
||||
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<typeof loadConfig>;
|
||||
}
|
||||
|
||||
interface TerminalSession {
|
||||
sessionId: string;
|
||||
pty: IPty;
|
||||
ws: WebSocket;
|
||||
}
|
||||
|
||||
const activeSessions = new Map<string, TerminalSession>();
|
||||
|
||||
/**
|
||||
* 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"));
|
||||
|
|
@ -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");
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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<string[]>([]);
|
||||
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 (
|
||||
<div className="min-h-screen bg-[var(--color-bg-primary)] p-8">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="mb-2 text-3xl font-bold text-[var(--color-text-primary)]">
|
||||
Terminal Implementation Test & Documentation
|
||||
</h1>
|
||||
<p className="text-sm text-[var(--color-text-muted)]">
|
||||
Comparing sessions:
|
||||
<span className="ml-2 font-mono text-[var(--color-accent-red)]">OLD: {oldSessionId}</span>
|
||||
<span className="ml-2 font-mono text-[var(--color-accent-green)]">NEW: {newSessionId}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* The Problem */}
|
||||
<section className="mb-8 rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-6">
|
||||
<h2 className="mb-4 text-xl font-bold text-[var(--color-text-primary)]">🐛 The Problem</h2>
|
||||
<div className="space-y-3 text-sm text-[var(--color-text-secondary)]">
|
||||
<p>
|
||||
<strong className="text-[var(--color-text-primary)]">Issue:</strong> Browser clipboard (Cmd+C/Ctrl+C)
|
||||
only worked when an iTerm2 client was attached to the tmux session.
|
||||
</p>
|
||||
<p>
|
||||
<strong className="text-[var(--color-text-primary)]">Impact:</strong> Users had to keep iTerm2 tabs open
|
||||
in the background for clipboard to work in the web dashboard.
|
||||
</p>
|
||||
<p>
|
||||
<strong className="text-[var(--color-text-primary)]">Investigation time:</strong> 12+ hours of debugging
|
||||
across tmux, ttyd, xterm.js, and macOS clipboard systems.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Root Cause */}
|
||||
<section className="mb-8 rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-6">
|
||||
<h2 className="mb-4 text-xl font-bold text-[var(--color-text-primary)]">🔍 Root Cause Analysis</h2>
|
||||
<div className="space-y-4 text-sm">
|
||||
<div>
|
||||
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">
|
||||
1. How tmux Clipboard Works (OSC 52)
|
||||
</h3>
|
||||
<ul className="ml-6 list-disc space-y-1 text-[var(--color-text-secondary)]">
|
||||
<li>tmux uses OSC 52 escape sequences to synchronize clipboard with terminals</li>
|
||||
<li>Format: <code className="rounded bg-black px-1 py-0.5">\x1b]52;c;<base64>\x07</code></li>
|
||||
<li>Terminal must support OSC 52 and have proper capabilities declared</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">
|
||||
2. tmux Capability Detection (XDA)
|
||||
</h3>
|
||||
<ul className="ml-6 list-disc space-y-1 text-[var(--color-text-secondary)]">
|
||||
<li>tmux queries terminal capabilities using Device Attributes (DA/XDA)</li>
|
||||
<li>
|
||||
XDA query: <code className="rounded bg-black px-1 py-0.5">CSI > q</code> (also called XTVERSION)
|
||||
</li>
|
||||
<li>
|
||||
Terminal responds with identification string containing terminal type (e.g., "XTerm(370)", "iTerm2 ")
|
||||
</li>
|
||||
<li>Based on response, tmux enables features like TTYC_MS (clipboard support)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">3. The Missing Piece</h3>
|
||||
<div className="rounded-lg border border-[var(--color-accent-red)] bg-[var(--color-bg-tertiary)] p-4">
|
||||
<p className="font-semibold text-[var(--color-accent-red)]">
|
||||
xterm.js does NOT implement XDA (Extended Device Attributes)
|
||||
</p>
|
||||
<ul className="ml-6 mt-2 list-disc space-y-1 text-[var(--color-text-secondary)]">
|
||||
<li>
|
||||
XDA is marked as TODO in xterm.js codebase:{" "}
|
||||
<code className="rounded bg-black px-1 py-0.5">
|
||||
test.skip('CSI > Ps q - Report xterm name and version (XTVERSION)')
|
||||
</code>
|
||||
</li>
|
||||
<li>Without XDA response, tmux doesn't recognize the terminal as clipboard-capable</li>
|
||||
<li>tmux never emits OSC 52 sequences → clipboard doesn't work</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">
|
||||
4. Why iTerm2 "Fixed" It
|
||||
</h3>
|
||||
<ul className="ml-6 list-disc space-y-1 text-[var(--color-text-secondary)]">
|
||||
<li>
|
||||
iTerm2 sends proper XDA response identifying itself as{" "}
|
||||
<code className="rounded bg-black px-1 py-0.5">"iTerm2 "</code>
|
||||
</li>
|
||||
<li>tmux detects this and enables clipboard for the entire session</li>
|
||||
<li>OSC 52 sequences are then sent to ALL clients, including browser (ttyd/xterm.js)</li>
|
||||
<li>This is why clipboard "magically worked" when iTerm2 was attached</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* The Solution */}
|
||||
<section className="mb-8 rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-6">
|
||||
<h2 className="mb-4 text-xl font-bold text-[var(--color-text-primary)]">✅ The Solution</h2>
|
||||
<div className="space-y-4 text-sm">
|
||||
<div>
|
||||
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">DirectTerminal Implementation</h3>
|
||||
<p className="mb-2 text-[var(--color-text-secondary)]">
|
||||
Created custom terminal component that registers an XDA handler using xterm.js parser API:
|
||||
</p>
|
||||
<pre className="overflow-x-auto rounded-lg bg-black p-4 text-xs">
|
||||
<code className="text-[var(--color-accent-green)]">
|
||||
{`terminal.parser.registerCsiHandler(
|
||||
{ prefix: ">", final: "q" }, // CSI > q is XDA query
|
||||
() => {
|
||||
// Respond with XTerm identification
|
||||
terminal.write("\\x1bP>|XTerm(370)\\x1b\\\\");
|
||||
return true;
|
||||
}
|
||||
);`}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">What This Does</h3>
|
||||
<ol className="ml-6 list-decimal space-y-1 text-[var(--color-text-secondary)]">
|
||||
<li>Intercepts XDA queries from tmux</li>
|
||||
<li>
|
||||
Responds with <code className="rounded bg-black px-1 py-0.5">XTerm(370)</code> identification
|
||||
</li>
|
||||
<li>tmux detects "XTerm(" in response and enables TTYC_MS capability</li>
|
||||
<li>OSC 52 sequences now flow: tmux → WebSocket → xterm.js → navigator.clipboard</li>
|
||||
<li>
|
||||
<strong className="text-[var(--color-accent-green)]">Clipboard works without iTerm2!</strong>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">Architecture Changes</h3>
|
||||
<ul className="ml-6 list-disc space-y-1 text-[var(--color-text-secondary)]">
|
||||
<li>
|
||||
<strong>Old:</strong> Browser → ttyd (iframe) → tmux
|
||||
</li>
|
||||
<li>
|
||||
<strong>New:</strong> Browser → Custom WebSocket (node-pty) → tmux
|
||||
</li>
|
||||
<li>Bypasses ttyd for direct control over terminal initialization</li>
|
||||
<li>Full control over escape sequence handling and capabilities</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Node Version Requirement */}
|
||||
<section className="mb-8 rounded-lg border border-[var(--color-accent-orange)] bg-[var(--color-bg-secondary)] p-6">
|
||||
<h2 className="mb-4 text-xl font-bold text-[var(--color-text-primary)]">⚠️ Node Version Requirement</h2>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="rounded-lg border border-[var(--color-accent-orange)] bg-[var(--color-bg-tertiary)] p-4">
|
||||
<p className="font-semibold text-[var(--color-accent-orange)]">
|
||||
CRITICAL: This implementation requires Node 20.x (currently 20.20.0)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">Why Node 20?</h3>
|
||||
<ul className="ml-6 list-disc space-y-1 text-[var(--color-text-secondary)]">
|
||||
<li>
|
||||
<strong>node-pty 1.1.0 is incompatible with Node 25+</strong>
|
||||
</li>
|
||||
<li>
|
||||
Error on Node 25.6.1:{" "}
|
||||
<code className="rounded bg-black px-1 py-0.5">posix_spawnp failed</code>
|
||||
</li>
|
||||
<li>
|
||||
Root cause: node-pty's native module (darwin-arm64 prebuild) fails to spawn processes on Node 25
|
||||
</li>
|
||||
<li>No darwin-arm64 prebuilds available that work with Node 25</li>
|
||||
<li>Building from source also fails with the same error</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">When Can We Upgrade?</h3>
|
||||
<ul className="ml-6 list-disc space-y-1 text-[var(--color-text-secondary)]">
|
||||
<li>
|
||||
<strong>Option 1:</strong> Wait for node-pty 1.2.0 stable release with Node 25+ support
|
||||
</li>
|
||||
<li>
|
||||
<strong>Option 2:</strong> Test node-pty beta versions (currently 1.2.0-beta.11)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Option 3:</strong> Switch to alternative PTY library (e.g., xterm-pty, node-child-pty)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-[var(--color-bg-tertiary)] p-4">
|
||||
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">
|
||||
⚡ Testing Instructions for Upgrades
|
||||
</h3>
|
||||
<p className="mb-2 text-[var(--color-text-secondary)]">Before upgrading Node or node-pty:</p>
|
||||
<ol className="ml-6 list-decimal space-y-1 text-xs text-[var(--color-text-secondary)]">
|
||||
<li>
|
||||
Test node-pty directly:{" "}
|
||||
<code className="rounded bg-black px-1 py-0.5">
|
||||
node -e "const pty = require('node-pty'); pty.spawn('/bin/bash', [], {})"
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
If no <code className="rounded bg-black px-1 py-0.5">posix_spawnp failed</code> error, proceed
|
||||
</li>
|
||||
<li>Start dev servers and open this page</li>
|
||||
<li>Test DirectTerminal: verify connection, clipboard, resize</li>
|
||||
<li>Test multiple sessions to ensure stability</li>
|
||||
<li>If all tests pass, upgrade is safe</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Comparison Toggle */}
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setShowComparison(!showComparison)}
|
||||
className="rounded-lg bg-[var(--color-accent-blue)] px-4 py-2 text-sm font-semibold text-white transition-opacity hover:opacity-80"
|
||||
>
|
||||
{showComparison ? "Hide" : "Show"} Side-by-Side Comparison
|
||||
</button>
|
||||
<span className="text-sm text-[var(--color-text-secondary)]">
|
||||
Compare old (ttyd iframe) vs new (DirectTerminal with XDA)
|
||||
</span>
|
||||
</div>
|
||||
{oldSessionId === newSessionId && (
|
||||
<div className="mt-2 rounded border border-[var(--color-accent-orange)] bg-[var(--color-bg-tertiary)] p-2 text-xs text-[var(--color-text-secondary)]">
|
||||
⚠️ Using same session for both terminals. To avoid port conflicts, use different sessions:
|
||||
<code className="ml-1 rounded bg-black px-1">?old_session=ao-orchestrator&new_session=ao-20</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Side-by-Side Comparison */}
|
||||
{showComparison && (
|
||||
<section className="mb-8">
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{/* Old Implementation */}
|
||||
<div className="rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-4">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<span className="rounded bg-[var(--color-accent-red)] px-2 py-1 text-xs font-semibold text-white">
|
||||
OLD
|
||||
</span>
|
||||
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
ttyd iframe (no XDA)
|
||||
</h3>
|
||||
</div>
|
||||
<div className="mb-2 rounded bg-[var(--color-bg-tertiary)] p-2 text-xs text-[var(--color-text-secondary)]">
|
||||
❌ Clipboard requires iTerm2 attached
|
||||
<br />
|
||||
✅ Battle-tested (ttyd)
|
||||
<br />
|
||||
❌ No control over capabilities
|
||||
</div>
|
||||
<Terminal sessionId={oldSessionId} />
|
||||
</div>
|
||||
|
||||
{/* New Implementation */}
|
||||
<div className="rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-4">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<span className="rounded bg-[var(--color-accent-green)] px-2 py-1 text-xs font-semibold text-white">
|
||||
NEW
|
||||
</span>
|
||||
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
DirectTerminal (with XDA)
|
||||
</h3>
|
||||
</div>
|
||||
<div className="mb-2 rounded bg-[var(--color-bg-tertiary)] p-2 text-xs text-[var(--color-text-secondary)]">
|
||||
✅ Clipboard works standalone
|
||||
<br />
|
||||
✅ Full control over terminal
|
||||
<br />
|
||||
⚠️ Requires Node 20.x (node-pty)
|
||||
</div>
|
||||
<DirectTerminal sessionId={newSessionId} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 rounded-lg border border-[var(--color-accent-blue)] bg-[var(--color-bg-secondary)] p-4">
|
||||
<h3 className="mb-2 text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
🧪 How to Test Clipboard
|
||||
</h3>
|
||||
<ol className="ml-6 list-decimal space-y-1 text-sm text-[var(--color-text-secondary)]">
|
||||
<li>Drag-select text in BOTH terminals above</li>
|
||||
<li>Press Cmd+C (macOS) or Ctrl+Shift+C (Linux/Windows)</li>
|
||||
<li>Paste in another application (Notes, VS Code, etc.)</li>
|
||||
<li>
|
||||
<strong className="text-[var(--color-text-primary)]">
|
||||
Expected: RIGHT terminal works, LEFT may not (unless iTerm2 is attached)
|
||||
</strong>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Debugging Journey */}
|
||||
<section className="rounded-lg border border-[var(--color-accent-purple)] bg-[var(--color-bg-secondary)] p-6">
|
||||
<h2 className="mb-4 text-xl font-bold text-[var(--color-text-primary)]">🔬 The Debugging Journey</h2>
|
||||
|
||||
<div className="space-y-4 text-sm text-[var(--color-text-secondary)]">
|
||||
<div className="rounded-lg bg-[var(--color-bg-tertiary)] p-4">
|
||||
<p className="font-semibold text-[var(--color-accent-red)]">
|
||||
Total Time Wasted: 12+ hours across Feb 15-16, 2026
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">❌ What We Tried (That Didn't Work)</h3>
|
||||
<ol className="ml-6 list-decimal space-y-2">
|
||||
<li>
|
||||
<strong>Suspected ttyd clipboard handling</strong> - Spent hours analyzing ttyd source code, checking OSC 52
|
||||
passthrough. ttyd was innocent - it passes escape sequences correctly.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Blamed xterm.js configuration</strong> - Tried every possible xterm.js option, clipboard addon
|
||||
configurations, terminal type settings. None made a difference.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Investigated macOS clipboard permissions</strong> - Checked browser permissions, sandbox attributes,
|
||||
navigator.clipboard API. All were correct.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Suspected WebSocket encoding issues</strong> - Checked binary vs text mode, UTF-8 encoding,
|
||||
base64 handling. All correct.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Tried force-enabling tmux clipboard</strong> - Used <code className="rounded bg-black px-1 py-0.5">
|
||||
set-option -s set-clipboard on</code> in tmux.conf. Didn't help - tmux needs the terminal to declare support.
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">💡 The Breakthrough</h3>
|
||||
<div className="space-y-2">
|
||||
<p>
|
||||
<strong>What finally worked:</strong> Registering an XDA (Extended Device Attributes) handler in xterm.js
|
||||
using <code className="rounded bg-black px-1 py-0.5">terminal.parser.registerCsiHandler()</code>
|
||||
</p>
|
||||
<pre className="mt-2 overflow-x-auto rounded-lg bg-black p-3 text-xs">
|
||||
<code className="text-[var(--color-accent-green)]">
|
||||
{`terminal.parser.registerCsiHandler(
|
||||
{ prefix: ">", final: "q" },
|
||||
() => {
|
||||
terminal.write("\\x1bP>|XTerm(370)\\x1b\\\\");
|
||||
return true;
|
||||
}
|
||||
);`}
|
||||
</code>
|
||||
</pre>
|
||||
<p className="mt-2">
|
||||
This single handler made tmux recognize our terminal as clipboard-capable. Clipboard immediately started working.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">🎯 How We Finally Figured It Out</h3>
|
||||
<ol className="ml-6 list-decimal space-y-2">
|
||||
<li>
|
||||
<strong>Deep-dive into tmux source code</strong> - Used DeepWiki.com to analyze tmux's terminal capability
|
||||
detection logic in <code className="rounded bg-black px-1 py-0.5">tty-keys.c</code> and{" "}
|
||||
<code className="rounded bg-black px-1 py-0.5">tty.c</code>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Discovered XDA queries</strong> - Found that tmux sends{" "}
|
||||
<code className="rounded bg-black px-1 py-0.5">CSI > q</code> (XTVERSION) to detect terminal type
|
||||
</li>
|
||||
<li>
|
||||
<strong>Traced the "iTerm2 magic"</strong> - Realized why clipboard worked when iTerm2 was attached:
|
||||
iTerm2 responds to XDA queries, enabling clipboard for the entire tmux session
|
||||
</li>
|
||||
<li>
|
||||
<strong>Checked xterm.js implementation</strong> - Found that XDA is marked as TODO in xterm.js tests:{" "}
|
||||
<code className="rounded bg-black px-1 py-0.5">
|
||||
test.skip('CSI > Ps q - Report xterm name and version (XTVERSION)')
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Implemented custom handler</strong> - Used xterm.js parser API to register our own XDA handler
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-[var(--color-accent-orange)] bg-[var(--color-bg-tertiary)] p-4">
|
||||
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">
|
||||
⚡ How We Could Have Figured It Out Faster
|
||||
</h3>
|
||||
<ol className="ml-6 list-decimal space-y-2 text-[var(--color-text-secondary)]">
|
||||
<li>
|
||||
<strong>Start with tmux source code first</strong> - Instead of debugging xterm.js and ttyd, we should have
|
||||
immediately checked how tmux detects terminal capabilities
|
||||
</li>
|
||||
<li>
|
||||
<strong>Monitor escape sequences</strong> - Running{" "}
|
||||
<code className="rounded bg-black px-1 py-0.5">tmux -vvv</code> or using a terminal protocol analyzer
|
||||
would have revealed the XDA queries being sent
|
||||
</li>
|
||||
<li>
|
||||
<strong>Compare working vs broken scenarios</strong> - Should have captured and diffed the escape sequences
|
||||
when iTerm2 was attached vs not attached earlier
|
||||
</li>
|
||||
<li>
|
||||
<strong>Check xterm.js issues/limitations first</strong> - A GitHub search for "xterm.js XDA" or
|
||||
"xterm.js device attributes" would have revealed it's unimplemented
|
||||
</li>
|
||||
<li>
|
||||
<strong>Read tmux documentation on terminal types</strong> - tmux man page mentions terminal capability
|
||||
detection, but we focused on clipboard settings instead
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-2 font-semibold text-[var(--color-text-primary)]">📚 Key Resources</h3>
|
||||
<ul className="ml-6 list-disc space-y-1">
|
||||
<li>tmux source analysis: DeepWiki.com (Feb 15, 2026)</li>
|
||||
<li>xterm.js parser API documentation</li>
|
||||
<li>XTerm Control Sequences: XTVERSION / Device Attributes</li>
|
||||
<li>tmux <code className="rounded bg-black px-1 py-0.5">tty-keys.c</code>: Terminal type detection logic</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Implementation Files */}
|
||||
<section className="mt-6 rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-6">
|
||||
<h2 className="mb-4 text-xl font-bold text-[var(--color-text-primary)]">📁 Implementation Files</h2>
|
||||
<ul className="ml-6 list-disc space-y-1 text-sm text-[var(--color-text-secondary)]">
|
||||
<li>
|
||||
<code className="rounded bg-black px-1 py-0.5">
|
||||
packages/web/src/components/DirectTerminal.tsx
|
||||
</code> - Main component with XDA handler
|
||||
</li>
|
||||
<li>
|
||||
<code className="rounded bg-black px-1 py-0.5">
|
||||
packages/web/server/direct-terminal-ws.ts
|
||||
</code> - WebSocket server using node-pty
|
||||
</li>
|
||||
<li>
|
||||
<code className="rounded bg-black px-1 py-0.5">
|
||||
packages/web/src/app/dev/terminal-test/page.tsx
|
||||
</code> - This test page
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-8 border-t border-[var(--color-border-default)] pt-4 text-center text-xs text-[var(--color-text-secondary)]">
|
||||
<p>Investigation: Feb 15-16, 2026 • Duration: 12+ hours • Status: ✅ Resolved</p>
|
||||
<p className="mt-1">Node 20.20.0 • node-pty 1.1.0 • xterm.js 5.3.0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TerminalTestPage() {
|
||||
return (
|
||||
<Suspense fallback={<div className="flex min-h-screen items-center justify-center">Loading...</div>}>
|
||||
<TerminalTestPageContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className="min-h-screen bg-[var(--color-bg-primary)] p-8">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-[var(--color-text-primary)]">
|
||||
DirectTerminal Test - XDA Clipboard Support
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-[var(--color-text-muted)]">
|
||||
This terminal has XDA (Extended Device Attributes) handler registered.
|
||||
<br />
|
||||
tmux should recognize it as XTerm and enable clipboard support (OSC 52).
|
||||
</p>
|
||||
<div className="mt-4 rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-4">
|
||||
<h2 className="mb-2 text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
Testing: <span className="text-[var(--color-accent-green)]">{sessionId}</span>
|
||||
</h2>
|
||||
<h2 className="mb-2 text-sm font-semibold text-[var(--color-text-primary)]">Test Steps:</h2>
|
||||
<ol className="list-inside list-decimal space-y-1 text-sm text-[var(--color-text-muted)]">
|
||||
<li>Connected to tmux session: {sessionId}</li>
|
||||
<li>
|
||||
Verify XDA badge shows <span className="text-[var(--color-accent-green)]">✓ XDA</span>
|
||||
</li>
|
||||
<li>Drag-select text in the terminal</li>
|
||||
<li>Press Cmd+C (macOS) or Ctrl+C (Linux/Windows)</li>
|
||||
<li>Paste elsewhere to verify clipboard works</li>
|
||||
<li>
|
||||
<strong>Expected:</strong> Clipboard works without iTerm2 attachment!
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="mt-4 rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-secondary)] p-4">
|
||||
<h2 className="mb-2 text-sm font-semibold text-[var(--color-text-primary)]">Technical Details:</h2>
|
||||
<ul className="list-inside list-disc space-y-1 text-sm text-[var(--color-text-muted)]">
|
||||
<li>Registers CSI > q (XDA) handler in xterm.js parser</li>
|
||||
<li>Responds with DCS > | XTerm(370) ST sequence</li>
|
||||
<li>tmux detects "XTerm(" and enables TTYC_MS (clipboard capability)</li>
|
||||
<li>OSC 52 sequences flow: tmux → WebSocket → xterm.js → navigator.clipboard</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Test with specified session - key forces remount on fullscreen change */}
|
||||
<DirectTerminal
|
||||
key={`${sessionId}-${startFullscreen ? "fullscreen" : "normal"}`}
|
||||
sessionId={sessionId}
|
||||
startFullscreen={startFullscreen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TestDirectPage() {
|
||||
return (
|
||||
<Suspense fallback={<div className="flex min-h-screen items-center justify-center">Loading...</div>}>
|
||||
<TestDirectPageContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<HTMLDivElement>(null);
|
||||
const terminalInstance = useRef<TerminalType | null>(null);
|
||||
const fitAddon = useRef<FitAddonType | null>(null);
|
||||
const ws = useRef<WebSocket | null>(null);
|
||||
const [fullscreen, setFullscreen] = useState(startFullscreen);
|
||||
const [status, setStatus] = useState<"connecting" | "connected" | "error">("connecting");
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden rounded-lg border border-[var(--color-border-default)] bg-black",
|
||||
fullscreen && "fixed inset-0 z-50 rounded-none border-0",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 border-b border-[var(--color-border-default)] bg-[var(--color-bg-tertiary)] px-3 py-2">
|
||||
<div className={cn("h-2 w-2 rounded-full", statusColor)} />
|
||||
<span className="font-[var(--font-mono)] text-xs text-[var(--color-text-muted)]">{sessionId}</span>
|
||||
<span className={cn("text-[10px] font-medium uppercase tracking-wide", statusTextColor)}>{statusText}</span>
|
||||
<span className="ml-2 rounded bg-[var(--color-bg-secondary)] px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-wide text-[var(--color-accent-green)]">
|
||||
XDA
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setFullscreen(!fullscreen)}
|
||||
className="ml-auto rounded px-2 py-0.5 text-[11px] text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-bg-secondary)] hover:text-[var(--color-text-primary)]"
|
||||
>
|
||||
{fullscreen ? "exit fullscreen" : "fullscreen"}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
ref={terminalRef}
|
||||
className={cn("w-full p-2", fullscreen ? "h-[calc(100vh-40px)]" : "h-[600px]")}
|
||||
style={{ overflow: "hidden", display: "flex", flexDirection: "column" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
</h3>
|
||||
|
||||
<Terminal sessionId={session.id} />
|
||||
<DirectTerminal sessionId={session.id} startFullscreen={startFullscreen} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
|
|||
421
pnpm-lock.yaml
421
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: {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue