From c4d9ca19ce8aa8c647000f277c2ffb370e250b1b Mon Sep 17 00:00:00 2001 From: pavel Date: Tue, 10 Feb 2026 20:57:03 +0100 Subject: [PATCH] fixxxeeess --- frontend/src/main.js | 64 ++++++++++++++++++++++++++++++++++++------ frontend/src/style.css | 5 ++-- src/agent.rs | 14 +++++---- src/api.rs | 4 ++- src/auth.rs | 29 +++++++++++++++++-- src/scheduler.rs | 2 +- src/server.rs | 59 +++++++++++++++++++++++++++----------- src/tools.rs | 19 +------------ 8 files changed, 142 insertions(+), 54 deletions(-) diff --git a/frontend/src/main.js b/frontend/src/main.js index 5535086..e5d1e55 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -16,7 +16,8 @@ const state = { selectedRunId: null, currentView: 'dashboard', // 'dashboard' or 'task' isEditing: false, - token: localStorage.getItem('auth_token') + token: localStorage.getItem('auth_token'), + refreshToken: localStorage.getItem('refresh_token') }; // DOM elements @@ -67,7 +68,24 @@ async function fetchWithAuth(url, options = {}) { 'Authorization': `Bearer ${state.token}` }; - const response = await fetch(url, { ...options, headers }); + let response = await fetch(url, { ...options, headers }); + + if (response.status === 401 && state.refreshToken) { + // Try to refresh token + try { + const success = await attemptTokenRefresh(); + if (success) { + // Retry original request with new token + const newHeaders = { + ...options.headers, + 'Authorization': `Bearer ${state.token}` + }; + response = await fetch(url, { ...options, headers: newHeaders }); + } + } catch (error) { + console.error('Token refresh failed:', error); + } + } if (response.status === 401) { logout(); @@ -77,6 +95,32 @@ async function fetchWithAuth(url, options = {}) { return response; } +async function attemptTokenRefresh() { + try { + const response = await fetch(`${API_URL}/auth/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refresh_token: state.refreshToken }) + }); + + if (response.ok) { + const data = await response.json(); + if (data.access_token) { + state.token = data.access_token; + localStorage.setItem('auth_token', data.access_token); + if (data.refresh_token) { + state.refreshToken = data.refresh_token; + localStorage.setItem('refresh_token', data.refresh_token); + } + return true; + } + } + } catch (error) { + console.error('Error during token refresh:', error); + } + return false; +} + async function fetchTasks() { try { const response = await fetchWithAuth(`${API_URL}/tasks`); @@ -134,10 +178,10 @@ async function fetchRecentRuns() { function renderTaskList() { const sortedTasks = [...state.tasks].sort((a, b) => { const aDate = a.runs && a.runs.length > 0 - ? new Date(a.runs[a.runs.length - 1].created_at) + ? new Date(a.runs[0].created_at) : new Date(a.created_at); const bDate = b.runs && b.runs.length > 0 - ? new Date(b.runs[b.runs.length - 1].created_at) + ? new Date(b.runs[0].created_at) : new Date(b.created_at); return bDate - aDate; }); @@ -145,7 +189,7 @@ function renderTaskList() { taskListEl.innerHTML = sortedTasks .map((task) => { const latestRun = task.runs && task.runs.length > 0 - ? task.runs[task.runs.length - 1] + ? task.runs[0] : null; const status = latestRun ? latestRun.status : 'pending'; const date = latestRun ? new Date(latestRun.created_at) : new Date(task.created_at); @@ -192,8 +236,6 @@ function selectTask(id, runId = null) { function renderRunHistory(task) { runListEl.innerHTML = task.runs - .slice() - .reverse() .map( (run, index) => `
  • @@ -250,7 +292,7 @@ function showTaskView(task) { dashboardViewEl.classList.add('hidden'); taskViewEl.classList.remove('hidden'); - const run = task.runs.find(r => r.id === state.selectedRunId) || task.runs[task.runs.length - 1]; + const run = task.runs.find(r => r.id === state.selectedRunId) || task.runs[0]; viewGoalEl.textContent = task.goal; @@ -473,7 +515,9 @@ async function showLogin() { async function logout() { state.token = null; + state.refreshToken = null; localStorage.removeItem('auth_token'); + localStorage.removeItem('refresh_token'); showLogin(); } @@ -493,6 +537,10 @@ async function handleCallback() { if (data.access_token) { state.token = data.access_token; localStorage.setItem('auth_token', data.access_token); + if (data.refresh_token) { + state.refreshToken = data.refresh_token; + localStorage.setItem('refresh_token', data.refresh_token); + } callbackOverlay.classList.add('hidden'); appEl.classList.remove('hidden'); initializeApp(); diff --git a/frontend/src/style.css b/frontend/src/style.css index a6e72c5..742bc21 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -87,7 +87,7 @@ body { .dashboard-content { flex: 1; - overflow: hidden; + overflow-y: auto; border-radius: 12px; display: flex; flex-direction: column; @@ -246,7 +246,7 @@ body { display: flex; gap: 24px; flex: 1; - overflow: hidden; + min-height: 0; } .run-history { @@ -272,6 +272,7 @@ body { #run-list { list-style: none; + flex: 1; overflow-y: auto; padding: 8px; } diff --git a/src/agent.rs b/src/agent.rs index 1865a22..2bbc042 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -19,13 +19,13 @@ impl Agent { zen_api_key: Option, tavily_api_key: Option, initial_message: String, - ) -> Self { + ) -> Result> { let intro = format!( "You are an autonomous agent. You have access to tools that can help you achieve your goals. Use them wisely. The user is unable to respond to you so do not ask for clarification and use the answer tool once you to give your final answer. current date is {}", - Utc::now().to_rfc3339() + Utc::now().format("%B %d, %Y %H:%M:%S UTC").to_string() ); println!("initial_message: {}", intro); let messages = vec![ @@ -45,8 +45,12 @@ impl Agent { let tools = Some(tools::get_tools()); - Self { - client: reqwest::Client::new(), + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(60)) + .build()?; + + Ok(Self { + client, url: "https://opencode.ai/zen/v1/chat/completions".to_string(), zen_api_key, tavily_api_key, @@ -54,7 +58,7 @@ impl Agent { tools, logs: String::new(), answer: None, - } + }) } fn log(&mut self, message: &str) { diff --git a/src/api.rs b/src/api.rs index 0f4cf6b..2d2ae37 100644 --- a/src/api.rs +++ b/src/api.rs @@ -72,7 +72,9 @@ pub async fn perform_search( query: &str, api_key: &str, ) -> Result> { - let client = reqwest::Client::new(); + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build()?; let response = client .post("https://api.tavily.com/search") .json(&serde_json::json!({ diff --git a/src/auth.rs b/src/auth.rs index a294d4d..0cf0f67 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -15,11 +15,13 @@ pub struct Claims { #[derive(Debug, Deserialize)] struct Jwk { - kty: String, + #[serde(rename = "kty")] + _kty: String, kid: String, n: String, e: String, - alg: Option, + #[serde(rename = "alg")] + _alg: Option, } #[derive(Debug, Deserialize)] @@ -146,4 +148,27 @@ impl Authenticator { Ok(res) } + + pub async fn refresh_token( + &self, + refresh_token: String, + ) -> Result> { + let params = [ + ("grant_type", "refresh_token"), + ("refresh_token", &refresh_token), + ("client_id", &self.client_id), + ("client_secret", &self.client_secret), + ]; + + let res = self + .client + .post(&self.token_url) + .form(¶ms) + .send() + .await? + .json() + .await?; + + Ok(res) + } } diff --git a/src/scheduler.rs b/src/scheduler.rs index 0c2c493..d20f257 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -95,7 +95,7 @@ impl Scheduler { run.insert(&db).await?; // Start agent in background - let mut agent = Agent::new(zen_key, tavily_key, task.goal.clone()); + let mut agent = Agent::new(zen_key, tavily_key, task.goal.clone())?; tokio::spawn(async move { let (logs, answer) = match agent.run().await { diff --git a/src/server.rs b/src/server.rs index baac0da..be7ca3c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -127,6 +127,7 @@ pub async fn start(db_url: &str) -> Result<(), Box> { .route("/tasks/:id/runs", post(rerun_task)) .route("/runs/recent", get(get_recent_runs)) .route("/auth/callback", get(auth_callback)) + .route("/auth/refresh", post(auth_refresh)) .layer(cors) .with_state(state); @@ -150,21 +151,24 @@ async fn list_tasks( let response = tasks .into_iter() - .map(|(t, runs)| TaskResponse { - id: t.id, - goal: t.goal, - cron: t.cron, - created_at: t.created_at, - runs: runs - .into_iter() - .map(|r| TaskRunResponse { - id: r.id, - status: r.status, - logs: r.logs, - answer: r.answer, - created_at: r.created_at, - }) - .collect(), + .map(|(t, mut runs)| { + runs.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + TaskResponse { + id: t.id, + goal: t.goal, + cron: t.cron, + created_at: t.created_at, + runs: runs + .into_iter() + .map(|r| TaskRunResponse { + id: r.id, + status: r.status, + logs: r.logs, + answer: r.answer, + created_at: r.created_at, + }) + .collect(), + } }) .collect(); @@ -240,7 +244,8 @@ async fn execute_agent_run( state.zen_api_key.clone(), state.tavily_api_key.clone(), goal.clone(), - ); + ) + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; let (logs, answer) = match agent.run().await { Ok((logs, answer)) => (logs, answer), @@ -298,6 +303,7 @@ async fn update_task( get_task_inner(id, &state).await.map(Json) } +#[allow(dead_code)] pub struct AuthenticatedUser(pub crate::auth::Claims); #[axum::async_trait] @@ -342,6 +348,23 @@ pub struct AuthCallbackQuery { pub redirect_uri: String, } +#[derive(Deserialize)] +struct RefreshRequest { + refresh_token: String, +} + +async fn auth_refresh( + State(state): State>, + Json(payload): Json, +) -> Result, (StatusCode, String)> { + state + .authenticator + .refresh_token(payload.refresh_token) + .await + .map(Json) + .map_err(|e| (StatusCode::UNAUTHORIZED, e.to_string())) +} + async fn auth_callback( State(state): State>, Query(query): Query, @@ -374,11 +397,13 @@ async fn get_task_inner(id: Uuid, state: &AppState) -> Result, ) -> Result<(Message, bool, Option), Box> { - let mut file_written = false; let mut answer = None; let name = &tool_call.function.name; @@ -66,30 +65,14 @@ pub async fn handle_tool_call( "Error: TAVILY_API_KEY is not set. Cannot perform real search.".to_string() }; (search_result, false) - } else if name == "write_file" { - let args: HashMap = serde_json::from_str(&tool_call.function.arguments)?; - let path = args.get("path").ok_or("Missing path argument")?; - let content = args.get("content").ok_or("Missing content argument")?; - - println!("--- Executing tool: write_file(path: \"{}\") ---", path); - - let write_result = match std::fs::write(path, content) { - Ok(_) => { - file_written = true; - format!("Successfully wrote content to {}", path) - } - Err(e) => format!("Error writing to {}: {}", path, e), - }; - (write_result, file_written) } else if name == "finish" { let args: HashMap = serde_json::from_str(&tool_call.function.arguments)?; let result = args.get("result").ok_or("Missing result argument")?; println!("--- Finishing task: {}", result); - file_written = true; answer = Some(result.clone()); - (result.clone(), file_written) + (result.clone(), true) } else { (format!("Error: Unknown tool {}", name), false) };