Skip to content

fix(ui): refresh + auto-switch worktrees on session idle#516

Open
omercnet wants to merge 2 commits into
NeuralNomadsAI:devfrom
omercnet:fix/059-worktree-detection-refresh
Open

fix(ui): refresh + auto-switch worktrees on session idle#516
omercnet wants to merge 2 commits into
NeuralNomadsAI:devfrom
omercnet:fix/059-worktree-detection-refresh

Conversation

@omercnet
Copy link
Copy Markdown
Contributor

@omercnet omercnet commented Jun 1, 2026

Stacked on #515 (task 058 question-reply idempotency). This PR's diff currently includes the 058 commit because it carries a baseline test fix (client-identity.ts / server-events.ts window/EventSource guards) required for the UI suite to be green. Merge #515 first; this PR's net change is the single commit fix(ui): refresh + auto-switch worktrees on session idle.

Problem

The opencode agent creates worktrees via git worktree add mid-session (outside CodeNomad's managed flow). CodeNomad cached its worktree list and never invalidated it for out-of-band git changes, so the files view stayed bound to a stale tree until a manual reload or restart.

Fix

packages/ui only — no server cache (server enumeration via git worktree list --porcelain is already live per request):

  • New debounced refreshWorktreesOnIdle(instanceId) in worktrees.ts, triggered from the existing handleSessionIdle path (no new sse-manager hook). Per-instance ~600ms trailing debounce, deduped on parent session id. Adds a reloadLoads in-flight guard to reloadWorktrees (it had none) to avoid overlap with rehydrateInstance.
  • Constrained auto-switch rule (PO-confirmed): on idle, diff the worktree list before/after reload. If exactly one new worktree appeared, auto-switch the active session's files view to it via setWorktreeSlugForParentSession (metadata-backed, per Migrate worktree mappings to session metadata #514). Guarded: switch only when the idle session is the active session AND the user hasn't manually switched in the meantime. Zero or multiple new worktrees → refresh list only, no switch (ambiguous attribution is not reliable without a per-session signal).

Why the constraint

CodeNomad cannot reliably attribute a newly-created worktree to a specific session (no per-session cwd signal). The single-new-worktree heuristic covers the common case (agent creates one worktree mid-turn) safely without yanking the user's view.

Validation

  • UI suite 56/56 pass (bun test --conditions browser) — 49 existing + 7 new
  • UI typecheck clean (tsc --noEmit), build OK
  • Tests cover: reload-on-idle, debounce coalescing, in-flight guard, single-new→switch, zero/multi→no-switch, different-active-session→no-switch

omercnet added 2 commits June 1, 2026 11:42
Eliminate opencode's "reply for unknown request" warning caused by
orphaned/stale question replies in the CodeNomad UI. Previously the
question request lifecycle lacked the idempotency and stale-guard layer
that permissions already had, so a question popup answered after a
reconnect, restart, or rapid double-submit could POST a reply for a
request the backend no longer tracked.

User-visible behavior change:
- Answering a question prompt that has already expired no longer fails
  silently or mis-routes; the UI now surfaces a localized "this prompt
  expired, send your answer as a message instead" notice (new i18n key
  toolCall.question.errors.expired added to all 7 locales).
- Rapid double-submit (Enter + button) can no longer fire two replies.
- Reconnect/rehydrate no longer re-surfaces an already-answered question.

Implementation:
- New stores/question-replies.ts ledger (markQuestionReplied /
  hasRepliedQuestion / pruneRepliedQuestions / clearRepliedQuestions),
  a structural mirror of permission-replies.ts (no shared abstraction,
  per YAGNI; prune semantics may diverge).
- sendQuestionReply/sendQuestionReject reconcile against question.list()
  before POSTing (no error-string parsing); on absent requestID they
  take the expired-prompt path instead of POSTing to a "root" fallback,
  and resolve the owning worktree when the stored slug is missing.
  markQuestionReplied is recorded on success before removeQuestionFromQueue.
- syncPendingQuestions adopts the full timestamp-based prune+filter
  pattern so replied questions are never rehydrated.
- The question ledger is cleared only on instance removal, never on
  rehydrate, so in-flight stale replies cannot slip through.
- session-events stale guards on handleQuestionAsked/handleQuestionAnswered.
- tool-call.tsx synchronous double-submit guard before the await boundary.

Also fixes two pre-existing module-load crashes uncovered while making the
UI test suite green (ownership policy): client-identity.ts dereferenced
window storage before its typeof-window guard, and server-events.ts opened
an EventSource at module load without an EventSource guard. Both fixes are
minimal and production-safe.

Validation: UI suite 49/49 pass (bun test --conditions browser), UI
typecheck clean (tsc --noEmit). Evidence packet under
evidences/058-question-reply-idempotency/.
The opencode agent can run `git worktree add` mid-session. CodeNomad
cached its worktree list with no invalidation trigger for out-of-band
git changes, so the files view stayed bound to a stale list and the
session remained mapped to its old worktree slug.

Wire a debounced refresh into the existing onSessionIdle path
(sse-manager -> handleSessionIdle) via a new refreshWorktreesOnIdle in
the worktrees store. On idle we reload the live server enumeration
(no server cache added) and apply a constrained auto-switch:

- Per-instance trailing debounce (~600ms) keyed by instanceId, deduped
  on the parent session id, so idle storms coalesce into one fetch.
- Add a reloadLoads in-flight guard to reloadWorktrees so it no longer
  overlaps with rehydrateInstance's reload.
- Diff the slug set before/after reload. Auto-switch only when exactly
  one worktree was added AND the idle session is the active session AND
  the parent session's slug has not changed since the snapshot (user
  has not manually switched). Zero or multiple additions refresh only.
- The switch persists via setWorktreeSlugForParentSession, which writes
  through OpenCode session metadata (per NeuralNomadsAI#514) so it survives reload.

Delete/prune paths are untouched. Adds co-located tests covering idle
reload, debounce coalescing, the in-flight guard, single-new auto-switch,
zero/multi-new no-switch, and the active-session guard.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 1, 2026

PR builds are available as GitHub Actions artifacts:

https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/26755126669

Artifacts expire in 7 days.
Artifacts:

  • pr-516-d0c0a7225f91a08acd0fc1c439088509f19ce5bf-tauri-macos
  • pr-516-d0c0a7225f91a08acd0fc1c439088509f19ce5bf-tauri-macos-arm64
  • pr-516-d0c0a7225f91a08acd0fc1c439088509f19ce5bf-tauri-linux
  • pr-516-d0c0a7225f91a08acd0fc1c439088509f19ce5bf-tauri-windows
  • pr-516-d0c0a7225f91a08acd0fc1c439088509f19ce5bf-electron-macos
  • pr-516-d0c0a7225f91a08acd0fc1c439088509f19ce5bf-electron-windows
  • pr-516-d0c0a7225f91a08acd0fc1c439088509f19ce5bf-electron-linux

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