feat(macos): manage cron jobs

This commit is contained in:
Peter Steinberger
2025-12-13 12:09:27 +00:00
parent 5f159c43c5
commit cf90bd9c86
2 changed files with 47 additions and 4 deletions

View File

@@ -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?

View File

@@ -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),