claude-discover 1.10.5 → 1.10.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@ import { styleText } from 'node:util'
10
10
  // Formatting
11
11
  const THRESHOLDS = { // (white, yellow, red)
12
12
  context: [0, 100_000, 250_000],
13
- limit: [0, 60, 90],
13
+ limit: [0, 60, 90], // keep in sync with src/renderer/utils/thresholds.js
14
14
  cache: [90, 60, 0],
15
15
  tokens: [0, 250_000, 1_000_000],
16
16
  session: [0, 3_000_000, 8_000_000],
package/out/main/main.js CHANGED
@@ -1,4 +1,4 @@
1
- import { app, dialog, BrowserWindow, shell, nativeTheme, Menu, ipcMain } from "electron";
1
+ import { app, shell, dialog, BrowserWindow, nativeTheme, Menu, ipcMain } from "electron";
2
2
  import { EventEmitter } from "node:events";
3
3
  import os, { homedir } from "node:os";
4
4
  import path, { join, dirname, basename } from "node:path";
@@ -6,12 +6,12 @@ import fs from "node:fs";
6
6
  import { sync } from "write-file-atomic";
7
7
  import fsp from "node:fs/promises";
8
8
  import crypto from "node:crypto";
9
+ import { pathToFileURL, fileURLToPath } from "node:url";
9
10
  import which from "which";
10
11
  import { execFile, spawn } from "node:child_process";
11
12
  import { promisify } from "node:util";
12
13
  import latestVersion from "latest-version";
13
14
  import semver from "semver";
14
- import { pathToFileURL, fileURLToPath } from "node:url";
15
15
  import windowStateKeeper from "electron-window-state";
16
16
  import contextMenu from "electron-context-menu";
17
17
  import { FindOverlay } from "electron-find-overlay";
@@ -680,21 +680,16 @@ function extractText(content) {
680
680
  if (!Array.isArray(content)) return "";
681
681
  return content.map((b) => typeof b === "string" ? b : b?.type === "text" ? b.text : "").filter(Boolean).join("\n");
682
682
  }
683
- function shortProject(dir, depth = 2) {
683
+ function shortProject(dir, depth) {
684
684
  if (!dir) return "(no project)";
685
685
  const parts = dir.replace(/\\/g, "/").split("/").filter(Boolean);
686
686
  return parts.slice(-depth).join("/") || dir;
687
687
  }
688
- const TEMP_ROOT = /^(.*?\/AppData\/Local\/Temp|\/(?:private\/)?(?:tmp|var\/folders\/[^/]+\/[^/]+\/T))\/(.+)/i;
689
- function tempProject(dir) {
690
- const m = dir?.replace(/\\/g, "/").match(TEMP_ROOT);
691
- return m ? { project: dir.slice(0, m[1].length), tag: m[2] } : null;
692
- }
693
- function suffixLabels(dirs, depth = 2) {
694
- const labels = new Map([...new Set(dirs)].map((dir) => [dir, shortProject(dir, depth)]));
688
+ function suffixLabels(dirs) {
689
+ const labels = new Map([...new Set(dirs)].map((dir) => [dir, shortProject(dir, 1)]));
695
690
  const counts = /* @__PURE__ */ new Map();
696
691
  for (const label of labels.values()) counts.set(label, (counts.get(label) ?? 0) + 1);
697
- for (const [dir, label] of labels) if (counts.get(label) > 1) labels.set(dir, shortProject(dir, depth + 1));
692
+ for (const [dir, label] of labels) if (counts.get(label) > 1) labels.set(dir, shortProject(dir, 2));
698
693
  return labels;
699
694
  }
700
695
  function classifySource(meta) {
@@ -715,10 +710,6 @@ function freshMeta({ sessionId, parentSessionId, filePath, parentFilePath, fileS
715
710
  continuesTo: null,
716
711
  entrypoint: null,
717
712
  project: null,
718
- worktree: null,
719
- worktreePath: null,
720
- tempPath: null,
721
- tempTag: null,
722
713
  gitBranch: null,
723
714
  version: null,
724
715
  model: null,
@@ -937,19 +928,6 @@ class SessionParser {
937
928
  if (meta.startedAt == null) meta.startedAt = mtimeFallback;
938
929
  if (meta.lastActivityAt == null) meta.lastActivityAt = mtimeFallback;
939
930
  meta.source = classifySource(meta);
940
- const wt = meta.project && meta.project.replace(/\\/g, "/").match(/\/\.claude\/worktrees\/([^/]+)/);
941
- if (wt) {
942
- meta.worktree = wt[1];
943
- meta.worktreePath = meta.project.slice(0, wt.index + wt[0].length);
944
- meta.project = meta.project.slice(0, wt.index);
945
- }
946
- const tmp = tempProject(meta.project);
947
- if (tmp) {
948
- meta.tempPath = meta.project;
949
- meta.tempTag = tmp.tag;
950
- meta.project = tmp.project;
951
- }
952
- meta.projectShort = shortProject(meta.project);
953
931
  meta.activeMs = meta.activityPeriods.reduce((sum, p) => sum + Math.max(0, p.end - p.start), 0);
954
932
  const t = meta.tokens;
955
933
  meta.totalTokens = t.input + t.output + t.cacheRead + t.cacheCreation;
@@ -1235,6 +1213,52 @@ class MetaCache {
1235
1213
  return this.periodMetas.delete(filePath);
1236
1214
  }
1237
1215
  }
1216
+ const WORKTREE = /\/\.claude\/worktrees\/([^/]+)/;
1217
+ const TEMP_ROOT = /^(.*?\/AppData\/Local\/Temp|\/(?:private\/)?(?:tmp|var\/folders\/[^/]+\/[^/]+\/T))\/(.+)/i;
1218
+ const WINDOWS = process.platform === "win32";
1219
+ const LOCAL_DIR = WINDOWS ? /^[a-z]:[\\/]/i : /^\//;
1220
+ class ProjectResolver {
1221
+ #repoRoots = /* @__PURE__ */ new Map();
1222
+ // `subdir` is where in the project the session ran: a repo or worktree subfolder, or a temp run dir
1223
+ async resolve(cwd) {
1224
+ const out = { project: cwd, subdir: null, worktree: null, worktreePath: null, tempPath: null };
1225
+ const wt = cwd?.replace(/\\/g, "/").match(WORKTREE);
1226
+ if (wt) {
1227
+ out.worktree = wt[1];
1228
+ out.worktreePath = cwd.slice(0, wt.index + wt[0].length);
1229
+ out.subdir = cwd.slice(out.worktreePath.length + 1).replace(/\\/g, "/") || null;
1230
+ out.project = cwd.slice(0, wt.index);
1231
+ }
1232
+ const tmp = out.project?.replace(/\\/g, "/").match(TEMP_ROOT);
1233
+ if (tmp) {
1234
+ out.tempPath = out.project;
1235
+ out.subdir = tmp[2];
1236
+ out.project = out.project.slice(0, tmp[1].length);
1237
+ return out;
1238
+ }
1239
+ const root = await this.repoRoot(out.project);
1240
+ if (root && root !== out.project) {
1241
+ out.subdir = [path.relative(root, out.project).replaceAll("\\", "/"), out.subdir].filter(Boolean).join("/");
1242
+ out.project = root;
1243
+ }
1244
+ return out;
1245
+ }
1246
+ // The nearest ancestor (or dir itself) holding a .git — a dir, or a file in worktrees/submodules — else null
1247
+ repoRoot(dir) {
1248
+ if (!LOCAL_DIR.test(dir ?? "")) return Promise.resolve(null);
1249
+ if (!this.#repoRoots.has(dir)) this.#repoRoots.set(dir, this.#findRepoRoot(dir));
1250
+ return this.#repoRoots.get(dir);
1251
+ }
1252
+ async #findRepoRoot(dir) {
1253
+ const home = os.homedir();
1254
+ const isHome = WINDOWS ? (d) => d.toLowerCase() === home.toLowerCase() : (d) => d === home;
1255
+ for (let d = dir; ; d = path.dirname(d)) {
1256
+ if (isHome(d)) return null;
1257
+ if (await fsp.stat(path.join(d, ".git")).catch(() => null)) return d;
1258
+ if (path.dirname(d) === d) return null;
1259
+ }
1260
+ }
1261
+ }
1238
1262
  const seed = {
1239
1263
  "claude-opus-5": { "input": 5, "output": 25, "cacheRead": 0.5, "cacheWrite": 6.25, "cacheWrite1h": 10, "fastModeMplr": 2 },
1240
1264
  "claude-fable-5-1": { "input": 10, "output": 50, "cacheRead": 0.25, "cacheWrite": 12.5, "cacheWrite1h": 20 },
@@ -1381,6 +1405,7 @@ class SessionsService extends EventEmitter {
1381
1405
  this.pricing = new Pricing();
1382
1406
  this.throttleMs = throttleMs;
1383
1407
  this.metaCache = new MetaCache();
1408
+ this.projects = new ProjectResolver();
1384
1409
  this.activeDay = null;
1385
1410
  this.scanAbort = null;
1386
1411
  this.updateTimer = null;
@@ -1453,7 +1478,10 @@ class SessionsService extends EventEmitter {
1453
1478
  start() {
1454
1479
  this.pricing.on("update", () => this._reprice());
1455
1480
  this.scanner.watch({
1456
- onChange: (p, stat) => this._refresh(p, stat),
1481
+ onChange: (p, stat) => {
1482
+ this.emit("activity");
1483
+ this._refresh(p, stat);
1484
+ },
1457
1485
  onUnlink: (p) => this._evict(p)
1458
1486
  });
1459
1487
  return this;
@@ -1484,7 +1512,8 @@ class SessionsService extends EventEmitter {
1484
1512
  pricing: this.pricing
1485
1513
  });
1486
1514
  await reader.stream((obj) => parser.feed(obj));
1487
- return parser.finalize(stat.mtimeMs);
1515
+ const meta = parser.finalize(stat.mtimeMs);
1516
+ return Object.assign(meta, await this.projects.resolve(meta.project));
1488
1517
  }
1489
1518
  // Resume/fork copies prior message.ids verbatim. Walk earliest-first; if a
1490
1519
  // session's ids overlap one already counted, rescan it with the overlap
@@ -1506,7 +1535,7 @@ class SessionsService extends EventEmitter {
1506
1535
  return stripInternal(rescanned ? { ...m, ...rescanned } : m);
1507
1536
  }));
1508
1537
  const labels = suffixLabels(sessions.map((m) => m.project));
1509
- for (const m of sessions) m.projectShort = labels.get(m.project);
1538
+ for (const m of sessions) m.projectShort = m.tempPath ? "Temp" : labels.get(m.project);
1510
1539
  return sessions;
1511
1540
  }
1512
1541
  _dedupedDay(day) {
@@ -1571,6 +1600,53 @@ class WorkHours {
1571
1600
  return data;
1572
1601
  }
1573
1602
  }
1603
+ const VIEWABLE_EXT = /* @__PURE__ */ new Set([".md", ".txt", ".json", ".jsonl", ".log", ".csv", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".pdf"]);
1604
+ async function openLinkSafely(href, baseFile) {
1605
+ if (/^(https?:|mailto:)/i.test(href)) return shell.openExternal(href);
1606
+ if (!baseFile) return;
1607
+ let decoded;
1608
+ try {
1609
+ decoded = decodeURIComponent(href);
1610
+ } catch {
1611
+ return;
1612
+ }
1613
+ const baseDir = path.dirname(path.resolve(baseFile));
1614
+ const abs = path.resolve(baseDir, decoded);
1615
+ const rel = path.relative(baseDir, abs);
1616
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return;
1617
+ if (!VIEWABLE_EXT.has(path.extname(abs).toLowerCase())) return shell.showItemInFolder(abs);
1618
+ const err = await shell.openPath(abs);
1619
+ if (err) shell.showItemInFolder(abs);
1620
+ return err;
1621
+ }
1622
+ const DEV_ORIGIN = process.env.ELECTRON_RENDERER_URL && new URL(process.env.ELECTRON_RENDERER_URL).origin;
1623
+ const APP_PAGES = pathToFileURL(path.join(import.meta.dirname, "../renderer/")).href;
1624
+ function lockNavigation(contents) {
1625
+ const toBrowser = (url) => /^(https?|mailto):/i.test(url) && shell.openExternal(url);
1626
+ const block = (e, url) => {
1627
+ const { href, origin } = new URL(url);
1628
+ if (href.startsWith(APP_PAGES) || origin === DEV_ORIGIN) return;
1629
+ e.preventDefault();
1630
+ toBrowser(url);
1631
+ };
1632
+ contents.setWindowOpenHandler(({ url }) => {
1633
+ toBrowser(url);
1634
+ return { action: "deny" };
1635
+ });
1636
+ contents.on("will-navigate", block);
1637
+ contents.on("will-redirect", block);
1638
+ }
1639
+ async function withTimeout(promise, ms, cleanup) {
1640
+ const { promise: timeout, reject } = Promise.withResolvers();
1641
+ const timer = setTimeout(reject, ms, new Error(`timed out after ${ms / 1e3}s`));
1642
+ try {
1643
+ return await Promise.race([promise, timeout]);
1644
+ } finally {
1645
+ clearTimeout(timer);
1646
+ cleanup?.();
1647
+ }
1648
+ }
1649
+ const USAGE_TIMEOUT = 3e4;
1574
1650
  class AgentRunner {
1575
1651
  async run(text, sender) {
1576
1652
  const { query } = await import("@anthropic-ai/claude-agent-sdk");
@@ -1597,6 +1673,14 @@ class AgentRunner {
1597
1673
  }
1598
1674
  return { code: 0 };
1599
1675
  }
1676
+ // The structured /usage data, from a session that's closed whether it answers, fails or hangs
1677
+ async usage() {
1678
+ const { query } = await import("@anthropic-ai/claude-agent-sdk");
1679
+ const q = query({ prompt: (async function* () {
1680
+ })(), options: { tools: [], settingSources: [] } });
1681
+ const usage = q.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET({ skipBehaviors: true });
1682
+ return withTimeout(usage, USAGE_TIMEOUT, () => q.close());
1683
+ }
1600
1684
  }
1601
1685
  const mtimeOf = (path2) => fs.statSync(path2, { throwIfNoEntry: false })?.mtimeMs;
1602
1686
  class ClaudeSettings {
@@ -1612,13 +1696,13 @@ class ClaudeSettings {
1612
1696
  }
1613
1697
  }
1614
1698
  }
1615
- // Days Claude Code keeps transcripts before auto-deleting them; undefined when unset (its default is 30).
1616
- get cleanupPeriodDays() {
1617
- return this.cfg.cleanupPeriodDays;
1699
+ // Any top-level setting; undefined when unset, and setting undefined removes the key.
1700
+ get(key) {
1701
+ return this.cfg[key];
1618
1702
  }
1619
- set cleanupPeriodDays(days) {
1620
- if (days === void 0) delete this.cfg.cleanupPeriodDays;
1621
- else this.cfg.cleanupPeriodDays = days;
1703
+ set(key, value) {
1704
+ if (value === void 0) delete this.cfg[key];
1705
+ else this.cfg[key] = value;
1622
1706
  }
1623
1707
  // Claude Code's status line config: { type: 'command', command }; undefined when unset.
1624
1708
  get statusLine() {
@@ -1800,7 +1884,8 @@ class ProxySwitch {
1800
1884
  } catch {
1801
1885
  throw new Error("Cannot reach api.anthropic.com — not enabling capture. Check your network and try again.");
1802
1886
  }
1803
- await this.#exit();
1887
+ if (!await this.#exit())
1888
+ throw new Error(`The running proxy on ${PROXY_URL} did not exit — try again.`);
1804
1889
  await this.#service.install();
1805
1890
  if (!await this.#settle(true)) {
1806
1891
  await this.#service.uninstall().catch(() => {
@@ -1820,17 +1905,17 @@ class ProxySwitch {
1820
1905
  settings.save();
1821
1906
  await new Promise((r) => setTimeout(r, SETTINGS_RELOAD_GRACE_MS));
1822
1907
  }
1823
- await this.#exit();
1908
+ const exited = await this.#exit();
1824
1909
  await this.#service.uninstall();
1910
+ if (!exited) throw new Error(`The proxy on ${PROXY_URL} did not exit and still holds the port.`);
1825
1911
  }
1826
- // Ask a running instance to exit via its control route and wait for the port to free up
1912
+ // Ask a running instance to exit via its control route; true once the port is free
1827
1913
  async #exit() {
1828
1914
  try {
1829
1915
  await fetch(`${PROXY_URL}${EXIT_ROUTE}`, { method: "POST", signal: AbortSignal.timeout(1e3) });
1830
1916
  } catch {
1831
- return;
1832
1917
  }
1833
- await this.#settle(false);
1918
+ return this.#settle(false);
1834
1919
  }
1835
1920
  async #running() {
1836
1921
  try {
@@ -1876,28 +1961,37 @@ class StatuslineSwitch {
1876
1961
  }
1877
1962
  }
1878
1963
  }
1879
- const DEFAULT_DAYS = 30;
1880
- const RETAIN_DAYS = 365;
1881
- class RetentionSwitch {
1964
+ const SETTINGS = [
1965
+ { key: "cleanupPeriodDays", value: 365, fallback: 30, isOn: (days) => days >= 365, oneWay: true },
1966
+ // this app can only browse transcripts Claude Code hasn't swept yet; lowering it would let the next sweep delete them for good
1967
+ { key: "showThinkingSummaries", value: true, fallback: false }
1968
+ // otherwise transcripts hold only empty thinking blocks
1969
+ ];
1970
+ const isOn = (s, settings) => {
1971
+ const value = settings.get(s.key) ?? s.fallback;
1972
+ return s.isOn ? s.isOn(value) : value === s.value;
1973
+ };
1974
+ class ClaudeSettingsSwitch {
1882
1975
  status() {
1883
- const days = new ClaudeSettings().cleanupPeriodDays ?? DEFAULT_DAYS;
1884
- return { days, raised: days >= RETAIN_DAYS };
1976
+ const settings = new ClaudeSettings();
1977
+ return { applied: SETTINGS.filter((s) => isOn(s, settings)).length, total: SETTINGS.length };
1885
1978
  }
1886
- // Raise retention to RETAIN_DAYS; an already-higher value is left as is.
1979
+ // A value that already counts as on (e.g. a longer retention) is left as is
1887
1980
  activate() {
1888
1981
  const settings = new ClaudeSettings();
1889
- if ((settings.cleanupPeriodDays ?? DEFAULT_DAYS) < RETAIN_DAYS) {
1890
- settings.cleanupPeriodDays = RETAIN_DAYS;
1891
- settings.save();
1982
+ const off = SETTINGS.filter((s) => !isOn(s, settings));
1983
+ for (const s of off) {
1984
+ settings.set(s.key, s.value);
1892
1985
  }
1986
+ if (off.length) settings.save();
1893
1987
  }
1894
- // Remove the setting — back to Claude Code's default.
1895
1988
  deactivate() {
1896
1989
  const settings = new ClaudeSettings();
1897
- if (settings.cleanupPeriodDays !== void 0) {
1898
- settings.cleanupPeriodDays = void 0;
1899
- settings.save();
1990
+ const set = SETTINGS.filter((s) => !s.oneWay && settings.get(s.key) !== void 0);
1991
+ for (const s of set) {
1992
+ settings.set(s.key, void 0);
1900
1993
  }
1994
+ if (set.length) settings.save();
1901
1995
  }
1902
1996
  }
1903
1997
  class ClaudeDirSwitch {
@@ -1934,7 +2028,7 @@ class Switchers {
1934
2028
  this.#switches = {
1935
2029
  proxy: new ProxySwitch(),
1936
2030
  statusline: new StatuslineSwitch(),
1937
- retention: new RetentionSwitch(),
2031
+ settings: new ClaudeSettingsSwitch(),
1938
2032
  claudedir: new ClaudeDirSwitch({ restart })
1939
2033
  };
1940
2034
  }
@@ -2000,41 +2094,65 @@ class AutoUpdate {
2000
2094
  return semver.gt(latest, current) ? { current, latest } : null;
2001
2095
  }
2002
2096
  }
2003
- const VIEWABLE_EXT = /* @__PURE__ */ new Set([".md", ".txt", ".json", ".jsonl", ".log", ".csv", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".pdf"]);
2004
- async function openLinkSafely(href, baseFile) {
2005
- if (/^(https?:|mailto:)/i.test(href)) return shell.openExternal(href);
2006
- if (!baseFile) return;
2007
- let decoded;
2008
- try {
2009
- decoded = decodeURIComponent(href);
2010
- } catch {
2011
- return;
2097
+ const EVERY = 6e4;
2098
+ const toWindow = (w) => w?.utilization == null ? null : {
2099
+ utilization: w.utilization,
2100
+ // 0..100
2101
+ resetsAt: w.resets_at ? Math.round(Date.parse(w.resets_at) / 6e4) * 6e4 : null
2102
+ // the server sends 16:29:59.72 for 16:30
2103
+ };
2104
+ const toExtra = (e) => e && {
2105
+ on: e.is_enabled,
2106
+ reason: !e.is_enabled && !e.user_disabled ? e.disabled_reason : null,
2107
+ // e.g. 'out_of_credits'
2108
+ used: e.used_credits,
2109
+ limit: e.monthly_limit,
2110
+ currency: e.currency,
2111
+ decimals: e.decimal_places ?? 2
2112
+ };
2113
+ class RateLimits {
2114
+ #agentRunner;
2115
+ #limits = null;
2116
+ // the latest fetch's promise, shared by concurrent callers
2117
+ #fetchedAt = 0;
2118
+ #active = false;
2119
+ // a transcript changed since the last fetch
2120
+ constructor(agentRunner) {
2121
+ this.#agentRunner = agentRunner;
2122
+ }
2123
+ // A transcript changed
2124
+ markActive() {
2125
+ this.#active = true;
2126
+ }
2127
+ // Refetched once over a minute old; with `ifActive`, only if Claude Code wrote a transcript since
2128
+ get({ ifActive = false } = {}) {
2129
+ if (Date.now() - this.#fetchedAt >= EVERY && (this.#active || !ifActive)) {
2130
+ this.#fetchedAt = Date.now();
2131
+ this.#active = false;
2132
+ this.#limits = this.#fetch();
2133
+ }
2134
+ return this.#limits;
2135
+ }
2136
+ async #fetch() {
2137
+ try {
2138
+ const { subscription_type: plan, rate_limits: rl } = await this.#agentRunner.usage();
2139
+ return rl && {
2140
+ at: Date.now(),
2141
+ plan,
2142
+ // 'pro' | 'max' | 'team' | 'enterprise'
2143
+ fiveHour: toWindow(rl.five_hour),
2144
+ sevenDay: toWindow(rl.seven_day),
2145
+ models: (rl.model_scoped ?? []).filter((m) => m.utilization != null).map((m) => ({ name: m.display_name, ...toWindow(m) })),
2146
+ // per-model weekly caps
2147
+ sources: (rl.seven_day_breakdown?.rows ?? []).filter((r) => r.percent > 0).map((r) => ({ name: r.display_name, pct: r.percent })),
2148
+ // undocumented: Claude Code / Chats / Cowork shares of the week
2149
+ extra: toExtra(rl.extra_usage)
2150
+ };
2151
+ } catch (err) {
2152
+ console.warn("[rate-limits]", err.message);
2153
+ return null;
2154
+ }
2012
2155
  }
2013
- const baseDir = path.dirname(path.resolve(baseFile));
2014
- const abs = path.resolve(baseDir, decoded);
2015
- const rel = path.relative(baseDir, abs);
2016
- if (rel.startsWith("..") || path.isAbsolute(rel)) return;
2017
- if (!VIEWABLE_EXT.has(path.extname(abs).toLowerCase())) return shell.showItemInFolder(abs);
2018
- const err = await shell.openPath(abs);
2019
- if (err) shell.showItemInFolder(abs);
2020
- return err;
2021
- }
2022
- const DEV_ORIGIN = process.env.ELECTRON_RENDERER_URL && new URL(process.env.ELECTRON_RENDERER_URL).origin;
2023
- const APP_PAGES = pathToFileURL(path.join(import.meta.dirname, "../renderer/")).href;
2024
- function lockNavigation(contents) {
2025
- const toBrowser = (url) => /^(https?|mailto):/i.test(url) && shell.openExternal(url);
2026
- const block = (e, url) => {
2027
- const { href, origin } = new URL(url);
2028
- if (href.startsWith(APP_PAGES) || origin === DEV_ORIGIN) return;
2029
- e.preventDefault();
2030
- toBrowser(url);
2031
- };
2032
- contents.setWindowOpenHandler(({ url }) => {
2033
- toBrowser(url);
2034
- return { action: "deny" };
2035
- });
2036
- contents.on("will-navigate", block);
2037
- contents.on("will-redirect", block);
2038
2156
  }
2039
2157
  const __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
2040
2158
  const DEV_URL = process.env.ELECTRON_RENDERER_URL;
@@ -2072,6 +2190,7 @@ class MainWindow {
2072
2190
  this.findBar = new FindOverlay(this.win);
2073
2191
  this.findBar.on("show", () => this.send("find:active", true));
2074
2192
  this.findBar.on("hide", () => this.send("find:active", false));
2193
+ this.win.on("closed", () => this.findBar = null);
2075
2194
  return this.win;
2076
2195
  }
2077
2196
  send(channel, payload) {
@@ -2092,13 +2211,15 @@ class Application {
2092
2211
  this.workHours = new WorkHours();
2093
2212
  this.sessionsService = new SessionsService();
2094
2213
  this.autoUpdate = new AutoUpdate();
2214
+ this.rateLimits = new RateLimits(this.agentRunner);
2095
2215
  this.switchers = new Switchers({ restart: () => this.restart() });
2096
2216
  }
2097
2217
  start() {
2098
- const { deepLink, win, agentRunner, workHours, sessionsService, autoUpdate, switchers } = this;
2218
+ const { deepLink, win, agentRunner, workHours, sessionsService, autoUpdate, rateLimits, switchers } = this;
2099
2219
  Menu.setApplicationMenu(this.#buildMenu());
2100
2220
  sessionsService.on("update", (update) => win.send("sessions:update", update));
2101
2221
  sessionsService.on("progress", (p) => win.send("sessions:scan-progress", p));
2222
+ sessionsService.on("activity", () => rateLimits.markActive());
2102
2223
  deepLink?.on("open", (target) => {
2103
2224
  win.focus();
2104
2225
  win.send("deeplink:open-session", target);
@@ -2114,17 +2235,13 @@ class Application {
2114
2235
  ipcMain.handle("work-hours:set", (_e, data) => workHours.write(data));
2115
2236
  ipcMain.handle("auto-update:check", () => autoUpdate.check());
2116
2237
  ipcMain.handle("auto-update:install", () => autoUpdate.install());
2238
+ ipcMain.handle("rate-limits:get", (_e, opts) => rateLimits.get(opts));
2117
2239
  ipcMain.handle("agent:run", (e, text) => agentRunner.run(text, e.sender));
2118
2240
  ipcMain.handle("shell:open-link", (_e, href, baseFile) => openLinkSafely(href, baseFile));
2119
2241
  ipcMain.handle("deeplink:take-pending", () => deepLink?.takePending() ?? null);
2120
2242
  ipcMain.on("find:close", () => win.findBar?.hide());
2121
2243
  ipcMain.on("claude-settings:get", (e) => e.returnValue = { claudeDir: CLAUDE_DIR });
2122
- app.on("activate", () => {
2123
- if (BrowserWindow.getAllWindows().length === 0) win.create();
2124
- });
2125
- app.on("window-all-closed", () => {
2126
- if (process.platform !== "darwin") app.quit();
2127
- });
2244
+ app.on("window-all-closed", () => app.quit());
2128
2245
  app.on("will-quit", async (e) => {
2129
2246
  e.preventDefault();
2130
2247
  await switchers.deactivateOnQuit();
@@ -2170,6 +2287,8 @@ class Application {
2170
2287
  { role: "paste" },
2171
2288
  { role: "selectAll" },
2172
2289
  { label: "Find…", accelerator: "CmdOrCtrl+F", click: () => win.findBar?.show() },
2290
+ { label: "Find Next", accelerator: "CmdOrCtrl+G", click: () => win.findBar?.next() },
2291
+ { label: "Find Previous", accelerator: "CmdOrCtrl+Shift+G", click: () => win.findBar?.prev() },
2173
2292
  { label: "Deselect", accelerator: "Escape", click: (_i, w) => win.findBar?.visible ? win.findBar.hide() : w?.webContents.unselect() }
2174
2293
  ]
2175
2294
  },
@@ -24,6 +24,7 @@ electron.contextBridge.exposeInMainWorld("api", {
24
24
  onAgentOutput: subscribe("agent:output"),
25
25
  checkUpdate: () => electron.ipcRenderer.invoke("auto-update:check"),
26
26
  installUpdate: () => electron.ipcRenderer.invoke("auto-update:install"),
27
+ getRateLimits: (opts) => electron.ipcRenderer.invoke("rate-limits:get", opts),
27
28
  openLink: (href, baseFile) => electron.ipcRenderer.invoke("shell:open-link", href, baseFile),
28
29
  onFindActive: subscribe("find:active"),
29
30
  findClose: () => electron.ipcRenderer.send("find:close"),