feat(settings): promote Quick Controls to the top-level Settings landing

Persistent connection is a connection-level control flipped frequently, not chat-specific, so move the Quick Controls card out of Chat settings to the Settings root (beside the Active Agent / Profile lock cards) and add a Turn-complete alerts toggle. Both wire to the existing ConnectionViewModel flows (gatewayKeepAlive / notifyTurnComplete) - no new prefs. Removes the card from ChatSettingsScreen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bailey Dixon
2026-06-30 10:16:46 -04:00
co-authored by Claude Opus 4.8
parent 9142fb8f79
commit 62961322b9
2 changed files with 107 additions and 58 deletions
@@ -127,64 +127,10 @@ fun ChatSettingsScreen(
.padding(horizontal = 16.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
// Quick Controls — the most-reached-for chat controls, pinned to the
// very top of Settings. Intentionally extensible: more quick toggles
// can join this card later; for now it hosts only Persistent
// connection (relocated from the mid-list section below).
Text(
text = "Quick Controls",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
)
Card(
modifier = Modifier
.fillMaxWidth()
.gradientBorder(
shape = RoundedCornerShape(12.dp),
isDarkTheme = isDarkTheme
),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Persistent connection — opt-in, both flavors. Holds the
// app's connection to Hermes open in the background via a
// persistent notification so messages and live features stay
// responsive (relay-paired setups also keep device control +
// notification mirroring reachable). Off by default; uses
// more battery. Same pref/key as the former mid-list toggle.
val gatewayKeepAlive by connectionViewModel.gatewayKeepAlive.collectAsState()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Persistent connection",
style = MaterialTheme.typography.bodyMedium
)
Text(
text = if (gatewayKeepAlive) {
"Keeping your connection to Hermes open in the background"
} else {
"Connect on demand only · saves battery"
},
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
checked = gatewayKeepAlive,
onCheckedChange = { connectionViewModel.setGatewayKeepAlive(it) }
)
}
}
}
// (Quick Controls — Persistent connection etc. — moved to the
// top-level Settings landing, since they're connection-level controls
// flipped frequently, not chat-specific. See SettingsScreen's
// QuickControlsCard.)
Card(
modifier = Modifier
.fillMaxWidth()
@@ -377,6 +377,17 @@ fun SettingsScreen(
isDarkTheme = isDarkTheme,
)
// ── Quick Controls ─────────────────────────────────────────
// The switches flipped most often, pinned to the top-level Settings
// landing instead of buried in a sub-screen. Persistent connection is
// connection-level (not chat-specific), so it belongs here beside the
// agent / profile cards. Extensible — add more frequently-toggled
// switches in QuickControlsCard.
QuickControlsCard(
connectionViewModel = connectionViewModel,
isDarkTheme = isDarkTheme,
)
// (The "Active Connection quick-look card" that used to live
// here — showing API / Relay / Session status rows with a
// clickable shortcut into a separate singular-connection
@@ -780,6 +791,98 @@ private fun ProfileLockCard(
}
}
/**
* Quick Controls card on the top-level Settings landing — the switches the user
* flips most often (Persistent connection, turn-complete alerts), kept out of
* the per-feature sub-screens so they're one tap from the Settings root. Wired
* straight to the same ConnectionViewModel flows the sub-screens use.
*/
@Composable
private fun QuickControlsCard(
connectionViewModel: ConnectionViewModel,
isDarkTheme: Boolean,
) {
val gatewayKeepAlive by connectionViewModel.gatewayKeepAlive.collectAsState()
val notifyTurnComplete by connectionViewModel.notifyTurnComplete.collectAsState()
Card(
modifier = Modifier
.fillMaxWidth()
.gradientBorder(
shape = RoundedCornerShape(12.dp),
isDarkTheme = isDarkTheme,
),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
),
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
text = "Quick Controls",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
)
// Persistent connection — connection-level keep-alive: holds the app
// process up via a notification so the gateway chat socket (and, for
// relay-paired setups, device control + notification mirroring) stays
// reachable in the background. Off by default; uses more battery.
QuickControlToggle(
title = "Persistent connection",
subtitle = if (gatewayKeepAlive) {
"Keeping your connection to Hermes open in the background"
} else {
"Connect on demand only · saves battery"
},
checked = gatewayKeepAlive,
onCheckedChange = { connectionViewModel.setGatewayKeepAlive(it) },
)
HorizontalDivider()
QuickControlToggle(
title = "Turn-complete alerts",
subtitle = if (notifyTurnComplete) {
"Notify when a reply finishes while the app is in the background"
} else {
"No alert when a backgrounded reply finishes"
},
checked = notifyTurnComplete,
onCheckedChange = { connectionViewModel.setNotifyTurnComplete(it) },
)
}
}
}
@Composable
private fun QuickControlToggle(
title: String,
subtitle: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Spacer(modifier = Modifier.width(12.dp))
Switch(checked = checked, onCheckedChange = onCheckedChange)
}
}
/**
* The one surface that ALWAYS lists every profile (it never gates on the lock
* state — it's how the user picks the target or unlocks). A master "Lock to a