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:
1 parent
91bbc73ae5
commit
fff1fb49e8
12 files changed
+833
-170
No files matched your search
@@ -88,10 +88,11 @@ fun <T> requestFromServer(
|
||||
}
|
||||
}
|
||||
|
||||
// One row of GET /sessions.
|
||||
// One row of GET /sessions. `provider` is what runs it, `host` where --
|
||||
// the two are independent, so a session names both.
|
||||
data class SessionSummary(
|
||||
val id: String,
|
||||
val kind: String,
|
||||
val provider: String,
|
||||
val title: String,
|
||||
val host: String?,
|
||||
val model: String?,
|
||||
@@ -99,28 +100,61 @@ data class SessionSummary(
|
||||
val lastActivity: Double,
|
||||
)
|
||||
|
||||
private fun parseSession(session: JSONObject) = SessionSummary(
|
||||
id = session.getString("id"),
|
||||
provider = session.getString("provider"),
|
||||
title = session.getString("title"),
|
||||
host = session.optString("host").ifEmpty { null },
|
||||
model = session.optString("model").ifEmpty { null },
|
||||
status = session.getString("status"),
|
||||
lastActivity = session.getDouble("lastActivity"),
|
||||
)
|
||||
|
||||
fun fetchSessions(settings: ServerSettings): List<SessionSummary> =
|
||||
requestFromServer(settings, "/sessions") { connection ->
|
||||
val sessions = JSONArray(connection.inputStream.bufferedReader().readText())
|
||||
(0 until sessions.length()).map { i ->
|
||||
val session = sessions.getJSONObject(i)
|
||||
SessionSummary(
|
||||
id = session.getString("id"),
|
||||
kind = session.getString("kind"),
|
||||
title = session.getString("title"),
|
||||
host = session.optString("host").ifEmpty { null },
|
||||
model = session.optString("model").ifEmpty { null },
|
||||
status = session.getString("status"),
|
||||
lastActivity = session.getDouble("lastActivity"),
|
||||
(0 until sessions.length()).map { parseSession(sessions.getJSONObject(it)) }
|
||||
}
|
||||
|
||||
// What the server offers, so the spawn screen has no hardcoded lists: a
|
||||
// provider or host added to the server's config.json appears here with no
|
||||
// app rebuild.
|
||||
data class Provider(val name: String, val kind: String, val models: List<String>)
|
||||
|
||||
data class RemoteHost(val name: String, val address: String)
|
||||
|
||||
fun fetchProviders(settings: ServerSettings): List<Provider> =
|
||||
requestFromServer(settings, "/providers") { connection ->
|
||||
val providers = JSONArray(connection.inputStream.bufferedReader().readText())
|
||||
(0 until providers.length()).map { i ->
|
||||
val provider = providers.getJSONObject(i)
|
||||
val models = provider.optJSONArray("models")
|
||||
Provider(
|
||||
name = provider.getString("name"),
|
||||
kind = provider.getString("kind"),
|
||||
models = (0 until (models?.length() ?: 0)).map { models!!.getString(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Spawns a session and returns it as the list would show it. */
|
||||
fun fetchHosts(settings: ServerSettings): List<RemoteHost> =
|
||||
requestFromServer(settings, "/hosts") { connection ->
|
||||
val hosts = JSONArray(connection.inputStream.bufferedReader().readText())
|
||||
(0 until hosts.length()).map { i ->
|
||||
val host = hosts.getJSONObject(i)
|
||||
RemoteHost(name = host.getString("name"), address = host.getString("address"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawns a session and returns it as the list would show it. [host] is the
|
||||
* name of a configured host, or null to run on the backend machine itself.
|
||||
*/
|
||||
fun spawnSession(
|
||||
settings: ServerSettings,
|
||||
kind: String,
|
||||
provider: String,
|
||||
title: String,
|
||||
host: String? = null,
|
||||
model: String? = null,
|
||||
cwd: String? = null,
|
||||
permissionMode: String? = null,
|
||||
@@ -129,22 +163,15 @@ fun spawnSession(
|
||||
settings,
|
||||
"/sessions",
|
||||
method = "POST",
|
||||
jsonBody = JSONObject().put("kind", kind).put("title", title).apply {
|
||||
jsonBody = JSONObject().put("provider", provider).put("title", title).apply {
|
||||
if (!host.isNullOrBlank()) put("host", host)
|
||||
if (!model.isNullOrBlank()) put("model", model)
|
||||
if (!cwd.isNullOrBlank()) put("cwd", cwd)
|
||||
if (!permissionMode.isNullOrBlank()) put("permissionMode", permissionMode)
|
||||
}.toString(),
|
||||
readTimeoutMs = 30000,
|
||||
) { connection ->
|
||||
val session = JSONObject(connection.inputStream.bufferedReader().readText())
|
||||
SessionSummary(
|
||||
id = session.getString("id"),
|
||||
kind = session.getString("kind"),
|
||||
title = session.getString("title"),
|
||||
host = session.optString("host").ifEmpty { null },
|
||||
model = session.optString("model").ifEmpty { null },
|
||||
status = session.getString("status"),
|
||||
lastActivity = session.getDouble("lastActivity"),
|
||||
)
|
||||
parseSession(JSONObject(connection.inputStream.bufferedReader().readText()))
|
||||
}
|
||||
|
||||
fun sendMessage(
|
||||
|
||||
Reference in new issue
Block a user