From cf90bd9c86eb8cd315b706cd376459407b37f287 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 13 Dec 2025 12:09:27 +0000 Subject: [PATCH] feat(macos): manage cron jobs --- apps/macos/Sources/Clawdis/CronModels.swift | 3 +- apps/macos/Sources/Clawdis/CronSettings.swift | 48 +++++++++++++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/apps/macos/Sources/Clawdis/CronModels.swift b/apps/macos/Sources/Clawdis/CronModels.swift index c440671a4..04f929989 100644 --- a/apps/macos/Sources/Clawdis/CronModels.swift +++ b/apps/macos/Sources/Clawdis/CronModels.swift @@ -131,7 +131,6 @@ enum CronPayload: Codable, Equatable { } struct CronIsolation: Codable, Equatable { - var postToMain: Bool? var postToMainPrefix: String? } @@ -180,6 +179,7 @@ struct CronEvent: Codable, Sendable { let durationMs: Int? let status: String? let error: String? + let summary: String? let nextRunAtMs: Int? } @@ -191,6 +191,7 @@ struct CronRunLogEntry: Codable, Identifiable, Sendable { let action: String let status: String? let error: String? + let summary: String? let runAtMs: Int? let durationMs: Int? let nextRunAtMs: Int? diff --git a/apps/macos/Sources/Clawdis/CronSettings.swift b/apps/macos/Sources/Clawdis/CronSettings.swift index 5be14cb56..058cfdd97 100644 --- a/apps/macos/Sources/Clawdis/CronSettings.swift +++ b/apps/macos/Sources/Clawdis/CronSettings.swift @@ -215,6 +215,11 @@ struct CronSettings: View { @ViewBuilder private func jobContextMenu(_ job: CronJob) -> some View { Button("Run now") { Task { await self.store.runJob(id: job.id, force: true) } } + if job.sessionTarget == .isolated { + Button("Open transcript") { + WebChatManager.shared.show(sessionKey: "cron:\(job.id)") + } + } Divider() Button(job.enabled ? "Disable" : "Enable") { Task { await self.store.setJobEnabled(id: job.id, enabled: !job.enabled) } @@ -251,6 +256,12 @@ struct CronSettings: View { .labelsHidden() Button("Run") { Task { await self.store.runJob(id: job.id, force: true) } } .buttonStyle(.borderedProminent) + if job.sessionTarget == .isolated { + Button("Transcript") { + WebChatManager.shared.show(sessionKey: "cron:\(job.id)") + } + .buttonStyle(.bordered) + } Button("Edit") { self.editingJob = job self.editorError = nil @@ -348,6 +359,13 @@ struct CronSettings: View { .foregroundStyle(.secondary) } } + if let summary = entry.summary, !summary.isEmpty { + Text(summary) + .font(.caption) + .foregroundStyle(.secondary) + .textSelection(.enabled) + .lineLimit(2) + } if let error = entry.error, !error.isEmpty { Text(error) .font(.caption) @@ -567,7 +585,7 @@ private struct CronJobEditor: View { } } - if self.payloadKind == .agentTurn || self.sessionTarget == .isolated { + if self.sessionTarget == .isolated { Section("Main session summary") { Text("Isolated jobs always post a summary back into the main session when they finish.") .font(.caption) @@ -603,9 +621,16 @@ private struct CronJobEditor: View { } .padding(18) .onAppear { self.hydrateFromJob() } + .onChange(of: self.payloadKind) { _, newValue in + if newValue == .agentTurn, self.sessionTarget == .main { + self.sessionTarget = .isolated + } + } .onChange(of: self.sessionTarget) { _, newValue in if newValue == .isolated { self.payloadKind = .agentTurn + } else if newValue == .main, self.payloadKind == .agentTurn { + self.payloadKind = .systemEvent } } } @@ -719,6 +744,22 @@ private struct CronJobEditor: View { } }() + if self.sessionTarget == .main, payload["kind"] as? String == "agentTurn" { + throw NSError( + domain: "Cron", + code: 0, + userInfo: [ + NSLocalizedDescriptionKey: "Main session jobs require systemEvent payloads (switch Session target to isolated).", + ]) + } + + if self.sessionTarget == .isolated, payload["kind"] as? String == "systemEvent" { + throw NSError( + domain: "Cron", + code: 0, + userInfo: [NSLocalizedDescriptionKey: "Isolated jobs require agentTurn payloads."]) + } + if payload["kind"] as? String == "systemEvent" { if (payload["text"] as? String ?? "").isEmpty { throw NSError( @@ -744,7 +785,7 @@ private struct CronJobEditor: View { ] if !name.isEmpty { root["name"] = name } - if payload["kind"] as? String == "agentTurn" || self.sessionTarget == .isolated { + if self.sessionTarget == .isolated { let trimmed = self.postPrefix.trimmingCharacters(in: .whitespacesAndNewlines) root["isolation"] = [ "postToMainPrefix": trimmed.isEmpty ? "Cron" : trimmed, @@ -831,7 +872,7 @@ struct CronSettings_Previews: PreviewProvider { channel: "last", to: nil, bestEffortDeliver: true), - isolation: CronIsolation(postToMain: nil, postToMainPrefix: "Cron"), + isolation: CronIsolation(postToMainPrefix: "Cron"), state: CronJobState( nextRunAtMs: Int(Date().addingTimeInterval(3600).timeIntervalSince1970 * 1000), runningAtMs: nil, @@ -848,6 +889,7 @@ struct CronSettings_Previews: PreviewProvider { action: "finished", status: "ok", error: nil, + summary: "All good.", runAtMs: nil, durationMs: 1234, nextRunAtMs: nil),