Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a95e4fb
perf(extensions): cap manifest/bundle caches with LRU eviction
cegomez-gr Jul 16, 2026
a3dfd28
perf(main): stop pre-warming window-manager-worker at startup
cegomez-gr Jul 16, 2026
1922a7d
perf(main): auto-shutdown idle transcription servers after 10 minutes
cegomez-gr Jul 16, 2026
c63e3e7
perf(main): make extension discovery and commands cache non-blocking
cegomez-gr Jul 16, 2026
4187158
fix(build): unpack extract-file-icon and node-gyp-build from asar
cegomez-gr Jul 16, 2026
c4b808f
fix(main): drain in-flight extension/script work before quitting
cegomez-gr Jul 16, 2026
4f1696d
fix: install esbuild's macOS platform binaries as optional dependencies
cegomez-gr Jul 16, 2026
95c2aef
feat: add Confetti, Fireworks, Snow, and Rain effect commands
cegomez-gr Jul 16, 2026
eb37e4c
feat: add settings, extension store, updates, and login item to tray …
cegomez-gr Jul 17, 2026
befebaf
fix: don't kill transcription servers mid-request on idle-shutdown
cegomez-gr Jul 17, 2026
780919b
fix(extension-runner): clear lastBuildError after a successful rebuild
cegomez-gr Jul 17, 2026
350260e
feat(main): add opt-in heap diagnostics for the main process
cegomez-gr Jul 17, 2026
a3fa918
fix(window-manager): stop swallowing worker stdout/stderr
cegomez-gr Jul 20, 2026
97a960d
fix(build): unpack extract-file-icon and node-gyp-build from asar
cegomez-gr Jul 20, 2026
fd2403b
fix(main): raise V8 old-space limit to avoid heap-ceiling SIGTRAP abort
cegomez-gr Jul 20, 2026
cec2a6c
Merge branch 'feat/feat-confetti' into feat/fix-review-crash
cegomez-gr Jul 20, 2026
d57fe05
Merge branch 'feat/Fix-shoutdown' into feat/fix-review-crash
cegomez-gr Jul 20, 2026
02c19d0
fix(main): actually raise the main process's V8 heap ceiling via re-exec
cegomez-gr Jul 21, 2026
0c22c50
fix(file-search-index): stop OOM spikes from full-home-directory rein…
cegomez-gr Jul 21, 2026
3bbf2c2
docs: add session handoff for the crash investigation
cegomez-gr Jul 22, 2026
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
142 changes: 142 additions & 0 deletions handoffs/current.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Current Handoff

## Current Status

Investigating a recurring `EXC_BREAKPOINT`/`SIGTRAP` crash on `CrBrowserMain`
(main process). Confirmed root cause: `file-search-index.ts` rebuilds a
~1.1M-entry home-directory index on an interval, and previously kept the old
snapshot alive while building the new one — a multi-GB spike that blows any
V8 heap ceiling. Fix is written, compiled, and packaged into the latest DMG,
but **not yet confirmed working** — the last test run showed old (pre-fix)
behavior, most likely because a stale process/build was tested rather than
the new one. Branch `feat/fix-review-crash` also now contains two previously
orphaned, unmerged upstream branches (see Decisions).

## Completed Work

- **`lastBuildError` leak** (`extension-runner.ts`): now cleared on successful
rebuild instead of growing forever.
- **Window-manager worker fixes**: stopped silencing its stdout/stderr
(`main.ts`), which surfaced the real bug — `extract-file-icon` /
`node-gyp-build` weren't in `asarUnpack`, so `node-window-manager` failed to
load in packaged builds (`package.json`).
- **V8 heap ceiling fix, corrected**: first attempt
(`app.commandLine.appendSwitch('js-flags', ...)`) confirmed NOT to reach the
main process (only affects Chromium renderer processes). Second attempt
(`v8.setFlagsFromString`) confirmed ineffective too (heap_size_limit is
fixed at isolate creation). Working fix: re-exec the process once with
`--js-flags=--max-old-space-size=4096` on argv (env-marker guarded against
looping). Verified live: process reports `~4096MB` limit.
- **Diagnostics added** (`SC_HEAP_DEBUG=1` env gate): periodic
`process.memoryUsage()` log every 5 min, heap snapshot capture via
`Cmd+Alt+Shift+H` or `SIGUSR2`, written to `/tmp` (not `~/Library/Logs` —
that directory has repeatedly lost files minutes after being written,
cause unconfirmed, treat as unreliable for this investigation).
- **Recovered two orphaned upstream branches** (both had open PRs, neither
merged into `main`, which itself is stalled at `2da7b9e`) and merged both
into `feat/fix-review-crash`, cleanly, no conflicts:
- `feat/Fix-shoutdown` (PR #630): LRU-cache extension bundles, stop
pre-warming window-manager-worker, non-blocking discovery, idle
transcription server shutdown (+ mid-request-kill bugfix), drain
in-flight extension/script work before quit (this one has its own ADR
diagnosing a *different* EXC_BREAKPOINT/SIGTRAP cause — a Node callback
completing after Electron tears down V8 on quit; not yet confirmed
whether it's still occurring separately from the FileIndex issue).
- `feat/feat-confetti` (PR #631): expanded tray menu (Settings, Extension
Store, Launch at Login, Check for Updates), Confetti/Fireworks/Snow/Rain
commands, esbuild moved to `optionalDependencies` (fixes `EBADPLATFORM`
on `npm install` for single-arch Macs).
- Pushed today's 4 relevant commits (lastBuildError, heap diagnostics,
window-manager stdio, heap-limit fix) onto `feat/Fix-shoutdown` and to
origin, updating PR #630 live (11 commits now).
- **`file-search-index.ts` fix** (latest commit, root-cause fix):
- Old snapshot dropped (`activeIndex = null`) before building the new one,
instead of after — eliminates the double-memory rebuild spike. Trade-off:
search returns empty during the rebuild window.
- `MAX_INDEX_ENTRIES` 1.2M → 400k; `DEFAULT_REFRESH_INTERVAL_MS` 8min →
60min.
- Added a manual `system-rebuild-file-index` command
(`commands.ts`/`executeCommand`) for on-demand refresh.
- Compiled binary verified to contain `400000` (not `1200000`) — the fix is
genuinely in the shipped DMG.

## Work In Progress

- **Confirming the FileIndex fix actually resolves the crash.** Last test run
showed 1.1M entries and sub-60min interval rebuilds — i.e. old behavior —
strongly suggesting a stale process or pre-fix install was tested, not that
the fix failed. User was about to redo the test cleanly (kill all
processes, reinstall from `~/Downloads/SuperCmd-1.0.26-arm64.dmg`, verify
`grep -ao "400000" .../app.asar` before launching).

## Pending Work

1. Re-verify the FileIndex fix with a clean process + confirmed-current
build, ideally past the ~48–96min window where every prior crash occurred.
2. If it still crashes: get another `[HeapDebug]`/`[FileIndex]` log + `.ips`
from `~/Library/Logs/DiagnosticReports/` (read it immediately, it may
disappear) and check whether the heap trend shows another single big
jump (another oversized structure) or a slow genuine leak this time.
3. If it holds: consider whether `feat/Fix-shoutdown`'s quit-time race
condition (drain-before-quit, already merged) is a second, independent,
much rarer cause — no direct evidence yet that it's firing separately.
4. Two open upstream PRs (#630, #631) are `MERGEABLE` but `BLOCKED` on
review — no CI configured on this repo, no human reviewer assigned.
`main` itself has been stalled at `2da7b9e` this whole time; consider
whether that's expected or itself a process gap worth raising.
5. Optional, not started: `global.gc()` periodic-forcing was proposed as a
heap mitigation but deferred by user request ("ya veremos") in favor of
chasing the FileIndex root cause first.

## Blockers

- None technical. Confirmation is blocked only on the user's next test run.

## Decisions

- Bring both orphaned branches into `feat/fix-review-crash` rather than
leave them stranded or merge straight to `main` (user's call, asked
explicitly).
- Accept a brief "search returns nothing" window during FileIndex rebuilds
in exchange for not doubling peak memory (user's call, picked this option
explicitly over alternatives).
- Diagnostics write to `/tmp`, not `~/Library/Logs` — that directory has
been unreliable mid-session (files vanishing) independent of anything in
this codebase; don't rely on it for future debugging in this repo either.

## Risks

- The ~48–96min crash timing has been remarkably consistent across multiple
different fixes (heap ceiling raised, FileIndex capped) — if the next test
still crashes around that window, the FileIndex theory may be wrong or
incomplete, and the quit-time-race theory (already fixed via
drain-before-quit) or a third, unfound cause should be considered before
assuming "still the same bug."
- `git log --oneline` output was momentarily misleading mid-session (looked
stale after a `cherry-pick --abort`) — cross-checked via `rev-parse`/
`cat-file` and it was fine, but worth using low-level plumbing commands to
double check if branch state ever looks wrong again in this repo.
- Multiple stale/orphaned SuperCmd processes have repeatedly confused test
results this session (old tray menu, "app still runs after deleting it",
and likely the last FileIndex test). Always confirm zero running processes
and verify the installed binary's content (e.g. `grep -ao` a known string)
before trusting a test result in this project going forward.

## Next Actions

- Wait for user's clean re-test of the FileIndex fix.
- Depending on result, either close out the crash investigation or resume
hunting (see Pending Work #2–3).

## References

- Crash reports (may vanish from disk, read immediately if consulted):
`~/Library/Logs/DiagnosticReports/SuperCmd-2026-07-{17,20,21}-*.ips`
- `src/main/file-search-index.ts`, `src/main/extension-runner.ts`,
`src/main/main.ts`, `src/main/commands.ts`
- Upstream: `SuperCmdLabs/SuperCmd` PR #630, PR #631
- ADR (via `codebase-memory-mcp`, project
`Users-cgomez-orca-workspaces-SuperCmd-Fix-shoutdown`): quit-time-race
diagnosis for the drain-before-quit fix
- Local DMG builds: `~/Downloads/SuperCmd-1.0.26-arm64.dmg` (rebuilt several
times today — always verify MD5/content before trusting a test against it)
8 changes: 6 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@
},
"dependencies": {
"@aptabase/electron": "^0.3.1",
"@esbuild/darwin-arm64": "0.19.12",
"@esbuild/darwin-x64": "0.19.12",
"@phosphor-icons/react": "^2.1.10",
"@raycast/api": "^1.104.5",
"electron-builder-notarize": "^1.5.2",
Expand All @@ -57,6 +55,10 @@
"react-dom": "^18.2.0",
"transliteration": "^2.6.1"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "0.19.12",
"@esbuild/darwin-x64": "0.19.12"
},
"build": {
"appId": "com.supercmd.app",
"productName": "SuperCmd",
Expand All @@ -76,6 +78,8 @@
"node_modules/@esbuild/**",
"node_modules/node-edge-tts/**",
"node_modules/node-window-manager/**",
"node_modules/extract-file-icon/**",
"node_modules/node-gyp-build/**",
"node_modules/electron-liquid-glass/**"
],
"files": [
Expand Down
3 changes: 1 addition & 2 deletions scripts/measure-extension-bundle-cache.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,7 @@ async function main() {

const menuBar = await measure('repeated-menu-bar-background-prep', fixture.extDir, async () => {
for (let i = 0; i < iterations; i++) {
const commands = runner
.discoverInstalledExtensionCommands()
const commands = (await runner.discoverInstalledExtensionCommands())
.filter((command) => command.mode === 'menu-bar');
assert.equal(commands.length, 1);
const bundle = await runner.getExtensionBundle(commands[0].extName, commands[0].cmdName);
Expand Down
100 changes: 88 additions & 12 deletions src/main/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import { app } from 'electron';
import { exec, execFile, spawn } from 'child_process';
import { promisify } from 'util';
import { rebuildFileSearchIndex } from './file-search-index';
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
Expand Down Expand Up @@ -67,6 +68,22 @@ const SHUTDOWN_ICON_DATA_URL = svgToBase64DataUrl(
'<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none"><defs><linearGradient id="sdBg" x1="12" y1="10" x2="52" y2="54" gradientUnits="userSpaceOnUse"><stop stop-color="#fca5a5" stop-opacity="0.7"/><stop offset="1" stop-color="#dc2626" stop-opacity="0.82"/></linearGradient></defs><rect x="8" y="8" width="48" height="48" rx="15" fill="url(#sdBg)"/><g transform="translate(18,18) scale(1.167)" stroke="rgba(255,255,255,0.92)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"/><line x1="12" y1="2" x2="12" y2="12"/></g></svg>'
);

const CONFETTI_ICON_DATA_URL = svgToBase64DataUrl(
'<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none"><defs><linearGradient id="cfBg" x1="12" y1="10" x2="52" y2="54" gradientUnits="userSpaceOnUse"><stop stop-color="#f0abfc" stop-opacity="0.7"/><stop offset="1" stop-color="#a21caf" stop-opacity="0.82"/></linearGradient></defs><rect x="8" y="8" width="48" height="48" rx="15" fill="url(#cfBg)"/><g transform="translate(18,18) scale(1.167)" stroke="rgba(255,255,255,0.92)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"><path d="M4 20 12 6 20 10 8 16Z"/><path d="M12 6c1-1.2 2.6-1.8 4-1.4"/><circle cx="21" cy="3" r="0.9" fill="rgba(255,255,255,0.92)" stroke="none"/><circle cx="17" cy="1.5" r="0.9" fill="rgba(255,255,255,0.92)" stroke="none"/><circle cx="23" cy="7" r="0.9" fill="rgba(255,255,255,0.92)" stroke="none"/></g></svg>'
);

const FIREWORKS_ICON_DATA_URL = svgToBase64DataUrl(
'<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none"><defs><linearGradient id="fwBg" x1="12" y1="10" x2="52" y2="54" gradientUnits="userSpaceOnUse"><stop stop-color="#fde047" stop-opacity="0.7"/><stop offset="1" stop-color="#a16207" stop-opacity="0.82"/></linearGradient></defs><rect x="8" y="8" width="48" height="48" rx="15" fill="url(#fwBg)"/><g transform="translate(18,18) scale(1.167)" stroke="rgba(255,255,255,0.92)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"><line x1="12" y1="22" x2="12" y2="14"/><line x1="12" y1="2" x2="12" y2="6"/><line x1="5" y1="9" x2="8" y2="10.5"/><line x1="19" y1="9" x2="16" y2="10.5"/><line x1="7" y1="4" x2="9.5" y2="7"/><line x1="17" y1="4" x2="14.5" y2="7"/><circle cx="12" cy="10" r="1.1" fill="rgba(255,255,255,0.92)" stroke="none"/></g></svg>'
);

const SNOW_ICON_DATA_URL = svgToBase64DataUrl(
'<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none"><defs><linearGradient id="snBg" x1="12" y1="10" x2="52" y2="54" gradientUnits="userSpaceOnUse"><stop stop-color="#a5f3fc" stop-opacity="0.7"/><stop offset="1" stop-color="#0e7490" stop-opacity="0.82"/></linearGradient></defs><rect x="8" y="8" width="48" height="48" rx="15" fill="url(#snBg)"/><g transform="translate(18,18) scale(1.167)" stroke="rgba(255,255,255,0.92)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="2" x2="12" y2="22"/><line x1="4.2" y1="7" x2="19.8" y2="17"/><line x1="19.8" y1="7" x2="4.2" y2="17"/></g></svg>'
);

const RAIN_ICON_DATA_URL = svgToBase64DataUrl(
'<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none"><defs><linearGradient id="rnBg" x1="12" y1="10" x2="52" y2="54" gradientUnits="userSpaceOnUse"><stop stop-color="#a5b4fc" stop-opacity="0.7"/><stop offset="1" stop-color="#3730a3" stop-opacity="0.82"/></linearGradient></defs><rect x="8" y="8" width="48" height="48" rx="15" fill="url(#rnBg)"/><g transform="translate(18,18) scale(1.167)" stroke="rgba(255,255,255,0.92)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"/><line x1="8" y1="19" x2="6.5" y2="22"/><line x1="13" y1="19" x2="11.5" y2="22"/></g></svg>'
);

export interface CommandInfo {
id: string;
title: string;
Expand Down Expand Up @@ -129,19 +146,21 @@ function getCommandsDiskCachePath(): string {
return commandsDiskCachePath;
}

function loadCommandsDiskCache(): CommandInfo[] | null {
async function loadCommandsDiskCache(): Promise<CommandInfo[] | null> {
try {
const raw = fs.readFileSync(getCommandsDiskCachePath(), 'utf-8');
const raw = await fs.promises.readFile(getCommandsDiskCachePath(), 'utf-8');
const parsed = JSON.parse(raw) as { version: number; commands: CommandInfo[] };
if (parsed?.version !== COMMANDS_DISK_CACHE_VERSION) return null;
const cmds = parsed.commands;
// Re-attach icons from the per-app icon disk cache (fast file reads).
for (const cmd of cmds) {
if (cmd.path) {
const icon = getCachedIcon(cmd.path);
if (icon) cmd.iconDataUrl = icon;
}
}
// Re-attach icons from the per-app icon disk cache. Reading them
// concurrently (fs.promises, backed by libuv's threadpool) instead of one
// fs.readFileSync per command avoids serializing every icon read on the
// main thread right before the first window is created.
await Promise.all(cmds.map(async (cmd) => {
if (!cmd.path) return;
const icon = await getCachedIconAsync(cmd.path);
if (icon) cmd.iconDataUrl = icon;
}));
return cmds;
} catch {
return null;
Expand All @@ -163,8 +182,8 @@ function saveCommandsDiskCache(commands: CommandInfo[]): void {
}

/** Call once after app.whenReady() to pre-populate the in-memory cache from disk. */
export function initCommandsCache(): void {
const cmds = loadCommandsDiskCache();
export async function initCommandsCache(): Promise<void> {
const cmds = await loadCommandsDiskCache();
if (cmds) {
cachedCommands = cmds;
staleCommandsFallback = cmds;
Expand Down Expand Up @@ -207,6 +226,15 @@ function getCachedIcon(bundlePath: string): string | undefined {
return undefined;
}

async function getCachedIconAsync(bundlePath: string): Promise<string | undefined> {
try {
const cacheFile = path.join(getIconCacheDir(), `${iconCacheKey(bundlePath)}.b64`);
return await fs.promises.readFile(cacheFile, 'utf-8');
} catch {
return undefined;
}
}

function setCachedIcon(bundlePath: string, dataUrl: string): void {
try {
const cacheFile = path.join(getIconCacheDir(), `${iconCacheKey(bundlePath)}.b64`);
Expand Down Expand Up @@ -1308,13 +1336,51 @@ async function discoverAndBuildCommands(): Promise<CommandInfo[]> {
keywords: ['emoji', 'picker', 'trigger', 'smiley', 'emoticon'],
category: 'system',
},
{
id: 'system-confetti',
title: 'Confetti',
subtitle: 'Celebrate with a burst of confetti',
keywords: ['confetti', 'celebrate', 'party', 'fun', 'congrats', 'congratulations', 'celebration'],
iconDataUrl: CONFETTI_ICON_DATA_URL,
category: 'system',
},
{
id: 'system-fireworks',
title: 'Fireworks',
subtitle: 'Launch a fireworks show',
keywords: ['fireworks', 'rockets', 'celebrate', 'party', 'fun', 'show', 'pyrotechnics'],
iconDataUrl: FIREWORKS_ICON_DATA_URL,
category: 'system',
},
{
id: 'system-snow',
title: 'Snow',
subtitle: 'Let it snow on your screen',
keywords: ['snow', 'snowfall', 'winter', 'christmas', 'fun', 'weather'],
iconDataUrl: SNOW_ICON_DATA_URL,
category: 'system',
},
{
id: 'system-rain',
title: 'Rain',
subtitle: 'Make it rain on your screen',
keywords: ['rain', 'rainfall', 'storm', 'weather', 'fun'],
iconDataUrl: RAIN_ICON_DATA_URL,
category: 'system',
},
{
id: 'system-reset-launcher-position',
title: 'Reset Launcher Position',
keywords: ['reset', 'position', 'center', 'move', 'launcher', 'window', 'default'],
iconDataUrl: RESET_POSITION_ICON_DATA_URL,
category: 'system',
},
{
id: 'system-rebuild-file-index',
title: 'Rebuild File Search Index',
keywords: ['rebuild', 'refresh', 'reindex', 'file', 'search', 'index', 'supercmd'],
category: 'system',
},
{
id: 'system-open-settings',
title: 'SuperCmd Settings',
Expand Down Expand Up @@ -1849,7 +1915,7 @@ async function discoverAndBuildCommands(): Promise<CommandInfo[]> {
// Installed community extensions
let extensionCommands: CommandInfo[] = [];
try {
extensionCommands = discoverInstalledExtensionCommands().map((ext) => ({
extensionCommands = (await discoverInstalledExtensionCommands()).map((ext) => ({
id: ext.id,
title: ext.title,
subtitle: ext.extensionTitle,
Expand Down Expand Up @@ -2108,6 +2174,16 @@ export async function executeCommand(id: string): Promise<boolean> {
}
}

if (id === 'system-rebuild-file-index') {
try {
await rebuildFileSearchIndex('manual-command');
return true;
} catch (error) {
console.error('Failed to rebuild file search index:', error);
return false;
}
}

// Use stale fallback when available to avoid blocking on a fresh discovery
// while the cache is being rebuilt in the background.
const commands = cachedCommands ?? staleCommandsFallback ?? await getAvailableCommands();
Expand Down
Loading