Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
159 commits
Select commit Hold shift + click to select a range
77c5d7c
🤖 fix: downgrade Electron 38→31 for node-pty C++17 compatibility
sreya Nov 11, 2025
5ce9bd5
feat: add backend PTY service and terminal server
sreya Nov 11, 2025
666cc8d
feat: add IPC types for terminal operations
sreya Nov 11, 2025
b9746fe
feat: add frontend terminal component
sreya Nov 11, 2025
13b4e62
fix: SSH terminal timeout and add multi-instance support
sreya Nov 11, 2025
1205cfc
🤖 fix: prevent terminal reconnection loop and add debug logging
sreya Nov 11, 2025
c54cd45
🤖 fix: make node-pty import lazy to prevent startup crash
sreya Nov 11, 2025
b013189
🤖 fix: use console instead of log service in frontend hook
sreya Nov 11, 2025
90061e0
🤖 debug: add console logging to TerminalView component
sreya Nov 11, 2025
1d8095d
🤖 debug: add WebSocket message flow logging
sreya Nov 11, 2025
4dc33ea
🤖 fix: WebSocket message handler wasn't being set up
sreya Nov 11, 2025
a282c2f
🤖 fix: ensure terminal is ready before setting up WebSocket handler
sreya Nov 11, 2025
651cc1a
🤖 debug: send newline to trigger shell prompt
sreya Nov 11, 2025
8c0cb8d
🤖 debug: change PTY logs from debug to info level
sreya Nov 11, 2025
dd8cc20
🤖 debug: add try-catch around SSH exec to catch errors
sreya Nov 11, 2025
994a967
🤖 fix: send attach message to register WebSocket with session
sreya Nov 11, 2025
67865e4
🤖 fix: use 'bash -l' instead of 'exec $SHELL' for SSH terminals
sreya Nov 11, 2025
79c6e0e
🤖 feat: add embedded terminal for SSH workspaces
sreya Nov 11, 2025
86e043f
🤖 fix: remove vite-plugin-top-level-await for Bun compatibility
sreya Nov 11, 2025
e5c9b72
Revert "🤖 fix: remove vite-plugin-top-level-await for Bun compatibility"
sreya Nov 11, 2025
26214ef
🤖 fix: add postinstall patch for vite-plugin-top-level-await
sreya Nov 11, 2025
3eb30c3
🤖 fix: downgrade chalk 5→4 for CommonJS compatibility
sreya Nov 11, 2025
dc20c07
🤖 fix: use login shell for local PTY sessions
sreya Nov 11, 2025
1d75904
🤖 fix: set TERM_PROGRAM and disable problematic zsh features
sreya Nov 11, 2025
6fee74c
Revert "🤖 fix: set TERM_PROGRAM and disable problematic zsh features"
sreya Nov 11, 2025
c982c7b
fix: synchronize terminal size between PTY and display
sreya Nov 12, 2025
1c3fe26
no patching
sreya Nov 12, 2025
dca8263
test: upgrade to Electron 38 + node-pty beta39
sreya Nov 13, 2025
6f6e602
🤖 fix: update ghostty-wasm to ghostty-web and improve PTY error handling
sreya Nov 13, 2025
05f052c
fix: remove duplicate ghostty-web dependency and fix async calls
sreya Nov 13, 2025
074c587
🤖 fix: add ExtensionMetadataService and fix async/type errors after r…
sreya Nov 13, 2025
c20222a
🤖 test: add unit tests for terminal/PTY functionality
sreya Nov 13, 2025
a3c08b1
revert: restore chalk to v5 to match main branch
sreya Nov 13, 2025
a7ce049
refactor: remove debug console.log statements from terminal components
sreya Nov 13, 2025
d5faac7
fix: restore --add-project CLI option and project initialization
sreya Nov 13, 2025
ec31e28
🤖 refactor: import ghostty-vt.wasm directly from package
sreya Nov 13, 2025
0efd49e
🤖 refactor: enable and simplify terminal service unit tests
sreya Nov 13, 2025
3479b2e
🤖 refactor: remove trivial terminal service unit tests
sreya Nov 13, 2025
0db0ba7
🤖 refactor: remove wasmPath now that ghostty-web handles it
sreya Nov 13, 2025
3bc778c
🤖 fix: use HTTPS instead of SSH for ghostty-web dependency
sreya Nov 13, 2025
b5e69a3
bun
sreya Nov 13, 2025
aac958c
🤖 chore: explicitly use HEAD for ghostty-web git dependency
sreya Nov 13, 2025
3594132
🤖 chore: update lockfile for latest ghostty-web commit
sreya Nov 13, 2025
eb6c123
🤖 feat: refactor terminals to pop-out windows with multiple windows p…
sreya Nov 13, 2025
63e4ddc
🤖 fix: resolve build issues with terminal window
sreya Nov 13, 2025
05f7369
🤖 fix: use static import for TerminalView in dev mode
sreya Nov 13, 2025
e99cb95
🤖 feat: publish ghostty-web to GitHub Packages for CI/CD
sreya Nov 13, 2025
704fca6
🤖 feat: update to ghostty-web 0.1.1 from npm
sreya Nov 13, 2025
b3e48b3
🤖 debug: add logging to diagnose window.api issue in terminal window
sreya Nov 13, 2025
a791e7a
🤖 fix: ensure preload script is built before starting dev server
sreya Nov 13, 2025
1818bad
🤖 fix: prevent browser API shim from overriding Electron preload
sreya Nov 13, 2025
eb81054
🤖 debug: add logging to preload and browser API to diagnose loading i…
sreya Nov 13, 2025
c0ef9e2
🤖 debug: add logging to diagnose window.api loading issue
sreya Nov 13, 2025
4c6cce1
🤖 fix: correct preload script path in terminal window manager
sreya Nov 13, 2025
bb7b76c
🤖 fix: send clear screen on terminal connection to fix prompt position
sreya Nov 13, 2025
a031841
🤖 debug: add logging to trace double input issue
sreya Nov 13, 2025
8078806
🤖 fix: remove StrictMode from terminal window to prevent double-mounting
sreya Nov 13, 2025
82f2ae6
🤖 debug: add logging to trace window resize handling
sreya Nov 13, 2025
bcfd055
🤖 fix: add terminalReady to ResizeObserver dependencies
sreya Nov 13, 2025
131c56e
🤖 debug: add more logging to diagnose resize issues
sreya Nov 13, 2025
8f6e343
🤖 fix: add window resize listener as backup for terminal resizing
sreya Nov 13, 2025
5526594
🤖 fix: prevent terminal session from reconnecting on every resize
sreya Nov 13, 2025
c64a4fd
🤖 fix: terminal not spawning after removing terminalSize from deps
sreya Nov 13, 2025
ba5dd32
fix: prevent terminal session recreation on resize
sreya Nov 13, 2025
1e80bdb
🤖 fix: debounce PTY resize to prevent vim cursor position issues
sreya Nov 13, 2025
2cd3cea
🤖 fix: send resize to PTY immediately instead of debouncing
sreya Nov 13, 2025
a5a8dcf
🤖 fix: properly debounce PTY resize to prevent vim display corruption
sreya Nov 13, 2025
a0a3fa2
🤖 fix: detect maximize/minimize and send immediate resize
sreya Nov 13, 2025
8678040
🤖 fix: improve maximize/minimize detection with size delta
sreya Nov 13, 2025
28ccc0b
🤖 fix: simplify to always-debounce strategy for vim stability
sreya Nov 13, 2025
a74e17e
🤖 debug: add logging to detect color code corruption source
sreya Nov 13, 2025
23bb489
🤖 debug: add hex dump for malformed escape sequences
sreya Nov 13, 2025
4812eb4
🤖 debug: add backend logging to detect PTY data corruption
sreya Nov 13, 2025
5796edb
🤖 debug: log every PTY chunk with hex dump
sreya Nov 13, 2025
36eab74
🤖 fix: buffer PTY data to prevent escape sequences from splitting
sreya Nov 13, 2025
8766cf9
🤖 chore: remove debug logging after fixing escape sequence splitting
sreya Nov 13, 2025
209e7ca
🤖 feat: improve terminal window titles to show project and branch
sreya Nov 13, 2025
8dc77e0
🤖 fix: remove hardcoded Terminal title from HTML
sreya Nov 13, 2025
e8d182f
🤖 fix: look up workspace metadata for terminal window titles
sreya Nov 13, 2025
18b8631
🤖 fix: correct Config import path and type annotations
sreya Nov 13, 2025
97a30fc
Merge branch 'main' into local-pty
sreya Nov 13, 2025
ee5bfd4
🤖 refactor: simplify browser API shim initialization
sreya Nov 13, 2025
7f2ad26
🤖 chore: remove vim swap file
sreya Nov 13, 2025
efa06e8
🤖 chore: cleanup debug logs and unnecessary files
sreya Nov 13, 2025
147d808
🤖 chore: remove remaining console.log from preload.ts
sreya Nov 13, 2025
443fff6
🤖 fix: disable electron-builder native rebuild and update terminal test
sreya Nov 13, 2025
17a9a66
🤖 fix: add eslint-disable for terminal service files
sreya Nov 13, 2025
ab20774
🤖 chore: remove accidental .bak files
sreya Nov 13, 2025
f2769eb
🤖 fix: remaining lint errors for terminal feature
sreya Nov 13, 2025
7c44343
🤖 fix: resolve final lint errors
sreya Nov 13, 2025
c9c3581
🤖 fix: resolve final 6 lint errors
sreya Nov 13, 2025
181ebfe
🤖 fix: remove async from remaining terminal IPC handlers
sreya Nov 13, 2025
a6ecb55
🤖 fix: complete async removal and IIFE closure
sreya Nov 13, 2025
091bd06
Revert unnecessary ipcMain.ts changes
sreya Nov 13, 2025
2024d14
Make terminal server initialization lazy
sreya Nov 13, 2025
34a99f4
Restore updateRecency call in sendMessage handler
sreya Nov 13, 2025
724938a
Fix all async/await issues in IPC handlers
sreya Nov 13, 2025
10518fa
Restore WORKSPACE_OPEN_TERMINAL handler and openTerminal method
sreya Nov 13, 2025
635f813
Restore deleteWorkspace call to extensionMetadata
sreya Nov 13, 2025
fd73b88
Fix formatting: add blank line before isCommandAvailable
sreya Nov 13, 2025
b5d0a7b
Move helper methods before openTerminal and fix escaping
sreya Nov 13, 2025
5c2e7ba
revert more changes
sreya Nov 13, 2025
98bebc0
Revert main-server.ts changes to match original structure
sreya Nov 13, 2025
a044df0
fmt
sreya Nov 13, 2025
325d5f5
Revert createWindow to synchronous
sreya Nov 13, 2025
e5ef5c5
Fix terminal window opening - await async openTerminalWindow call
sreya Nov 13, 2025
24270a0
Add logging to terminal window open handler for debugging
sreya Nov 13, 2025
296eb5d
Add frontend logging for terminal open debugging
sreya Nov 13, 2025
eec8003
Fix terminalServer.start() to return the promise
sreya Nov 13, 2025
247ec96
Add preload logging to trace terminal window API calls
sreya Nov 13, 2025
4263554
Add log to WORKSPACE_OPEN_TERMINAL to trace which handler is called
sreya Nov 13, 2025
4b6e414
Add console.log to both terminal handlers for debugging
sreya Nov 13, 2025
cfcaafd
Add logging and stack trace to workspace.openTerminal (old API)
sreya Nov 13, 2025
c863b8f
Use console.error for workspace.openTerminal to make it more visible
sreya Nov 13, 2025
9247988
Fix useCreationWorkspace to skip branch loading when projectPath is e…
sreya Nov 13, 2025
60a6d47
Revert unnecessary vite.config.ts changes
sreya Nov 13, 2025
e59cc63
fmt
sreya Nov 13, 2025
679d933
revert accidental diff
sreya Nov 13, 2025
c74940b
🤖 refactor: replace TerminalServer WebSocket with IPC for desktop + s…
sreya Nov 14, 2025
fe65373
🤖 fix: resolve linting errors from terminal refactor
sreya Nov 14, 2025
7124dcb
🤖 fix: resolve import and dynamic import lint errors
sreya Nov 14, 2025
b82c178
🤖 fix: implement terminal support in browser/server mode
sreya Nov 14, 2025
9cb55b3
🤖 fix: correct dev-server command to use main-server.js directly
sreya Nov 14, 2025
83eb538
🤖 fix: increase nodemon delay to 1000ms for dev-server
sreya Nov 14, 2025
65415fc
rm comments
sreya Nov 14, 2025
becd147
🤖 refactor: address review comments on terminal IPC refactor
sreya Nov 14, 2025
74d3274
🤖 fix: revert terminal auto-open (breaks IPC routing)
sreya Nov 14, 2025
ca43b55
🤖 fix: send terminal IPC events to correct window
sreya Nov 14, 2025
eb1694e
🤖 refactor: use event.stopPropagation() in terminal instead of focus …
sreya Nov 14, 2025
d274456
🤖 refactor: replace fs.existsSync with async fs.access in ptyService
sreya Nov 14, 2025
7df02e8
🤖 refactor: use native event listener to stop terminal keyboard propa…
sreya Nov 14, 2025
aed32ee
🤖 fix: terminal input not working due to capture phase event listener
sreya Nov 14, 2025
b35c2e5
🤖 refactor: remove vestigial terminal focus checks
sreya Nov 14, 2025
36050b0
🤖 fix: add stub update handlers for server mode
sreya Nov 14, 2025
633ee3d
🤖 debug: add logging to handleAddWorkspace
sreya Nov 14, 2025
95d6530
Merge branch 'main' into local-pty
sreya Nov 14, 2025
9f9dc55
🤖 feat: add terminal window support for browser/server mode
sreya Nov 14, 2025
b3e9668
🤖 fix: import browser API in terminal-window for browser mode
sreya Nov 14, 2025
2b3f5a0
🤖 fix: handle BrowserWindow.fromWebContents in server mode
sreya Nov 14, 2025
2cb3fdf
🤖 fix: handle null event in server mode
sreya Nov 14, 2025
5da9b1a
🤖 fix: change TERMINAL_INPUT from on() to handle() for server mode
sreya Nov 14, 2025
491d780
🤖 fix: create unique terminal windows in browser mode
sreya Nov 14, 2025
6e3e6e3
🤖 chore: remove debug logging from terminal handlers
sreya Nov 14, 2025
9324068
🤖 refactor: skip update checks in browser mode instead of stub handlers
sreya Nov 14, 2025
7b06d01
fmt
sreya Nov 14, 2025
ea25cc7
🤖 fix: prevent nodemon from watching main.js in dev-server
sreya Nov 14, 2025
5897608
🤖 feat: set terminal window title to show project/workspace name
sreya Nov 14, 2025
c673be7
🤖 fix: remove error when opening terminal in server mode
sreya Nov 14, 2025
a508097
🤖 fix: use workspace.list() to get metadata for terminal title
sreya Nov 14, 2025
e4ec87c
Revert "🤖 fix: remove error when opening terminal in server mode"
sreya Nov 14, 2025
b356201
fmt
sreya Nov 14, 2025
f6fdddb
🤖 fix: add postinstall script to rebuild native dependencies
sreya Nov 14, 2025
d697d32
ci
sreya Nov 14, 2025
6c48b73
build
sreya Nov 14, 2025
10ac0d5
Merge branch 'main' into local-pty
sreya Nov 14, 2025
b3da99a
readd dependency
sreya Nov 14, 2025
291d12d
omit 'server' arg to main-server
sreya Nov 14, 2025
2e011cf
make lint
sreya Nov 14, 2025
84432e2
Merge branch 'main' into local-pty
sreya Nov 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Vim swap files
*.swp
*.swo
*~
# Font files (copied from node_modules during build)
public/fonts/

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ dev: node_modules/.installed build-main ## Start development server (Vite + node
"bun x nodemon --watch src --watch tsconfig.main.json --watch tsconfig.json --ext ts,tsx,json --ignore dist --ignore node_modules --exec node scripts/build-main-watch.js" \
"vite"
else
dev: node_modules/.installed build-main ## Start development server (Vite + tsgo watcher for 10x faster type checking)
dev: node_modules/.installed build-main build-preload## Start development server (Vite + tsgo watcher for 10x faster type checking)
@bun x concurrently -k \
"bun x concurrently \"$(TSGO) -w -p tsconfig.main.json\" \"bun x tsc-alias -w -p tsconfig.main.json\"" \
"vite"
Expand Down
521 changes: 346 additions & 175 deletions bun.lock

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@ai-sdk/anthropic": "^2.0.44",
"@ai-sdk/openai": "^2.0.66",
"@openrouter/ai-sdk-provider": "^1.2.2",
"ghostty-web": "^0.1.1",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-scroll-area": "^1.2.10",
Expand All @@ -71,6 +72,7 @@
"minimist": "^1.2.8",
"motion": "^12.23.24",
"ollama-ai-provider-v2": "^1.5.4",
"node-pty": "1.1.0-beta39",
"rehype-harden": "^1.1.5",
"shescape": "^2.1.6",
"source-map-support": "^0.5.21",
Expand Down Expand Up @@ -120,6 +122,7 @@
"electron-builder": "^24.6.0",
"electron-devtools-installer": "^4.0.0",
"electron-mock-ipc": "^0.3.12",
"electron-rebuild": "^3.2.9",
"esbuild": "^0.25.11",
"escape-html": "^1.0.3",
"eslint": "^9.36.0",
Expand Down Expand Up @@ -211,6 +214,7 @@
"target": "nsis",
"icon": "build/icon.png",
"artifactName": "${productName}-${version}-${arch}.${ext}"
}
},
"npmRebuild": false
}
}
16 changes: 16 additions & 0 deletions src/App.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,22 @@ function setupMockAPI(options: {
window: {
setTitle: () => Promise.resolve(undefined),
},
terminal: {
create: () =>
Promise.resolve({
sessionId: "mock-session",
workspaceId: "mock-workspace",
cols: 80,
rows: 24,
}),
close: () => Promise.resolve(undefined),
resize: () => Promise.resolve(undefined),
sendInput: () => undefined,
onOutput: () => () => undefined,
onExit: () => () => undefined,
openWindow: () => Promise.resolve(undefined),
closeWindow: () => Promise.resolve(undefined),
},
update: {
check: () => Promise.resolve(undefined),
download: () => Promise.resolve(undefined),
Expand Down
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ function AppInner() {
}, [selectedWorkspace, workspaceMetadata, setSelectedWorkspace]);

const openWorkspaceInTerminal = useCallback((workspaceId: string) => {
void window.api.workspace.openTerminal(workspaceId);
void window.api.terminal.openWindow(workspaceId);
}, []);

const handleRemoveProject = useCallback(
Expand Down
30 changes: 30 additions & 0 deletions src/browser/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,33 @@ const webApi: IPCApi = {
return Promise.resolve();
},
},
terminal: {
create: (params) => invokeIPC(IPC_CHANNELS.TERMINAL_CREATE, params),
close: (sessionId) => invokeIPC(IPC_CHANNELS.TERMINAL_CLOSE, sessionId),
resize: (params) => invokeIPC(IPC_CHANNELS.TERMINAL_RESIZE, params),
sendInput: (sessionId: string, data: string) => {
// Send via IPC - in browser mode this becomes an HTTP POST
void invokeIPC(IPC_CHANNELS.TERMINAL_INPUT, sessionId, data);
},
Comment on lines +256 to +259
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be slow over the network. Fine for now, but we should probably allow the wsManager to send data for the IPC at some point.

onOutput: (sessionId: string, callback: (data: string) => void) => {
// Subscribe to terminal output events via WebSocket
const channel = `terminal:output:${sessionId}`;
return wsManager.on(channel, callback as (data: unknown) => void);
},
onExit: (sessionId: string, callback: (exitCode: number) => void) => {
// Subscribe to terminal exit events via WebSocket
const channel = `terminal:exit:${sessionId}`;
return wsManager.on(channel, callback as (data: unknown) => void);
},
openWindow: (workspaceId) => {
// In browser mode, open a new window/tab with the terminal page
// Use a unique name with timestamp to create a new window each time
const url = `/terminal.html?workspaceId=${encodeURIComponent(workspaceId)}`;
window.open(url, `terminal-${workspaceId}-${Date.now()}`, "width=1000,height=600");
return invokeIPC(IPC_CHANNELS.TERMINAL_WINDOW_OPEN, workspaceId);
},
closeWindow: (workspaceId) => invokeIPC(IPC_CHANNELS.TERMINAL_WINDOW_CLOSE, workspaceId),
},
update: {
check: () => invokeIPC(IPC_CHANNELS.UPDATE_CHECK),
download: () => invokeIPC(IPC_CHANNELS.UPDATE_DOWNLOAD),
Expand All @@ -263,6 +290,9 @@ const webApi: IPCApi = {
server: {
getLaunchProject: () => invokeIPC("server:getLaunchProject"),
},
// In browser mode, set platform to "browser" to differentiate from Electron
platform: "browser" as const,
versions: {},
};

if (typeof window.api === "undefined") {
Expand Down
3 changes: 2 additions & 1 deletion src/components/AIView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
// Track active tab to conditionally enable resize functionality
// RightSidebar notifies us of tab changes via onTabChange callback
const [activeTab, setActiveTab] = useState<TabType>("costs");

const isReviewTabActive = activeTab === "review";

// Resizable sidebar for Review tab only
Expand Down Expand Up @@ -195,7 +196,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
);

const handleOpenTerminal = useCallback(() => {
void window.api.workspace.openTerminal(workspaceId);
void window.api.terminal.openWindow(workspaceId);
}, [workspaceId]);

// Auto-scroll when messages or todos update (during streaming)
Expand Down
248 changes: 248 additions & 0 deletions src/components/TerminalView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import { useRef, useEffect, useState } from "react";
import { Terminal, FitAddon } from "ghostty-web";
import { useTerminalSession } from "@/hooks/useTerminalSession";

interface TerminalViewProps {
workspaceId: string;
sessionId?: string;
visible: boolean;
}

export function TerminalView({ workspaceId, sessionId, visible }: TerminalViewProps) {
const containerRef = useRef<HTMLDivElement>(null);
const termRef = useRef<Terminal | null>(null);
const fitAddonRef = useRef<FitAddon | null>(null);
const [terminalError, setTerminalError] = useState<string | null>(null);
const [terminalReady, setTerminalReady] = useState(false);
const [terminalSize, setTerminalSize] = useState<{ cols: number; rows: number } | null>(null);

// Handler for terminal output
const handleOutput = (data: string) => {
const term = termRef.current;
if (term) {
term.write(data);
}
};

// Handler for terminal exit
const handleExit = (exitCode: number) => {
const term = termRef.current;
if (term) {
term.write(`\r\n[Process exited with code ${exitCode}]\r\n`);
}
};

const {
sendInput,
resize,
error: sessionError,
} = useTerminalSession(workspaceId, sessionId, visible, terminalSize, handleOutput, handleExit);

// Keep refs to latest functions so callbacks always use current version
const sendInputRef = useRef(sendInput);
const resizeRef = useRef(resize);

useEffect(() => {
sendInputRef.current = sendInput;
resizeRef.current = resize;
}, [sendInput, resize]);

// Initialize terminal when visible
useEffect(() => {
if (!containerRef.current || !visible) {
return;
}

let terminal: Terminal | null = null;

const initTerminal = async () => {
try {
terminal = new Terminal({
fontSize: 13,
fontFamily: "Monaco, Menlo, 'Courier New', monospace",
cursorBlink: true,
theme: {
background: "#1e1e1e",
foreground: "#d4d4d4",
cursor: "#d4d4d4",
cursorAccent: "#1e1e1e",
selectionBackground: "#264f78",
black: "#000000",
red: "#cd3131",
green: "#0dbc79",
yellow: "#e5e510",
blue: "#2472c8",
magenta: "#bc3fbc",
cyan: "#11a8cd",
white: "#e5e5e5",
brightBlack: "#666666",
brightRed: "#f14c4c",
brightGreen: "#23d18b",
brightYellow: "#f5f543",
brightBlue: "#3b8eea",
brightMagenta: "#d670d6",
brightCyan: "#29b8db",
brightWhite: "#ffffff",
},
});

const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);

await terminal.open(containerRef.current!);
fitAddon.fit();

const { cols, rows } = terminal;

// Set terminal size so PTY session can be created with matching dimensions
// Use stable object reference to prevent unnecessary effect re-runs
setTerminalSize((prev) => {
if (prev?.cols === cols && prev?.rows === rows) {
return prev;
}
return { cols, rows };
});

// User input → IPC (use ref to always get latest sendInput)
terminal.onData((data: string) => {
sendInputRef.current(data);
});

termRef.current = terminal;
fitAddonRef.current = fitAddon;
setTerminalReady(true);
} catch (err) {
console.error("Failed to initialize terminal:", err);
setTerminalError(err instanceof Error ? err.message : "Failed to initialize terminal");
}
};

void initTerminal();

return () => {
if (terminal) {
terminal.dispose();
}
termRef.current = null;
fitAddonRef.current = null;
setTerminalReady(false);
setTerminalSize(null);
};
// Note: sendInput and resize are intentionally not in deps
// They're used in callbacks, not during effect execution
}, [visible, workspaceId]);

// Resize on container size change
useEffect(() => {
if (!visible || !fitAddonRef.current || !containerRef.current || !termRef.current) {
return;
}

let lastCols = 0;
let lastRows = 0;
let resizeTimeoutId: ReturnType<typeof setTimeout> | null = null;
let pendingResize: { cols: number; rows: number } | null = null;

// Use both ResizeObserver (for container changes) and window resize (as backup)
const handleResize = () => {
if (fitAddonRef.current && termRef.current) {
try {
// Resize terminal UI to fit container immediately for responsive UX
fitAddonRef.current.fit();

// Get new dimensions
const { cols, rows } = termRef.current;

// Only process if dimensions actually changed
if (cols === lastCols && rows === lastRows) {
return;
}

lastCols = cols;
lastRows = rows;

// Update state (with stable reference to prevent unnecessary re-renders)
setTerminalSize((prev) => {
if (prev?.cols === cols && prev?.rows === rows) {
return prev;
}
return { cols, rows };
});

// Store pending resize
pendingResize = { cols, rows };

// Always debounce PTY resize to prevent vim corruption
// Clear any pending timeout and set a new one
if (resizeTimeoutId !== null) {
clearTimeout(resizeTimeoutId);
}

resizeTimeoutId = setTimeout(() => {
if (pendingResize) {
console.log(
`[TerminalView] Sending resize to PTY: ${pendingResize.cols}x${pendingResize.rows}`
);
// Double requestAnimationFrame to ensure vim is ready
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (pendingResize) {
resizeRef.current(pendingResize.cols, pendingResize.rows);
pendingResize = null;
}
});
});
}
resizeTimeoutId = null;
}, 300); // 300ms debounce - enough time for vim to stabilize
} catch (err) {
console.error("[TerminalView] Error fitting terminal:", err);
}
}
};

const resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(containerRef.current);

// Also listen to window resize as backup
window.addEventListener("resize", handleResize);

return () => {
if (resizeTimeoutId !== null) {
clearTimeout(resizeTimeoutId);
}
resizeObserver.disconnect();
window.removeEventListener("resize", handleResize);
};
}, [visible, terminalReady]); // terminalReady ensures ResizeObserver is set up after terminal is initialized

if (!visible) return null;

const errorMessage = terminalError ?? sessionError;

return (
<div
className="terminal-view"
style={{
width: "100%",
height: "100%",
backgroundColor: "#1e1e1e",
}}
>
{errorMessage && (
<div className="border-b border-red-900/30 bg-red-900/20 p-2 text-sm text-red-400">
Terminal Error: {errorMessage}
</div>
)}
<div
ref={containerRef}
className="terminal-container"
style={{
width: "100%",
height: "100%",
overflow: "hidden",
}}
/>
</div>
);
}
Loading