Don't offer an empty spawn form when the options never arrived
SpawnScreen kept its own `loading` flag and `error` string, the third mechanism in this app for a state two screens already share. That was not just untidy: on a failed fetch it set the error, left `providers` and `hosts` at the empty lists they started as, and rendered the form anyway -- so "couldn't reach the server" arrived as a Provider row with no providers in it, which is what a server offering nothing would also look like. The error sat below both empty pickers. It now holds `LoadState<SpawnOptions>` like the others: loading shows the spinner, a failure reports and stops, and the form exists only where there is something to fill it with. The spawn action keeps its own error, renamed `spawnError` so the two can't be confused again. They are different in kind and the distinction is the one the session list just learned: a fetch that never answered leaves no form worth showing, while a spawn the server refused leaves a filled-in form the user still wants, so that one stays beside the button that produced it. Verified on the emulator: the form loading with both providers, the Claude fields appearing when claude-cli is selected (which also exercises the snake_case kind the phone now compares against), and the failure state with the server stopped -- reporting alone, with Cancel still working. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
1 parent
4e760a4a72
commit
78c054918f
1 file changed
+32
-15
@@ -55,9 +55,10 @@ fun SpawnScreen(
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var providers by remember { mutableStateOf<List<Provider>>(emptyList()) }
|
||||
var hosts by remember { mutableStateOf<List<RemoteHost>>(emptyList()) }
|
||||
var loading by remember { mutableStateOf(true) }
|
||||
// What the form is made of, and whether we have it yet. A failure here
|
||||
// is not the same as a server with nothing to offer, so it must not
|
||||
// reach the pickers as empty lists -- see LoadState.
|
||||
var options by remember { mutableStateOf<LoadState<SpawnOptions>>(LoadState.Loading) }
|
||||
|
||||
var provider by remember { mutableStateOf<Provider?>(null) }
|
||||
var host by remember { mutableStateOf<String?>(null) }
|
||||
@@ -66,20 +67,21 @@ fun SpawnScreen(
|
||||
var cwd by remember { mutableStateOf("") }
|
||||
var permissionMode by remember { mutableStateOf("manual") }
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
// Only the spawn's own failure. The fetch's lives in `options`: this
|
||||
// one leaves a filled-in form worth keeping, and that one leaves
|
||||
// nothing to fill in.
|
||||
var spawnError by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
val loaded = withContext(Dispatchers.IO) {
|
||||
fetchProviders(settings) to fetchHosts(settings)
|
||||
options = try {
|
||||
val fetched = withContext(Dispatchers.IO) {
|
||||
SpawnOptions(fetchProviders(settings), fetchHosts(settings))
|
||||
}
|
||||
providers = loaded.first
|
||||
hosts = loaded.second
|
||||
provider = providers.firstOrNull()
|
||||
provider = fetched.providers.firstOrNull()
|
||||
LoadState.Loaded(fetched)
|
||||
} catch (e: ApiException) {
|
||||
error = e.message
|
||||
LoadState.failed(e)
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
|
||||
val current = provider
|
||||
@@ -99,10 +101,21 @@ fun SpawnScreen(
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
if (loading) {
|
||||
// Nothing below is fillable until the options are here, and a
|
||||
// failure to fetch them leaves no form worth showing -- so this
|
||||
// reports and stops, rather than offering empty pickers under an
|
||||
// error message.
|
||||
val (providers, hosts) = when (val state = options) {
|
||||
is LoadState.Loading -> {
|
||||
CircularProgressIndicator()
|
||||
return@Column
|
||||
}
|
||||
is LoadState.Error -> {
|
||||
Text(state.message, color = MaterialTheme.colorScheme.error)
|
||||
return@Column
|
||||
}
|
||||
is LoadState.Loaded -> state.value
|
||||
}
|
||||
|
||||
ChipGroup(
|
||||
label = "Provider",
|
||||
@@ -177,7 +190,8 @@ fun SpawnScreen(
|
||||
}
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
error?.let {
|
||||
// Beside the button that produced it.
|
||||
spawnError?.let {
|
||||
Text(it, color = MaterialTheme.colorScheme.error)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
@@ -201,7 +215,7 @@ fun SpawnScreen(
|
||||
}
|
||||
onSpawned(spawned)
|
||||
} catch (e: ApiException) {
|
||||
error = e.message
|
||||
spawnError = e.message
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
@@ -211,6 +225,9 @@ fun SpawnScreen(
|
||||
}
|
||||
}
|
||||
|
||||
/** What the spawn form is built from, fetched as one thing. */
|
||||
private data class SpawnOptions(val providers: List<Provider>, val hosts: List<RemoteHost>)
|
||||
|
||||
/**
|
||||
* A labeled row of choices that wraps onto as many lines as it needs.
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user