Skip to content

sidebar: async project tree loads with stale-result protection - #668

Closed
andyrewlee wants to merge 1 commit into
advisor/054-diff-visual-rowsfrom
advisor/055-async-project-tree
Closed

andyrewlee wants to merge 1 commit into
advisor/054-diff-visual-rowsfrom
advisor/055-async-project-tree

Conversation

@andyrewlee

Copy link
Copy Markdown
Owner

Summary

Stacked on #667 (plan 054). Implements plan 055: the project tree no longer performs os.ReadDir on the Bubble Tea Update goroutine.

  • Async boundary: SetWorkspace, expand, refresh, . hidden-toggle, and rebase-fallback all return tea.Cmd; commands snapshot the per-model readDir seam and a path — never a mutable node — and yield immutable ProjectTreeDirectoryLoaded results. Update remains the sole owner of every node.
  • Stale-result protection: results carry workspace generation + per-node request identity (pendingRequest); superseded refreshes, collapse-during-load, cross-workspace stragglers, and pre-rebase reads are all discarded without mutating the tree. Queued obsolete jobs are dropped before ever running.
  • Bounded concurrency: at most 4 directory reads in flight per tree; the rest queue. Stale running jobs release their logical slot on delivery without publishing content (a kernel-blocked ReadDir can't be canceled — retained as a documented limitation).
  • Refresh UX: the old tree stays visible behind a shadow pendingRoot; remembered expansion paths restore incrementally as listings arrive, and selection restores by path — unless the user navigated during the refresh (navVersion fence), in which case their cursor wins. Deleted targets fall through to the flat-list clamp.
  • Error honesty: a failed read marks the node with a sanitized …-suffix error + r retry hint; it never renders as an empty directory. Root load shows a Loading… state.
  • Routing: TabbedSidebar.Update and internal/app route ProjectTreeDirectoryLoaded to the tree regardless of focus or active tab (same pattern as BranchChangesLoaded/AheadBehindLoaded); follow-up restore commands propagate as returned cmds.
  • Docs: ARCHITECTURE.md names the leaf-widget async boundary; README gains the Project-tab key table; docs/CONFIG.md and docs/ORCHESTRATION.md document the loading contract (no config/session surface).

Test plan

  • go test -race ./internal/ui/sidebar ./internal/app — green
  • env -u AMUX_WORKSPACES_ROOT make test-race — all packages green
  • env -u AMUX_WORKSPACES_ROOT make verify-loop — green
  • make harness-presets — all presets render
  • make lint-strict-new — 0 issues
  • git diff --check — clean
  • New coverage: delayed root/child loads, stale workspace results, repeated refresh, collapse/re-expand races, nested expansion restore, user-navigation-wins selection, deleted target clamp, error marker + retry, hidden-tab + unfocused + app-level routing, follow-up cmd propagation, concurrency cap, queued-obsolete drop, stale-slot release, content-version bumps, hidden-file filtering at apply time.
  • [!] PERF_STRICT=1 make perf-check — exceeds baselines on all 3 presets at host load ~60; identical failure reproduced on parent commit 55ebdd5 (ambient contention, not a render regression; the changed code adds ~2 comparisons per rendered row and isn't exercised by the monitor preset at all).
  • [!] make test-race-tmux / make devcheck — all assertions pass including tmux/pty race suites; e2e hit the known ambient t.TempDir RemoveAll teardown flake twice (unlinkat ... directory not empty) — same signature recorded on plans 050/051/054.

…ction

Move every os.ReadDir off the Bubble Tea Update goroutine: expand, refresh,
workspace switch, and hidden-toggle now return commands that produce
immutable ProjectTreeDirectoryLoaded snapshots. Update remains the sole owner
of tree nodes and applies a result only when its workspace generation and
per-node request identity still match, so superseded refreshes, collapse-
during-load, and workspace churn can never mutate current state.

Reads run through a bounded per-tree scheduler (4 in flight, FIFO queue);
superseded queued jobs are dropped before they ever run, and stale running
jobs release their slots on delivery without publishing content. Refresh
keeps the old tree visible behind a shadow root, then restores expansion and
selection by path as each remembered listing arrives — unless the user
navigated in between.

Results route through TabbedSidebar.Update and the app input switch
regardless of focus or active tab. Failed reads surface a sanitized
node-local marker instead of masquerading as an empty directory.

Gates: go test -race ./internal/ui/sidebar green; full make test-race green;
verify-loop green; harness-presets ran clean; lint-strict-new 0 issues.
Ambient: PERF_STRICT=1 perf-check exceeds baselines on all presets at load
~60 — identical failure on parent commit 55ebdd5 (host contention, not a
render regression). test-race-tmux/devcheck e2e hit the known TempDir
RemoveAll teardown flake twice; all assertions passed.
@andyrewlee

Copy link
Copy Markdown
Owner Author

Superseded — content landed on main via merge 8580442 (plans/ scaffolding intentionally excluded).

@andyrewlee andyrewlee closed this Sep 28, 2026
@andyrewlee
andyrewlee deleted the advisor/055-async-project-tree branch September 28, 2026 07:22
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