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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions