mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-01 10:43:28 +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.
282 lines
9.4 KiB
Swift
282 lines
9.4 KiB
Swift
import SwiftUI
|
|
|
|
struct CommandPanel<Content: View>: View {
|
|
var tint: Color?
|
|
var isProminent = false
|
|
var padding: CGFloat = 13
|
|
@ViewBuilder var content: Content
|
|
|
|
init(
|
|
tint: Color? = nil,
|
|
isProminent: Bool = false,
|
|
padding: CGFloat = 13,
|
|
@ViewBuilder content: () -> Content)
|
|
{
|
|
self.tint = tint
|
|
self.isProminent = isProminent
|
|
self.padding = padding
|
|
self.content = content()
|
|
}
|
|
|
|
var body: some View {
|
|
ProCard(
|
|
tint: self.tint,
|
|
isProminent: self.isProminent,
|
|
padding: self.padding,
|
|
radius: 12)
|
|
{
|
|
self.content
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CommandControlBackground: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
var body: some View {
|
|
LinearGradient(
|
|
colors: self.colorScheme == .dark ? self.darkColors : self.lightColors,
|
|
startPoint: .top,
|
|
endPoint: .bottom)
|
|
.overlay(alignment: .top) {
|
|
if self.colorScheme == .light {
|
|
LinearGradient(
|
|
colors: [
|
|
Color.white.opacity(0.34),
|
|
Color.clear,
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing)
|
|
.frame(height: 260)
|
|
}
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
|
|
private var darkColors: [Color] {
|
|
[
|
|
Color(red: 12 / 255, green: 13 / 255, blue: 15 / 255),
|
|
Color(red: 7 / 255, green: 8 / 255, blue: 10 / 255),
|
|
Color(red: 4 / 255, green: 5 / 255, blue: 6 / 255),
|
|
]
|
|
}
|
|
|
|
private var lightColors: [Color] {
|
|
[
|
|
Color(red: 247 / 255, green: 248 / 255, blue: 249 / 255),
|
|
Color(red: 251 / 255, green: 252 / 255, blue: 253 / 255),
|
|
.white,
|
|
]
|
|
}
|
|
}
|
|
|
|
struct CommandSessionRow: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
let item: CommandCenterTab.WorkItem
|
|
|
|
var body: some View {
|
|
HStack(alignment: .center, spacing: 12) {
|
|
Image(systemName: self.item.icon)
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundStyle(self.item.color)
|
|
.frame(width: 30, height: 30)
|
|
.background {
|
|
RoundedRectangle(cornerRadius: 9, style: .continuous)
|
|
.fill(self.item.color.opacity(0.12))
|
|
}
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
|
Text(self.item.title)
|
|
.font(.subheadline.weight(.semibold))
|
|
.lineLimit(1)
|
|
.minimumScaleFactor(0.82)
|
|
Spacer(minLength: 6)
|
|
Text(self.item.trailing)
|
|
.font(.caption2.weight(.medium))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
HStack(spacing: 8) {
|
|
Text(self.item.detail)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
Spacer(minLength: 6)
|
|
if let progress = self.item.progress {
|
|
ProProgressBar(progress: progress, color: self.item.color)
|
|
.frame(width: 68)
|
|
}
|
|
Text(self.progressLabel)
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundStyle(self.item.color)
|
|
.lineLimit(1)
|
|
.frame(width: 48, alignment: .trailing)
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 9)
|
|
.background {
|
|
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
.fill(self.rowFill)
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
.strokeBorder(self.rowBorder, lineWidth: 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var progressLabel: String {
|
|
guard let progress = self.item.progress else {
|
|
return self.item.state
|
|
}
|
|
if self.item.state == "offline" || self.item.state == "off" || self.item.state == "idle" {
|
|
return self.item.state
|
|
}
|
|
return "\(Int((progress * 100).rounded()))%"
|
|
}
|
|
|
|
private var rowFill: Color {
|
|
self.colorScheme == .dark ? Color.white.opacity(0.035) : Color.black.opacity(0.025)
|
|
}
|
|
|
|
private var rowBorder: Color {
|
|
self.colorScheme == .dark ? Color.white.opacity(0.065) : Color.black.opacity(0.045)
|
|
}
|
|
}
|
|
|
|
struct CommandApprovalRow: View {
|
|
let item: CommandCenterTab.ApprovalItem
|
|
|
|
var body: some View {
|
|
HStack(spacing: 10) {
|
|
Image(systemName: self.item.icon)
|
|
.font(.caption.weight(.bold))
|
|
.foregroundStyle(.white)
|
|
.frame(width: 30, height: 30)
|
|
.background {
|
|
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
.fill(self.item.color)
|
|
}
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(self.item.title)
|
|
.font(.subheadline.weight(.semibold))
|
|
.lineLimit(1)
|
|
Text(self.item.detail)
|
|
.font(.caption2.weight(.medium))
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
Spacer(minLength: 8)
|
|
Text(self.item.priority)
|
|
.font(.caption.weight(.bold))
|
|
.foregroundStyle(self.item.color)
|
|
.padding(.horizontal, 9)
|
|
.padding(.vertical, 5)
|
|
.background {
|
|
Capsule()
|
|
.fill(self.item.color.opacity(0.10))
|
|
}
|
|
}
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 7)
|
|
}
|
|
}
|
|
|
|
struct CommandEmptyStateRow: View {
|
|
let icon: String
|
|
let title: String
|
|
let detail: String
|
|
|
|
var body: some View {
|
|
HStack(spacing: 10) {
|
|
Image(systemName: self.icon)
|
|
.font(.caption.weight(.bold))
|
|
.foregroundStyle(OpenClawBrand.ok)
|
|
.frame(width: 30, height: 30)
|
|
.background {
|
|
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
.fill(OpenClawBrand.ok.opacity(0.10))
|
|
}
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(self.title)
|
|
.font(.subheadline.weight(.semibold))
|
|
.lineLimit(1)
|
|
Text(self.detail)
|
|
.font(.caption2.weight(.medium))
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
Spacer(minLength: 0)
|
|
}
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 9)
|
|
.background {
|
|
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
.fill(Color.black.opacity(0.06))
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
.strokeBorder(Color.primary.opacity(0.055), lineWidth: 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CommandTaskRow: View {
|
|
let item: CommandCenterTab.WorkItem
|
|
|
|
var body: some View {
|
|
HStack(alignment: .center, spacing: 6) {
|
|
Text(self.item.title)
|
|
.font(.footnote.weight(.semibold))
|
|
.lineLimit(1)
|
|
.minimumScaleFactor(0.80)
|
|
.frame(maxWidth: .infinity, minHeight: 20, alignment: .leading)
|
|
Text(self.item.detail)
|
|
.font(.caption.weight(.medium))
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
.minimumScaleFactor(0.78)
|
|
.frame(width: 64, alignment: .leading)
|
|
if let progress = self.item.progress {
|
|
ProProgressBar(progress: progress, color: self.item.color)
|
|
.frame(width: 56)
|
|
}
|
|
Text(self.item.state)
|
|
.font(.footnote.weight(.medium))
|
|
.foregroundStyle(self.item.progress == nil ? self.item.color : .secondary)
|
|
.lineLimit(1)
|
|
.frame(width: self.item.progress == nil ? 58 : 34, alignment: .trailing)
|
|
}
|
|
.padding(.vertical, 8)
|
|
}
|
|
}
|
|
|
|
struct CommandLiveActivityRow: View {
|
|
let title: String
|
|
let value: String
|
|
let color: Color
|
|
|
|
var body: some View {
|
|
HStack(spacing: 8) {
|
|
ProStatusDot(color: self.color)
|
|
Text(self.title)
|
|
.font(.subheadline.weight(.semibold))
|
|
.lineLimit(1)
|
|
Spacer(minLength: 8)
|
|
Text(self.value)
|
|
.font(.caption.weight(.medium))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 9)
|
|
.background {
|
|
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
.fill(Color.black.opacity(0.08))
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
.strokeBorder(Color.primary.opacity(0.06), lineWidth: 1)
|
|
}
|
|
}
|
|
}
|
|
}
|