diff --git a/desktop/src-tauri/src/classroom_commands/generate.rs b/desktop/src-tauri/src/classroom_commands/generate.rs index 11c7ea7..2576ef3 100644 --- a/desktop/src-tauri/src/classroom_commands/generate.rs +++ b/desktop/src-tauri/src/classroom_commands/generate.rs @@ -137,10 +137,12 @@ pub async fn classroom_generate( } }; - // Helper: check if cancelled + // Helper: check if cancelled (try_lock avoids blocking the async runtime) let is_cancelled = || { - let t = tasks.blocking_lock(); - !t.contains_key(&topic_clone) + match tasks.try_lock() { + Ok(t) => !t.contains_key(&topic_clone), + Err(_) => false, // Lock contested — treat as not cancelled + } }; // Helper: emit progress event diff --git a/desktop/src-tauri/src/kernel_commands/chat.rs b/desktop/src-tauri/src/kernel_commands/chat.rs index bb283ab..45584c8 100644 --- a/desktop/src-tauri/src/kernel_commands/chat.rs +++ b/desktop/src-tauri/src/kernel_commands/chat.rs @@ -163,7 +163,11 @@ pub async fn agent_chat_stream( None, ); engines.insert(request.agent_id.clone(), engine); - tracing::info!("[agent_chat_stream] Auto-initialized heartbeat for agent: {}", request.agent_id); + // Start the engine after insertion via the stored reference + if let Some(e) = engines.get(&request.agent_id) { + e.start().await; + } + tracing::info!("[agent_chat_stream] Auto-initialized and started heartbeat for agent: {}", request.agent_id); } } diff --git a/desktop/src/lib/saas-auth.ts b/desktop/src/lib/saas-auth.ts index 5e926e5..d799433 100644 --- a/desktop/src/lib/saas-auth.ts +++ b/desktop/src/lib/saas-auth.ts @@ -20,15 +20,15 @@ export function installAuthMethods(ClientClass: { prototype: any }): void { * Login with username and password. * Auto-sets the client token on success. */ - proto.login = async function (this: { token: string | null; request(method: string, path: string, body?: unknown): Promise }, username: string, password: string, totpCode?: string): Promise { + proto.login = async function (this: { token: string | null; refreshTokenValue: string | null; request(method: string, path: string, body?: unknown): Promise }, username: string, password: string, totpCode?: string): Promise { const body: Record = { username, password }; - if (totpCode) body.totp_code = totpCode; - // Clear stale token before login — avoid sending expired token on auth endpoint + if (totpCode) body.totp_code = totpCode; // Clear stale token before login — avoid sending expired token on auth endpoint this.token = null; const data = await this.request( 'POST', '/api/v1/auth/login', body, ); this.token = data.token; + this.refreshTokenValue = data.refresh_token; return data; }; @@ -36,7 +36,7 @@ export function installAuthMethods(ClientClass: { prototype: any }): void { * Register a new account. * Auto-sets the client token on success. */ - proto.register = async function (this: { token: string | null; request(method: string, path: string, body?: unknown): Promise }, data: { + proto.register = async function (this: { token: string | null; refreshTokenValue: string | null; request(method: string, path: string, body?: unknown): Promise }, data: { username: string; email: string; password: string; @@ -48,6 +48,7 @@ export function installAuthMethods(ClientClass: { prototype: any }): void { 'POST', '/api/v1/auth/register', data, ); this.token = result.token; + this.refreshTokenValue = result.refresh_token; return result; }; @@ -62,9 +63,17 @@ export function installAuthMethods(ClientClass: { prototype: any }): void { * Refresh the current token. * Auto-updates the client token on success. */ - proto.refreshToken = async function (this: { token: string | null; request(method: string, path: string, body?: unknown): Promise }): Promise { - const data = await this.request('POST', '/api/v1/auth/refresh'); + proto.refreshToken = async function (this: { token: string | null; refreshTokenValue: string | null; request(method: string, path: string, body?: unknown): Promise }): Promise { + if (!this.refreshTokenValue) { + throw new Error('No refresh token available'); + } + const data = await this.request('POST', '/api/v1/auth/refresh', { + refresh_token: this.refreshTokenValue, + }); this.token = data.token; + if (data.refresh_token) { + this.refreshTokenValue = data.refresh_token; + } return data.token; }; diff --git a/desktop/src/lib/saas-client.ts b/desktop/src/lib/saas-client.ts index 8bb394d..cb86b8f 100644 --- a/desktop/src/lib/saas-client.ts +++ b/desktop/src/lib/saas-client.ts @@ -133,6 +133,7 @@ export type { export class SaaSClient { private baseUrl: string; private token: string | null = null; + private refreshTokenValue: string | null = null; /** * Refresh mutex: shared Promise to prevent concurrent token refresh. @@ -172,6 +173,16 @@ export class SaaSClient { this.token = token; } + /** Set or clear the refresh token (in-memory only, never persisted) */ + setRefreshToken(token: string | null): void { + this.refreshTokenValue = token; + } + + /** Get the current refresh token */ + getRefreshToken(): string | null { + return this.refreshTokenValue; + } + /** Check if the client is authenticated (token in memory or cookie-based) */ isAuthenticated(): boolean { return !!this.token || this._cookieAuth; diff --git a/desktop/src/lib/saas-types.ts b/desktop/src/lib/saas-types.ts index 1fc6d70..33b9eef 100644 --- a/desktop/src/lib/saas-types.ts +++ b/desktop/src/lib/saas-types.ts @@ -93,6 +93,7 @@ export interface SaaSLoginResponse { /** Refresh response from POST /api/v1/auth/refresh */ export interface SaaSRefreshResponse { token: string; + refresh_token: string; } /** TOTP setup response from POST /api/v1/auth/totp/setup */