Skip to content

Add per-thread multi-terminal sessions across server contracts and UI#18

Merged
juliusmarminge merged 6 commits intomainfrom
codething/0e78a4e3
Feb 13, 2026
Merged

Add per-thread multi-terminal sessions across server contracts and UI#18
juliusmarminge merged 6 commits intomainfrom
codething/0e78a4e3

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Feb 13, 2026

Summary

  • add multi-terminal support per thread in server TerminalManager with session keys scoped by threadId + terminalId
  • persist terminal history per terminal, keep legacy default-terminal history compatibility, and support closing one or all terminals for a thread
  • propagate terminalId through terminal events/snapshots/contracts and update WebSocket tests/mocks accordingly
  • extend web thread state with terminal collections (terminalIds, activeTerminalId, terminalLayout, splitTerminalIds)
  • update terminal drawer/chat wiring to create, split, and activate terminals in the UI

Testing

  • apps/server/src/terminalManager.test.ts: added checks for isolated multi-terminal sessions, cleared event terminalId, and close-all behavior without terminalId
  • apps/server/src/wsServer.test.ts: updated terminal mock/session assertions to include default and explicit terminalId
  • packages/contracts/src/terminal.test.ts: updated/added coverage for terminal contract schema changes
  • apps/web/src/persistenceSchema.test.ts and apps/web/src/store.test.ts: updated coverage for new thread terminal persistence/store fields
  • Lint/tests: Not run

Open with Devin

Summary by CodeRabbit

  • New Features

    • Multi-terminal per-thread UI: create, split, switch, and close terminals with tabs/split layouts, grouping, and per-terminal focus/autofocus.
    • Threads now initialize and persist richer terminal state (terminal lists, active terminal, groups).
  • Bug Fixes

    • Per-terminal history persistence and isolated I/O; clearing/closing cleans per-terminal histories and emits terminal-aware events/snapshots.
    • Persistence format upgraded to include terminal grouping and defaults for smoother migrations.

- Support multiple terminal IDs per thread across contracts, server manager, and WS handling
- Persist terminal history per terminal ID and allow closing all terminals for a thread
- Update web thread state and terminal drawer to create, split, and switch active terminals
- Expand tests for multi-terminal behavior and terminal ID propagation
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Sessions are refactored to be terminal-scoped (threadId + terminalId). Added terminalId everywhere: contracts/schemas, server session/history management, websocket handlers, web state, persistence v7, UI components and tests to support multiple terminals per thread, per-terminal history files, and lifecycle (open/write/close/clear) semantics.

Changes

Cohort / File(s) Summary
Server: terminal manager & tests
apps/server/src/terminalManager.ts, apps/server/src/terminalManager.test.ts
Sessions indexed by composite key (threadId+terminalId); terminalId added to session state, snapshots, events; per-terminal history paths, persist queues, and cleanup; added helpers toSafeTerminalId, toSessionKey, sessionsForThread, closeSession, deleteAllHistoryForThread; tests expanded for multi-terminal behavior and history cleanup.
Server: websocket handlers & tests
apps/server/src/wsServer.test.ts
Handlers now accept/propagate terminalId (defaulted to DEFAULT_TERMINAL_ID); session keys use (threadId,terminalId); close supports per-terminal or whole-thread deletion; events include terminalId; tests updated to assert presence of terminalId.
Contracts / IPC / terminal schemas & tests
packages/contracts/src/terminal.ts, packages/contracts/src/terminal.test.ts, packages/contracts/src/ipc.ts
Added DEFAULT_TERMINAL_ID; introduced terminalSessionInputSchema and terminalClearInputSchema with terminalId defaulting; open/write/resize/clear inputs and event/snapshot schemas include terminalId; exported TerminalSessionInput/TerminalClearInput; IPC NativeApi.terminal.clear input type updated.
Web types & persistence schema + tests
apps/web/src/types.ts, apps/web/src/persistenceSchema.ts, apps/web/src/persistenceSchema.test.ts
Added DEFAULT_THREAD_TERMINAL_ID and ThreadTerminalGroup; persisted thread schema extended with terminalIds, activeTerminalId, terminalGroups, activeTerminalGroupId; introduced persistedState v7 and hydration/persistence logic to normalize/derive terminal groups and active group id; tests updated for v7 and legacy migration.
Web state & tests
apps/web/src/store.ts, apps/web/src/store.test.ts
Added actions SPLIT_THREAD_TERMINAL, NEW_THREAD_TERMINAL, SET_THREAD_ACTIVE_TERMINAL, CLOSE_THREAD_TERMINAL; implemented normalization helpers (normalizeTerminalIds, normalizeTerminalGroups, normalizeThreadTerminals, closeThreadTerminal) and reducer handlers to maintain terminal lists, groups, active id, and closing behavior; tests cover split/new/set/close flows.
Web UI components
apps/web/src/components/ThreadTerminalDrawer.tsx, apps/web/src/components/ChatView.tsx, apps/web/src/components/Sidebar.tsx, apps/web/src/App.tsx
Introduced TerminalViewport; expanded ThreadTerminalDrawer and ChatView/Sidebar/App wiring to include terminalIds, activeTerminalId, terminalGroups, activeTerminalGroupId and new callbacks (onSplitTerminal, onNewTerminal, onActiveTerminalChange, onCloseTerminal); UI supports tabs/split, new/close actions, autofocus and resize handling; new thread initialization includes terminal state.
Misc: tests & small updates
apps/web/src/persistenceSchema.test.ts, apps/server/src/terminalManager.test.ts, packages/contracts/src/terminal.test.ts
Tests updated to expect terminalId defaults, include terminalId on snapshots/events, validate per-terminal history behavior and cleanup, and expose DEFAULT_TERMINAL_ID/DEFAULT_THREAD_TERMINAL_ID.
IPC signature change
packages/contracts/src/ipc.ts
NativeApi.terminal.clear input type changed to TerminalClearInput.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Web as Web UI
participant WS as WS Server
participant TM as TerminalManager
participant FS as FileSystem
Web->>WS: terminal.open (threadId, terminalId)
WS->>TM: requireSession(threadId, terminalId) / open session
TM->>TM: create sessionKey (toSessionKey)
TM-->>WS: session opened (includes terminalId)
WS-->>Web: open response / event (includes terminalId)
Web->>WS: terminal.write (threadId, terminalId, input)
WS->>TM: write(sessionKey, input)
TM->>TM: append transcript, queuePersist(threadId,terminalId)
TM->>FS: enqueue persist write -> write per-terminal history file
FS-->>TM: write complete
Web->>WS: terminal.close (threadId, terminalId?)
alt terminalId provided
WS->>TM: closeSession(sessionKey)
TM->>TM: deleteHistory(threadId,terminalId)
TM->>FS: remove per-terminal history file
else no terminalId (close all)
WS->>TM: sessionsForThread(threadId) -> close all sessions
TM->>FS: remove all per-thread history files
end
TM-->>WS: emit closed events (include terminalId)
WS-->>Web: closed events

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main objective: adding multi-terminal session support per thread across server, contracts, and UI components.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ 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 codething/0e78a4e3

No actionable comments were generated in the recent review. 🎉


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

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Feb 13, 2026

Add per-thread multi-terminal sessions and UI controls across web app and server to support split and grouped terminals with persistence v7

Introduce terminalId across contracts, server, and web UI; persist per-terminal history and thread terminal grouping; add reducer actions and drawer UI for split, new, activate, and close terminal flows; migrate persisted state to v7 with grouped terminals.

📍Where to Start

Start with the terminal contracts (DEFAULT_TERMINAL_ID, schemas) in terminal.ts, then follow server session handling in terminalManager.ts, and finally review state normalization and actions in store.ts.


Macroscope summarized 93d0ac4.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 13, 2026

Greptile Overview

Greptile Summary

This PR adds multi-terminal support per thread, allowing users to create, split, and manage multiple isolated terminal sessions within a single conversation thread.

Key Changes:

  • Server TerminalManager now keys sessions by threadId + terminalId composite key, enabling multiple isolated terminals per thread
  • Terminal history is persisted per terminal with backward-compatible legacy path fallback for default terminals
  • All terminal contracts, events, and snapshots now include terminalId field
  • Web UI adds three terminal layouts: single (default), tabs (multiple terminals), and split (side-by-side)
  • Store normalization ensures terminal state consistency across actions and persistence hydration
  • Comprehensive test coverage added for multi-terminal isolation, event routing, and state management

Issues Found:

  • Legacy history migration bug in terminalManager.ts:603-609 - when legacy history file is found, it's read but not migrated to the new path format, causing future writes to continue using the legacy path instead of the terminalId-scoped path

Confidence Score: 3/5

  • This PR has one critical bug in legacy history migration that will cause data persistence issues
  • Score reflects a well-architected multi-terminal implementation with comprehensive test coverage, but the legacy history migration bug in terminalManager.ts is critical - it prevents proper migration from old to new history paths, causing terminals to continue using legacy paths indefinitely. The implementation is otherwise solid with proper session isolation, thorough test coverage, and careful state normalization in the UI layer.
  • Pay close attention to apps/server/src/terminalManager.ts - the readHistory method at lines 603-609 needs to migrate legacy history to the new path format

Important Files Changed

Filename Overview
packages/contracts/src/terminal.ts Added terminalId field to all terminal schemas and events, introduced DEFAULT_TERMINAL_ID constant, and created new terminalSessionInputSchema base schema
apps/server/src/terminalManager.ts Refactored to support multi-terminal sessions per thread using composite threadId + terminalId keys, with legacy history compatibility; critical bug found in legacy migration logic
apps/web/src/store.ts Added three new terminal actions (SPLIT_THREAD_TERMINAL, NEW_THREAD_TERMINAL, SET_THREAD_ACTIVE_TERMINAL) with normalization logic to ensure consistent terminal state
apps/web/src/persistenceSchema.ts Added four terminal fields to persisted thread schema with hydration logic that normalizes and validates terminal state on load
apps/web/src/components/ThreadTerminalDrawer.tsx Major refactor: extracted TerminalViewport component, added tab bar UI, split terminal layout support, and proper terminal lifecycle with terminalId propagation

Sequence Diagram

sequenceDiagram
    participant User
    participant ChatView
    participant Store
    participant ThreadTerminalDrawer
    participant TerminalViewport
    participant WebSocket
    participant TerminalManager
    participant PTY

    User->>ChatView: Click "New Terminal"
    ChatView->>Store: dispatch(NEW_THREAD_TERMINAL)
    Store->>Store: Generate unique terminalId
    Store->>Store: Add to terminalIds array
    Store->>Store: Set as activeTerminalId
    Store->>Store: Switch layout to "tabs"
    Store-->>ChatView: Updated thread state
    ChatView->>ThreadTerminalDrawer: Render with new terminalIds
    ThreadTerminalDrawer->>TerminalViewport: Mount new terminal viewport

    TerminalViewport->>TerminalViewport: Create xterm.js Terminal
    TerminalViewport->>WebSocket: terminal.open({threadId, terminalId, cwd, cols, rows})
    WebSocket->>TerminalManager: open(input)
    TerminalManager->>TerminalManager: Create sessionKey from threadId+terminalId
    TerminalManager->>TerminalManager: readHistory(threadId, terminalId)
    TerminalManager->>PTY: spawn shell process
    PTY-->>TerminalManager: Process started
    TerminalManager->>TerminalManager: Store session in Map with composite key
    TerminalManager-->>WebSocket: TerminalSessionSnapshot
    WebSocket-->>TerminalViewport: Snapshot with history
    TerminalViewport->>TerminalViewport: Write history to terminal
    TerminalViewport->>TerminalViewport: Focus terminal

    User->>TerminalViewport: Type command
    TerminalViewport->>WebSocket: terminal.write({threadId, terminalId, data})
    WebSocket->>TerminalManager: write(input)
    TerminalManager->>TerminalManager: requireSession(threadId, terminalId)
    TerminalManager->>PTY: process.write(data)

    PTY->>TerminalManager: onData(output)
    TerminalManager->>TerminalManager: Append to session.history
    TerminalManager->>TerminalManager: queuePersist(threadId, terminalId, history)
    TerminalManager->>WebSocket: emit("event", {type: "output", threadId, terminalId, data})
    WebSocket->>TerminalViewport: Push terminal event
    TerminalViewport->>TerminalViewport: terminal.write(output)

    User->>ChatView: Click "Split Terminal"
    ChatView->>Store: dispatch(SPLIT_THREAD_TERMINAL)
    Store->>Store: Generate new terminalId
    Store->>Store: Set splitTerminalIds=[first, second]
    Store->>Store: Switch layout to "split"
    Store-->>ChatView: Updated thread state
    ChatView->>ThreadTerminalDrawer: Render split layout
    ThreadTerminalDrawer->>TerminalViewport: Mount two viewports side-by-side
    Note over TerminalViewport,PTY: Each viewport opens its own session with unique terminalId
Loading

Last reviewed commit: d9ab1bf

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.

15 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 13, 2026

Additional Comments (1)

apps/server/src/terminalManager.ts
Legacy history isn't migrated to new path. When legacy file is found, it's read and returned but future writes will still go to legacy path instead of the new terminalId-scoped path. Should migrate on first read:

    try {
      const raw = await fs.promises.readFile(this.legacyHistoryPath(threadId), "utf8");
      const capped = capHistory(raw, this.historyLineLimit);
      await fs.promises.writeFile(historyPath, capped, "utf8");
      await fs.promises.rm(this.legacyHistoryPath(threadId), { force: true });
      return capped;

}

private async deleteAllHistoryForThread(threadId: string): Promise<void> {
const threadPrefix = `${toSafeThreadId(threadId)}_`;
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:690 Using _ as a filename delimiter between base64url values (e.g., threadId, terminalId) can collide with encoded data and risk incorrect matching/deletion. Suggest a separator outside the base64url alphabet (e.g., . or ~) for threadPrefix/historyPath, and adjust the matching/deletion logic accordingly.

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

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

Using `_` as a filename delimiter between base64url values (e.g., `threadId`, `terminalId`) can collide with encoded data and risk incorrect matching/deletion. Suggest a separator outside the base64url alphabet (e.g., `.` or `~`) for `threadPrefix`/`historyPath`, and adjust the matching/deletion logic accordingly.

- Add terminal close controls in tab and split terminal views
- Send `exit` before closing and update active terminal/focus in chat state
- Add reducer support and tests for closing tabs, split fallback, and final-terminal drawer hide
Comment thread apps/web/src/persistenceSchema.ts Outdated
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/components/ThreadTerminalDrawer.tsx (1)

111-371: ⚠️ Potential issue | 🟠 Major

Remove autoFocus from the init effect dependencies to prevent unnecessary terminal recreation.

Including autoFocus in the dependency array causes the entire effect to re-run whenever autoFocus changes, triggering terminal disposal and recreation along with extra api.terminal.open() and api.terminal.resize() calls, resulting in visible flicker. The dedicated autoFocus effect (lines 372–382) already handles focus independently, so the init effect can safely omit this dependency.

🛠️ Suggested fix
-  }, [api, autoFocus, cwd, terminalId, threadId]);
+  }, [api, cwd, terminalId, threadId]);
🤖 Fix all issues with AI agents
In `@apps/web/src/store.ts`:
- Around line 152-191: The normalizeThreadTerminals function currently preserves
terminalLayout:"split" even when validSplitIds.length < 2; change it so when
thread.terminalLayout === "split" but there are fewer than 2 valid split ids you
downgrade the layout to "tabs" if terminalIds.length > 1 or to "single"
otherwise, clear splitTerminalIds to an empty array, and ensure activeTerminalId
is selected from terminalIds (falling back to DEFAULT_THREAD_TERMINAL_ID) —
update the branch that computes validSplitIds/splitTerminalIds to return an
object with terminalLayout set to the appropriate downgraded value and a
consistent activeTerminalId when validSplitIds.length < 2.

Comment thread apps/web/src/store.ts
- Replace `terminalLayout`/`splitTerminalIds` with `terminalGroups` and `activeTerminalGroupId`
- Update terminal drawer UI to render grouped splits with a terminal sidebar
- Bump persisted web state to v7 with legacy v6 migration coverage in tests
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx Outdated
Co-authored-by: macroscopeapp[bot] <170038800+macroscopeapp[bot]@users.noreply.github.com>
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 303-315: The close handler currently only sends "exit\n" and then
dispatches CLOSE_THREAD_TERMINAL; update the handler (the lambda that captures
activeThreadId, api, dispatch and calls api.terminal.write) to first attempt
api.terminal.close({ threadId: activeThreadId, terminalId }) and await it (or
call it and catch errors), and if api.terminal.close is not available or fails,
fall back to api.terminal.write({ threadId: activeThreadId, terminalId, data:
"exit\n" }); preserve the existing dispatch({ type: "CLOSE_THREAD_TERMINAL",
threadId: activeThreadId, terminalId }) and setTerminalFocusRequestId update,
and ensure all API calls are wrapped in .catch to avoid blocking the UI.

Comment thread apps/web/src/components/ChatView.tsx
- Call `api.terminal.close` when available when closing a terminal tab
- Fall back to writing `exit\n` to preserve compatibility with older/uninitialized terminals
- Pass `deleteHistory: true` when closing a terminal
- Fallback path now clears terminal before sending `exit`
- Use `gitCwd` as terminal cwd when available
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