From 2f9d0a2cac65f0797b8851522db5a702658e048c Mon Sep 17 00:00:00 2001 From: William Correa Date: Mon, 2 Mar 2026 11:39:40 -0300 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20UI=20polish=20=E2=80=94=20settings,?= =?UTF-8?q?=20tabs,=20chat=20dots,=20overscroll,=20session=20badges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove duplicate "Check Permissions" button in WhisperSettings - Make settings tabs full-width with equal distribution - Remove "Settings" text from window title bar - Add session count badge on workspace cards - Disable macOS rubber-band overscroll - Fix terminal chat dot alignment with text baseline - Close review panel on plan approval - Set min-width on Send/Stop button to prevent layout shift --- apps/tauri/src-tauri/src/lib.rs | 1 + apps/tauri/src-tauri/src/sessions.rs | 25 +++++++ apps/tauri/src-tauri/tauri.conf.json | 2 +- apps/tauri/src/SettingsApp.tsx | 4 +- apps/tauri/src/components/HomeScreen.tsx | 21 ++++++ apps/tauri/src/components/MarkdownViewer.tsx | 3 +- apps/tauri/src/components/TerminalInput.tsx | 4 +- apps/tauri/src/components/TerminalMessage.tsx | 6 +- apps/tauri/src/components/WorkspaceCard.tsx | 12 +++- .../components/settings/WhisperSettings.tsx | 66 ++++++++----------- apps/tauri/src/components/ui/tabs.tsx | 2 +- apps/tauri/src/index.css | 13 ++++ 12 files changed, 106 insertions(+), 53 deletions(-) diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs index 8991d0c..3c1e745 100644 --- a/apps/tauri/src-tauri/src/lib.rs +++ b/apps/tauri/src-tauri/src/lib.rs @@ -759,6 +759,7 @@ pub fn run() { acp::commands::acp_send_prompt, acp::commands::acp_set_mode, acp::commands::acp_cancel, + sessions::count_workspace_sessions, sessions::session_list, sessions::session_create, sessions::session_get, diff --git a/apps/tauri/src-tauri/src/sessions.rs b/apps/tauri/src-tauri/src/sessions.rs index 751cc55..2f7223f 100644 --- a/apps/tauri/src-tauri/src/sessions.rs +++ b/apps/tauri/src-tauri/src/sessions.rs @@ -174,11 +174,36 @@ pub fn delete_session(conn: &Connection, id: &str) -> Result<(), String> { Ok(()) } +pub fn count_sessions_batch(conn: &Connection, workspace_paths: &[String]) -> Result, String> { + let mut results = Vec::with_capacity(workspace_paths.len()); + let mut stmt = conn + .prepare("SELECT COUNT(*) FROM sessions WHERE workspace_path = ?1") + .map_err(|e| format!("Prepare error: {}", e))?; + for path in workspace_paths { + let count: i64 = stmt + .query_row(params![path], |row| row.get(0)) + .unwrap_or(0); + if count > 0 { + results.push((path.clone(), count)); + } + } + Ok(results) +} + // --- Tauri commands --- use crate::comments::CommentsDb; use tauri::Manager; +#[tauri::command] +pub fn count_workspace_sessions( + workspace_paths: Vec, + db: tauri::State, +) -> Result, String> { + let conn = db.0.lock().map_err(|e| e.to_string())?; + count_sessions_batch(&conn, &workspace_paths) +} + #[tauri::command] pub fn session_list( workspace_path: String, diff --git a/apps/tauri/src-tauri/tauri.conf.json b/apps/tauri/src-tauri/tauri.conf.json index 25aa772..461b44b 100644 --- a/apps/tauri/src-tauri/tauri.conf.json +++ b/apps/tauri/src-tauri/tauri.conf.json @@ -39,7 +39,7 @@ }, { "label": "settings", - "title": "Settings", + "title": "", "url": "settings.html", "width": 600, "height": 520, diff --git a/apps/tauri/src/SettingsApp.tsx b/apps/tauri/src/SettingsApp.tsx index ab58044..4b19e30 100644 --- a/apps/tauri/src/SettingsApp.tsx +++ b/apps/tauri/src/SettingsApp.tsx @@ -20,8 +20,6 @@ export function SettingsApp() { const currentWindow = getCurrentWindow(); if (currentWindow.label !== "settings") return; - currentWindow.setTitle(t("settings.title")).catch(console.error); - const handleClose = currentWindow.onCloseRequested(async (event) => { event.preventDefault(); await currentWindow.hide(); @@ -48,7 +46,7 @@ export function SettingsApp() {

{t("settings.title")}

- + {t("settings.whisper")} diff --git a/apps/tauri/src/components/HomeScreen.tsx b/apps/tauri/src/components/HomeScreen.tsx index c0f28e9..f37a580 100644 --- a/apps/tauri/src/components/HomeScreen.tsx +++ b/apps/tauri/src/components/HomeScreen.tsx @@ -12,6 +12,7 @@ export function HomeScreen () { const { t } = useTranslation(); const { workspaces, openFile, openDirectory, expandWorkspace, closeWorkspace } = useApp(); const [commentCounts, setCommentCounts] = useState>({}); + const [sessionCounts, setSessionCounts] = useState>({}); const byRecent = (a: { lastAccessed: Date }, b: { lastAccessed: Date }) => new Date(b.lastAccessed).getTime() - new Date(a.lastAccessed).getTime(); @@ -34,10 +35,29 @@ export function HomeScreen () { } }, [fileWorkspaces.map((w) => w.path).join(",")]); + const fetchSessionCounts = useCallback(async () => { + if (dirWorkspaces.length === 0) return; + const paths = dirWorkspaces.map((w) => w.path); + try { + const results = await invoke<[string, number][]>("count_workspace_sessions", { workspacePaths: paths }); + const counts: Record = {}; + for (const [path, count] of results) { + counts[path] = count; + } + setSessionCounts(counts); + } catch { + // silently ignore + } + }, [dirWorkspaces.map((w) => w.path).join(",")]); + useEffect(() => { fetchCommentCounts(); }, [fetchCommentCounts]); + useEffect(() => { + fetchSessionCounts(); + }, [fetchSessionCounts]); + const handleOpenFile = () => openFile(); const handleOpenDirectory = () => openDirectory(); @@ -142,6 +162,7 @@ export function HomeScreen () { diff --git a/apps/tauri/src/components/MarkdownViewer.tsx b/apps/tauri/src/components/MarkdownViewer.tsx index ffa545c..f1c216e 100644 --- a/apps/tauri/src/components/MarkdownViewer.tsx +++ b/apps/tauri/src/components/MarkdownViewer.tsx @@ -202,8 +202,9 @@ export function MarkdownViewer({ const reviewText = review.generateReview(); const hasComments = review.comments.some((c) => !c.resolved); review.resolveAll(); + review.setIsPanelOpen(false); onApprovePlan(hasComments ? reviewText : undefined); - }, [review.generateReview, review.comments, review.resolveAll, onApprovePlan]); + }, [review.generateReview, review.comments, review.resolveAll, review.setIsPanelOpen, onApprovePlan]); // --- Embedded mode (session plan) --- if (embedded) { diff --git a/apps/tauri/src/components/TerminalInput.tsx b/apps/tauri/src/components/TerminalInput.tsx index ab46a54..a84a496 100644 --- a/apps/tauri/src/components/TerminalInput.tsx +++ b/apps/tauri/src/components/TerminalInput.tsx @@ -55,7 +55,7 @@ export function TerminalInput({ onClick={onCancel} size="sm" variant="destructive" - className="text-xs gap-1.5" + className="text-xs gap-1.5 min-w-[100px]" > {t("acp.cancel")} @@ -63,7 +63,7 @@ export function TerminalInput({ ) : ( -
+ + {/* Long Recording Alert */} @@ -401,18 +399,6 @@ export function WhisperSettings() { - {/* Check Permissions */} -
- -
); } diff --git a/apps/tauri/src/components/ui/tabs.tsx b/apps/tauri/src/components/ui/tabs.tsx index ba20fbb..7d4dbd3 100644 --- a/apps/tauri/src/components/ui/tabs.tsx +++ b/apps/tauri/src/components/ui/tabs.tsx @@ -27,7 +27,7 @@ const TabsTrigger = React.forwardRef< Date: Mon, 2 Mar 2026 11:58:17 -0300 Subject: [PATCH 2/3] fix: address CodeRabbit review feedback - Collapse review panel visually on plan approval via reviewRef - Optimize count_sessions_batch to single SQL query with IN clause - Memoize workspace paths in HomeScreen to avoid render allocations --- apps/tauri/src-tauri/src/sessions.rs | 27 +++++++++++--------- apps/tauri/src/components/HomeScreen.tsx | 19 +++++++------- apps/tauri/src/components/MarkdownViewer.tsx | 1 + 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/apps/tauri/src-tauri/src/sessions.rs b/apps/tauri/src-tauri/src/sessions.rs index 2f7223f..5de5c6a 100644 --- a/apps/tauri/src-tauri/src/sessions.rs +++ b/apps/tauri/src-tauri/src/sessions.rs @@ -175,19 +175,22 @@ pub fn delete_session(conn: &Connection, id: &str) -> Result<(), String> { } pub fn count_sessions_batch(conn: &Connection, workspace_paths: &[String]) -> Result, String> { - let mut results = Vec::with_capacity(workspace_paths.len()); - let mut stmt = conn - .prepare("SELECT COUNT(*) FROM sessions WHERE workspace_path = ?1") - .map_err(|e| format!("Prepare error: {}", e))?; - for path in workspace_paths { - let count: i64 = stmt - .query_row(params![path], |row| row.get(0)) - .unwrap_or(0); - if count > 0 { - results.push((path.clone(), count)); - } + if workspace_paths.is_empty() { + return Ok(Vec::new()); } - Ok(results) + let placeholders: Vec = (1..=workspace_paths.len()).map(|i| format!("?{}", i)).collect(); + let sql = format!( + "SELECT workspace_path, COUNT(*) FROM sessions WHERE workspace_path IN ({}) GROUP BY workspace_path HAVING COUNT(*) > 0", + placeholders.join(", ") + ); + let mut stmt = conn.prepare(&sql).map_err(|e| format!("Prepare error: {}", e))?; + let params: Vec<&dyn rusqlite::ToSql> = workspace_paths.iter().map(|p| p as &dyn rusqlite::ToSql).collect(); + let rows = stmt + .query_map(params.as_slice(), |row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))) + .map_err(|e| format!("Query error: {}", e))? + .collect::, _>>() + .map_err(|e| format!("Row error: {}", e))?; + Ok(rows) } // --- Tauri commands --- diff --git a/apps/tauri/src/components/HomeScreen.tsx b/apps/tauri/src/components/HomeScreen.tsx index f37a580..077d616 100644 --- a/apps/tauri/src/components/HomeScreen.tsx +++ b/apps/tauri/src/components/HomeScreen.tsx @@ -2,7 +2,7 @@ import logoSvg from "@/assets/logo.svg"; import { Button } from "@/components/ui/button"; import { useApp } from "@/contexts/AppContext"; import { FileText, FolderOpen } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { WorkspaceCard } from "./WorkspaceCard"; @@ -20,11 +20,13 @@ export function HomeScreen () { const fileWorkspaces = workspaces.filter((w) => w.type === "file").sort(byRecent); const dirWorkspaces = workspaces.filter((w) => w.type === "directory").sort(byRecent); + const filePaths = useMemo(() => fileWorkspaces.map((w) => w.path), [fileWorkspaces]); + const dirPaths = useMemo(() => dirWorkspaces.map((w) => w.path), [dirWorkspaces]); + const fetchCommentCounts = useCallback(async () => { - if (fileWorkspaces.length === 0) return; - const paths = fileWorkspaces.map((w) => w.path); + if (filePaths.length === 0) return; try { - const results = await invoke<[string, number][]>("count_unresolved_comments", { filePaths: paths }); + const results = await invoke<[string, number][]>("count_unresolved_comments", { filePaths }); const counts: Record = {}; for (const [path, count] of results) { counts[path] = count; @@ -33,13 +35,12 @@ export function HomeScreen () { } catch { // silently ignore } - }, [fileWorkspaces.map((w) => w.path).join(",")]); + }, [filePaths]); const fetchSessionCounts = useCallback(async () => { - if (dirWorkspaces.length === 0) return; - const paths = dirWorkspaces.map((w) => w.path); + if (dirPaths.length === 0) return; try { - const results = await invoke<[string, number][]>("count_workspace_sessions", { workspacePaths: paths }); + const results = await invoke<[string, number][]>("count_workspace_sessions", { workspacePaths: dirPaths }); const counts: Record = {}; for (const [path, count] of results) { counts[path] = count; @@ -48,7 +49,7 @@ export function HomeScreen () { } catch { // silently ignore } - }, [dirWorkspaces.map((w) => w.path).join(",")]); + }, [dirPaths]); useEffect(() => { fetchCommentCounts(); diff --git a/apps/tauri/src/components/MarkdownViewer.tsx b/apps/tauri/src/components/MarkdownViewer.tsx index f1c216e..7e3bce0 100644 --- a/apps/tauri/src/components/MarkdownViewer.tsx +++ b/apps/tauri/src/components/MarkdownViewer.tsx @@ -202,6 +202,7 @@ export function MarkdownViewer({ const reviewText = review.generateReview(); const hasComments = review.comments.some((c) => !c.resolved); review.resolveAll(); + reviewRef.current?.collapse(); review.setIsPanelOpen(false); onApprovePlan(hasComments ? reviewText : undefined); }, [review.generateReview, review.comments, review.resolveAll, review.setIsPanelOpen, onApprovePlan]); From 4c54b2cc44fd7473b77169da85ec3d1b34c19d0b Mon Sep 17 00:00:00 2001 From: William Correa Date: Mon, 2 Mar 2026 12:09:55 -0300 Subject: [PATCH 3/3] fix: chunk SQLite batch query and prevent review panel reopen on approve - Chunk workspace_paths into batches of 999 in count_sessions_batch to stay under SQLite's SQLITE_MAX_VARIABLE_NUMBER limit - Track previous phase with a ref so the useEffect that expands the review panel only triggers on transition into "reviewing", preventing it from immediately reverting the close-on-approve --- apps/tauri/src-tauri/src/sessions.rs | 34 ++++++++++++-------- apps/tauri/src/components/MarkdownViewer.tsx | 5 ++- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/apps/tauri/src-tauri/src/sessions.rs b/apps/tauri/src-tauri/src/sessions.rs index 5de5c6a..bae13a8 100644 --- a/apps/tauri/src-tauri/src/sessions.rs +++ b/apps/tauri/src-tauri/src/sessions.rs @@ -178,19 +178,27 @@ pub fn count_sessions_batch(conn: &Connection, workspace_paths: &[String]) -> Re if workspace_paths.is_empty() { return Ok(Vec::new()); } - let placeholders: Vec = (1..=workspace_paths.len()).map(|i| format!("?{}", i)).collect(); - let sql = format!( - "SELECT workspace_path, COUNT(*) FROM sessions WHERE workspace_path IN ({}) GROUP BY workspace_path HAVING COUNT(*) > 0", - placeholders.join(", ") - ); - let mut stmt = conn.prepare(&sql).map_err(|e| format!("Prepare error: {}", e))?; - let params: Vec<&dyn rusqlite::ToSql> = workspace_paths.iter().map(|p| p as &dyn rusqlite::ToSql).collect(); - let rows = stmt - .query_map(params.as_slice(), |row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))) - .map_err(|e| format!("Query error: {}", e))? - .collect::, _>>() - .map_err(|e| format!("Row error: {}", e))?; - Ok(rows) + + const MAX_VARS: usize = 999; + let mut results: Vec<(String, i64)> = Vec::new(); + + for chunk in workspace_paths.chunks(MAX_VARS) { + let placeholders: Vec = (1..=chunk.len()).map(|i| format!("?{}", i)).collect(); + let sql = format!( + "SELECT workspace_path, COUNT(*) FROM sessions WHERE workspace_path IN ({}) GROUP BY workspace_path HAVING COUNT(*) > 0", + placeholders.join(", ") + ); + let mut stmt = conn.prepare(&sql).map_err(|e| format!("Prepare error: {}", e))?; + let params: Vec<&dyn rusqlite::ToSql> = chunk.iter().map(|p| p as &dyn rusqlite::ToSql).collect(); + let rows = stmt + .query_map(params.as_slice(), |row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))) + .map_err(|e| format!("Query error: {}", e))? + .collect::, _>>() + .map_err(|e| format!("Row error: {}", e))?; + results.extend(rows); + } + + Ok(results) } // --- Tauri commands --- diff --git a/apps/tauri/src/components/MarkdownViewer.tsx b/apps/tauri/src/components/MarkdownViewer.tsx index 7e3bce0..7fe7ffb 100644 --- a/apps/tauri/src/components/MarkdownViewer.tsx +++ b/apps/tauri/src/components/MarkdownViewer.tsx @@ -70,6 +70,7 @@ export function MarkdownViewer({ const [reviewCollapsed, setReviewCollapsed] = useState(true); const review = useComments(); + const prevPhaseRef = useRef(phase); const loadContent = useCallback(async (path: string) => { try { @@ -189,12 +190,14 @@ export function MarkdownViewer({ }, [resolvedPath, embedded]); useEffect(() => { - if (embedded && phase === "reviewing" && reviewRef.current?.isCollapsed()) { + const enteringReview = prevPhaseRef.current !== "reviewing" && phase === "reviewing"; + if (embedded && enteringReview && reviewRef.current?.isCollapsed()) { reviewRef.current.expand(); } if (!embedded && review.isPanelOpen && reviewRef.current?.isCollapsed()) { reviewRef.current.expand(); } + prevPhaseRef.current = phase; }, [phase, review.isPanelOpen, embedded]); const handleApprove = useCallback(() => {