Merge remote-tracking branch 'origin/main'

This commit is contained in:
Peter Steinberger
2025-12-14 02:37:13 +00:00
4 changed files with 105 additions and 60 deletions

View File

@@ -1,6 +1,5 @@
plugins {
id("com.android.application") version "8.5.2" apply false
id("com.android.application") version "8.6.1" apply false
id("org.jetbrains.kotlin.android") version "1.9.24" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.24" apply false
}

View File

@@ -1,4 +1,5 @@
import SwiftUI
import UIKit
struct RootTabs: View {
@EnvironmentObject private var appModel: NodeAppModel
@@ -13,29 +14,13 @@ struct RootTabs: View {
.tabItem { Label("Voice", systemImage: "mic") }
SettingsTab()
.tabItem {
VStack {
ZStack(alignment: .topTrailing) {
Image(systemName: "gearshape")
Circle()
.fill(self.settingsIndicatorColor)
.frame(width: 9, height: 9)
.overlay(
Circle()
.stroke(.black.opacity(0.2), lineWidth: 0.5))
.shadow(
color: self.settingsIndicatorGlowColor,
radius: self.settingsIndicatorGlowRadius,
x: 0,
y: 0)
.scaleEffect(self.settingsIndicatorScale)
.opacity(self.settingsIndicatorOpacity)
.offset(x: 7, y: -2)
}
Text("Settings")
}
}
.tabItem { Label("Settings", systemImage: "gearshape") }
}
.background(TabBarControllerAccessor { tabBarController in
guard let item = tabBarController.tabBar.items?[Self.settingsTabIndex] else { return }
item.badgeValue = ""
item.badgeColor = self.settingsBadgeColor
})
.onAppear { self.updateConnectingPulse(for: self.bridgeIndicatorState) }
.onChange(of: self.bridgeIndicatorState) { _, newValue in
self.updateConnectingPulse(for: newValue)
@@ -48,55 +33,25 @@ struct RootTabs: View {
case disconnected
}
private static let settingsTabIndex = 2
private var bridgeIndicatorState: BridgeIndicatorState {
if self.appModel.bridgeServerName != nil { return .connected }
if self.appModel.bridgeStatusText.localizedCaseInsensitiveContains("connecting") { return .connecting }
return .disconnected
}
private var settingsIndicatorColor: Color {
private var settingsBadgeColor: UIColor {
switch self.bridgeIndicatorState {
case .connected:
Color.green
UIColor.systemGreen
case .connecting:
Color.yellow
UIColor.systemYellow.withAlphaComponent(self.isConnectingPulse ? 1.0 : 0.6)
case .disconnected:
Color.red
UIColor.systemRed
}
}
private var settingsIndicatorGlowColor: Color {
switch self.bridgeIndicatorState {
case .connected:
Color.green.opacity(0.75)
case .connecting:
Color.yellow.opacity(0.6)
case .disconnected:
Color.clear
}
}
private var settingsIndicatorGlowRadius: CGFloat {
switch self.bridgeIndicatorState {
case .connected:
6
case .connecting:
self.isConnectingPulse ? 6 : 3
case .disconnected:
0
}
}
private var settingsIndicatorScale: CGFloat {
guard self.bridgeIndicatorState == .connecting else { return 1 }
return self.isConnectingPulse ? 1.12 : 0.96
}
private var settingsIndicatorOpacity: Double {
guard self.bridgeIndicatorState == .connecting else { return 1 }
return self.isConnectingPulse ? 1.0 : 0.75
}
private func updateConnectingPulse(for state: BridgeIndicatorState) {
guard state == .connecting else {
withAnimation(.easeOut(duration: 0.2)) { self.isConnectingPulse = false }
@@ -109,3 +64,41 @@ struct RootTabs: View {
}
}
}
private struct TabBarControllerAccessor: UIViewControllerRepresentable {
let onResolve: (UITabBarController) -> Void
func makeUIViewController(context: Context) -> ResolverViewController {
ResolverViewController(onResolve: self.onResolve)
}
func updateUIViewController(_ uiViewController: ResolverViewController, context: Context) {
uiViewController.onResolve = self.onResolve
uiViewController.resolveIfPossible()
}
}
private final class ResolverViewController: UIViewController {
var onResolve: (UITabBarController) -> Void
init(onResolve: @escaping (UITabBarController) -> Void) {
self.onResolve = onResolve
super.init(nibName: nil, bundle: nil)
self.view.isHidden = true
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.resolveIfPossible()
}
func resolveIfPossible() {
guard let tabBarController = self.tabBarController else { return }
self.onResolve(tabBarController)
}
}

View File

@@ -0,0 +1,43 @@
import ClawdisKit
import Foundation
import Testing
@Suite struct DeepLinkParserTests {
@Test func parseRejectsNonClawdisScheme() {
let url = URL(string: "https://example.com/agent?message=hi")!
#expect(DeepLinkParser.parse(url) == nil)
}
@Test func parseRejectsEmptyMessage() {
let url = URL(string: "clawdis://agent?message=%20%20%0A")!
#expect(DeepLinkParser.parse(url) == nil)
}
@Test func parseAgentLinkParsesCommonFields() {
let url = URL(string: "clawdis://agent?message=Hello&deliver=1&sessionKey=node-iris&thinking=low&timeoutSeconds=30")!
#expect(
DeepLinkParser.parse(url) == .agent(
.init(
message: "Hello",
sessionKey: "node-iris",
thinking: "low",
deliver: true,
to: nil,
channel: nil,
timeoutSeconds: 30,
key: nil)))
}
@Test func parseRejectsNegativeTimeoutSeconds() {
let url = URL(string: "clawdis://agent?message=Hello&timeoutSeconds=-1")!
#expect(DeepLinkParser.parse(url) == .agent(.init(
message: "Hello",
sessionKey: nil,
thinking: nil,
deliver: false,
to: nil,
channel: nil,
timeoutSeconds: nil,
key: nil)))
}
}

View File

@@ -9,6 +9,16 @@ packages:
ClawdisKit:
path: ../shared/ClawdisKit
schemes:
Clawdis:
shared: true
build:
targets:
Clawdis: all
test:
targets:
- ClawdisTests
targets:
Clawdis:
type: application