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

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

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

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Feb 13, 2026

Copy link
Copy Markdown
Member

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

coderabbitaiBot commented Feb 13, 2026

Copy link
Copy Markdown

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe 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

macroscopeappBot commented Feb 13, 2026

Copy link
Copy Markdown
Contributor

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 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

FilenameOverview
packages/contracts/src/terminal.tsAdded terminalId field to all terminal schemas and events, introduced DEFAULT_TERMINAL_ID constant, and created new terminalSessionInputSchema base schema
apps/server/src/terminalManager.tsRefactored 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.tsAdded 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.tsAdded four terminal fields to persisted thread schema with hydration logic that normalizes and validates terminal state on load
apps/web/src/components/ThreadTerminalDrawer.tsxMajor 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

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown

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
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 threadapps/web/src/persistenceSchema.ts Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 threadapps/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 threadapps/web/src/components/ThreadTerminalDrawer.tsx Outdated
Co-authored-by: macroscopeapp[bot] <170038800+macroscopeapp[bot]@users.noreply.github.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 threadapps/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
@juliusmarminge
juliusmarminge merged commit b26e2d1 into mainFeb 13, 2026
2 of 3 checks passed
aorwall added a commit to aorwall/t3code that referenced this pull request Aug 12, 2026
Adds the host half of the preview-annotation protocol for a framed page, so the
browser build can pick, draw on and comment about elements the way the Electron
app already could. The guest carries its own runtime — @moatless/inspector,
injected by the previewed app's build — and announces itself over postMessage;
a cross-origin frame cannot be injected into from outside.
Readiness is established from both directions, because the guest announcing
itself on load and the host asking for an announcement each lose a different
race. Back and forward, previously absent in a frame, now answer to the guest's
history.
Fork delta is additive: PreviewView.tsx gains 126 lines with zero
re-indentation, PreviewMoreMenu.tsx stays byte-identical to upstream behind a
fork-only PreviewFrameMoreMenu sibling, and ThreadPreviewMiniPlayer.tsx takes a
19-line wrap in line with existing FEATURES-gate precedent. All hunks in
upstream files are // Fork: marked; inventory and gaps register updated.
Known gap: @moatless/inspector 0.3.0 is not on npm (latest is 0.2.1), so a
previewed app declaring the dependency shadows the sandbox image's baked copy
and sees the annotate button disabled. Recorded in docs/fork/gaps.md.
Sign up for freeto 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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

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

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

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Feb 13, 2026

Copy link
Copy Markdown
Member

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

coderabbitaiBot commented Feb 13, 2026

Copy link
Copy Markdown

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe 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

macroscopeappBot commented Feb 13, 2026

Copy link
Copy Markdown
Contributor

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 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

FilenameOverview
packages/contracts/src/terminal.tsAdded terminalId field to all terminal schemas and events, introduced DEFAULT_TERMINAL_ID constant, and created new terminalSessionInputSchema base schema
apps/server/src/terminalManager.tsRefactored 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.tsAdded 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.tsAdded four terminal fields to persisted thread schema with hydration logic that normalizes and validates terminal state on load
apps/web/src/components/ThreadTerminalDrawer.tsxMajor 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

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown

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
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 threadapps/web/src/persistenceSchema.ts Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 threadapps/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 threadapps/web/src/components/ThreadTerminalDrawer.tsx Outdated
Co-authored-by: macroscopeapp[bot] <170038800+macroscopeapp[bot]@users.noreply.github.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 threadapps/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
@juliusmarminge
juliusmarminge merged commit b26e2d1 into mainFeb 13, 2026
2 of 3 checks passed
aorwall added a commit to aorwall/t3code that referenced this pull request Aug 12, 2026
Adds the host half of the preview-annotation protocol for a framed page, so the
browser build can pick, draw on and comment about elements the way the Electron
app already could. The guest carries its own runtime — @moatless/inspector,
injected by the previewed app's build — and announces itself over postMessage;
a cross-origin frame cannot be injected into from outside.
Readiness is established from both directions, because the guest announcing
itself on load and the host asking for an announcement each lose a different
race. Back and forward, previously absent in a frame, now answer to the guest's
history.
Fork delta is additive: PreviewView.tsx gains 126 lines with zero
re-indentation, PreviewMoreMenu.tsx stays byte-identical to upstream behind a
fork-only PreviewFrameMoreMenu sibling, and ThreadPreviewMiniPlayer.tsx takes a
19-line wrap in line with existing FEATURES-gate precedent. All hunks in
upstream files are // Fork: marked; inventory and gaps register updated.
Known gap: @moatless/inspector 0.3.0 is not on npm (latest is 0.2.1), so a
previewed app declaring the dependency shadows the sandbox image's baked copy
and sees the annotate button disabled. Recorded in docs/fork/gaps.md.
Sign up for freeto 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

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

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

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Feb 13, 2026

Copy link
Copy Markdown
Member

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

coderabbitaiBot commented Feb 13, 2026

Copy link
Copy Markdown

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe 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

macroscopeappBot commented Feb 13, 2026

Copy link
Copy Markdown
Contributor

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 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

FilenameOverview
packages/contracts/src/terminal.tsAdded terminalId field to all terminal schemas and events, introduced DEFAULT_TERMINAL_ID constant, and created new terminalSessionInputSchema base schema
apps/server/src/terminalManager.tsRefactored 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.tsAdded 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.tsAdded four terminal fields to persisted thread schema with hydration logic that normalizes and validates terminal state on load
apps/web/src/components/ThreadTerminalDrawer.tsxMajor 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

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown

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
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 threadapps/web/src/persistenceSchema.ts Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 threadapps/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 threadapps/web/src/components/ThreadTerminalDrawer.tsx Outdated
Co-authored-by: macroscopeapp[bot] <170038800+macroscopeapp[bot]@users.noreply.github.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 threadapps/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
@juliusmarminge
juliusmarminge merged commit b26e2d1 into mainFeb 13, 2026
2 of 3 checks passed
aorwall added a commit to aorwall/t3code that referenced this pull request Aug 12, 2026
Adds the host half of the preview-annotation protocol for a framed page, so the
browser build can pick, draw on and comment about elements the way the Electron
app already could. The guest carries its own runtime — @moatless/inspector,
injected by the previewed app's build — and announces itself over postMessage;
a cross-origin frame cannot be injected into from outside.
Readiness is established from both directions, because the guest announcing
itself on load and the host asking for an announcement each lose a different
race. Back and forward, previously absent in a frame, now answer to the guest's
history.
Fork delta is additive: PreviewView.tsx gains 126 lines with zero
re-indentation, PreviewMoreMenu.tsx stays byte-identical to upstream behind a
fork-only PreviewFrameMoreMenu sibling, and ThreadPreviewMiniPlayer.tsx takes a
19-line wrap in line with existing FEATURES-gate precedent. All hunks in
upstream files are // Fork: marked; inventory and gaps register updated.
Known gap: @moatless/inspector 0.3.0 is not on npm (latest is 0.2.1), so a
previewed app declaring the dependency shadows the sandbox image's baked copy
and sees the annotate button disabled. Recorded in docs/fork/gaps.md.
Sign up for freeto 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

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

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

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Feb 13, 2026

Copy link
Copy Markdown
Member

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

coderabbitaiBot commented Feb 13, 2026

Copy link
Copy Markdown

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe 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

macroscopeappBot commented Feb 13, 2026

Copy link
Copy Markdown
Contributor

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 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

FilenameOverview
packages/contracts/src/terminal.tsAdded terminalId field to all terminal schemas and events, introduced DEFAULT_TERMINAL_ID constant, and created new terminalSessionInputSchema base schema
apps/server/src/terminalManager.tsRefactored 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.tsAdded 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.tsAdded four terminal fields to persisted thread schema with hydration logic that normalizes and validates terminal state on load
apps/web/src/components/ThreadTerminalDrawer.tsxMajor 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

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown

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
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 threadapps/web/src/persistenceSchema.ts Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 threadapps/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 threadapps/web/src/components/ThreadTerminalDrawer.tsx Outdated
Co-authored-by: macroscopeapp[bot] <170038800+macroscopeapp[bot]@users.noreply.github.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 threadapps/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
@juliusmarminge
juliusmarminge merged commit b26e2d1 into mainFeb 13, 2026
2 of 3 checks passed
aorwall added a commit to aorwall/t3code that referenced this pull request Aug 12, 2026
Adds the host half of the preview-annotation protocol for a framed page, so the
browser build can pick, draw on and comment about elements the way the Electron
app already could. The guest carries its own runtime — @moatless/inspector,
injected by the previewed app's build — and announces itself over postMessage;
a cross-origin frame cannot be injected into from outside.
Readiness is established from both directions, because the guest announcing
itself on load and the host asking for an announcement each lose a different
race. Back and forward, previously absent in a frame, now answer to the guest's
history.
Fork delta is additive: PreviewView.tsx gains 126 lines with zero
re-indentation, PreviewMoreMenu.tsx stays byte-identical to upstream behind a
fork-only PreviewFrameMoreMenu sibling, and ThreadPreviewMiniPlayer.tsx takes a
19-line wrap in line with existing FEATURES-gate precedent. All hunks in
upstream files are // Fork: marked; inventory and gaps register updated.
Known gap: @moatless/inspector 0.3.0 is not on npm (latest is 0.2.1), so a
previewed app declaring the dependency shadows the sandbox image's baked copy
and sees the annotate button disabled. Recorded in docs/fork/gaps.md.
Sign up for freeto 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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

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

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

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Feb 13, 2026

Copy link
Copy Markdown
Member

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

coderabbitaiBot commented Feb 13, 2026

Copy link
Copy Markdown

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe 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

macroscopeappBot commented Feb 13, 2026

Copy link
Copy Markdown
Contributor

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 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

FilenameOverview
packages/contracts/src/terminal.tsAdded terminalId field to all terminal schemas and events, introduced DEFAULT_TERMINAL_ID constant, and created new terminalSessionInputSchema base schema
apps/server/src/terminalManager.tsRefactored 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.tsAdded 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.tsAdded four terminal fields to persisted thread schema with hydration logic that normalizes and validates terminal state on load
apps/web/src/components/ThreadTerminalDrawer.tsxMajor 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

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown

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
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 threadapps/web/src/persistenceSchema.ts Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 threadapps/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 threadapps/web/src/components/ThreadTerminalDrawer.tsx Outdated
Co-authored-by: macroscopeapp[bot] <170038800+macroscopeapp[bot]@users.noreply.github.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 threadapps/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
@juliusmarminge
juliusmarminge merged commit b26e2d1 into mainFeb 13, 2026
2 of 3 checks passed
aorwall added a commit to aorwall/t3code that referenced this pull request Aug 12, 2026
Adds the host half of the preview-annotation protocol for a framed page, so the
browser build can pick, draw on and comment about elements the way the Electron
app already could. The guest carries its own runtime — @moatless/inspector,
injected by the previewed app's build — and announces itself over postMessage;
a cross-origin frame cannot be injected into from outside.
Readiness is established from both directions, because the guest announcing
itself on load and the host asking for an announcement each lose a different
race. Back and forward, previously absent in a frame, now answer to the guest's
history.
Fork delta is additive: PreviewView.tsx gains 126 lines with zero
re-indentation, PreviewMoreMenu.tsx stays byte-identical to upstream behind a
fork-only PreviewFrameMoreMenu sibling, and ThreadPreviewMiniPlayer.tsx takes a
19-line wrap in line with existing FEATURES-gate precedent. All hunks in
upstream files are // Fork: marked; inventory and gaps register updated.
Known gap: @moatless/inspector 0.3.0 is not on npm (latest is 0.2.1), so a
previewed app declaring the dependency shadows the sandbox image's baked copy
and sees the annotate button disabled. Recorded in docs/fork/gaps.md.
Sign up for freeto 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

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

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

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Feb 13, 2026

Copy link
Copy Markdown
Member

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

coderabbitaiBot commented Feb 13, 2026

Copy link
Copy Markdown

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe 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

macroscopeappBot commented Feb 13, 2026

Copy link
Copy Markdown
Contributor

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 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

FilenameOverview
packages/contracts/src/terminal.tsAdded terminalId field to all terminal schemas and events, introduced DEFAULT_TERMINAL_ID constant, and created new terminalSessionInputSchema base schema
apps/server/src/terminalManager.tsRefactored 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.tsAdded 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.tsAdded four terminal fields to persisted thread schema with hydration logic that normalizes and validates terminal state on load
apps/web/src/components/ThreadTerminalDrawer.tsxMajor 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

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown

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
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 threadapps/web/src/persistenceSchema.ts Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 threadapps/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 threadapps/web/src/components/ThreadTerminalDrawer.tsx Outdated
Co-authored-by: macroscopeapp[bot] <170038800+macroscopeapp[bot]@users.noreply.github.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 threadapps/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
@juliusmarminge
juliusmarminge merged commit b26e2d1 into mainFeb 13, 2026
2 of 3 checks passed
aorwall added a commit to aorwall/t3code that referenced this pull request Aug 12, 2026
Adds the host half of the preview-annotation protocol for a framed page, so the
browser build can pick, draw on and comment about elements the way the Electron
app already could. The guest carries its own runtime — @moatless/inspector,
injected by the previewed app's build — and announces itself over postMessage;
a cross-origin frame cannot be injected into from outside.
Readiness is established from both directions, because the guest announcing
itself on load and the host asking for an announcement each lose a different
race. Back and forward, previously absent in a frame, now answer to the guest's
history.
Fork delta is additive: PreviewView.tsx gains 126 lines with zero
re-indentation, PreviewMoreMenu.tsx stays byte-identical to upstream behind a
fork-only PreviewFrameMoreMenu sibling, and ThreadPreviewMiniPlayer.tsx takes a
19-line wrap in line with existing FEATURES-gate precedent. All hunks in
upstream files are // Fork: marked; inventory and gaps register updated.
Known gap: @moatless/inspector 0.3.0 is not on npm (latest is 0.2.1), so a
previewed app declaring the dependency shadows the sandbox image's baked copy
and sees the annotate button disabled. Recorded in docs/fork/gaps.md.
Sign up for freeto 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

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

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

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Feb 13, 2026

Copy link
Copy Markdown
Member

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

coderabbitaiBot commented Feb 13, 2026

Copy link
Copy Markdown

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe 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

macroscopeappBot commented Feb 13, 2026

Copy link
Copy Markdown
Contributor

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 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

FilenameOverview
packages/contracts/src/terminal.tsAdded terminalId field to all terminal schemas and events, introduced DEFAULT_TERMINAL_ID constant, and created new terminalSessionInputSchema base schema
apps/server/src/terminalManager.tsRefactored 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.tsAdded 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.tsAdded four terminal fields to persisted thread schema with hydration logic that normalizes and validates terminal state on load
apps/web/src/components/ThreadTerminalDrawer.tsxMajor 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

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown

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
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 threadapps/web/src/persistenceSchema.ts Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 threadapps/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 threadapps/web/src/components/ThreadTerminalDrawer.tsx Outdated
Co-authored-by: macroscopeapp[bot] <170038800+macroscopeapp[bot]@users.noreply.github.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 threadapps/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
@juliusmarminge
juliusmarminge merged commit b26e2d1 into mainFeb 13, 2026
2 of 3 checks passed
aorwall added a commit to aorwall/t3code that referenced this pull request Aug 12, 2026
Adds the host half of the preview-annotation protocol for a framed page, so the
browser build can pick, draw on and comment about elements the way the Electron
app already could. The guest carries its own runtime — @moatless/inspector,
injected by the previewed app's build — and announces itself over postMessage;
a cross-origin frame cannot be injected into from outside.
Readiness is established from both directions, because the guest announcing
itself on load and the host asking for an announcement each lose a different
race. Back and forward, previously absent in a frame, now answer to the guest's
history.
Fork delta is additive: PreviewView.tsx gains 126 lines with zero
re-indentation, PreviewMoreMenu.tsx stays byte-identical to upstream behind a
fork-only PreviewFrameMoreMenu sibling, and ThreadPreviewMiniPlayer.tsx takes a
19-line wrap in line with existing FEATURES-gate precedent. All hunks in
upstream files are // Fork: marked; inventory and gaps register updated.
Known gap: @moatless/inspector 0.3.0 is not on npm (latest is 0.2.1), so a
previewed app declaring the dependency shadows the sandbox image's baked copy
and sees the annotate button disabled. Recorded in docs/fork/gaps.md.
Sign up for freeto 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

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

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

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Feb 13, 2026

Copy link
Copy Markdown
Member

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

coderabbitaiBot commented Feb 13, 2026

Copy link
Copy Markdown

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe 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

macroscopeappBot commented Feb 13, 2026

Copy link
Copy Markdown
Contributor

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 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

FilenameOverview
packages/contracts/src/terminal.tsAdded terminalId field to all terminal schemas and events, introduced DEFAULT_TERMINAL_ID constant, and created new terminalSessionInputSchema base schema
apps/server/src/terminalManager.tsRefactored 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.tsAdded 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.tsAdded four terminal fields to persisted thread schema with hydration logic that normalizes and validates terminal state on load
apps/web/src/components/ThreadTerminalDrawer.tsxMajor 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

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown

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
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 threadapps/web/src/persistenceSchema.ts Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 threadapps/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 threadapps/web/src/components/ThreadTerminalDrawer.tsx Outdated
Co-authored-by: macroscopeapp[bot] <170038800+macroscopeapp[bot]@users.noreply.github.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 threadapps/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
@juliusmarminge
juliusmarminge merged commit b26e2d1 into mainFeb 13, 2026
2 of 3 checks passed
aorwall added a commit to aorwall/t3code that referenced this pull request Aug 12, 2026
Adds the host half of the preview-annotation protocol for a framed page, so the
browser build can pick, draw on and comment about elements the way the Electron
app already could. The guest carries its own runtime — @moatless/inspector,
injected by the previewed app's build — and announces itself over postMessage;
a cross-origin frame cannot be injected into from outside.
Readiness is established from both directions, because the guest announcing
itself on load and the host asking for an announcement each lose a different
race. Back and forward, previously absent in a frame, now answer to the guest's
history.
Fork delta is additive: PreviewView.tsx gains 126 lines with zero
re-indentation, PreviewMoreMenu.tsx stays byte-identical to upstream behind a
fork-only PreviewFrameMoreMenu sibling, and ThreadPreviewMiniPlayer.tsx takes a
19-line wrap in line with existing FEATURES-gate precedent. All hunks in
upstream files are // Fork: marked; inventory and gaps register updated.
Known gap: @moatless/inspector 0.3.0 is not on npm (latest is 0.2.1), so a
previewed app declaring the dependency shadows the sandbox image's baked copy
and sees the annotate button disabled. Recorded in docs/fork/gaps.md.
Sign up for freeto 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

@juliusmarminge