Skip to content

feat: enhance terminal with WebGL, ligatures, images, and themes - #1

Merged
gl11tchy merged 4 commits into
mainfrom
feat/terminal-enhancements
Dec 28, 2025
Merged

feat: enhance terminal with WebGL, ligatures, images, and themes#1
gl11tchy merged 4 commits into
mainfrom
feat/terminal-enhancements

Conversation

@gl11tchy

@gl11tchy gl11tchy commented Dec 27, 2025

Copy link
Copy Markdown
Owner

Summary

Upgrades the terminal with GPU acceleration, font ligatures, inline image support, and a theme system.

Changes

New Features

  • WebGL Renderer: GPU-accelerated rendering for smoother scrolling and large outputs (with canvas fallback)
  • Ligature Support: Programming font ligatures work with Fira Code, JetBrains Mono, etc.
  • Image Support: Sixel graphics and iTerm2 inline image protocol for CLI image tools
  • Theme System: 11 theme presets with live switching:
    • VS Code Dark/Light
    • Dracula
    • Nord
    • Monokai
    • One Dark
    • Solarized Dark/Light
    • GitHub Dark
    • Tokyo Night
    • Catppuccin Mocha

UI

  • Theme picker dropdown in terminal toolbar
  • Theme preference persisted to localStorage

Performance

  • Fast scroll modifier (hold Alt for 5x scroll speed)
  • Optimized scroll settings

Bug Fixes

  • Fixed React StrictMode double-mount issue in PTY spawning

Files Changed

  • package.json - Added 3 new xterm addon dependencies
  • src/lib/terminalThemes.ts - New theme definitions file
  • src/stores/editorStore.ts - Added theme state management
  • src/hooks/useTerminal.ts - Integrated new addons and theme support
  • src/components/Terminal/TerminalTabs.tsx - Added theme picker UI
  • src-tauri/src/commands/pty.rs - Fixed double-spawn issue

Testing

  1. npm run tauri dev
  2. Open terminal - should use WebGL renderer
  3. Click theme icon in terminal toolbar to switch themes
  4. Type => !== -> with a ligature font to see ligatures
  5. Test fast scrolling by holding Alt while scrolling
  • Bugs found:
    • Potential xterm addon version incompatibilities with @xterm/xterm (verify addon-webgl, addon-ligatures, addon-image compatibility and add runtime guards).
    • Missing dependency in TerminalTabs keyboard handler useEffect (setThemeDropdownOpen may be omitted, risking stale closure for Escape handling).
    • (Verify) ensure PTY child termination is explicit in kill_pty (child.kill() present and error-handled) — commit claims fixed, validate behavior on Windows/macOS/Linux.
  • Single combined prompt for Claude Code to remedy:

Fix remaining terminal-enhancements issues and add verification/tests:

  1. TerminalTabs keyboard handler
    • Add setThemeDropdownOpen to the useEffect dependency array (and any other missing deps).
    • Ensure Escape handling reliably closes theme dropdown or search without stale closures; add unit/integration test exercising Escape with dropdown open and closed.
  2. xterm addon compatibility and runtime guards
    • Validate compatibility between @xterm/xterm and the added addons; pin tested addon versions or adjust @xterm/xterm version accordingly.
    • Add runtime feature guards/fallbacks when loading addon-webgl / addon-ligatures / addon-image so missing or incompatible addon APIs do not crash the app; log graceful warnings and fallbacks.
    • Add a CI check or lightweight test that the terminal initializes with WebGL addon present and falls back to canvas when WebGL fails.
  3. PTY lifecycle verification
    • Confirm src-tauri/src/commands/pty.rs uses HashMap::entry() to avoid TOCTOU and that kill_pty explicitly kills the child process with error handling for different platforms; if not fully implemented, call child.kill() and await/handle potential errors.
    • Add an automated or manual test to ensure no orphaned child processes are left after rapid mount/unmount or React StrictMode double-mount scenarios.
  4. ResizeObserver & lifecycle cleanup verification
    • Ensure ResizeObserver is stored in a ref/closure and disconnect() is called on cleanup (verify current cleanup path).
    • Ensure initializedRef and all terminal-related refs are reset on unmount; add tests for repeated mount/unmount cycles under React StrictMode.
  5. Tests & manual verification steps
    • Add unit/integration tests or CI scripts for:
      • Keyboard Escape behavior with theme dropdown/search.
      • WebGL load with canvas fallback.
      • Rapid mount/unmount under React StrictMode (no orphaned PTY children, single spawn).
      • Cross-platform PTY kill behavior.
    • Provide concise manual verification steps for reviewers to reproduce the above.

Apply changes with minimal API surface disruption and include the brief tests/manual steps above.

- Add WebGL renderer addon for GPU-accelerated rendering
- Add ligatures addon for programming font support (Fira Code, JetBrains Mono)
- Add image addon for sixel/iTerm2 inline image support
- Add 11 terminal theme presets (VS Code, Dracula, Nord, Monokai, etc.)
- Add theme picker UI in terminal toolbar
- Add performance optimizations (fast scroll, optimized settings)
- Fix React StrictMode double-mount issue in PTY spawning
- Persist theme preference to localStorage
@coderabbitai

coderabbitai Bot commented Dec 27, 2025

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds terminal theming and a theme picker, persists theme in the editor store, refactors terminal init to load WebGL/Ligatures/Image addons and use ResizeObserver, wires PTY I/O to the terminal, deduplicates PTY spawn and ensures kill-on-remove, and adds three xterm addon dependencies.

Changes

Cohort / File(s) Summary
Theme Infrastructure
src/lib/terminalThemes.ts, src/stores/editorStore.ts
New terminalThemes module (themes, themeDisplayNames, getTheme(), themeNames). EditorState now includes terminalTheme: string and setTerminalTheme(theme: string) persisted to storage; minor type/quote normalization.
Terminal UI & Theme Selection
src/components/Terminal/TerminalTabs.tsx
Adds theme picker button/dropdown, outside-click and Escape handling, keyboard shortcuts preserved, and wires selection to useEditorStore (terminalTheme / setTerminalTheme).
Terminal Hook Refactor & Addons
src/hooks/useTerminal.ts
Reworks init with an initialized ref, subscribes to store theme, loads WebGL, Ligatures, Image, Fit, WebLinks, Search addons, uses ResizeObserver for fit/size sync, centralizes PTY I/O wiring, consolidates cleanup, and reduces public return surface to { initTerminal, focus, search, clearSearch }.
PTY Backend Changes
src-tauri/src/commands/pty.rs
spawn_shell uses HashMap entry API to check-and-insert PTY instances (prevents duplicates); kill_pty removes entry and explicitly kills the child process when present.
Dependencies
package.json
Added @xterm/addon-image ^0.8.0, @xterm/addon-ligatures ^0.9.0, and @xterm/addon-webgl ^0.18.0.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant TerminalTabs as TerminalTabs (UI)
    participant EditorStore as EditorStore
    participant useTerminal as useTerminal Hook
    participant XTerm as XTerm Instance

    rect rgb(240,248,255)
    Note over User,XTerm: Theme selection flow
    User->>TerminalTabs: Open dropdown & select theme
    TerminalTabs->>EditorStore: setTerminalTheme(name)
    EditorStore->>useTerminal: notify subscription (theme changed)
    useTerminal->>XTerm: terminal.setOption(theme: ITheme)
    XTerm->>XTerm: apply theme options
    end
Loading
sequenceDiagram
    participant React as Component
    participant useTerminal as useTerminal Hook
    participant EditorStore as EditorStore
    participant PTY as PTY Backend
    participant XTerm as XTerm Instance

    rect rgb(240,248,255)
    Note over React,XTerm: Terminal init, addons, PTY wiring
    React->>useTerminal: initTerminal(containerId)
    useTerminal->>EditorStore: read terminalTheme
    useTerminal->>XTerm: create instance with theme/options
    useTerminal->>XTerm: load addons (WebGL, Ligatures, Image, Fit, WebLinks, Search)
    useTerminal->>useTerminal: attach ResizeObserver → fit + sync size
    useTerminal->>PTY: spawn_shell (entry check)
    PTY-->>useTerminal: stream events
    useTerminal->>XTerm: on PTY data → terminal.write()
    XTerm->>PTY: on user input → PTY.write()
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🐇
I hopped to add colors and speed,
With WebGL, ligatures, and image feed.
Themes now dance at the user’s call,
PTYs spawn once — no duplicates at all.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately and concisely summarizes the main enhancements: WebGL rendering, ligatures, images, and themes. It directly reflects the primary changes across the codebase.
✨ Finishing touches
  • 📝 Docstrings were successfully generated.
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/terminal-enhancements

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cae9cc9 and 59b9840.

📒 Files selected for processing (1)
  • src-tauri/src/commands/pty.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src-tauri/src/commands/pty.rs

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

@coderabbitai coderabbitai Bot 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: 5

Caution

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

⚠️ Outside diff range comments (1)
src-tauri/src/commands/pty.rs (1)

154-162: Explicitly terminate the child process before removing the PTY instance.

kill_pty removes the instance from the HashMap but does not explicitly call child.kill() or child.wait(). The portable_pty crate does not guarantee process termination on Drop—you must explicitly call these methods to reap or terminate the spawned shell process. Without this, child processes become orphaned and accumulate over time.

Implement a custom Drop for PtyInstance that calls child.kill(), or modify kill_pty to explicitly terminate the child before removal:

pub fn kill_pty(id: String) -> Result<(), String> {
    let mut instances = PTY_INSTANCES.lock().unwrap();
    
    if let Some(mut instance) = instances.remove(&id) {
        // Explicitly kill the child process
        let _ = instance.child.kill();
        Ok(())
    } else {
        Err(format!("PTY instance not found: {}", id))
    }
}
🧹 Nitpick comments (3)
src/lib/terminalThemes.ts (1)

281-298: LGTM! Well-structured theme system.

The theme infrastructure is clean:

  • getTheme provides a safe fallback for invalid theme names
  • themeDisplayNames provides user-friendly labels for the UI
  • themeNames enables iteration over available themes

For stronger type safety, you could derive the theme name type from the themes object to ensure themeDisplayNames stays in sync:

🔎 Optional type-safe enhancement
// At the top of the file, after themes definition:
export type ThemeName = keyof typeof themes;

export const themeDisplayNames: Record<ThemeName, string> = {
  // ... existing entries
};

export const getTheme = (name: string): ITheme =>
  themes[name as ThemeName] ?? themes['vscode-dark'];

export const themeNames = Object.keys(themes) as ThemeName[];
src/components/Terminal/TerminalTabs.tsx (1)

119-167: Theme picker UI looks good; consider accessibility improvements.

The dropdown implementation is solid with proper click-outside handling and keyboard support. For improved accessibility, consider adding ARIA attributes.

🔎 Optional accessibility enhancements
 <button
   ref={themeButtonRef}
   className={`h-full px-3 transition-colors ${...}`}
   onClick={() => setThemeDropdownOpen(!themeDropdownOpen)}
   title="Terminal Theme"
+  aria-haspopup="listbox"
+  aria-expanded={themeDropdownOpen}
 >
 <div
   ref={themeDropdownRef}
   className="absolute right-0 bottom-full mb-1 w-48 ..."
+  role="listbox"
+  aria-label="Terminal themes"
 >
   {themeNames.map((name) => (
     <button
       key={name}
       className={`w-full px-3 py-1.5 text-left text-xs ...`}
       onClick={() => {...}}
+      role="option"
+      aria-selected={terminalTheme === name}
     >
src-tauri/src/commands/pty.rs (1)

86-110: Consider coordination between reader thread cleanup and kill_pty.

The reader thread cleans up PTY_INSTANCES on exit (line 109), and kill_pty also removes entries (line 157). If kill_pty is called while the reader thread is active, there's no explicit coordination:

  1. kill_pty removes the entry
  2. Reader thread continues reading until it encounters an error
  3. Reader thread attempts to remove an already-removed entry (which is safe, but redundant)

This is not a bug—HashMap::remove is idempotent and the read loop will eventually terminate. However, for cleaner shutdown, you might consider signaling the reader thread to exit when kill_pty is called.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 29492db and df682f2.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • package.json
  • src-tauri/src/commands/pty.rs
  • src/components/Terminal/TerminalTabs.tsx
  • src/hooks/useTerminal.ts
  • src/lib/terminalThemes.ts
  • src/stores/editorStore.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/Terminal/TerminalTabs.tsx (1)
src/lib/terminalThemes.ts (2)
  • themeNames (298-298)
  • themeDisplayNames (281-293)
src/stores/editorStore.ts (2)
src/types/index.ts (5)
  • RemoteConnection (17-25)
  • FileEntry (2-8)
  • GitStatus (87-92)
  • GitBranches (107-111)
  • GitCommit (94-99)
src/components/Git/GitBranches.tsx (1)
  • GitBranches (35-89)
🔇 Additional comments (5)
src/stores/editorStore.ts (1)

31-31: LGTM!

The terminal theme state management is well-structured:

  • State field properly typed as string
  • Setter action is straightforward
  • Persistence via partialize ensures theme preference survives page reloads
  • Default "vscode-dark" aligns with the fallback in getTheme()

Also applies to: 86-86, 136-136, 416-416, 693-693

src/hooks/useTerminal.ts (3)

38-51: LGTM on terminal configuration.

The configuration is well-thought-out:

  • Font stack with ligature-capable fonts
  • allowProposedApi: true enables ligatures addon
  • Performance options (fastScrollModifier, smoothScrollDuration: 0) align with PR goals

67-76: Good defensive handling for WebGL fallback.

The try/catch with context loss handler ensures graceful degradation to canvas rendering when WebGL isn't available or fails.


145-153: Theme subscription correctly integrated.

The store subscription pattern is correct, and adding the unsubscribe function to unlistenersRef ensures proper cleanup.

src-tauri/src/commands/pty.rs (1)

9-18: LGTM!

The global state management using Arc<Mutex<HashMap>> is the standard Rust pattern for thread-safe shared state. The PtyInstance struct correctly retains the child process handle for lifetime management, even though it's not directly accessed (hence #[allow(dead_code)]). This ensures the child process isn't prematurely dropped.

Comment thread package.json Outdated
Comment thread src-tauri/src/commands/pty.rs Outdated
Comment thread src/components/Terminal/TerminalTabs.tsx Outdated
Comment thread src/hooks/useTerminal.ts
Comment thread src/hooks/useTerminal.ts
- Fix ResizeObserver memory leak by storing in ref and disconnecting on cleanup
- Add setThemeDropdownOpen to useEffect dependency array
- Downgrade xterm addon versions for compatibility with xterm 5.5.0:
  - addon-webgl: 0.19.0 → 0.18.0
  - addon-ligatures: 0.10.0 → 0.9.0
  - addon-image: 0.9.0 → 0.8.0
- Fix Escape key handler to only close one element at a time
- Reset initializedRef on unmount to allow proper re-initialization
- Clean up all refs properly in cleanup effect
@gl11tchy

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Dec 27, 2025

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gl11tchy

Copy link
Copy Markdown
Owner Author

@coderabbitai - have all reported bugs been addressed? is it safe to merge?

Replace separate check-then-insert with HashMap::entry() API for
atomic check-and-insert. Prevents potential resource leaks if two
threads call spawn_shell with the same id concurrently.
@coderabbitai

coderabbitai Bot commented Dec 28, 2025

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Get PR details including status checks
echo "=== PR Details ==="
gh pr view 1 --json title,state,isDraft,mergeable,reviewDecision,statusCheckRollup

echo -e "\n=== Review Comments ==="
# Get all review comments
gh pr view 1 --json comments --jq '.comments[] | "[\(.author.login)] \(.body)\n---"'

echo -e "\n=== Review Threads ==="
# Get review threads to see if any are unresolved
gh api repos/gl11tchy/levaia/pulls/1/reviews --jq '.[] | "Review by \(.user.login): \(.state) - \(.body // "No comment")"'

echo -e "\n=== Check Runs ==="
# Get status of CI checks
gh pr checks 1

Length of output: 24265


@gl11tchy - Based on the review, there's one critical issue that should be addressed before merging:

🚨 Outstanding Issue

Child Process Termination in src-tauri/src/commands/pty.rs:

The kill_pty function (lines 154-162) removes the PTY instance from the HashMap but doesn't explicitly call child.kill() to terminate the spawned shell process. The portable_pty crate doesn't guarantee process termination on Drop, which means child processes could become orphaned and accumulate over time, leading to resource leaks.

Recommended fix:

pub fn kill_pty(id: String) -> Result<(), String> {
    let mut instances = PTY_INSTANCES.lock().unwrap();
    
    if let Some(mut instance) = instances.remove(&id) {
        // Explicitly kill the child process
        let _ = instance.child.kill();
        Ok(())
    } else {
        Err(format!("PTY instance not found: {}", id))
    }
}

✅ What's Good

  • Theme system implementation is solid
  • WebGL rendering with proper fallback handling
  • Terminal configuration looks good
  • PTY deduplication logic works correctly

📝 Optional Improvements (Nitpicks)

There are 3 optional suggestions for improvements (type safety, accessibility, thread coordination), but these aren't blocking issues.

Recommendation

Not safe to merge yet - I'd recommend fixing the child process termination issue first to prevent potential resource leaks in production. Once that's addressed, the PR should be good to go! 🚀

The portable_pty crate doesn't guarantee process termination on Drop,
so explicitly call child.kill() when removing PTY instances to prevent
orphaned shell processes from accumulating.

@coderabbitai coderabbitai Bot 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

🧹 Nitpick comments (5)
src-tauri/src/commands/pty.rs (1)

22-23: Consider moving import to module scope.

The Entry import inside the function works but is unconventional. Moving it to the top-level use statement improves discoverability.

🔎 Proposed change
 use portable_pty::{native_pty_system, CommandBuilder, PtySize};
-use std::collections::HashMap;
+use std::collections::{hash_map::Entry, HashMap};
 use std::io::{Read, Write};

Then remove line 22:

 pub fn spawn_shell(app: AppHandle, id: String) -> Result<(), String> {
-    use std::collections::hash_map::Entry;
-
     let pty_system = native_pty_system();
src/components/Terminal/TerminalTabs.tsx (3)

51-56: Remove stable setter from dependency array.

setThemeDropdownOpen is a React useState setter, which is stable across renders. Including it in the dependency array is unnecessary (though harmless). Same applies to toggleTerminalSearch if it's from a stable store action.

🔎 Proposed change
   }, [
     terminalSearchVisible,
     toggleTerminalSearch,
     themeDropdownOpen,
-    setThemeDropdownOpen,
   ]);

138-148: Theme icon may not be intuitive.

The current icon resembles a clock. A palette or paint-brush icon would better convey "theme" functionality. This is a minor UX suggestion.

🔎 Alternative palette icon
             <svg
               className="w-4 h-4"
               viewBox="0 0 16 16"
               fill="none"
               stroke="currentColor"
               strokeWidth="1.5"
             >
-              <circle cx="8" cy="8" r="5" />
-              <path d="M8 3v5l3 3" />
+              <circle cx="8" cy="8" r="6" />
+              <circle cx="6" cy="6" r="1.5" fill="currentColor" />
+              <circle cx="10" cy="6" r="1.5" fill="currentColor" />
+              <circle cx="6" cy="10" r="1.5" fill="currentColor" />
             </svg>

150-173: Dropdown lacks keyboard navigation.

The theme dropdown only supports mouse selection. For accessibility, consider adding:

  • role="listbox" on the dropdown
  • Arrow key navigation between options
  • Enter/Space to select the focused option
  • aria-activedescendant for screen readers

This is optional for an internal tool but recommended for accessibility compliance.

src/hooks/useTerminal.ts (1)

133-143: Consider debouncing resize callbacks.

The ResizeObserver callback triggers both fitAddon.fit() and an IPC call on every resize event. During window resizing, this can fire many times per second, potentially flooding the backend with resize commands.

Consider debouncing to reduce IPC overhead.

🔎 Proposed debounced resize
+      let resizeTimeout: ReturnType<typeof setTimeout> | null = null;
       const resizeObserver = new ResizeObserver(() => {
-        fitAddon.fit();
-        const { rows, cols } = terminal;
-        if (isRemote && remote) {
-          invoke("ssh_resize_shell", { ptyId: id, rows, cols }).catch(
-            console.error,
-          );
-        } else {
-          invoke("resize_pty", { id, rows, cols }).catch(console.error);
-        }
+        if (resizeTimeout) clearTimeout(resizeTimeout);
+        resizeTimeout = setTimeout(() => {
+          fitAddon.fit();
+          const { rows, cols } = terminal;
+          if (isRemote && remote) {
+            invoke("ssh_resize_shell", { ptyId: id, rows, cols }).catch(
+              console.error,
+            );
+          } else {
+            invoke("resize_pty", { id, rows, cols }).catch(console.error);
+          }
+        }, 100);
       });
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df682f2 and cae9cc9.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • package.json
  • src-tauri/src/commands/pty.rs
  • src/components/Terminal/TerminalTabs.tsx
  • src/hooks/useTerminal.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json
🧰 Additional context used
🧬 Code graph analysis (1)
src/hooks/useTerminal.ts (3)
src/stores/editorStore.ts (1)
  • useEditorStore (120-703)
src/lib/terminalThemes.ts (1)
  • getTheme (295-296)
src/components/Terminal/Terminal.tsx (1)
  • Terminal (11-45)
🔇 Additional comments (6)
src-tauri/src/commands/pty.rs (1)

66-83: TOCTOU race condition properly fixed with atomic Entry API.

The fix correctly uses HashMap::entry() for atomic check-and-insert, addressing the previously flagged race condition. If a PTY already exists for the given id, the newly allocated resources (pair, child, writer, reader) are dropped automatically when returning early.

One minor observation: resources are allocated before the atomic check, so in the double-mount scenario, a PTY is briefly opened and immediately closed. This is acceptable given React StrictMode double-mounts are infrequent and the simplicity gained.

src/components/Terminal/TerminalTabs.tsx (1)

39-46: Escape key handling properly fixed.

The if/else-if pattern correctly ensures only one element closes at a time, addressing the previously flagged issue.

src/hooks/useTerminal.ts (4)

172-199: Comprehensive cleanup addresses all previously flagged issues.

The cleanup effect now properly:

  • Disconnects ResizeObserver and nulls the ref
  • Calls all unlisteners (including theme subscription)
  • Kills the PTY
  • Disposes the terminal and clears refs
  • Resets initializedRef to allow remount re-initialization

68-77: WebGL addon with fallback handling is well implemented.

The try/catch properly handles environments where WebGL isn't supported, and the onContextLoss handler disposes the addon gracefully. xterm's canvas renderer activates automatically as fallback.


147-155: Theme subscription correctly wired with cleanup.

The subscription uses a local variable to track the current theme, avoiding redundant updates. The unsubscribe function is properly pushed to unlistenersRef for cleanup.


164-170: Search functions are correctly implemented.

Comment thread src/hooks/useTerminal.ts
Comment on lines +29 to +34
const initTerminal = useCallback(
async (container: HTMLDivElement) => {
// Prevent double initialization (React StrictMode)
if (initializedRef.current) return;
initializedRef.current = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Async initialization may race with cleanup on rapid mount/unmount.

If the component unmounts while initTerminal is still executing (e.g., between await listen() and await invoke("spawn_shell")), the cleanup effect runs but the async function continues. This could result in:

  • Writing to a disposed terminal
  • Pushing to a cleared unlistenersRef
  • Setting refs that were nulled

Consider adding an abort signal or mounted flag to bail out of the async function if cleanup has started.

🔎 Proposed fix using AbortController or mounted flag
 export function useTerminal({ id, onExit, remote }: UseTerminalOptions) {
   const terminalRef = useRef<Terminal | null>(null);
   const fitAddonRef = useRef<FitAddon | null>(null);
   const searchAddonRef = useRef<SearchAddon | null>(null);
   const resizeObserverRef = useRef<ResizeObserver | null>(null);
   const unlistenersRef = useRef<UnlistenFn[]>([]);
   const initializedRef = useRef(false);
+  const mountedRef = useRef(true);
   const isRemote = !!remote;

   const initTerminal = useCallback(
     async (container: HTMLDivElement) => {
       // Prevent double initialization (React StrictMode)
       if (initializedRef.current) return;
       initializedRef.current = true;

       // ... terminal setup ...

+      // Check if still mounted before proceeding with async operations
+      if (!mountedRef.current) return;
+
       // Listen for PTY output
       const dataUnlisten = await listen<string>(`pty-data-${id}`, (event) => {
         terminal.write(event.payload);
       });

+      if (!mountedRef.current) {
+        dataUnlisten();
+        return;
+      }
+
       // ... rest of setup ...
     },
     [id, onExit, isRemote, remote],
   );

   // Cleanup
   useEffect(() => {
+    mountedRef.current = true;
     return () => {
+      mountedRef.current = false;
       // ... existing cleanup ...
     };
   }, [id, isRemote, remote]);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/hooks/useTerminal.ts around lines 29-34, initTerminal is async and may
continue after the component unmounts, causing writes to a disposed terminal and
pushes to cleared refs; fix by adding an abort signal or mounted flag checked
after each await (or pass an AbortController.signal into async operations like
listen/invoke), bail out early if aborted/unmounted, and avoid mutating refs or
terminal instance when aborted; ensure cleanup sets the abort flag/signal before
awaiting pending work and that initTerminal checks it (and returns) before
performing side effects such as writing to the terminal or pushing into
unlistenersRef.

@gl11tchy
gl11tchy merged commit 80bbdb4 into main Dec 28, 2025
1 check passed
@gl11tchy
gl11tchy deleted the feat/terminal-enhancements branch December 28, 2025 02:18
@coderabbitai

coderabbitai Bot commented Dec 28, 2025

Copy link
Copy Markdown

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #2

coderabbitai Bot added a commit that referenced this pull request Dec 28, 2025
Docstrings generation was requested by @gl11tchy.

* #1 (comment)

The following files were modified:

* `src-tauri/src/commands/pty.rs`
* `src/components/Terminal/TerminalTabs.tsx`
* `src/hooks/useTerminal.ts`
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