mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-01 11:43:32 +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.
179 lines
6.9 KiB
Swift
179 lines
6.9 KiB
Swift
import OpenClawKit
|
|
import OpenClawProtocol
|
|
import SwiftUI
|
|
|
|
extension AgentProTab {
|
|
var cronStatusCard: some View {
|
|
ProCard(radius: AgentLayout.cardRadius) {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack {
|
|
Text("Scheduler")
|
|
.font(.headline)
|
|
Spacer()
|
|
ProValuePill(
|
|
value: self.overview?.cronStatus?.enabled == true ? "on" : "off",
|
|
color: self.cronColor)
|
|
}
|
|
HStack(spacing: 10) {
|
|
let jobCount = self.overview?.cronStatus?.jobs
|
|
?? self.overview?.cronJobs.count
|
|
?? 0
|
|
self.detailMetric(label: "Jobs", value: "\(jobCount)")
|
|
self.detailMetric(label: "Next", value: self.cronNextRunLabel)
|
|
}
|
|
if let cronActionStatusText {
|
|
Text(cronActionStatusText)
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(2)
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
|
}
|
|
|
|
var cronNextRunLabel: String {
|
|
guard let nextWakeAtMs = self.overview?.cronStatus?.nextwakeatms else { return "none" }
|
|
return Self.relativeTime(fromMilliseconds: nextWakeAtMs)
|
|
}
|
|
|
|
func cronJobsList(limit: Int?) -> some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
ProSectionHeader(title: "Jobs")
|
|
ProCard(padding: 0, radius: AgentLayout.cardRadius) {
|
|
let jobs = self.sortedCronJobs
|
|
let visible = limit.map { Array(jobs.prefix($0)) } ?? jobs
|
|
if visible.isEmpty {
|
|
self.emptyCronRow
|
|
.padding(14)
|
|
} else {
|
|
VStack(spacing: 0) {
|
|
ForEach(Array(visible.enumerated()), id: \.element.id) { index, job in
|
|
self.cronJobDetailRow(job)
|
|
if index < visible.count - 1 {
|
|
Divider().padding(.leading, 60)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
|
}
|
|
}
|
|
|
|
var sortedCronJobs: [CronJob] {
|
|
(self.overview?.cronJobs ?? [])
|
|
.sorted { lhs, rhs in
|
|
let lhsNext = AgentProValueReader.intValue(lhs.state["nextRunAtMs"])
|
|
let rhsNext = AgentProValueReader.intValue(rhs.state["nextRunAtMs"])
|
|
switch (lhsNext, rhsNext) {
|
|
case let (lhsNext?, rhsNext?): return lhsNext < rhsNext
|
|
case (_?, nil): return true
|
|
case (nil, _?): return false
|
|
case (nil, nil): return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
|
|
}
|
|
}
|
|
}
|
|
|
|
func cronJobDetailRow(_ job: CronJob) -> some View {
|
|
let busy = self.cronActionBusyIDs.contains(job.id)
|
|
return HStack(alignment: .top, spacing: 12) {
|
|
ProIconBadge(
|
|
systemName: job.enabled ? "clock.arrow.circlepath" : "pause.circle",
|
|
color: job.enabled ? OpenClawBrand.accent : .secondary)
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(job.name)
|
|
.font(.subheadline.weight(.semibold))
|
|
.lineLimit(1)
|
|
Text(self.cronJobDetail(job))
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(2)
|
|
Text(self.cronScheduleSummary(job))
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
HStack(spacing: 8) {
|
|
Button {
|
|
Task { await self.runCronJob(job) }
|
|
} label: {
|
|
Label("Run", systemImage: "play.fill")
|
|
}
|
|
.disabled(busy || !self.gatewayConnected)
|
|
|
|
Button {
|
|
Task { await self.setCronJob(job, enabled: !job.enabled) }
|
|
} label: {
|
|
Label(job.enabled ? "Pause" : "Enable", systemImage: job.enabled ? "pause.fill" : "checkmark")
|
|
}
|
|
.disabled(busy || !self.gatewayConnected)
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.controlSize(.mini)
|
|
}
|
|
Spacer(minLength: 8)
|
|
if busy {
|
|
ProgressView()
|
|
.progressViewStyle(.circular)
|
|
.controlSize(.small)
|
|
} else {
|
|
Text(self.cronJobState(job))
|
|
.font(.caption2.weight(.semibold))
|
|
.foregroundStyle(job.enabled ? OpenClawBrand.accent : .secondary)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
.padding(.vertical, 10)
|
|
.padding(.horizontal, 14)
|
|
}
|
|
|
|
@MainActor
|
|
func runCronJob(_ job: CronJob) async {
|
|
await self.runCronAction(job, success: "Queued \(job.name).") {
|
|
let params = CronRunParams(id: job.id, mode: "force")
|
|
_ = try await self.requestGateway(method: "cron.run", params: params, timeoutSeconds: 20)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
func setCronJob(_ job: CronJob, enabled: Bool) async {
|
|
await self.runCronAction(job, success: enabled ? "Enabled \(job.name)." : "Paused \(job.name).") {
|
|
let params = CronUpdateParams(id: job.id, patch: CronUpdatePatch(enabled: enabled))
|
|
_ = try await self.requestGateway(method: "cron.update", params: params, timeoutSeconds: 20)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
func runCronAction(
|
|
_ job: CronJob,
|
|
success: String,
|
|
action: () async throws -> Void) async
|
|
{
|
|
guard self.gatewayConnected else { return }
|
|
self.cronActionBusyIDs.insert(job.id)
|
|
self.cronActionStatusText = nil
|
|
defer { self.cronActionBusyIDs.remove(job.id) }
|
|
do {
|
|
try await action()
|
|
self.cronActionStatusText = success
|
|
await self.refreshOverview(force: true)
|
|
} catch {
|
|
self.cronActionStatusText = Self.skillMutationMessage(error)
|
|
}
|
|
}
|
|
|
|
func cronScheduleSummary(_ job: CronJob) -> String {
|
|
guard let schedule = job.schedule.value as? [String: AnyCodable] else { return "Schedule configured" }
|
|
if let expr = Self.stringValue(schedule["expr"]) {
|
|
return "Cron \(expr)"
|
|
}
|
|
if let everyMs = AgentProValueReader.intValue(schedule["everyMs"]) {
|
|
return "Every \(Self.duration(milliseconds: everyMs))"
|
|
}
|
|
if let kind = Self.stringValue(schedule["kind"]) {
|
|
return kind
|
|
}
|
|
return "Schedule configured"
|
|
}
|
|
}
|