mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-31 23:17:27 +00:00
Summary: - Replace the legacy iOS shell with Pro Command, Chat, Agents, and Settings tabs. - Wire iOS chat/session/settings/diagnostics and realtime Talk flows through gateway-backed APIs. - Add gateway/session and shared chat coverage for the new iOS flow. Verification: - git diff --check - node scripts/run-vitest.mjs src/gateway/server.sessions.create.test.ts src/gateway/talk-realtime-relay.test.ts - swift test --filter ChatViewModelTests (apps/shared/OpenClawKit) - xcodebuild build for Nimrod's iPhone succeeded; install succeeded; launch was blocked because the phone was locked Known follow-up: - Preserve traceLevel in sessions.create parent runtime inheritance and keep the changelog credit in the follow-up patch.
578 lines
18 KiB
Swift
578 lines
18 KiB
Swift
import SwiftUI
|
|
|
|
enum OpenClawProMetric {
|
|
static let pagePadding: CGFloat = 20
|
|
static let cardRadius: CGFloat = 14
|
|
static let controlRadius: CGFloat = 12
|
|
static let bottomScrollInset: CGFloat = 96
|
|
static let heroRadius: CGFloat = 22
|
|
}
|
|
|
|
struct OpenClawProBackground: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
var body: some View {
|
|
LinearGradient(
|
|
colors: OpenClawBrand.canvasColors(for: self.colorScheme),
|
|
startPoint: .top,
|
|
endPoint: .bottom)
|
|
.ignoresSafeArea()
|
|
.overlay(alignment: .top) {
|
|
if self.colorScheme == .light {
|
|
LinearGradient(
|
|
colors: [
|
|
OpenClawBrand.accent.opacity(0.05),
|
|
.clear,
|
|
],
|
|
startPoint: .topTrailing,
|
|
endPoint: .bottomLeading)
|
|
.frame(height: 260)
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ProSectionHeader: View {
|
|
let title: String
|
|
var actionTitle: String?
|
|
var action: (() -> Void)?
|
|
var uppercase = true
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Text(self.title)
|
|
.font(.caption.weight(.medium))
|
|
.foregroundStyle(.secondary)
|
|
.textCase(self.uppercase ? .uppercase : nil)
|
|
Spacer()
|
|
if let actionTitle {
|
|
if let action {
|
|
Button(actionTitle, action: action)
|
|
.font(.caption.weight(.medium))
|
|
.foregroundStyle(OpenClawBrand.accent)
|
|
} else {
|
|
Text(actionTitle)
|
|
.font(.caption.weight(.medium))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
|
}
|
|
}
|
|
|
|
struct ProCard<Content: View>: View {
|
|
var tint: Color?
|
|
var isProminent: Bool = false
|
|
var padding: CGFloat = 14
|
|
var radius: CGFloat = OpenClawProMetric.cardRadius
|
|
@ViewBuilder var content: Content
|
|
|
|
var body: some View {
|
|
self.content
|
|
.padding(self.padding)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.proPanelSurface(
|
|
tint: self.tint,
|
|
radius: self.radius,
|
|
isProminent: self.isProminent)
|
|
}
|
|
}
|
|
|
|
private struct ProPanelBackground: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
let radius: CGFloat
|
|
let tint: Color?
|
|
let isProminent: Bool
|
|
|
|
var body: some View {
|
|
let shape = RoundedRectangle(cornerRadius: self.radius, style: .continuous)
|
|
shape
|
|
.fill(self.fill)
|
|
.overlay {
|
|
ProPanelTexture()
|
|
.opacity(self.colorScheme == .dark ? 0.22 : 0.08)
|
|
.clipShape(shape)
|
|
}
|
|
.overlay {
|
|
shape.strokeBorder(self.borderStyle, lineWidth: 1)
|
|
}
|
|
.overlay {
|
|
shape
|
|
.strokeBorder(Color.black.opacity(self.colorScheme == .dark ? 0.40 : 0.055), lineWidth: 0.7)
|
|
.padding(1)
|
|
}
|
|
.overlay(alignment: .top) {
|
|
shape
|
|
.strokeBorder(Color.white.opacity(self.colorScheme == .dark ? 0.07 : 0.36), lineWidth: 0.7)
|
|
.mask(alignment: .top) {
|
|
Rectangle().frame(height: 28)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var fill: AnyShapeStyle {
|
|
if self.colorScheme == .dark {
|
|
let base = self.isProminent
|
|
? Color(red: 15 / 255, green: 17 / 255, blue: 19 / 255)
|
|
: Color(red: 10 / 255, green: 12 / 255, blue: 14 / 255)
|
|
return AnyShapeStyle(base)
|
|
}
|
|
|
|
let gradient = LinearGradient(
|
|
colors: [
|
|
Color.white.opacity(0.98),
|
|
(self.tint ?? Color.white).opacity(self.tint == nil ? 0.92 : 0.12),
|
|
Color(red: 246 / 255, green: 247 / 255, blue: 249 / 255),
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing)
|
|
return AnyShapeStyle(gradient)
|
|
}
|
|
|
|
private var borderStyle: AnyShapeStyle {
|
|
if self.colorScheme == .dark {
|
|
return AnyShapeStyle(Color.white.opacity(self.isProminent ? 0.15 : 0.11))
|
|
}
|
|
|
|
let gradient = LinearGradient(
|
|
colors: [
|
|
Color.white.opacity(0.72),
|
|
(self.tint ?? OpenClawBrand.accent).opacity(0.10),
|
|
Color.black.opacity(0.08),
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing)
|
|
return AnyShapeStyle(gradient)
|
|
}
|
|
}
|
|
|
|
private struct ProPanelTexture: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
var body: some View {
|
|
Canvas { context, size in
|
|
let color = self.colorScheme == .dark ? Color.white.opacity(0.11) : Color.black.opacity(0.08)
|
|
for y in stride(from: 2.0, through: size.height, by: 6.5) {
|
|
let offset = Int(y / 6.5).isMultiple(of: 2) ? 0.0 : 3.25
|
|
for x in stride(from: 2.0 + offset, through: size.width, by: 6.5) {
|
|
let dot = CGRect(x: x, y: y, width: 0.7, height: 0.7)
|
|
context.fill(Path(ellipseIn: dot), with: .color(color))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct ProLightGlassModifier: ViewModifier {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
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))
|
|
} else {
|
|
content
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct ProGlassSurfaceModifier: ViewModifier {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
let fill: Color
|
|
let stroke: Color
|
|
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))
|
|
} else {
|
|
surfaced
|
|
}
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func proPanelSurface(
|
|
tint: Color? = nil,
|
|
radius: CGFloat = OpenClawProMetric.cardRadius,
|
|
isProminent: Bool = false) -> some View
|
|
{
|
|
self.modifier(ProPanelSurfaceModifier(
|
|
tint: tint,
|
|
radius: radius,
|
|
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))
|
|
}
|
|
}
|
|
|
|
private struct ProPanelSurfaceModifier: ViewModifier {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
let tint: Color?
|
|
let radius: CGFloat
|
|
let isProminent: Bool
|
|
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.background {
|
|
ProPanelBackground(
|
|
radius: self.radius,
|
|
tint: self.tint,
|
|
isProminent: self.isProminent)
|
|
}
|
|
.modifier(ProLightGlassModifier(radius: self.radius))
|
|
.shadow(
|
|
color: self.colorScheme == .dark ? .black.opacity(0.60) : .black.opacity(0.045),
|
|
radius: self.isProminent ? 20 : 12,
|
|
y: self.isProminent ? 10 : 6)
|
|
}
|
|
}
|
|
|
|
struct ProIconBadge: View {
|
|
let systemName: String
|
|
let color: Color
|
|
|
|
var body: some View {
|
|
Image(systemName: self.systemName)
|
|
.font(.subheadline.weight(.semibold))
|
|
.foregroundStyle(self.color)
|
|
.frame(width: 34, height: 34)
|
|
.background {
|
|
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
.fill(self.color.opacity(0.12))
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ProStatusDot: View {
|
|
var color: Color
|
|
|
|
var body: some View {
|
|
Circle()
|
|
.fill(self.color)
|
|
.frame(width: 8, height: 8)
|
|
.shadow(color: self.color.opacity(0.35), radius: 4)
|
|
}
|
|
}
|
|
|
|
struct ProValuePill: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
let value: String
|
|
let color: Color
|
|
|
|
var body: some View {
|
|
Text(self.value)
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundStyle(self.color)
|
|
.lineLimit(1)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 5)
|
|
.background {
|
|
Capsule()
|
|
.fill(self.color.opacity(self.colorScheme == .dark ? 0.12 : 0.08))
|
|
}
|
|
}
|
|
}
|
|
|
|
struct OpenClawProMark: View {
|
|
var size: CGFloat = 42
|
|
var shadowRadius: CGFloat = 10
|
|
|
|
var body: some View {
|
|
Image("OpenClawIcon")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: self.size, height: self.size)
|
|
.shadow(color: OpenClawBrand.accent.opacity(0.28), radius: self.shadowRadius, y: self.shadowRadius / 2)
|
|
.accessibilityLabel("OpenClaw")
|
|
}
|
|
}
|
|
|
|
struct ProProgressBar: View {
|
|
let progress: Double
|
|
var color: Color = OpenClawBrand.accentHot
|
|
|
|
var body: some View {
|
|
GeometryReader { proxy in
|
|
let clamped = max(0, min(self.progress, 1))
|
|
ZStack(alignment: .leading) {
|
|
Capsule()
|
|
.fill(Color.primary.opacity(0.10))
|
|
Capsule()
|
|
.fill(self.color)
|
|
.frame(width: proxy.size.width * clamped)
|
|
}
|
|
}
|
|
.frame(height: 3)
|
|
}
|
|
}
|
|
|
|
struct ProWorkRow: View {
|
|
let icon: String
|
|
let title: String
|
|
let detail: String
|
|
let state: String
|
|
let trailing: String
|
|
let color: Color
|
|
var progress: Double?
|
|
|
|
var body: some View {
|
|
HStack(alignment: .top, spacing: 12) {
|
|
ProIconBadge(systemName: self.icon, color: self.color)
|
|
VStack(alignment: .leading, spacing: 5) {
|
|
HStack(alignment: .firstTextBaseline) {
|
|
Text(self.title)
|
|
.font(.subheadline.weight(.semibold))
|
|
Spacer(minLength: 8)
|
|
Text(self.trailing)
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Text(self.detail)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
HStack(spacing: 8) {
|
|
if let progress {
|
|
ProProgressBar(progress: progress, color: self.color)
|
|
.frame(maxWidth: 120)
|
|
}
|
|
Text(self.state)
|
|
.font(.caption2.weight(.semibold))
|
|
.foregroundStyle(self.color)
|
|
}
|
|
}
|
|
}
|
|
.padding(.vertical, 9)
|
|
}
|
|
}
|
|
|
|
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))
|
|
}
|
|
.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 ProSegmentedControl: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
let labels: [String]
|
|
@Binding var selection: Int
|
|
|
|
var body: some View {
|
|
HStack(spacing: 4) {
|
|
ForEach(Array(self.labels.enumerated()), id: \.offset) { index, label in
|
|
Button {
|
|
self.selection = index
|
|
} label: {
|
|
Text(label)
|
|
.font(.subheadline.weight(self.selection == index ? .semibold : .regular))
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 9)
|
|
.background(self.segmentFill(isSelected: self.selection == index), in: Capsule())
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(4)
|
|
.background {
|
|
Capsule()
|
|
.fill(self.trackFill)
|
|
.overlay {
|
|
Capsule().strokeBorder(self.trackStroke, lineWidth: 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func segmentFill(isSelected: Bool) -> Color {
|
|
guard isSelected else { return .clear }
|
|
return self.colorScheme == .dark ? Color.white.opacity(0.12) : Color.primary.opacity(0.08)
|
|
}
|
|
|
|
private var trackFill: Color {
|
|
self.colorScheme == .dark ? Color.white.opacity(0.045) : Color.white.opacity(0.72)
|
|
}
|
|
|
|
private var trackStroke: Color {
|
|
self.colorScheme == .dark ? Color.white.opacity(0.10) : Color.black.opacity(0.06)
|
|
}
|
|
}
|
|
|
|
struct ProHeroActionButton: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
let title: String
|
|
let detail: String
|
|
let systemImage: String
|
|
let action: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: self.action) {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: self.systemImage)
|
|
.font(.headline.weight(.semibold))
|
|
.foregroundStyle(.white)
|
|
.frame(width: 42, height: 42)
|
|
.background(OpenClawBrand.accentHot, in: RoundedRectangle(cornerRadius: 13, style: .continuous))
|
|
|
|
VStack(alignment: .leading, spacing: 3) {
|
|
Text(self.title)
|
|
.font(.subheadline.weight(.semibold))
|
|
.foregroundStyle(.primary)
|
|
Text(self.detail)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
|
|
Spacer(minLength: 8)
|
|
|
|
Image(systemName: "arrow.right")
|
|
.font(.subheadline.weight(.bold))
|
|
.foregroundStyle(OpenClawBrand.accentHot)
|
|
}
|
|
.padding(12)
|
|
.proGlassSurface(
|
|
fill: self.colorScheme == .dark ? Color.white.opacity(0.045) : Color.white.opacity(0.68),
|
|
stroke: OpenClawBrand.accent.opacity(self.colorScheme == .dark ? 0.22 : 0.14),
|
|
radius: 18,
|
|
isProminent: true,
|
|
interactive: true)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
|
|
struct ProMetricTile: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
let title: String
|
|
let value: String
|
|
let icon: String
|
|
let color: Color
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
HStack {
|
|
Image(systemName: self.icon)
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundStyle(self.color)
|
|
.frame(width: 24, height: 24)
|
|
.background(self.color.opacity(self.colorScheme == .dark ? 0.18 : 0.10), in: Circle())
|
|
Spacer(minLength: 4)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(self.value)
|
|
.font(.headline.weight(.bold))
|
|
.lineLimit(1)
|
|
.minimumScaleFactor(0.72)
|
|
Text(self.title)
|
|
.font(.caption2.weight(.medium))
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
.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)
|
|
}
|
|
}
|
|
|
|
struct ProStatusRow: View {
|
|
let icon: String
|
|
let title: String
|
|
let detail: String
|
|
let value: String
|
|
let color: Color
|
|
|
|
var body: some View {
|
|
HStack(alignment: .center, spacing: 12) {
|
|
ProIconBadge(systemName: self.icon, color: self.color)
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(self.title)
|
|
.font(.subheadline.weight(.semibold))
|
|
Text(self.detail)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
Spacer(minLength: 8)
|
|
ProValuePill(value: self.value, color: self.color)
|
|
}
|
|
.padding(.vertical, 11)
|
|
}
|
|
}
|
|
|
|
struct ProTimelineRow: View {
|
|
let done: Bool
|
|
let title: String
|
|
let detail: String
|
|
|
|
var body: some View {
|
|
HStack(alignment: .top, spacing: 10) {
|
|
ProIconBadge(
|
|
systemName: self.done ? "checkmark.circle.fill" : "clock.fill",
|
|
color: self.done ? OpenClawBrand.ok : OpenClawBrand.warn)
|
|
VStack(alignment: .leading, spacing: 3) {
|
|
Text(self.title)
|
|
.font(.subheadline.weight(.medium))
|
|
Text(self.detail)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|