feat(ssh): integrate SSH terminal into settings navigation

- Add SshTerminal to SettingsRoute enum
- Add SSH entry in settings list (ShellScreen -> SettingsShellScreen)
- Create SshTerminalSettingsScreen hosting the SSH terminal UI
- Copy SSH source files to app/src/main/java source root
- Wire up SettingsDetailScreen dispatch for SshTerminal route
This commit is contained in:
茂之钳
2026-05-23 07:33:03 +00:00
parent 5f03ffc305
commit a537ae9ef1
6 changed files with 673 additions and 4 deletions
@@ -24,6 +24,9 @@ import ai.openclaw.app.ui.design.ClawStatusPill
import ai.openclaw.app.ui.design.ClawTextBadge
import ai.openclaw.app.ui.design.ClawTextField
import ai.openclaw.app.ui.design.ClawTheme
import ai.openclaw.app.ui.tergent.SshTerminalManager
import ai.openclaw.app.ui.tergent.SshConnectDialog
import ai.openclaw.app.ui.tergent.SshTerminalScreen
import android.Manifest
import android.content.Context
import android.content.Intent
@@ -45,6 +48,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
@@ -54,6 +58,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material3.IconButton
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.automirrored.filled.ScreenShare
@@ -77,12 +82,13 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -111,6 +117,7 @@ internal enum class SettingsRoute {
Appearance,
Health,
About,
SshTerminal,
}
@Composable
@@ -138,6 +145,7 @@ internal fun SettingsDetailScreen(
SettingsRoute.Appearance -> AppearanceSettingsScreen(onBack = onBack)
SettingsRoute.Health -> HealthLogsSettingsScreen(viewModel = viewModel, onBack = onBack)
SettingsRoute.About -> AboutSettingsScreen(viewModel = viewModel, onBack = onBack)
SettingsRoute.SshTerminal -> SshTerminalSettingsScreen(viewModel = viewModel, onBack = onBack)
}
}
@@ -1144,3 +1152,58 @@ private fun openNotificationListenerSettings(context: Context) {
val intent = Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
@Composable
internal fun SshTerminalSettingsScreen(
viewModel: MainViewModel,
onBack: () -> Unit,
) {
val manager = remember { SshTerminalManager() }
var showConnectDialog by remember { mutableStateOf(false) }
DisposableEffect(Unit) {
onDispose {
manager.disconnect()
}
}
if (showConnectDialog) {
ai.openclaw.app.ui.tergent.SshConnectDialog(
onDismiss = { showConnectDialog = false },
onConnect = { config ->
showConnectDialog = false
manager.connect(config)
}
)
}
Box(modifier = Modifier.fillMaxSize()) {
ai.openclaw.app.ui.tergent.SshTerminalScreen(
manager = manager,
onDisconnect = {
manager.disconnect()
onBack()
},
onShowConnectDialog = { showConnectDialog = true },
modifier = Modifier.fillMaxSize(),
)
// Back button overlay
IconButton(
onClick = {
manager.disconnect()
onBack()
},
modifier = Modifier
.align(Alignment.TopStart)
.padding(8.dp)
.size(36.dp),
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
tint = Color(0xFF9CA3AF),
)
}
}
}
@@ -38,6 +38,7 @@ import androidx.compose.material.icons.automirrored.filled.ExitToApp
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.automirrored.filled.ScreenShare
import androidx.compose.material.icons.filled.Cloud
import androidx.compose.material.icons.filled.Computer
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.Notifications
@@ -724,6 +725,7 @@ private fun SettingsShellScreen(
SettingsRow("Phone Capabilities", if (cameraEnabled) "Camera enabled" else "Locked", Icons.Default.Lock, status = !cameraEnabled, route = SettingsRoute.PhoneCapabilities),
SettingsRow("Gateway", gatewaySummary(statusText, isConnected), Icons.Default.Cloud, status = isConnected, route = SettingsRoute.Gateway),
SettingsRow("Appearance", "Dark", Icons.Default.Palette, route = SettingsRoute.Appearance),
SettingsRow("SSH 终端", "Remote shell", Icons.Default.Computer, route = SettingsRoute.SshTerminal),
SettingsRow("Health", "Diagnostics", Icons.Default.Settings, status = isConnected, route = SettingsRoute.Health),
SettingsRow("About", "Version and update", Icons.Default.Storage, route = SettingsRoute.About),
),