Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ data class SavedServer(
val os: String? = null,
val sshBanner: String? = null,
val rememberedByUser: Boolean = false,
val detachedTransport: Boolean = false,
val alleycatHost: String? = null,
val alleycatNodeId: String? = null,
val alleycatRelay: String? = null,
Expand Down Expand Up @@ -64,6 +65,7 @@ data class SavedServer(
os?.let { put("os", it) }
sshBanner?.let { put("sshBanner", it) }
put("rememberedByUser", rememberedByUser)
put("detachedTransport", detachedTransport)
alleycatHost?.let { put("alleycatHost", it) }
alleycatNodeId?.let { put("alleycatNodeId", it) }
alleycatRelay?.let { put("alleycatRelay", it) }
Expand Down Expand Up @@ -201,6 +203,7 @@ data class SavedServer(
} else {
true
},
detachedTransport = obj.optBoolean("detachedTransport"),
alleycatHost = if (obj.has("alleycatHost")) obj.getString("alleycatHost") else null,
alleycatNodeId = obj.optString("alleycatNodeId").ifBlank { null },
alleycatRelay = obj.optString("alleycatRelay").ifBlank { null },
Expand All @@ -226,6 +229,7 @@ fun SavedServer.toRecord(context: Context? = null) = SavedServerRecord(
sshPortForwardingEnabled = sshPortForwardingEnabled,
websocketUrl = websocketURL,
rememberedByUser = rememberedByUser,
detachedTransport = detachedTransport,
alleycatHost = alleycatHost,
alleycatUdpPort = alleycatUdpPort,
alleycatNodeId = alleycatNodeId,
Expand Down Expand Up @@ -368,6 +372,20 @@ object SavedServerStore {
}
}

fun updateDetachedTransport(context: Context, serverId: String, detachedTransport: Boolean) {
val existing = load(context)
val updated = existing.map { server ->
if (server.id == serverId) {
if (server.detachedTransport != detachedTransport) server.copy(detachedTransport = detachedTransport) else server
} else {
server
}
}
if (updated != existing) {
save(context, updated)
}
}

private fun normalizedHostKey(host: String): String {
val trimmed = host.trim().trimStart('[').trimEnd(']').replace("%25", "%")
val withoutScope = if (!trimmed.contains(":")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ fun DiscoveryScreen(
host = agentContext.host,
stateRoot = sshBridgeStateRoot(context, agentContext.host),
runtimeKinds = selectedKinds,
transport = SshBridgeTransport.EPHEMERAL,
transport = if (agentContext.server.detachedTransport) SshBridgeTransport.DETACHED else SshBridgeTransport.EPHEMERAL,
)
val server = agentContext.server.copy(
id = result.serverId,
Expand Down Expand Up @@ -1267,6 +1267,7 @@ internal fun SSHLoginDialog(
var unlockMacosKeychain by remember(server.id) {
mutableStateOf(initialCredential?.unlockMacosKeychain ?: false)
}
var detachedTransport by remember(server.id) { mutableStateOf(server.detachedTransport) }
var isConnecting by remember(server.id) { mutableStateOf(false) }
var errorMessage by remember(server.id) { mutableStateOf<String?>(null) }
val hostDisplay = if (server.resolvedSshPort == 22) {
Expand Down Expand Up @@ -1394,6 +1395,31 @@ internal fun SSHLoginDialog(
fontSize = 12.sp,
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Switch(
checked = detachedTransport,
onCheckedChange = {
detachedTransport = it
SavedServerStore(context).updateDetachedTransport(context, server.id, it)
},
enabled = !isConnecting,
)
Column {
Text(
text = "Detached sessions",
color = LitterTheme.textPrimary,
fontSize = 12.sp,
)
Text(
text = "Agent sessions survive app close on this server. They keep running on the host and re-attach when you return.",
color = LitterTheme.textSecondary,
fontSize = 11.sp,
)
}
}
if (errorMessage != null) {
Text(
text = errorMessage!!,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ private fun ServerEditSheet(
sshPortForwardingEnabled = null,
websocketURL = null,
rememberedByUser = true,
detachedTransport = false,
)
}
ServerConnectionMode.SSH -> {
Expand Down Expand Up @@ -693,6 +694,7 @@ private fun ServerEditSheet(
sshPortForwardingEnabled = null,
websocketURL = null,
rememberedByUser = true,
detachedTransport = false,
)
}
ServerConnectionMode.DIRECT_CODEX -> {
Expand Down Expand Up @@ -721,6 +723,7 @@ private fun ServerEditSheet(
sshPortForwardingEnabled = null,
websocketURL = null,
rememberedByUser = true,
detachedTransport = false,
)
}
ServerConnectionMode.WEBSOCKET -> {
Expand Down Expand Up @@ -750,6 +753,7 @@ private fun ServerEditSheet(
sshPortForwardingEnabled = null,
websocketURL = rawURL,
rememberedByUser = true,
detachedTransport = false,
)
}
ServerConnectionMode.SLINGSHOT -> {
Expand All @@ -760,6 +764,7 @@ private fun ServerEditSheet(
saved.copy(
name = name,
rememberedByUser = true,
detachedTransport = false,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,51 @@ class SavedServerTransportTest {
port = 0,
alleycatNodeId = nodeId,
)

@Test
fun detachedTransportDefaultsToFalse() {
val server =
SavedServer(
id = "server-detached-default",
name = "SSH Host",
hostname = "10.0.0.5",
port = 22,
source = "ssh",
)

assertFalse(server.detachedTransport)
}

@Test
fun detachedTransportPersistsThroughJsonRoundTrip() {
val server =
SavedServer(
id = "server-detached-json",
name = "SSH Host",
hostname = "10.0.0.5",
port = 22,
detachedTransport = true,
)

val restored = SavedServer.fromJson(server.toJson().toString())

assertTrue(restored.detachedTransport)
}

@Test
fun detachedTransportDefaultsToFalseWhenMissingFromJson() {
val legacy =
SavedServer(
id = "server-legacy",
name = "SSH Host",
hostname = "10.0.0.5",
port = 22,
)
val json = legacy.toJson().toString()
.replace("\"detachedTransport\":false", "")

val restored = SavedServer.fromJson(json)

assertFalse(restored.detachedTransport)
}
}
2 changes: 2 additions & 0 deletions shared/rust-bridge/codex-mobile-client/src/ffi/reconnect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,7 @@ mod tests {
ssh_port_forwarding_enabled: None,
websocket_url: None,
remembered_by_user: true,
detached_transport: false,
alleycat_host: None,
alleycat_udp_port: None,
alleycat_node_id: None,
Expand Down Expand Up @@ -773,6 +774,7 @@ mod tests {
ssh_port_forwarding_enabled: None,
websocket_url: None,
remembered_by_user: true,
detached_transport: false,
alleycat_host: None,
alleycat_udp_port: None,
alleycat_node_id: None,
Expand Down
2 changes: 2 additions & 0 deletions shared/rust-bridge/codex-mobile-client/src/reconnect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub struct SavedServerRecord {
pub ssh_port_forwarding_enabled: Option<bool>,
pub websocket_url: Option<String>,
pub remembered_by_user: bool,
pub detached_transport: bool,
/// Legacy Alleycat marker. Unsupported after the iroh-backed host migration;
/// records with only these fields require a new pairing scan.
pub alleycat_host: Option<String>,
Expand Down Expand Up @@ -917,6 +918,7 @@ mod tests {
ssh_port_forwarding_enabled: None,
websocket_url: None,
remembered_by_user: true,
detached_transport: false,
alleycat_host: None,
alleycat_udp_port: None,
alleycat_node_id: None,
Expand Down