Stats history with year filter, folding and XLSX/CSV export; snippets, read-only links, commit suggestions, weekly review, offline copies, server speech-to-text, planning model, job reports; fal.ai health fix
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c08c6c7436
commit
388ca96419
31 files changed
+1518
-82
No files matched your search
@@ -68,6 +68,13 @@ pub struct JobSettings {
|
||||
/// Chat in which the user works out the prompt with a model.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub assistant_session: Option<String>,
|
||||
/// Push notification with the report when a run ends.
|
||||
#[serde(default = "default_notify")]
|
||||
pub notify: bool,
|
||||
}
|
||||
|
||||
fn default_notify() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl JobSettings {
|
||||
@@ -81,6 +88,7 @@ impl JobSettings {
|
||||
timeout_minutes: default_timeout(),
|
||||
cwd: None,
|
||||
assistant_session: None,
|
||||
notify: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +129,7 @@ pub struct JobSpec {
|
||||
pub cwd: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub assistant_session: Option<String>,
|
||||
pub notify: bool,
|
||||
pub prompt: String,
|
||||
}
|
||||
|
||||
@@ -135,6 +144,7 @@ impl JobSpec {
|
||||
timeout_minutes: s.timeout_minutes,
|
||||
cwd: s.cwd.clone(),
|
||||
assistant_session: s.assistant_session.clone(),
|
||||
notify: s.notify,
|
||||
prompt,
|
||||
}
|
||||
}
|
||||
@@ -149,6 +159,7 @@ impl JobSpec {
|
||||
timeout_minutes: self.timeout_minutes,
|
||||
cwd: self.cwd.clone(),
|
||||
assistant_session: self.assistant_session.clone(),
|
||||
notify: self.notify,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,6 +229,7 @@ pub fn parse(path: &Path) -> Result<JobSpec> {
|
||||
timeout_minutes: meta.get("timeout_minutes").and_then(|v| unquote(v).parse().ok()).unwrap_or(60).clamp(1, 24 * 60),
|
||||
cwd: meta.get("cwd").map(unquote).filter(|c| !c.is_empty()),
|
||||
assistant_session: None,
|
||||
notify: meta.get("notify").map(|v| unquote(v) != "false").unwrap_or(true),
|
||||
prompt: body.trim().to_string(),
|
||||
})
|
||||
}
|
||||
@@ -342,6 +354,7 @@ pub async fn run_with(app: &Arc<App>, job: &Job, trigger: &str, payload: Option<
|
||||
let job_id = job.id.clone();
|
||||
let sid = session.id.clone();
|
||||
let timeout = Duration::from_secs(spec.timeout_minutes * 60);
|
||||
let (spec_name, spec_notify) = (spec.name.clone(), spec.notify);
|
||||
tokio::spawn(async move {
|
||||
let started = std::time::Instant::now();
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
@@ -349,15 +362,48 @@ pub async fn run_with(app: &Arc<App>, job: &Job, trigger: &str, payload: Option<
|
||||
if started.elapsed() > timeout {
|
||||
crate::runner::abort(&app2, &sid);
|
||||
let _ = app2.store.job_finished(&job_id, Some(&sid), "timeout");
|
||||
report(&app2, &sid, &spec_name, spec_notify, "timeout").await;
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
}
|
||||
let _ = app2.store.job_finished(&job_id, Some(&sid), "done");
|
||||
report(&app2, &sid, &spec_name, spec_notify, "done").await;
|
||||
});
|
||||
Ok(session.id)
|
||||
}
|
||||
|
||||
/// Push the report of a finished run (the agent's last message) to the owner.
|
||||
async fn report(app: &App, session_id: &str, name: &str, notify: bool, status: &str) {
|
||||
if !notify {
|
||||
return;
|
||||
}
|
||||
let Ok(session) = app.session(session_id) else { return };
|
||||
let Some(owner) = session.owner_id.clone() else { return };
|
||||
let prefs: Value = app.store.user_setting(&owner, "prefs").unwrap_or_default();
|
||||
let de = prefs["lang"].as_str().unwrap_or("").starts_with("de");
|
||||
let last = app
|
||||
.store
|
||||
.messages(session_id)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.find(|m| matches!(m.role, crate::types::Role::Assistant))
|
||||
.map(|m| m.parts.iter().filter_map(|p| if let Part::Text { text } = p { Some(text.as_str()) } else { None }).collect::<Vec<_>>().join("\n"))
|
||||
.unwrap_or_default();
|
||||
let flat: String = last.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
let body: String = if flat.chars().count() > 220 { format!("{}…", flat.chars().take(220).collect::<String>()) } else { flat };
|
||||
let title = match (status, de) {
|
||||
("timeout", true) => format!("{name} – Zeitlimit erreicht"),
|
||||
("timeout", false) => format!("{name} – timed out"),
|
||||
(_, true) => format!("{name} – fertig"),
|
||||
(_, false) => format!("{name} – finished"),
|
||||
};
|
||||
let payload = json!({"kind": "job", "title": title, "body": body, "tag": format!("job-{session_id}"), "url": format!("/#/s/{session_id}")});
|
||||
let (sent, _) = crate::push::send_to_user(app, &owner, &payload).await;
|
||||
tracing::info!(job = name, sent, "automation report");
|
||||
}
|
||||
|
||||
/// Background scheduler: checks every 30 s which jobs are due.
|
||||
pub fn spawn_scheduler(app: Arc<App>) {
|
||||
tokio::spawn(async move {
|
||||
|
||||
Reference in new issue
Block a user