From 455d304885bb3a75517e67d2901b61aaf50aa36f Mon Sep 17 00:00:00 2001 From: MDietermann Date: Wed, 12 Aug 2026 17:58:02 +0200 Subject: [PATCH] fix: address final review findings - Finding 1: add AppState::authed_get/authed_post helpers that attach Bearer token when present; all 4 commands now use them - Finding 2: MessageInput accepts onSent callback, calls it after successful send; ChatPage passes refreshKey bump as onSent - Finding 3: MessageList useEffect uses cancellation flag so stale in-flight responses cannot overwrite a newly selected conversation - Finding 4: scrollIntoView moved to separate effect keyed on messages.length so scroll only fires when new messages arrive --- src-tauri/src/commands/conversation.rs | 3 +-- src-tauri/src/commands/message.rs | 6 ++---- src-tauri/src/commands/workspace.rs | 3 +-- src-tauri/src/state.rs | 17 ++++++++++++++++ src/components/MessageInput.tsx | 8 +++++++- src/components/MessageList.tsx | 28 ++++++++++++++++++++------ src/pages/ChatPage.tsx | 8 ++++++-- 7 files changed, 56 insertions(+), 17 deletions(-) diff --git a/src-tauri/src/commands/conversation.rs b/src-tauri/src/commands/conversation.rs index 3de8e5c..4b352d0 100644 --- a/src-tauri/src/commands/conversation.rs +++ b/src-tauri/src/commands/conversation.rs @@ -7,8 +7,7 @@ pub async fn list_conversations( workspace_id: String, ) -> Result, String> { state - .client - .get(format!("{}/conversations?workspace_id={}", BASE_URL, workspace_id)) + .authed_get(&format!("{}/conversations?workspace_id={}", BASE_URL, workspace_id)) .send() .await .map_err(|e| e.to_string())? diff --git a/src-tauri/src/commands/message.rs b/src-tauri/src/commands/message.rs index abe54c6..ef66e4b 100644 --- a/src-tauri/src/commands/message.rs +++ b/src-tauri/src/commands/message.rs @@ -7,8 +7,7 @@ pub async fn list_messages( conversation_id: String, ) -> Result, String> { state - .client - .get(format!("{}/conversations/{}/messages", BASE_URL, conversation_id)) + .authed_get(&format!("{}/conversations/{}/messages", BASE_URL, conversation_id)) .send() .await .map_err(|e| e.to_string())? @@ -37,8 +36,7 @@ pub async fn send_message( }); state - .client - .post(format!("{}/messages", BASE_URL)) + .authed_post(&format!("{}/messages", BASE_URL)) .json(&body) .send() .await diff --git a/src-tauri/src/commands/workspace.rs b/src-tauri/src/commands/workspace.rs index 1d177c9..7c0ca48 100644 --- a/src-tauri/src/commands/workspace.rs +++ b/src-tauri/src/commands/workspace.rs @@ -4,8 +4,7 @@ use crate::state::{AppState, BASE_URL}; #[tauri::command] pub async fn list_workspaces(state: tauri::State<'_, AppState>) -> Result, String> { state - .client - .get(format!("{}/workspaces", BASE_URL)) + .authed_get(&format!("{}/workspaces", BASE_URL)) .send() .await .map_err(|e| e.to_string())? diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 4eebae3..b5f419e 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -16,4 +16,21 @@ impl AppState { user_id: Mutex::new(None), } } + + pub fn authed_get(&self, url: &str) -> reqwest::RequestBuilder { + let builder = self.client.get(url); + // ponytail: omit header when no token — server may not require auth yet + match self.token.lock().unwrap().clone() { + Some(t) => builder.bearer_auth(t), + None => builder, + } + } + + pub fn authed_post(&self, url: &str) -> reqwest::RequestBuilder { + let builder = self.client.post(url); + match self.token.lock().unwrap().clone() { + Some(t) => builder.bearer_auth(t), + None => builder, + } + } } diff --git a/src/components/MessageInput.tsx b/src/components/MessageInput.tsx index f8caa73..837494c 100644 --- a/src/components/MessageInput.tsx +++ b/src/components/MessageInput.tsx @@ -1,7 +1,12 @@ import { useState } from "react"; import { invoke } from "@tauri-apps/api/core"; -export default function MessageInput({ conversationId }: { conversationId: string }) { +interface Props { + conversationId: string; + onSent: () => void; +} + +export default function MessageInput({ conversationId, onSent }: Props) { const [content, setContent] = useState(""); const [error, setError] = useState(null); @@ -11,6 +16,7 @@ export default function MessageInput({ conversationId }: { conversationId: strin await invoke("send_message", { conversationId, content }); setContent(""); setError(null); + onSent(); } catch (e) { setError(String(e)); } diff --git a/src/components/MessageList.tsx b/src/components/MessageList.tsx index 88778da..896d65e 100644 --- a/src/components/MessageList.tsx +++ b/src/components/MessageList.tsx @@ -8,25 +8,41 @@ interface Message { created_at: string; } -export default function MessageList({ conversationId }: { conversationId: string }) { +interface Props { + conversationId: string; + refreshKey: number; +} + +export default function MessageList({ conversationId, refreshKey }: Props) { const [messages, setMessages] = useState([]); const [error, setError] = useState(null); const bottomRef = useRef(null); + const prevLengthRef = useRef(0); + // Fix 3: cancellation flag prevents stale responses from overwriting new conversation useEffect(() => { + let cancelled = false; const load = () => invoke("list_messages", { conversationId }) .then((msgs) => { - setMessages(msgs); - bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + if (!cancelled) setMessages(msgs); }) - .catch((e) => setError(String(e))); + .catch((e) => { if (!cancelled) setError(String(e)); }); load(); // ponytail: polling every 3s — replace with WebSocket when server supports it const id = setInterval(load, 3000); - return () => clearInterval(id); - }, [conversationId]); + return () => { cancelled = true; clearInterval(id); }; + // Fix 2: refreshKey in deps triggers immediate reload after send + }, [conversationId, refreshKey]); + + // Fix 4: scroll only when message count increases, not on every poll + useEffect(() => { + if (messages.length > prevLengthRef.current) { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + prevLengthRef.current = messages.length; + } + }, [messages.length]); return (
diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 199dc62..c40f422 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -7,6 +7,7 @@ import MessageInput from "../components/MessageInput"; export default function ChatPage() { const { workspaceId } = useParams<{ workspaceId: string }>(); const [conversationId, setConversationId] = useState(null); + const [refreshKey, setRefreshKey] = useState(0); return (
@@ -18,8 +19,11 @@ export default function ChatPage() {
{conversationId ? ( <> - - + + setRefreshKey((k) => k + 1)} + /> ) : (