Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/tauri/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
36 changes: 36 additions & 0 deletions apps/tauri/src-tauri/src/sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,47 @@ pub fn delete_session(conn: &Connection, id: &str) -> Result<(), String> {
Ok(())
}

pub fn count_sessions_batch(conn: &Connection, workspace_paths: &[String]) -> Result<Vec<(String, i64)>, String> {
if workspace_paths.is_empty() {
return Ok(Vec::new());
}

const MAX_VARS: usize = 999;
let mut results: Vec<(String, i64)> = Vec::new();

for chunk in workspace_paths.chunks(MAX_VARS) {
let placeholders: Vec<String> = (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::<Result<Vec<_>, _>>()
.map_err(|e| format!("Row error: {}", e))?;
results.extend(rows);
}

Ok(results)
}

// --- Tauri commands ---

use crate::comments::CommentsDb;
use tauri::Manager;

#[tauri::command]
pub fn count_workspace_sessions(
workspace_paths: Vec<String>,
db: tauri::State<CommentsDb>,
) -> Result<Vec<(String, i64)>, 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,
Expand Down
2 changes: 1 addition & 1 deletion apps/tauri/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
},
{
"label": "settings",
"title": "Settings",
"title": "",
"url": "settings.html",
"width": 600,
"height": 520,
Expand Down
4 changes: 1 addition & 3 deletions apps/tauri/src/SettingsApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -48,7 +46,7 @@ export function SettingsApp() {
<div className="p-6 flex-1 overflow-y-auto">
<h1 className="text-lg font-semibold mb-4">{t("settings.title")}</h1>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="mb-4">
<TabsList className="mb-4 w-full">
<TabsTrigger value="whisper" className="gap-1.5">
<Mic className="h-3.5 w-3.5" />
{t("settings.whisper")}
Expand Down
32 changes: 27 additions & 5 deletions apps/tauri/src/components/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

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

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

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<string, number> = {};
for (const [path, count] of results) {
counts[path] = count;
Expand All @@ -32,12 +35,30 @@ export function HomeScreen () {
} catch {
// silently ignore
}
}, [fileWorkspaces.map((w) => w.path).join(",")]);
}, [filePaths]);

const fetchSessionCounts = useCallback(async () => {
if (dirPaths.length === 0) return;
try {
const results = await invoke<[string, number][]>("count_workspace_sessions", { workspacePaths: dirPaths });
const counts: Record<string, number> = {};
for (const [path, count] of results) {
counts[path] = count;
}
setSessionCounts(counts);
} catch {
// silently ignore
}
}, [dirPaths]);

useEffect(() => {
fetchCommentCounts();
}, [fetchCommentCounts]);

useEffect(() => {
fetchSessionCounts();
}, [fetchSessionCounts]);

const handleOpenFile = () => openFile();
const handleOpenDirectory = () => openDirectory();

Expand Down Expand Up @@ -142,6 +163,7 @@ export function HomeScreen () {
<WorkspaceCard
key={workspace.id}
workspace={workspace}
sessionCount={sessionCounts[workspace.path]}
onExpand={expandWorkspace}
onClose={closeWorkspace}
/>
Expand Down
9 changes: 7 additions & 2 deletions apps/tauri/src/components/MarkdownViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export function MarkdownViewer({
const [reviewCollapsed, setReviewCollapsed] = useState(true);

const review = useComments();
const prevPhaseRef = useRef<PlanPhase | undefined>(phase);

const loadContent = useCallback(async (path: string) => {
try {
Expand Down Expand Up @@ -189,21 +190,25 @@ 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(() => {
if (!onApprovePlan) return;
const reviewText = review.generateReview();
const hasComments = review.comments.some((c) => !c.resolved);
review.resolveAll();
reviewRef.current?.collapse();
review.setIsPanelOpen(false);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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) {
Expand Down
4 changes: 2 additions & 2 deletions apps/tauri/src/components/TerminalInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ export function TerminalInput({
onClick={onCancel}
size="sm"
variant="destructive"
className="text-xs gap-1.5"
className="text-xs gap-1.5 min-w-[100px]"
>
<Square className="h-3 w-3" />
{t("acp.cancel")}
</Button>
) : (
<Button
size="sm"
className="text-xs gap-1.5 font-mono"
className="text-xs gap-1.5 font-mono min-w-[100px]"
onClick={handleSend}
disabled={!input.trim() || disabled}
>
Expand Down
6 changes: 3 additions & 3 deletions apps/tauri/src/components/TerminalMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function TerminalMessage({ message, isLast, isStreaming }: TerminalMessag
if (message.type === "tool") {
return (
<div className="terminal-msg">
<span className="mt-[5px] flex-shrink-0">
<span className="dot-wrapper text-sm flex-shrink-0">
<span className="dot dot-tool" />
</span>
<details className="font-mono text-sm text-muted-foreground">
Expand All @@ -49,7 +49,7 @@ export function TerminalMessage({ message, isLast, isStreaming }: TerminalMessag
if (message.type === "thinking") {
return (
<div className="terminal-msg">
<span className="mt-[5px] flex-shrink-0">
<span className="dot-wrapper text-sm flex-shrink-0">
<span className="dot dot-thinking" />
</span>
<div className="terminal-markdown font-mono text-sm text-muted-foreground/70 italic break-words min-w-0">
Expand Down Expand Up @@ -79,7 +79,7 @@ function AgentMessage({ message, isLast, isStreaming }: TerminalMessageProps) {

return (
<div className="terminal-msg">
<span className="mt-[5px] flex-shrink-0">
<span className="dot-wrapper text-sm flex-shrink-0">
<span className="dot dot-agent" />
</span>
<div className="terminal-markdown font-mono text-sm text-foreground break-words min-w-0">
Expand Down
12 changes: 10 additions & 2 deletions apps/tauri/src/components/WorkspaceCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MessageSquare, X } from 'lucide-react';
import { MessageSquare, MessagesSquare, X } from 'lucide-react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import {
Expand All @@ -21,11 +21,12 @@ import { shortenPath } from '@/lib/format-path';
interface WorkspaceCardProps {
workspace: Workspace;
unresolvedComments?: number;
sessionCount?: number;
onExpand: (id: string, rect?: CardRect) => void;
onClose: (id: string) => void;
}

export function WorkspaceCard({ workspace, unresolvedComments, onExpand, onClose }: WorkspaceCardProps) {
export function WorkspaceCard({ workspace, unresolvedComments, sessionCount, onExpand, onClose }: WorkspaceCardProps) {
const { t, i18n } = useTranslation();

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

{sessionCount != null && sessionCount > 0 && (
<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">
<MessagesSquare className="h-3 w-3" />
<span className="font-semibold text-[10px]">{sessionCount}</span>
</span>
)}

<h3 className="font-semibold text-sm truncate pr-6">{workspace.displayName}</h3>
<p className="text-xs text-muted-foreground truncate mt-1" dir="rtl" title={workspace.path}>
<bdi>{shortenPath(workspace.path)}</bdi>
Expand Down
66 changes: 26 additions & 40 deletions apps/tauri/src/components/settings/WhisperSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from "@/components/ui/select";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Check, Download, Keyboard, Loader2, Mic, Shield, Trash2 } from "lucide-react";
import { Check, Download, Keyboard, Loader2, Mic, Trash2 } from "lucide-react";

const { invoke } = window.__TAURI__.core;
const { listen } = window.__TAURI__.event;
Expand Down Expand Up @@ -268,33 +268,31 @@ export function WhisperSettings() {
{/* Microphone */}
<div className="space-y-2">
<Label className="text-sm font-medium">{t("whisper.audioDevice")}</Label>
<div className="flex items-center gap-2">
<Select
value={settings.selected_device ?? "__default__"}
onValueChange={handleDeviceChange}
>
<SelectTrigger className="flex-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="__default__">{t("whisper.defaultDevice")}</SelectItem>
{devices.map((d) => (
<SelectItem key={d.name} value={d.name}>
{d.name} {d.is_default ? `(${t("whisper.defaultDevice")})` : ""}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
size="icon"
className="h-9 w-9 shrink-0"
onClick={handleCheckPermissions}
title={t("whisper.checkPermissions")}
>
<Shield className="h-4 w-4" />
</Button>
</div>
<Select
value={settings.selected_device ?? "__default__"}
onValueChange={handleDeviceChange}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="__default__">{t("whisper.defaultDevice")}</SelectItem>
{devices.map((d) => (
<SelectItem key={d.name} value={d.name}>
{d.name} {d.is_default ? `(${t("whisper.defaultDevice")})` : ""}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
className="text-xs"
onClick={handleCheckPermissions}
>
<Mic className="h-3.5 w-3.5 mr-1.5" />
{t("whisper.checkPermissions")}
</Button>
</div>

{/* Long Recording Alert */}
Expand Down Expand Up @@ -401,18 +399,6 @@ export function WhisperSettings() {
</div>
</div>

{/* Check Permissions */}
<div>
<Button
variant="outline"
size="sm"
className="text-xs"
onClick={handleCheckPermissions}
>
<Mic className="h-3.5 w-3.5 mr-1.5" />
{t("whisper.checkPermissions")}
</Button>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion apps/tauri/src/components/ui/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
"inline-flex flex-1 items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
className,
)}
{...props}
Expand Down
Loading