Skip to content

Commit 0a4d2a4

Browse files
authored
fix: UI polish — settings, chat, overscroll, session badges (#32)
* fix: UI polish — settings, tabs, chat dots, overscroll, session badges - 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 * 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 * 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
1 parent 40282fb commit 0a4d2a4

12 files changed

Lines changed: 128 additions & 59 deletions

File tree

apps/tauri/src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,7 @@ pub fn run() {
759759
acp::commands::acp_send_prompt,
760760
acp::commands::acp_set_mode,
761761
acp::commands::acp_cancel,
762+
sessions::count_workspace_sessions,
762763
sessions::session_list,
763764
sessions::session_create,
764765
sessions::session_get,

apps/tauri/src-tauri/src/sessions.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,47 @@ pub fn delete_session(conn: &Connection, id: &str) -> Result<(), String> {
174174
Ok(())
175175
}
176176

177+
pub fn count_sessions_batch(conn: &Connection, workspace_paths: &[String]) -> Result<Vec<(String, i64)>, String> {
178+
if workspace_paths.is_empty() {
179+
return Ok(Vec::new());
180+
}
181+
182+
const MAX_VARS: usize = 999;
183+
let mut results: Vec<(String, i64)> = Vec::new();
184+
185+
for chunk in workspace_paths.chunks(MAX_VARS) {
186+
let placeholders: Vec<String> = (1..=chunk.len()).map(|i| format!("?{}", i)).collect();
187+
let sql = format!(
188+
"SELECT workspace_path, COUNT(*) FROM sessions WHERE workspace_path IN ({}) GROUP BY workspace_path HAVING COUNT(*) > 0",
189+
placeholders.join(", ")
190+
);
191+
let mut stmt = conn.prepare(&sql).map_err(|e| format!("Prepare error: {}", e))?;
192+
let params: Vec<&dyn rusqlite::ToSql> = chunk.iter().map(|p| p as &dyn rusqlite::ToSql).collect();
193+
let rows = stmt
194+
.query_map(params.as_slice(), |row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?)))
195+
.map_err(|e| format!("Query error: {}", e))?
196+
.collect::<Result<Vec<_>, _>>()
197+
.map_err(|e| format!("Row error: {}", e))?;
198+
results.extend(rows);
199+
}
200+
201+
Ok(results)
202+
}
203+
177204
// --- Tauri commands ---
178205

179206
use crate::comments::CommentsDb;
180207
use tauri::Manager;
181208

209+
#[tauri::command]
210+
pub fn count_workspace_sessions(
211+
workspace_paths: Vec<String>,
212+
db: tauri::State<CommentsDb>,
213+
) -> Result<Vec<(String, i64)>, String> {
214+
let conn = db.0.lock().map_err(|e| e.to_string())?;
215+
count_sessions_batch(&conn, &workspace_paths)
216+
}
217+
182218
#[tauri::command]
183219
pub fn session_list(
184220
workspace_path: String,

apps/tauri/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
},
4040
{
4141
"label": "settings",
42-
"title": "Settings",
42+
"title": "",
4343
"url": "settings.html",
4444
"width": 600,
4545
"height": 520,

apps/tauri/src/SettingsApp.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ export function SettingsApp() {
2020
const currentWindow = getCurrentWindow();
2121
if (currentWindow.label !== "settings") return;
2222

23-
currentWindow.setTitle(t("settings.title")).catch(console.error);
24-
2523
const handleClose = currentWindow.onCloseRequested(async (event) => {
2624
event.preventDefault();
2725
await currentWindow.hide();
@@ -48,7 +46,7 @@ export function SettingsApp() {
4846
<div className="p-6 flex-1 overflow-y-auto">
4947
<h1 className="text-lg font-semibold mb-4">{t("settings.title")}</h1>
5048
<Tabs value={activeTab} onValueChange={setActiveTab}>
51-
<TabsList className="mb-4">
49+
<TabsList className="mb-4 w-full">
5250
<TabsTrigger value="whisper" className="gap-1.5">
5351
<Mic className="h-3.5 w-3.5" />
5452
{t("settings.whisper")}

apps/tauri/src/components/HomeScreen.tsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import logoSvg from "@/assets/logo.svg";
22
import { Button } from "@/components/ui/button";
33
import { useApp } from "@/contexts/AppContext";
44
import { FileText, FolderOpen } from "lucide-react";
5-
import { useCallback, useEffect, useState } from "react";
5+
import { useCallback, useEffect, useMemo, useState } from "react";
66
import { useTranslation } from "react-i18next";
77
import { WorkspaceCard } from "./WorkspaceCard";
88

@@ -12,18 +12,21 @@ export function HomeScreen () {
1212
const { t } = useTranslation();
1313
const { workspaces, openFile, openDirectory, expandWorkspace, closeWorkspace } = useApp();
1414
const [commentCounts, setCommentCounts] = useState<Record<string, number>>({});
15+
const [sessionCounts, setSessionCounts] = useState<Record<string, number>>({});
1516

1617
const byRecent = (a: { lastAccessed: Date }, b: { lastAccessed: Date }) =>
1718
new Date(b.lastAccessed).getTime() - new Date(a.lastAccessed).getTime();
1819

1920
const fileWorkspaces = workspaces.filter((w) => w.type === "file").sort(byRecent);
2021
const dirWorkspaces = workspaces.filter((w) => w.type === "directory").sort(byRecent);
2122

23+
const filePaths = useMemo(() => fileWorkspaces.map((w) => w.path), [fileWorkspaces]);
24+
const dirPaths = useMemo(() => dirWorkspaces.map((w) => w.path), [dirWorkspaces]);
25+
2226
const fetchCommentCounts = useCallback(async () => {
23-
if (fileWorkspaces.length === 0) return;
24-
const paths = fileWorkspaces.map((w) => w.path);
27+
if (filePaths.length === 0) return;
2528
try {
26-
const results = await invoke<[string, number][]>("count_unresolved_comments", { filePaths: paths });
29+
const results = await invoke<[string, number][]>("count_unresolved_comments", { filePaths });
2730
const counts: Record<string, number> = {};
2831
for (const [path, count] of results) {
2932
counts[path] = count;
@@ -32,12 +35,30 @@ export function HomeScreen () {
3235
} catch {
3336
// silently ignore
3437
}
35-
}, [fileWorkspaces.map((w) => w.path).join(",")]);
38+
}, [filePaths]);
39+
40+
const fetchSessionCounts = useCallback(async () => {
41+
if (dirPaths.length === 0) return;
42+
try {
43+
const results = await invoke<[string, number][]>("count_workspace_sessions", { workspacePaths: dirPaths });
44+
const counts: Record<string, number> = {};
45+
for (const [path, count] of results) {
46+
counts[path] = count;
47+
}
48+
setSessionCounts(counts);
49+
} catch {
50+
// silently ignore
51+
}
52+
}, [dirPaths]);
3653

3754
useEffect(() => {
3855
fetchCommentCounts();
3956
}, [fetchCommentCounts]);
4057

58+
useEffect(() => {
59+
fetchSessionCounts();
60+
}, [fetchSessionCounts]);
61+
4162
const handleOpenFile = () => openFile();
4263
const handleOpenDirectory = () => openDirectory();
4364

@@ -142,6 +163,7 @@ export function HomeScreen () {
142163
<WorkspaceCard
143164
key={workspace.id}
144165
workspace={workspace}
166+
sessionCount={sessionCounts[workspace.path]}
145167
onExpand={expandWorkspace}
146168
onClose={closeWorkspace}
147169
/>

apps/tauri/src/components/MarkdownViewer.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export function MarkdownViewer({
7070
const [reviewCollapsed, setReviewCollapsed] = useState(true);
7171

7272
const review = useComments();
73+
const prevPhaseRef = useRef<PlanPhase | undefined>(phase);
7374

7475
const loadContent = useCallback(async (path: string) => {
7576
try {
@@ -189,21 +190,25 @@ export function MarkdownViewer({
189190
}, [resolvedPath, embedded]);
190191

191192
useEffect(() => {
192-
if (embedded && phase === "reviewing" && reviewRef.current?.isCollapsed()) {
193+
const enteringReview = prevPhaseRef.current !== "reviewing" && phase === "reviewing";
194+
if (embedded && enteringReview && reviewRef.current?.isCollapsed()) {
193195
reviewRef.current.expand();
194196
}
195197
if (!embedded && review.isPanelOpen && reviewRef.current?.isCollapsed()) {
196198
reviewRef.current.expand();
197199
}
200+
prevPhaseRef.current = phase;
198201
}, [phase, review.isPanelOpen, embedded]);
199202

200203
const handleApprove = useCallback(() => {
201204
if (!onApprovePlan) return;
202205
const reviewText = review.generateReview();
203206
const hasComments = review.comments.some((c) => !c.resolved);
204207
review.resolveAll();
208+
reviewRef.current?.collapse();
209+
review.setIsPanelOpen(false);
205210
onApprovePlan(hasComments ? reviewText : undefined);
206-
}, [review.generateReview, review.comments, review.resolveAll, onApprovePlan]);
211+
}, [review.generateReview, review.comments, review.resolveAll, review.setIsPanelOpen, onApprovePlan]);
207212

208213
// --- Embedded mode (session plan) ---
209214
if (embedded) {

apps/tauri/src/components/TerminalInput.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,15 @@ export function TerminalInput({
5555
onClick={onCancel}
5656
size="sm"
5757
variant="destructive"
58-
className="text-xs gap-1.5"
58+
className="text-xs gap-1.5 min-w-[100px]"
5959
>
6060
<Square className="h-3 w-3" />
6161
{t("acp.cancel")}
6262
</Button>
6363
) : (
6464
<Button
6565
size="sm"
66-
className="text-xs gap-1.5 font-mono"
66+
className="text-xs gap-1.5 font-mono min-w-[100px]"
6767
onClick={handleSend}
6868
disabled={!input.trim() || disabled}
6969
>

apps/tauri/src/components/TerminalMessage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function TerminalMessage({ message, isLast, isStreaming }: TerminalMessag
2626
if (message.type === "tool") {
2727
return (
2828
<div className="terminal-msg">
29-
<span className="mt-[5px] flex-shrink-0">
29+
<span className="dot-wrapper text-sm flex-shrink-0">
3030
<span className="dot dot-tool" />
3131
</span>
3232
<details className="font-mono text-sm text-muted-foreground">
@@ -49,7 +49,7 @@ export function TerminalMessage({ message, isLast, isStreaming }: TerminalMessag
4949
if (message.type === "thinking") {
5050
return (
5151
<div className="terminal-msg">
52-
<span className="mt-[5px] flex-shrink-0">
52+
<span className="dot-wrapper text-sm flex-shrink-0">
5353
<span className="dot dot-thinking" />
5454
</span>
5555
<div className="terminal-markdown font-mono text-sm text-muted-foreground/70 italic break-words min-w-0">
@@ -79,7 +79,7 @@ function AgentMessage({ message, isLast, isStreaming }: TerminalMessageProps) {
7979

8080
return (
8181
<div className="terminal-msg">
82-
<span className="mt-[5px] flex-shrink-0">
82+
<span className="dot-wrapper text-sm flex-shrink-0">
8383
<span className="dot dot-agent" />
8484
</span>
8585
<div className="terminal-markdown font-mono text-sm text-foreground break-words min-w-0">

apps/tauri/src/components/WorkspaceCard.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MessageSquare, X } from 'lucide-react';
1+
import { MessageSquare, MessagesSquare, X } from 'lucide-react';
22
import { Card } from '@/components/ui/card';
33
import { Button } from '@/components/ui/button';
44
import {
@@ -21,11 +21,12 @@ import { shortenPath } from '@/lib/format-path';
2121
interface WorkspaceCardProps {
2222
workspace: Workspace;
2323
unresolvedComments?: number;
24+
sessionCount?: number;
2425
onExpand: (id: string, rect?: CardRect) => void;
2526
onClose: (id: string) => void;
2627
}
2728

28-
export function WorkspaceCard({ workspace, unresolvedComments, onExpand, onClose }: WorkspaceCardProps) {
29+
export function WorkspaceCard({ workspace, unresolvedComments, sessionCount, onExpand, onClose }: WorkspaceCardProps) {
2930
const { t, i18n } = useTranslation();
3031

3132
const prefix = workspace.type === 'directory' ? 'workspace' : 'document';
@@ -89,6 +90,13 @@ export function WorkspaceCard({ workspace, unresolvedComments, onExpand, onClose
8990
</span>
9091
)}
9192

93+
{sessionCount != null && sessionCount > 0 && (
94+
<span className="absolute top-1.5 right-1.5 inline-flex items-center gap-0.5 text-muted-foreground group-hover:opacity-0 transition-opacity">
95+
<MessagesSquare className="h-3 w-3" />
96+
<span className="font-semibold text-[10px]">{sessionCount}</span>
97+
</span>
98+
)}
99+
92100
<h3 className="font-semibold text-sm truncate pr-6">{workspace.displayName}</h3>
93101
<p className="text-xs text-muted-foreground truncate mt-1" dir="rtl" title={workspace.path}>
94102
<bdi>{shortenPath(workspace.path)}</bdi>

apps/tauri/src/components/settings/WhisperSettings.tsx

Lines changed: 26 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from "@/components/ui/select";
1313
import { useTranslation } from "react-i18next";
1414
import { toast } from "sonner";
15-
import { Check, Download, Keyboard, Loader2, Mic, Shield, Trash2 } from "lucide-react";
15+
import { Check, Download, Keyboard, Loader2, Mic, Trash2 } from "lucide-react";
1616

1717
const { invoke } = window.__TAURI__.core;
1818
const { listen } = window.__TAURI__.event;
@@ -268,33 +268,31 @@ export function WhisperSettings() {
268268
{/* Microphone */}
269269
<div className="space-y-2">
270270
<Label className="text-sm font-medium">{t("whisper.audioDevice")}</Label>
271-
<div className="flex items-center gap-2">
272-
<Select
273-
value={settings.selected_device ?? "__default__"}
274-
onValueChange={handleDeviceChange}
275-
>
276-
<SelectTrigger className="flex-1">
277-
<SelectValue />
278-
</SelectTrigger>
279-
<SelectContent>
280-
<SelectItem value="__default__">{t("whisper.defaultDevice")}</SelectItem>
281-
{devices.map((d) => (
282-
<SelectItem key={d.name} value={d.name}>
283-
{d.name} {d.is_default ? `(${t("whisper.defaultDevice")})` : ""}
284-
</SelectItem>
285-
))}
286-
</SelectContent>
287-
</Select>
288-
<Button
289-
variant="outline"
290-
size="icon"
291-
className="h-9 w-9 shrink-0"
292-
onClick={handleCheckPermissions}
293-
title={t("whisper.checkPermissions")}
294-
>
295-
<Shield className="h-4 w-4" />
296-
</Button>
297-
</div>
271+
<Select
272+
value={settings.selected_device ?? "__default__"}
273+
onValueChange={handleDeviceChange}
274+
>
275+
<SelectTrigger>
276+
<SelectValue />
277+
</SelectTrigger>
278+
<SelectContent>
279+
<SelectItem value="__default__">{t("whisper.defaultDevice")}</SelectItem>
280+
{devices.map((d) => (
281+
<SelectItem key={d.name} value={d.name}>
282+
{d.name} {d.is_default ? `(${t("whisper.defaultDevice")})` : ""}
283+
</SelectItem>
284+
))}
285+
</SelectContent>
286+
</Select>
287+
<Button
288+
variant="outline"
289+
size="sm"
290+
className="text-xs"
291+
onClick={handleCheckPermissions}
292+
>
293+
<Mic className="h-3.5 w-3.5 mr-1.5" />
294+
{t("whisper.checkPermissions")}
295+
</Button>
298296
</div>
299297

300298
{/* Long Recording Alert */}
@@ -401,18 +399,6 @@ export function WhisperSettings() {
401399
</div>
402400
</div>
403401

404-
{/* Check Permissions */}
405-
<div>
406-
<Button
407-
variant="outline"
408-
size="sm"
409-
className="text-xs"
410-
onClick={handleCheckPermissions}
411-
>
412-
<Mic className="h-3.5 w-3.5 mr-1.5" />
413-
{t("whisper.checkPermissions")}
414-
</Button>
415-
</div>
416402
</div>
417403
);
418404
}

0 commit comments

Comments
 (0)