mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +00:00
fix: Run Now agent-run timing + non-404 webview placeholder
Two independent fixes that both blocked the "install → Run Now → see the Site tab render" loop. 1. CronViewModel.runNow stopped blocking on `cron tick`. Previously the UI waited up to 60 s on the tick before deciding whether the job succeeded, so any agent run that did real work (an LLM call + a few HTTP GETs + a file write = easily 90 s+) surfaced a false "Run failed" toast while the job kept running in the background. Dashboard updates landed minutes later, confusing the user. New shape: show "Agent started — dashboard will update when it finishes" the instant `cron run` queues the job, then call `cron tick` with a 300 s timeout to force execution. Tick failures are logged but don't overwrite the started-toast — HermesFileWatcher picks up the dashboard.json rewrite automatically when the agent finishes. 2. site-status-checker's webview widget pointed at `github.com/awizemann/scarf/tree/main/templates/awizemann/...`. The templates/ path only exists on project-sharing, not main, so GitHub returned 404 in the Site tab until the first cron run replaced the URL with the user's configured site. Switched the placeholder to `awizemann.github.io/scarf/` which always renders. Bundle + catalog rebuilt against the updated dashboard.json. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,23 +73,46 @@ final class CronViewModel {
|
|||||||
// with `hermes cron tick` which runs all due jobs once and
|
// with `hermes cron tick` which runs all due jobs once and
|
||||||
// exits. Redundant-but-harmless when the gateway is running;
|
// exits. Redundant-but-harmless when the gateway is running;
|
||||||
// the actual trigger when it isn't.
|
// the actual trigger when it isn't.
|
||||||
|
//
|
||||||
|
// Feedback model: show a "Agent started" toast as soon as
|
||||||
|
// `cron run` succeeds, WITHOUT waiting for `cron tick` to
|
||||||
|
// return. Agent jobs routinely run past a minute (network IO +
|
||||||
|
// an LLM call + a file rewrite), and earlier versions with a
|
||||||
|
// 60s tick timeout surfaced a misleading "Run failed" toast
|
||||||
|
// every time while the job kept running in the background.
|
||||||
|
// The app's HermesFileWatcher picks up the dashboard.json
|
||||||
|
// rewrite that the agent lands at the end — that's what the
|
||||||
|
// user actually watches for, not this toast.
|
||||||
let svc = fileService
|
let svc = fileService
|
||||||
let jobID = job.id
|
let jobID = job.id
|
||||||
Task.detached { [weak self] in
|
Task.detached { [weak self] in
|
||||||
let runResult = svc.runHermesCLI(args: ["cron", "run", jobID], timeout: 30)
|
let runResult = svc.runHermesCLI(args: ["cron", "run", jobID], timeout: 30)
|
||||||
// Give `cron run` a moment to register the queue entry
|
|
||||||
// before forcing the tick. A few hundred ms is enough;
|
|
||||||
// longer only delays the user-visible feedback.
|
|
||||||
try? await Task.sleep(for: .milliseconds(250))
|
|
||||||
let tickResult = svc.runHermesCLI(args: ["cron", "tick"], timeout: 60)
|
|
||||||
await MainActor.run { [weak self] in
|
await MainActor.run { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
if runResult.exitCode == 0 && tickResult.exitCode == 0 {
|
if runResult.exitCode != 0 {
|
||||||
self.message = "Job executed (see Output panel for details)"
|
self.message = "Run failed to queue: \(runResult.output.prefix(200))"
|
||||||
} else {
|
self.logger.warning("cron run failed: \(runResult.output)")
|
||||||
let errOutput = runResult.exitCode != 0 ? runResult.output : tickResult.output
|
self.load()
|
||||||
self.message = "Run failed: \(errOutput.prefix(200))"
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
||||||
self.logger.warning("cron runNow failed: run=\(runResult.exitCode), tick=\(tickResult.exitCode) output=\(errOutput)")
|
self?.message = nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.message = "Agent started — dashboard will update when it finishes"
|
||||||
|
self.load()
|
||||||
|
}
|
||||||
|
// `cron run` is queued; now force the tick. The 300s
|
||||||
|
// timeout catches truly stuck processes without killing
|
||||||
|
// the long-but-valid agent case that blew up the 60s
|
||||||
|
// version. A timeout here is survivable — the Hermes
|
||||||
|
// scheduler re-runs due jobs on its own cadence — so we
|
||||||
|
// log but don't surface it as a failure toast.
|
||||||
|
try? await Task.sleep(for: .milliseconds(250))
|
||||||
|
let tickResult = svc.runHermesCLI(args: ["cron", "tick"], timeout: 300)
|
||||||
|
await MainActor.run { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
if tickResult.exitCode != 0 {
|
||||||
|
self.logger.warning("cron tick exited non-zero (job may still complete via scheduler): \(tickResult.output)")
|
||||||
}
|
}
|
||||||
self.load()
|
self.load()
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
||||||
|
|||||||
@@ -889,6 +889,10 @@
|
|||||||
},
|
},
|
||||||
"••••••••••" : {
|
"••••••••••" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"+ %lld more…" : {
|
||||||
|
"comment" : "A button that shows the number of files that were left behind by the template uninstaller.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"<%@>" : {
|
"<%@>" : {
|
||||||
|
|
||||||
@@ -6664,6 +6668,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Delete %@ from Finder if you don't need these files anymore." : {
|
||||||
|
"comment" : "A note that lets the user delete",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Delete %@?" : {
|
"Delete %@?" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -15447,6 +15455,9 @@
|
|||||||
},
|
},
|
||||||
"Project directory will also be removed (nothing user-owned left inside)." : {
|
"Project directory will also be removed (nothing user-owned left inside)." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Project folder kept" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Project Name" : {
|
"Project Name" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -16511,6 +16522,10 @@
|
|||||||
"comment" : "A label that instructs the user to remove a project from Scarf's list of installed projects.",
|
"comment" : "A label that instructs the user to remove a project from Scarf's list of installed projects.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Remove %@ from Scarf's project list (files are kept on disk)" : {
|
||||||
|
"comment" : "A confirmation dialog that",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Remove %@?" : {
|
"Remove %@?" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -16591,8 +16606,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Remove from Scarf" : {
|
"Remove from List" : {
|
||||||
"comment" : "A context menu option to remove a project from Scarf.",
|
"comment" : "A confirmation dialog that asks whether a user is sure they want to remove a project from Scarf's list.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Remove from List (keep files)…" : {
|
||||||
|
"comment" : "A button that removes a project from Scarf's list, but not from disk.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Remove from Scarf's project list?" : {
|
||||||
|
"comment" : "Title of a dialog that asks the user to confirm removing a project from Scarf's project list.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Remove the entire namespace dir recursively" : {
|
"Remove the entire namespace dir recursively" : {
|
||||||
@@ -21572,6 +21595,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"These files weren't installed by the template (the agent or you created them after install), so Scarf left them in place along with the folder itself." : {
|
||||||
|
"comment" : "A description of the files Scarf left in place when uninstalling a template.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"These list fields must be edited directly in config.yaml." : {
|
"These list fields must be edited directly in config.yaml." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -22602,8 +22629,8 @@
|
|||||||
"comment" : "A button that uninstalls a template.",
|
"comment" : "A button that uninstalls a template.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Uninstall Template…" : {
|
"Uninstall Template (remove installed files)…" : {
|
||||||
"comment" : "A contextual menu item that uninstalls a template.",
|
"comment" : "A button that removes a project's files from the system.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Unknown: %@" : {
|
"Unknown: %@" : {
|
||||||
|
|||||||
Binary file not shown.
@@ -54,7 +54,7 @@
|
|||||||
{
|
{
|
||||||
"type": "webview",
|
"type": "webview",
|
||||||
"title": "First Watched Site",
|
"title": "First Watched Site",
|
||||||
"url": "https://github.com/awizemann/scarf/tree/main/templates/awizemann/site-status-checker",
|
"url": "https://awizemann.github.io/scarf/",
|
||||||
"height": 420
|
"height": 420
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
"name": "Alan Wizemann",
|
"name": "Alan Wizemann",
|
||||||
"url": "https://github.com/awizemann/scarf"
|
"url": "https://github.com/awizemann/scarf"
|
||||||
},
|
},
|
||||||
"bundleSha256": "2a4e0aba5bd4d86be3153d87c6ce219b9068223daebfad6f9db2b82c3752fac5",
|
"bundleSha256": "0a20802a8830a7cfdd1afa2888e42e113c9a17a37439384a3037d32ad1f24c1f",
|
||||||
"bundleSize": 7583,
|
"bundleSize": 7569,
|
||||||
"category": "monitoring",
|
"category": "monitoring",
|
||||||
"config": {
|
"config": {
|
||||||
"modelRecommendation": {
|
"modelRecommendation": {
|
||||||
|
|||||||
Reference in New Issue
Block a user