* fix: prevent crash when clicking external links with no default browser (#663)
On systems like Tiny11 where no default browser is associated with
http/https URLs, shell.openExternal() rejects with Windows error 0x483
("No application is associated..."). The main process treated that
rejection as an unhandledRejection, which the global handler re-throws
as fatal, crashing the entire app.
Root cause: windowManager.cjs used `void shell?.openExternal?.(url)`
inside a try/catch, assuming the try would cover the call. `void` only
discards the returned Promise — it does not catch async rejections,
so when openExternal rejected, the error escaped as a floating
unhandledRejection.
The IPC handler in main.cjs (`netcatty:openExternal`) also awaited
shell.openExternal() without any try/catch. Electron's ipcMain.handle
forwards rejections to the renderer over IPC, but the renderer-side
fallback called `window.open()`, which re-entered the same buggy
windowManager path — and that is where the process actually died.
Changes:
- windowManager.cjs: attach an explicit `.catch` on the openExternal
Promise in both createExternalOnlyWindowOpenHandler and
createAppWindowOpenHandler so rejections cannot propagate.
- main.cjs: wrap the IPC handler in try/catch and return a structured
{ success, error } result instead of throwing. This lets the
renderer render an informative message.
- global.d.ts: update the openExternal return type to match.
- useApplicationBackend.ts: read the structured result and throw on
failure so callers can react; drop the now-redundant window.open()
fallback for the Electron branch (kept only for non-Electron envs).
- SettingsApplicationTab.tsx: show a friendly toast ("No default
browser configured — please set one in system settings") when
openExternal fails, instead of the previous silent failure.
- i18n: add en + zh-CN strings for the toast.
Closes#663
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: fall back to in-app browser window when system has no default browser
Instead of showing a toast when shell.openExternal() fails (e.g. Tiny11
with no default browser), open the URL in a minimal in-app BrowserWindow
so users can still read the linked page.
windowManager.cjs now exposes:
- openFallbackBrowser(url, opts): creates a stripped-down BrowserWindow
that loads the URL. No preload script (remote content must never
touch contextBridge), contextIsolation/nodeIntegration/sandbox all
set to safe defaults, and an isolated persist:netcatty-fallback-browser
session so cookies and storage do not leak into the main app.
Basic Alt+Left / Alt+Right / Ctrl-or-Cmd+R shortcuts for navigation
and reload.
- tryOpenExternalWithFallback(shell, url, opts): tries
shell.openExternal first; on rejection, falls back to
openFallbackBrowser. Returns { success, fallback?: "in-app-browser" }.
All three external-URL call paths now route through this helper:
- main.cjs netcatty:openExternal IPC handler
- createExternalOnlyWindowOpenHandler (popup blocker for child windows)
- createAppWindowOpenHandler (main/settings window window-open handler)
The renderer-side toast is retained as a last-resort for the rare case
that both system and in-app browsers fail (e.g. BrowserWindow creation
error). Copy updated to reflect the new behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: preserve rejection semantics for failed external opens
Per Codex review on PR #676: returning { success, error } from
bridge.openExternal changed the contract from "reject on failure" to
"resolve with a failure object on failure", which silently broke
callers that rely on rejection to abort flows.
useCloudSync's OAuth path is the clearest example: it wraps
bridge.openExternal in a try/catch and rejects browserPromise inside
the catch. With the resolved-failure contract, that catch never fires,
so Promise.race([callbackPromise, browserPromise]) can hang
indefinitely when no browser is available.
Revert the contract:
- tryOpenExternalWithFallback resolves void on success (system browser
or in-app fallback) and throws on total failure
- main.cjs IPC handler awaits and lets rejections propagate
- global.d.ts openExternal is Promise<void> again
- useApplicationBackend just awaits — rejections propagate naturally
- SettingsApplicationTab's existing try/catch + toast continues to
work as before
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: propagate fallback browser loadURL failures
Per Codex P2: openFallbackBrowser swallowed loadURL rejections by
attaching a .catch that only logged, so any caller using
tryOpenExternalWithFallback as a success signal saw an opened window
as success even when the page failed to load. OAuth flows would then
wait for the downstream callback timeout instead of canceling early
on malformed or unreachable URLs.
openFallbackBrowser now returns { window, loaded } where `loaded` is
the raw loadURL Promise, and tryOpenExternalWithFallback awaits it in
the fallback path. On initial load failure, the broken window is
closed and the original shell.openExternal error is re-thrown.
The internal popup handler inside the fallback window keeps its
fire-and-forget behavior (it must return synchronously) but now
explicitly catches the loaded rejection to avoid unhandledRejection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Auto-discover and use default SSH keys (~/.ssh/id_ed25519, id_ecdsa, id_rsa) when no credentials configured
- Add Windows SSH Agent service status detection with UI warning
- Remove file extension filter on key import to allow any file type
- Unify Host Details panel card styles with consistent icons and titles
Co-Authored-By: Claude <noreply@anthropic.com>
Refactors the settings interface by deleting the monolithic modal dialog and splitting settings into dedicated tabs and components, improving maintainability and user experience. Enables application info retrieval through a new IPC bridge for use in About/Application screens. Streamlines code by removing redundant state and logic tied to the old settings modal.