Skip to content

Make CLI launch spinners actually animate#62

Merged
willwashburn merged 2 commits intomainfrom
relayfile-async-mount
May 8, 2026
Merged

Make CLI launch spinners actually animate#62
willwashburn merged 2 commits intomainfrom
relayfile-async-mount

Conversation

@willwashburn
Copy link
Copy Markdown
Member

Summary

The CLI launch spinners (install + sandbox-mount setup) were frozen on their first frame. This bundles three changes that fix it end-to-end:

  • runInstallWithSpinner: replace spawnSync with async spawn + Promise so ora's setInterval frame timer can fire while the install runs. runInstall / runInstallOrThrow become async; call sites await.
  • Add a setup spinner around the sandbox-mount setup phase (createMount + git config + optional in-mount install + config-file writes + autosync start). Final .succeed("Sandbox mount ready → ...") just before the harness child spawns. Wired into catch/finally so setup errors don't leak a hung spinner.
  • Bump @relayfile/local-mount from ^0.6.15 to ^0.7.0 and await createMount(...). 0.7.0 made createMount async and yields with setImmediate between directory entries (relayfile#104) — without this, the setup spinner would still be frozen during the initial mirror.

Why all three together

Each fix addresses a distinct event-loop block:

Where Block source Fix
Install spinner spawnSync async spawn
Setup spinner: animation sync createMount walk relayfile 0.7 async + await
Setup spinner: existence (no spinner at all) add ora around the setup phase

Shipping any subset would leave at least one spinner still frozen.

API impact

MountHandle interface is unchanged in 0.7.0 — syncBack, startAutoSync, cleanup keep the same shape. The only consumer-visible change is createMount returning Promise<MountHandle> instead of MountHandle, handled with one await.

Test plan

  • pnpm -r build clean
  • pnpm -r test (130 CLI tests + workspace tests, all green)
  • Manual: run agentworkforce agent <persona> against a non-trivial repo and confirm both spinners animate visibly during install and during the sandbox-mount setup phase.
  • Manual: confirm Ctrl-C during the existing sync-back phase still aborts cleanly (this PR doesn't change that path).
  • Manual: confirm an install failure surfaces the buffered output via spinner.fail exactly once (no duplicate "Aborting" line).

🤖 Generated with Claude Code

Two distinct event-loop blocking issues kept the CLI launch spinners frozen
on their first frame. Both fixed here:

1. runInstallWithSpinner used spawnSync, which blocks Node's event loop for
   the entire install. Ora's frame redraw runs on a setInterval, so the
   spinner only ever painted its first frame. Switched to async spawn +
   Promise so the loop keeps ticking. runInstall and runInstallOrThrow are
   now async; both call sites await them.

2. The sandbox-mount setup phase had no spinner at all — just a static
   "• sandbox mount → ..." line followed by a multi-second silent pause
   while createMount mirrored the project tree. Added a setup spinner that
   covers createMount, git config, the in-mount install (paused so the
   install spinner takes over the line), config-file writes, and autosync
   start, finalizing with .succeed("Sandbox mount ready → ...") just before
   the harness child spawns. Wired into the catch/finally so a setup error
   doesn't leak a hung spinner. Moved createMount inside the try block so
   its initial-mirror failures clean up the spinner correctly.

The setup spinner only animates because @relayfile/local-mount 0.7.0
made createMount async and yields with setImmediate between directory
entries (relayfile #104). Bumped from ^0.6.15 to ^0.7.0 and switched
the call site to `await createMount(...)`. The MountHandle interface is
unchanged, so the rest of the lifecycle (syncBack / startAutoSync /
cleanup) is unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f7348081-8165-4dd1-b299-cfdbe84d2f15

📥 Commits

Reviewing files that changed from the base of the PR and between fb0137f and e2ff76b.

📒 Files selected for processing (1)
  • packages/cli/src/cli.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/cli.ts

📝 Walkthrough

Walkthrough

Bump @relayfile/local-mount to ^0.7.0; convert CLI install commands to async spawn with buffered output and promise results; add a dedicated setup spinner around awaited createMount, coordinate deferred installs with spinner lifecycle, and harden SIGINT and mount teardown handling.

Changes

Async Install and Mount Setup with Spinners

Layer / File(s) Summary
Dependency Update
packages/cli/package.json
@relayfile/local-mount bumped from ^0.6.15 to ^0.7.0.
Async Install Implementation
packages/cli/src/cli.ts
runInstallWithSpinner rewritten from spawnSync to async spawn with stdout/stderr buffering and promise-based exit code computation; runInstall and runInstallOrThrow converted to async/await.
Mount Setup Spinner
packages/cli/src/cli.ts
Mount setup phase gains an ora spinner started before createMount showing "Setting up sandbox mount…".
Mount Creation and Configuration
packages/cli/src/cli.ts
createMount is awaited inside the try block; configureGitForMount runs after mount creation; deferred installs run with the setup spinner stopped and then restarted.
SIGINT Pre-launch Handling
packages/cli/src/cli.ts
SIGINT handler refactored to handle pre-launch teardown: fail/clear spinner, remove session root, and exit with code 130 when child not yet spawned.
Child Process Spawn and Spinner Cleanup
packages/cli/src/cli.ts
Setup spinner is succeeded and cleared before spawning the child; childSpawned is set true before spawn to separate pre-launch and runtime signal handling.
Error Handling and Teardown Safety
packages/cli/src/cli.ts
On setup errors spinner is failed/cleared; finally block stops/clears spinner if present; mount teardown uses guarded handle?.cleanup().

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped where spinners hum and grow,

Async installs now skip the snow,
Mounts await with patient cheer,
Teardown tidy, signals clear. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main objective: fixing frozen CLI launch spinners by making them animate.
Description check ✅ Passed The description is directly related to the changeset, providing a detailed explanation of the three coordinated changes that fix the frozen spinners.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch relayfile-async-mount

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fb0137f9dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/cli/src/cli.ts
Comment on lines +441 to +449
let buffered = '';
child.stdout?.setEncoding('utf8');
child.stderr?.setEncoding('utf8');
child.stdout?.on('data', (chunk: string) => {
buffered += chunk;
});
child.stderr?.on('data', (chunk: string) => {
buffered += chunk;
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound installer output buffering to avoid OOM

runInstallWithSpinner now appends all child stdout/stderr into a single in-memory string with no size limit. If an install command emits very large logs (for example verbose package-manager output or repeated error traces), this can cause unbounded memory growth and terminate the CLI before it reports the actual install failure. The previous sync path had an explicit buffer cap; this async path should keep a similar limit (or truncate/stream) to avoid regressions on noisy installs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/cli/src/cli.ts (1)

1288-1318: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle Ctrl-C during pre-launch mount setup.

Installing this SIGINT listener before createMount() suppresses Node’s default Ctrl-C exit, but the handler no-ops until isSyncing becomes true. During the new setup phase there is no child process yet to receive SIGINT, so Ctrl-C is ignored while the mount is being prepared. Either register the listener only after the harness child is spawned, or make the pre-launch path fail the setup spinner, tear down the session dir, and exit 130.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/cli.ts` around lines 1288 - 1318, The SIGINT handler
(sigintHandler) is registered before createMount() but no-ops while isSyncing is
false, causing Ctrl-C to be ignored during pre-launch mount setup; fix by either
registering process.on('SIGINT', sigintHandler) only after the harness child is
spawned (so the child receives SIGINT), or change sigintHandler to handle
pre-launch teardown: if isSyncing is false, fail/stop the setup spinner
(syncSpinner), remove the session dir (rmSync(sessionRoot, { recursive: true,
force: true })), and exit(130); ensure shutdownController.abort() is still used
when aborting during sync, and keep references to sigintHandler, syncSpinner,
isSyncing, sessionRoot, shutdownController and createMount() to locate the
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/cli/src/cli.ts`:
- Around line 1288-1318: The SIGINT handler (sigintHandler) is registered before
createMount() but no-ops while isSyncing is false, causing Ctrl-C to be ignored
during pre-launch mount setup; fix by either registering process.on('SIGINT',
sigintHandler) only after the harness child is spawned (so the child receives
SIGINT), or change sigintHandler to handle pre-launch teardown: if isSyncing is
false, fail/stop the setup spinner (syncSpinner), remove the session dir
(rmSync(sessionRoot, { recursive: true, force: true })), and exit(130); ensure
shutdownController.abort() is still used when aborting during sync, and keep
references to sigintHandler, syncSpinner, isSyncing, sessionRoot,
shutdownController and createMount() to locate the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d2f69c1f-44f2-4804-afd0-4520723d9cfb

📥 Commits

Reviewing files that changed from the base of the PR and between 7a4ba1f and fb0137f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • packages/cli/package.json
  • packages/cli/src/cli.ts

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

Registering any 'SIGINT' listener suppresses Node's default exit-on-SIGINT,
so the previous no-op-when-not-syncing handler silently swallowed Ctrl-C
during pre-launch mount setup. With sync createMount this was masked by
the blocked event loop; now that relayfile 0.7's createMount yields, the
swallow is observable — Ctrl-C looks like a hang until setup finishes.

Add a `childSpawned` phase flag and split the handler into three branches:
pre-launch (fail setup spinner, rm session dir, exit 130), child running
(no-op so the child receives Ctrl-C via the TTY pgid), and syncing
(existing abort logic, unchanged). `childSpawned` flips inside the spawn
Promise executor right before `spawn()` so the window between
setup-spinner-succeed and child-spawn still falls into the pre-launch
teardown branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@willwashburn willwashburn merged commit 71d521d into main May 8, 2026
2 checks passed
@willwashburn willwashburn deleted the relayfile-async-mount branch May 8, 2026 18:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant