Repository navigation
Conversation
Sidebar repository icons were resolved only for the local endpoint, because the owner lookup ran `git config` in the workspace directory on this machine. A remote device's rows always fell back to the generic GitHub mark. Avatar requests are now keyed by a `Checkout`: a local directory read by local Git, or, for a saved SSH device, the Git directory its daemon reported, read over the same bounded, noninteractive SSH config read the pull request lookup already uses. Remote directories are still never resolved against this machine's repositories, and other socket targets get no lookup. Worktrees share their common Git directory, so one SSH read covers every worktree of a repository on a host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[Medium impact] The PR appears safe to merge; the latest changes address the remaining reconnect and deleted-device concerns. SummaryThe PR adds owner avatars for SSH workspaces and keeps their reads tied to device connections.
Reviews (4) · Last reviewed commit: "fix: retry SSH avatars a reconnect misse..." · Reviewed by Greptile |
A failed SSH read of a remote checkout's Git configuration was cached like a checkout without a GitHub owner, so a device that was briefly unreachable when its first snapshot arrived kept the generic mark until the app restarted. The owner lookup now returns an error only when the host could not be read. Such a checkout is marked unreachable with a retry time 60 seconds later; the next sidebar snapshot update after that asks again. A host read without a GitHub owner is still cached for good, and a retry reuses its entry, so it never counts against the request bound as a new one.
cc727de stopped reserving an empty chevron column on rows that draw no chevron, and updated the headless width checks, but the native paint probe still subtracted ARROW_RESERVE from a group member's label. The Windows native GUI job, the only CI job that runs it, then failed on every branch with "child label width: actual 179px, expected 161px".
The headless checks of the native child probe built their painted labels at the old width, which still gave up the arrow column. Each now adds ARROW_RESERVE (18px) to match the probe's expectation.
|
|
SSH owner reads shared the single lookup worker with local Git lookups. A host that stopped answering after its workspace snapshot arrived held that worker for up to the 10 s read budget per repository, so several queued remote repositories could delay a newly opened local workspace's avatar by about a minute, even when its image was already on disk. Local lookups now have their own queue and worker, and SSH reads run on a separate pool of four readers, so a single stalled host cannot block other devices either. The 128-entry request bound still spans both.
relaunch_retries_then_alerts_when_open_keeps_failing failed twice in local `just ci` runs at load average ~67: the script had logged "open failed" but had not yet run the alert when the 10 s wait expired. That path sleeps 3.25 s between about a dozen process spawns. The deadline bounds only a failing wait, so passing runs are no slower.
resting_on_a_workspace_opens_its_menu_once polled at Instant::now() plus half the 600 ms dwell and expected the menu to stay closed, but the drift move had stamped its own Instant::now() earlier. At load average ~65, more than 300 ms passed between the two and the menu opened. The early checks now poll at an instant taken before the move, which can never reach the dwell.
Remote avatar reads kept their own 60 s retry timer, independent of the window's connection to the device. A read could dial a host the window already knew was down, and a device that came back waited for that timer before its rows got avatars. Avatars now follow each saved SSH endpoint's connection. While no endpoint on a host is connected, nothing is requested for it, reads queued for it never dial, and running reads are cancelled through the SSH read's cancel hook. When a connection comes up with a snapshot, the host's unreadable checkouts are due at once, so that first snapshot asks for them again. A read that fails while the device stays connected still waits 60 s. Another session's endpoint on the same host keeps the host connected.
# Conflicts: # crates/herdr-gpui/src/sidebar/layout_tests.rs
|
|
|
@greptileai review |
…devices A read still pending when its device reconnected was skipped by that connection's snapshot, and a later failure left it waiting for another endpoint update. Pending slots now record when they were asked, and a failure that arrives after a newer connection asks again at once. Devices removed from the catalog were never followed again, so their link kept the host marked online and queued reads still dialed it. The poll loop now drops links for endpoints that left the catalog.
|
@greptileai review |
The login shell timeout test gave the shell one second to start and record its pids, which a busy machine missed. The closed-port test dropped its listener, so a parallel test could be given the port or a probe could connect to itself; an accepted connection now keeps the port in use.
|
The only red check on |
Summary
Sidebar rows for a remote (SSH) device always showed the generic GitHub mark, while the same repository on the local device showed its owner's avatar. The owner lookup only ran for endpoint 0, because it ran
git configin the workspace directory on this machine, and a remote directory must never be resolved against local repositories.This change keys avatar requests by a
Checkout:Checkout::Local(cwd)covers the local daemon. It uses local Git and behaves as before.Checkout::Ssh { target, git_dir }covers a saved SSH device. It readsremote.upstream.url, thenremote.origin.url, from the Git directory the daemon reported (worktree.key). The read goes throughherdr_client::remote_config_value, the same bounded, noninteractive SSH read the pull request lookup already uses, with a 10 s budget shared by both keys.Worktrees share their common Git directory, so one SSH read covers every worktree of a repository on a host. Remote entries are stored per host, then per Git directory, so a sidebar row looks up its avatar without allocating. The existing 128-entry request bound applies across local and remote entries.
A remote workspace with no worktree metadata, such as a plain non-Herdr directory, still shows the mark. This matches how pull request lookups treat remote workspaces.
Following the device's connection
A failed SSH read (host down, SSH timeout, connection refused) is no longer cached as "no owner" until the app restarts. The lookup returns
herdr_client::Result<Option<String>>:Ok(None)means the host was read and has no GitHub owner, which is still cached for good. An error marks the checkoutUnreachable.Remote reads follow the window's own connection to each saved SSH device (
Avatars::follow, fed every poll from the endpoint'sonline_since):Unreachablecheckouts become due at once, and that first snapshot asks for them again. A device that comes back gets its avatars right away, with no timer to wait out.A retry reuses its existing entry, so it never counts against the request bound as a new checkout.
Slow hosts never delay local avatars
SSH reads can take the full 10 s budget when a host stops answering, so they no longer share a worker with local Git lookups. Local lookups have their own queue and worker. SSH reads run on a pool of four readers, so one stalled host doesn't block other devices either. The 128-entry request bound still covers both queues.
Test fixes on this branch
f1ca89db/2e7ac189fixed the Windows native sidebar child-width probe.mainhas since taken the same fix (test(sidebar): expect the child label to take the chevron column #746); after mergingmain, this branch differs from it only by one comment.test(updater):relaunch_retries_then_alerts_when_open_keeps_failingwaited 10 s for a path that sleeps 3.25 s between about a dozen process spawns, and failed twice in localjust ciat load average ~67. The wait is now 30 s. This bounds only a failing run, so passing runs are no slower.test(sidebar):resting_on_a_workspace_opens_its_menu_oncechecked "not yet open" atInstant::now()+ 300 ms, measured from a later clock reading than the move it tests, so on a loaded machine the menu could open before the check ran. Those checks now poll at an instant taken before the move.Screenshots
macOS, dark theme, with two saved SSH devices (
lin-omarchy,lin-big-omarchy).After the change,
herdr-gpuionlin-omarchyandGRiiionlin-big-omarchyshow their owners' avatars.wgsextract-clionlin-big-omarchykeeps the mark because that host's daemon reports no worktree metadata for it (checked withherdr workspace list).QA
cargo test --locked -p herdr-gpui avatars: 29 passed, 2 ignored (live).avatars/tests/remote.rscovers:avatars/tests/retry.rscovers:reads_the_repository_owner_over_ssh -- --ignoredon macOS arm64 against both saved devices: read ownerpenso(lin-omarchy,/home/mac/src/herdr-gpui/.git) andtheontho(lin-big-omarchy,/home/mac/src/GRiii/.git). Against an unresolvable host it returnsErr(RemoteCommand(255)), which the app retries, where before this was silently cached as "no owner".avatars/tests/reconnect.rscovers:avatars/tests/workers.rscovers: local and SSH requests going to separate queues, and a full SSH queue still accepting local lookups.just bundle release qa-menu, ran it with the existing config and saved SSH devices, and confirmed in the window that remote repository rows show owner avatars (screenshots above). This was before the retry, connection-following, and worker-split changes, which are covered by the unit tests and live read above and change nothing visible.just cion macOS arm64 at702f02b9(merged withmainat32b0b878): passed, 10220 tests, 0 failed.just test-sidebaron macOS arm64: the child-width probe now expects 179px and all 26 child checks pass; the deliberate bad-width fixture still fails as intended. The main native test then hit its 20 s timeout at the outside-click step on this laptop, whose display can't fit the test's 1000–1200px windows (frames are skipped as "does not fit the display"); the Windows native GUI job raises its resolution, so that job is the real check.ssh … git configprocesses linger for that host (ps), and that the avatars return once it reconnects.🤖 Generated with Claude Code
https://claude.ai/code/session_01PZZjmvxMrYtHtHTBju7zD8