feat: move talk mode to overlay button
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- macOS: Voice Wake now fully tears down the Speech pipeline when disabled (cancel pending restarts, drop stale callbacks) to avoid high CPU in the background.
|
- macOS: Voice Wake now fully tears down the Speech pipeline when disabled (cancel pending restarts, drop stale callbacks) to avoid high CPU in the background.
|
||||||
|
- macOS menu: add a Talk Mode action alongside the Open Dashboard/Chat/Canvas entries.
|
||||||
- iOS/Android nodes: enable scrolling for loaded web pages in the Canvas WebView (default scaffold stays touch-first).
|
- iOS/Android nodes: enable scrolling for loaded web pages in the Canvas WebView (default scaffold stays touch-first).
|
||||||
- macOS menu: device list now uses `node.list` (devices only; no agent/tool presence entries).
|
- macOS menu: device list now uses `node.list` (devices only; no agent/tool presence entries).
|
||||||
- macOS menu: device list now shows connected nodes only.
|
- macOS menu: device list now shows connected nodes only.
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
- iOS/Android nodes: status pill now surfaces camera activity instead of overlay toasts.
|
- iOS/Android nodes: status pill now surfaces camera activity instead of overlay toasts.
|
||||||
- iOS/Android/macOS nodes: camera snaps recompress to keep base64 payloads under 5 MB.
|
- iOS/Android/macOS nodes: camera snaps recompress to keep base64 payloads under 5 MB.
|
||||||
- iOS/Android nodes: status pill now surfaces pairing, screen recording, voice wake, and foreground-required states.
|
- iOS/Android nodes: status pill now surfaces pairing, screen recording, voice wake, and foreground-required states.
|
||||||
|
- iOS/Android nodes: Talk Mode now lives on a side bubble (with an iOS toggle to hide it), and Android settings no longer show the Talk Mode switch.
|
||||||
- macOS menu: top status line now shows pending node pairing approvals (incl. repairs).
|
- macOS menu: top status line now shows pending node pairing approvals (incl. repairs).
|
||||||
- CLI: avoid spurious gateway close errors after successful request/response cycles.
|
- CLI: avoid spurious gateway close errors after successful request/response cycles.
|
||||||
- Agent runtime: clamp tool-result images to the 5MB Anthropic limit to avoid hard request rejections.
|
- Agent runtime: clamp tool-result images to the 5MB Anthropic limit to avoid hard request rejections.
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import android.webkit.WebResourceError
|
|||||||
import android.webkit.WebResourceRequest
|
import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebResourceResponse
|
import android.webkit.WebResourceResponse
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -28,6 +30,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||||||
import androidx.compose.material3.FilledTonalIconButton
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -72,6 +76,11 @@ fun RootScreen(viewModel: MainViewModel) {
|
|||||||
val screenRecordActive by viewModel.screenRecordActive.collectAsState()
|
val screenRecordActive by viewModel.screenRecordActive.collectAsState()
|
||||||
val isForeground by viewModel.isForeground.collectAsState()
|
val isForeground by viewModel.isForeground.collectAsState()
|
||||||
val voiceWakeStatusText by viewModel.voiceWakeStatusText.collectAsState()
|
val voiceWakeStatusText by viewModel.voiceWakeStatusText.collectAsState()
|
||||||
|
val talkEnabled by viewModel.talkEnabled.collectAsState()
|
||||||
|
val audioPermissionLauncher =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
|
if (granted) viewModel.setTalkEnabled(true)
|
||||||
|
}
|
||||||
val activity =
|
val activity =
|
||||||
remember(cameraHud, screenRecordActive, isForeground, statusText, voiceWakeStatusText) {
|
remember(cameraHud, screenRecordActive, isForeground, statusText, voiceWakeStatusText) {
|
||||||
// Status pill owns transient activity state so it doesn't overlap the connection indicator.
|
// Status pill owns transient activity state so it doesn't overlap the connection indicator.
|
||||||
@@ -211,6 +220,30 @@ fun RootScreen(viewModel: MainViewModel) {
|
|||||||
icon = { Icon(Icons.Default.ChatBubble, contentDescription = "Chat") },
|
icon = { Icon(Icons.Default.ChatBubble, contentDescription = "Chat") },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Talk mode gets a dedicated side bubble instead of burying it in settings.
|
||||||
|
OverlayIconButton(
|
||||||
|
onClick = {
|
||||||
|
val next = !talkEnabled
|
||||||
|
if (next) {
|
||||||
|
val micOk =
|
||||||
|
ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) ==
|
||||||
|
PackageManager.PERMISSION_GRANTED
|
||||||
|
if (!micOk) audioPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
||||||
|
viewModel.setTalkEnabled(true)
|
||||||
|
} else {
|
||||||
|
viewModel.setTalkEnabled(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
val tint = if (talkEnabled) MaterialTheme.colorScheme.primary else LocalContentColor.current
|
||||||
|
Icon(
|
||||||
|
Icons.Default.RecordVoiceOver,
|
||||||
|
contentDescription = "Talk Mode",
|
||||||
|
tint = tint,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
OverlayIconButton(
|
OverlayIconButton(
|
||||||
onClick = { sheet = Sheet.Settings },
|
onClick = { sheet = Sheet.Settings },
|
||||||
icon = { Icon(Icons.Default.Settings, contentDescription = "Settings") },
|
icon = { Icon(Icons.Default.Settings, contentDescription = "Settings") },
|
||||||
|
|||||||
@@ -62,8 +62,6 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
val wakeWords by viewModel.wakeWords.collectAsState()
|
val wakeWords by viewModel.wakeWords.collectAsState()
|
||||||
val voiceWakeMode by viewModel.voiceWakeMode.collectAsState()
|
val voiceWakeMode by viewModel.voiceWakeMode.collectAsState()
|
||||||
val voiceWakeStatusText by viewModel.voiceWakeStatusText.collectAsState()
|
val voiceWakeStatusText by viewModel.voiceWakeStatusText.collectAsState()
|
||||||
val talkEnabled by viewModel.talkEnabled.collectAsState()
|
|
||||||
val talkStatusText by viewModel.talkStatusText.collectAsState()
|
|
||||||
val isConnected by viewModel.isConnected.collectAsState()
|
val isConnected by viewModel.isConnected.collectAsState()
|
||||||
val manualEnabled by viewModel.manualEnabled.collectAsState()
|
val manualEnabled by viewModel.manualEnabled.collectAsState()
|
||||||
val manualHost by viewModel.manualHost.collectAsState()
|
val manualHost by viewModel.manualHost.collectAsState()
|
||||||
@@ -309,28 +307,6 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
|
|
||||||
// Voice
|
// Voice
|
||||||
item { Text("Voice", style = MaterialTheme.typography.titleSmall) }
|
item { Text("Voice", style = MaterialTheme.typography.titleSmall) }
|
||||||
item {
|
|
||||||
ListItem(
|
|
||||||
headlineContent = { Text("Talk Mode") },
|
|
||||||
supportingContent = { Text(talkStatusText) },
|
|
||||||
trailingContent = {
|
|
||||||
Switch(
|
|
||||||
checked = talkEnabled,
|
|
||||||
onCheckedChange = { on ->
|
|
||||||
if (on) {
|
|
||||||
val micOk =
|
|
||||||
ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) ==
|
|
||||||
PackageManager.PERMISSION_GRANTED
|
|
||||||
if (!micOk) audioPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
|
||||||
viewModel.setTalkEnabled(true)
|
|
||||||
} else {
|
|
||||||
viewModel.setTalkEnabled(false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item {
|
item {
|
||||||
val enabled = voiceWakeMode != VoiceWakeMode.Off
|
val enabled = voiceWakeMode != VoiceWakeMode.Off
|
||||||
ListItem(
|
ListItem(
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ struct RootCanvas: View {
|
|||||||
|
|
||||||
private struct CanvasContent: View {
|
private struct CanvasContent: View {
|
||||||
@Environment(NodeAppModel.self) private var appModel
|
@Environment(NodeAppModel.self) private var appModel
|
||||||
|
@AppStorage("talk.enabled") private var talkEnabled: Bool = false
|
||||||
|
@AppStorage("talk.button.enabled") private var talkButtonEnabled: Bool = true
|
||||||
var systemColorScheme: ColorScheme
|
var systemColorScheme: ColorScheme
|
||||||
var bridgeStatus: StatusPill.BridgeState
|
var bridgeStatus: StatusPill.BridgeState
|
||||||
var voiceWakeEnabled: Bool
|
var voiceWakeEnabled: Bool
|
||||||
@@ -141,6 +143,19 @@ private struct CanvasContent: View {
|
|||||||
}
|
}
|
||||||
.accessibilityLabel("Chat")
|
.accessibilityLabel("Chat")
|
||||||
|
|
||||||
|
if self.talkButtonEnabled {
|
||||||
|
// Talk mode lives on a side bubble so it doesn't get buried in settings.
|
||||||
|
OverlayButton(
|
||||||
|
systemImage: self.appModel.talkMode.isEnabled ? "waveform.circle.fill" : "waveform.circle",
|
||||||
|
brighten: self.brightenButtons)
|
||||||
|
{
|
||||||
|
let next = !self.appModel.talkMode.isEnabled
|
||||||
|
self.talkEnabled = next
|
||||||
|
self.appModel.setTalkEnabled(next)
|
||||||
|
}
|
||||||
|
.accessibilityLabel("Talk Mode")
|
||||||
|
}
|
||||||
|
|
||||||
OverlayButton(systemImage: "gearshape.fill", brighten: self.brightenButtons) {
|
OverlayButton(systemImage: "gearshape.fill", brighten: self.brightenButtons) {
|
||||||
self.openSettings()
|
self.openSettings()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ struct SettingsTab: View {
|
|||||||
@AppStorage("node.instanceId") private var instanceId: String = UUID().uuidString
|
@AppStorage("node.instanceId") private var instanceId: String = UUID().uuidString
|
||||||
@AppStorage("voiceWake.enabled") private var voiceWakeEnabled: Bool = false
|
@AppStorage("voiceWake.enabled") private var voiceWakeEnabled: Bool = false
|
||||||
@AppStorage("talk.enabled") private var talkEnabled: Bool = false
|
@AppStorage("talk.enabled") private var talkEnabled: Bool = false
|
||||||
|
@AppStorage("talk.button.enabled") private var talkButtonEnabled: Bool = true
|
||||||
@AppStorage("camera.enabled") private var cameraEnabled: Bool = true
|
@AppStorage("camera.enabled") private var cameraEnabled: Bool = true
|
||||||
@AppStorage("screen.preventSleep") private var preventSleep: Bool = true
|
@AppStorage("screen.preventSleep") private var preventSleep: Bool = true
|
||||||
@AppStorage("bridge.preferredStableID") private var preferredBridgeStableID: String = ""
|
@AppStorage("bridge.preferredStableID") private var preferredBridgeStableID: String = ""
|
||||||
@@ -161,6 +162,8 @@ struct SettingsTab: View {
|
|||||||
.onChange(of: self.talkEnabled) { _, newValue in
|
.onChange(of: self.talkEnabled) { _, newValue in
|
||||||
self.appModel.setTalkEnabled(newValue)
|
self.appModel.setTalkEnabled(newValue)
|
||||||
}
|
}
|
||||||
|
// Keep this separate so users can hide the side bubble without disabling Talk Mode.
|
||||||
|
Toggle("Show Talk Button", isOn: self.$talkButtonEnabled)
|
||||||
|
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
VoiceWakeWordsSettingsView()
|
VoiceWakeWordsSettingsView()
|
||||||
|
|||||||
Reference in New Issue
Block a user