feat(macos): manage cron jobs
This commit is contained in:
@@ -131,7 +131,6 @@ enum CronPayload: Codable, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct CronIsolation: Codable, Equatable {
|
struct CronIsolation: Codable, Equatable {
|
||||||
var postToMain: Bool?
|
|
||||||
var postToMainPrefix: String?
|
var postToMainPrefix: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +179,7 @@ struct CronEvent: Codable, Sendable {
|
|||||||
let durationMs: Int?
|
let durationMs: Int?
|
||||||
let status: String?
|
let status: String?
|
||||||
let error: String?
|
let error: String?
|
||||||
|
let summary: String?
|
||||||
let nextRunAtMs: Int?
|
let nextRunAtMs: Int?
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +191,7 @@ struct CronRunLogEntry: Codable, Identifiable, Sendable {
|
|||||||
let action: String
|
let action: String
|
||||||
let status: String?
|
let status: String?
|
||||||
let error: String?
|
let error: String?
|
||||||
|
let summary: String?
|
||||||
let runAtMs: Int?
|
let runAtMs: Int?
|
||||||
let durationMs: Int?
|
let durationMs: Int?
|
||||||
let nextRunAtMs: Int?
|
let nextRunAtMs: Int?
|
||||||
|
|||||||
@@ -215,6 +215,11 @@ struct CronSettings: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func jobContextMenu(_ job: CronJob) -> some View {
|
private func jobContextMenu(_ job: CronJob) -> some View {
|
||||||
Button("Run now") { Task { await self.store.runJob(id: job.id, force: true) } }
|
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()
|
Divider()
|
||||||
Button(job.enabled ? "Disable" : "Enable") {
|
Button(job.enabled ? "Disable" : "Enable") {
|
||||||
Task { await self.store.setJobEnabled(id: job.id, enabled: !job.enabled) }
|
Task { await self.store.setJobEnabled(id: job.id, enabled: !job.enabled) }
|
||||||
@@ -251,6 +256,12 @@ struct CronSettings: View {
|
|||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
Button("Run") { Task { await self.store.runJob(id: job.id, force: true) } }
|
Button("Run") { Task { await self.store.runJob(id: job.id, force: true) } }
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
|
if job.sessionTarget == .isolated {
|
||||||
|
Button("Transcript") {
|
||||||
|
WebChatManager.shared.show(sessionKey: "cron:\(job.id)")
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
}
|
||||||
Button("Edit") {
|
Button("Edit") {
|
||||||
self.editingJob = job
|
self.editingJob = job
|
||||||
self.editorError = nil
|
self.editorError = nil
|
||||||
@@ -348,6 +359,13 @@ struct CronSettings: View {
|
|||||||
.foregroundStyle(.secondary)
|
.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 {
|
if let error = entry.error, !error.isEmpty {
|
||||||
Text(error)
|
Text(error)
|
||||||
.font(.caption)
|
.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") {
|
Section("Main session summary") {
|
||||||
Text("Isolated jobs always post a summary back into the main session when they finish.")
|
Text("Isolated jobs always post a summary back into the main session when they finish.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
@@ -603,9 +621,16 @@ private struct CronJobEditor: View {
|
|||||||
}
|
}
|
||||||
.padding(18)
|
.padding(18)
|
||||||
.onAppear { self.hydrateFromJob() }
|
.onAppear { self.hydrateFromJob() }
|
||||||
|
.onChange(of: self.payloadKind) { _, newValue in
|
||||||
|
if newValue == .agentTurn, self.sessionTarget == .main {
|
||||||
|
self.sessionTarget = .isolated
|
||||||
|
}
|
||||||
|
}
|
||||||
.onChange(of: self.sessionTarget) { _, newValue in
|
.onChange(of: self.sessionTarget) { _, newValue in
|
||||||
if newValue == .isolated {
|
if newValue == .isolated {
|
||||||
self.payloadKind = .agentTurn
|
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["kind"] as? String == "systemEvent" {
|
||||||
if (payload["text"] as? String ?? "").isEmpty {
|
if (payload["text"] as? String ?? "").isEmpty {
|
||||||
throw NSError(
|
throw NSError(
|
||||||
@@ -744,7 +785,7 @@ private struct CronJobEditor: View {
|
|||||||
]
|
]
|
||||||
if !name.isEmpty { root["name"] = name }
|
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)
|
let trimmed = self.postPrefix.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
root["isolation"] = [
|
root["isolation"] = [
|
||||||
"postToMainPrefix": trimmed.isEmpty ? "Cron" : trimmed,
|
"postToMainPrefix": trimmed.isEmpty ? "Cron" : trimmed,
|
||||||
@@ -831,7 +872,7 @@ struct CronSettings_Previews: PreviewProvider {
|
|||||||
channel: "last",
|
channel: "last",
|
||||||
to: nil,
|
to: nil,
|
||||||
bestEffortDeliver: true),
|
bestEffortDeliver: true),
|
||||||
isolation: CronIsolation(postToMain: nil, postToMainPrefix: "Cron"),
|
isolation: CronIsolation(postToMainPrefix: "Cron"),
|
||||||
state: CronJobState(
|
state: CronJobState(
|
||||||
nextRunAtMs: Int(Date().addingTimeInterval(3600).timeIntervalSince1970 * 1000),
|
nextRunAtMs: Int(Date().addingTimeInterval(3600).timeIntervalSince1970 * 1000),
|
||||||
runningAtMs: nil,
|
runningAtMs: nil,
|
||||||
@@ -848,6 +889,7 @@ struct CronSettings_Previews: PreviewProvider {
|
|||||||
action: "finished",
|
action: "finished",
|
||||||
status: "ok",
|
status: "ok",
|
||||||
error: nil,
|
error: nil,
|
||||||
|
summary: "All good.",
|
||||||
runAtMs: nil,
|
runAtMs: nil,
|
||||||
durationMs: 1234,
|
durationMs: 1234,
|
||||||
nextRunAtMs: nil),
|
nextRunAtMs: nil),
|
||||||
|
|||||||
Reference in New Issue
Block a user