feat(ios): modernize the app with iOS 26 Liquid Glass (#98452)

* feat(ios): adopt iOS 26 Liquid Glass design

* refactor(ios): refine Liquid Glass hierarchy

* docs(ios): require Xcode 26

* refactor(ios): remove obsolete design helpers

* fix(ios): keep agent tab navigation hidden

* fix(ios): compact chat composer at rest

* refactor(ios): reduce interface density

* fix(ios): keep native string inventory current
This commit is contained in:
Peter Steinberger 2026-07-01 19:06:04 +01:00 committed by GitHub
parent 68bfa42b9b
commit 8502ef6c59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1675 additions and 1618 deletions

File diff suppressed because it is too large Load diff

55
apps/ios/DESIGN.md Normal file
View file

@ -0,0 +1,55 @@
# iOS design system
OpenClaw follows the native iOS 26 design language while keeping an iOS 18 deployment target. Use SwiftUI system structure first, Liquid Glass for interactive chrome, and quiet opaque surfaces for content.
## Principles
- Prefer `NavigationStack`, `TabView`, `List`, `Form`, toolbars, sheets, and system controls. They adopt the current platform appearance automatically.
- Reserve Liquid Glass for navigation and interactive controls. Do not apply glass to every card, row, or status surface.
- Keep content hierarchy clear with typography, spacing, and grouping before adding backgrounds.
- Use semantic colors. Red means destructive or stopped; orange means attention; green means healthy. Neutral actions use the app accent.
- Preserve Dynamic Type, VoiceOver labels, Reduce Motion, increased contrast, and 44-point touch targets.
- Use continuous corners and concentric geometry. Nested controls should visually follow their container shape.
Apple references: [Adopting Liquid Glass](https://developer.apple.com/documentation/technologyoverviews/adopting-liquid-glass), [Applying Liquid Glass to custom views](https://developer.apple.com/documentation/swiftui/applying-liquid-glass-to-custom-views), and [Build a SwiftUI app with the new design](https://developer.apple.com/videos/play/wwdc2025/323/).
## Tokens
`OpenClawProMetric` in `Sources/Design/OpenClawProComponents.swift` is the source of truth for shared geometry:
- `pagePadding`: standard page gutter
- `cardRadius`: content group radius
- `controlRadius`: inset control radius
- `compactControlSize`: compact circular control size
- `bottomScrollInset`: clearance above persistent navigation
Feature-local layout enums may define row heights and grid dimensions, but should reference the shared radius instead of introducing a new card shape.
## Components
- `OpenClawProBackground`: grouped page background
- `ProCard`: quiet content grouping; never Liquid Glass
- `ProIconBadge`, `ProValuePill`: compact semantic indicators
- `OpenClawNoticeBanner`: shared connection and runtime notices
- `OpenClawAdaptiveHeaderRow`: responsive destination heading
- `OpenClawGlassControlGroup`: performance and morphing boundary for nearby glass controls
- `openClawGlassButton(prominent:tint:)`: iOS 26 glass button with an iOS 18 bordered fallback
- `openClawTabBarBehavior()`: iOS 26 tab-bar minimization with an earlier-system no-op
## Liquid Glass rules
Use `openClawGlassButton` for primary actions, compact header controls, and navigation-adjacent controls. Use the prominent style for one primary action per region. Wrap nearby controls in `OpenClawGlassControlGroup`.
Do not place Liquid Glass behind reading content, forms, metrics, or every card in a scroll view. Excess glass weakens hierarchy, increases rendering cost, and competes with the system tab bar and navigation chrome.
Keep new iOS APIs behind `#available(iOS 26.0, *)`. The fallback must preserve the same label, action, tint meaning, accessibility, and approximate hit target.
## Review checklist
- Uses a native container or control where one exists.
- Uses shared spacing and corner tokens.
- Has one obvious primary action.
- Keeps semantic color independent from decoration.
- Works in light and dark mode, Dynamic Type, and compact phone layouts.
- Verifies iOS 26 appearance in the simulator and preserves the iOS 18 fallback path.
- Adds matched before/after evidence for a visual change.

View file

@ -18,7 +18,7 @@ This iOS app is super-alpha and internal-use only. The first public App Store re
## Exact Xcode Manual Deploy Flow
1. Prereqs:
- Xcode 16+
- Xcode 26.x
- `pnpm`
- `xcodegen`
- Apple Development signing set up in Xcode
@ -51,7 +51,7 @@ pnpm ios:open
Prereqs:
- Xcode 16+
- Xcode 26.x
- `pnpm`
- `xcodegen`
- `fastlane`

View file

@ -37,8 +37,9 @@ extension AgentProTab {
}
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
}
.navigationTitle("Agents")
.navigationBarTitleDisplayMode(.inline)
// The roster owns its large adaptive header; a second navigation title
// wastes vertical space and makes the phone tab look double-stacked.
.toolbar(.hidden, for: .navigationBar)
}
var skillsDestination: some View {

View file

@ -29,32 +29,20 @@ extension AgentProTab {
func agentDetail(for agent: AgentSummary) -> String {
let parts = [
self.normalized(agent.workspace),
self.modelLabel(for: agent),
agent.id == self.appModel.gatewayDefaultAgentId ? "default" : nil,
agent.id == self.appModel.gatewayDefaultAgentId ? "Default" : nil,
].compactMap(\.self)
return parts.isEmpty ? agent.id : parts.joined(separator: "")
}
func agentSessionSummary(_ agent: AgentSummary) -> String {
guard self.gatewayConnected else { return "0" }
if agent.id == self.activeAgentID {
return self.appModel.isOperatorGatewayConnected ? "1 running" : "0"
}
return "0"
}
func agentRuntimeSummary(_ agent: AgentSummary) -> String {
if let runtime = agent.agentruntime,
let id = runtime["id"]?.value as? String,
let normalized = self.normalized(id)
{
return normalized
}
if let model = self.modelLabel(for: agent) {
return Self.shortModelLabel(model)
}
return "default"
func agentAccessibilityLabel(
_ agent: AgentSummary,
isActive: Bool,
state: AgentRosterState) -> String
{
let status = state == .online ? "Online" : "Ready"
let selection = isActive ? "Selected" : "Not selected"
return "\(self.agentName(for: agent)), \(self.agentDetail(for: agent)), \(status), \(selection)"
}
func agentRosterState(for agent: AgentSummary) -> AgentRosterState {
@ -75,15 +63,6 @@ extension AgentProTab {
return nil
}
static func shortModelLabel(_ model: String) -> String {
let trimmed = model.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return "default" }
let leaf = trimmed.split(separator: "/").last.map(String.init) ?? trimmed
return leaf
.replacingOccurrences(of: "claude-", with: "")
.replacingOccurrences(of: "gpt-", with: "")
}
func presenceLabel(_ entry: PresenceEntry) -> String? {
self.normalized(entry.host)
?? self.normalized(entry.devicefamily)

View file

@ -16,22 +16,18 @@ extension AgentProTab {
OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)
}
} accessory: {
HStack(spacing: 10) {
self.gatewayPillButton
self.headerIconButton(
systemName: "magnifyingglass",
label: "Search agents",
action: {
withAnimation(.snappy(duration: 0.18)) {
self.agentSearchPresented.toggle()
}
})
self.headerIconButton(
systemName: "arrow.clockwise",
label: self.overviewLoading ? "Refreshing agents" : "Refresh agents",
action: {
self.overviewRefreshNonce += 1
})
OpenClawGlassControlGroup {
HStack(spacing: 10) {
self.gatewayPillButton
self.headerIconButton(
systemName: "magnifyingglass",
label: "Search agents",
action: {
withAnimation(.snappy(duration: 0.18)) {
self.agentSearchPresented.toggle()
}
})
}
}
.padding(.top, 2)
}
@ -41,15 +37,8 @@ extension AgentProTab {
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.font(.subheadline)
.padding(.horizontal, 12)
.textFieldStyle(.roundedBorder)
.frame(height: 38)
.background {
Capsule()
.fill(self.searchFieldFill)
.overlay {
Capsule().strokeBorder(self.searchFieldStroke, lineWidth: 1)
}
}
.transition(.move(edge: .top).combined(with: .opacity))
}
}
@ -63,7 +52,8 @@ extension AgentProTab {
Button(action: openSettings) {
OpenClawGatewayCompactPill()
}
.buttonStyle(.plain)
.buttonBorderShape(.capsule)
.openClawGlassButton()
.accessibilityHint("Opens Settings / Gateway")
} else {
OpenClawGatewayCompactPill()
@ -71,46 +61,32 @@ extension AgentProTab {
}
var agentFilters: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
HStack(spacing: 10) {
Picker("Agent status", selection: self.$agentRosterFilter) {
ForEach(AgentRosterFilter.allCases) { filter in
Button {
withAnimation(.snappy(duration: 0.18)) {
self.agentRosterFilter = filter
}
} label: {
Text(filter.title)
.font(.caption.weight(.semibold))
.foregroundStyle(self.agentRosterFilter == filter ? .primary : .secondary)
.padding(.horizontal, 15)
.frame(height: AgentLayout.filterHeight)
.background {
Capsule()
.fill(self.agentRosterFilter == filter
? Color.primary.opacity(0.13)
: Color.primary.opacity(0.055))
}
.overlay {
Capsule()
.strokeBorder(Color.primary.opacity(self.agentRosterFilter == filter ? 0.22 : 0.06))
}
}
.buttonStyle(.plain)
}
if self.agentFiltersActive {
self.headerIconButton(
systemName: "xmark",
label: "Clear filters",
action: {
self.agentRosterFilter = .all
self.agentSearchText = ""
})
.frame(width: AgentLayout.filterHeight, height: AgentLayout.filterHeight)
Text(filter.title).tag(filter)
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
.pickerStyle(.segmented)
if self.agentFiltersActive {
Button {
withAnimation(.snappy(duration: 0.18)) {
self.agentRosterFilter = .all
self.agentSearchText = ""
}
} label: {
Image(systemName: "xmark.circle.fill")
.font(.title3)
.foregroundStyle(.secondary)
.frame(width: 44, height: 44)
.contentShape(Circle())
}
.buttonStyle(.plain)
.accessibilityLabel("Clear filters")
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
var agentFiltersActive: Bool {
@ -246,68 +222,44 @@ extension AgentProTab {
func agentRow(_ agent: AgentSummary) -> some View {
let isActive = agent.id == self.activeAgentID
let state = self.agentRosterState(for: agent)
return HStack(alignment: .top, spacing: 12) {
self.agentAvatar(agent, state: state)
return Button {
guard !isActive else { return }
self.appModel.setSelectedAgentId(agent.id)
} label: {
HStack(alignment: .center, spacing: 12) {
self.agentAvatar(agent, state: state)
VStack(alignment: .leading, spacing: 8) {
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 6) {
Text(self.agentName(for: agent))
.font(.subheadline.weight(.semibold))
.lineLimit(1)
HStack(spacing: 4) {
Circle()
.fill(state.color)
.frame(width: 6, height: 6)
Text(state.title)
.font(.caption2.weight(.semibold))
}
.foregroundStyle(state.color)
VStack(alignment: .leading, spacing: 3) {
Text(self.agentName(for: agent))
.font(.subheadline.weight(.semibold))
.foregroundStyle(.primary)
.lineLimit(1)
}
Text(self.agentDetail(for: agent))
.font(.caption)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(1)
}
.layoutPriority(1)
HStack(spacing: 0) {
self.agentMetric(label: "Sessions", value: self.agentSessionSummary(agent))
Divider()
.frame(height: 24)
.padding(.horizontal, 12)
self.agentMetric(label: "Runtime", value: self.agentRuntimeSummary(agent))
Spacer(minLength: 8)
if isActive {
Image(systemName: "checkmark")
.font(.subheadline.weight(.semibold))
.foregroundStyle(OpenClawBrand.accent)
.frame(width: 24, height: 44)
.accessibilityHidden(true)
}
}
.layoutPriority(1)
Button {
self.appModel.setSelectedAgentId(agent.id)
} label: {
Image(systemName: isActive ? "checkmark" : "arrow.right")
.font(.caption.weight(.bold))
}
.buttonStyle(.plain)
.foregroundStyle(isActive ? OpenClawBrand.accent : .primary)
.frame(width: AgentLayout.actionButtonSize, height: AgentLayout.actionButtonSize)
.background {
Circle()
.fill(self.iconButtonFill)
.overlay {
Circle().strokeBorder(self.iconButtonStroke, lineWidth: 1)
}
}
.accessibilityLabel(isActive ? "Default agent" : "Set default agent")
}
.padding(.vertical, 14)
.padding(.horizontal, 13)
.frame(maxWidth: .infinity, minHeight: AgentLayout.rowMinHeight, alignment: .leading)
.contentShape(Rectangle())
.onTapGesture {
self.appModel.setSelectedAgentId(agent.id)
.padding(.vertical, 10)
.padding(.horizontal, 13)
.frame(maxWidth: .infinity, minHeight: AgentLayout.rowMinHeight, alignment: .leading)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityLabel(self.agentAccessibilityLabel(agent, isActive: isActive, state: state))
.accessibilityHint(isActive ? "Selected agent" : "Selects this agent")
}
func headerIconButton(
@ -319,15 +271,9 @@ extension AgentProTab {
Image(systemName: systemName)
.font(.subheadline.weight(.semibold))
.frame(width: AgentLayout.filterHeight, height: AgentLayout.filterHeight)
.background {
Circle()
.fill(self.iconButtonFill)
.overlay {
Circle().strokeBorder(self.iconButtonStroke, lineWidth: 1)
}
}
}
.buttonStyle(.plain)
.buttonBorderShape(.circle)
.openClawGlassButton()
.accessibilityLabel(label)
}
@ -338,40 +284,19 @@ extension AgentProTab {
.foregroundStyle(.white)
.minimumScaleFactor(0.62)
.lineLimit(1)
.frame(width: 48, height: 48)
.frame(width: 42, height: 42)
.background(
Circle()
.fill(
LinearGradient(
colors: [
self.agentTint(for: agent, state: state),
Color.primary.opacity(0.38),
],
startPoint: .topLeading,
endPoint: .bottomTrailing)))
.fill(self.agentTint(for: agent, state: state).gradient))
.overlay(Circle().strokeBorder(Color.white.opacity(0.18), lineWidth: 1))
Circle()
.fill(state.color)
.frame(width: 10, height: 10)
.overlay(Circle().strokeBorder(Color.primary.opacity(0.15), lineWidth: 1))
.frame(width: 9, height: 9)
.overlay(Circle().strokeBorder(Color(uiColor: .systemBackground), lineWidth: 2))
}
}
func agentMetric(label: String, value: String) -> some View {
VStack(alignment: .leading, spacing: 2) {
Text(label)
.font(.caption2)
.foregroundStyle(.secondary)
Text(value)
.font(.caption.weight(.semibold))
.foregroundStyle(.primary)
.lineLimit(1)
.minimumScaleFactor(0.74)
}
.frame(minWidth: 60, alignment: .leading)
}
func agentMenuRow(
icon: String,
title: String,
@ -562,22 +487,6 @@ extension AgentProTab {
self.appModel.isOperatorGatewayConnected
}
private var searchFieldFill: Color {
self.colorScheme == .dark ? Color.white.opacity(0.045) : Color.white.opacity(0.78)
}
private var searchFieldStroke: Color {
self.colorScheme == .dark ? Color.white.opacity(0.11) : Color.black.opacity(0.07)
}
private var iconButtonFill: Color {
self.colorScheme == .dark ? Color.white.opacity(0.065) : Color.white.opacity(0.78)
}
private var iconButtonStroke: Color {
self.colorScheme == .dark ? Color.white.opacity(0.14) : Color.black.opacity(0.07)
}
var emptyAgentsTitle: String {
if !self.gatewayConnected { return "Agents unavailable" }
if !self.agentSearchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return "No matches" }
@ -600,7 +509,6 @@ extension AgentProTab {
self.appModel.isOperatorGatewayConnected ? "operator" : "no-operator",
self.activeAgentID,
self.scenePhase == .active ? "active" : "inactive",
"\(self.overviewRefreshNonce)",
].joined(separator: ":")
}

View file

@ -3,7 +3,6 @@ import SwiftUI
struct AgentProTab: View {
@Environment(NodeAppModel.self) var appModel
@Environment(\.colorScheme) var colorScheme
@Environment(\.scenePhase) var scenePhase
let directRoute: AgentRoute?
let headerLeadingAction: OpenClawSidebarHeaderAction?
@ -13,7 +12,6 @@ struct AgentProTab: View {
@State var overview: AgentOverviewSnapshot?
@State var overviewErrorText: String?
@State var overviewLoading: Bool = false
@State var overviewRefreshNonce: Int = 0
@State var agentRosterFilter: AgentRosterFilter = .all
@State var agentSearchPresented = false
@State var agentSearchText = ""
@ -84,24 +82,16 @@ struct AgentProTab: View {
}
enum AgentLayout {
static let cardRadius: CGFloat = 12
static let cardRadius: CGFloat = OpenClawProMetric.cardRadius
static let filterHeight: CGFloat = 34
static let rowMinHeight: CGFloat = 104
static let rowMinHeight: CGFloat = 72
static let metricTileHeight: CGFloat = 94
static let actionButtonSize: CGFloat = 34
}
enum AgentRosterState: Equatable {
case online
case ready
var title: String {
switch self {
case .online: "Online"
case .ready: "Ready"
}
}
var color: Color {
switch self {
case .online: OpenClawBrand.ok
@ -185,7 +175,7 @@ struct AgentProTab: View {
private func directDestination(for route: AgentRoute) -> some View {
self.destination(for: route)
.toolbar(
self.directHeaderLeadingAction(for: route) == nil ? .visible : .hidden,
route == .agents || self.directHeaderLeadingAction(for: route) != nil ? .hidden : .visible,
for: .navigationBar)
}
}

View file

@ -135,16 +135,9 @@ struct ChatProTab: View {
.frame(width: 38, height: 38)
.background(
Circle()
.fill(
LinearGradient(
colors: [
OpenClawBrand.accent,
OpenClawBrand.accentHot,
],
startPoint: .topLeading,
endPoint: .bottomTrailing)))
.fill(OpenClawBrand.accent.gradient))
.overlay(Circle().strokeBorder(.white.opacity(0.18), lineWidth: 1))
.shadow(color: OpenClawBrand.accent.opacity(0.18), radius: 10, y: 5)
.shadow(color: OpenClawBrand.accent.opacity(0.14), radius: 5, y: 2)
} else {
ProIconBadge(systemName: "bubble.left", color: OpenClawBrand.accent)
}
@ -208,7 +201,8 @@ struct ChatProTab: View {
Button(action: openSettings) {
self.connectionPill
}
.buttonStyle(.plain)
.buttonBorderShape(.capsule)
.openClawGlassButton()
.accessibilityHint("Opens Settings / Gateway")
} else {
self.connectionPill
@ -223,16 +217,8 @@ struct ChatProTab: View {
.lineLimit(1)
}
.foregroundStyle(self.gatewayPillColor)
.padding(.horizontal, 10)
.padding(.horizontal, 4)
.frame(height: 30)
.background {
Capsule()
.fill(self.gatewayPillColor.opacity(0.11))
}
.overlay {
Capsule()
.strokeBorder(self.gatewayPillColor.opacity(0.16), lineWidth: 1)
}
}
private var gatewayConnected: Bool {
@ -281,8 +267,8 @@ struct ChatProTab: View {
?? Self.defaultHeaderTitle(showsAgentBadge: self.showsAgentBadge, agentDisplayName: self.agentDisplayName)
}
private var headerDisplaySubtitle: String {
self.normalized(self.headerSubtitle) ?? "AI Assistant"
private var headerDisplaySubtitle: String? {
self.normalized(self.headerSubtitle)
}
nonisolated static func defaultHeaderTitle(showsAgentBadge: Bool, agentDisplayName: String) -> String {

View file

@ -31,22 +31,12 @@ struct CommandPanel<Content: View>: View {
}
struct CommandControlBackground: View {
@Environment(\.colorScheme) private var colorScheme
var body: some View {
Color(uiColor: self.colorScheme == .dark ? .systemBackground : .systemGroupedBackground)
.overlay(alignment: .top) {
if self.colorScheme == .light {
Color.white.opacity(0.20)
.frame(height: 140)
}
}
.ignoresSafeArea()
OpenClawProBackground()
}
}
struct CommandSessionRow: View {
@Environment(\.colorScheme) private var colorScheme
let item: CommandCenterTab.WorkItem
var body: some View {
@ -88,16 +78,9 @@ struct CommandSessionRow: View {
}
}
}
.padding(.horizontal, 10)
.padding(.vertical, 8)
.background {
RoundedRectangle(cornerRadius: OpenClawProMetric.controlRadius, style: .continuous)
.fill(self.rowFill)
.overlay {
RoundedRectangle(cornerRadius: OpenClawProMetric.controlRadius, style: .continuous)
.strokeBorder(self.rowBorder, lineWidth: 1)
}
}
.padding(.horizontal, 4)
.padding(.vertical, 6)
.contentShape(Rectangle())
}
private var progressLabel: String {
@ -109,41 +92,16 @@ struct CommandSessionRow: View {
}
return "\(Int((progress * 100).rounded()))%"
}
private var rowFill: Color {
self.colorScheme == .dark ? Color.white.opacity(0.035) : Color(uiColor: .systemBackground)
}
private var rowBorder: Color {
Color(uiColor: .separator).opacity(self.colorScheme == .dark ? 0.24 : 0.22)
}
}
struct CommandViewMoreRow: View {
@Environment(\.colorScheme) private var colorScheme
var body: some View {
Text("View More")
Label("View More", systemImage: "chevron.right")
.font(.subheadline.weight(.bold))
.foregroundStyle(OpenClawBrand.accent)
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.background {
RoundedRectangle(cornerRadius: OpenClawProMetric.controlRadius, style: .continuous)
.fill(self.rowFill)
.overlay {
RoundedRectangle(cornerRadius: OpenClawProMetric.controlRadius, style: .continuous)
.strokeBorder(self.rowBorder, lineWidth: 1)
}
}
}
private var rowFill: Color {
self.colorScheme == .dark ? Color.white.opacity(0.035) : Color(uiColor: .systemBackground)
}
private var rowBorder: Color {
Color(uiColor: .separator).opacity(self.colorScheme == .dark ? 0.24 : 0.22)
.contentShape(Rectangle())
}
}
@ -173,15 +131,7 @@ struct CommandEmptyStateRow: View {
}
Spacer(minLength: 0)
}
.padding(.horizontal, 8)
.padding(.vertical, 8)
.background {
RoundedRectangle(cornerRadius: OpenClawProMetric.controlRadius, style: .continuous)
.fill(Color(uiColor: .systemBackground))
.overlay {
RoundedRectangle(cornerRadius: OpenClawProMetric.controlRadius, style: .continuous)
.strokeBorder(Color(uiColor: .separator).opacity(0.22), lineWidth: 1)
}
}
.padding(.horizontal, 4)
.padding(.vertical, 6)
}
}

View file

@ -119,14 +119,13 @@ struct CommandCenterTab: View {
}
} accessory: {
Button(action: self.openSettings) {
ProCapsule(
title: self.gatewayStateText,
color: self.gatewayStatusColor,
icon: self.gatewayConnected ? "checkmark.circle.fill" : "wifi.slash")
Image(systemName: "gearshape.fill")
.font(.subheadline.weight(.semibold))
.frame(width: OpenClawProMetric.compactControlSize, height: OpenClawProMetric.compactControlSize)
}
.buttonStyle(.plain)
.accessibilityLabel("Gateway \(self.gatewayStateText)")
.accessibilityHint("Opens Settings / Gateway")
.openClawGlassButton()
.accessibilityLabel("Gateway settings")
.accessibilityHint("Opens gateway settings")
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
@ -150,17 +149,13 @@ struct CommandCenterTab: View {
private var gatewayCard: some View {
CommandPanel(isProminent: true, padding: 12) {
VStack(alignment: .leading, spacing: 10) {
self.cardHeader(
title: "Gateway",
value: self.gatewayStateText,
color: self.gatewayStatusColor,
icon: self.gatewayConnected ? "checkmark.circle.fill" : "wifi.slash")
self.cardHeader(title: "Gateway")
HStack(spacing: 0) {
self.gatewayFact(
icon: "network",
title: "Connection",
value: self.gatewayConnected ? "Online" : "Offline",
value: self.gatewayConnectionText,
color: self.gatewayStatusColor)
Divider().frame(height: 38)
self.gatewayFact(
@ -175,17 +170,7 @@ struct CommandCenterTab: View {
value: self.gatewayAgentCountText,
color: OpenClawBrand.accentHot)
}
.padding(.vertical, 9)
.background {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(self.colorScheme == .dark ? Color.black.opacity(0.16) : Color.black.opacity(0.026))
.overlay {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.strokeBorder(
Color.primary.opacity(self.colorScheme == .dark ? 0.08 : 0.045),
lineWidth: 1)
}
}
.padding(.vertical, 7)
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
@ -215,10 +200,7 @@ struct CommandCenterTab: View {
private var defaultChatSessionSection: some View {
CommandPanel(padding: 12) {
VStack(spacing: 10) {
self.cardHeader(
title: "Agent session",
value: nil,
color: OpenClawBrand.accent)
self.cardHeader(title: "Agent session")
Button {
self.open(.chat(nil))
@ -233,10 +215,7 @@ struct CommandCenterTab: View {
private var recentSessions: some View {
CommandPanel(padding: 12) {
VStack(spacing: 10) {
self.cardHeader(
title: "Recent sessions",
value: nil,
color: .secondary)
self.cardHeader(title: "Recent sessions")
if self.recentSessionPreviewRows.isEmpty {
CommandEmptyStateRow(
@ -276,64 +255,47 @@ struct CommandCenterTab: View {
}
}
private func cardHeader(
title: String,
value: String?,
color: Color,
icon: String? = nil,
badgeValue: String? = nil,
action: (() -> Void)? = nil) -> some View
{
private func cardHeader(title: String) -> some View {
HStack(spacing: 8) {
Text(title)
.font(.subheadline.weight(.semibold))
.foregroundStyle(.secondary)
if let badgeValue {
Text(badgeValue)
.font(.caption2.weight(.bold))
.foregroundStyle(.white)
.padding(.horizontal, 6)
.padding(.vertical, 3)
.background(OpenClawBrand.accentHot, in: Capsule())
}
Spacer(minLength: 8)
if let value {
if let action {
Button(value, action: action)
.font(.caption.weight(.semibold))
.foregroundStyle(color)
} else {
HStack(spacing: 4) {
if let icon {
Image(systemName: icon)
.font(.caption2.weight(.bold))
}
Text(value)
}
.font(.caption.weight(.semibold))
.foregroundStyle(color)
}
}
}
}
private var gatewayConnected: Bool {
GatewayStatusBuilder.build(appModel: self.appModel) == .connected
self.gatewayDisplayState == .connected
}
private var gatewayStateText: String {
guard !self.gatewayConnected else { return "Healthy" }
let status = self.appModel.gatewayDisplayStatusText.trimmingCharacters(in: .whitespacesAndNewlines)
let lowercased = status.lowercased()
if lowercased.contains("approval") { return "Approval" }
if lowercased.contains("reconnect") { return "Reconnecting" }
if lowercased.contains("connect") { return "Connecting" }
if lowercased.contains("idle") { return "Idle" }
return "Offline"
private var gatewayDisplayState: GatewayDisplayState {
GatewayStatusBuilder.build(appModel: self.appModel)
}
private var gatewayConnectionText: String {
switch self.gatewayDisplayState {
case .connected:
"Online"
case .connecting:
"Connecting"
case .error:
"Attention"
case .disconnected:
"Offline"
}
}
private var gatewayStatusColor: Color {
self.gatewayConnected ? OpenClawBrand.ok : .secondary
switch self.gatewayDisplayState {
case .connected:
OpenClawBrand.ok
case .connecting:
OpenClawBrand.accent
case .error:
OpenClawBrand.warn
case .disconnected:
.secondary
}
}
private var gatewayAddressText: String {

View file

@ -58,7 +58,8 @@ struct IPadSidebarScreenChrome<Content: View>: View {
Button(action: gatewayAction) {
OpenClawGatewayCompactPill()
}
.buttonStyle(.plain)
.buttonBorderShape(.capsule)
.openClawGlassButton()
.accessibilityHint("Opens Settings / Gateway")
} else {
OpenClawGatewayCompactPill()

View file

@ -55,7 +55,8 @@ struct OpenClawDocsScreen: View {
Button(action: gatewayAction) {
OpenClawGatewayCompactPill()
}
.buttonStyle(.plain)
.buttonBorderShape(.capsule)
.openClawGlassButton()
.accessibilityHint("Opens Settings / Gateway")
} else {
OpenClawGatewayCompactPill()

View file

@ -1,25 +1,17 @@
import SwiftUI
enum OpenClawProMetric {
static let pagePadding: CGFloat = 18
static let cardRadius: CGFloat = 10
static let controlRadius: CGFloat = 8
static let pagePadding: CGFloat = 16
static let cardRadius: CGFloat = 16
static let controlRadius: CGFloat = 12
static let compactControlSize: CGFloat = 36
static let bottomScrollInset: CGFloat = 96
}
struct OpenClawProBackground: View {
@Environment(\.colorScheme) private var colorScheme
var body: some View {
Color(uiColor: self.colorScheme == .dark ? .systemBackground : .systemGroupedBackground)
Color(uiColor: .systemGroupedBackground)
.ignoresSafeArea()
.overlay(alignment: .top) {
if self.colorScheme == .light {
Color.white.opacity(0.22)
.frame(height: 140)
.ignoresSafeArea()
}
}
}
}
@ -83,76 +75,87 @@ private struct ProPanelBackground: View {
.overlay {
shape.strokeBorder(self.borderStyle, lineWidth: 1)
}
.overlay {
if self.isProminent {
shape.strokeBorder(
OpenClawBrand.accent.opacity(self.colorScheme == .dark ? 0.12 : 0.07),
lineWidth: 1)
.padding(1)
}
}
}
private var fill: AnyShapeStyle {
let base = self.isProminent
? Color(uiColor: .systemBackground)
: Color(uiColor: .secondarySystemGroupedBackground)
if let tint {
let gradient = LinearGradient(
colors: [
base,
tint.opacity(self.colorScheme == .dark ? 0.08 : 0.045),
base,
],
startPoint: .topLeading,
endPoint: .bottomTrailing)
return AnyShapeStyle(gradient)
}
return AnyShapeStyle(base)
let color = self.isProminent ? UIColor.systemBackground : UIColor.secondarySystemGroupedBackground
return AnyShapeStyle(Color(uiColor: color))
}
private var borderStyle: AnyShapeStyle {
AnyShapeStyle(Color(uiColor: .separator).opacity(self.colorScheme == .dark ? 0.26 : 0.30))
if let tint {
return AnyShapeStyle(tint.opacity(self.isProminent ? 0.18 : 0.10))
}
return AnyShapeStyle(Color(uiColor: .separator).opacity(self.colorScheme == .dark ? 0.22 : 0.12))
}
}
private struct ProLightGlassModifier: ViewModifier {
private struct ProInsetSurfaceModifier: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
let tint: Color
let radius: CGFloat
func body(content: Content) -> some View {
if #available(iOS 26.0, *), self.colorScheme == .light {
content.glassEffect(.regular, in: .rect(cornerRadius: self.radius))
let shape = RoundedRectangle(cornerRadius: self.radius, style: .continuous)
content.background {
shape
.fill(Color(uiColor: .tertiarySystemGroupedBackground))
.overlay {
shape.strokeBorder(
self.tint.opacity(self.colorScheme == .dark ? 0.18 : 0.10),
lineWidth: 1)
}
}
}
}
private struct OpenClawGlassButtonModifier: ViewModifier {
let prominent: Bool
let tint: Color?
func body(content: Content) -> some View {
if #available(iOS 26.0, *) {
if self.prominent {
content
.buttonStyle(.glassProminent)
.tint(self.tint ?? OpenClawBrand.accent)
} else {
content
.buttonStyle(.glass)
.tint(self.tint)
}
} else if self.prominent {
content
.buttonStyle(.borderedProminent)
.tint(self.tint ?? OpenClawBrand.accent)
} else {
content
.buttonStyle(.bordered)
.tint(self.tint)
}
}
}
private struct OpenClawTabBarBehaviorModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 26.0, *) {
content.tabBarMinimizeBehavior(.onScrollDown)
} else {
content
}
}
}
private struct ProGlassSurfaceModifier: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
let fill: Color
let stroke: Color
private struct OpenClawGlassSurfaceModifier: ViewModifier {
let radius: CGFloat
let isProminent: Bool
var interactive = false
func body(content: Content) -> some View {
let shape = RoundedRectangle(cornerRadius: self.radius, style: .continuous)
let surfaced = content.background {
shape
.fill(self.fill)
.overlay {
shape.strokeBorder(self.stroke, lineWidth: self.isProminent ? 1.2 : 1)
}
}
if #available(iOS 26.0, *), self.colorScheme == .light {
surfaced.glassEffect(
self.interactive ? .regular.interactive() : .regular,
in: .rect(cornerRadius: self.radius))
if #available(iOS 26.0, *) {
content.glassEffect(.regular, in: .rect(cornerRadius: self.radius))
} else {
surfaced
content.background(
.regularMaterial,
in: RoundedRectangle(cornerRadius: self.radius, style: .continuous))
}
}
}
@ -169,19 +172,20 @@ extension View {
isProminent: isProminent))
}
func proGlassSurface(
fill: Color,
stroke: Color,
radius: CGFloat,
isProminent: Bool = false,
interactive: Bool = false) -> some View
{
self.modifier(ProGlassSurfaceModifier(
fill: fill,
stroke: stroke,
radius: radius,
isProminent: isProminent,
interactive: interactive))
func proInsetSurface(tint: Color, radius: CGFloat) -> some View {
self.modifier(ProInsetSurfaceModifier(tint: tint, radius: radius))
}
func openClawGlassButton(prominent: Bool = false, tint: Color? = nil) -> some View {
self.modifier(OpenClawGlassButtonModifier(prominent: prominent, tint: tint))
}
func openClawTabBarBehavior() -> some View {
self.modifier(OpenClawTabBarBehaviorModifier())
}
func openClawGlassSurface(radius: CGFloat = OpenClawProMetric.controlRadius) -> some View {
self.modifier(OpenClawGlassSurfaceModifier(radius: radius))
}
}
@ -199,11 +203,12 @@ private struct ProPanelSurfaceModifier: ViewModifier {
tint: self.tint,
isProminent: self.isProminent)
}
.modifier(ProLightGlassModifier(radius: self.radius))
.shadow(
color: self.colorScheme == .dark ? .black.opacity(0.22) : .black.opacity(0.028),
radius: self.isProminent ? 9 : 4,
y: self.isProminent ? 4 : 1)
color: self.isProminent
? (self.colorScheme == .dark ? .black.opacity(0.14) : .black.opacity(0.045))
: .clear,
radius: self.isProminent ? 5 : 0,
y: self.isProminent ? 2 : 0)
}
}
@ -253,11 +258,13 @@ struct OpenClawSidebarRevealButton: View {
let button = Button(action: self.headerAction.action) {
Image(systemName: self.headerAction.systemName)
.font(.system(size: 16, weight: .semibold))
.frame(width: 38, height: 38)
.frame(
width: OpenClawProMetric.compactControlSize,
height: OpenClawProMetric.compactControlSize)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.foregroundStyle(OpenClawBrand.accent)
.buttonBorderShape(.circle)
.openClawGlassButton(tint: OpenClawBrand.accent)
.accessibilityLabel(self.headerAction.accessibilityLabel)
if let accessibilityIdentifier = self.headerAction.accessibilityIdentifier {
@ -277,9 +284,105 @@ struct OpenClawSidebarHeaderLeadingSlot: View {
}
}
struct OpenClawGlassControlGroup<Content: View>: View {
@ViewBuilder let content: Content
var body: some View {
if #available(iOS 26.0, *) {
GlassEffectContainer(spacing: 8) {
self.content
}
} else {
self.content
}
}
}
enum OpenClawNoticeDetail {
case accent(String)
case requestID(String)
}
struct OpenClawNoticeBanner: View {
let icon: String
let title: String
let message: String
let ownerLabel: String
let tint: Color
var detail: OpenClawNoticeDetail?
var primaryActionTitle: String?
var onPrimaryAction: (() -> Void)?
var secondaryActionTitle: String?
var onSecondaryAction: (() -> Void)?
var body: some View {
ProCard(tint: self.tint, padding: 14) {
VStack(alignment: .leading, spacing: 12) {
HStack(alignment: .top, spacing: 12) {
ProIconBadge(systemName: self.icon, color: self.tint)
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(self.title)
.font(.subheadline.weight(.semibold))
.multilineTextAlignment(.leading)
Spacer(minLength: 0)
Text(self.ownerLabel)
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
}
Text(self.message)
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
self.detailView
}
}
if self.onPrimaryAction != nil || self.onSecondaryAction != nil {
OpenClawGlassControlGroup {
HStack(spacing: 10) {
if let primaryActionTitle, let onPrimaryAction {
Button(primaryActionTitle, action: onPrimaryAction)
.openClawGlassButton(prominent: true)
.controlSize(.small)
}
if let secondaryActionTitle, let onSecondaryAction {
Button(secondaryActionTitle, action: onSecondaryAction)
.openClawGlassButton()
.controlSize(.small)
}
}
}
}
}
}
}
@ViewBuilder
private var detailView: some View {
if let detail {
switch detail {
case let .accent(value):
Text(value)
.font(.caption.weight(.medium))
.foregroundStyle(self.tint)
.fixedSize(horizontal: false, vertical: true)
case let .requestID(value):
Text("Request ID: \(value)")
.font(.system(.caption, design: .monospaced).weight(.medium))
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
}
}
}
struct OpenClawAdaptiveHeaderRow<Leading: View, Accessory: View>: View {
let title: String
let subtitle: String
let subtitle: String?
var titleFont: Font = .title3.weight(.semibold)
var subtitleFont: Font = .subheadline
var subtitleLineLimit: Int? = 2
@ -288,7 +391,7 @@ struct OpenClawAdaptiveHeaderRow<Leading: View, Accessory: View>: View {
init(
title: String,
subtitle: String,
subtitle: String? = nil,
titleFont: Font = .title3.weight(.semibold),
subtitleFont: Font = .subheadline,
subtitleLineLimit: Int? = 2,
@ -351,11 +454,13 @@ struct OpenClawAdaptiveHeaderRow<Leading: View, Accessory: View>: View {
.lineLimit(2)
.minimumScaleFactor(0.86)
.fixedSize(horizontal: false, vertical: true)
Text(self.subtitle)
.font(self.subtitleFont)
.foregroundStyle(.secondary)
.lineLimit(self.subtitleLineLimit)
.fixedSize(horizontal: false, vertical: true)
if let subtitle, !subtitle.isEmpty {
Text(subtitle)
.font(self.subtitleFont)
.foregroundStyle(.secondary)
.lineLimit(self.subtitleLineLimit)
.fixedSize(horizontal: false, vertical: true)
}
}
}
}
@ -422,47 +527,22 @@ struct ProProgressBar: View {
}
}
struct ProCapsule: View {
@Environment(\.colorScheme) private var colorScheme
let title: String
let color: Color
var icon: String?
var body: some View {
HStack(spacing: 6) {
if let icon {
Image(systemName: icon)
.font(.caption.weight(.semibold))
}
Text(self.title)
.font(.caption.weight(.semibold))
.lineLimit(1)
.minimumScaleFactor(0.78)
}
.fixedSize(horizontal: true, vertical: false)
.foregroundStyle(self.color)
.padding(.horizontal, 10)
.padding(.vertical, 7)
.background {
Capsule()
.fill(self.color.opacity(self.colorScheme == .dark ? 0.16 : 0.10))
.overlay {
Capsule()
.strokeBorder(self.color.opacity(self.colorScheme == .dark ? 0.30 : 0.18), lineWidth: 1)
}
}
}
}
struct OpenClawGatewayCompactPill: View {
@Environment(NodeAppModel.self) private var appModel
var body: some View {
ProCapsule(
title: self.title,
color: self.color,
icon: self.icon)
.accessibilityLabel("Gateway \(self.title)")
HStack(spacing: 6) {
Image(systemName: self.icon)
.font(.caption.weight(.semibold))
Text(self.title)
.font(.caption.weight(.semibold))
.lineLimit(1)
}
.foregroundStyle(self.color)
.padding(.horizontal, 4)
.frame(minHeight: 30)
.fixedSize(horizontal: true, vertical: false)
.accessibilityLabel("Gateway \(self.title)")
}
private var title: String {
@ -536,10 +616,7 @@ struct ProMetricTile: View {
}
.padding(11)
.frame(maxWidth: .infinity, alignment: .leading)
.proGlassSurface(
fill: self.colorScheme == .dark ? Color.white.opacity(0.04) : Color.white.opacity(0.52),
stroke: self.color.opacity(self.colorScheme == .dark ? 0.18 : 0.10),
radius: 16)
.proInsetSurface(tint: self.color, radius: OpenClawProMetric.controlRadius)
}
}

View file

@ -27,7 +27,7 @@ struct RootTabsPhoneControlHub: View {
.safeAreaPadding(.bottom, self.bottomScrollInset)
}
.navigationTitle("Control")
.navigationBarTitleDisplayMode(.inline)
.navigationBarTitleDisplayMode(.large)
.navigationDestination(for: RootTabs.SidebarDestination.self) { destination in
self.detail(for: destination)
.navigationBarBackButtonHidden(true)
@ -39,15 +39,19 @@ struct RootTabsPhoneControlHub: View {
}
}
@ViewBuilder
private var headerCard: some View {
if self.isCompactHeight {
ProCard(padding: 8, radius: OpenClawProMetric.cardRadius) {
ProCard(padding: 0, radius: OpenClawProMetric.cardRadius) {
Button {
self.openPhoneRootDestination(.gateway)
} label: {
HStack(spacing: 12) {
OpenClawProMark(size: 24, shadowRadius: 3)
VStack(alignment: .leading, spacing: 3) {
OpenClawProMark(
size: self.isCompactHeight ? 28 : 34,
shadowRadius: self.isCompactHeight ? 3 : 5)
VStack(alignment: .leading, spacing: 4) {
Text(self.sidebarActiveAgentTitle)
.font(.subheadline.weight(.semibold))
.font(.headline)
.foregroundStyle(.primary)
.lineLimit(1)
Text(self.gatewayDisplayLabel)
.font(.footnote)
@ -56,71 +60,29 @@ struct RootTabsPhoneControlHub: View {
.truncationMode(.middle)
}
Spacer(minLength: 8)
ProValuePill(value: self.gatewayStateText, color: self.gatewayStateColor)
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
} else {
ProCard(radius: OpenClawProMetric.cardRadius) {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 12) {
OpenClawProMark(size: 32, shadowRadius: 4)
VStack(alignment: .leading, spacing: 3) {
Text(self.sidebarActiveAgentTitle)
.font(.headline)
.lineLimit(1)
Text(self.gatewayDisplayLabel)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
}
Spacer(minLength: 8)
ProValuePill(value: self.gatewayStateText, color: self.gatewayStateColor)
HStack(spacing: 7) {
ProStatusDot(color: self.gatewayStateColor)
Text(self.gatewayStateText)
.font(.footnote.weight(.semibold))
.foregroundStyle(self.gatewayStateColor)
Image(systemName: "chevron.right")
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
}
self.gatewayActionRow
}
.padding(self.isCompactHeight ? 10 : 14)
.contentShape(Rectangle())
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
.buttonStyle(.plain)
.accessibilityLabel("Gateway \(self.gatewayStateText)")
.accessibilityHint("Opens Settings / Gateway")
}
}
private var gatewayActionRow: some View {
Button {
self.openPhoneRootDestination(.gateway)
} label: {
HStack(spacing: 10) {
ProStatusDot(color: self.gatewayStateColor)
VStack(alignment: .leading, spacing: 2) {
Text(self.gatewayStateText)
.font(.subheadline.weight(.semibold))
.foregroundStyle(.primary)
Text(self.gatewayDisplayLabel)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
}
Spacer(minLength: 8)
Text(self.gatewayActionTitle)
.font(.footnote.weight(.semibold))
.foregroundStyle(OpenClawBrand.accent)
Image(systemName: "chevron.right")
.font(.caption2.weight(.bold))
.foregroundStyle(.secondary)
}
.padding(10)
.background(Color.primary.opacity(0.055), in: RoundedRectangle(cornerRadius: 8, style: .continuous))
}
.buttonStyle(.plain)
.accessibilityLabel("Gateway \(self.gatewayStateText)")
.accessibilityHint("Opens Settings / Gateway")
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
private func groupSection(_ group: RootTabs.SidebarGroup) -> some View {
VStack(alignment: .leading, spacing: self.isCompactHeight ? 6 : 8) {
ProSectionHeader(title: group.title.capitalized)
ProSectionHeader(title: group.title.capitalized, uppercase: false)
ProCard(padding: 0, radius: OpenClawProMetric.cardRadius) {
VStack(spacing: 0) {
ForEach(Array(group.destinations.enumerated()), id: \.element.id) { index, destination in
@ -157,21 +119,15 @@ struct RootTabsPhoneControlHub: View {
private func rowLabel(_ destination: RootTabs.SidebarDestination) -> some View {
HStack(alignment: .center, spacing: 12) {
ProIconBadge(systemName: destination.systemImage, color: .secondary)
VStack(alignment: .leading, spacing: 3) {
Text(destination.title)
.font(.subheadline.weight(.semibold))
.foregroundStyle(.primary)
Text(destination.subtitle)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(1)
}
Text(destination.title)
.font(.subheadline.weight(.semibold))
.foregroundStyle(.primary)
Spacer(minLength: 8)
Image(systemName: "chevron.right")
.font(.caption2.weight(.bold))
.foregroundStyle(.secondary)
}
.padding(.vertical, self.isCompactHeight ? 8 : 10)
.padding(.vertical, self.isCompactHeight ? 7 : 9)
.padding(.horizontal, 14)
.contentShape(Rectangle())
}
@ -310,19 +266,6 @@ struct RootTabsPhoneControlHub: View {
}
}
private var gatewayActionTitle: String {
switch GatewayStatusBuilder.build(appModel: self.appModel) {
case .connected:
"Manage"
case .connecting:
"Details"
case .error:
"Fix"
case .disconnected:
"Connect"
}
}
private var isCompactHeight: Bool {
self.verticalSizeClass == .compact
}

View file

@ -5,7 +5,6 @@ extension SettingsProTab {
var settingsHeader: some View {
OpenClawAdaptiveHeaderRow(
title: "Settings",
subtitle: "Gateway, permissions, voice, and device controls.",
titleFont: .title3.weight(.semibold),
subtitleFont: .callout)
{
@ -40,8 +39,8 @@ extension SettingsProTab {
}
var gatewaySection: some View {
VStack(alignment: .leading, spacing: 8) {
ProSectionHeader(title: "Gateway", uppercase: false)
// Keep this container boundary: copying a directly returned generic ProCard crashes SwiftUI on iOS 26.
VStack(alignment: .leading, spacing: 0) {
ProCard(padding: 0, radius: SettingsLayout.cardRadius) {
VStack(spacing: 0) {
NavigationLink(value: SettingsRoute.gateway) {
@ -51,15 +50,6 @@ extension SettingsProTab {
.contentShape(Rectangle())
}
.buttonStyle(.plain)
Divider()
self.gatewayDetailRow(label: "Address", value: self.gatewayAddress)
Divider()
self.gatewayDetailRow(label: "Server", value: self.gatewayServer)
Divider()
self.gatewayDetailRow(label: "Agents", value: "\(self.appModel.gatewayAgents.count)")
Divider()
self.gatewayActions
.padding(14)
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
@ -73,11 +63,14 @@ extension SettingsProTab {
color: self.gatewayStatusColor)
VStack(alignment: .leading, spacing: 3) {
Text("Connection")
Text(self.gatewayServer)
.font(.subheadline.weight(.semibold))
Text(self.gatewayStatusDetail)
.lineLimit(1)
Text(self.gatewaySummaryDetail)
.font(.caption)
.foregroundStyle(self.gatewayStatusColor)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
}
Spacer(minLength: 8)
@ -88,20 +81,10 @@ extension SettingsProTab {
}
}
func gatewayDetailRow(label: String, value: String) -> some View {
HStack {
Text(label)
.font(.caption)
.foregroundStyle(.secondary)
Spacer(minLength: 8)
Text(value)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
}
.padding(.horizontal, 14)
.frame(height: 40)
var gatewaySummaryDetail: String {
let agentCount = self.appModel.gatewayAgents.count
let agents = agentCount == 1 ? "1 agent" : "\(agentCount) agents"
return "\(self.gatewayStatusDetail)\(agents)"
}
var gatewayActions: some View {
@ -128,51 +111,70 @@ extension SettingsProTab {
}
var settingsListSection: some View {
VStack(spacing: 10) {
self.settingsListRow(
icon: "checkmark.shield.fill",
title: "Approvals",
detail: self.approvalsDetail,
route: .approvals,
color: self.pendingApproval == nil ? .secondary : OpenClawBrand.warn,
badgeValue: self.pendingApproval == nil ? nil : "1")
self.settingsListRow(
icon: "person.2",
title: "Permissions",
detail: self.permissionsDetail,
route: .permissions)
self.settingsListRow(
icon: "point.3.connected.trianglepath.dotted",
title: "Channels / Integrations",
detail: "Message routing and external channel clients.",
route: .channels)
self.settingsListRow(
icon: "waveform",
title: "Voice & Talk",
detail: self.voiceDetail,
route: .voice)
self.settingsListRow(
icon: "globe",
title: "Diagnostics",
detail: self.diagnosticsDetail,
route: .diagnostics)
self.settingsListRow(
icon: "hand.raised",
title: "Privacy",
detail: self.privacyDetail,
route: .privacy)
self.settingsListRow(
icon: "bell",
title: "Notifications",
detail: self.notificationStatusText,
route: .notifications)
self.settingsListRow(
icon: "info.circle",
title: "About",
detail: DeviceInfoHelper.openClawVersionString(),
route: .about)
VStack(alignment: .leading, spacing: 18) {
ProCard(padding: 0, radius: SettingsLayout.cardRadius) {
VStack(spacing: 0) {
self.settingsListRow(
icon: "checkmark.shield.fill",
title: "Approvals",
detail: self.approvalsDetail,
route: .approvals,
color: self.pendingApproval == nil ? .secondary : OpenClawBrand.warn,
badgeValue: self.pendingApproval == nil ? nil : "1")
Divider().padding(.leading, 58)
self.settingsListRow(
icon: "person.2",
title: "Permissions",
detail: self.permissionsDetail,
route: .permissions)
Divider().padding(.leading, 58)
self.settingsListRow(
icon: "point.3.connected.trianglepath.dotted",
title: "Channels",
detail: "Connected services and message routing",
route: .channels)
Divider().padding(.leading, 58)
self.settingsListRow(
icon: "waveform",
title: "Voice & Talk",
detail: self.voiceDetail,
route: .voice)
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
VStack(alignment: .leading, spacing: 8) {
ProSectionHeader(title: "Device", uppercase: false)
ProCard(padding: 0, radius: SettingsLayout.cardRadius) {
VStack(spacing: 0) {
self.settingsListRow(
icon: "globe",
title: "Diagnostics",
detail: self.diagnosticsDetail,
route: .diagnostics)
Divider().padding(.leading, 58)
self.settingsListRow(
icon: "hand.raised",
title: "Privacy",
detail: self.privacyDetail,
route: .privacy)
Divider().padding(.leading, 58)
self.settingsListRow(
icon: "bell",
title: "Notifications",
detail: self.notificationStatusText,
route: .notifications)
Divider().padding(.leading, 58)
self.settingsListRow(
icon: "info.circle",
title: "About",
detail: DeviceInfoHelper.openClawVersionString(),
route: .about)
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
func settingsListRow(
@ -202,9 +204,10 @@ extension SettingsProTab {
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
}
.padding(12)
.padding(.horizontal, 14)
.padding(.vertical, 9)
.frame(maxWidth: .infinity, minHeight: SettingsLayout.rowHeight, alignment: .leading)
.proPanelSurface(radius: SettingsLayout.cardRadius)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
@ -612,15 +615,12 @@ extension SettingsProTab {
.minimumScaleFactor(0.76)
}
.frame(maxWidth: .infinity)
.frame(height: 34)
.foregroundStyle(color)
.background(color.opacity(0.09), in: RoundedRectangle(cornerRadius: 8, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.strokeBorder(color.opacity(0.14))
}
.frame(height: 32)
}
.buttonStyle(.plain)
.buttonStyle(.bordered)
.buttonBorderShape(.roundedRectangle(radius: 8))
.tint(color)
.controlSize(.small)
.disabled(isBusy || isDisabled)
}

View file

@ -16,7 +16,7 @@ enum SettingsRoute: Hashable {
}
enum SettingsLayout {
static let cardRadius: CGFloat = 12
static let cardRadius: CGFloat = OpenClawProMetric.cardRadius
static let rowHeight: CGFloat = 58
}

View file

@ -3,7 +3,6 @@ import SwiftUI
struct TalkProTab: View {
@Environment(NodeAppModel.self) private var appModel
@AppStorage("talk.enabled") private var talkEnabled: Bool = false
@AppStorage(TalkSpeechLocale.storageKey) private var talkSpeechLocale: String = TalkSpeechLocale.automaticID
@AppStorage(TalkDefaults.speakerphoneEnabledKey) private var talkSpeakerphoneEnabled: Bool =
TalkDefaults.speakerphoneEnabledByDefault
@AppStorage("talk.background.enabled") private var talkBackgroundEnabled: Bool = false
@ -85,7 +84,7 @@ struct TalkProTab: View {
ZStack {
CommandControlBackground()
ScrollView {
VStack(alignment: .leading, spacing: 10) {
VStack(alignment: .leading, spacing: 12) {
self.header
if let fallbackIssue = self.fallbackIssue {
TalkRuntimeIssueBanner(
@ -97,8 +96,6 @@ struct TalkProTab: View {
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
self.voiceHeroCard
self.conversationCard
self.voiceModeCard
self.controlsCard
}
.padding(.top, 16)
@ -109,54 +106,30 @@ struct TalkProTab: View {
}
private var header: some View {
HStack(alignment: .center, spacing: 11) {
OpenClawAdaptiveHeaderRow(
title: "Talk",
subtitle: self.headerSubtitle,
titleFont: .system(size: 30, weight: .bold),
subtitleFont: .caption.weight(.medium),
subtitleLineLimit: 1)
{
if let headerLeadingAction {
OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)
}
OpenClawProMark(size: 31, shadowRadius: 9)
VStack(alignment: .leading, spacing: 2) {
Text("Talk")
.font(.system(size: 27, weight: .bold, design: .rounded))
Text(self.headerSubtitle)
.font(.caption.weight(.medium))
.foregroundStyle(.secondary)
.lineLimit(1)
}
Spacer(minLength: 8)
self.statusChip
} accessory: {
EmptyView()
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
private var statusChip: some View {
HStack(spacing: 5) {
Circle()
.fill(self.state.color)
.frame(width: 7, height: 7)
Text(self.state.chipText)
.font(.caption.weight(.bold))
.foregroundStyle(self.state.color)
}
.padding(.horizontal, 10)
.padding(.vertical, 7)
.background {
Capsule(style: .continuous)
.fill(self.state.color.opacity(0.11))
.overlay {
Capsule(style: .continuous)
.strokeBorder(self.state.color.opacity(0.22), lineWidth: 1)
}
}
}
private var voiceHeroCard: some View {
CommandPanel(tint: self.state.color, isProminent: true, padding: 16) {
VStack(alignment: .center, spacing: 16) {
CommandPanel(isProminent: true, padding: 16) {
VStack(alignment: .center, spacing: 14) {
TalkProOrb(
mode: self.state.waveformMode(micLevel: self.appModel.talkMode.micLevel),
color: self.state.color,
systemImage: self.state.icon)
.frame(height: 188)
.frame(height: 132)
.accessibilityHidden(true)
VStack(spacing: 5) {
@ -172,84 +145,20 @@ struct TalkProTab: View {
Button(action: self.handlePrimaryAction) {
Label(self.state.primaryButtonTitle, systemImage: self.state.primaryButtonIcon)
.font(.subheadline.weight(.bold))
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(self.state.primaryButtonFill)
.shadow(color: self.state.primaryButtonFill.opacity(0.22), radius: 18, y: 8)
}
}
.buttonStyle(.plain)
.buttonBorderShape(.capsule)
.openClawGlassButton(prominent: true, tint: self.state.primaryButtonFill)
.disabled(self.state.primaryAction == .waiting)
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
private var conversationCard: some View {
CommandPanel(padding: 0) {
VStack(spacing: 0) {
self.cardHeader(title: "Conversation", value: self.state.chipText, color: self.state.color)
.padding(.horizontal, 12)
.padding(.top, 11)
.padding(.bottom, 3)
self.infoRow(icon: "person.crop.circle.fill", title: "Agent", value: self.appModel.chatAgentName)
Divider().padding(.leading, 54)
self.infoRow(
icon: "bubble.left.and.text.bubble.right.fill",
title: "Session",
value: self.appModel.chatSessionKey)
Divider().padding(.leading, 54)
self.infoRow(icon: self.state.icon, title: "Runtime", value: self.appModel.talkMode.statusText)
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
private var voiceModeCard: some View {
CommandPanel(padding: 0) {
VStack(spacing: 0) {
self.cardHeader(
title: "Voice mode",
value: "Settings ",
color: OpenClawBrand.accent,
action: self.openVoiceSettings)
.padding(.horizontal, 12)
.padding(.top, 11)
.padding(.bottom, 3)
self.infoRow(
icon: "waveform",
title: "Configured",
value: self.appModel.talkMode.gatewayTalkVoiceModeTitle)
Divider().padding(.leading, 54)
self.infoRow(
icon: "waveform",
title: "Active now",
value: self.activeModeText)
Divider().padding(.leading, 54)
self.infoRow(icon: "antenna.radiowaves.left.and.right", title: "Transport", value: self.transportText)
if let issueText = self.talkIssueText {
Divider().padding(.leading, 54)
self.infoRow(icon: "exclamationmark.triangle.fill", title: "Last issue", value: issueText)
}
Divider().padding(.leading, 54)
self.infoRow(icon: "key.fill", title: "Permission", value: self.permissionText)
Divider().padding(.leading, 54)
self.infoRow(icon: "globe", title: "Speech language", value: self.speechLocaleText)
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
private var controlsCard: some View {
CommandPanel(padding: 0) {
VStack(spacing: 0) {
self.cardHeader(title: "Controls", value: nil, color: .secondary)
.padding(.horizontal, 12)
.padding(.top, 11)
.padding(.bottom, 3)
self.controlToggleRow("Speakerphone", isOn: self.talkSpeakerphoneBinding)
Divider().padding(.leading, 14)
self.controlToggleRow("Background listening", isOn: self.$talkBackgroundEnabled)
@ -291,55 +200,6 @@ struct TalkProTab: View {
}
}
private func cardHeader(
title: String,
value: String?,
color: Color,
action: (() -> Void)? = nil) -> some View
{
HStack(spacing: 8) {
Text(title)
.font(.subheadline.weight(.bold))
Spacer(minLength: 8)
if let value {
if let action {
Button(value, action: action)
.font(.caption.weight(.semibold))
.foregroundStyle(color)
} else {
Text(value)
.font(.caption.weight(.semibold))
.foregroundStyle(color)
}
}
}
}
private func infoRow(icon: String, title: String, value: String) -> some View {
HStack(spacing: 10) {
Image(systemName: icon)
.font(.caption.weight(.bold))
.foregroundStyle(self.state.color)
.frame(width: 30, height: 30)
.background {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(self.state.color.opacity(0.11))
}
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.caption2.weight(.medium))
.foregroundStyle(.secondary)
Text(value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "" : value)
.font(.subheadline.weight(.semibold))
.lineLimit(1)
.minimumScaleFactor(0.78)
}
Spacer(minLength: 0)
}
.padding(.horizontal, 12)
.padding(.vertical, 9)
}
private var gatewayConnected: Bool {
!self.appModel.isAppleReviewDemoModeEnabled &&
GatewayStatusBuilder.build(appModel: self.appModel) == .connected
@ -372,41 +232,6 @@ struct TalkProTab: View {
return "Routes voice to \(self.appModel.chatAgentName)."
}
private var transportText: String {
let provider = self.appModel.talkMode.gatewayTalkProviderLabel.trimmingCharacters(in: .whitespacesAndNewlines)
let transport = self.appModel.talkMode.gatewayTalkTransportLabel.trimmingCharacters(in: .whitespacesAndNewlines)
if provider.isEmpty || provider == "Not loaded" { return transport.isEmpty ? "Not loaded" : transport }
if transport.isEmpty || transport == "Not loaded" { return provider }
return "\(provider)\(transport)"
}
private var activeModeText: String {
let title = self.appModel.talkMode.gatewayTalkActiveModeTitle.trimmingCharacters(in: .whitespacesAndNewlines)
let subtitle = (self.appModel.talkMode.gatewayTalkActiveModeSubtitle ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
if title.isEmpty { return "Not active" }
if subtitle.isEmpty { return title }
return "\(title)\(subtitle)"
}
private var talkIssueText: String? {
let text = (self.appModel.talkMode.gatewayTalkLastIssueText ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
return text.isEmpty ? nil : text
}
private var permissionText: String {
if let failure = self.appModel.talkMode.gatewayTalkPermissionState.failureMessage {
return failure
}
return self.appModel.talkMode.gatewayTalkPermissionState.statusLabel
}
private var speechLocaleText: String {
if self.talkSpeechLocale == TalkSpeechLocale.automaticID { return "Automatic" }
return self.talkSpeechLocale
}
private func alignPersistedTalkState() {
if self.appModel.isAppleReviewDemoModeEnabled,
self.talkEnabled || self.appModel.talkMode.isEnabled
@ -579,7 +404,7 @@ struct TalkProState: Equatable {
return OpenClawBrand.warn
default:
if !self.isConfigLoaded { return OpenClawBrand.warn }
return self.isEnabled ? OpenClawBrand.ok : OpenClawBrand.accentHot
return self.isEnabled ? OpenClawBrand.ok : .secondary
}
}
@ -677,13 +502,13 @@ private struct TalkProOrb: View {
}
Circle()
.fill(self.color.opacity(0.13))
.frame(width: 128, height: 128)
.frame(width: 104, height: 104)
.overlay {
Circle()
.strokeBorder(self.color.opacity(0.30), lineWidth: 1)
}
TalkProWaveform(mode: self.mode, tint: self.color, barCount: 18)
.frame(width: 116, height: 52)
TalkProWaveform(mode: self.mode, tint: self.color, barCount: 12)
.frame(width: 92, height: 44)
.opacity(self.showsWaveform ? 1 : 0)
Image(systemName: self.systemImage)
.font(.system(size: 34, weight: .bold))

View file

@ -2,68 +2,22 @@ import SwiftUI
import UIKit
struct TalkRuntimeIssueBanner: View {
@Environment(\.colorScheme) private var colorScheme
let issue: TalkRuntimeIssue
var onOpenSettings: (() -> Void)?
var onShowDetails: (() -> Void)?
var body: some View {
VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .top, spacing: 10) {
Image(systemName: self.iconName)
.font(.headline.weight(.semibold))
.foregroundStyle(self.tint)
.frame(width: 20)
.padding(.top, 2)
VStack(alignment: .leading, spacing: 5) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(self.issue.fallbackBannerTitle)
.font(.subheadline.weight(.semibold))
.multilineTextAlignment(.leading)
Spacer(minLength: 0)
Text(self.issue.fallbackBannerOwnerLabel)
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
}
Text(self.issue.fallbackBannerMessage)
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
Text(self.issue.displayMessage)
.font(.caption.weight(.medium))
.foregroundStyle(self.tint)
.fixedSize(horizontal: false, vertical: true)
}
}
HStack(spacing: 10) {
if let onOpenSettings {
Button("Open Settings", action: onOpenSettings)
.buttonStyle(.borderedProminent)
.controlSize(.small)
}
if let onShowDetails {
Button("Details", action: onShowDetails)
.buttonStyle(.bordered)
.controlSize(.small)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(13)
.background {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(.ultraThickMaterial)
.overlay {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.strokeBorder(Color.primary.opacity(self.colorScheme == .dark ? 0.12 : 0.07), lineWidth: 1)
}
.shadow(color: .black.opacity(self.colorScheme == .dark ? 0.16 : 0.07), radius: 16, y: 7)
}
OpenClawNoticeBanner(
icon: self.iconName,
title: self.issue.fallbackBannerTitle,
message: self.issue.fallbackBannerMessage,
ownerLabel: self.issue.fallbackBannerOwnerLabel,
tint: self.tint,
detail: .accent(self.issue.displayMessage),
primaryActionTitle: "Open Settings",
onPrimaryAction: self.onOpenSettings,
secondaryActionTitle: "Details",
onSecondaryAction: self.onShowDetails)
}
private var iconName: String {

View file

@ -3,71 +3,23 @@ import SwiftUI
import UIKit
struct GatewayProblemBanner: View {
@Environment(\.colorScheme) private var colorScheme
let problem: GatewayConnectionProblem
var primaryActionTitle: String?
var onPrimaryAction: (() -> Void)?
var onShowDetails: (() -> Void)?
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(alignment: .top, spacing: 10) {
Image(systemName: self.iconName)
.font(.headline.weight(.semibold))
.foregroundStyle(self.tint)
.frame(width: 20)
.padding(.top, 2)
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(self.problem.title)
.font(.subheadline.weight(.semibold))
.multilineTextAlignment(.leading)
Spacer(minLength: 0)
Text(self.ownerLabel)
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
}
Text(self.problem.message)
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
if let requestId = self.problem.requestId {
Text("Request ID: \(requestId)")
.font(.system(.caption, design: .monospaced).weight(.medium))
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
}
}
HStack(spacing: 10) {
if let primaryActionTitle, let onPrimaryAction {
Button(primaryActionTitle, action: onPrimaryAction)
.buttonStyle(.borderedProminent)
.controlSize(.small)
}
if let onShowDetails {
Button("Details", action: onShowDetails)
.buttonStyle(.bordered)
.controlSize(.small)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(14)
.background {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(.ultraThickMaterial)
.overlay {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.strokeBorder(Color.primary.opacity(self.colorScheme == .dark ? 0.12 : 0.07), lineWidth: 1)
}
.shadow(color: .black.opacity(self.colorScheme == .dark ? 0.18 : 0.08), radius: 18, y: 8)
}
OpenClawNoticeBanner(
icon: self.iconName,
title: self.problem.title,
message: self.problem.message,
ownerLabel: self.ownerLabel,
tint: self.tint,
detail: self.problem.requestId.map(OpenClawNoticeDetail.requestID),
primaryActionTitle: self.primaryActionTitle,
onPrimaryAction: self.onPrimaryAction,
secondaryActionTitle: "Details",
onSecondaryAction: self.onShowDetails)
}
private var iconName: String {

View file

@ -188,6 +188,7 @@ struct RootTabs: View {
.tabItem { Label("Settings", systemImage: "gearshape.fill") }
.tag(AppTab.settings)
}
.openClawTabBarBehavior()
}
private var sidebarSplitContent: some View {
@ -402,7 +403,6 @@ struct RootTabs: View {
ChatProTab(
headerLeadingAction: self.sidebarHeaderLeadingAction,
headerTitle: "Chat",
headerSubtitle: "Agent conversation",
showsAgentBadge: false,
ownsNavigationStack: false,
openSettings: { self.selectSidebarDestination(.gateway) })

View file

@ -69,26 +69,6 @@ extension RootTabs {
}
}
var subtitle: String {
switch self {
case .chat: "Agent chat and recent work."
case .talk: "Realtime voice and fallback controls."
case .overview: "Status, entry points, health."
case .activity: "Gateway, session, and device activity."
case .agents: "Agent roster and readiness."
case .workboard: "Agent work queue and session handoff."
case .skillWorkshop: "Review and apply proposed skills."
case .instances: "Latest presence from OpenClaw nodes."
case .sessions: "Active sessions and defaults."
case .dreaming: "Memory signals and background synthesis."
case .usage: "API usage and costs."
case .cron: "Wakeups and recurring runs."
case .docs: "Reference docs and setup guides."
case .settings: "Connection, permissions, channels, and app options."
case .gateway: "Pairing, diagnostics, permissions, and device controls."
}
}
var systemImage: String {
switch self {
case .chat: "bubble.left"

View file

@ -1,8 +1,6 @@
import SwiftUI
struct VoiceWakeToast: View {
@Environment(\.colorScheme) private var colorScheme
var command: String
var body: some View {
@ -19,10 +17,7 @@ struct VoiceWakeToast: View {
}
.padding(.vertical, 10)
.padding(.horizontal, 12)
.proGlassSurface(
fill: self.colorScheme == .dark ? Color.white.opacity(0.055) : Color.white.opacity(0.72),
stroke: self.colorScheme == .dark ? Color.white.opacity(0.12) : Color.black.opacity(0.08),
radius: 14)
.openClawGlassSurface()
.accessibilityLabel("Voice Wake triggered")
.accessibilityValue("Command: \(self.command)")
}

View file

@ -174,7 +174,9 @@ struct RootTabsSourceGuardTests {
let dreamingSource = try String(contentsOf: Self.agentProDreamingDestinationSourceURL(), encoding: .utf8)
#expect(!source.contains("ToolbarItem"))
#expect(source.contains("self.directHeaderLeadingAction(for: route) == nil ? .visible : .hidden"))
#expect(source
.contains("route == .agents || self.directHeaderLeadingAction(for: route) != nil ? .hidden : .visible"))
#expect(destinationsSource.contains(".toolbar(.hidden, for: .navigationBar)"))
#expect(destinationsSource.contains("self.directHeaderLeadingAction(for: .instances)"))
#expect(destinationsSource.contains("self.directHeaderLeadingAction(for: .dreaming)"))
#expect(destinationsSource.contains("self.directHeader(\n for: .usage"))
@ -184,6 +186,80 @@ struct RootTabsSourceGuardTests {
#expect(dreamingSource.contains("OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)"))
}
@Test func `iOS 26 chrome uses native glass while content cards stay quiet`() throws {
let rootSource = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
let componentsSource = try String(contentsOf: Self.proComponentsSourceURL(), encoding: .utf8)
let cardSurface = try Self.extract(
componentsSource,
from: "private struct ProPanelSurfaceModifier: ViewModifier",
to: "struct ProIconBadge: View")
#expect(rootSource.contains(".openClawTabBarBehavior()"))
#expect(componentsSource.contains("content.tabBarMinimizeBehavior(.onScrollDown)"))
#expect(componentsSource.contains(".buttonStyle(.glassProminent)"))
#expect(componentsSource.contains(".buttonStyle(.glass)"))
#expect(componentsSource.contains("GlassEffectContainer(spacing: 8)"))
#expect(componentsSource.contains("if #available(iOS 26.0, *)"))
#expect(componentsSource.contains(".buttonStyle(.borderedProminent)"))
#expect(componentsSource.contains(".buttonStyle(.bordered)"))
#expect(componentsSource.contains("struct OpenClawNoticeBanner: View"))
#expect(!cardSurface.contains("glassEffect"))
}
@Test func `professional layout avoids nested pills and card stacks`() throws {
let componentsSource = try String(contentsOf: Self.proComponentsSourceURL(), encoding: .utf8)
let agentSource = try String(contentsOf: Self.agentProTabOverviewSourceURL(), encoding: .utf8)
let talkSource = try String(contentsOf: Self.talkProTabSourceURL(), encoding: .utf8)
let settingsSource = try String(contentsOf: Self.settingsProTabSectionsSourceURL(), encoding: .utf8)
let overviewSource = try String(contentsOf: Self.commandCenterSourceURL(), encoding: .utf8)
let overviewRowsSource = try String(contentsOf: Self.commandCenterSupportSourceURL(), encoding: .utf8)
let gatewayStatus = try Self.extract(
componentsSource,
from: "struct OpenClawGatewayCompactPill: View",
to: "struct ProMetricTile: View")
let agentFilters = try Self.extract(
agentSource,
from: "var agentFilters: some View",
to: "var agentFiltersActive: Bool")
let agentRow = try Self.extract(
agentSource,
from: "func agentRow(_ agent: AgentSummary) -> some View",
to: "func headerIconButton(")
let settingsList = try Self.extract(
settingsSource,
from: "var settingsListSection: some View",
to: "func settingsListRow(")
let settingsRow = try Self.extract(
settingsSource,
from: "func settingsListRow(",
to: "func destination(for route:")
#expect(gatewayStatus.contains("HStack(spacing: 6)"))
#expect(!gatewayStatus.contains("ProCapsule("))
#expect(!gatewayStatus.contains("Capsule()"))
#expect(agentFilters.contains("Picker(\"Agent status\""))
#expect(agentFilters.contains(".pickerStyle(.segmented)"))
#expect(!agentFilters.contains(".openClawGlassButton("))
#expect(!agentRow.contains("agentMetric"))
#expect(!agentRow.contains("chevron.right"))
#expect(agentRow.contains("Image(systemName: \"checkmark\")"))
#expect(agentRow.contains("agentAccessibilityLabel"))
#expect(!talkSource.contains("conversationCard"))
#expect(!talkSource.contains("voiceModeCard"))
#expect(!talkSource.contains("statusChip"))
#expect(!settingsList.contains("ProSectionHeader(title: \"OpenClaw\""))
#expect(settingsList.contains("ProSectionHeader(title: \"Device\""))
#expect(settingsList.matches(of: /ProCard\(padding: 0/).count == 2)
#expect(settingsRow.contains(".contentShape(Rectangle())"))
#expect(!overviewSource.contains("ProCapsule("))
#expect(overviewSource.contains("value: self.gatewayConnectionText"))
#expect(overviewSource.contains("switch self.gatewayDisplayState"))
#expect(overviewSource.contains("case .connecting:"))
#expect(overviewSource.contains("case .error:"))
#expect(!overviewRowsSource.contains("private var rowFill"))
#expect(overviewRowsSource.matches(of: /.contentShape\(Rectangle\(\)\)/).count >= 2)
}
@Test func `routed headers use shared adaptive layout`() throws {
let componentsSource = try String(contentsOf: Self.proComponentsSourceURL(), encoding: .utf8)
let featureChromeSource = try String(contentsOf: Self.iPadSidebarScreenChromeSourceURL(), encoding: .utf8)
@ -257,7 +333,11 @@ struct RootTabsSourceGuardTests {
@Test func `phone hub header stays task first`() throws {
let source = try String(contentsOf: Self.phoneHubSourceURL(), encoding: .utf8)
#expect(source.contains("private var gatewayActionRow: some View"))
#expect(source.contains("private var headerCard: some View"))
#expect(source.contains(".accessibilityLabel(\"Gateway \\(self.gatewayStateText)\")"))
#expect(!source.contains("private var gatewayActionRow: some View"))
#expect(!source.contains("ProValuePill(value: self.gatewayStateText"))
#expect(!source.contains("destination.subtitle"))
#expect(source.contains("self.openPhoneRootDestination(.gateway)"))
#expect(source.contains("private var phoneDetailBackAction: OpenClawSidebarHeaderAction"))
#expect(source.contains("accessibilityLabel: \"Back to Control\""))
@ -272,7 +352,7 @@ struct RootTabsSourceGuardTests {
#expect(!source.contains("private func metric(label:"))
}
@Test func phoneHubClearsDetailPathBeforeRootTabHandoff() throws {
@Test func `phone hub clears detail path before root tab handoff`() throws {
let source = try String(contentsOf: Self.phoneHubSourceURL(), encoding: .utf8)
let handoff = try Self.extract(
source,
@ -287,7 +367,7 @@ struct RootTabsSourceGuardTests {
#expect(clearRange.lowerBound < openRange.lowerBound)
}
@Test func workboardUsesRealGatewayMethods() throws {
@Test func `workboard uses real gateway methods`() throws {
let source = try String(contentsOf: Self.iPadWorkboardScreenSourceURL(), encoding: .utf8)
#expect(source.contains("workboard.cards.list"))
@ -514,6 +594,8 @@ struct RootTabsSourceGuardTests {
#expect(chromeSource.contains("let gatewayAction: (() -> Void)?"))
#expect(chromeSource.contains("private var gatewayPill: some View"))
#expect(chromeSource.contains("Button(action: gatewayAction)"))
#expect(chromeSource.contains(".buttonBorderShape(.capsule)"))
#expect(chromeSource.contains(".openClawGlassButton()"))
#expect(chromeSource.contains(".accessibilityHint(\"Opens Settings / Gateway\")"))
#expect(featureSource.matches(of: /gatewayAction: self\.openSettings/).count == 2)
#expect(rootSource.contains("IPadActivityScreen("))
@ -541,7 +623,7 @@ struct RootTabsSourceGuardTests {
#expect(!rootSource.contains("showGatewayActions"))
#expect(!rootSource.contains("gatewayActionsDialog"))
#expect(overviewSource.contains("Button(action: self.openSettings)"))
#expect(overviewSource.contains(".accessibilityHint(\"Opens Settings / Gateway\")"))
#expect(overviewSource.contains(".accessibilityHint(\"Opens gateway settings\")"))
#expect(agentSource.contains("let openSettings: (() -> Void)?"))
#expect(agentOverviewSource.contains("OpenClawGatewayCompactPill()"))
#expect(agentOverviewSource.contains("Button(action: openSettings)"))
@ -550,7 +632,11 @@ struct RootTabsSourceGuardTests {
.count >= 3)
#expect(chatSource.contains("let openSettings: (() -> Void)?"))
#expect(chatSource.contains("private var connectionPillButton: some View"))
#expect(chatSource.contains(".buttonBorderShape(.capsule)"))
#expect(chatSource.contains(".openClawGlassButton()"))
#expect(docsSource.contains("let gatewayAction: (() -> Void)?"))
#expect(docsSource.contains(".buttonBorderShape(.capsule)"))
#expect(docsSource.contains(".openClawGlassButton()"))
#expect(settingsSource.contains("NavigationLink(value: SettingsRoute.gateway)"))
#expect(rootSource.contains("case .settings:"))
#expect(rootSource
@ -589,7 +675,7 @@ struct RootTabsSourceGuardTests {
#expect(notificationGuidanceSource.contains("suppressFuture: true"))
#expect(notificationGuidanceSource.contains("Text(\"Don't show again\")"))
#expect(rootSource.contains("private func selectSettingsRoute(_ route: SettingsRoute)"))
#expect(settingsSource.contains("title: \"Channels / Integrations\""))
#expect(settingsSource.contains("title: \"Channels\""))
#expect(settingsSource.contains("route: .channels"))
#expect(docsSource.contains(".accessibilityHint(\"Opens Settings / Gateway\")"))
}
@ -638,7 +724,8 @@ struct RootTabsSourceGuardTests {
#expect(actionsSource.contains("self.gatewayController.refreshActiveGatewayRegistrationFromSettings()"))
#expect(actionsSource.contains("self.gatewayController.restartDiscovery()"))
#expect(actionsSource.contains("await self.appModel.refreshGatewayOverviewIfConnected()"))
#expect(actionsSource.contains("self.gatewayController.requestLocalNetworkAccess(reason: \"settings_preflight\")"))
#expect(actionsSource
.contains("self.gatewayController.requestLocalNetworkAccess(reason: \"settings_preflight\")"))
#expect(controllerSource.contains("await self.tcpReachabilityProbe("))
#expect(controllerSource.contains("Check Tailscale or LAN."))
#expect(actionsSource.contains("Tailscale is off on this device. Turn it on, then try again."))
@ -710,7 +797,7 @@ struct RootTabsSourceGuardTests {
#expect(chatSource.matches(of: /self\.appModel\.makeChatTransport\(\)/).count == 2)
#expect(appModelSource.contains("return IOSGatewayChatTransport(gateway: self.operatorSession)"))
#expect(settingsSectionsSource.contains("Message routing and external channel clients."))
#expect(settingsSectionsSource.contains("Connected services and message routing"))
#expect(channelsSource.contains("\"clickclack\": SettingsChannelFallbackMetadata"))
#expect(channelsSource.contains("label: \"ClickClack\""))
#expect(channelsSource.contains("Self-hosted chat bot routing."))
@ -751,6 +838,13 @@ struct RootTabsSourceGuardTests {
.appendingPathComponent("Sources/Design/CommandCenterTab.swift")
}
private static func commandCenterSupportSourceURL() -> URL {
URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent("Sources/Design/CommandCenterSupport.swift")
}
private static func agentProTabSourceURL() -> URL {
URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
@ -852,6 +946,13 @@ struct RootTabsSourceGuardTests {
.appendingPathComponent("Sources/Design/ChatProTab.swift")
}
private static func talkProTabSourceURL() -> URL {
URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent("Sources/Design/TalkProTab.swift")
}
private static func docsSourceURL() -> URL {
URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()

View file

@ -30,15 +30,15 @@ final class OpenClawSnapshotUITests: XCTestCase {
}
override func tearDownWithError() throws {
app?.terminate()
app = nil
self.app?.terminate()
self.app = nil
try super.tearDownWithError()
}
func testConnectedGatewayTabs() {
for appearance in ScreenshotAppearance.allCases {
for target in Self.screenshotTargets {
launchApp(for: target, appearance: appearance)
self.launchApp(for: target, appearance: appearance)
let name = appearance == .light ? target.name : "\(target.name)-dark"
snapshot(name, timeWaitingForIdle: 5)
}
@ -47,26 +47,50 @@ final class OpenClawSnapshotUITests: XCTestCase {
func testControlOverviewNavigation() throws {
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone control hub only")
launchApp(for: ScreenshotTarget(
self.launchApp(for: ScreenshotTarget(
initialTab: "control",
initialDestination: "control",
name: "control-overview-navigation"
))
name: "control-overview-navigation"))
let overview = app?.buttons.containing(.staticText, identifier: "Overview").firstMatch
let overview = self.app?.buttons.containing(.staticText, identifier: "Overview").firstMatch
XCTAssertTrue(overview?.waitForExistence(timeout: 5) == true)
overview?.tap()
XCTAssertTrue(app?.buttons["Back to Control"].waitForExistence(timeout: 5) == true)
XCTAssertEqual(app?.state, .runningForeground)
XCTAssertTrue(self.app?.buttons["Back to Control"].waitForExistence(timeout: 5) == true)
XCTAssertEqual(self.app?.state, .runningForeground)
}
func testChatComposerStartsCompactAndGrowsWithDraft() throws {
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone composer proof only")
self.launchApp(for: ScreenshotTarget(
initialTab: "chat",
initialDestination: "chat",
name: "chat-composer-growth"))
let textField = try XCTUnwrap(app?.textFields.firstMatch)
XCTAssertTrue(textField.waitForExistence(timeout: 8))
let compactHeight = textField.frame.height
XCTAssertLessThanOrEqual(compactHeight, 44)
self.attachScreenshot(named: "chat-composer-compact")
textField.tap()
textField.typeText(
"Draft a polished launch note that covers the new design, validation, rollout plan, and follow-up details for the team.")
let composerGrew = expectation(
for: NSPredicate { _, _ in textField.frame.height >= compactHeight + 12 },
evaluatedWith: textField)
wait(for: [composerGrew], timeout: 4)
self.attachScreenshot(named: "chat-composer-expanded")
self.app?.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.2)).tap()
XCTAssertTrue(self.app?.keyboards.firstMatch.waitForNonExistence(timeout: 3) == true)
}
func testLiveGatewayControlOverviewNavigation() throws {
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone control hub only")
try XCTSkipUnless(
ProcessInfo.processInfo.environment["OPENCLAW_IOS_LIVE_GATEWAY"] == "1",
"Set OPENCLAW_IOS_LIVE_GATEWAY=1 and copy a fresh setup code to the simulator pasteboard"
)
"Set OPENCLAW_IOS_LIVE_GATEWAY=1 and copy a fresh setup code to the simulator pasteboard")
let app = XCUIApplication()
addUIInterruptionMonitor(withDescription: "Local network access") { alert in
@ -104,10 +128,10 @@ final class OpenClawSnapshotUITests: XCTestCase {
let overview = app.buttons.containing(.staticText, identifier: "Overview").firstMatch
XCTAssertTrue(overview.waitForExistence(timeout: 8))
attachScreenshot(named: "live-gateway-control")
self.attachScreenshot(named: "live-gateway-control")
overview.tap()
XCTAssertTrue(app.buttons["Back to Control"].waitForExistence(timeout: 8))
attachScreenshot(named: "live-gateway-overview")
self.attachScreenshot(named: "live-gateway-overview")
XCTAssertEqual(app.state, .runningForeground)
}
@ -118,7 +142,7 @@ final class OpenClawSnapshotUITests: XCTestCase {
self.app?.terminate()
let app = XCUIApplication()
setupSnapshot(app, waitForAnimations: false)
setupSnapshot(app)
app.launchArguments += [
"--openclaw-screenshot-mode",
"--openclaw-appearance",
@ -137,7 +161,7 @@ final class OpenClawSnapshotUITests: XCTestCase {
}
private func attachScreenshot(named name: String) {
guard let app = app else { return }
guard let app else { return }
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = name
attachment.lifetime = .keepAlways

View file

@ -3,7 +3,7 @@ options:
bundleIdPrefix: ai.openclawfoundation
deploymentTarget:
iOS: "18.0"
xcodeVersion: "16.0"
xcodeVersion: "26.0"
settings:
base:

View file

@ -333,11 +333,12 @@ struct OpenClawChatComposer: View {
private var cleanEditor: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .center, spacing: 8) {
HStack(alignment: .bottom, spacing: 8) {
self.compactAccessory(self.attachmentPicker)
HStack(alignment: .center, spacing: 8) {
HStack(alignment: .bottom, spacing: 8) {
self.editorOverlay
.padding(.vertical, self.cleanEditorTextPadding)
.frame(minHeight: self.cleanEditorMinHeight)
if let talkControl {
@ -348,10 +349,10 @@ struct OpenClawChatComposer: View {
.padding(.trailing, 6)
.frame(minHeight: self.cleanEditorMinHeight)
.background(
Capsule(style: .continuous)
RoundedRectangle(cornerRadius: self.cleanEditorCornerRadius, style: .continuous)
.fill(OpenClawChatTheme.composerField)
.overlay(
Capsule(style: .continuous)
RoundedRectangle(cornerRadius: self.cleanEditorCornerRadius, style: .continuous)
.strokeBorder(OpenClawChatTheme.composerBorder)))
self.sendButton
@ -516,16 +517,13 @@ struct OpenClawChatComposer: View {
text: self.$viewModel.input,
axis: .vertical)
.font(.body)
.textFieldStyle(.plain)
.lineLimit(1...4)
.fixedSize(horizontal: false, vertical: true)
.submitLabel(.send)
.onSubmit {
self.sendDraftIfEnabled()
}
.frame(
minHeight: self.textMinHeight,
idealHeight: self.textMinHeight,
maxHeight: self.textMaxHeight,
alignment: self.editorTextAlignment)
.padding(.horizontal, self.cleanFieldTextInset)
.padding(.vertical, self.composerChrome == .clean ? 0 : 6)
.focused(self.$isFocused)
@ -637,6 +635,14 @@ struct OpenClawChatComposer: View {
max(self.cleanControlHeight, self.textMinHeight)
}
private var cleanEditorCornerRadius: CGFloat {
self.cleanControlHeight / 2
}
private var cleanEditorTextPadding: CGFloat {
6
}
private var sendButtonSize: CGFloat {
self.composerChrome == .clean ? self.cleanControlHeight : 44
}
@ -661,10 +667,6 @@ struct OpenClawChatComposer: View {
self.composerChrome == .clean ? .leading : .topLeading
}
private var editorTextAlignment: Alignment {
self.composerChrome == .clean ? .leading : .top
}
private var sendButtonFill: Color {
self.userAccent ?? OpenClawChatTheme.userBubble
}

View file

@ -12,6 +12,9 @@ struct ChatDynamicTypeSourceGuardTests {
#expect(!sources.composer.contains(".frame(height: self.cleanControlHeight)"))
#expect(!sources.composer.contains("return self.composerChrome == .clean ? 48 : 64"))
#expect(sources.composer.contains("@ScaledMetric(relativeTo: .body)"))
#expect(sources.composer.contains(".textFieldStyle(.plain)"))
#expect(sources.composer.contains(".lineLimit(1...4)"))
#expect(sources.composer.contains(".fixedSize(horizontal: false, vertical: true)"))
}
private static func scopedChatTextSources() throws -> (

View file

@ -123,7 +123,8 @@ const CONDITIONAL_BRANCHES = [
/\bif\s*\([^)]*\)\s*"((?:\\.|[^"\\])*)"\s*else\s*"((?:\\.|[^"\\])*)"/gu,
/\?\s*"((?:\\.|[^"\\])*)"\s*:\s*"((?:\\.|[^"\\])*)"/gu,
];
const UI_STRING_NAME_RE = /(?:title|subtitle|body|message|label|text|description|prompt|help)$/iu;
const UI_STRING_NAME_RE =
/(?:title|subtitle|body|message|label|text|description|detail|prompt|help)$/iu;
const APPLE_STRING_PROPERTY = /\bvar\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*String\s*\{/gu;
const APPLE_SWITCH_BRANCH =
/(?:\bcase\b[^:\n]+|\bdefault)\s*:\s*(?:return\s+)?"((?:\\.|[^"\\])*)"/gu;
@ -135,7 +136,7 @@ const ANDROID_RESOURCE_COLLECTIONS =
/<(?:string-array|plurals)\b[^>]*>([\s\S]*?)<\/(?:string-array|plurals)>/gu;
const ANDROID_RESOURCE_ITEMS = /<item\b[^>]*>([\s\S]*?)<\/item>/gu;
const APPLE_NAMED_LITERALS =
/\b(?:title|subtitle|label|message|text|prompt|description|help)\s*:\s*(?:"""([\s\S]*?)"""|"((?:\\.|[^"\\])*)")/gu;
/\b([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(?:"""([\s\S]*?)"""|"((?:\\.|[^"\\])*)")/gu;
const APPLE_VIEW_TYPE = /\bstruct\s+([A-Za-z_][A-Za-z0-9_]*)[^:{\n]*:\s*[^{\n]*\bView\b/gu;
const APPLE_VIEW_FUNCTION =
/\bfunc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\([^{}]*?\)\s*(?:async\s*)?(?:throws\s*)?->\s*some\s+View\b/gu;
@ -622,12 +623,18 @@ function extractCandidates(
}
}
for (const match of source.matchAll(APPLE_NAMED_LITERALS)) {
const argumentName = match[1];
const callName = enclosingCallName(source, match.index ?? 0);
if (!callName || !uiCallNames.has(callName)) {
if (
!argumentName ||
!UI_STRING_NAME_RE.test(argumentName) ||
!callName ||
!uiCallNames.has(callName)
) {
continue;
}
const multiline = match[1];
const literal = multiline ?? match[2];
const multiline = match[2];
const literal = multiline ?? match[3];
if (literal) {
addCandidate(
entries,

View file

@ -39,7 +39,7 @@ describe("native app i18n inventory", () => {
),
).toBe(true);
expect(entries.some((entry) => entry.source === "QR Scanner Unavailable")).toBe(true);
expect(entries.some((entry) => entry.source === "Request ID: \\(requestId)")).toBe(true);
expect(entries.some((entry) => entry.source === "Request ID: \\(value)")).toBe(true);
expect(entries.some((entry) => entry.source === "Open ${row.title}")).toBe(true);
expect(entries.some((entry) => entry.source === "$deviceModel · $appVersion")).toBe(true);
expect(entries.some((entry) => entry.source === "Approval command copied")).toBe(true);
@ -54,8 +54,25 @@ describe("native app i18n inventory", () => {
expect(entries.some((entry) => entry.source === "DIARY")).toBe(true);
expect(entries.some((entry) => entry.source === "ask OpenClaw $prompt")).toBe(true);
expect(entries.some((entry) => entry.source === "OpenClaw is paused")).toBe(true);
expect(entries.some((entry) => entry.source === "Last issue")).toBe(true);
expect(entries.some((entry) => entry.source === "Agent chat and recent work.")).toBe(true);
expect(entries.some((entry) => entry.source === "Connected services and message routing")).toBe(
true,
);
expect(
entries.some(
(entry) =>
entry.path === "apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift" &&
entry.kind === "ui-named-argument" &&
entry.source === "Details",
),
).toBe(true);
expect(
entries.some(
(entry) =>
entry.path === "apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift" &&
entry.kind === "ui-named-argument" &&
entry.source === "Open Settings",
),
).toBe(true);
expect(entries.some((entry) => entry.source === "No sessions yet")).toBe(true);
expect(entries.some((entry) => entry.source === "Dont show this again")).toBe(true);
expect(entries.some((entry) => entry.source === "Use Manual Gateway")).toBe(true);