Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/workflows/build-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,100 @@ jobs:
path: apps/desktop/release/*-mac.yml
retention-days: ${{ inputs.artifact_retention_days }}
if-no-files-found: error

build-linux:
name: Build - Linux (${{ matrix.arch }})
runs-on: ubuntu-latest
environment: production

strategy:
fail-fast: false
matrix:
arch: [x64]

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: "1.3.2"

- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y libarchive-tools rpm

- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ github.sha }}
restore-keys: |
${{ runner.os }}-bun-

- name: Install dependencies
run: bun install --frozen

- name: Set version suffix
if: inputs.version_suffix != ''
working-directory: apps/desktop
run: |
# Read current version and append suffix
CURRENT_VERSION=$(node -p "require('./package.json').version")
NEW_VERSION="${CURRENT_VERSION}${{ inputs.version_suffix }}"
echo "Setting version to: $NEW_VERSION"
# Update package.json version using node
node -e "
const fs = require('fs');
const pkg = require('./package.json');
pkg.version = '$NEW_VERSION';
fs.writeFileSync('./package.json', JSON.stringify(pkg, null, '\t') + '\n');
"
echo "Updated package.json version to $NEW_VERSION"

- name: Clean dev folder
working-directory: apps/desktop
run: bun run clean:dev

- name: Compile app with electron-vite
working-directory: apps/desktop
env:
NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}
NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GH_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }}
NEXT_PUBLIC_WEB_URL: ${{ secrets.NEXT_PUBLIC_WEB_URL }}
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
NEXT_PUBLIC_DOCS_URL: ${{ secrets.NEXT_PUBLIC_DOCS_URL }}
SENTRY_DSN_DESKTOP: ${{ secrets.SENTRY_DSN_DESKTOP }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: bun run compile:app

- name: Build Electron app
working-directory: apps/desktop
run: bun run package -- --publish never --config ${{ inputs.electron_builder_config }} --linux

- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.artifact_prefix }}-linux-${{ matrix.arch }}-appimage
path: apps/desktop/release/*.AppImage
retention-days: ${{ inputs.artifact_retention_days }}
if-no-files-found: error

- name: Upload deb artifact
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.artifact_prefix }}-linux-${{ matrix.arch }}-deb
path: apps/desktop/release/*.deb
retention-days: ${{ inputs.artifact_retention_days }}
if-no-files-found: error

- name: Upload Linux auto-update manifest
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.artifact_prefix }}-linux-update-manifest
path: apps/desktop/release/*-linux.yml
retention-days: ${{ inputs.artifact_retention_days }}
if-no-files-found: error
25 changes: 25 additions & 0 deletions .github/workflows/release-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ jobs:
run: |
cd release-artifacts
# Create stable-named copies (without version) for /releases/latest/download/ URLs

# macOS DMG files
for file in *.dmg; do
if [[ -f "$file" ]]; then
# Extract architecture from filename (e.g., Superset-0.0.1-arm64.dmg -> arm64)
Expand All @@ -70,6 +72,8 @@ jobs:
echo "Created stable copy: Superset-${arch}.dmg"
fi
done

# macOS ZIP files
for file in *-mac.zip; do
if [[ -f "$file" ]]; then
# Extract architecture from filename (e.g., Superset-0.0.1-arm64-mac.zip -> arm64)
Expand All @@ -78,6 +82,27 @@ jobs:
echo "Created stable copy: Superset-${arch}-mac.zip"
fi
done

# Linux AppImage files
for file in *.AppImage; do
if [[ -f "$file" ]]; then
# Extract architecture from filename (e.g., superset-0.0.1-x64.AppImage -> x64)
arch=$(echo "$file" | sed -E 's/.*-([^-]+)\.AppImage$/\1/')
cp "$file" "Superset-${arch}.AppImage"
echo "Created stable copy: Superset-${arch}.AppImage"
fi
done

# Linux DEB files
for file in *.deb; do
if [[ -f "$file" ]]; then
# Extract architecture from filename (e.g., superset-0.0.1-amd64.deb -> amd64)
arch=$(echo "$file" | sed -E 's/.*-([^-]+)\.deb$/\1/')
cp "$file" "Superset-${arch}.deb"
echo "Created stable copy: Superset-${arch}.deb"
fi
done

echo "Release artifacts:"
ls -la

Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/create-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,11 @@ else
echo -e "${BLUE}Latest URL:${NC} ${LATEST_URL}"
echo ""
echo -e "${BLUE}Direct download:${NC}"
echo " • ${LATEST_URL}/download/Superset-arm64.dmg"
echo " macOS:"
echo " • ${LATEST_URL}/download/Superset-arm64.dmg"
echo " Linux:"
echo " • ${LATEST_URL}/download/Superset-x64.AppImage"
echo " • ${LATEST_URL}/download/Superset-amd64.deb"
echo ""
else
success "Draft release created!"
Expand Down
19 changes: 18 additions & 1 deletion apps/desktop/src/lib/electron-app/factories/app/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,24 @@ export async function makeAppSetup(
return window;
}

PLATFORM.IS_LINUX && app.disableHardwareAcceleration();
// Allow users with problematic GPU drivers to disable hardware acceleration
if (process.env.SUPERSET_DISABLE_GPU === "1") {
app.disableHardwareAcceleration();
}

// Enable GPU optimizations on Linux
if (PLATFORM.IS_LINUX) {
app.commandLine.appendSwitch("enable-gpu-rasterization");
app.commandLine.appendSwitch("enable-zero-copy");
app.commandLine.appendSwitch("ignore-gpu-blocklist");
// Canvas OOP rasterization offloads canvas rendering to GPU process
app.commandLine.appendSwitch(
"enable-features",
"CanvasOopRasterization,VaapiVideoDecodeLinuxGL",
);
// Auto-detect Wayland vs X11 for native compositor integration
app.commandLine.appendSwitch("ozone-platform-hint", "auto");
}
Comment on lines +72 to +89
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The GPU acceleration escape hatch (SUPERSET_DISABLE_GPU=1) doesn't fully disable GPU features on Linux. When SUPERSET_DISABLE_GPU=1 is set, disableHardwareAcceleration() is called, but then the Linux-specific GPU switches (enable-gpu-rasterization, enable-zero-copy, etc.) are still applied unconditionally. This creates inconsistent behavior where the user attempts to disable GPU acceleration, but GPU-specific features are still enabled via command-line switches. The Linux GPU optimization block should be wrapped in a condition that checks if GPU acceleration wasn't disabled.

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +89
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find package.json in the desktop app to check Electron version
find . -name "package.json" -type f | head -20

Repository: superset-sh/superset

Length of output: 586


🏁 Script executed:

# Search for electron in package files
rg "electron" --type json -l | head -10

Repository: superset-sh/superset

Length of output: 163


🏁 Script executed:

cat ./apps/desktop/package.json | rg "electron" -A 1 -B 1

Repository: superset-sh/superset

Length of output: 1388


🌐 Web query:

Electron 39 Chromium version compatibility

💡 Result:

Electron 39.x is built on Chromium 142 (Chrome/Chromium “M142”). (endoflife.date)

Exact Chromium patch versions depend on the Electron 39 minor/patch release, for example:

To verify for any specific 39.x version, check its entry on Electron Releases (it lists the bundled Chromium/Node/V8). (releases.electronjs.org)

Citations:


🌐 Web query:

VaapiVideoDecodeLinuxGL Chromium flag electron 39

💡 Result:

Electron 39 ships with Chromium 142.0.7444.52 (releases.electronjs.org), so you’re dealing with “modern” Linux VA-API behavior/flags.

What VaapiVideoDecodeLinuxGL is

--enable-features=VaapiVideoDecodeLinuxGL is the Chromium feature-flag that enables VA-API hardware video decoding on Linux via the GL path (best-effort / not officially supported). Chromium’s own VA-API doc lists it as the decode switch and also calls out the commonly-paired GL/ANGLE switches: --use-gl=angle and --use-angle=gl. (chromium.googlesource.com)

How to pass it in Electron 39

Add the switches before app.whenReady():

const { app } = require('electron');

app.commandLine.appendSwitch('enable-features', 'VaapiVideoDecodeLinuxGL');
app.commandLine.appendSwitch('use-gl', 'angle');
app.commandLine.appendSwitch('use-angle', 'gl');
// optional, sometimes needed:
// app.commandLine.appendSwitch('ignore-gpu-blocklist');

app.whenReady().then(() => { /* create windows */ });

Important: flag rename in newer Chromium

Multiple Linux guides report that starting around Chromium 131, VA-API decode flags were effectively renamed from Vaapi* to Accelerated* (and --use-gl=angle --use-angle=gl may no longer be required on some setups). The commonly suggested replacements are:

  • --enable-features=AcceleratedVideoDecodeLinuxGL,AcceleratedVideoDecodeLinuxZeroCopyGL (plus AcceleratedVideoEncoder if you care about encode)
  • Sometimes also VaapiIgnoreDriverChecks (esp. non-Intel / finicky configs) (dev.to) (wiki.debian.org)

So on Electron 39, if VaapiVideoDecodeLinuxGL doesn’t do anything, try:

app.commandLine.appendSwitch(
  'enable-features',
  'AcceleratedVideoDecodeLinuxGL,AcceleratedVideoDecodeLinuxZeroCopyGL'
);

Verify it worked

Use Chromium’s recommended check:

  • chrome://media-internals → for the playing video, look for video_decoder showing a GPU/VAAPI decoder. (chromium.googlesource.com)

Citations:


🌐 Web query:

enable-gpu-rasterization enable-zero-copy ignore-gpu-blocklist electron 39 deprecated

💡 Result:

Those three flags are Chromium “about:flags”/GPU tuning switches, not Electron APIs. In Electron v39, they’re commonly reported as “deprecated/ignored” because Electron only guarantees a limited set of supported command-line switches, and unsupported switches “will have no effect” (even if Chromium used to accept them). [1]

What to do instead:

  • Don’t rely on --enable-gpu-rasterization, --enable-zero-copy, --ignore-gpu-blocklist for Electron v39—they’re not in Electron’s supported-switch list, so behavior can change or become a no-op. [1]
  • If your goal is “use the discrete GPU,” use Electron-supported switches like --force_high_performance_gpu (or the low-power counterpart). [1]
  • If you’re trying to tweak Chromium rendering behavior, Electron recommends discovering the current Chromium-in-Electron switches via Chromium’s flags/version pages and noting many modern toggles are now --enable-features=... entries rather than standalone switches. [1]
  • For background on GPU rasterization being a Chromium feature primarily controlled via chrome://flags/#enable-gpu-rasterization / related mechanisms, see Chromium’s graphics docs. [2]

Sources: Electron “Supported Command Line Switches” (unsupported switches have no effect; guidance on Chromium flags) [1]; Chromium graphics doc on enabling GPU rasterization [2].


Remove or replace unsupported GPU flags for Electron 39.

The flags enable-gpu-rasterization, enable-zero-copy, and ignore-gpu-blocklist are not in Electron's supported command-line switches list and will have no effect in Electron 39. Remove these or replace with Electron-supported alternatives like --force_high_performance_gpu if targeting discrete GPU usage.

Additionally, VaapiVideoDecodeLinuxGL is deprecated in Chromium 142 (bundled with Electron 39.x) in favor of AcceleratedVideoDecodeLinuxGL and AcceleratedVideoDecodeLinuxZeroCopyGL. Update the enable-features flag to use the modern accelerated video decode names for better compatibility.

Current code
app.commandLine.appendSwitch("enable-gpu-rasterization");
app.commandLine.appendSwitch("enable-zero-copy");
app.commandLine.appendSwitch("ignore-gpu-blocklist");
app.commandLine.appendSwitch(
	"enable-features",
	"CanvasOopRasterization,VaapiVideoDecodeLinuxGL",
);
🤖 Prompt for AI Agents
In `@apps/desktop/src/lib/electron-app/factories/app/setup.ts` around lines 77 -
89, In the PLATFORM.IS_LINUX block update the GPU flags: remove the unsupported
switches passed via app.commandLine.appendSwitch("enable-gpu-rasterization"),
("enable-zero-copy") and ("ignore-gpu-blocklist") and instead use a supported
alternative for forcing discrete GPU when needed (e.g., the Electron-supported
force_high_performance_gpu switch) via app.commandLine.appendSwitch; also update
the enable-features value passed to
app.commandLine.appendSwitch("enable-features", ...) to replace the deprecated
"VaapiVideoDecodeLinuxGL" with the modern names
"AcceleratedVideoDecodeLinuxGL,AcceleratedVideoDecodeLinuxZeroCopyGL" while
keeping CanvasOopRasterization as appropriate. Ensure all changes are made
inside the same PLATFORM.IS_LINUX conditional around
app.commandLine.appendSwitch.


PLATFORM.IS_WINDOWS &&
app.setAppUserModelId(
Expand Down
48 changes: 44 additions & 4 deletions apps/desktop/src/lib/trpc/routers/external/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import nodePath from "node:path";
import { EXTERNAL_APPS, type ExternalApp } from "@superset/local-db";

/** Map of app IDs to their macOS application names */
const APP_NAMES: Record<ExternalApp, string | null> = {
const MAC_APP_NAMES: Record<ExternalApp, string | null> = {
finder: null, // Handled specially with shell.showItemInFolder
vscode: "Visual Studio Code",
"vscode-insiders": "Visual Studio Code - Insiders",
Expand All @@ -29,17 +29,57 @@ const APP_NAMES: Record<ExternalApp, string | null> = {
rustrover: "RustRover",
};

/** Map of app IDs to their Linux CLI commands */
const LINUX_APP_COMMANDS: Record<ExternalApp, string | null> = {
finder: null,
vscode: "code",
"vscode-insiders": "code-insiders",
cursor: "cursor",
zed: "zed",
xcode: null, // macOS only
iterm: null, // macOS only
warp: null, // macOS only
terminal: "x-terminal-emulator",
ghostty: "ghostty",
sublime: "subl",
intellij: "idea",
webstorm: "webstorm",
pycharm: "pycharm",
phpstorm: "phpstorm",
rubymine: "rubymine",
goland: "goland",
clion: "clion",
rider: "rider",
datagrip: "datagrip",
appcode: null, // macOS only
fleet: "fleet",
rustrover: "rustrover",
};

/**
* Get the command and args to open a path in the specified app.
* Uses `open -a` for macOS apps to avoid PATH issues in production builds.
* Uses `open -a` on macOS, direct CLI commands on Linux.
*/
export function getAppCommand(
app: ExternalApp,
targetPath: string,
): { command: string; args: string[] } | null {
const appName = APP_NAMES[app];
if (process.platform === "darwin") {
const appName = MAC_APP_NAMES[app];
if (!appName) return null;
return { command: "open", args: ["-a", appName, targetPath] };
}

if (process.platform === "linux") {
const command = LINUX_APP_COMMANDS[app];
if (!command) return null;
return { command, args: [targetPath] };
}

// Windows: use start command
const appName = MAC_APP_NAMES[app];
if (!appName) return null;
return { command: "open", args: ["-a", appName, targetPath] };
return { command: "cmd", args: ["/c", "start", "", appName, targetPath] };
Comment on lines +79 to +82
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The Windows implementation in getAppCommand uses MAC_APP_NAMES which contains application display names like "Visual Studio Code", "IntelliJ IDEA", etc. These are not valid executable names for Windows. The cmd /c start command expects either executable names (like "code.exe") or properly registered application names. This implementation will fail to open applications on Windows. A WINDOWS_APP_COMMANDS map similar to LINUX_APP_COMMANDS should be created with appropriate Windows executable names or registered application names.

Copilot uses AI. Check for mistakes.
}

/**
Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/src/main/lib/auto-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export function dismissUpdate(): void {
}

export function checkForUpdates(): void {
if (env.NODE_ENV === "development" || !PLATFORM.IS_MAC) {
if (env.NODE_ENV === "development" || PLATFORM.IS_WINDOWS) {
return;
}
isDismissed = false;
Expand All @@ -125,11 +125,11 @@ export function checkForUpdatesInteractive(): void {
});
return;
}
if (!PLATFORM.IS_MAC) {
if (PLATFORM.IS_WINDOWS) {
dialog.showMessageBox({
type: "info",
title: "Updates",
message: "Auto-updates are only available on macOS.",
message: "Auto-updates are not yet available on Windows.",
});
return;
}
Expand Down Expand Up @@ -198,7 +198,7 @@ export function simulateError(): void {
}

export function setupAutoUpdater(): void {
if (env.NODE_ENV === "development" || !PLATFORM.IS_MAC) {
if (env.NODE_ENV === "development" || PLATFORM.IS_WINDOWS) {
return;
}

Expand Down
11 changes: 8 additions & 3 deletions apps/desktop/src/main/lib/notification-sound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,15 @@ function playSoundFile(soundPath: string): void {
`(New-Object Media.SoundPlayer '${soundPath}').PlaySync()`,
]);
} else {
// Linux - try common audio players
execFile("paplay", [soundPath], (error) => {
// Linux - try common audio players in order of preference:
// pw-play (PipeWire), paplay (PulseAudio), aplay (ALSA)
execFile("pw-play", [soundPath], (error) => {
if (error) {
execFile("aplay", [soundPath]);
execFile("paplay", [soundPath], (error) => {
if (error) {
execFile("aplay", [soundPath]);
}
});
}
});
Comment on lines +64 to 74
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add error logging when all audio players fail.

If all three audio players fail, the error is silently swallowed. This makes debugging audio issues on Linux unnecessarily difficult. Additionally, consider using distinct parameter names to avoid shadowing error in the nested callbacks.

As per coding guidelines: "Never swallow errors silently; at minimum log errors with context before rethrowing or handling them explicitly."

🔊 Proposed fix to add error logging
 		// Linux - try common audio players in order of preference:
 		// pw-play (PipeWire), paplay (PulseAudio), aplay (ALSA)
-		execFile("pw-play", [soundPath], (error) => {
-			if (error) {
-				execFile("paplay", [soundPath], (error) => {
-					if (error) {
-						execFile("aplay", [soundPath]);
+		execFile("pw-play", [soundPath], (pwError) => {
+			if (pwError) {
+				execFile("paplay", [soundPath], (paError) => {
+					if (paError) {
+						execFile("aplay", [soundPath], (alsaError) => {
+							if (alsaError) {
+								console.warn(
+									`[notification-sound] All audio players failed for: ${soundPath}`
+								);
+							}
+						});
 					}
 				});
 			}
 		});
📝 Committable suggestion

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

Suggested change
// Linux - try common audio players in order of preference:
// pw-play (PipeWire), paplay (PulseAudio), aplay (ALSA)
execFile("pw-play", [soundPath], (error) => {
if (error) {
execFile("aplay", [soundPath]);
execFile("paplay", [soundPath], (error) => {
if (error) {
execFile("aplay", [soundPath]);
}
});
}
});
// Linux - try common audio players in order of preference:
// pw-play (PipeWire), paplay (PulseAudio), aplay (ALSA)
execFile("pw-play", [soundPath], (pwError) => {
if (pwError) {
execFile("paplay", [soundPath], (paError) => {
if (paError) {
execFile("aplay", [soundPath], (alsaError) => {
if (alsaError) {
console.warn(
`[notification-sound] All audio players failed for: ${soundPath}`
);
}
});
}
});
}
});
🤖 Prompt for AI Agents
In `@apps/desktop/src/main/lib/notification-sound.ts` around lines 64 - 74, The
nested execFile callbacks currently swallow errors and shadow the same `error`
name; update the Linux audio fallback block (where execFile is called with
"pw-play", "paplay", "aplay" and uses `soundPath`) to use distinct callback
parameter names (e.g., errPw, errPap, errAplay) and, if the final execFile call
fails, log a helpful error message including the soundPath and which players
were attempted (using console.error or the project logger). Ensure each
intermediate failure is either logged or propagated as needed and avoid reusing
the same `error` variable in nested callbacks.

}
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/main/lib/terminal/port-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ class PortManager extends EventEmitter {
if (this.scanInterval) return;

this.scanInterval = setInterval(() => {
// Skip scanning when no sessions are registered (adaptive polling)
if (this.sessions.size === 0 && this.daemonSessions.size === 0) return;

this.scanAllSessions().catch((error) => {
console.error("[PortManager] Scan error:", error);
});
Expand Down
32 changes: 25 additions & 7 deletions apps/desktop/src/main/lib/window-state/bounds-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,29 +50,47 @@ export interface InitialWindowBounds {
/**
* Computes initial window bounds from saved state, with fallbacks.
*
* - No saved state → default to primary display size, centered
* - No saved state → maximize on primary display
* - Saved position visible → restore exactly
* - Saved position not visible (monitor disconnected) → use saved size, but center
* - Saved size is much smaller than current display → scale up proportionally
*/
export function getInitialWindowBounds(
savedState: WindowState | null,
): InitialWindowBounds {
const { workAreaSize } = screen.getPrimaryDisplay();

// No saved state → default to primary display size, centered
// No saved state → maximize by default for best first-launch experience
if (!savedState) {
return {
width: workAreaSize.width,
height: workAreaSize.height,
center: true,
isMaximized: false,
isMaximized: true,
};
}

const { width, height } = clampToWorkArea(
savedState.width,
savedState.height,
);
let { width, height } = clampToWorkArea(savedState.width, savedState.height);

// Scale up if saved bounds are much smaller than current work area
// This handles moving from a small screen to a large screen
const areaRatio =
(width * height) / (workAreaSize.width * workAreaSize.height);
if (areaRatio < 0.6) {
// Saved window covers less than 60% of current screen → scale up
// Use 90% of work area while maintaining aspect ratio
const savedAspect = width / height;
const targetWidth = Math.round(workAreaSize.width * 0.9);
const targetHeight = Math.round(targetWidth / savedAspect);

if (targetHeight <= workAreaSize.height) {
width = targetWidth;
height = targetHeight;
} else {
height = Math.round(workAreaSize.height * 0.9);
width = Math.round(height * savedAspect);
}
}
Comment on lines +73 to +93
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scale against the display that contains the saved bounds.

Right now scaling is based on the primary display’s work area. If the window was saved on a smaller secondary display, the scaled bounds can exceed that display and be treated as off-screen, causing an unexpected recenter onto the primary display. Consider deriving the work area from the display matching the saved bounds before applying the scale. Also, please extract the scaling thresholds to constants and switch clampToWorkArea to object params for consistency.

🔧 Suggested update (display-aware scaling + constants + object params)
 const MIN_VISIBLE_OVERLAP = 50;
 const MIN_WINDOW_SIZE = 400;
+const SCALE_UP_AREA_RATIO_THRESHOLD = 0.6;
+const SCALE_UP_WORKAREA_RATIO = 0.9;

 function clampToWorkArea(
-	width: number,
-	height: number,
+	{ width, height }: { width: number; height: number },
 ): { width: number; height: number } {
 	const { workAreaSize } = screen.getPrimaryDisplay();
 	return {
 		width: Math.min(Math.max(width, MIN_WINDOW_SIZE), workAreaSize.width),
 		height: Math.min(Math.max(height, MIN_WINDOW_SIZE), workAreaSize.height),
 	};
 }

 export function getInitialWindowBounds(
 	savedState: WindowState | null,
 ): InitialWindowBounds {
-	const { workAreaSize } = screen.getPrimaryDisplay();
+	const primaryWorkArea = screen.getPrimaryDisplay().workAreaSize;

 	// No saved state → maximize by default for best first-launch experience
 	if (!savedState) {
 		return {
-			width: workAreaSize.width,
-			height: workAreaSize.height,
+			width: primaryWorkArea.width,
+			height: primaryWorkArea.height,
 			center: true,
 			isMaximized: true,
 		};
 	}

-	let { width, height } = clampToWorkArea(savedState.width, savedState.height);
+	let { width, height } = clampToWorkArea({
+		width: savedState.width,
+		height: savedState.height,
+	});
+	const baseBounds: Rectangle = {
+		x: savedState.x,
+		y: savedState.y,
+		width,
+		height,
+	};
+	const { workAreaSize } = screen.getDisplayMatching(baseBounds);

 	// Scale up if saved bounds are much smaller than current work area
 	// This handles moving from a small screen to a large screen
 	const areaRatio =
 		(width * height) / (workAreaSize.width * workAreaSize.height);
-	if (areaRatio < 0.6) {
+	if (areaRatio < SCALE_UP_AREA_RATIO_THRESHOLD) {
 		// Saved window covers less than 60% of current screen → scale up
 		// Use 90% of work area while maintaining aspect ratio
 		const savedAspect = width / height;
-		const targetWidth = Math.round(workAreaSize.width * 0.9);
+		const targetWidth = Math.round(
+			workAreaSize.width * SCALE_UP_WORKAREA_RATIO,
+		);
 		const targetHeight = Math.round(targetWidth / savedAspect);

 		if (targetHeight <= workAreaSize.height) {
 			width = targetWidth;
 			height = targetHeight;
 		} else {
-			height = Math.round(workAreaSize.height * 0.9);
+			height = Math.round(
+				workAreaSize.height * SCALE_UP_WORKAREA_RATIO,
+			);
 			width = Math.round(height * savedAspect);
 		}
 	}

As per coding guidelines, Extract hardcoded magic numbers, strings, and enums to named constants at module top instead of leaving them inline in logic; Use object parameters for functions with 2 or more parameters instead of positional arguments.

🤖 Prompt for AI Agents
In `@apps/desktop/src/main/lib/window-state/bounds-validation.ts` around lines 73
- 93, Change scaling to use the work area of the display that contains the saved
bounds rather than the primary display: locate where workAreaSize is used and
replace it with a display-specific work area obtained from a helper (e.g.,
getDisplayForBounds or similar) using savedState.width/height and
savedState.x/y; extract the magic numbers into module-level constants (e.g.,
MIN_AREA_RATIO = 0.6, WORKAREA_SCALE = 0.9) and use them instead of 0.6/0.9; and
update clampToWorkArea to accept an object parameter (e.g., clampToWorkArea({
width, height })) and update its call site here accordingly. Ensure the scaling
math (savedAspect, targetWidth/targetHeight) uses the display-specific
workAreaSize and keep the same aspect-preserving logic.


const savedBounds: Rectangle = {
x: savedState.x,
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/main/terminal-host/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export class Session {
this.emulator = new HeadlessEmulator({
cols: options.cols,
rows: options.rows,
scrollback: options.scrollbackLines ?? 10000,
scrollback: options.scrollbackLines ?? 5000,
});

// Set initial CWD
Expand Down Expand Up @@ -940,7 +940,7 @@ export class Session {
if (process.platform === "win32") {
return process.env.COMSPEC || "cmd.exe";
}
return process.env.SHELL || "/bin/zsh";
return process.env.SHELL || "/bin/sh";
}

/**
Expand Down
Loading
Loading