Bugs found during the 2026-06-12 architecture review. Each one was verified against the code by hand, not just flagged by a scanner.
Status: all 8 verified bugs fixed on 2026-06-12. Fix notes inline per bug. The suspected section is untouched and still open.
- Where:
src/main/index.ts(nosetWindowOpenHandleranywhere) +src/renderer/components/terminal/TerminalInstance.tsx:198(new WebLinksAddon()) - What happens: WebLinksAddon's default activate handler calls
window.open(uri). The main process never installs awebContents.setWindowOpenHandler, so Electron creates a brand-new bare BrowserWindow for any URL clicked in a terminal. No menu, no preload, and arbitrary remote content running inside the app instead of the default browser. - Also: there is no
will-navigateguard, so a stray navigation in the renderer would replace the app UI. - Severity: high (security posture + UX)
- Suggested direction:
setWindowOpenHandlerreturning{ action: 'deny' }+shell.openExternal(url)for http/https, plus awill-navigatepreventDefault. - Fixed:
setWindowOpenHandlerdenies all popups and routes http/https toshell.openExternal;will-navigateis blocked outside the app origin (dev server URL orfile://) and external http/https links open in the system browser.
- Where:
src/main/services/pty-manager.ts:65-67env.GIT_CONFIG_COUNT = '1' env.GIT_CONFIG_KEY_0 = 'credential.helper' env.GIT_CONFIG_VALUE_0 = 'osxkeychain'
- What happens: this env is applied to every PTY on every platform, but the app ships Linux (AppImage/deb) and Windows (nsis) builds. On those platforms git will try to run the nonexistent
git-credential-osxkeychainhelper, printing warnings and breaking the user's configured credential helper. - Severity: medium (broken git auth UX on Linux/Windows)
- Fixed: the three
GIT_CONFIG_*env vars are only set whenos.platform() === 'darwin'.
- Where:
src/renderer/components/terminal/TerminalInstance.tsx:247-252 - What happens: terminals are intentionally cached in the module-level
terminalsMap, andonIncomingDatais only created once, at terminal creation. That closure capturesel(the container div from the first mount). When the tab remounts elsewhere (layout switch, tab move), the xterm element is re-parented into a new container, but the closure still reads the old detached div:After any remount,const hidden = el.offsetParent === null // always true once the old div is detached
hiddenis permanentlytrue, so theautoScroll && (atBottom || hidden)branch always scrolls to bottom — the "don't yank the scroll position while reading history" behavior silently breaks. - Severity: medium
- Note: same pattern means a changed
initialCommand/cwdprop would also be ignored by the cached closure, but in practice those never change for a tab. Theelcapture is the part that bites. - Fixed: the hidden check now reads
terminal.element?.offsetParent— the xterm root element is re-parented into the live container on remount, so it tracks the current DOM instead of the first-mount div.
- Where:
src/main/services/pty-manager.ts:68-75const loginPath = execSync('/bin/bash -ilc "echo $PATH"', ...)
- What happens:
execSyncwith-i(interactive) blocks the main process for however long the user's bashrc takes (nvm users: easily 500ms+), and it runs once per terminal creation, not cached. Every new terminal can freeze the whole app for that duration. On Windows/bin/bashdoesn't exist and this throws every time (caught, but still spawns a failing process per terminal). - Severity: medium (main-process jank on every terminal open)
- Fixed: probe result is cached at module level (one probe per app run, success or failure), skipped entirely on win32, and given a 5s timeout.
- Where:
src/renderer/hooks/useQueueRunner.ts:21-36 - What happens: two related defects:
lastDispatchAtRefis a single ref shared by all tabs. Dispatching on tab A blocks dispatch on tab B for 2s even though they are independent terminals.- When the effect bails on the throttle (
Date.now() - last < 2000), nothing re-triggers it. If no store state changes in the next render cycle, the queued item sits until some unrelated state change re-runs the effect.
- Severity: low-medium (queue items occasionally need a "nudge")
- Fixed: throttle is now a per-tab
Map; when the effect bails on the cooldown it schedules a timeout that re-triggers the effect, so queued items no longer wait for an unrelated state change.
- Where:
src/renderer/components/sidebar/FileTree.tsx(handleInlineSubmit) - What happens: if
fs.createFile/createFolderrejects (permissions, name collision with invalid chars), the error goes toconsole.erroronly andsetInlineInput(null)still closes the input. To the user the create just silently does nothing. (Tree refresh itself is fine — the main-process watcher pushesfs:tree-changed.) - Severity: low (UX)
- Fixed: on create/rename failure the inline input stays open, refocuses, and shows the error message (IPC prefix stripped) in red below the field; Escape dismisses.
- Where:
src/main/index.ts:75if (input.meta && input.alt && /^[1-9]$/.test(input.key))
- What happens: on macOS, Option+digit produces a symbol character in
key(e.g. Option+1 →¡), not the digit.input.codewould beDigit1. Whether this fires depends on keyboard layout; on most layouts the switch-project shortcut never matches. - Severity: low (feature likely dead on many layouts)
- Fixed: matches on
input.code(Digit1–Digit9) instead ofinput.key, layout-independent.
- Where:
src/main/ipc/terminal.ts(terminal:write),src/main/services/pty-manager.ts:148-150 - What happens:
writePtysilently no-ops when the tab has no PTY (e.g. PTY died, renderer still thinks it's alive). Queue runner and menu actions (/clear,/commit) write into the void with no error signal, and the queue item is already dequeued — the prompt is lost. - Severity: low-medium (lost queue items when a PTY died underneath a tab)
- Fixed:
writePty/terminal:writenow return a boolean; the queue runner checksterminal.has(tabId)before dequeuing, so an item targeting a dead PTY stays queued instead of being lost.
Statistics.tsx builds sessions independently in sessions, todayMs, and heatmap memos. Not a correctness bug today (deps are complete), but the triple computation is heavy with large histories, and the three code paths can drift. Addressed structurally in the refactor (single memoized source), with identical outputs.
DiffPanel.tsx:543-545 relies on the next effect run to clear zones. In every path I traced the zones are cleared (or the editor unmounts), but it is fragile against future edits. Worth tightening when we touch DiffPanel after the binary-preview work lands.
CommandPalette.tsx scores substring hits at ~1000 and fuzzy hits at ~1-5 per char, so a long path containing the query as a substring outranks a much better fuzzy match. Ranking choice, not a crash — flagging in case results feel wrong in practice.
window-all-closed/before-quit call flush/compact helpers without awaiting; if the activity/token flush does async I/O at quit, the write can be cut off. The current implementations are sync writes, so it holds today — but it's load-bearing on that assumption.
git-fetch-service.ts fetches every registered project concurrently. With many projects this spikes processes/network at once. Throttle/queue if users report fan noise on fetch ticks.