Providers and hosts: what runs, and where, as independent choices

A session now names a provider (what: driver kind, command, models) and
optionally a host (where: an ssh target). Keeping them independent is
what the real setup needs -- the backend runs where the phone can reach
it, which isn't where the CLI is installed -- and it means any provider
can be sent to any host rather than a machine being baked into one.

The first provider is claude-cli, named for the CLI rather than bare
"claude", which would suggest the credit-billed API. A fresh config is
seeded with it so a new install has something to spawn and a worked
example to edit; echo stays a built-in provider needing no config.

ssh.rs builds the child process either way: locally, or `ssh -T` with
BatchMode and keepalives, every argument single-quoted for the remote
shell (a working directory that tries to close the quote and start a
command is covered by a test), and `exec` so dropping the connection
takes the CLI down instead of orphaning it.

App: the spawn screen reads /providers and /hosts instead of hardcoded
lists, so config changes need no rebuild. Chip rows are FlowRow, fixing
the reported bug where a row of models that didn't fit wrapped *inside*
each chip -- one letter of "haiku" per line -- rather than onto a second
line.

Verified: 29 tests, clippy clean; the same claude-cli provider run once
locally and once over ssh, with the remote one visibly in a different
environment; an unknown host name refused with the configured list; and
the spawn screen on the emulator showing server-driven providers, hosts,
and models that wrap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
irisandClaude Fable 5 committed 2026-08-25 03:34:33 -04:00
1 parent 91bbc73ae5
commit fff1fb49e8
12 files changed
+833 -170

No files matched your search

+8 -1
View File
@@ -17,6 +17,7 @@ mod auth;
mod config;
mod routes;
mod session;
mod ssh;
mod usage;
use std::net::{IpAddr, SocketAddr};
@@ -153,8 +154,14 @@ async fn main() -> Result<()> {
.with_context(|| format!("failed to load {}", config_path.display()))?,
);
tracing::info!("config: {}", config_path.display());
for provider in manager.providers() {
tracing::info!(" provider {} ({:?})", provider.name, provider.kind);
}
for host in manager.hosts() {
tracing::info!(" host {} -> {}", host.name, host.address);
}
for info in manager.sessions() {
tracing::info!(" session {} ({:?}, {:?})", info.id, info.kind, info.status);
tracing::info!(" session {} ({}, {:?})", info.id, info.provider, info.status);
}
let bind_ip = match args.bind {