Skip to content

feat: Terminal#14

Merged
juliusmarminge merged 3 commits intomainfrom
feat/terminal
Feb 12, 2026
Merged

feat: Terminal#14
juliusmarminge merged 3 commits intomainfrom
feat/terminal

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Feb 12, 2026

Adds a terminal

  • CMD+J to toggle visibility (persisted per thread)
  • Resizable (height persisted per thread)
  • CMD+K (Mac), CTRL+L (others) to clear the buffer

Open with Devin

Summary by CodeRabbit

  • New Features
    • Integrated per-thread terminal with open/write/resize/clear/restart/close operations and live I/O/events (output, exited, restarted, cleared).
    • Real-time terminal event streaming to clients via WebSocket.
    • Terminal drawer UI: resizable, theme-aware, height sync, focus management, and keyboard shortcuts (Cmd/Ctrl+J to toggle; Ctrl+L/Cmd+K to clear).
    • Per-thread terminal state and height persisted with a default height.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 12, 2026

Walkthrough

Adds a thread-based terminal subsystem: server PTY adapter and TerminalManager, WebSocket RPC + terminalEvent channel, client XTerm.js terminal UI and shortcuts, expanded contracts/schemas for a multi-method/event terminal API, persistence changes for per-thread terminal state, and tests across server, client, and contracts.

Changes

Cohort / File(s) Summary
Server PTY & Terminal Manager
apps/server/src/ptyAdapter.ts, apps/server/src/terminalManager.ts, apps/server/src/terminalManager.test.ts, apps/server/package.json
Adds node-pty dependency and a typed PTY adapter; implements TerminalManager (open/write/resize/clear/restart/close), eventing, persistence hooks, and tests.
WebSocket Server Integration
apps/server/src/wsServer.ts, apps/server/src/wsServer.test.ts
Adds terminal RPC methods and terminalEvent channel; wires optional TerminalManager into server options and test harness (MockTerminalManager).
Client Terminal UI & Styling
apps/web/src/components/ThreadTerminalDrawer.tsx, apps/web/src/components/ChatView.tsx, apps/web/src/index.css, apps/web/src/main.tsx, apps/web/package.json
Adds ThreadTerminalDrawer using XTerm.js, integrates terminal toggle in ChatView, imports XTerm CSS, and adjusts terminal scrollbar styling; adds XTerm deps.
Client Shortcuts & Helpers
apps/web/src/terminal-shortcuts.ts, apps/web/src/terminal-shortcuts.test.ts
Adds cross-platform shortcut utilities (toggle and clear) and unit tests.
Client State & Persistence
apps/web/src/store.ts, apps/web/src/store.test.ts, apps/web/src/types.ts, apps/web/src/App.tsx, apps/web/src/components/Sidebar.tsx, apps/web/src/persistenceSchema.ts, apps/web/src/persistenceSchema.test.ts
Extends Thread with terminalOpen and terminalHeight and DEFAULT_THREAD_TERMINAL_HEIGHT; adds store actions and handlers; bumps persisted schema to v6 and updates hydration/persistence tests.
Client API (WS)
apps/web/src/wsNativeApi.ts
Replaces single terminal.run with granular methods: open, write, resize, clear, restart, close, and onEvent subscription.
Contracts / IPC & WS enums
packages/contracts/src/terminal.ts, packages/contracts/src/terminal.test.ts, packages/contracts/src/ipc.ts, packages/contracts/src/ws.ts
Replaces command-style terminal schema with thread/session/event schemas and snapshots; updates NativeApi.terminal shape, WS method constants, channel, and related tests.
Server git & output truncation
apps/server/src/git.ts, apps/server/src/git.test.ts
Adds TerminalCommandInput/Result types locally; implements maxOutputBytes truncation for runGit/runTerminalCommand and test for truncation behavior.
Client tests & misc
apps/web/src/persistenceSchema.test.ts, apps/web/src/store.test.ts, apps/web/src/terminal-shortcuts.test.ts, apps/server/src/wsServer.test.ts
Adds/updates tests to cover terminal persistence, store actions, shortcut behavior, and WS terminal event routing.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client as Web Client
participant WS as WS Server
participant TM as TerminalManager
participant PTY as Node PTY
participant Store as Persistence

Client->>WS: terminal.open(threadId,cwd,cols,rows)
WS->>TM: open(...)
TM->>PTY: spawn(shell,cwd,cols,rows,env)
PTY-->>TM: started(pid)
TM->>Store: persist session snapshot/history
TM-->>WS: emit started event
WS-->>Client: push terminalEvent(started)
Client->>WS: terminal.write(data)
WS->>TM: write(threadId,data)
TM->>PTY: write(data)
PTY-->>TM: output(data)
TM->>Store: append history
TM-->>WS: emit output event
WS-->>Client: push terminalEvent(output)
PTY-->>TM: exit(code)
TM->>Store: finalize session (exit)
TM-->>WS: emit exited event
WS-->>Client: push terminalEvent(exited)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: Terminal' is related to the changeset but overly broad and lacks specificity about the main changes. Consider a more descriptive title that captures the key feature, such as 'feat: Add terminal component with persistence and keyboard shortcuts' or 'feat: Implement per-thread terminal UI with resize and history management'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/terminal

Comment @coderabbitai help to get the list of available commands and usage tips.

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Feb 12, 2026

Add interactive terminal with session API, WebSocket methods, event channel, PTY-backed TerminalManager, and UI drawer with Cmd/Ctrl+J toggle and output capping at 1,000,000 bytes

Introduce a session-based terminal contract and server routing, implement a PTY-backed TerminalManager with event broadcasting, and add a web terminal drawer UI with resizing, history restore, and shortcut handling; update persistence to version 6 with terminalOpen and terminalHeight. Key entry points: apps/server/src/wsServer.ts, packages/contracts/src/terminal.ts, and apps/web/src/components/ThreadTerminalDrawer.tsx.

📍Where to Start

Start in createServer in apps/server/src/wsServer.ts to see terminal RPC methods and event broadcasting, then review TerminalManager in apps/server/src/terminalManager.ts and the contract changes in packages/contracts/src/terminal.ts.


Macroscope summarized 42b2a88.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 12, 2026

Greptile Overview

Greptile Summary

This PR adds a full-featured integrated terminal panel to the web UI with resizable drawer, persistent height per thread, and robust server-side PTY management.

Major changes:

  • Introduced TerminalManager on server with PTY lifecycle, history persistence to .logs/terminals/, event streaming, and queued file writes
  • Added ThreadTerminalDrawer component with drag-to-resize, theme sync via MutationObserver, keyboard shortcuts (Cmd/Ctrl+J toggle, Ctrl+L/Cmd+K clear), and xterm.js FitAddon integration
  • Implemented terminal state in store with terminalOpen and terminalHeight fields, bumped persistence schema to v6 with backward-compatible migration
  • Created comprehensive contracts with Zod schemas for terminal operations (open, write, resize, clear, restart, close) and event types (started, output, exited, error, cleared, restarted)
  • Added WebSocket methods and terminal.event channel for real-time terminal I/O streaming
  • Built abstraction layer (PtyAdapter) over node-pty for testability with comprehensive test coverage across terminal lifecycle, history capping, restart, and exit scenarios

The architecture follows the project's patterns: server manages PTY sessions, streams events through WebSocket push messages, and web UI renders terminal with client-side state persistence.

Confidence Score: 4/5

  • Safe to merge with one duplicate event listener to fix
  • Strong implementation with comprehensive tests, proper cleanup, and good architecture. One logic issue found: duplicate window resize listener in ThreadTerminalDrawer.tsx causes redundant API calls. Otherwise follows project patterns well with Zod validation, event-driven design, and robust error handling.
  • apps/web/src/components/ThreadTerminalDrawer.tsx - remove duplicate resize listener on line 366

Important Files Changed

Filename Overview
packages/contracts/src/terminal.ts Defines comprehensive terminal contracts with proper validation bounds for cols/rows, status types, and event schemas
apps/server/src/ptyAdapter.ts Clean adapter pattern wrapping node-pty with proper event subscription/unsubscription
apps/server/src/terminalManager.ts Robust terminal lifecycle manager with history persistence, process cleanup, and event emission - well-tested
apps/server/src/wsServer.ts Integrates TerminalManager, routes terminal methods, broadcasts events to clients, and disposes on shutdown
apps/web/src/components/ThreadTerminalDrawer.tsx Feature-rich terminal UI with resize, theme sync, shortcuts, and event handling - has overlapping resize listeners that may cause extra API calls
apps/web/src/components/ChatView.tsx Integrates terminal drawer with toggle shortcut (Cmd/Ctrl+J), focus management, and height persistence
apps/web/src/store.ts Adds terminal state actions (toggle, set open/height) and bumps persistence version to v6 with legacy migration
apps/web/src/persistenceSchema.ts Adds terminalOpen and terminalHeight fields to thread schema with proper validation and defaults

Sequence Diagram

sequenceDiagram
    participant UI as ThreadTerminalDrawer
    participant API as WsNativeApi
    participant WS as WebSocket Server
    participant TM as TerminalManager
    participant PTY as node-pty

    UI->>API: terminal.open({threadId, cwd, cols, rows})
    API->>WS: WS_METHODS.terminalOpen
    WS->>TM: terminalManager.open(input)
    TM->>TM: Validate cwd, flush persist queue
    TM->>TM: Read history from .logs/terminals/
    TM->>PTY: ptyAdapter.spawn({shell, cwd, cols, rows})
    PTY-->>TM: ptyProcess with pid
    TM->>TM: Subscribe to onData, onExit
    TM-->>WS: TerminalSessionSnapshot
    WS-->>API: snapshot
    API-->>UI: snapshot
    UI->>UI: Write history to xterm, focus terminal

    loop Terminal I/O
        UI->>API: terminal.write({threadId, data})
        API->>WS: WS_METHODS.terminalWrite
        WS->>TM: terminalManager.write(input)
        TM->>PTY: ptyProcess.write(data)
        
        PTY->>TM: onData callback (output)
        TM->>TM: Append to history, cap lines, queue persist
        TM->>WS: Emit "output" event
        WS->>API: Push to WS_CHANNELS.terminalEvent
        API->>UI: onEvent callback
        UI->>UI: terminal.write(event.data)
    end

    UI->>API: terminal.resize({threadId, cols, rows})
    API->>WS: WS_METHODS.terminalResize
    WS->>TM: terminalManager.resize(input)
    TM->>PTY: ptyProcess.resize(cols, rows)

    PTY->>TM: onExit callback
    TM->>TM: Set status="exited", cleanup handles
    TM->>WS: Emit "exited" event
    WS->>API: Push to WS_CHANNELS.terminalEvent
    API->>UI: onEvent callback
    UI->>UI: onThreadExited() - close drawer
Loading

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

26 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@juliusmarminge juliusmarminge changed the title Improve terminal layout, sizing, and performance feat: Terminal Feb 12, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/server/src/terminalManager.ts`:
- Around line 348-359: The queuePersist method can leave an unhandled rejection
if fs.promises.writeFile fails; update queuePersist (referencing queuePersist,
task, previous, next, persistQueues and the writeFile call) to attach a
rejection handler that logs the error and prevents unhandled rejections — e.g.
wrap the writeFile call in a try/catch inside task or append .catch(...) to next
to handle and log errors (using the module's logger or console.error), while
preserving the existing finally cleanup that removes the entry from
persistQueues.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/web/src/components/ChatView.tsx`:
- Around line 1210-1211: There's a duplicate BranchToolbar rendering — two
consecutive <BranchToolbar envMode={envMode} onEnvModeChange={setEnvMode}
envLocked={envLocked} /> lines; remove the redundant duplicate so only a single
BranchToolbar instance remains (keep the one that correctly passes envMode,
onEnvModeChange/setEnvMode and envLocked) to prevent the toolbar from rendering
twice.
🧹 Nitpick comments (6)
apps/web/src/components/ThreadTerminalDrawer.tsx (1)

201-215: Duplicate window resize listeners may cause redundant work.

There are two window.addEventListener("resize", ...) calls:

  1. Lines 201-215: Handles drawer height clamping and syncing
  2. Line 366: Handles terminal fitting and backend resize notification

Both fire on every window resize, leading to duplicate calls to clampDrawerHeight and syncHeight. Consider consolidating resize handling into a single listener or ensuring they don't conflict.

♻️ Suggested consolidation

You could merge the resize logic into one handler:

  useEffect(() => {
    const onWindowResize = () => {
      const clampedHeight = clampDrawerHeight(drawerHeightRef.current);
      if (clampedHeight !== drawerHeightRef.current) {
        setDrawerHeight(clampedHeight);
      }
      if (!resizeStateRef.current) {
        syncHeight(clampedHeight);
      }
+     // Also fit terminal
+     const activeTerminal = terminalRef.current;
+     const activeFitAddon = fitAddonRef.current;
+     if (activeTerminal && activeFitAddon) {
+       activeFitAddon.fit();
+       void api.terminal
+         .resize({
+           threadId,
+           cols: activeTerminal.cols,
+           rows: activeTerminal.rows,
+         })
+         .catch(() => undefined);
+     }
    };
    window.addEventListener("resize", onWindowResize);
    return () => {
      window.removeEventListener("resize", onWindowResize);
    };
-  }, [syncHeight]);
+  }, [api, syncHeight, threadId]);

Then remove the duplicate listener from the terminal setup effect (line 366).

Also applies to: 366-367

apps/web/src/persistenceSchema.ts (2)

72-80: V5 and V6 schemas appear identical.

Both persistedStateV5Schema and persistedStateV6Schema have the same structure - only the version literal differs. If V5 was a development iteration that shipped, this is fine for backward compatibility. Otherwise, V5 could potentially be removed to simplify the schema union.


36-39: Consider aligning schema bounds with UI constraints.

The schema allows terminalHeight min of 120, but ThreadTerminalDrawer uses MIN_DRAWER_HEIGHT = 180. While the schema being more permissive is acceptable for storage flexibility, consider documenting this discrepancy or aligning the values to avoid confusion.

apps/server/src/terminalManager.test.ts (1)

103-134: Consider wrapping test body in try/finally for cleanup.

In tests like spawns lazily and reuses running terminal per thread, if an assertion fails before manager.dispose(), the manager won't be disposed. While this may not cause issues in isolated tests, using try/finally would ensure consistent cleanup:

it("spawns lazily and reuses running terminal per thread", async () => {
  const { manager, ptyAdapter } = makeManager();
  try {
    const first = await manager.open(openInput());
    // ... assertions ...
  } finally {
    manager.dispose();
  }
});

This is a minor concern since vitest runs tests in isolation, but it's a good practice for resource-heavy tests.

packages/contracts/src/terminal.test.ts (1)

97-118: Good coverage of discriminated union event types.

The terminalEventSchema tests validate both "output" and "exited" event variants. Consider adding tests for other event types ("started", "error", "cleared", "restarted") for complete coverage.

apps/server/src/terminalManager.ts (1)

226-232: Consider flushing pending persists on dispose (optional).

The dispose method clears persistQueues without awaiting pending writes. This could lose in-flight history data on shutdown. If history preservation during graceful shutdown is important, consider adding an optional flush:

async dispose(): Promise<void> {
  for (const session of this.sessions.values()) {
    this.stopProcess(session);
  }
  // Optionally flush pending persists
  await Promise.all([...this.persistQueues.values()].map(p => p.catch(() => {})));
  this.sessions.clear();
  this.persistQueues.clear();
}

However, the current synchronous approach is acceptable for fast shutdown scenarios.

Co-authored-by: codex <codex@users.noreply.github.com>
}
if (chunk.length <= remaining) {
return {
next: `${target}${chunk.toString()}`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low

src/git.ts:50 Using chunk.toString() directly can corrupt multi-byte UTF-8 characters that span chunk boundaries. Consider using Node's StringDecoder to handle partial characters correctly.

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/server/src/git.ts around line 50:

Using `chunk.toString()` directly can corrupt multi-byte UTF-8 characters that span chunk boundaries. Consider using Node's `StringDecoder` to handle partial characters correctly.

Comment on lines +299 to +300
} catch (error) {
if (ptyProcess) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low

src/terminalManager.ts:299 Consider calling this.cleanupProcessHandles(session) in the catch block before killing the process to prevent a memory leak when an exception occurs after listeners are attached.

Suggested change
} catch (error) {
if (ptyProcess) {
} catch (error) {
this.cleanupProcessHandles(session);
if (ptyProcess) {

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/server/src/terminalManager.ts around lines 299-300:

Consider calling `this.cleanupProcessHandles(session)` in the `catch` block before killing the process to prevent a memory leak when an exception occurs after listeners are attached.


private async readHistory(threadId: string): Promise<string> {
try {
const raw = await fs.promises.readFile(this.historyPath(threadId), "utf8");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High

src/terminalManager.ts:439 Reading entire history files into memory could cause OOM if legacy files are very large. Consider using streaming reads or checking file size first and truncating oversized files before reading.

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/server/src/terminalManager.ts around line 439:

Reading entire history files into memory could cause OOM if legacy files are very large. Consider using streaming reads or checking file size first and truncating oversized files before reading.

});
}

dispose(): void {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low

src/terminalManager.ts:244 dispose() clears threadLocks without awaiting pending operations. If called while close() is suspended (e.g., in flushPersistQueue), a concurrent open() can acquire the lock and create a session whose history is then deleted when close() resumes. Consider awaiting all pending locks before clearing them.

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/server/src/terminalManager.ts around line 244:

`dispose()` clears `threadLocks` without awaiting pending operations. If called while `close()` is suspended (e.g., in `flushPersistQueue`), a concurrent `open()` can acquire the lock and create a session whose history is then deleted when `close()` resumes. Consider awaiting all pending locks before clearing them.

}

private async readHistory(threadId: string): Promise<string> {
try {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low

src/terminalManager.ts:438 Consider moving the writeFile call outside the try block (or into its own try/catch). If readFile succeeds but writeFile fails with ENOENT, the current catch block swallows the error and falls through to legacy lookup, silently losing the already-loaded history.

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/server/src/terminalManager.ts around line 438:

Consider moving the `writeFile` call outside the `try` block (or into its own try/catch). If `readFile` succeeds but `writeFile` fails with `ENOENT`, the current catch block swallows the error and falls through to legacy lookup, silently losing the already-loaded history.

});
child.on("close", (code, signal) => {
clearTimeout(timeout);
if (outputTruncated) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low

src/git.ts:105 When output is truncated, the function appends a warning to stderr but still returns code: 0. Callers like listGitBranches check only code and will silently parse incomplete data. Consider either returning a non-zero code when truncated, adding a truncated field to the result, or throwing an error.

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/server/src/git.ts around line 105:

When output is truncated, the function appends a warning to `stderr` but still returns `code: 0`. Callers like `listGitBranches` check only `code` and will silently parse incomplete data. Consider either returning a non-zero code when truncated, adding a `truncated` field to the result, or throwing an error.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/web/src/components/ThreadTerminalDrawer.tsx`:
- Around line 263-282: The clear-shortcut handler currently sends a form-feed
via sendClearShortcut (which calls api.terminal.write with "\u000c") but should
call the server-side clear endpoint to reset persisted history; update
sendClearShortcut to call await api.terminal.clear({ threadId }) instead of
api.terminal.write, and preserve the existing try/catch behavior to surface
errors via writeSystemMessage(activeTerminal, ...). Keep the
terminal.attachCustomKeyEventHandler logic (isTerminalClearShortcut,
event.preventDefault/stopPropagation and return false) so the cleared event flow
you already handle remains intact.
🧹 Nitpick comments (2)
apps/web/src/components/ThreadTerminalDrawer.tsx (1)

110-382: Consider extracting terminal setup + resize logic into subcomponents/hooks.

This component is doing several distinct jobs (resize handling, terminal lifecycle, theme observer, shortcut wiring). Splitting into focused hooks/subcomponents would make it easier to maintain and test.

Based on learnings: "Extract large React components into multiple subcomponents with granular functionality. Co-locate subcomponents in the same file as the main component. Avoid hoisting callbacks too high up the component tree; prefer colocating logic close to JSX."

apps/server/src/git.ts (1)

71-108: Clarify whether maxOutputBytes is per-stream or total.

Right now stdout and stderr can each reach maxOutputBytes, so total capture can be ~2× the stated cap, while the truncation note reads like a single limit. If you want a hard overall cap, track a shared remaining budget across both streams or rename to maxOutputBytesPerStream.

Comment on lines +263 to +282
const sendClearShortcut = async () => {
const activeTerminal = terminalRef.current;
if (!activeTerminal) return;
try {
await api.terminal.write({ threadId, data: "\u000c" });
} catch (error) {
writeSystemMessage(
activeTerminal,
error instanceof Error ? error.message : "Failed to clear terminal",
);
}
};

terminal.attachCustomKeyEventHandler((event) => {
if (!isTerminalClearShortcut(event)) return true;
event.preventDefault();
event.stopPropagation();
void sendClearShortcut();
return false;
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use api.terminal.clear for the clear shortcut to reset persisted history.

Right now the shortcut only sends \u000c, so the server-side history is retained and will replay on reopen; it also misses the “clear scrollback” semantics expected from Cmd+K. Calling the clear API aligns with the cleared event you already handle.

🔧 Suggested change
-        await api.terminal.write({ threadId, data: "\u000c" });
+        await api.terminal.clear({ threadId });
🤖 Prompt for AI Agents
In `@apps/web/src/components/ThreadTerminalDrawer.tsx` around lines 263 - 282, The
clear-shortcut handler currently sends a form-feed via sendClearShortcut (which
calls api.terminal.write with "\u000c") but should call the server-side clear
endpoint to reset persisted history; update sendClearShortcut to call await
api.terminal.clear({ threadId }) instead of api.terminal.write, and preserve the
existing try/catch behavior to surface errors via
writeSystemMessage(activeTerminal, ...). Keep the
terminal.attachCustomKeyEventHandler logic (isTerminalClearShortcut,
event.preventDefault/stopPropagation and return false) so the cleared event flow
you already handle remains intact.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@apps/web/src/components/ChatView.tsx`:
- Around line 1212-1234: The terminal drawer is being opened with the project
root as cwd (cwd={activeProject.cwd}) which can run commands in the wrong
workspace for threads that have a worktree; change the cwd prop passed to
ThreadTerminalDrawer to use the thread's worktree when available (e.g.,
activeThread.worktree or similar) and fall back to activeProject.cwd otherwise,
updating the JSX at the ThreadTerminalDrawer usage so the cwd prop becomes the
conditional value.
- Around line 412-421: The keydown handler in the useEffect (the inline function
referenced by handler) should ignore repeated keydown events to avoid rapid
toggling; update the handler used with window.addEventListener to check
event.repeat (return early if true) before calling isTerminalToggleShortcut and
toggleTerminalVisibility, keeping the existing guards for activeThreadId and
event.preventDefault(); reference the useEffect, handler,
isTerminalToggleShortcut, toggleTerminalVisibility, and activeThreadId symbols
when making this change.
🧹 Nitpick comments (1)
apps/web/src/components/ChatView.tsx (1)

695-705: Add toggle semantics for a11y (optional).

Consider exposing toggle state for assistive tech.

♿ Suggested tweak
           <button
             type="button"
             className={`rounded-md px-2 py-1 text-[10px] transition-colors duration-150 ${
               activeThread.terminalOpen
                 ? "bg-accent text-foreground"
                 : "text-muted-foreground/40 hover:text-muted-foreground/60"
             }`}
+            aria-pressed={activeThread.terminalOpen}
             onClick={toggleTerminalVisibility}
           >

Comment on lines +412 to +421
useEffect(() => {
const handler = (event: globalThis.KeyboardEvent) => {
if (!activeThreadId) return;
if (!isTerminalToggleShortcut(event)) return;
event.preventDefault();
toggleTerminalVisibility();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [activeThreadId, toggleTerminalVisibility]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against key-repeat toggling.

Holding Cmd/Ctrl+J will emit repeat keydown events and can rapidly flip the terminal open/closed.

💡 Suggested fix
   useEffect(() => {
     const handler = (event: globalThis.KeyboardEvent) => {
+      if (event.repeat) return;
       if (!activeThreadId) return;
       if (!isTerminalToggleShortcut(event)) return;
       event.preventDefault();
       toggleTerminalVisibility();
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const handler = (event: globalThis.KeyboardEvent) => {
if (!activeThreadId) return;
if (!isTerminalToggleShortcut(event)) return;
event.preventDefault();
toggleTerminalVisibility();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [activeThreadId, toggleTerminalVisibility]);
useEffect(() => {
const handler = (event: globalThis.KeyboardEvent) => {
if (event.repeat) return;
if (!activeThreadId) return;
if (!isTerminalToggleShortcut(event)) return;
event.preventDefault();
toggleTerminalVisibility();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [activeThreadId, toggleTerminalVisibility]);
🤖 Prompt for AI Agents
In `@apps/web/src/components/ChatView.tsx` around lines 412 - 421, The keydown
handler in the useEffect (the inline function referenced by handler) should
ignore repeated keydown events to avoid rapid toggling; update the handler used
with window.addEventListener to check event.repeat (return early if true) before
calling isTerminalToggleShortcut and toggleTerminalVisibility, keeping the
existing guards for activeThreadId and event.preventDefault(); reference the
useEffect, handler, isTerminalToggleShortcut, toggleTerminalVisibility, and
activeThreadId symbols when making this change.

Comment on lines +1212 to +1234
{activeThread.terminalOpen && api && activeProject && (
<ThreadTerminalDrawer
key={activeThread.id}
api={api}
threadId={activeThread.id}
cwd={activeProject.cwd}
height={activeThread.terminalHeight}
focusRequestId={terminalFocusRequestId}
onHeightChange={(height) =>
dispatch({
type: "SET_THREAD_TERMINAL_HEIGHT",
threadId: activeThread.id,
height,
})
}
onThreadExited={() =>
dispatch({
type: "SET_THREAD_TERMINAL_OPEN",
threadId: activeThread.id,
open: false,
})
}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use the thread worktree as terminal cwd.

Line 1217 always uses the project root; for worktree threads this can send commands to the wrong workspace. Prefer the thread’s worktree when present.

🛠️ Proposed fix
-          cwd={activeProject.cwd}
+          cwd={activeThread.worktreePath ?? activeProject.cwd}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{activeThread.terminalOpen && api && activeProject && (
<ThreadTerminalDrawer
key={activeThread.id}
api={api}
threadId={activeThread.id}
cwd={activeProject.cwd}
height={activeThread.terminalHeight}
focusRequestId={terminalFocusRequestId}
onHeightChange={(height) =>
dispatch({
type: "SET_THREAD_TERMINAL_HEIGHT",
threadId: activeThread.id,
height,
})
}
onThreadExited={() =>
dispatch({
type: "SET_THREAD_TERMINAL_OPEN",
threadId: activeThread.id,
open: false,
})
}
/>
{activeThread.terminalOpen && api && activeProject && (
<ThreadTerminalDrawer
key={activeThread.id}
api={api}
threadId={activeThread.id}
cwd={activeThread.worktreePath ?? activeProject.cwd}
height={activeThread.terminalHeight}
focusRequestId={terminalFocusRequestId}
onHeightChange={(height) =>
dispatch({
type: "SET_THREAD_TERMINAL_HEIGHT",
threadId: activeThread.id,
height,
})
}
onThreadExited={() =>
dispatch({
type: "SET_THREAD_TERMINAL_OPEN",
threadId: activeThread.id,
open: false,
})
}
/>
🤖 Prompt for AI Agents
In `@apps/web/src/components/ChatView.tsx` around lines 1212 - 1234, The terminal
drawer is being opened with the project root as cwd (cwd={activeProject.cwd})
which can run commands in the wrong workspace for threads that have a worktree;
change the cwd prop passed to ThreadTerminalDrawer to use the thread's worktree
when available (e.g., activeThread.worktree or similar) and fall back to
activeProject.cwd otherwise, updating the JSX at the ThreadTerminalDrawer usage
so the cwd prop becomes the conditional value.

this.schedulePersist(threadId);
}

private async persistHistory(threadId: string, history: string): Promise<void> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low

src/terminalManager.ts:385 enqueuePersistWrite catches all errors internally, so persistHistory never rejects. If the disk write fails in clear(), in-memory history is wiped but old data remains on disk—reappearing after reload. Consider propagating write errors so callers can handle them.

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/server/src/terminalManager.ts around line 385:

`enqueuePersistWrite` catches all errors internally, so `persistHistory` never rejects. If the disk write fails in `clear()`, in-memory history is wiped but old data remains on disk—reappearing after reload. Consider propagating write errors so callers can handle them.

@juliusmarminge juliusmarminge merged commit 36cdceb into main Feb 12, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant