A setup is a machine, and it carries what that machine can run
Providers and hosts were two independent lists, and a session named one of each. They were never independent: a provider only exists on a machine where that program is installed, so the spawn screen offered the whole cross-product, including "the Claude CLI on the box that hasn't got it". The picker could not know, because nothing in the model said. Now a setup is a machine -- optional ssh, plus the providers it has -- and spawning is two choices in order: pick a setup, then one of its providers. The impossible pairs stop being expressible rather than being validated against. Provider names are unique within a setup and only within one, so two machines can each have a `claude-cli`, which was previously either a name collision or two entries called things like "claude" and "claude on the vm". It also settles the "Run on" problem properly. That control was offered for every provider but honoured only by the Claude driver -- an echo session sent to a host ran locally and said otherwise. There is no such control now: the machine is chosen first, and echo is a provider of the setup with no ssh, where it belongs, since it runs in-process and has no transport to cross. The built-in echo provider is gone as a concept. It used to be conjured at read time and never written to the file, which meant a provider nobody could see or edit; it is now seeded into the config on first run alongside claude-cli. What the file says is what there is, and deleting it is a choice rather than a state to be repaired. A config in the old shape is refused with instructions rather than loaded. `Config` defaults unknown fields away, so `providers:` and `hosts:` would otherwise have vanished into an empty config that was then seeded over -- a migration nobody would notice until their setups were gone. Verified against a running server and on the emulator: a fresh install seeds "this machine" with echo and claude-cli and the file reads cleanly; a two-setup config lists both with their own providers; spawning on a setup works and the session row names it; asking for a provider a setup lacks says which it offers, and an unknown setup says which exist. On the phone, selecting "dev vm" narrows the provider chips to that machine's one and shows its address. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
1 parent
7cf36005ae
commit
ecac404fd4
10 files changed
+422
-328
No files matched your search
+39
-44
@@ -3,10 +3,9 @@
|
||||
//! wraps the whole router in.
|
||||
//!
|
||||
//! ```text
|
||||
//! GET /providers what can be spawned
|
||||
//! GET /hosts machines a session can be run on
|
||||
//! GET /setups machines, each with what it can run
|
||||
//! GET /sessions list (id, provider, title, model, status, last activity)
|
||||
//! POST /sessions spawn {provider, title?, model?, cwd?, permissionMode?}
|
||||
//! POST /sessions spawn {setup, provider, title?, model?, cwd?, params?}
|
||||
//! GET /sessions/{id}/events?after=N SSE: transcript replay from N, then live
|
||||
//! POST /sessions/{id}/message {text, attachmentIds?}
|
||||
//! POST /sessions/{id}/answer {questionId, answer} (questions and permissions)
|
||||
@@ -46,8 +45,7 @@ use crate::session::{LiveSession, SessionInfo, SessionManager, SpawnSpec};
|
||||
|
||||
pub fn router(manager: Arc<SessionManager>) -> Router {
|
||||
Router::new()
|
||||
.route("/providers", get(list_providers))
|
||||
.route("/hosts", get(list_hosts))
|
||||
.route("/setups", get(list_setups))
|
||||
.route("/sessions", get(list_sessions).post(spawn_session))
|
||||
.route("/sessions/{id}", delete(delete_session))
|
||||
.route("/sessions/{id}/events", get(events))
|
||||
@@ -113,9 +111,25 @@ async fn list_sessions(State(manager): State<Arc<SessionManager>>) -> axum::Json
|
||||
}
|
||||
|
||||
/// What the spawn screen needs to render itself, so the phone holds no
|
||||
/// hardcoded list: an entry added to `config.ron` shows up with no app
|
||||
/// rebuild. Providers and hosts are listed separately because they are
|
||||
/// independent choices -- any provider can be run on any host.
|
||||
/// hardcoded list: a setup added to `config.ron` shows up with no app
|
||||
/// rebuild.
|
||||
///
|
||||
/// One list rather than two, because the choice is a pair and the halves
|
||||
/// are not independent. A provider only exists on a machine that has it
|
||||
/// installed, so listing providers and machines separately offered their
|
||||
/// whole cross-product -- including "the Claude CLI on the box that hasn't
|
||||
/// got it".
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SetupInfo {
|
||||
name: String,
|
||||
/// Where it runs, for telling two setups apart. Absent for the one
|
||||
/// that is this machine.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
address: Option<String>,
|
||||
providers: Vec<ProviderInfo>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ProviderInfo {
|
||||
@@ -124,41 +138,23 @@ struct ProviderInfo {
|
||||
models: Vec<String>,
|
||||
}
|
||||
|
||||
async fn list_providers(
|
||||
State(manager): State<Arc<SessionManager>>,
|
||||
) -> axum::Json<Vec<ProviderInfo>> {
|
||||
async fn list_setups(State(manager): State<Arc<SessionManager>>) -> axum::Json<Vec<SetupInfo>> {
|
||||
axum::Json(
|
||||
manager
|
||||
.providers()
|
||||
.setups()
|
||||
.into_iter()
|
||||
.map(|provider| ProviderInfo {
|
||||
name: provider.name,
|
||||
kind: provider.kind,
|
||||
models: provider.models,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HostInfo {
|
||||
name: String,
|
||||
/// Shown under the name so a host can be told apart from its label.
|
||||
address: String,
|
||||
}
|
||||
|
||||
/// Configured remote machines. Running on the backend itself is always
|
||||
/// available and deliberately absent here -- it is the "no host" case, not
|
||||
/// an entry that could be edited away.
|
||||
async fn list_hosts(State(manager): State<Arc<SessionManager>>) -> axum::Json<Vec<HostInfo>> {
|
||||
axum::Json(
|
||||
manager
|
||||
.hosts()
|
||||
.into_iter()
|
||||
.map(|host| HostInfo {
|
||||
name: host.name,
|
||||
address: host.address,
|
||||
.map(|setup| SetupInfo {
|
||||
name: setup.name,
|
||||
address: setup.ssh.map(|ssh| ssh.address),
|
||||
providers: setup
|
||||
.providers
|
||||
.into_iter()
|
||||
.map(|provider| ProviderInfo {
|
||||
name: provider.name,
|
||||
kind: provider.kind,
|
||||
models: provider.models,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
@@ -167,10 +163,9 @@ async fn list_hosts(State(manager): State<Arc<SessionManager>>) -> axum::Json<Ve
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SpawnRequest {
|
||||
/// Which machine, and which of the things it offers.
|
||||
setup: String,
|
||||
provider: String,
|
||||
/// Name of a configured host; absent runs on the backend machine.
|
||||
#[serde(default)]
|
||||
host: Option<String>,
|
||||
#[serde(default)]
|
||||
title: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -192,8 +187,8 @@ async fn spawn_session(
|
||||
) -> Result<axum::Json<SessionInfo>, ApiError> {
|
||||
let info = manager
|
||||
.spawn_session(SpawnSpec {
|
||||
setup: body.setup,
|
||||
provider: body.provider,
|
||||
host: body.host,
|
||||
title: body.title,
|
||||
model: body.model,
|
||||
cwd: body.cwd,
|
||||
|
||||
Reference in new issue
Block a user