Skip to content

Track per-thread terminal running state in UI and store#19

Merged
juliusmarminge merged 5 commits intomainfrom
codething/696df84f
Feb 13, 2026
Merged

Track per-thread terminal running state in UI and store#19
juliusmarminge merged 5 commits intomainfrom
codething/696df84f

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Feb 13, 2026

Summary

  • add terminalRunning to thread state and initialize it across thread creation and hydration paths
  • route terminal events through the store via APPLY_TERMINAL_EVENT instead of handling only exited inline
  • update reducer logic to keep terminal running/open state in sync for started, restarted, exited, and error events
  • wire ThreadTerminalDrawer running-state updates back to the store
  • show a pulsing Terminal status pill in the sidebar when a thread terminal is running
  • extend unit tests for reducer behavior and persistence shape updates

Testing

  • apps/web/src/store.test.ts: verifies SET_THREAD_TERMINAL_RUNNING, APPLY_TERMINAL_EVENT started handling, and exited handling (stops running + closes drawer)
  • apps/web/src/persistenceSchema.test.ts: verifies persisted thread shape includes terminal state fields
  • Not run: project lint/scripts in this PR context

Open with Devin

Summary by CodeRabbit

  • New Features

    • Terminal status indicator shows when one or more terminals are running for a thread.
    • Subprocess activity reporting emits activity events for running child processes.
  • Improvements

    • More graceful terminal close behavior with a reliable fallback.
  • Tests

    • Added coverage for terminal activity events and terminal lifecycle scenarios.
  • Chores

    • Migrated terminal history storage to a new per-terminal format with backward-compatible migration.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 13, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds terminal lifecycle tracking: threads gain runningTerminalIds; store handles APPLY_TERMINAL_EVENT; UI shows Terminal status and improved close behavior; server adds subprocess activity polling and migrates terminal-scoped history files; contracts add an "activity" terminal event variant; tests updated accordingly.

Changes

Cohort / File(s) Summary
Types & Store
apps/web/src/types.ts, apps/web/src/store.ts, apps/web/src/App.tsx
Added runningTerminalIds: string[] to Thread; imported TerminalEvent; added APPLY_TERMINAL_EVENT action and reducer logic; App effect listens for terminal events and dispatches them.
Web UI
apps/web/src/components/Sidebar.tsx, apps/web/src/components/ChatView.tsx
Sidebar: added Terminal status pill, terminalStatusPill() helper, and initialize runningTerminalIds for new threads. ChatView: updated terminal-close flow to call api.terminal.close(...) when available, otherwise fall back to writing "exit\n"; adjusted effect deps.
Persistence & Hydration
apps/web/src/persistenceSchema.ts, apps/web/src/persistenceSchema.test.ts
Hydrate thread now initializes runningTerminalIds: []; tests updated to include the new field.
State Tests
apps/web/src/store.test.ts
Added TerminalEvent imports, test helpers, and multiple tests asserting runningTerminalIds lifecycle across start, activity, exit, close, and multi-terminal scenarios.
Server Terminal Manager
apps/server/src/terminalManager.ts, apps/server/src/terminalManager.test.ts
Added subprocess monitoring (checker, poll interval, polling loop, session hasRunningSubprocess state); integrated polling into lifecycle methods; migrated history to terminal-scoped paths with legacy-file fallback and cleanup; tests added/updated for subprocess activity and migration.
Contracts
packages/contracts/src/terminal.ts, packages/contracts/src/terminal.test.ts
Added terminalActivityEventSchema (type "activity", hasRunningSubprocess: boolean) and extended terminalEventSchema union; test added to validate activity events.

Sequence Diagram

sequenceDiagram
    participant Terminal as Terminal
    participant Server as Server/TerminalManager
    participant App as App (web)
    participant Store as Redux Store
    participant UI as Sidebar/ChatView

    Terminal->>Server: emits low-level process events / subprocess state
    Server->>App: sends terminal events (started/activity/exited)
    App->>App: effect receives event
    App->>Store: dispatch APPLY_TERMINAL_EVENT { event }
    Store->>Store: update thread.runningTerminalIds based on event
    Store->>UI: state update triggers re-render
    UI->>UI: render Terminal status pill or update ChatView terminal state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (16 files):

⚔️ README.md (content)
⚔️ apps/server/package.json (content)
⚔️ apps/server/src/terminalManager.test.ts (content)
⚔️ apps/server/src/terminalManager.ts (content)
⚔️ apps/web/src/App.tsx (content)
⚔️ apps/web/src/components/ChatView.tsx (content)
⚔️ apps/web/src/components/Sidebar.tsx (content)
⚔️ apps/web/src/persistenceSchema.test.ts (content)
⚔️ apps/web/src/persistenceSchema.ts (content)
⚔️ apps/web/src/store.test.ts (content)
⚔️ apps/web/src/store.ts (content)
⚔️ apps/web/src/types.ts (content)
⚔️ package.json (content)
⚔️ packages/contracts/src/terminal.test.ts (content)
⚔️ packages/contracts/src/terminal.ts (content)
⚔️ turbo.json (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Track per-thread terminal running state in UI and store' accurately captures the main objective of the PR, which is to add and manage per-thread terminal running state across UI components and the store layer.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codething/696df84f

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

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Feb 13, 2026

Track per-thread terminal subprocess state and emit TerminalManager.activity events to update runningTerminalIds in UI and store

Add periodic subprocess polling in TerminalManager with new activity events, migrate legacy transcript paths on read, and route terminal events to update runningTerminalIds in the web store and show a sidebar status pill. Contracts validate the new activity event.

📍Where to Start

Start with subprocess polling and event emission in TerminalManager: see updateSubprocessPollingState and pollSubprocessActivity in apps/server/src/terminalManager.ts.


Macroscope summarized b185c14.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 13, 2026

Greptile Overview

Greptile Summary

Added terminalRunning boolean field to thread state, tracking whether a thread's terminal has an active process. Terminal events are now centrally routed through the store via APPLY_TERMINAL_EVENT action, which updates both running state and drawer visibility based on event type (started/restarted set running=true, exited/error set running=false). The UI displays a pulsing "Terminal" status pill in the sidebar when a terminal is active.

Key changes:

  • Centralized terminal event handling in App.tsx dispatches all events to store
  • Dual event listeners: App.tsx for store updates, ThreadTerminalDrawer for xterm UI updates
  • Proper initialization: new threads and hydrated threads start with terminalRunning: false
  • Comprehensive test coverage for new actions and reducer logic

Confidence Score: 4/5

  • Safe to merge with minor suggestion for robustness
  • The implementation is well-structured with proper state management, event routing, and comprehensive test coverage. The architecture correctly separates concerns (store updates vs terminal UI). One minor improvement suggested: the status check in APPLY_TERMINAL_EVENT could be more defensive by also accepting "starting" status for started/restarted events.
  • No files require special attention. The suggestion in apps/web/src/store.ts is a minor robustness improvement.

Important Files Changed

Filename Overview
apps/web/src/store.ts Added terminal state actions and reducer logic; status check on started/restarted events could be more robust
apps/web/src/App.tsx Routes all terminal events through APPLY_TERMINAL_EVENT action; initializes terminalRunning to false
apps/web/src/components/Sidebar.tsx Adds pulsing Terminal status pill when thread terminal is running; initializes new threads with terminalRunning: false
apps/web/src/components/ThreadTerminalDrawer.tsx Adds onRunningStateChange callback prop; updates running state on terminal open and error events

Sequence Diagram

sequenceDiagram
    participant User
    participant ChatView
    participant ThreadTerminalDrawer
    participant Store
    participant App
    participant API

    Note over User,API: Terminal Open Flow
    User->>ChatView: Open terminal
    ChatView->>ThreadTerminalDrawer: Render drawer
    ThreadTerminalDrawer->>API: terminal.open(threadId, cwd, cols, rows)
    API-->>ThreadTerminalDrawer: snapshot (status: "running")
    ThreadTerminalDrawer->>Store: onRunningStateChange(true)
    Store->>Store: SET_THREAD_TERMINAL_RUNNING

    Note over User,API: Terminal Event Flow (started/restarted)
    API->>App: terminal.onEvent({type: "started", snapshot})
    App->>Store: APPLY_TERMINAL_EVENT
    Store->>Store: Update terminalRunning based on snapshot.status
    API->>ThreadTerminalDrawer: terminal.onEvent({type: "started"})
    ThreadTerminalDrawer->>ThreadTerminalDrawer: Clear terminal, write history

    Note over User,API: Terminal Event Flow (exited)
    API->>App: terminal.onEvent({type: "exited"})
    App->>Store: APPLY_TERMINAL_EVENT
    Store->>Store: Set terminalRunning=false, terminalOpen=false
    API->>ThreadTerminalDrawer: terminal.onEvent({type: "exited"})
    ThreadTerminalDrawer->>Store: onThreadExited()

    Note over User,API: Terminal Event Flow (error)
    API->>App: terminal.onEvent({type: "error"})
    App->>Store: APPLY_TERMINAL_EVENT
    Store->>Store: Set terminalRunning=false
    API->>ThreadTerminalDrawer: terminal.onEvent({type: "error"})
    ThreadTerminalDrawer->>Store: onRunningStateChange(false)
    ThreadTerminalDrawer->>ThreadTerminalDrawer: Write error message

    Note over User,API: UI Update
    Store-->>ChatView: State update
    ChatView-->>User: Terminal status pill visible in sidebar
Loading

Last reviewed commit: 69a04d1

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

9 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment thread apps/web/src/store.ts Outdated
Comment on lines +398 to +403
terminalRunning:
action.event.type === "started" || action.event.type === "restarted"
? action.event.snapshot.status === "running"
: action.event.type === "exited" || action.event.type === "error"
? false
: t.terminalRunning,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Status check could be more robust. While started events currently always have status: "running" (terminal sets "starting" → spawns PTY → sets "running" → emits event), consider checking for "starting" as well or setting terminalRunning: true unconditionally for these event types. This would make the code more resilient to future changes in the terminal manager.

Suggested change
terminalRunning:
action.event.type === "started" || action.event.type === "restarted"
? action.event.snapshot.status === "running"
: action.event.type === "exited" || action.event.type === "error"
? false
: t.terminalRunning,
terminalRunning:
action.event.type === "started" || action.event.type === "restarted"
? action.event.snapshot.status === "running" || action.event.snapshot.status === "starting"
: action.event.type === "exited" || action.event.type === "error"
? false
: t.terminalRunning,

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/web/src/store.test.ts`:
- Around line 79-109: The type error is caused because overrides:
Partial<TerminalEvent> allows changing the discriminant "type" and yields an
incompatible union; update both makeTerminalStartedEvent and
makeTerminalActivityEvent to accept overrides: Partial<Omit<TerminalEvent,
"type">> (i.e., exclude the "type" field from the override) so the functions
keep their fixed discriminant ("started" or "activity") while still allowing
other fields to be overridden; keep the return type as TerminalEvent and spread
overrides as before.

Comment thread apps/web/src/store.test.ts Outdated
Comment on lines +79 to +109
function makeTerminalStartedEvent(overrides: Partial<TerminalEvent> = {}): TerminalEvent {
return {
type: "started",
threadId: "thread-local-1",
terminalId: DEFAULT_THREAD_TERMINAL_ID,
createdAt: "2026-02-09T00:00:01.000Z",
snapshot: {
threadId: "thread-local-1",
terminalId: DEFAULT_THREAD_TERMINAL_ID,
cwd: "/tmp/project",
status: "running",
pid: 1234,
history: "",
exitCode: null,
exitSignal: null,
updatedAt: "2026-02-09T00:00:01.000Z",
},
...overrides,
};
}

function makeTerminalActivityEvent(overrides: Partial<TerminalEvent> = {}): TerminalEvent {
return {
type: "activity",
threadId: "thread-local-1",
terminalId: DEFAULT_THREAD_TERMINAL_ID,
createdAt: "2026-02-09T00:00:02.000Z",
hasRunningSubprocess: true,
...overrides,
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix type error causing pipeline failure.

The pipeline fails with TS2719 because Partial<TerminalEvent> allows overriding the type discriminator, causing TypeScript to infer an incompatible union type. The spread of overrides could change type: "started" to any other event type.

🔧 Proposed fix: Use specific override types
-function makeTerminalStartedEvent(overrides: Partial<TerminalEvent> = {}): TerminalEvent {
+function makeTerminalStartedEvent(
+  overrides: Partial<Omit<Extract<TerminalEvent, { type: "started" }>, "type">> = {},
+): TerminalEvent {
   return {
     type: "started",
     threadId: "thread-local-1",
     terminalId: DEFAULT_THREAD_TERMINAL_ID,
     createdAt: "2026-02-09T00:00:01.000Z",
     snapshot: {
       threadId: "thread-local-1",
       terminalId: DEFAULT_THREAD_TERMINAL_ID,
       cwd: "/tmp/project",
       status: "running",
       pid: 1234,
       history: "",
       exitCode: null,
       exitSignal: null,
       updatedAt: "2026-02-09T00:00:01.000Z",
     },
     ...overrides,
-  };
+  } as TerminalEvent;
 }

-function makeTerminalActivityEvent(overrides: Partial<TerminalEvent> = {}): TerminalEvent {
+function makeTerminalActivityEvent(
+  overrides: Partial<Omit<Extract<TerminalEvent, { type: "activity" }>, "type">> = {},
+): TerminalEvent {
   return {
     type: "activity",
     threadId: "thread-local-1",
     terminalId: DEFAULT_THREAD_TERMINAL_ID,
     createdAt: "2026-02-09T00:00:02.000Z",
     hasRunningSubprocess: true,
     ...overrides,
-  };
+  } as TerminalEvent;
 }

Alternatively, a simpler fix using type assertions:

🔧 Simpler fix with type assertions
-function makeTerminalStartedEvent(overrides: Partial<TerminalEvent> = {}): TerminalEvent {
-  return {
+function makeTerminalStartedEvent(
+  overrides: Partial<TerminalEvent> & { type?: "started" } = {},
+): TerminalEvent {
+  return ({
     type: "started",
     threadId: "thread-local-1",
     terminalId: DEFAULT_THREAD_TERMINAL_ID,
     createdAt: "2026-02-09T00:00:01.000Z",
     snapshot: {
       threadId: "thread-local-1",
       terminalId: DEFAULT_THREAD_TERMINAL_ID,
       cwd: "/tmp/project",
       status: "running",
       pid: 1234,
       history: "",
       exitCode: null,
       exitSignal: null,
       updatedAt: "2026-02-09T00:00:01.000Z",
     },
     ...overrides,
-  };
+  }) as TerminalEvent;
 }

-function makeTerminalActivityEvent(overrides: Partial<TerminalEvent> = {}): TerminalEvent {
-  return {
+function makeTerminalActivityEvent(
+  overrides: Partial<TerminalEvent> & { type?: "activity" } = {},
+): TerminalEvent {
+  return ({
     type: "activity",
     threadId: "thread-local-1",
     terminalId: DEFAULT_THREAD_TERMINAL_ID,
     createdAt: "2026-02-09T00:00:02.000Z",
     hasRunningSubprocess: true,
     ...overrides,
-  };
+  }) as TerminalEvent;
 }
📝 Committable suggestion

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

Suggested change
function makeTerminalStartedEvent(overrides: Partial<TerminalEvent> = {}): TerminalEvent {
return {
type: "started",
threadId: "thread-local-1",
terminalId: DEFAULT_THREAD_TERMINAL_ID,
createdAt: "2026-02-09T00:00:01.000Z",
snapshot: {
threadId: "thread-local-1",
terminalId: DEFAULT_THREAD_TERMINAL_ID,
cwd: "/tmp/project",
status: "running",
pid: 1234,
history: "",
exitCode: null,
exitSignal: null,
updatedAt: "2026-02-09T00:00:01.000Z",
},
...overrides,
};
}
function makeTerminalActivityEvent(overrides: Partial<TerminalEvent> = {}): TerminalEvent {
return {
type: "activity",
threadId: "thread-local-1",
terminalId: DEFAULT_THREAD_TERMINAL_ID,
createdAt: "2026-02-09T00:00:02.000Z",
hasRunningSubprocess: true,
...overrides,
};
}
function makeTerminalStartedEvent(
overrides: Partial<TerminalEvent> & { type?: "started" } = {},
): TerminalEvent {
return ({
type: "started",
threadId: "thread-local-1",
terminalId: DEFAULT_THREAD_TERMINAL_ID,
createdAt: "2026-02-09T00:00:01.000Z",
snapshot: {
threadId: "thread-local-1",
terminalId: DEFAULT_THREAD_TERMINAL_ID,
cwd: "/tmp/project",
status: "running",
pid: 1234,
history: "",
exitCode: null,
exitSignal: null,
updatedAt: "2026-02-09T00:00:01.000Z",
},
...overrides,
}) as TerminalEvent;
}
function makeTerminalActivityEvent(
overrides: Partial<TerminalEvent> & { type?: "activity" } = {},
): TerminalEvent {
return ({
type: "activity",
threadId: "thread-local-1",
terminalId: DEFAULT_THREAD_TERMINAL_ID,
createdAt: "2026-02-09T00:00:02.000Z",
hasRunningSubprocess: true,
...overrides,
}) as TerminalEvent;
}
🧰 Tools
🪛 GitHub Actions: CI

[error] 80-80: TS2719: Type '{ createdAt: string; type: "started"; threadId: string; terminalId: string; snapshot: { status: "running" | "error" | "starting" | "exited"; cwd: string; threadId: string; updatedAt: string; ... 4 more ...; exitSignal: number | null; }; } | ... 5 more ... | { ...; }' is not assignable to type '{ createdAt: string; type: "started"; threadId: string; terminalId: string; snapshot: { status: "running" | "error" | "starting" | "exited"; cwd: string; threadId: string; updatedAt: string; ... 4 more ...; exitSignal: number | null; }; } | ... 5 more ... | { ...; }'. Two different types with this name exist, but they are unrelated. Type '{ createdAt: string; type: "started" | "output"; data?: string; threadId: string; terminalId: string; snapshot: { threadId: string; terminalId: string; cwd: string; status: "running"; pid: number; history: string; exitCode: null; exitSignal: null; updatedAt: string; }; }' is not assignable to type '{ createdAt: string; type: "started"; threadId: string; terminalId: string; snapshot: { status: "running" | "error" | "starting" | "exited"; cwd: string; threadId: string; updatedAt: string; ... 4 more ...; exitSignal: number | null; }; } | ... 5 more ... | { ...; }'.

🤖 Prompt for AI Agents
In `@apps/web/src/store.test.ts` around lines 79 - 109, The type error is caused
because overrides: Partial<TerminalEvent> allows changing the discriminant
"type" and yields an incompatible union; update both makeTerminalStartedEvent
and makeTerminalActivityEvent to accept overrides: Partial<Omit<TerminalEvent,
"type">> (i.e., exclude the "type" field from the override) so the functions
keep their fixed discriminant ("started" or "activity") while still allowing
other fields to be overridden; keep the return type as TerminalEvent and spread
overrides as before.

});
}

dispose(): void {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low

src/terminalManager.ts:434 Consider adding a disposed flag that's checked by runWithThreadLock or startSession. Currently, pending open operations can complete after dispose, restarting the subprocess polling timer and leaking resources.

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

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

Consider adding a `disposed` flag that's checked by `runWithThreadLock` or `startSession`. Currently, pending `open` operations can complete after `dispose`, restarting the subprocess polling timer and leaking resources.

juliusmarminge and others added 5 commits February 12, 2026 22:15
- add `terminalRunning` to thread state and hydration defaults
- apply terminal events in reducer to sync running/open state (close drawer on exit)
- wire drawer running-state callbacks and show a Terminal status pill in sidebar
- extend store and persistence tests for terminal running/exited behavior
- On open, read legacy transcript files and write them to the new terminal-scoped history path
- Remove legacy files after migration and warn if cleanup fails
- Update tests to assert migration, content preservation, and legacy file removal
- Attempt `api.terminal.close` when toggling an open terminal off
- Fall back to sending `exit` if close is unavailable or fails
- Include `api` in callback dependencies to keep behavior current
- Add cross-platform subprocess polling in `TerminalManager` and emit `activity` events when child-process state changes
- Update web store to derive `runningTerminalIds` from `activity.hasRunningSubprocess` instead of start/restart events
- Extend terminal contracts and tests to include the new `activity` event type

Co-authored-by: codex <codex@users.noreply.github.com>
- Pass subprocess test options only when defined in `terminalManager` tests
- Narrow web store terminal event fixtures to `started`/`activity` event shapes
Comment thread apps/server/src/terminalManager.ts
@juliusmarminge juliusmarminge merged commit 804572c into main Feb 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant