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.
- package/bin/claude/statusline.mjs +1 -1
- package/out/main/main.js +216 -97
- package/out/preload/preload.cjs +1 -0
- package/out/renderer/assets/{index-CHnuzPLr.js → index-BnOl_kG3.js} +463 -222
- package/out/renderer/assets/{index-BMh5255F.css → index-DEYU-s3r.css} +247 -61
- package/out/renderer/assets/{vendors-CpMM-Zb0.js → vendors-C1QIL8dC.js} +2974 -3644
- package/out/renderer/assets/{vendors-B4XMnGdr.css → vendors-CXTqG1IV.css} +1 -1
- package/out/renderer/index.html +4 -4
- package/package.json +5 -5
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
689
|
-
|
|
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,
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
//
|
|
1616
|
-
get
|
|
1617
|
-
return this.cfg
|
|
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
|
|
1620
|
-
if (
|
|
1621
|
-
else this.cfg
|
|
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
|
|
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
|
-
|
|
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
|
|
1880
|
-
|
|
1881
|
-
|
|
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
|
|
1884
|
-
return {
|
|
1976
|
+
const settings = new ClaudeSettings();
|
|
1977
|
+
return { applied: SETTINGS.filter((s) => isOn(s, settings)).length, total: SETTINGS.length };
|
|
1885
1978
|
}
|
|
1886
|
-
//
|
|
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
|
-
|
|
1890
|
-
|
|
1891
|
-
settings.
|
|
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
|
-
|
|
1898
|
-
|
|
1899
|
-
settings.
|
|
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
|
-
|
|
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
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
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("
|
|
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
|
},
|
package/out/preload/preload.cjs
CHANGED
|
@@ -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"),
|