Skip to content

fix(orchestration): don't steal active-tab focus when a director spawns a worker via the CLI - #35

Merged
zaridan merged 2 commits into
mainfrom
zaridan/fix-orch-worker-create-focus-steal
Jun 24, 2026
Merged

fix(orchestration): don't steal active-tab focus when a director spawns a worker via the CLI#35
zaridan merged 2 commits into
mainfrom
zaridan/fix-orch-worker-create-focus-steal

Conversation

@zaridan

@zaridan zaridan commented Jun 24, 2026

Copy link
Copy Markdown
Owner

The bug (CLI→IPC focus-steal gap #29 missed)

An in-app Orcastrator (Smart director) spawns each worker by running orca worktree create --agent … from its own terminal. In the CLI create handler, any --agent forced activate: true:

activate: flags.get('activate') === true || flags.get('run-hooks') === true || Boolean(startupAgent),

That flowed activate: trueworktree.create RPC → createManagedWorktree's shouldActivatenotifyActivateWorktreeui:activateWorktree IPC → renderer setActiveWorktree, switching the user's active tab to every new worker as the director created it.

#29 added suppressActivation to the in-app renderer launch functions, but never covered this CLI → IPC path.

The fix — auto-suppress when the create originates from a director worktree

When orca worktree create runs from within an Orcastrator/director worktree (the director spawning a worker), the agent/run-hooks-derived activation is suppressed: activate: false is sent, so notifyActivateWorktree is simply never called. The worktree is still created, its agent spawned, and revealed in the sidebar + Mission Control — only the active-tab switch is skipped.

  • Normal (non-director) origin → still activates, exactly as today.
  • Explicit --activateoverrides the suppression (still activates).

Which layer & why

CLI layer — the smallest change that reuses the existing main-side shouldActivate gate with no IPC/renderer changes and no focus-steal because notifyActivateWorktree isn't called. The CLI is also the only layer that can distinguish an explicit --activate from the derived --agent/--run-hooks activation, which is required for the override rule.

Director identity is the durable worktree displayName prefix "Orcastrator · " (the same restart-surviving marker the sidebar uses to reattach directors). It's moved to src/shared/orchestrator-identity.ts so the CLI and renderer share one constant. The CLI already resolves the originating worktree from cwd for lineage; that same lookup now also reports whether the origin is a director — no extra round-trip.

Tests (fail without the fix)

In src/cli/index.test.ts:

  • Director origin + --agentactivate: false but still created/revealed (this one fails on main).
  • Normal origin + --agentactivate: true (no regression).
  • Director origin + --agent --activateactivate: true (explicit override wins).

Verification

typecheck ✓ · pnpm test (20504 passed; the only failures were 2 flaky node-pty-fd-leak native tests that pass in isolation, unrelated) ✓ · oxlint on changed files ✓ · build:cli + electron-vite build ✓

🤖 Generated with Claude Code

zaridan and others added 2 commits June 24, 2026 12:11
…ns a worker via the CLI

#29 stopped in-app renderer launches from stealing the user's active tab, but
missed the CLI→IPC path: an in-app Orcastrator spawns each worker by running
`orca worktree create --agent …` from its own terminal. `--agent` forces
`activate: true`, so every worker switched the user's active tab — yanking them
away from their work.

Fix at the CLI layer (smallest, reuses the existing main-side `shouldActivate`
gate — no IPC/renderer change): when the create originates from a director
worktree, suppress the agent/run-hooks-derived activation so `notifyActivateWorktree`
is never called. The worktree is still created, its agent spawned, and revealed
in the sidebar + Mission Control — only the active-tab switch is skipped. A normal
(non-director) origin still activates as before, and an explicit `--activate`
overrides the suppression.

Director identity is the durable worktree displayName prefix ("Orcastrator · "),
moved to src/shared/orchestrator-identity.ts so the CLI and renderer share one
constant. The CLI already resolves the originating worktree from cwd for lineage;
that same lookup now also reports whether the origin is a director, so no extra
round-trip is added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt --repo

The first pass only computed `originIsOrchestrator` inside the cwd parent/repo-
inference block, gated by `(!explicitParentWorktree && !explicitParentWorkspace
&& !noParent) || needsCwdRepoInference`. The real /orcastrate pattern spawns
workers parent-less (parent=None) with an explicit `--repo`, which skips that
block — so suppression never applied and the focus-steal still happened.

Make the orchestrator-origin suppression decision independent of the parent/repo
gate: whenever activation would be derived (run-hooks or startupAgent) and
`--activate` was not explicitly passed, resolve the cwd worktree context to learn
whether the origin is a director. Reuse the context already resolved by the gated
block when it ran; otherwise do one extra `resolveCurrentWorktreeContext` call —
guarded so it is skipped when activation wouldn't be derived anyway (no extra RPC
on the common non-agent create path).

Tests: `--no-parent --repo --agent` from a director cwd → activate:false;
the same from a normal cwd → activate:true.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zaridan

zaridan commented Jun 24, 2026

Copy link
Copy Markdown
Owner Author

Revision: close the --no-parent --repo coverage gap

The first pass computed originIsOrchestrator only inside the cwd parent/repo-inference block (gated by (!explicitParentWorktree && !explicitParentWorkspace && !noParent) || needsCwdRepoInference). The real /orcastrate pattern spawns workers parent-less (parent=None) with an explicit --repo, which skips that block — so originIsOrchestrator stayed false and the focus-steal still happened.

Fix: the suppression decision is now independent of the parent/repo gate. Whenever activation would be derived (--run-hooks / --agent) and --activate was not explicitly passed, the cwd worktree context is resolved to determine director-ness. It reuses the context already resolved by the gated block when that ran; otherwise it does one extra resolveCurrentWorktreeContext call — guarded so there's no extra RPC on the common non-agent create path.

Added tests: --no-parent --repo --agent from a director cwd → activate:false; the same from a normal cwd → activate:true. All existing CLI tests stay green (134 total).

@zaridan
zaridan merged commit 7e80060 into main Jun 24, 2026
6 checks passed
@zaridan
zaridan deleted the zaridan/fix-orch-worker-create-focus-steal branch June 24, 2026 19:29
zaridan added a commit that referenced this pull request Jun 25, 2026
#29 added a `suppressActivation` flag to keep a director from yanking the
user's active tab when a worker is spawned programmatically. That flag was
also applied to `launchOrchestratorForProject`, which is reached only by the
user clicking "+" in the ORCASTRATORS sidebar section — a deliberate, manual
action that SHOULD focus the new window, exactly like the "+" for a regular
worktree. As a result, opening a new Orcastrator silently left the user on
their current worktree.

User-facing change: opening a new Orcastrator now activates (focuses) it.

- orchestrator-launch.ts: stop passing `suppressActivation: true` to
  `activateAndRevealWorktree`.
- director-worktree-shell.ts: drop the trailing `{ suppressActivation: true }`
  on the store create so the host-side (SSH/remote) `activate` gate also
  focuses the new director. `launchOrchestratorForProject` is its only caller,
  so no per-caller threading is needed.

Worker-spawn focus suppression is untouched: the CLI worker-spawn path (#35)
and any renderer worker-create path still suppress activation. A director
spawning a WORKER must still not steal focus.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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