Skip to content

Refactor: migrate sync commandRunner.exec() callers to async #123

@parsakhaz

Description

@parsakhaz

Why

commandRunner.exec() (synchronous) is currently used in 72 places across 9 files. This blocks two things:

  1. Out-of-process execution contexts. Any future support for SSH, Docker, Lima, GitHub Codespaces, or similar requires the command to round-trip across a process boundary or network — both of which are inherently async. The sync API is a hard wall against any of those.
  2. Event loop responsiveness. execSync blocks the Electron main process. For long-running git operations (large repos), this manifests as UI freezes during diff loads, dashboard refreshes, and spotlight indexing.

This issue is a pure refactor with zero new functionality. It exists to unblock Remote SSH support and to remove a class of UI-freeze bugs.

Scope

Migrate every sync `commandRunner.exec()` call to `commandRunner.execAsync()`, cascading `async`/`await` upward through callers as needed. Per-file call counts from `grep -rn 'commandRunner.exec(' main/src`:

  • `main/src/services/gitDiffManager.ts` — 18 calls (highest risk: this is the entire diff view backend; any race condition here will manifest as a broken or stale diff view)
  • `main/src/ipc/git.ts` — 23 calls (mechanical; handlers are already declared `async`, the bodies just use sync exec for historical reasons)
  • `main/src/services/spotlightManager.ts` — 11 calls
  • `main/src/ipc/dashboard.ts` — 9 calls
  • `main/src/ipc/project.ts` — 4 calls (project init flow: `git init`, initial commit)
  • `main/src/events.ts` — 4 calls (some may be in synchronous contexts; audit each)
  • `main/src/services/executionTracker.ts` — 1 call
  • `main/src/services/gitFileWatcher.ts` — 1 call
  • `main/src/ipc/file.ts:947` — 1 call (`git:execute-project` handler)

Total: 72 calls.

Approach

  1. Walk each file in the order above (lowest-risk first, leaving `gitDiffManager.ts` for last when the pattern is fully understood).
  2. For each call site:
    • Convert `const out = commandRunner.exec(cmd, cwd)` → `const { stdout: out } = await commandRunner.execAsync(cmd, cwd)`
    • Walk up the call stack and add `async`/`await` until you hit a function that's already async (usually an IPC handler).
  3. If a caller genuinely cannot be made async (deep synchronous code path with no obvious entry point), document the constraint and leave a TODO referencing this issue. Try hard to avoid this — there should be zero exceptions.
  4. After each file is migrated, run `pnpm typecheck` and `pnpm lint` and manually smoke the affected feature before moving to the next file.

Validation

Automated

  • `pnpm typecheck` — clean
  • `pnpm lint` — clean
  • `grep -rn 'commandRunner.exec(' main/src` returns zero matches outside the `CommandRunner` class definition itself

Manual regression smoke (REQUIRED — typecheck does not prove behavior)

Test on a real local project with multiple sessions and a non-trivial git history:

  • Diff view: open a session, view a file diff, switch files, refresh — diff loads and updates correctly
  • Git log: open git history view, scroll through commits, click a commit to view its diff
  • Git status: edit a file, see uncommitted changes appear; commit it, see them clear
  • Git rebase from main: works end-to-end without errors
  • Git squash and rebase: works end-to-end
  • Dashboard refresh: open dashboard, click refresh, all counters update
  • Spotlight search: type a query, see results from worktree contents
  • Project init: create a brand new project in a fresh directory; `git init` runs, initial commit lands
  • Run command from file:execute-project handler: trigger via the script execution UI
  • GitFileWatcher: edit a file outside Pane, see the change reflected
  • No UI freezes: scroll through a session with a large diff — should remain responsive

Out of Scope

  • Any SSH-related code
  • Any new abstractions (`RemoteFs`, `ExecutionContext`, etc.)
  • Any new features
  • WSL changes (the WSL routing already works through `execAsync` and is unaffected)

Success Criteria

  • All 72 call sites migrated to async
  • Manual regression smoke passes for every feature listed above
  • Zero typecheck or lint errors
  • PR description includes a per-file summary of what was changed
  • Diff is purely a refactor — no behavior changes intended

Why This Is a Prerequisite for Remote SSH

The Remote SSH support effort needs to add an `if (sshContext)` branch to `CommandExecutor.execAsync` that delegates to a network-backed exec call. The sync version (`execSync`) cannot delegate to a network call because there's no way to block on async I/O from a sync function. Any caller that uses `commandRunner.exec()` (sync) will therefore need to either:

  1. Throw a hard error at runtime when invoked on an SSH project, or
  2. Be migrated to async

Doing the migration as part of the SSH PR couples a feature to a refactor and makes regressions impossible to bisect. Doing it as a standalone refactor lets us validate it independently and ship the SSH feature additively on top.

The Remote SSH plan is at `tmp/ready-plans/2026-04-10-remote-ssh-support.md` in the `remote-ssh-like-vs-code` branch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions