Merge remote-tracking branch 'origin/main'

This commit is contained in:
Peter Steinberger
2025-12-14 05:32:24 +00:00
5 changed files with 48 additions and 32 deletions

View File

@@ -3,9 +3,12 @@ package com.steipete.clawdis.node.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -26,7 +29,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.zIndex import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import com.steipete.clawdis.node.MainViewModel import com.steipete.clawdis.node.MainViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -37,23 +41,22 @@ fun RootScreen(viewModel: MainViewModel) {
val safeButtonInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) val safeButtonInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
CanvasView(viewModel = viewModel, modifier = Modifier.fillMaxSize().zIndex(0f)) CanvasView(viewModel = viewModel, modifier = Modifier.fillMaxSize())
}
Box( // Keep the overlay buttons above the WebView canvas (AndroidView), otherwise they may not receive touches.
Popup(alignment = Alignment.TopCenter, properties = PopupProperties(focusable = false)) {
Row(
modifier = modifier =
Modifier Modifier
.fillMaxSize() .fillMaxWidth()
.zIndex(1f)
.windowInsetsPadding(safeButtonInsets) .windowInsetsPadding(safeButtonInsets)
.padding(12.dp), .padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) { ) {
Box(modifier = Modifier.align(Alignment.TopStart)) { Button(onClick = { sheet = Sheet.Chat }) { Text("Chat") }
Button(onClick = { sheet = Sheet.Chat }) { Text("Chat") } Button(onClick = { sheet = Sheet.Settings }) { Text("Settings") }
}
Box(modifier = Modifier.align(Alignment.TopEnd)) {
Button(onClick = { sheet = Sheet.Settings }) { Text("Settings") }
}
} }
} }
@@ -65,7 +68,6 @@ fun RootScreen(viewModel: MainViewModel) {
when (sheet) { when (sheet) {
Sheet.Chat -> ChatSheet(viewModel = viewModel) Sheet.Chat -> ChatSheet(viewModel = viewModel)
Sheet.Settings -> SettingsSheet(viewModel = viewModel) Sheet.Settings -> SettingsSheet(viewModel = viewModel)
null -> {}
} }
} }
} }

View File

@@ -9,10 +9,16 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
@@ -62,7 +68,12 @@ fun SettingsSheet(viewModel: MainViewModel) {
LazyColumn( LazyColumn(
state = listState, state = listState,
modifier = Modifier.fillMaxWidth().imePadding(), modifier =
Modifier
.fillMaxWidth()
.fillMaxHeight()
.imePadding()
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)),
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(14.dp), verticalArrangement = Arrangement.spacedBy(14.dp),
) { ) {

View File

@@ -6,7 +6,7 @@ struct MasterDiscoveryInlineList: View {
var discovery: MasterDiscoveryModel var discovery: MasterDiscoveryModel
var currentTarget: String? var currentTarget: String?
var onSelect: (MasterDiscoveryModel.DiscoveredMaster) -> Void var onSelect: (MasterDiscoveryModel.DiscoveredMaster) -> Void
@State private var hoveredMasterID: MasterDiscoveryModel.DiscoveredMaster.ID? @State private var hoveredGatewayID: MasterDiscoveryModel.DiscoveredMaster.ID?
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
@@ -25,19 +25,19 @@ struct MasterDiscoveryInlineList: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} else { } else {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
ForEach(self.discovery.masters.prefix(6)) { master in ForEach(self.discovery.masters.prefix(6)) { gateway in
let target = self.suggestedSSHTarget(master) let target = self.suggestedSSHTarget(gateway)
let selected = target != nil && self.currentTarget? let selected = target != nil && self.currentTarget?
.trimmingCharacters(in: .whitespacesAndNewlines) == target .trimmingCharacters(in: .whitespacesAndNewlines) == target
Button { Button {
withAnimation(.spring(response: 0.25, dampingFraction: 0.9)) { withAnimation(.spring(response: 0.25, dampingFraction: 0.9)) {
self.onSelect(master) self.onSelect(gateway)
} }
} label: { } label: {
HStack(alignment: .center, spacing: 10) { HStack(alignment: .center, spacing: 10) {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(master.displayName) Text(gateway.displayName)
.font(.callout.weight(.semibold)) .font(.callout.weight(.semibold))
.lineLimit(1) .lineLimit(1)
.truncationMode(.tail) .truncationMode(.tail)
@@ -65,7 +65,7 @@ struct MasterDiscoveryInlineList: View {
RoundedRectangle(cornerRadius: 10, style: .continuous) RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(self.rowBackground( .fill(self.rowBackground(
selected: selected, selected: selected,
hovered: self.hoveredMasterID == master.id))) hovered: self.hoveredGatewayID == gateway.id)))
.overlay( .overlay(
RoundedRectangle(cornerRadius: 10, style: .continuous) RoundedRectangle(cornerRadius: 10, style: .continuous)
.strokeBorder( .strokeBorder(
@@ -75,8 +75,8 @@ struct MasterDiscoveryInlineList: View {
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.onHover { hovering in .onHover { hovering in
self.hoveredMasterID = hovering ? master self.hoveredGatewayID = hovering ? gateway
.id : (self.hoveredMasterID == master.id ? nil : self.hoveredMasterID) .id : (self.hoveredGatewayID == gateway.id ? nil : self.hoveredGatewayID)
} }
} }
} }
@@ -89,13 +89,13 @@ struct MasterDiscoveryInlineList: View {
.help("Click a discovered master to fill the SSH target.") .help("Click a discovered master to fill the SSH target.")
} }
private func suggestedSSHTarget(_ master: MasterDiscoveryModel.DiscoveredMaster) -> String? { private func suggestedSSHTarget(_ gateway: MasterDiscoveryModel.DiscoveredMaster) -> String? {
let host = master.tailnetDns ?? master.lanHost let host = gateway.tailnetDns ?? gateway.lanHost
guard let host else { return nil } guard let host else { return nil }
let user = NSUserName() let user = NSUserName()
var target = "\(user)@\(host)" var target = "\(user)@\(host)"
if master.sshPort != 22 { if gateway.sshPort != 22 {
target += ":\(master.sshPort)" target += ":\(gateway.sshPort)"
} }
return target return target
} }
@@ -118,8 +118,8 @@ struct MasterDiscoveryMenu: View {
Button(self.discovery.statusText) {} Button(self.discovery.statusText) {}
.disabled(true) .disabled(true)
} else { } else {
ForEach(self.discovery.masters) { master in ForEach(self.discovery.masters) { gateway in
Button(master.displayName) { self.onSelect(master) } Button(gateway.displayName) { self.onSelect(gateway) }
} }
} }
} label: { } label: {

View File

@@ -4,8 +4,8 @@ import Observation
// We use master as the on-the-wire service name; keep the model aligned with the protocol/docs. // We use master as the on-the-wire service name; keep the model aligned with the protocol/docs.
@MainActor @MainActor
// swiftlint:disable:next inclusive_language
@Observable @Observable
// swiftlint:disable:next inclusive_language
final class MasterDiscoveryModel { final class MasterDiscoveryModel {
// swiftlint:disable:next inclusive_language // swiftlint:disable:next inclusive_language
struct DiscoveredMaster: Identifiable, Equatable { struct DiscoveredMaster: Identifiable, Equatable {

View File

@@ -213,7 +213,8 @@ struct OnboardingView: View {
"The connected AI agent (e.g. Claude) can trigger powerful actions on your Mac, " + "The connected AI agent (e.g. Claude) can trigger powerful actions on your Mac, " +
"including running commands, reading/writing files, and capturing screenshots — " + "including running commands, reading/writing files, and capturing screenshots — " +
"depending on the permissions you grant.\n\n" + "depending on the permissions you grant.\n\n" +
"Only enable Clawdis if you understand the risks and trust the prompts and integrations you use.") "Only enable Clawdis if you understand the risks and trust the prompts and " +
"integrations you use.")
.font(.subheadline) .font(.subheadline)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
@@ -529,8 +530,10 @@ struct OnboardingView: View {
} }
Text( Text(
"This writes your identity to `~/.clawdis/clawdis.json` and into `AGENTS.md` inside the workspace. " + "This writes your identity to `~/.clawdis/clawdis.json` and into `AGENTS.md` " +
"Treat that workspace as the agents “memory” and consider making it a (private) git repo.") "inside the workspace. " +
"Treat that workspace as the agents “memory” and consider making it a (private) git " +
"repo.")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)