diff --git a/src-tauri/src/acp/connection.rs b/src-tauri/src/acp/connection.rs index 146216d..10d61ba 100644 --- a/src-tauri/src/acp/connection.rs +++ b/src-tauri/src/acp/connection.rs @@ -844,6 +844,12 @@ async fn run_connection( // Only emit a visible error for unexpected failures; // "Method not found" is expected for agents that don't // support session resume (e.g. Cline). + // "Authentication required" is expected for agents whose + // credentials have expired (e.g. Gemini CLI) — skip + // session/new too since it will also fail. + if err_str.contains("Authentication required") { + return Ok(()); + } if !err_str.contains("Method not found") { crate::web::event_bridge::emit_event( &emitter_clone, diff --git a/src-tauri/src/commands/conversations.rs b/src-tauri/src/commands/conversations.rs index 0aee1c3..b82ce93 100644 --- a/src-tauri/src/commands/conversations.rs +++ b/src-tauri/src/commands/conversations.rs @@ -289,11 +289,12 @@ pub async fn get_folder_conversation_core( match parser.get_conversation(&eid) { Ok(d) => Ok((d.turns, d.session_stats, None)), Err(crate::parsers::ParseError::ConversationNotFound(_)) => { - // For agents like OpenClaw and Cline, the external_id is an - // ACP session UUID that doesn't correspond to any local file. - // Fall back to matching by folder_path and started_at from - // the parsed conversation list. - if at == AgentType::OpenClaw || at == AgentType::Cline { + // The external_id may no longer match any local file — + // e.g. an ACP session UUID (OpenClaw, Cline) or a stale + // ID after session/new fallback overwrote the original + // (Gemini CLI). Fall back to matching by folder_path + // and started_at from the parsed conversation list. + if matches!(at, AgentType::OpenClaw | AgentType::Cline | AgentType::Gemini) { if let Ok(all) = parser.list_conversations() { // Filter by folder_path first, then find the closest // started_at match within 300 seconds of db_created_at. diff --git a/src/components/conversations/conversation-detail-panel.tsx b/src/components/conversations/conversation-detail-panel.tsx index abb2a7e..e4cc05d 100644 --- a/src/components/conversations/conversation-detail-panel.tsx +++ b/src/components/conversations/conversation-detail-panel.tsx @@ -225,7 +225,9 @@ const ConversationTabView = memo(function ConversationTabView({ // resolves, we can't update the DB status yet. This ref records the // desired status so the createConversation callback can apply it. const deferredStatusRef = useRef(null) - const externalIdSavedRef = useRef(false) + // For existing conversations (opened from sidebar), the external_id is + // already persisted — don't let a session/new fallback overwrite it. + const externalIdSavedRef = useRef(conversationId != null) const sessionIdRef = useRef(null) const syncCancelRef = useRef<(() => void) | null>(null) @@ -477,6 +479,11 @@ const ConversationTabView = memo(function ConversationTabView({ if (statusUpdatedRef.current) return const persistedId = dbConvIdRef.current if (!persistedId) return + // Only update status if the user actually interacted in this session. + // A pure history view (opened from sidebar, no messages sent) should + // not flip the conversation to "completed" just because the ACP + // connection disconnected (e.g. agent auth expired). + if (!hasSentMessage) return if (connStatus === "disconnected") { statusUpdatedRef.current = true updateConversationLocal(persistedId, { status: "completed" }) @@ -490,7 +497,7 @@ const ConversationTabView = memo(function ConversationTabView({ console.error("[ConversationTabView] update status:", e) ) } - }, [connStatus, updateConversationLocal]) + }, [connStatus, hasSentMessage, updateConversationLocal]) useEffect(() => { if (dbConversationId == null) return