Skip to content

Commit 3df6846

Browse files
feat: wire remaining notifications; manual index off the shared DB lock
Notifications: the Agent Error and Task Completed toggles were dead settings. Wire them (and the existing Review Completed) through one notifyIfEnabled helper that respects each toggle's default: - Review Completed -> review finishes (default on) - Agent Error -> review or fix run fails (default on) - Task Completed -> agent fix completes (default off) Indexer: instead of holding the shared DbState connection lock for the whole manual re-index, trigger_index now opens its own WAL connection (the same pattern the periodic/startup indexer already uses) on a blocking thread. With WAL + busy_timeout, other DB-backed commands stay responsive during a cold index. The indexer logic itself is unchanged. Bumps desktop app to 1.1.58. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent b69c623 commit 3df6846

4 files changed

Lines changed: 61 additions & 27 deletions

File tree

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@code-reviewer/desktop",
3-
"version": "1.1.57",
3+
"version": "1.1.58",
44
"private": true,
55
"scripts": {
66
"dev": "lsof -ti:1420 | xargs kill -9 2>/dev/null; vite",

apps/desktop/src-tauri/src/commands/history.rs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use serde::Serialize;
77
use serde_json::{json, Value};
88
use std::io::BufRead;
99
use std::sync::Mutex;
10-
use tauri::{AppHandle, Emitter, State};
10+
use tauri::{AppHandle, Emitter, Manager, State};
1111

1212
static FULL_INDEX_LOCK: Mutex<()> = Mutex::new(());
1313

@@ -178,17 +178,20 @@ pub fn emit_session_archive_updated(app: &AppHandle, summary: &FullIndexSummary)
178178
}
179179

180180
#[tauri::command]
181-
pub async fn trigger_index(app: AppHandle, db: State<'_, DbState>) -> Result<Value, String> {
182-
// Run the full index on a blocking thread: it reads + parses every session
183-
// JSONL and writes SQLite synchronously, which would otherwise stall the
184-
// Tauri async runtime worker for the whole (cold) index.
185-
let conn = db.0.clone();
181+
pub async fn trigger_index(app: AppHandle) -> Result<Value, String> {
182+
// Index against a private WAL connection on a blocking thread — exactly the
183+
// pattern the periodic/startup indexer already uses (main.rs:run_full_index).
184+
// This keeps the full (cold) index off both the async runtime worker AND the
185+
// shared DbState connection lock, so other DB-backed commands stay responsive
186+
// during a manual re-index. FULL_INDEX_LOCK (held inside
187+
// run_full_index_summary_with_conn) still serializes against the periodic run.
188+
let app_data_dir = app
189+
.path()
190+
.app_data_dir()
191+
.map_err(|e| format!("failed to resolve app data dir: {e}"))?;
186192
let summary = tokio::task::spawn_blocking(move || {
187-
let conn = conn.lock().map_err(|e| e.to_string())?;
188-
let _index_guard = FULL_INDEX_LOCK
189-
.lock()
190-
.map_err(|e| format!("full index lock poisoned: {e}"))?;
191-
run_full_index_unlocked(&conn)
193+
let conn = crate::db::init_db(app_data_dir).map_err(|e| e.to_string())?;
194+
run_full_index_summary_with_conn(&conn)
192195
})
193196
.await
194197
.map_err(|e| format!("index task join error: {e}"))??;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-utils/schema.json",
33
"identifier": "com.codevetter.desktop",
44
"productName": "CodeVetter",
5-
"version": "1.1.57",
5+
"version": "1.1.58",
66
"build": {
77
"beforeDevCommand": "npm run dev",
88
"beforeBuildCommand": "npm run build",

apps/desktop/src/pages/QuickReview.tsx

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,26 @@ function sameHistoryFile(historyFile: string, findingFile: string) {
725725
return left === right || left.endsWith(`/${right}`) || right.endsWith(`/${left}`);
726726
}
727727

728+
/**
729+
* Fire a desktop notification if the matching Settings toggle is enabled.
730+
* `defaultOn` mirrors the toggle's default so an unset preference behaves like
731+
* the Settings UI. Best-effort: never throws into the calling flow.
732+
*/
733+
async function notifyIfEnabled(
734+
prefKey: string,
735+
defaultOn: boolean,
736+
title: string,
737+
body: string,
738+
): Promise<void> {
739+
try {
740+
const raw = await getPreference(prefKey);
741+
const enabled = raw == null ? defaultOn : raw === "true";
742+
if (enabled) await sendTrayNotification(title, body);
743+
} catch {
744+
// Notifications are best-effort; ignore permission/plugin failures.
745+
}
746+
}
747+
728748
function parseDiffIntoFiles(diff: string): DiffFile[] {
729749
if (!diff.trim()) return [];
730750
const files: DiffFile[] = [];
@@ -1304,20 +1324,13 @@ export default function QuickReview() {
13041324
setSelectedFindings(new Set());
13051325
// Core action: a code review run completed (also fires `activated` once).
13061326
trackCoreAction("review_run");
1307-
// Fire the "Review Completed" desktop notification (default-on, like the
1308-
// tray monitor's quota notifications). Best-effort — never block the flow.
1309-
void getPreference("notify_review_done")
1310-
.then((raw) => {
1311-
if (raw === "false") return;
1312-
const count = res.findings_count ?? res.findings.length;
1313-
return sendTrayNotification(
1314-
"Review complete",
1315-
`${count} finding${count === 1 ? "" : "s"} · score ${Math.round(res.score)}/100 · ${res.diff_range || diffRange}`,
1316-
);
1317-
})
1318-
.catch(() => {
1319-
// Notifications are best-effort; ignore permission/plugin failures.
1320-
});
1327+
const count = res.findings_count ?? res.findings.length;
1328+
void notifyIfEnabled(
1329+
"notify_review_done",
1330+
true,
1331+
"Review complete",
1332+
`${count} finding${count === 1 ? "" : "s"} · score ${Math.round(res.score)}/100 · ${res.diff_range || diffRange}`,
1333+
);
13211334
await blastPromise;
13221335
} catch (e) {
13231336
console.error("[CodeVetter] CLI review failed:", e);
@@ -1328,6 +1341,12 @@ export default function QuickReview() {
13281341
setError(
13291342
"The review couldn't finish. The AI agent may have failed or timed out — check the agent is installed and try again.",
13301343
);
1344+
void notifyIfEnabled(
1345+
"notify_agent_error",
1346+
true,
1347+
"Review failed",
1348+
"The AI agent failed or timed out during the review.",
1349+
);
13311350
}
13321351
} finally {
13331352
setIsReviewing(false);
@@ -2683,6 +2702,12 @@ export default function QuickReview() {
26832702
const completedAt = new Date().toISOString();
26842703
setFixResult(res);
26852704
setFixCompletedAt(completedAt);
2705+
void notifyIfEnabled(
2706+
"notify_task_complete",
2707+
false,
2708+
"Fix complete",
2709+
`${res.findings_fixed} finding${res.findings_fixed === 1 ? "" : "s"} fixed across ${res.changed_files.length} file${res.changed_files.length === 1 ? "" : "s"}.`,
2710+
);
26862711
recordProcedureExecutionEvents(
26872712
procedureEventsForFixResult(activeProcedureSteps, res),
26882713
{
@@ -2709,6 +2734,12 @@ export default function QuickReview() {
27092734
}
27102735
} catch (e) {
27112736
setError(`Fix failed: ${String(e)}`);
2737+
void notifyIfEnabled(
2738+
"notify_agent_error",
2739+
true,
2740+
"Fix failed",
2741+
"The AI agent failed while applying the selected fixes.",
2742+
);
27122743
} finally {
27132744
setIsFixing(null);
27142745
unlisten?.();

0 commit comments

Comments
 (0)