Skip to content

Commit 5e07dcc

Browse files
AssistantAssistant
authored andcommitted
Release CodeVetter 1.1.13
1 parent bcc143e commit 5e07dcc

6 files changed

Lines changed: 177 additions & 5 deletions

File tree

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,21 @@ The product should prefer narrow, evidence-backed loops over broad "code intelli
4444

4545
## Installation
4646

47+
### Ask Your Agent To Install
48+
49+
Give your coding agent this prompt:
50+
51+
```text
52+
Install CodeVetter from the latest GitHub release:
53+
https://github.com/sarthak-fleet/CodeVetter/releases/latest
54+
55+
Detect this machine's OS and CPU architecture, download the matching CodeVetter app archive, verify the release asset hash when available, extract it, install CodeVetter.app into /Applications on macOS, remove the quarantine attribute if needed, and launch the app once to verify it starts.
56+
```
57+
58+
Prefer the app archive over the DMG until the macOS bundle is Developer ID signed and notarized.
59+
60+
### Development Install
61+
4762
```bash
4863
# Clone and install dependencies (uses npm workspaces)
4964
git clone https://github.com/sarthak-fleet/CodeVetter.git

apps/desktop/docs/RELEASING.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@
2525
npm run tauri:build
2626
```
2727

28-
3. The build produces (in `src-tauri/target/release/bundle/`):
29-
- `CodeVetter_x.y.z_aarch64.dmg` — installer for manual download
28+
3. The release workflow produces:
3029
- `CodeVetter.app.tar.gz` — compressed app bundle for auto-update
3130
- `CodeVetter.app.tar.gz.sig` — signature file
3231
- `latest.json` — update manifest (contains version, download URL, signature)
3332

33+
Prefer the app archive for manual installs until the macOS bundle is Developer ID signed and notarized. Unsigned or ad-hoc signed DMGs fail Gatekeeper for new installs.
34+
3435
4. Create a GitHub release at `sarthak-fleet/CodeVetter`:
3536
- Tag: `v{x.y.z}`
3637
- Upload all build artifacts including `latest.json`

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.11",
3+
"version": "1.1.13",
44
"private": true,
55
"scripts": {
66
"dev": "lsof -ti:1420 | xargs kill -9 2>/dev/null; vite",

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.12",
5+
"version": "1.1.13",
66
"build": {
77
"beforeDevCommand": "npm run dev",
88
"beforeBuildCommand": "npm run build",

apps/desktop/src/lib/use-tray-monitor.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import { useEffect, useRef } from "react";
22

33
import {
4+
type AccountUsage,
5+
checkAccountUsage,
46
checkLiveUsage,
57
getPreference,
68
getTokenUsageStats,
79
isTauriAvailable,
810
listProviderAccounts,
11+
listSessions,
912
type LiveUsageResult,
1013
type ProviderAccount,
1114
sendTrayNotification,
15+
type SessionRow,
1216
setTrayMenu,
1317
setTrayText,
1418
} from "@/lib/tauri-ipc";
@@ -24,6 +28,11 @@ const SUPPORTED_PROVIDERS = new Set(["anthropic", "openai", "google"]);
2428
// notification. Each (accountId × window × threshold) only fires once per app
2529
// process — the ref below holds the latest threshold we've already notified.
2630
const NOTIFY_THRESHOLDS = [75, 90, 100];
31+
const WEEKLY_USAGE_NOTIFY_THRESHOLDS = [90, 100];
32+
const WEEKLY_PACE_AHEAD_NOTIFY_PCT = 50;
33+
const MIN_WEEKLY_PACE_NOTIFY_PCT = 10;
34+
const SESSION_USAGE_NOTIFY_THRESHOLD = 90;
35+
const ACTIVE_SESSION_WINDOW_MS = 15 * 60 * 1000;
2736

2837
function formatTokens(n: number): string {
2938
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(2)}B`;
@@ -110,6 +119,60 @@ function buildTrayLine(
110119
return `${label}${plan}${live.status ?? "ok"}`;
111120
}
112121

122+
function inferContextLimitTokens(model: string | null, agentType: string): number | null {
123+
const normalized = (model ?? "").toLowerCase();
124+
125+
if (normalized.includes("claude") || agentType === "claude-code") {
126+
return 200_000;
127+
}
128+
if (normalized.includes("gpt-4.1") || normalized.includes("gpt-4o")) {
129+
return 128_000;
130+
}
131+
if (
132+
normalized === "o3" ||
133+
normalized.startsWith("o3-") ||
134+
normalized === "o4-mini"
135+
) {
136+
return 200_000;
137+
}
138+
if (normalized.includes("gemini-1.5") || normalized.includes("gemini-2")) {
139+
return 1_000_000;
140+
}
141+
142+
return null;
143+
}
144+
145+
function sessionUsagePct(session: SessionRow): number | null {
146+
const limit = inferContextLimitTokens(session.model_used, session.agent_type);
147+
if (!limit) return null;
148+
const total = session.total_input_tokens + session.total_output_tokens;
149+
if (total <= 0) return null;
150+
return (total / limit) * 100;
151+
}
152+
153+
function sessionLabel(session: SessionRow): string {
154+
if (session.cwd) {
155+
const parts = session.cwd.split("/").filter(Boolean);
156+
const name = parts.at(-1);
157+
if (name) return name;
158+
}
159+
if (session.slug) return session.slug;
160+
return `${session.agent_type} session`;
161+
}
162+
163+
function isRecentlyActiveSession(session: SessionRow): boolean {
164+
if (!session.last_message) return false;
165+
const lastMessageMs = new Date(session.last_message).getTime();
166+
if (!Number.isFinite(lastMessageMs)) return false;
167+
return Date.now() - lastMessageMs <= ACTIVE_SESSION_WINDOW_MS;
168+
}
169+
170+
function weeklyPaceAheadPct(usage: AccountUsage): number | null {
171+
if (usage.week_pct == null || usage.expected_pct <= 0) return null;
172+
if (usage.week_pct < MIN_WEEKLY_PACE_NOTIFY_PCT) return null;
173+
return ((usage.week_pct - usage.expected_pct) / usage.expected_pct) * 100;
174+
}
175+
113176
async function loadCadenceSecs(): Promise<number> {
114177
if (!isTauriAvailable()) return DEFAULT_CADENCE_SECS;
115178
try {
@@ -133,6 +196,16 @@ async function loadNotificationsEnabled(): Promise<boolean> {
133196
}
134197
}
135198

199+
async function loadSessionNotificationsEnabled(): Promise<boolean> {
200+
if (!isTauriAvailable()) return true;
201+
try {
202+
const raw = await getPreference("notify_session_usage_thresholds");
203+
return raw !== "false";
204+
} catch {
205+
return true;
206+
}
207+
}
208+
136209
/**
137210
* Mounts a single global tray monitor at App level so the menu-bar icon stays
138211
* fresh regardless of which page the user is on. Polls accounts + live usage
@@ -142,6 +215,9 @@ export function useTrayMonitor(): void {
142215
// Persist last-notified threshold per accountId across re-renders so we
143216
// don't re-fire the same notification on every poll.
144217
const lastNotifiedRef = useRef<Record<string, number>>({});
218+
const weeklyUsageNotifiedRef = useRef<Record<string, number>>({});
219+
const weeklyPaceNotifiedRef = useRef<Record<string, number>>({});
220+
const sessionUsageNotifiedRef = useRef<Record<string, number>>({});
145221

146222
useEffect(() => {
147223
if (!isTauriAvailable()) return;
@@ -170,6 +246,14 @@ export function useTrayMonitor(): void {
170246
if (r.status === "fulfilled") liveMap[supported[i].id] = r.value;
171247
});
172248

249+
const usageResults = await Promise.allSettled(
250+
accounts.map((a) => checkAccountUsage(a.id))
251+
);
252+
const usageMap: Record<string, AccountUsage> = {};
253+
usageResults.forEach((r, i) => {
254+
if (r.status === "fulfilled") usageMap[accounts[i].id] = r.value;
255+
});
256+
173257
// Today's tokens (separate query; cheap local SQLite call).
174258
const tokenUsage = await getTokenUsageStats().catch(() => null);
175259

@@ -207,6 +291,68 @@ export function useTrayMonitor(): void {
207291
`Worst window utilization: ${Math.round(pct)}%`
208292
).catch(() => {});
209293
}
294+
295+
for (const a of accounts) {
296+
const usage = usageMap[a.id];
297+
if (!usage) continue;
298+
const label = a.name || a.provider;
299+
300+
if (usage.week_pct != null) {
301+
const crossed = WEEKLY_USAGE_NOTIFY_THRESHOLDS.filter(
302+
(t) => usage.week_pct != null && usage.week_pct >= t
303+
).pop();
304+
const last = weeklyUsageNotifiedRef.current[a.id] ?? 0;
305+
if (crossed && crossed > last) {
306+
weeklyUsageNotifiedRef.current[a.id] = crossed;
307+
const verb = crossed >= 100 ? "over weekly baseline" : `at ${crossed}% weekly`;
308+
await sendTrayNotification(
309+
`${label} ${verb}`,
310+
`Weekly usage is ${Math.round(usage.week_pct)}% of baseline.`
311+
).catch(() => {});
312+
}
313+
}
314+
315+
const aheadPct = weeklyPaceAheadPct(usage);
316+
const paceLast = weeklyPaceNotifiedRef.current[a.id] ?? 0;
317+
if (
318+
aheadPct != null &&
319+
aheadPct >= WEEKLY_PACE_AHEAD_NOTIFY_PCT &&
320+
paceLast < WEEKLY_PACE_AHEAD_NOTIFY_PCT
321+
) {
322+
weeklyPaceNotifiedRef.current[a.id] = WEEKLY_PACE_AHEAD_NOTIFY_PCT;
323+
await sendTrayNotification(
324+
`${label} is ahead of weekly pace`,
325+
`Usage is ${Math.round(aheadPct)}% ahead of schedule (${Math.round(
326+
usage.week_pct ?? 0
327+
)}% used vs ${Math.round(usage.expected_pct)}% expected).`
328+
).catch(() => {});
329+
}
330+
}
331+
}
332+
333+
// ── Session context-usage notifications ────────────────────────
334+
const sessionNotifyEnabled = await loadSessionNotificationsEnabled();
335+
if (sessionNotifyEnabled) {
336+
const sessions = await listSessions(undefined, undefined, 25).catch(
337+
() => [] as SessionRow[]
338+
);
339+
for (const session of sessions) {
340+
if (!isRecentlyActiveSession(session)) continue;
341+
const pct = sessionUsagePct(session);
342+
if (pct == null || pct < SESSION_USAGE_NOTIFY_THRESHOLD) continue;
343+
const last = sessionUsageNotifiedRef.current[session.id] ?? 0;
344+
if (last >= SESSION_USAGE_NOTIFY_THRESHOLD) continue;
345+
sessionUsageNotifiedRef.current[session.id] =
346+
SESSION_USAGE_NOTIFY_THRESHOLD;
347+
348+
const total = session.total_input_tokens + session.total_output_tokens;
349+
await sendTrayNotification(
350+
`${sessionLabel(session)} is near its session limit`,
351+
`${Math.round(pct)}% used (${formatTokens(total)} tokens) for ${
352+
session.model_used ?? session.agent_type
353+
}.`
354+
).catch(() => {});
355+
}
210356
}
211357
} catch {
212358
// Swallow — never let a failed poll break the loop.

apps/desktop/src/pages/Settings.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,7 @@ export default function Settings() {
573573
const [notifyTaskComplete, toggleNotifyTaskComplete] = useBoolPref("notify_task_complete", false);
574574
const [notificationSound, toggleNotificationSound] = useBoolPref("notification_sound", true);
575575
const [notifyQuotaThresholds, toggleNotifyQuotaThresholds] = useBoolPref("notify_quota_thresholds", true);
576+
const [notifySessionUsageThresholds, toggleNotifySessionUsageThresholds] = useBoolPref("notify_session_usage_thresholds", true);
576577

577578
// Menu-bar tray
578579
const [trayCadence, setTrayCadence] = usePref("tray_refresh_cadence_secs", "120");
@@ -873,13 +874,22 @@ export default function Settings() {
873874

874875
<Toggle
875876
label="Provider Quota Thresholds"
876-
description="Notify when any provider window crosses 75%, 90%, or 100% utilization."
877+
description="Notify when provider windows, weekly baselines, or weekly pace cross important thresholds."
877878
enabled={notifyQuotaThresholds}
878879
onToggle={toggleNotifyQuotaThresholds}
879880
/>
880881

881882
<Divider />
882883

884+
<Toggle
885+
label="Session Usage Thresholds"
886+
description="Notify when an active session crosses 90% of its inferred context window."
887+
enabled={notifySessionUsageThresholds}
888+
onToggle={toggleNotifySessionUsageThresholds}
889+
/>
890+
891+
<Divider />
892+
883893
<Toggle
884894
label="Notification Sounds"
885895
description="Play a short tone for success, error, and info events."

0 commit comments

Comments
 (0)