Skip to content

feat: add integrated terminal panel#88

Open
OverCart345 wants to merge 1 commit intorealDuang:mainfrom
OverCart345:feat/integrated-terminal
Open

feat: add integrated terminal panel#88
OverCart345 wants to merge 1 commit intorealDuang:mainfrom
OverCart345:feat/integrated-terminal

Conversation

@OverCart345
Copy link
Copy Markdown
Contributor

Summary

Adds an integrated terminal panel to the chat interface. The terminal opens below the chat/file explorer area, supports multiple tabs per session, and preserves PTY state when switching between sessions.

Changes

  • TerminalPanel component — xterm.js with multi-tab UI, per-session state
  • terminal-manager.ts — node-pty service with IPC handlers (terminal:create/write/resize/destroy)
  • Toggle button in header, per-session open/closed state, panel always mounted (no PTY kill on hide)
  • Hack Nerd Font as default
  • Locale/encoding env vars set in PTY fixes non-ASCII characters on Linux

Test Plan

  • Manual testing (describe steps below)
  • Unit tests pass (bun run test:unit)
  • E2E tests pass (bun run test:e2e)
  • Type check passes (npm run typecheck)

Screenshots

image

@realDuang realDuang self-assigned this Apr 4, 2026
@realDuang realDuang added enhancement New feature or request roadmap Roadmap tracking item labels Apr 4, 2026
Copy link
Copy Markdown
Owner

@realDuang realDuang left a comment

Choose a reason for hiding this comment

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

Hey @OverCart345, thanks for putting this integrated terminal is definitely a feature we've been wanting. The xterm.js + node-pty combo is the right choice, and the multi-tab / per-session state management looks well thought out.together

That said, there's one architectural call I'd like us to align on before iterating further, plus a few things that need fixing:

Architecture: Gateway vs IPC

The biggest thing this PR goes through Electron IPC directly, bypassing the Gateway WebSocket layer. I get that it's simpler and IPC is slightly lower latency, but in practice:here

  • node-pty + xterm.js already supports WebSocket transport natively (this is exactly how VS Code Server / code-server / ttyd work). The PTY side stays the same, you just swap the transport.
  • We already have a mature Gateway infra with auto-reconnect, auth, and RPC. Hooking into it is close to zero extra work.
  • Going through Gateway means we get Web PTY support for free just a matter of exposing the same handlers. IPC locks us into Electron-only with no upgrade path.later

So the ask is: please rearchitect this to go through the Gateway (terminal.create, terminal.write, terminal.data as new request/notification types in ws-server.ts + engine-manager.ts). The TerminalPanel frontend can stay mostly as-is, just swap terminalAPI calls for gateway.terminalCreate() etc.

If we do this, the terminalAPI in electron-api.ts, the preload additions, and the IPC handlers all go the whole thing actually gets simpler.away

Other items

See inline comments for specifics, but the highlights:

  1. ** cwd is passed straight to pty.spawn() with zero validation. Need at minimum a directory-exists check and path normalization.Security**
  2. Process destroyAllTerminals() is exported but never wired into app.on('will-quit'). PTY children will orphan on quit.leak
  3. Hardcoded ru_RU.UTF- should be en_US.UTF-8 or detect from system.8
  4. No terminal-manager.ts needs unit tests, especially the lifecycle/cleanup paths.tests
  5. ** all xterm instances across all sessions live forever in DOM. Needs some kind of eviction for inactive sessions.Memory**

Happy to discuss the Gateway approach if you want to hop on a call or open a design issue first.


const env: Record<string, string> = { ...process.env } as Record<string, string>;
if (!env.LANG) env.LANG = "ru_RU.UTF-8";
if (!env.LC_CTYPE) env.LC_CTYPE = env.LANG;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This ru_RU.UTF-8 fallback looks like it leaked from your local env :) Should be en_US.UTF-8 as a safe default — or better, detect via app.getLocale() and map to a POSIX locale.

if (!env.LC_CTYPE) env.LC_CTYPE = env.LANG;
if (!env.LC_ALL) env.LC_ALL = env.LANG;

const ptyProcess = pty.spawn(shell, [], {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The cwd param comes straight from the renderer and gets passed directly to pty.spawn() with no validation.

At minimum we need:

  • path.resolve() to normalize
  • fs.existsSync() + statSync().isDirectory() check
  • Maybe restrict to known project directories (the session already knows its directory)

This is especially important since CodeMux supports remote access — a compromised renderer could spawn shells in arbitrary directories.

term.pty.kill();
terminals.delete(id);
}
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

destroyAllTerminals() is exported but never actually called anywhere. It needs to be wired into app.on("will-quit") in electron/main/index.ts, otherwise all PTY child processes become orphans when the app quits.

Also — there's no error handling around pty.spawn(). If the shell doesn't exist or cwd is invalid, this will throw uncaught.

return isStartupReady();
});

// ===========================================================================
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Per the architecture discussion — the terminal service should go through the Gateway WebSocket layer (as new request/notification types like terminal.create, terminal.write, terminal.data) rather than IPC.

Reasoning:

  • node-pty + xterm.js natively supports WebSocket transport (same as VS Code Server / code-server)
  • We already have Gateway infra with auth, reconnect, RPC
  • Going through Gateway gives us Web PTY support later for free
  • The IPC handlers, preload additions, and electron-api.ts wrapper all become unnecessary

The TerminalPanel frontend stays mostly the same — just swap terminalAPI.* calls for gateway.terminal*() methods.

const xterm = new XTerm({
cursorBlink: true,
fontSize: 13,
fontFamily:
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Each tab registers a global terminal:data listener that receives output from all terminals, then filters by ID. With N tabs open, every PTY write fans out to N handlers in the renderer.

Once this moves to Gateway, consider using per-terminal subscription channels (or at least do the ID dispatch on the server side before sending to the client).

@@ -0,0 +1,348 @@
import { createSignal, createEffect, onCleanup, For, Show } from "solid-js";
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

A few hard-coded strings that need i18n treatment:

  • "New terminal" (button title)
  • "Hide terminal panel" (button title)
  • `Terminal ${n}` (tab label)

Should use t().terminal.* — you already added the locale key for togglePanel, just need a few more.

stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="flex-shrink-0 opacity-60"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

All tabs from all sessions stay in allTabs() permanently — DOM nodes and xterm instances never get cleaned up. If someone uses 20+ sessions over a workday, that's 20+ xterm renderers alive in memory.

Consider some kind of LRU eviction for inactive sessions — e.g., dispose xterm instances after a session has been inactive for a while, and re-attach from the still-running PTY when the user switches back.

Comment thread src/pages/Chat.tsx
return session?.title || "";
});

// Terminal panel state — per-session open/closed
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Terminal state management (terminalOpenBySession, terminalHeight, toggleTerminal, closeTerminal, addTerminalTab) adds ~35 lines here. Chat.tsx is already 2300+ lines.

Would be cleaner to pull this into a dedicated src/stores/terminal.ts — same pattern as fileStore for the file explorer panel. Keeps Chat.tsx as a composition root rather than accumulating more state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request roadmap Roadmap tracking item

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants