Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Utilities
uuid = { version = "1.0", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde", "clock"] }
chrono-tz = "0.10.4"
cron = "0.15.0"
regex = "1"
base64 = "0.22"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "gif", "webp", "bmp"] }
Expand Down
1 change: 1 addition & 0 deletions src/apps/cli/src/agent/core_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ impl Agent for CoreAgentAdapter {
tool_id,
tool_name,
result,
result_for_assistant: _,
duration_ms,
} => {
let result_str = serde_json::to_string(&result)
Expand Down
93 changes: 93 additions & 0 deletions src/apps/desktop/src/api/cron_api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//! Scheduled jobs API.

use bitfun_core::service::cron::{
get_global_cron_service, CreateCronJobRequest, CronJob, UpdateCronJobRequest,
};
use log::{debug, error};
use serde::Deserialize;

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListCronJobsRequest {
pub workspace_path: Option<String>,
pub session_id: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateCronJobCommandRequest {
pub job_id: String,
#[serde(flatten)]
pub changes: UpdateCronJobRequest,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteCronJobRequest {
pub job_id: String,
}

fn cron_service() -> Result<std::sync::Arc<bitfun_core::service::cron::CronService>, String> {
get_global_cron_service().ok_or_else(|| "Cron service is not initialized".to_string())
}

#[tauri::command]
pub async fn list_cron_jobs(request: ListCronJobsRequest) -> Result<Vec<CronJob>, String> {
debug!(
"Listing scheduled jobs: workspace_path={:?}, session_id={:?}",
request.workspace_path, request.session_id
);

let service = cron_service()?;
Ok(service
.list_jobs_filtered(
request.workspace_path.as_deref(),
request.session_id.as_deref(),
)
.await)
}

#[tauri::command]
pub async fn create_cron_job(request: CreateCronJobRequest) -> Result<CronJob, String> {
debug!(
"Creating scheduled job: name={}, session_id={}, workspace_path={}",
request.name, request.session_id, request.workspace_path
);

let service = cron_service()?;
service.create_job(request).await.map_err(|error| {
error!("Failed to create scheduled job: {}", error);
format!("Failed to create scheduled job: {}", error)
})
}

#[tauri::command]
pub async fn update_cron_job(request: UpdateCronJobCommandRequest) -> Result<CronJob, String> {
debug!("Updating scheduled job: job_id={}", request.job_id);

let service = cron_service()?;
service
.update_job(&request.job_id, request.changes)
.await
.map_err(|error| {
error!(
"Failed to update scheduled job {}: {}",
request.job_id, error
);
format!("Failed to update scheduled job: {}", error)
})
}

#[tauri::command]
pub async fn delete_cron_job(request: DeleteCronJobRequest) -> Result<bool, String> {
debug!("Deleting scheduled job: job_id={}", request.job_id);

let service = cron_service()?;
service.delete_job(&request.job_id).await.map_err(|error| {
error!(
"Failed to delete scheduled job {}: {}",
request.job_id, error
);
format!("Failed to delete scheduled job: {}", error)
})
}
3 changes: 2 additions & 1 deletion src/apps/desktop/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

pub mod agentic_api;
pub mod ai_memory_api;
pub mod browser_api;
pub mod ai_rules_api;
pub mod app_state;
pub mod browser_api;
pub mod btw_api;
pub mod clipboard_file_api;
pub mod commands;
pub mod config_api;
pub mod context_upload_api;
pub mod cron_api;
pub mod diff_api;
pub mod dto;
pub mod git_agent_api;
Expand Down
46 changes: 45 additions & 1 deletion src/apps/desktop/src/api/session_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

use bitfun_core::agentic::persistence::PersistenceManager;
use bitfun_core::infrastructure::PathManager;
use bitfun_core::service::session::{DialogTurnData, SessionMetadata};
use bitfun_core::service::session::{
DialogTurnData, SessionMetadata, SessionTranscriptExport, SessionTranscriptExportOptions,
};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::Arc;
Expand Down Expand Up @@ -33,6 +35,24 @@ pub struct SaveSessionMetadataRequest {
pub workspace_path: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportSessionTranscriptRequest {
pub session_id: String,
pub workspace_path: String,
#[serde(default = "default_tools")]
pub tools: bool,
#[serde(default)]
pub tool_inputs: bool,
#[serde(default)]
pub thinking: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turns: Option<Vec<String>>,
}

fn default_tools() -> bool {
false
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeletePersistedSessionRequest {
pub session_id: String,
Expand Down Expand Up @@ -118,6 +138,30 @@ pub async fn save_session_metadata(
.map_err(|e| format!("Failed to save session metadata: {}", e))
}

#[tauri::command]
pub async fn export_session_transcript(
request: ExportSessionTranscriptRequest,
path_manager: State<'_, Arc<PathManager>>,
) -> Result<SessionTranscriptExport, String> {
let workspace_path = PathBuf::from(&request.workspace_path);
let manager = PersistenceManager::new(path_manager.inner().clone())
.map_err(|e| format!("Failed to create persistence manager: {}", e))?;

manager
.export_session_transcript(
&workspace_path,
&request.session_id,
&SessionTranscriptExportOptions {
tools: request.tools,
tool_inputs: request.tool_inputs,
thinking: request.thinking,
turns: request.turns,
},
)
.await
.map_err(|e| format!("Failed to export session transcript: {}", e))
}

#[tauri::command]
pub async fn delete_persisted_session(
request: DeletePersistedSessionRequest,
Expand Down
18 changes: 18 additions & 0 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use api::ai_rules_api::*;
use api::clipboard_file_api::*;
use api::commands::*;
use api::config_api::*;
use api::cron_api::*;
use api::diff_api::*;
use api::git_agent_api::*;
use api::git_api::*;
Expand Down Expand Up @@ -468,6 +469,7 @@ pub async fn run() {
load_session_turns,
save_session_turn,
save_session_metadata,
export_session_transcript,
delete_persisted_session,
touch_session_activity,
load_persisted_session_metadata,
Expand Down Expand Up @@ -562,6 +564,10 @@ pub async fn run() {
reorder_opened_workspaces,
get_current_workspace,
scan_workspace_info,
list_cron_jobs,
create_cron_job,
update_cron_job,
delete_cron_job,
api::prompt_template_api::get_prompt_template_config,
api::prompt_template_api::save_prompt_template_config,
api::prompt_template_api::export_prompt_templates,
Expand Down Expand Up @@ -742,6 +748,18 @@ async fn init_agentic_system() -> anyhow::Result<(
coordinator.set_scheduler_notifier(scheduler.outcome_sender());
coordination::set_global_scheduler(scheduler.clone());

let cron_service =
bitfun_core::service::cron::CronService::new(path_manager.clone(), scheduler.clone())
.await
.map_err(|e| anyhow::anyhow!("Failed to initialize cron service: {}", e))?;
bitfun_core::service::cron::set_global_cron_service(cron_service.clone());
let cron_subscriber = Arc::new(bitfun_core::service::cron::CronEventSubscriber::new(
cron_service.clone(),
));
event_router.subscribe_internal("cron_jobs".to_string(), cron_subscriber);
cron_service.start();

log::info!("Cron service initialized and subscriber registered");
log::info!("Agentic system initialized");
Ok((
coordinator,
Expand Down
2 changes: 2 additions & 0 deletions src/crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ log = { workspace = true }

uuid = { workspace = true }
chrono = { workspace = true }
chrono-tz = { workspace = true }
cron = { workspace = true }
regex = { workspace = true }
base64 = { workspace = true }
image = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions src/crates/core/src/agentic/agents/claw_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ impl ClawMode {
"TerminalControl".to_string(),
"SessionControl".to_string(),
"SessionMessage".to_string(),
"SessionHistory".to_string(),
"Cron".to_string(),
],
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/crates/core/src/agentic/agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@ pub trait Agent: Send + Sync + 'static {
async fn build_prompt(&self, context: &PromptBuilderContext) -> BitFunResult<String> {
let prompt_components = PromptBuilder::new(context.clone());
let template_name = self.prompt_template_name(context.model_name.as_deref());
let system_prompt_template = get_embedded_prompt(template_name)
.ok_or_else(|| BitFunError::Agent(format!("{} not found in embedded files", template_name)))?;
let system_prompt_template = get_embedded_prompt(template_name).ok_or_else(|| {
BitFunError::Agent(format!("{} not found in embedded files", template_name))
})?;

let prompt = prompt_components
.build_prompt_from_template(system_prompt_template)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,12 +349,7 @@ Do not read from, modify, create, move, or delete files outside this workspace u
// Replace {AGENT_MEMORY}
if result.contains(PLACEHOLDER_AGENT_MEMORY) {
let workspace = Path::new(&self.context.workspace_path);
let agent_memory = match build_workspace_agent_memory_prompt(
workspace,
self.context.session_id.as_deref(),
)
.await
{
let agent_memory = match build_workspace_agent_memory_prompt(workspace).await {
Ok(prompt) => prompt,
Err(e) => {
warn!(
Expand Down
1 change: 1 addition & 0 deletions src/crates/core/src/agentic/coordination/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub enum DialogTriggerSource {
DesktopUi,
DesktopApi,
AgentSession,
ScheduledJob,
RemoteRelay,
Bot,
Cli,
Expand Down
1 change: 1 addition & 0 deletions src/crates/core/src/agentic/coordination/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ impl DialogSubmissionPolicy {
pub const fn for_source(trigger_source: DialogTriggerSource) -> Self {
let (queue_priority, skip_tool_confirmation) = match trigger_source {
DialogTriggerSource::AgentSession => (DialogQueuePriority::Low, true),
DialogTriggerSource::ScheduledJob => (DialogQueuePriority::Low, true),
DialogTriggerSource::DesktopUi
| DialogTriggerSource::DesktopApi
| DialogTriggerSource::Cli => (DialogQueuePriority::Normal, false),
Expand Down
15 changes: 0 additions & 15 deletions src/crates/core/src/agentic/execution/execution_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1137,19 +1137,4 @@ mod tests {
"model-primary"
);
}

#[test]
fn invalid_locked_auto_model_is_ignored() {
let mut ai_config = AIConfig::default();
ai_config.models = vec![build_model("model-primary", "Primary", "claude-sonnet-4.5")];
ai_config.default_models.primary = Some("model-primary".to_string());

assert_eq!(
ExecutionEngine::resolve_locked_auto_model_id(
&ai_config,
Some(&"deleted-fast-model".to_string())
),
None
);
}
}
Loading
Loading