Skip to content

Conversation

@mateuszruszkowski
Copy link

@mateuszruszkowski mateuszruszkowski commented Jan 17, 2026

Base Branch

  • This PR targets the develop branch (required for all feature/fix PRs)
  • This PR targets main (hotfix only - maintainers)

Description

This PR fixes two Windows-specific issues that significantly improve the developer experience:

  1. Full system PATH for Bash sessions: Windows Bash sessions now have access to all installed system tools (ddev, mkcert, chocolatey, etc.) by passing the complete Windows PATH to the SDK.

  2. DPI-aware terminal font scaling: Terminal text now scales correctly on high-DPI displays (125%, 150%, 200% scaling) and dynamically adjusts when moving windows between monitors with different DPI settings.

Related Issue

Fixes Windows users not being able to run installed system tools from Bash sessions and terminal text rendering issues on high-DPI displays.

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • 📚 Documentation
  • ♻️ Refactor
  • 🧪 Test

Area

  • Frontend
  • Backend
  • Fullstack

Commit Message Format

Follow conventional commits: <type>: <subject>

Types: feat, fix, docs, style, refactor, test, chore

Example: feat: add user authentication system

Checklist

  • I've synced with develop branch
  • I've tested my changes locally
  • I've followed the code principles (SOLID, DRY, KISS)
  • My PR is small and focused (< 400 lines ideally)

Platform Testing Checklist

CRITICAL: This project supports Windows, macOS, and Linux. Platform-specific bugs are a common source of breakage.

  • Windows tested (tested on Windows 11)
  • macOS tested (CI will verify)
  • Linux tested (CI will verify)
  • Used centralized platform/ module instead of direct process.platform checks
  • No hardcoded paths (used platform abstractions)

Note: Changes are Windows-specific and use the centralized platform module. CI will verify no regressions on macOS/Linux.

CI/Testing Requirements

  • All CI checks pass on all platforms (Windows, macOS, Linux) - pending CI
  • All existing tests pass locally
  • New features include test coverage - bug fixes, no new tests needed
  • Bug fixes include regression tests - manual testing performed

Screenshots

N/A - No UI changes, backend/terminal rendering improvements only.

Feature Toggle

  • Behind localStorage flag: use_feature_name
  • Behind settings toggle
  • Behind environment variable/config
  • N/A - Feature is complete and ready for all users

Breaking Changes

Breaking: No

Details: Backward compatible improvements. No API changes.

Summary by CodeRabbit

  • New Features

    • Terminal font size now dynamically adjusts to device DPI and display scaling.
  • Improvements

    • Expanded default tool-discovery paths on Windows for better detection of common development tools.
    • Improved Windows PATH handling so terminal and shell sessions have fuller access to system PATH entries.

✏️ Tip: You can customize this high-level summary in your review settings.

Windows Bash sessions were missing many system tools (ddev, mkcert, etc.)
because they only inherited a limited COMMON_BIN_PATHS list. This adds:

Backend (core/auth.py):
- Pass full Windows PATH to SDK via get_sdk_env_vars()
- Ensures Bash has access to all installed Windows tools

Backend (core/platform):
- Add get_windows_system_path() helper function

Frontend (env-utils.ts):
- Extend COMMON_BIN_PATHS for Windows with ddev and system paths
- Merge with upstream Node.js paths

Fixes issue where Windows tools were unavailable in Bash despite being
in system PATH.
Terminal text appeared blurry or too small on Windows with display scaling
(125%, 150%, 200%) because fontSize was fixed at 13px regardless of DPI.

Changes:
- Calculate fontSize based on window.devicePixelRatio
- Use Math.sqrt() scaling for smoother adjustment
- Minimum fontSize of 11px to maintain readability
- Listen for DPI changes (monitor switching, scaling changes)
- Dynamically update fontSize and re-fit terminal on DPI change

This fixes terminal text rendering on high-DPI Windows displays and when
moving windows between monitors with different scaling settings.
@CLAassistant
Copy link

CLAassistant commented Jan 17, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ AndyMik90
❌ mateuszruszkowski
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 17, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds Windows-specific PATH exposure in the backend, expands Windows candidate PATH entries in the frontend, and makes the terminal DPI-aware by dynamically adjusting font size and refitting on scale changes.

Changes

Cohort / File(s) Summary
Windows PATH Exposure
apps/backend/core/auth.py, apps/backend/core/platform/__init__.py
Adds get_windows_system_path() and uses it in SDK env var assembly on Windows to replace the session PATH with the system PATH when available; preserves existing CLAUDE_CODE_GIT_BASH_PATH auto-detection.
Windows Binary Path Discovery
apps/frontend/src/main/env-utils.ts
Expands COMMON_BIN_PATHS for Windows with additional user-local, Git, DDEV, Node.js (x64/x86), NVM/NPM, Scoop, Chocolatey, and system directories to increase discovery of installed tools.
Terminal DPI-Aware Rendering
apps/frontend/src/renderer/components/terminal/useXterm.ts
Replaces fixed fontSize with DPI-scaled calculation (base 13, scales via devicePixelRatio, min 11), applies to xterm options, and adds debounced matchMedia listeners to update font size and re-fit terminal on scale changes.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

bug, area/fullstack, 🔄 Checking, size/M

Suggested reviewers

  • AndyMik90
  • marcefalcondev

Poem

🐰 I nibble PATHs both near and far,

System bins now shine like a star.
DPI-wise I tweak each tiny font,
Terminals fit—no UI affront.
Hop, hop, bash and node, hooray! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 accurately summarizes the two main changes: Windows PATH fixes for Bash sessions and DPI-aware terminal font scaling.

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

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @mateuszruszkowski, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the developer experience for Windows users by resolving two critical issues. It ensures that Windows Bash sessions have complete access to system-wide tools by correctly propagating the full system PATH, and it introduces dynamic DPI-aware font scaling for the terminal, providing a consistent and readable interface across various display configurations.

Highlights

  • Windows PATH Resolution: Bash sessions on Windows now correctly inherit the full system PATH, allowing access to all installed system tools like ddev, mkcert, and Chocolatey, which were previously unavailable.
  • Dynamic Terminal DPI Scaling: The terminal font scaling has been improved for high-DPI displays (e.g., 125%, 150%, 200% scaling) and now dynamically adjusts when the application window is moved between monitors with different DPI settings.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@mateuszruszkowski
Copy link
Author

I have read the CLA Document and I hereby sign the CLA

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

🎉 Thanks for your first PR!

A maintainer will review it soon. Please make sure:

  • Your branch is synced with develop
  • CI checks pass
  • You've followed our contribution guide

Welcome to the Auto Claude community!

@sentry
Copy link

sentry bot commented Jan 17, 2026

Codecov Report

❌ Patch coverage is 90.00000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
apps/backend/core/platform/__init__.py 80.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment on lines +48 to +52
// Calculate optimal font size based on device pixel ratio (Windows DPI scaling)
// devicePixelRatio > 1 indicates high-DPI displays (125%, 150%, 200% scaling)
// Base size: 13px for 100% scaling (devicePixelRatio = 1)
const baseFontSize = 13;
const scaleFactor = window.devicePixelRatio || 1;

This comment was marked as outdated.


// Handle DPI/scale changes (Windows display scaling, moving between monitors)
useEffect(() => {
const handleDPIChange = debounce(() => {

This comment was marked as outdated.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces two valuable improvements for Windows users: providing the full system PATH to Bash sessions and implementing DPI-aware font scaling in the terminal. These changes will significantly enhance the developer experience.

My review focuses on refining the implementation. I've suggested reusing an existing helper function in the Python backend to simplify the PATH retrieval logic. For the frontend, I've identified a bug in the DPI change handler that prevents it from detecting subsequent DPI changes and proposed a fix. I've also pointed out some opportunities to improve code clarity and reduce duplication in the font scaling calculations. Overall, great work on tackling these platform-specific issues.

Comment on lines +334 to +372
useEffect(() => {
const handleDPIChange = debounce(() => {
if (xtermRef.current) {
const baseFontSize = 13;
const scaleFactor = window.devicePixelRatio || 1;
const adjustedFontSize = scaleFactor > 1
? Math.max(baseFontSize * (1 / Math.sqrt(scaleFactor)), 11)
: baseFontSize;

// Update font size dynamically
xtermRef.current.options.fontSize = Math.round(adjustedFontSize);

// Trigger a fit to recalculate terminal dimensions with new font size
if (fitAddonRef.current && terminalRef.current) {
const rect = terminalRef.current.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
fitAddonRef.current.fit();
}
}
}
}, 200); // Debounce DPI changes to avoid excessive recalculations

// Listen for DPI/scale changes via matchMedia
// This fires when user moves window between monitors with different DPI
// or changes Windows display scaling settings
const mediaQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);

// Modern API: addEventListener
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handleDPIChange);
return () => mediaQuery.removeEventListener('change', handleDPIChange);
}

// Fallback for older browsers: addListener (deprecated)
if (mediaQuery.addListener) {
mediaQuery.addListener(handleDPIChange);
return () => mediaQuery.removeListener?.(handleDPIChange);
}
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

high

This DPI change handler has a bug: it only detects the first DPI change. The mediaQuery is created once with the initial devicePixelRatio and is not updated after a change, so subsequent DPI changes are missed.

To fix this, the useEffect hook needs to re-run to create a new MediaQueryList with the updated devicePixelRatio. You can achieve this by introducing a state variable to track the pixel ratio.

  1. Add state to your component: const [dpr, setDpr] = useState(window.devicePixelRatio || 1);
  2. Update this useEffect to depend on dpr and update it within the handler. Using { once: true } for the event listener simplifies cleanup and re-registration.

Additionally, the font size calculation logic is duplicated from the initialization effect. When refactoring, please extract this logic into a shared helper function to follow the DRY principle.

  // Handle DPI/scale changes (Windows display scaling, moving between monitors)
  useEffect(() => {
    const handleDPIChange = debounce(() => {
      if (xtermRef.current) {
        const newScaleFactor = window.devicePixelRatio || 1;
        const baseFontSize = 13;
        const adjustedFontSize = newScaleFactor > 1
          ? Math.max(baseFontSize * (1 / Math.sqrt(newScaleFactor)), 11)
          : baseFontSize;

        xtermRef.current.options.fontSize = Math.round(adjustedFontSize);

        if (fitAddonRef.current && terminalRef.current) {
          const rect = terminalRef.current.getBoundingClientRect();
          if (rect.width > 0 && rect.height > 0) {
            fitAddonRef.current.fit();
          }
        }
        setDpr(newScaleFactor);
      }
    }, 200);

    const mediaQuery = window.matchMedia(`(resolution: ${dpr}dppx)`);
    mediaQuery.addEventListener('change', handleDPIChange, { once: true });

    return () => {
      mediaQuery.removeEventListener('change', handleDPIChange);
    };
  }, [dpr]);

Comment on lines +533 to +535
# Get PATH from environment - Windows is case-insensitive for env vars
path = os.environ.get("PATH") or os.environ.get("Path") or os.environ.get("path")
return path
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

To improve maintainability and reduce code duplication, this logic for retrieving the PATH environment variable on Windows can be simplified by using the existing get_env_var helper function from this same file, which already handles case-insensitive lookups correctly.

Suggested change
# Get PATH from environment - Windows is case-insensitive for env vars
path = os.environ.get("PATH") or os.environ.get("Path") or os.environ.get("path")
return path
# Use the existing helper for case-insensitive environment variable access.
return get_env_var("PATH")

Comment on lines +53 to +54
// Adjust font size inversely to pixel ratio to maintain visual consistency
// For 150% scaling (1.5x), use 13 * (1 / 1.5) ≈ 8.67 logical pixels
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This comment is misleading as it describes a direct inverse scaling (1 / 1.5), but the implementation on line 57 uses 1 / Math.sqrt(scaleFactor). To prevent confusion for future maintainers, please update the comment to accurately reflect the code's logic, which uses a square root to make the font size reduction less aggressive.

Suggested change
// Adjust font size inversely to pixel ratio to maintain visual consistency
// For 150% scaling (1.5x), use 13 * (1 / 1.5) ≈ 8.67 logical pixels
// Adjust font size using the square root of the scale factor to make the
// font size reduction less aggressive on high-DPI displays.

Copy link
Contributor

@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: 3

🤖 Fix all issues with AI agents
In `@apps/backend/core/platform/__init__.py`:
- Around line 519-535: The get_windows_system_path function duplicates
case-insensitive env lookup; replace the manual
os.environ.get("PATH")/("Path")/("path") logic by calling the existing helper
get_env_var("PATH") so behavior is centralized; update the return to use
get_env_var("PATH") inside get_windows_system_path and keep the early
is_windows() check intact.

In `@apps/frontend/src/main/env-utils.ts`:
- Around line 111-127: COMMON_BIN_PATHS currently contains hardcoded Windows
roots; replace those literals by constructing Windows paths from environment
variables and platform helpers: when process.platform === 'win32', read
ProgramFiles, ProgramFiles(x86) (or fallback to ProgramFiles), ProgramData,
SystemRoot, LOCALAPPDATA, APPDATA and USERPROFILE, use path.join or path.resolve
to build the specific subpaths (e.g. join(ProgramFiles, 'Git', 'cmd'),
join(ProgramFiles, 'nodejs'), join(LOCALAPPDATA, 'Programs', 'nodejs'),
join(APPDATA, 'npm'), join(USERPROFILE, '.local', 'bin'), join(ProgramData,
'chocolatey', 'bin'), etc.), expand '~' against USERPROFILE for POSIX-style
entries, filter out undefined or non-existent env values, and only include these
constructed entries in COMMON_BIN_PATHS so platform-specific roots are not
hardcoded.

In `@apps/frontend/src/renderer/components/terminal/useXterm.ts`:
- Around line 333-372: Guard against window.matchMedia being undefined and add a
resize fallback, then re-register the media query whenever DPI changes: inside
the effect, check if window.matchMedia exists before using it and fall back to
listening to window.resize; modify the handleDPIChange routine (and its
debounce) to, after adjusting xtermRef.current.options.fontSize and calling
fitAddonRef.current.fit() (using terminalRef to check bounds), recreate the
mediaQuery using the latest window.devicePixelRatio and reattach its 'change'
listener so the query isn't stale; ensure you remove the correct listeners on
cleanup (use removeEventListener when added and removeListener fallback for
older APIs) and reference the existing symbols handleDPIChange, xtermRef,
fitAddonRef, terminalRef, and mediaQuery when updating the logic.

Comment on lines +519 to +535
def get_windows_system_path() -> str | None:
"""
Get the full Windows system PATH environment variable.
This retrieves the complete PATH from the Windows environment, including
all user and system paths. Useful for ensuring Bash sessions have access
to all installed Windows tools.
Returns:
Full PATH string on Windows, None on other platforms
"""
if not is_windows():
return None

# Get PATH from environment - Windows is case-insensitive for env vars
path = os.environ.get("PATH") or os.environ.get("Path") or os.environ.get("path")
return path
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Reuse get_env_var("PATH") for consistency.

This duplicates the Windows case-insensitive env lookup logic already in get_env_var. Consider reusing that helper to keep behavior centralized.

♻️ Suggested refactor
-    path = os.environ.get("PATH") or os.environ.get("Path") or os.environ.get("path")
-    return path
+    return get_env_var("PATH")
🤖 Prompt for AI Agents
In `@apps/backend/core/platform/__init__.py` around lines 519 - 535, The
get_windows_system_path function duplicates case-insensitive env lookup; replace
the manual os.environ.get("PATH")/("Path")/("path") logic by calling the
existing helper get_env_var("PATH") so behavior is centralized; update the
return to use get_env_var("PATH") inside get_windows_system_path and keep the
early is_windows() check intact.

Comment on lines +111 to +127
// User-local binaries
'~/.local/bin', // User-local binaries (Claude CLI installed here)
// Common development tools
'C:\\Program Files\\Git\\cmd',
'C:\\Program Files\\Git\\bin',
'C:\\Program Files\\GitHub CLI',
'C:\\Program Files\\ddev', // DDEV local development
// Node.js and npm paths - critical for packaged Electron apps that don't inherit full PATH
'C:\\Program Files\\nodejs', // Standard Node.js installer (64-bit)
'C:\\Program Files (x86)\\nodejs', // 32-bit Node.js on 64-bit Windows
'~\\AppData\\Local\\Programs\\nodejs', // NVM for Windows / user install
'~\\AppData\\Roaming\\npm', // npm global scripts (claude.cmd lives here)
'~\\scoop\\apps\\nodejs\\current', // Scoop package manager
'C:\\ProgramData\\chocolatey\\bin', // Chocolatey package manager
// Common system paths
'C:\\Windows\\System32',
'C:\\Windows',
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid hardcoded Windows roots in COMMON_BIN_PATHS.

These new entries hardcode C:\... roots, which can break on non-default installs/localized Windows and conflicts with the platform-path guideline. Please derive these from environment variables (ProgramFiles, ProgramData, SystemRoot, LOCALAPPDATA, APPDATA, USERPROFILE) and filter missing values. Consider using platform path helpers for joins.

🔧 Suggested refactor pattern
+const windowsBinPaths = (() => {
+  const homeDir = os.homedir();
+  const programFiles = process.env.ProgramFiles;
+  const programFilesX86 = process.env['ProgramFiles(x86)'];
+  const programData = process.env.ProgramData;
+  const systemRoot = process.env.SystemRoot;
+  const localAppData = process.env.LOCALAPPDATA;
+  const appData = process.env.APPDATA;
+
+  return [
+    path.join(homeDir, '.local', 'bin'),
+    ...(programFiles ? [
+      path.join(programFiles, 'Git', 'cmd'),
+      path.join(programFiles, 'Git', 'bin'),
+      path.join(programFiles, 'GitHub CLI'),
+      path.join(programFiles, 'ddev'),
+      path.join(programFiles, 'nodejs'),
+    ] : []),
+    ...(programFilesX86 ? [path.join(programFilesX86, 'nodejs')] : []),
+    ...(localAppData ? [path.join(localAppData, 'Programs', 'nodejs')] : []),
+    ...(appData ? [path.join(appData, 'npm')] : []),
+    path.join(homeDir, 'scoop', 'apps', 'nodejs', 'current'),
+    ...(programData ? [path.join(programData, 'chocolatey', 'bin')] : []),
+    ...(systemRoot ? [path.join(systemRoot, 'System32'), systemRoot] : []),
+  ];
+})();
+
 export const COMMON_BIN_PATHS: Record<string, string[]> = {
   darwin: [
     ...
   ],
   linux: [
     ...
   ],
-  win32: [
-    // User-local binaries
-    '~/.local/bin',
-    // Common development tools
-    'C:\\Program Files\\Git\\cmd',
-    'C:\\Program Files\\Git\\bin',
-    'C:\\Program Files\\GitHub CLI',
-    'C:\\Program Files\\ddev',
-    // Node.js and npm paths - critical for packaged Electron apps that don't inherit full PATH
-    'C:\\Program Files\\nodejs',
-    'C:\\Program Files (x86)\\nodejs',
-    '~\\AppData\\Local\\Programs\\nodejs',
-    '~\\AppData\\Roaming\\npm',
-    '~\\scoop\\apps\\nodejs\\current',
-    'C:\\ProgramData\\chocolatey\\bin',
-    // Common system paths
-    'C:\\Windows\\System32',
-    'C:\\Windows',
-  ],
+  win32: windowsBinPaths,
 };

As per coding guidelines, avoid hardcoded platform-specific paths.

🤖 Prompt for AI Agents
In `@apps/frontend/src/main/env-utils.ts` around lines 111 - 127, COMMON_BIN_PATHS
currently contains hardcoded Windows roots; replace those literals by
constructing Windows paths from environment variables and platform helpers: when
process.platform === 'win32', read ProgramFiles, ProgramFiles(x86) (or fallback
to ProgramFiles), ProgramData, SystemRoot, LOCALAPPDATA, APPDATA and
USERPROFILE, use path.join or path.resolve to build the specific subpaths (e.g.
join(ProgramFiles, 'Git', 'cmd'), join(ProgramFiles, 'nodejs'),
join(LOCALAPPDATA, 'Programs', 'nodejs'), join(APPDATA, 'npm'),
join(USERPROFILE, '.local', 'bin'), join(ProgramData, 'chocolatey', 'bin'),
etc.), expand '~' against USERPROFILE for POSIX-style entries, filter out
undefined or non-existent env values, and only include these constructed entries
in COMMON_BIN_PATHS so platform-specific roots are not hardcoded.

Comment on lines +333 to +372
// Handle DPI/scale changes (Windows display scaling, moving between monitors)
useEffect(() => {
const handleDPIChange = debounce(() => {
if (xtermRef.current) {
const baseFontSize = 13;
const scaleFactor = window.devicePixelRatio || 1;
const adjustedFontSize = scaleFactor > 1
? Math.max(baseFontSize * (1 / Math.sqrt(scaleFactor)), 11)
: baseFontSize;

// Update font size dynamically
xtermRef.current.options.fontSize = Math.round(adjustedFontSize);

// Trigger a fit to recalculate terminal dimensions with new font size
if (fitAddonRef.current && terminalRef.current) {
const rect = terminalRef.current.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
fitAddonRef.current.fit();
}
}
}
}, 200); // Debounce DPI changes to avoid excessive recalculations

// Listen for DPI/scale changes via matchMedia
// This fires when user moves window between monitors with different DPI
// or changes Windows display scaling settings
const mediaQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);

// Modern API: addEventListener
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handleDPIChange);
return () => mediaQuery.removeEventListener('change', handleDPIChange);
}

// Fallback for older browsers: addListener (deprecated)
if (mediaQuery.addListener) {
mediaQuery.addListener(handleDPIChange);
return () => mediaQuery.removeListener?.(handleDPIChange);
}
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let me check the test environment setup and any existing tests for this file
cd apps/frontend && find . -name "*useXterm*" -type f | head -20

Repository: AndyMik90/Auto-Claude

Length of output: 173


🌐 Web query:

JSDOM window.matchMedia undefined test environment

💡 Result:

Cause: JSDOM (used by Jest) does not implement window.matchMedia, so calls fail with "window.matchMedia is not a function". [1][4]

Quick fixes

  • Add a global mock in your Jest setup file (setupFiles / setupTests) so it runs before your tested modules. Example:
    Object.defineProperty(window, 'matchMedia', {
    writable: true,
    value: query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: () => {},
    removeListener: () => {},
    addEventListener: () => {},
    removeEventListener: () => {},
    dispatchEvent: () => {},
    }),
    });
    (Put this in setupTests.ts or a file listed in jest.config.js -> setupFiles or setupFilesAfterEnv). [0][3][5]

  • Use a ready-made polyfill/mock package (e.g. mq-polyfill or @raymondwang/mock-match-media) and import it from your Jest setup so you can simulate media changes and event listeners. [2][1]

Notes

  • If matchMedia is invoked at module import time, be sure the setup file is loaded before those modules (use setupFiles, not just per-test mocks). [4][5]

References
[0] Stack Overflow: Jest test fails: TypeError: window.matchMedia is not a function.
[1] @raymondwang/mock-match-media (npm / libraries.io).
[2] mq-polyfill / community suggestions (example workarounds).
[3] Blog example adding Object.defineProperty in setupTests.
[4] Jest docs: Mocking methods not implemented in JSDOM / Manual mocks.


🌐 Web query:

media query resolution change re-registration browser behavior

💡 Result:

Short answer

  • Browsers update window.devicePixelRatio when the page/zoom/monitor DPI changes (e.g., dragging between displays). You can detect that by using matchMedia and its "change" event. [1][2]

Key behavior and practical pattern

  • If you create a media query that checks an exact resolution (e.g. (resolution: 2dppx)), that MediaQueryList is only true for that value; when the DPR changes the MediaQueryList fires "change" once but you typically must create a new query based on the new devicePixelRatio if you want to keep monitoring future changes. MDN’s example shows re-registering a new matchMedia query each time. [1]
  • In practice use either: (a) a range-based media query that stays relevant across DPRs, or (b) recreate a media-query listener when devicePixelRatio changes (read window.devicePixelRatio inside the handler and re-register). [1][2]

Caveats

  • Implementation details and exact event timing can vary across browsers; testing on target browsers is recommended. [1]

Sources
[1] MDN — Window.devicePixelRatio / matchMedia example.
[2] StackOverflow discussion on devicePixelRatio changing when dragging between monitors.


🏁 Script executed:

# Check if there are any test files or existing documentation about this issue
git log --oneline --all -- "apps/frontend/src/renderer/components/terminal/useXterm.ts" | head -10

Repository: AndyMik90/Auto-Claude

Length of output: 284


🏁 Script executed:

# Search for any CI/test failures related to matchMedia in the codebase
rg "matchMedia" --type ts --type tsx -B 2 -A 2

Repository: AndyMik90/Auto-Claude

Length of output: 92


🏁 Script executed:

# Search for matchMedia usage without the file type filter
rg "matchMedia" -B 2 -A 2

Repository: AndyMik90/Auto-Claude

Length of output: 3678


🏁 Script executed:

# Check the actual test file to see how it's set up
cat -n apps/frontend/src/renderer/components/terminal/__tests__/useXterm.test.ts | head -100

Repository: AndyMik90/Auto-Claude

Length of output: 3563


🏁 Script executed:

# Check the jest config to see if there's a setup file
find . -name "jest.config.*" -o -name "setupTests.*" -o -name "setup.ts" | grep -E "(jest|setup)" | head -10

Repository: AndyMik90/Auto-Claude

Length of output: 103


🏁 Script executed:

# Check the setup.ts file to see if matchMedia is mocked
cat -n apps/frontend/src/__tests__/setup.ts

Repository: AndyMik90/Auto-Claude

Length of output: 4890


🏁 Script executed:

# Let me also check if there are any vitest configs
find . -name "vitest.config.*" -o -name "vite.config.*" | head -10

Repository: AndyMik90/Auto-Claude

Length of output: 129


🏁 Script executed:

# Check the useXterm.ts file more completely to understand the full context
cat -n apps/frontend/src/renderer/components/terminal/useXterm.ts | sed -n '1,50p'

Repository: AndyMik90/Auto-Claude

Length of output: 2431


🏁 Script executed:

# Get lines 333-372 specifically from useXterm.ts
sed -n '333,372p' apps/frontend/src/renderer/components/terminal/useXterm.ts

Repository: AndyMik90/Auto-Claude

Length of output: 1704


🏁 Script executed:

# Check if there's a matchMedia mock anywhere in the codebase
rg "matchMedia.*value.*function|matchMedia.*mock|Object.defineProperty.*matchMedia" -A 5

Repository: AndyMik90/Auto-Claude

Length of output: 47


🏁 Script executed:

# Check the vitest config to see if it uses setup files
cat -n apps/frontend/vitest.config.ts

Repository: AndyMik90/Auto-Claude

Length of output: 1489


🏁 Script executed:

# Look at the test file to see if it tests the DPI change effect
cat -n apps/frontend/src/renderer/components/terminal/__tests__/useXterm.test.ts | grep -A 30 -B 5 "DPI\|matchMedia\|devicePixelRatio"

Repository: AndyMik90/Auto-Claude

Length of output: 47


🏁 Script executed:

# Check the full test file to see what tests exist
cat -n apps/frontend/src/renderer/components/terminal/__tests__/useXterm.test.ts | tail -200

Repository: AndyMik90/Auto-Claude

Length of output: 7081


🏁 Script executed:

# Search for any test that includes "DPI" or the specific effect
cat -n apps/frontend/src/renderer/components/terminal/__tests__/useXterm.test.ts | grep -i "dpi\|scale\|resize\|font" -B 2 -A 5

Repository: AndyMik90/Auto-Claude

Length of output: 736


🏁 Script executed:

# Let me check if there's a mock for window.matchMedia in the setup file
grep -n "matchMedia" apps/frontend/src/__tests__/setup.ts

Repository: AndyMik90/Auto-Claude

Length of output: 47


🏁 Script executed:

# Check if there are any recent commits mentioning matchMedia or DPI issues
git log --oneline --all --grep="matchMedia\|DPI\|display scale" | head -20

Repository: AndyMik90/Auto-Claude

Length of output: 327


🏁 Script executed:

# Look for any error reports or issues in comments
rg "matchMedia.*undefined|is not a function" --type ts --type tsx

Repository: AndyMik90/Auto-Claude

Length of output: 92


🏁 Script executed:

# Check if the test file actually runs the DPI effect
cat -n apps/frontend/src/renderer/components/terminal/__tests__/useXterm.test.ts | head -200

Repository: AndyMik90/Auto-Claude

Length of output: 7262


🏁 Script executed:

# Let me verify the actual behavior by checking the full useXterm.ts including dependencies
wc -l apps/frontend/src/renderer/components/terminal/useXterm.ts

Repository: AndyMik90/Auto-Claude

Length of output: 127


🏁 Script executed:

# Check other files using matchMedia to see how they handle it
rg "window.matchMedia" -B 3 -A 3 --type ts --type tsx

Repository: AndyMik90/Auto-Claude

Length of output: 92


🏁 Script executed:

# Search for matchMedia usage in all files
rg "window.matchMedia" -B 3 -A 3

Repository: AndyMik90/Auto-Claude

Length of output: 4558


🏁 Script executed:

# Check how other components handle matchMedia - look for guards or setup
rg "matchMedia" apps/frontend/src -B 2 -A 2

Repository: AndyMik90/Auto-Claude

Length of output: 3678


🏁 Script executed:

# Look at the recent DPI commit to understand what was added
git show 18485c3 --stat

Repository: AndyMik90/Auto-Claude

Length of output: 50377


🏁 Script executed:

# Let me see if there's actual test failure evidence or if this is a hypothetical issue
git log --oneline -20 --all -- "apps/frontend/src/renderer/components/terminal/__tests__/useXterm.test.ts"

Repository: AndyMik90/Auto-Claude

Length of output: 174


🏁 Script executed:

# Check if there's a vitest environment-specific mock needed
cat apps/frontend/vitest.config.ts | grep -A 5 -B 5 "environment"

Repository: AndyMik90/Auto-Claude

Length of output: 835


🏁 Script executed:

# Let me verify if the test file actually runs and what errors it produces
cat -n apps/frontend/src/renderer/components/terminal/__tests__/useXterm.test.ts | grep -A 50 "describe.*useXterm"

Repository: AndyMik90/Auto-Claude

Length of output: 2018


Guard window.matchMedia and re-register the query to keep DPI updates working.

window.matchMedia is undefined in test/JSDOM environments (causing test failures), and resolution-based media queries only fire once because they check an exact devicePixelRatio value. When DPI changes, the query becomes stale and won't fire again. Add a guard with a resize fallback and re-register the media query after each DPI change with the updated devicePixelRatio.

🐛 Suggested fix
   useEffect(() => {
     const handleDPIChange = debounce(() => {
       if (xtermRef.current) {
         const baseFontSize = 13;
         const scaleFactor = window.devicePixelRatio || 1;
         const adjustedFontSize = scaleFactor > 1
           ? Math.max(baseFontSize * (1 / Math.sqrt(scaleFactor)), 11)
           : baseFontSize;

         // Update font size dynamically
         xtermRef.current.options.fontSize = Math.round(adjustedFontSize);

         // Trigger a fit to recalculate terminal dimensions with new font size
         if (fitAddonRef.current && terminalRef.current) {
           const rect = terminalRef.current.getBoundingClientRect();
           if (rect.width > 0 && rect.height > 0) {
             fitAddonRef.current.fit();
           }
         }
       }
     }, 200); // Debounce DPI changes to avoid excessive recalculations

+    if (typeof window.matchMedia !== 'function') {
+      window.addEventListener('resize', handleDPIChange);
+      return () => window.removeEventListener('resize', handleDPIChange);
+    }
+
+    let mediaQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
+    const onChange = () => {
+      handleDPIChange();
+      // Re-register to keep tracking subsequent DPI changes
+      mediaQuery.removeEventListener?.('change', onChange);
+      mediaQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
+      mediaQuery.addEventListener?.('change', onChange);
+    };

-    const mediaQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
     // Modern API: addEventListener
     if (mediaQuery.addEventListener) {
-      mediaQuery.addEventListener('change', handleDPIChange);
-      return () => mediaQuery.removeEventListener('change', handleDPIChange);
+      mediaQuery.addEventListener('change', onChange);
+      return () => mediaQuery.removeEventListener('change', onChange);
     }

     // Fallback for older browsers: addListener (deprecated)
     if (mediaQuery.addListener) {
-      mediaQuery.addListener(handleDPIChange);
-      return () => mediaQuery.removeListener?.(handleDPIChange);
+      mediaQuery.addListener(onChange);
+      return () => mediaQuery.removeListener?.(onChange);
     }
   }, []);
🧰 Tools
🪛 GitHub Check: test-frontend (macos-latest)

[failure] 359-359: src/renderer/components/terminal/tests/useXterm.test.ts > useXterm keyboard handlers > Platform detection > should enable copy shortcuts on Linux (both CTRL+C and CTRL+SHIFT+C)
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/renderer/components/terminal/tests/useXterm.test.ts > useXterm keyboard handlers > Platform detection > should enable paste shortcuts on Linux (both CTRL+V and CTRL+SHIFT+V)
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/renderer/components/terminal/tests/useXterm.test.ts > useXterm keyboard handlers > Platform detection > should enable paste shortcuts on Windows (CTRL+V)
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > clipboard error handling without breaking terminal > should continue terminal operation after clipboard error
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > keyboard event propagation > should maintain correct handler ordering for existing shortcuts
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > keyboard event propagation > should prevent copy/paste events from interfering with other shortcuts
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > clipboard read with xterm paste integration > should not paste when clipboard is empty
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > clipboard read with xterm paste integration > should integrate clipboard.readText() with xterm.paste()
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > xterm.js selection API integration with clipboard write > should not call getSelection when hasSelection returns false
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > xterm.js selection API integration with clipboard write > should integrate xterm.hasSelection() with clipboard write
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15

🪛 GitHub Check: test-frontend (ubuntu-latest)

[failure] 359-359: src/renderer/components/terminal/tests/useXterm.test.ts > useXterm keyboard handlers > Platform detection > should enable copy shortcuts on Linux (both CTRL+C and CTRL+SHIFT+C)
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/renderer/components/terminal/tests/useXterm.test.ts > useXterm keyboard handlers > Platform detection > should enable paste shortcuts on Linux (both CTRL+V and CTRL+SHIFT+V)
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/renderer/components/terminal/tests/useXterm.test.ts > useXterm keyboard handlers > Platform detection > should enable paste shortcuts on Windows (CTRL+V)
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > clipboard error handling without breaking terminal > should continue terminal operation after clipboard error
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > keyboard event propagation > should maintain correct handler ordering for existing shortcuts
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > keyboard event propagation > should prevent copy/paste events from interfering with other shortcuts
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > clipboard read with xterm paste integration > should not paste when clipboard is empty
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > clipboard read with xterm paste integration > should integrate clipboard.readText() with xterm.paste()
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > xterm.js selection API integration with clipboard write > should not call getSelection when hasSelection returns false
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 359-359: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > xterm.js selection API integration with clipboard write > should integrate xterm.hasSelection() with clipboard write
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:359:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15

🤖 Prompt for AI Agents
In `@apps/frontend/src/renderer/components/terminal/useXterm.ts` around lines 333
- 372, Guard against window.matchMedia being undefined and add a resize
fallback, then re-register the media query whenever DPI changes: inside the
effect, check if window.matchMedia exists before using it and fall back to
listening to window.resize; modify the handleDPIChange routine (and its
debounce) to, after adjusting xtermRef.current.options.fontSize and calling
fitAddonRef.current.fit() (using terminalRef to check bounds), recreate the
mediaQuery using the latest window.devicePixelRatio and reattach its 'change'
listener so the query isn't stale; ensure you remove the correct listeners on
cleanup (use removeEventListener when added and removeListener fallback for
older APIs) and reference the existing symbols handleDPIChange, xtermRef,
fitAddonRef, terminalRef, and mediaQuery when updating the logic.

@AndyMik90 AndyMik90 self-assigned this Jan 17, 2026
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

if (mediaQuery.addListener) {
mediaQuery.addListener(handleDPIChange);
return () => mediaQuery.removeListener?.(handleDPIChange);
}
Copy link

Choose a reason for hiding this comment

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

DPI change detection misses multi-monitor transitions

Medium Severity

The matchMedia query is created once with the initial devicePixelRatio, but the change event only fires when the match state changes. Moving between two DPI values that are both different from the initial one (e.g., 2x → 1x when started at 1.5x) won't trigger the handler because the query remains in "not matching" state throughout. The correct pattern requires recreating the media query after each DPI change using { once: true } and re-registering a new listener for the updated ratio.

Fix in Cursor Fix in Web

win32: [
// Windows usually handles PATH better, but we can add common locations
// User-local binaries
'~/.local/bin', // User-local binaries (Claude CLI installed here)
Copy link

Choose a reason for hiding this comment

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

Windows path uses inconsistent forward slash separator

Medium Severity

The newly added '~/.local/bin' path uses a forward slash, while all other Windows tilde-prefixed paths use backslashes (e.g., '~\\AppData\\Roaming\\npm'). After expansion with os.homedir() (which returns backslashes on Windows), this creates mixed separators like 'C:\\Users\\username/.local/bin'. The currentPathSet.has(p) check at line 238 uses string comparison, so this won't match an existing PATH entry 'C:\\Users\\username\\.local\\bin', causing potential duplicate PATH entries.

Fix in Cursor Fix in Web

@AndyMik90
Copy link
Owner

🤖 Auto Claude PR Review

Merge Verdict: 🔴 BLOCKED

🔴 Blocked - 4 CI check(s) failing. Fix CI before merge.

Blocked: 4 CI check(s) failing. Fix CI before merge.

Risk Assessment

Factor Level Notes
Complexity Low Based on lines changed
Security Impact None Based on security findings
Scope Coherence Good Based on structural review

🚨 Blocking Issues (Must Fix)

  • Branch Out of Date: PR branch is behind the base branch and needs to be updated
  • CI Failed: CI Complete
  • CI Failed: test-frontend (windows-latest)
  • CI Failed: test-frontend (macos-latest)
  • CI Failed: test-frontend (ubuntu-latest)

Generated by Auto Claude PR Review


This review identified blockers that must be resolved before merge. Generated by Auto Claude.

Copy link
Contributor

@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/frontend/src/renderer/components/terminal/useXterm.ts`:
- Around line 383-399: Guard against window.matchMedia being undefined and make
the DPI listener re-register the media query after DPI changes: in the effect
that creates mediaQuery (the variable mediaQuery) check if typeof
window.matchMedia === 'function' before using it and return early if not (or add
the provided JSDOM mock in test setup to unblock CI); modify handleDPIChange so
it removes listeners from the existing mediaQuery, recomputes a new mediaQuery
using the current window.devicePixelRatio, and attaches the appropriate listener
via addEventListener/addListener (and corresponding
removeEventListener/removeListener) so subsequent DPI changes (e.g., moving
between monitors) are detected; ensure cleanup returns a function that removes
listeners from whichever mediaQuery is currently registered.
♻️ Duplicate comments (2)
apps/frontend/src/renderer/components/terminal/useXterm.ts (2)

54-56: Comment does not match implementation.

The comment describes direct inverse scaling (1 / 1.5), but the code uses 1 / Math.sqrt(scaleFactor). Update the comment to accurately reflect the square-root-based formula.


364-368: Extract duplicated font size calculation to a helper function.

This calculation is duplicated from lines 52-58. Extract to a reusable helper to follow DRY and ensure consistency if the formula changes.

♻️ Suggested refactor

Add a helper function at the top of the file (after imports):

/** Calculate DPI-adjusted font size for terminal */
function getAdjustedFontSize(baseFontSize = 13, minFontSize = 11): number {
  const scaleFactor = window.devicePixelRatio || 1;
  const adjustedFontSize = scaleFactor > 1
    ? Math.max(baseFontSize * (1 / Math.sqrt(scaleFactor)), minFontSize)
    : baseFontSize;
  return Math.round(adjustedFontSize);
}

Then use it in both locations:

-    const baseFontSize = 13;
-    const scaleFactor = window.devicePixelRatio || 1;
-    const adjustedFontSize = scaleFactor > 1
-      ? Math.max(baseFontSize * (1 / Math.sqrt(scaleFactor)), 11)
-      : baseFontSize;
+    const adjustedFontSize = getAdjustedFontSize();

Comment on lines +383 to +399
// Listen for DPI/scale changes via matchMedia
// This fires when user moves window between monitors with different DPI
// or changes Windows display scaling settings
const mediaQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);

// Modern API: addEventListener
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handleDPIChange);
return () => mediaQuery.removeEventListener('change', handleDPIChange);
}

// Fallback for older browsers: addListener (deprecated)
if (mediaQuery.addListener) {
mediaQuery.addListener(handleDPIChange);
return () => mediaQuery.removeListener?.(handleDPIChange);
}
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

CI blocker: Guard matchMedia and re-register query after DPI changes.

Two issues cause test failures and multi-monitor bugs:

  1. Test failure (CI blocker): window.matchMedia is undefined in JSDOM, causing TypeError: window.matchMedia is not a function across all test runs.

  2. Stale media query: The query is created once with the initial devicePixelRatio. After the first DPI change fires, the query won't detect subsequent changes (e.g., moving window between monitors multiple times).

🐛 Proposed fix
   // Handle DPI/scale changes (Windows display scaling, moving between monitors)
   useEffect(() => {
+    // Guard for test environments where matchMedia is not available
+    if (typeof window.matchMedia !== 'function') {
+      // Fallback to resize listener for basic DPI change detection
+      const handleResize = debounce(() => {
+        if (xtermRef.current) {
+          xtermRef.current.options.fontSize = getAdjustedFontSize();
+          if (fitAddonRef.current && terminalRef.current) {
+            const rect = terminalRef.current.getBoundingClientRect();
+            if (rect.width > 0 && rect.height > 0) {
+              fitAddonRef.current.fit();
+            }
+          }
+        }
+      }, 200);
+      window.addEventListener('resize', handleResize);
+      return () => window.removeEventListener('resize', handleResize);
+    }
+
     const handleDPIChange = debounce(() => {
       if (xtermRef.current) {
-        const baseFontSize = 13;
-        const scaleFactor = window.devicePixelRatio || 1;
-        const adjustedFontSize = scaleFactor > 1
-          ? Math.max(baseFontSize * (1 / Math.sqrt(scaleFactor)), 11)
-          : baseFontSize;
-
-        // Update font size dynamically
-        xtermRef.current.options.fontSize = Math.round(adjustedFontSize);
+        xtermRef.current.options.fontSize = getAdjustedFontSize();

         // Trigger a fit to recalculate terminal dimensions with new font size
         if (fitAddonRef.current && terminalRef.current) {
           const rect = terminalRef.current.getBoundingClientRect();
           if (rect.width > 0 && rect.height > 0) {
             fitAddonRef.current.fit();
           }
         }
       }
     }, 200); // Debounce DPI changes to avoid excessive recalculations

-    // Listen for DPI/scale changes via matchMedia
-    // This fires when user moves window between monitors with different DPI
-    // or changes Windows display scaling settings
-    const mediaQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
-
-    // Modern API: addEventListener
-    if (mediaQuery.addEventListener) {
-      mediaQuery.addEventListener('change', handleDPIChange);
-      return () => mediaQuery.removeEventListener('change', handleDPIChange);
-    }
-
-    // Fallback for older browsers: addListener (deprecated)
-    if (mediaQuery.addListener) {
-      mediaQuery.addListener(handleDPIChange);
-      return () => mediaQuery.removeListener?.(handleDPIChange);
-    }
+    // Re-register pattern: create new query after each DPI change
+    let mediaQuery: MediaQueryList;
+    const onChange = () => {
+      handleDPIChange();
+      // Re-register with updated devicePixelRatio to detect subsequent changes
+      mediaQuery.removeEventListener('change', onChange);
+      mediaQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
+      mediaQuery.addEventListener('change', onChange);
+    };
+
+    mediaQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
+    mediaQuery.addEventListener('change', onChange);
+
+    return () => {
+      mediaQuery.removeEventListener('change', onChange);
+    };
   }, []);

Alternatively, add a matchMedia mock to the test setup file (apps/frontend/src/__tests__/setup.ts) to unblock CI immediately:

Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: (query: string) => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: () => {},
    removeListener: () => {},
    addEventListener: () => {},
    removeEventListener: () => {},
    dispatchEvent: () => false,
  }),
});
🧰 Tools
🪛 GitHub Check: test-frontend (macos-latest)

[failure] 386-386: src/renderer/components/terminal/tests/useXterm.test.ts > useXterm keyboard handlers > Platform detection > should enable copy shortcuts on Linux (both CTRL+C and CTRL+SHIFT+C)
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/renderer/components/terminal/tests/useXterm.test.ts > useXterm keyboard handlers > Platform detection > should enable paste shortcuts on Linux (both CTRL+V and CTRL+SHIFT+V)
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/renderer/components/terminal/tests/useXterm.test.ts > useXterm keyboard handlers > Platform detection > should enable paste shortcuts on Windows (CTRL+V)
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > clipboard error handling without breaking terminal > should continue terminal operation after clipboard error
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > keyboard event propagation > should maintain correct handler ordering for existing shortcuts
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > keyboard event propagation > should prevent copy/paste events from interfering with other shortcuts
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > clipboard read with xterm paste integration > should not paste when clipboard is empty
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > clipboard read with xterm paste integration > should integrate clipboard.readText() with xterm.paste()
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > xterm.js selection API integration with clipboard write > should not call getSelection when hasSelection returns false
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > xterm.js selection API integration with clipboard write > should integrate xterm.hasSelection() with clipboard write
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15

🪛 GitHub Check: test-frontend (ubuntu-latest)

[failure] 386-386: src/renderer/components/terminal/tests/useXterm.test.ts > useXterm keyboard handlers > Platform detection > should enable copy shortcuts on Linux (both CTRL+C and CTRL+SHIFT+C)
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/renderer/components/terminal/tests/useXterm.test.ts > useXterm keyboard handlers > Platform detection > should enable paste shortcuts on Linux (both CTRL+V and CTRL+SHIFT+V)
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/renderer/components/terminal/tests/useXterm.test.ts > useXterm keyboard handlers > Platform detection > should enable paste shortcuts on Windows (CTRL+V)
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > clipboard error handling without breaking terminal > should continue terminal operation after clipboard error
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > keyboard event propagation > should maintain correct handler ordering for existing shortcuts
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > keyboard event propagation > should prevent copy/paste events from interfering with other shortcuts
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > clipboard read with xterm paste integration > should not paste when clipboard is empty
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > clipboard read with xterm paste integration > should integrate clipboard.readText() with xterm.paste()
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > xterm.js selection API integration with clipboard write > should not call getSelection when hasSelection returns false
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15


[failure] 386-386: src/tests/integration/terminal-copy-paste.test.ts > Terminal copy/paste integration > xterm.js selection API integration with clipboard write > should integrate xterm.hasSelection() with clipboard write
TypeError: window.matchMedia is not a function
❯ src/renderer/components/terminal/useXterm.ts:386:31
❯ Object.react_stack_bottom_frame ../../node_modules/react-dom/cjs/react-dom-client.development.js:25989:20
❯ runWithFiberInDEV ../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ commitHookEffectListMount ../../node_modules/react-dom/cjs/react-dom-client.development.js:13249:29
❯ commitHookPassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:13336:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15484:13
❯ recursivelyTraversePassiveMountEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:15439:11
❯ commitPassiveMountOnFiber ../../node_modules/react-dom/cjs/react-dom-client.development.js:15519:11
❯ flushPassiveEffects ../../node_modules/react-dom/cjs/react-dom-client.development.js:18432:9
❯ ../../node_modules/react-dom/cjs/react-dom-client.development.js:17923:15

🤖 Prompt for AI Agents
In `@apps/frontend/src/renderer/components/terminal/useXterm.ts` around lines 383
- 399, Guard against window.matchMedia being undefined and make the DPI listener
re-register the media query after DPI changes: in the effect that creates
mediaQuery (the variable mediaQuery) check if typeof window.matchMedia ===
'function' before using it and return early if not (or add the provided JSDOM
mock in test setup to unblock CI); modify handleDPIChange so it removes
listeners from the existing mediaQuery, recomputes a new mediaQuery using the
current window.devicePixelRatio, and attaches the appropriate listener via
addEventListener/addListener (and corresponding
removeEventListener/removeListener) so subsequent DPI changes (e.g., moving
between monitors) are detected; ensure cleanup returns a function that removes
listeners from whichever mediaQuery is currently registered.

Comment on lines +418 to +422
# in Bash because they're not in the limited COMMON_BIN_PATHS
if platform.system() == "Windows":
system_path = get_windows_system_path()
if system_path:
env["PATH"] = system_path
Copy link

Choose a reason for hiding this comment

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

Bug: On Windows, the code incorrectly copies the parent process's potentially limited PATH, which can prevent the subprocess from finding necessary system tools.
Severity: HIGH

Suggested Fix

Instead of copying os.environ.get("PATH"), the code should retrieve the true system PATH from the Windows environment, for example, by querying the registry or using ctypes.windll. This ensures the subprocess receives the complete system PATH, regardless of the parent process's environment.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: apps/backend/core/auth.py#L418-L422

Potential issue: On Windows, the code sets `env["PATH"]` for the SDK subprocess using
the parent Python process's `os.environ.get("PATH")`. Since the SDK merges environments
with `env` values overriding `os.environ`, this effectively replaces the subprocess's
inherited `PATH`. If the parent Python process is running in a constrained environment
(e.g., a virtualenv) with a limited `PATH`, this limited `PATH` will be passed to the
subprocess. This prevents the subprocess from accessing the full set of system tools,
which is contrary to the intended goal of the change.

Did we get this right? 👍 / 👎 to inform future reviews.

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.

3 participants