Conversation
Owner
Author
|
Updated this branch now that #2 (= upstream graykode#175,
|
Write the implementation phasing into docs/design/remote-ssh-monitoring.md, correcting a few assumptions against the current codebase: --json already exists as its own flag (no schema_version yet), config.rs is a hand-rolled key=value parser with no array-of-tables support, and the pid-keyed caches in collector/mod.rs are local-only so the (host, pid) identity concern is narrower than sketched -- it's really about guarding kill/jump call sites. Phase 0 (wire contract): - Add `schema_version` to the `--json` Snapshot, so a future RemoteCollector can reject a mismatched remote abtop version with a clear error. - Add `host: Option<String>` to AgentSession/SessionView (None = local) and an `AgentSession::is_local()` helper that later kill/jump guards will use. No behavior change: all existing constructors set `host: None`.
Decision: extend the hand-rolled key=value parser in config.rs rather than
add a toml dependency. remote_hosts is read-only from the app's perspective
(nothing writes it back, unlike theme/panels), so the existing
comment/unknown-key-preserving writer never needs to reproduce it -- pulling
in a real TOML crate for one narrow array-of-tables shape would just split
config.rs into two parsing styles for no correctness gain.
- RemoteHostConfig { name, ssh_target, ssh_opts, poll_interval_secs,
allow_remote_kill }, defaults matching the design doc (10s poll, kill
disabled).
- parse_config_body now tracks whether it's inside a [[remote_hosts]] block
(bool, not a general section stack -- there's exactly one table shape
today) and routes keys accordingly; any other table header ends the block.
- Entries missing name or ssh_target are dropped after parsing, keeping
config loading infallible rather than surfacing a parse error.
- Tests: single/multiple blocks, field defaults, malformed-entry drop,
inline comments on table headers, top-level keys still working alongside
a block, and rewrite_kv_lines leaving a remote_hosts block untouched when
saving an unrelated key (theme).
src/collector/remote.rs: one background thread per configured host runs `ssh ... abtop --json`, caches last-known-good sessions + reachability so a slow/hung SSH round trip never blocks the 2s tick loop (same idiom as DesktopRolloutScanner's Codex desktop-app rollout scan). Registered in MultiCollector::with_hidden_and_claude_config_dirs, gated on remote_hosts being non-empty. A few decisions landed differently than the design doc originally sketched (doc updated to match): - No new CLI flag: --json already exists, is fast (no up-to-30s summary wait), and is already redacted/versioned -- reused as-is over SSH instead of inventing `--once --json`. - RemoteSnapshotDto/RemoteSessionDto (remote.rs) are a dedicated, narrower Deserialize view of the same --json output, not new derives on the internal Snapshot/SessionView types. Every field is #[serde(default)], so an older/newer remote abtop degrades field-by-field; schema_version is still checked up front for a clear reject message instead of a parse failure. A few AgentSession fields aren't on the wire (context_history, mem_file_count/mem_line_count, pending/thinking_since_ms, file_accesses) and default to empty/zero for remote sessions. - The remote's precomputed `summary` string is reused as `initial_prompt` so App::session_summary shows it directly; drain_and_retry_summaries and has_retryable_summaries now gate on host.is_none() so this never triggers a second, local `claude --print` over the same text. - Two safety guards landed here rather than waiting for Phase 3: local git-stats recomputation and orphan-port tracking in MultiCollector::collect both now skip host.is_some() sessions. Without the first, `git -C <remote cwd>` would run against a local path that doesn't exist (or worse, one that does); without the second, a remote child's PID could coincidentally collide with a live local PID and feed kill_orphan_ports. This also means OrphanPort never needs a `host` field, and kill_orphan_ports never needs its own guard in Phase 3. model/session.rs: SessionStatus and ChildProcess gain Deserialize (plus a Default for SessionStatus = Unknown) so RemoteSessionDto can deserialize them directly instead of duplicating their shape. lib.rs: run_app now takes `cfg: &config::AppConfig` instead of four separate fields, both to thread remote_hosts through and to stay under clippy's too-many-arguments threshold. 7 new tests in collector::remote (schema mismatch, non-JSON input, missing- field defaulting, host tagging, agent_cli fallback, poll-interval/in-flight gating). 217 tests pass, clippy clean.
Completes the remote-ssh-monitoring design (docs/design/remote-ssh-monitoring.md). app.rs: - kill_selected and jump_to_session now bail with a status message naming the host when session.host.is_some(), instead of acting on session.pid against this machine's ps/kill/terminal-jump -- PIDs are only unique per host. kill_orphan_ports needed no guard: Phase 2 already excludes remote sessions from feeding local orphan-port tracking at collection time. - New App::remote_host_statuses(), delegating to MultiCollector, for the UI. collector/mod.rs: - MultiCollector now holds its RemoteCollector in a dedicated `remote` field instead of type-erased into `collectors: Vec<Box<dyn AgentCollector>>`, so remote_host_statuses() can be queried directly without downcasting a trait object. collect() still merges its sessions in exactly as before. ui/sessions.rs: - Project column shows "[host] project" for a remote session (project_display_name). - A session whose host is currently unreachable gets the same row dimming as a Done session, and its task line switches to "stale, Ns ago" / "unreachable" (remote_stale_task_text) instead of showing last-known-good task text as if it were live. - Note: the project column is 8-14 chars depending on terminal width, so "[host]" can crowd out most of the actual project name on a narrow terminal -- accepted as the same graceful truncation the rest of this panel already does. 9 new tests (2 in app.rs for the kill/jump guards, 7 in ui/sessions.rs for the host prefix and stale-text helpers + a render-level check). 224 tests pass, clippy clean.
Verified this feature end-to-end against a live remote host (see the new "Verified against a live host" section in docs/design/remote-ssh-monitoring.md). That exercise found one real bug: - --json and --once always showed remote sessions as empty on a fresh process. Both do exactly one tick and exit immediately, but a RemoteCollector poll runs on a background thread and is only picked up on the *next* collect() call -- so the single tick always raced past the first SSH round trip before it could land. Fixed with wait_for_remote_hosts in lib.rs: re-ticks every 200ms (up to 10s, a no-op when no hosts are configured -- covered by a new test) until every configured host has reported at least once, mirroring the existing summary-wait loop --once already had for a different subsystem. Also documents remote_hosts in README's Configuration section (with a full [[remote_hosts]] example), and fixes a pre-existing, unrelated inaccuracy I hit while testing: the config file is not at ~/.config/abtop/config.toml on macOS (dirs::config_dir() resolves to ~/Library/Application Support/abtop/ there) -- README now lists the actual path per platform. AGENTS.md's Non-Goals and architecture tree updated to reflect that remote monitoring has shipped. 226 tests pass, clippy clean.
…+PR#3 The automatic merge of feature/claude-launch-surface (PR#2) and feat/remote-ssh-monitoring (PR#3) succeeded without conflict markers, but left one real gap git couldn't have caught: PR#2 added AgentSession.launch_surface (and already put it on the --json wire via SessionView), but RemoteCollector's RemoteSessionDto -- new code from PR#3 that PR#2 never touched -- didn't know the field existed, so every remote session would have silently defaulted to LaunchSurface::Cli regardless of how it was actually launched on the remote host. - LaunchSurface gains Deserialize + a Default (Cli, matching ClaudeCollector::detect_launch_surface's own inconclusive-detection fallback), the same treatment SessionStatus already got for the same reason in RemoteSessionDto. - RemoteSessionDto/into_agent_session now parse and carry launch_surface through, so a remote session's desktop-app/IDE badge reflects the remote host's own process, not a merge-time default. - 2 new tests: launch_surface round-trips through parse_remote_snapshot, and a session object missing the field still degrades to Cli rather than failing to parse. 232 tests pass, clippy clean.
ntung
force-pushed
the
feat/remote-ssh-monitoring
branch
from
September 14, 2026 10:15
947a068 to
018de17
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements remote/SSH session monitoring, previously a v0.1 non-goal:
abtopcan now pollabtop --jsonon other hosts over SSH and merge their sessions into the local session list, tagged with a[host]prefix. Full design and rationale (including a few decisions that landed differently than the original sketch) indocs/design/remote-ssh-monitoring.md.Implemented in four phases, one commit each:
schema_versionon the--jsonSnapshot;host: Option<String>onAgentSession/SessionView(None= local).[[remote_hosts]]blocks inconfig.toml, parsed by extending the existing hand-rolled parser (no newtomldependency —remote_hostsis read-only from the app's perspective).RemoteCollector: one background thread per host runsssh ... abtop --json, caches last-known-good sessions + reachability so a slow/hung SSH round trip never blocks the 2s tick loop. Reuses the existing--jsonflag as-is (no new CLI flag) via a dedicated, narrowerDeserializeDTO. Found and fixed two real bugs while wiring this in: local git-stats recomputation and orphan-port tracking would otherwise run against remote paths/PIDs.kill_selected/jump_to_sessionrefuse to act on a remote session's PID (only unique per host); sessions panel shows[host] projectand grays out + shows "stale, Ns ago" for an unreachable host.Verified against a live host
Tested against a real remote box (Rocky Linux, a live Claude Code session) over passwordless SSH — installed a user-level Rust toolchain there, built this branch, ran
abtop --jsonnon-interactively exactly asRemoteCollectordoes, and confirmed the session merged into a localabtop --jsonrun tagged with the host, including its real git branch/added/modified counts. This found and fixed two more issues:--json/--oncealways showed remote sessions as empty on a fresh process (the poll is async; a single tick raced past it) — fixed with a bounded wait loop.~/.config/abtop/config.tomlon macOS (dirs::config_dir()resolves elsewhere there) — a pre-existing README inaccuracy, now fixed.Testing
cargo test),cargo clippy --all-targets -- -D warningsclean.