claude-discover 1.9.5 → 1.10.1

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/out/main/main.js CHANGED
@@ -6,7 +6,6 @@ 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 chokidar from "chokidar";
10
9
  import which from "which";
11
10
  import { execFile } from "node:child_process";
12
11
  import { promisify } from "node:util";
@@ -52,6 +51,7 @@ class DeepLink extends EventEmitter {
52
51
  });
53
52
  this.#register();
54
53
  this.#pending = findTarget(process.argv);
54
+ return this;
55
55
  }
56
56
  open(target) {
57
57
  if (target) this.emit("open", target);
@@ -564,10 +564,10 @@ class RequestParser {
564
564
  for (const t of fresh) this.seenTools.add(t.name);
565
565
  const schema = (t) => t.input_schema ? "```json\n" + JSON.stringify(t.input_schema, null, 2) + "\n```" : "";
566
566
  const strip = (file_path, pick) => {
567
- const list = fresh.filter((t) => pick(t.name));
567
+ const list = fresh.filter((t) => pick(t.name)).map((t) => ({ name: t.name, content: [t.description || "", schema(t)].filter(Boolean).join("\n\n") }));
568
568
  if (!list.length) return null;
569
- const content = list.map((t) => [`## ${t.name}`, t.description || "", schema(t)].filter(Boolean).join("\n\n")).join("\n\n");
570
- return toStrip(rec, "tools", file_path, `${list.length} tool${list.length === 1 ? "" : "s"}`, content);
569
+ const content = list.map((t) => [`## ${t.name}`, t.content].filter(Boolean).join("\n\n")).join("\n\n");
570
+ return { ...toStrip(rec, "tools", file_path, `${list.length} tool${list.length === 1 ? "" : "s"}`, content), tools: list };
571
571
  };
572
572
  return [strip("System Tools", (n) => !isMcp(n)), strip("MCP Tools", isMcp)].filter(Boolean);
573
573
  }
@@ -710,6 +710,7 @@ function freshMeta({ sessionId, parentSessionId, filePath, parentFilePath, fileS
710
710
  aiTitle: null,
711
711
  customTitle: null,
712
712
  agentName: null,
713
+ tag: null,
713
714
  firstUserPrompt: null,
714
715
  firstUserCommand: null,
715
716
  forkedFrom: null,
@@ -747,6 +748,7 @@ class SessionParser {
747
748
  if (t === "ai-title" && obj.aiTitle) meta.aiTitle = obj.aiTitle;
748
749
  if (t === "custom-title" && obj.customTitle) meta.customTitle = obj.customTitle;
749
750
  if (t === "agent-name" && obj.agentName) meta.agentName = obj.agentName;
751
+ if (t === "tag") meta.tag = obj.tag || null;
750
752
  if (t === "queue-operation" && obj.content?.includes("<scheduled-task")) meta.hasScheduledTask = true;
751
753
  if (t === "started" && obj.key && obj.agentId) meta.workflowAgents += 1;
752
754
  if (obj.cwd && !meta.project) meta.project = obj.cwd;
@@ -943,25 +945,130 @@ class StatCache {
943
945
  onProgress?.({ done: 1, total: 1, scanning: false });
944
946
  }
945
947
  }
948
+ async function listFiles(dir, filter) {
949
+ const entries = await fsp.readdir(dir, { recursive: true, withFileTypes: true });
950
+ return entries.filter((e) => e.isFile() && filter(e.name)).map((e) => path.join(e.parentPath, e.name));
951
+ }
952
+ class FileWatcher {
953
+ #root;
954
+ #opts;
955
+ #watcher = null;
956
+ #closed = false;
957
+ constructor(root, opts) {
958
+ this.#root = root;
959
+ this.#opts = opts;
960
+ try {
961
+ this.#watcher = new EventFileWatcher(root, { ...opts, onFail: (err) => this.#fallBack(err) });
962
+ } catch (err) {
963
+ this.#fallBack(err);
964
+ }
965
+ }
966
+ get mode() {
967
+ return this.#watcher instanceof IntervalFileWatcher ? "interval" : "event";
968
+ }
969
+ // Its first diff against the baseline also catches whatever changed while events were down
970
+ #fallBack(err) {
971
+ if (this.#closed) return;
972
+ console.warn("[watch] checking on an interval instead:", err.message);
973
+ this.#watcher = new IntervalFileWatcher(this.#root, this.#opts);
974
+ }
975
+ close() {
976
+ this.#closed = true;
977
+ this.#watcher?.close();
978
+ }
979
+ }
980
+ class EventFileWatcher {
981
+ #opts;
982
+ #handle = null;
983
+ #timers = /* @__PURE__ */ new Map();
984
+ // filePath -> pending settle timer
985
+ #closed = false;
986
+ // Throws if the OS refuses the watch, e.g. on \\wsl.localhost\...
987
+ constructor(root, opts) {
988
+ this.#opts = { settleMs: 100, ...opts };
989
+ this.#handle = fs.watch(root, { recursive: true }, (_type, name) => {
990
+ if (!name) return this.#opts.onDrop();
991
+ if (this.#opts.filter(name)) this.#settle(path.join(root, name));
992
+ });
993
+ this.#handle.on("error", (err) => {
994
+ this.close();
995
+ this.#opts.onFail(err);
996
+ });
997
+ }
998
+ // Events carry no stat or kind, so stat each path once its writes settle
999
+ #settle(filePath) {
1000
+ const { onChange, onUnlink, settleMs } = this.#opts;
1001
+ clearTimeout(this.#timers.get(filePath));
1002
+ this.#timers.set(filePath, setTimeout(async () => {
1003
+ this.#timers.delete(filePath);
1004
+ const stat = await fsp.stat(filePath).catch(() => null);
1005
+ if (this.#closed) return;
1006
+ if (!stat) onUnlink(filePath);
1007
+ else if (stat.isFile()) onChange(filePath, stat);
1008
+ }, settleMs));
1009
+ }
1010
+ close() {
1011
+ this.#closed = true;
1012
+ for (const timer of this.#timers.values()) {
1013
+ clearTimeout(timer);
1014
+ }
1015
+ this.#timers.clear();
1016
+ this.#handle?.close();
1017
+ }
1018
+ }
1019
+ class IntervalFileWatcher {
1020
+ #root;
1021
+ #opts;
1022
+ #timer = null;
1023
+ #checks = 0;
1024
+ #closed = false;
1025
+ // Stats on a share are the bulk of a check, so each one stats only new files and those written within hotMs;
1026
+ // every sweepEvery-th check stats them all, catching an idle transcript that grows again (e.g. a resumed session)
1027
+ constructor(root, opts) {
1028
+ this.#root = root;
1029
+ this.#opts = { intervalMs: 2e3, hotMs: 10 * 6e4, sweepEvery: 30, ...opts };
1030
+ this.#schedule();
1031
+ }
1032
+ // Timed from the end of the last check, so a slow share can't pile them up
1033
+ #schedule() {
1034
+ this.#timer = setTimeout(() => this.#check().finally(() => this.#closed || this.#schedule()), this.#opts.intervalMs);
1035
+ }
1036
+ async #check() {
1037
+ const { filter, onChange, onUnlink, baseline } = this.#opts;
1038
+ const known = baseline();
1039
+ if (!known) return;
1040
+ const files = await listFiles(this.#root, filter).catch(() => null);
1041
+ if (!files) return;
1042
+ const sweep = ++this.#checks % this.#opts.sweepEvery === 0;
1043
+ const now = Date.now();
1044
+ await Promise.all(files.map(async (fp) => {
1045
+ const old = known.get(fp);
1046
+ if (!sweep && old && now - old.mtimeMs >= this.#opts.hotMs) return;
1047
+ const stat = await fsp.stat(fp).catch(() => null);
1048
+ if (stat && !this.#closed && (!old || old.size !== stat.size || old.mtimeMs !== stat.mtimeMs)) onChange(fp, stat);
1049
+ }));
1050
+ if (this.#closed) return;
1051
+ const listed = new Set(files);
1052
+ for (const fp of known.keys()) {
1053
+ if (!listed.has(fp)) onUnlink(fp);
1054
+ }
1055
+ }
1056
+ close() {
1057
+ this.#closed = true;
1058
+ clearTimeout(this.#timer);
1059
+ }
1060
+ }
946
1061
  const isJsonl = (p) => p.endsWith(".jsonl");
947
1062
  class SessionsScanner {
948
1063
  constructor({ root = CLAUDE_PROJECTS_DIR } = {}) {
949
1064
  this.root = root;
950
1065
  this.watcher = null;
951
- this.watchWanted = false;
952
- this.walkDone = Promise.withResolvers();
953
1066
  this.statCache = new StatCache();
954
1067
  }
955
1068
  async _listDirs(p) {
956
1069
  const entries = await fsp.readdir(p, { withFileTypes: true }).catch(() => []);
957
1070
  return entries.filter((e) => e.isDirectory()).map((e) => path.join(e.parentPath, e.name));
958
1071
  }
959
- // Every .jsonl under `dir`, at any depth — subagent transcripts nest arbitrarily deep. Subtrees
960
- // can't be pruned by directory mtime: on Windows, appending to a transcript doesn't bump its parent dir.
961
- async _listJsonl(dir) {
962
- const entries = await fsp.readdir(dir, { recursive: true, withFileTypes: true }).catch(() => []);
963
- return entries.filter((e) => e.isFile() && isJsonl(e.name)).map((e) => path.join(e.parentPath, e.name));
964
- }
965
1072
  // Append-only transcripts span [birthtime, mtime], so files outside the period are skipped
966
1073
  // Note birthtime is trusted only when well older than mtime, since copies/restores (git checkout, scp, zip) get a fresh birthtime over old content.
967
1074
  _inPeriod(stat, day) {
@@ -973,14 +1080,10 @@ class SessionsScanner {
973
1080
  // `signal` (a superseded scan) stops everything; once complete with the watcher live, the
974
1081
  // StatCache mirrors disk and serves later scans in memory.
975
1082
  async scan(day, opts = {}) {
976
- if (this.statCache.complete && this.watcher) {
1083
+ if (this.statCache.complete) {
977
1084
  return this.statCache.scan((stat) => this._inPeriod(stat, day), opts);
978
1085
  }
979
- try {
980
- await this._walk(day, opts);
981
- } finally {
982
- this.walkDone.resolve();
983
- }
1086
+ return this._walk(day, opts);
984
1087
  }
985
1088
  async _walk(day, { onFile, onBatchDone, onProgress, signal } = {}) {
986
1089
  const projects = await this._listDirs(this.root);
@@ -989,7 +1092,8 @@ class SessionsScanner {
989
1092
  progress();
990
1093
  await Promise.all(projects.map(async (projDir) => {
991
1094
  if (signal?.aborted) return;
992
- const results = await Promise.all((await this._listJsonl(projDir)).map(async (fp) => {
1095
+ const files = await listFiles(projDir, isJsonl).catch(() => []);
1096
+ const results = await Promise.all(files.map(async (fp) => {
993
1097
  const stat = signal?.aborted ? null : await fsp.stat(fp).catch(() => null);
994
1098
  this.statCache.record(fp, stat);
995
1099
  return stat && this._inPeriod(stat, day) && !signal?.aborted ? onFile?.(fp, stat) : null;
@@ -1004,38 +1108,28 @@ class SessionsScanner {
1004
1108
  if (onBatchDone) await onBatchDone();
1005
1109
  progress();
1006
1110
  }
1007
- // Starts chokidar once a scan has finished, and resolves when it's live. Its setup walks the whole
1008
- // projects dir to register watchers, and on a cold disk those stats are the bulk of startup, so it
1009
- // waits instead of racing our walk. The gap is self-healing: `ignoreInitial` skips files present at
1010
- // startup anyway, and a session still being written emits on its next append.
1011
- async watch({ onChange, onUnlink } = {}) {
1012
- this.watchWanted = true;
1013
- await this.walkDone.promise;
1014
- if (!this.watchWanted) return;
1015
- const isUNC = this.root.startsWith("\\\\") || this.root.startsWith("//");
1016
- this.watcher = chokidar.watch(this.root, {
1017
- ignoreInitial: true,
1018
- alwaysStat: true,
1019
- awaitWriteFinish: { stabilityThreshold: 400, pollInterval: 100 },
1020
- ignored: (p, stats) => !!stats?.isFile() && !isJsonl(p),
1021
- ...isUNC && { usePolling: true, interval: 1e3 }
1111
+ // Starts before the first walk, so that walk already completes the StatCache
1112
+ watch({ onChange, onUnlink }) {
1113
+ try {
1114
+ fs.mkdirSync(this.root, { recursive: true });
1115
+ } catch {
1116
+ }
1117
+ this.watcher = new FileWatcher(this.root, {
1118
+ filter: isJsonl,
1119
+ onChange: (p, stat) => {
1120
+ this.statCache.record(p, stat);
1121
+ onChange(p, stat);
1122
+ },
1123
+ onUnlink: (p) => {
1124
+ this.statCache.remove(p);
1125
+ onUnlink(p);
1126
+ },
1127
+ onDrop: () => this.statCache.clear(),
1128
+ baseline: () => this.statCache.complete ? this.statCache.stats : null
1129
+ // null until a full walk exists to diff against
1022
1130
  });
1023
- const changed = (p, stat) => {
1024
- if (!isJsonl(p)) return;
1025
- this.statCache.record(p, stat);
1026
- onChange?.(p, stat);
1027
- };
1028
- const removed = (p) => {
1029
- if (!isJsonl(p)) return;
1030
- this.statCache.remove(p);
1031
- onUnlink?.(p);
1032
- };
1033
- this.watcher.on("add", changed);
1034
- this.watcher.on("change", changed);
1035
- this.watcher.on("unlink", removed);
1036
1131
  }
1037
1132
  stop() {
1038
- this.watchWanted = false;
1039
1133
  if (this.watcher) {
1040
1134
  this.watcher.close();
1041
1135
  this.watcher = null;
@@ -1993,7 +2087,7 @@ class Application {
1993
2087
  Menu.setApplicationMenu(this.#buildMenu());
1994
2088
  sessionsService.on("update", (sessions) => win.send("sessions:update", sessions));
1995
2089
  sessionsService.on("progress", (p) => win.send("sessions:scan-progress", p));
1996
- deepLink.on("open", (target) => {
2090
+ deepLink?.on("open", (target) => {
1997
2091
  win.focus();
1998
2092
  win.send("deeplink:open-session", target);
1999
2093
  });
@@ -2008,7 +2102,7 @@ class Application {
2008
2102
  ipcMain.handle("work-hours:set", (_e, data) => workHours.write(data));
2009
2103
  ipcMain.handle("agent:run", (e, text, systemTools, cache) => agentRunner.run(text, e.sender, systemTools, cache));
2010
2104
  ipcMain.handle("shell:open-link", (_e, href, baseFile) => openLinkSafely(href, baseFile));
2011
- ipcMain.handle("deeplink:take-pending", () => deepLink.takePending());
2105
+ ipcMain.handle("deeplink:take-pending", () => deepLink?.takePending() ?? null);
2012
2106
  ipcMain.on("find:query", (_e, text, options) => win.findBar?.query(text, options));
2013
2107
  ipcMain.on("find:stop", () => win.findBar?.stop());
2014
2108
  ipcMain.on("find:close", () => win.findBar?.hide());
@@ -2078,11 +2172,41 @@ class Application {
2078
2172
  ]);
2079
2173
  }
2080
2174
  }
2175
+ const APP_ID = "claude-discover";
2176
+ class Shortcuts {
2177
+ // Call before any window is created, so the window picks up the app id.
2178
+ activate() {
2179
+ if (process.platform !== "win32") return;
2180
+ app.setAppUserModelId(APP_ID);
2181
+ if (IS_EPHEMERAL || false) return;
2182
+ const installed = app.getAppPath().split(/[\\/]/).includes("node_modules");
2183
+ const name = installed ? "Claude Discover" : "Claude Discover (local)";
2184
+ const created = config.read().shortcuts ?? {};
2185
+ const install = fs.statSync(app.getAppPath()).birthtimeMs;
2186
+ const newInstall = created[name] !== install;
2187
+ for (const dir of [join(app.getPath("appData"), "Microsoft/Windows/Start Menu/Programs"), app.getPath("desktop")]) {
2188
+ const path2 = join(dir, `${name}.lnk`);
2189
+ const exists = fs.existsSync(path2);
2190
+ if (!exists && !newInstall) continue;
2191
+ const written = shell.writeShortcutLink(path2, exists ? "replace" : "create", {
2192
+ target: process.execPath,
2193
+ args: `"${app.getAppPath()}"`,
2194
+ description: "Browse your local Claude Code sessions",
2195
+ icon: process.execPath,
2196
+ iconIndex: 0,
2197
+ appUserModelId: APP_ID
2198
+ });
2199
+ if (!written) throw new Error(`cannot write ${path2}`);
2200
+ }
2201
+ if (newInstall) config.save({ shortcuts: { ...created, [name]: install } });
2202
+ }
2203
+ }
2081
2204
  const isFirstInstance = app.requestSingleInstanceLock();
2082
2205
  if (isFirstInstance) {
2083
- let application;
2084
- const deepLink = new DeepLink();
2085
- deepLink.activate();
2206
+ let application = null;
2207
+ let deepLink = null;
2208
+ optional("deep links", () => deepLink = new DeepLink().activate());
2209
+ optional("shortcuts", () => new Shortcuts().activate());
2086
2210
  app.whenReady().then(() => {
2087
2211
  application = new Application({ deepLink });
2088
2212
  application.start();
@@ -2092,7 +2216,7 @@ if (isFirstInstance) {
2092
2216
  if (argv.includes("--restart")) {
2093
2217
  application?.restart();
2094
2218
  } else if (target) {
2095
- deepLink.open(target);
2219
+ deepLink?.open(target);
2096
2220
  } else {
2097
2221
  application?.win.focus();
2098
2222
  }
@@ -2100,3 +2224,10 @@ if (isFirstInstance) {
2100
2224
  } else {
2101
2225
  app.exit(0);
2102
2226
  }
2227
+ function optional(label, activate) {
2228
+ try {
2229
+ activate();
2230
+ } catch (err) {
2231
+ console.warn(`[${label}] skipped:`, err.message);
2232
+ }
2233
+ }
@@ -627,6 +627,8 @@
627
627
 
628
628
  .worktree-tag { color: #34d399; }
629
629
 
630
+ .session-tag { color: #4ade80; }
631
+
630
632
  .fork-tag {
631
633
  color: var(--accent);
632
634
  font-weight: bold;
@@ -1076,6 +1078,10 @@ button.msg-summary:hover { color: var(--text); background: var(--bg-elev-2); bor
1076
1078
  .instruction-source:first-child { padding-top: 0; }
1077
1079
 
1078
1080
  /* Full-width toggle so the estimated token count can sit right-aligned, like msg-header-right. */
1081
+ /* Nested tool rows cancel the aux-body's right padding so their counts line up with the strips'. */
1082
+ .instruction-file .instruction-file { border-left-color: var(--border); margin-right: -8px; }
1083
+ /* A rule under the strip's count marks it as the sum of the tool counts below. */
1084
+ .aux-body > .instruction-file:first-child .instruction-tokens { border-top: 1px solid var(--border); padding-top: 2px; }
1079
1085
  .instruction-file .aux-toggle { display: flex; width: 100%; align-items: center; }
1080
1086
  .instruction-tokens { margin-left: auto; color: var(--text-faint); }
1081
1087
 
@@ -1,4 +1,4 @@
1
- import { r as reactExports, j as jsxRuntimeExports, f as format, s as startOfWeek, a as startOfMonth, b as startOfDay, e as endOfWeek, c as endOfMonth, d as endOfDay, g as addWeeks, h as addMonths, i as addDays, k as isSameWeek, l as isSameMonth, m as isSameDay, n as intervalToDuration, o as getDaysInMonth, p as addHours, q as encode, H as Hourglass, G as Gauge, M as MessageSquare, C as Clock, Z as Zap, D as DollarSign, R as React, t as Markdown$1, u as rehypeHighlight, v as remarkGfm, T as Terminal, J as JsonView, w as vscodeTheme, x as tippy, y as reactDomExports, z as an, A as qt, Q as Qt, B as nn, E as parse, F as delegate, I as clientExports } from "./vendors-JM3FOKMR.js";
1
+ import { r as reactExports, j as jsxRuntimeExports, f as format, s as startOfWeek, a as startOfMonth, b as startOfDay, e as endOfWeek, c as endOfMonth, d as endOfDay, g as addWeeks, h as addMonths, i as addDays, k as isSameWeek, l as isSameMonth, m as isSameDay, n as intervalToDuration, o as getDaysInMonth, p as addHours, q as encode, H as Hourglass, G as Gauge, M as MessageSquare, C as Clock, Z as Zap, D as DollarSign, R as React, t as Markdown$1, u as rehypeHighlight, v as remarkGfm, w as defaultUrlTransform, T as Terminal, J as JsonView, x as vscodeTheme, y as tippy, z as reactDomExports, A as an, B as qt, Q as Qt, E as nn, F as parse, I as delegate, K as clientExports } from "./vendors-D2DMOduT.js";
2
2
  const SOURCE_COLORS = {
3
3
  scheduled: "#14b8a6",
4
4
  cli: "#3b82f6",
@@ -34,13 +34,13 @@ function useLocalStorage(key, initial) {
34
34
  }
35
35
  function clearOutdatedLocalStorage() {
36
36
  const version = localStorage.getItem(VERSION_KEY);
37
- if (version !== "1.9.5") {
38
- const [MAJOR, MINOR, PATCH] = "1.9.5".split(".");
37
+ if (version !== "1.10.1") {
38
+ const [MAJOR, MINOR, PATCH] = "1.10.1".split(".");
39
39
  const [major, minor, patch] = (version ?? "").split(".");
40
40
  if (MAJOR !== major || MINOR !== minor) {
41
41
  localStorage.clear();
42
42
  }
43
- localStorage.setItem(VERSION_KEY, "1.9.5");
43
+ localStorage.setItem(VERSION_KEY, "1.10.1");
44
44
  }
45
45
  }
46
46
  function useMouseFontScale(storageKey) {
@@ -248,9 +248,6 @@ function WorkTimeOverlay({
248
248
  const id = setInterval(() => setNow(Date.now()), 6e4);
249
249
  return () => clearInterval(id);
250
250
  }, []);
251
- reactExports.useEffect(() => {
252
- if (workTime) window.api.setWorkHours({ work_hours: { start: fmtHM(workTime.startMin), end: fmtHM(workTime.endMin) } });
253
- }, [workTime]);
254
251
  const chartRight = chartLeft + chartWidth;
255
252
  const xForMin = (min) => chartLeft + (dayStart + min * 6e4 - viewStart) / (viewEnd - viewStart) * chartWidth;
256
253
  const xStart = workTime ? xForMin(workTime.startMin) : 0;
@@ -259,15 +256,18 @@ function WorkTimeOverlay({
259
256
  e.stopPropagation();
260
257
  e.preventDefault();
261
258
  setDragging(which);
259
+ let next = null;
262
260
  const onMove = (m) => {
263
261
  const rect = containerRef.current.getBoundingClientRect();
264
262
  const ratio = clamp((m.clientX - rect.left - chartLeft) / chartWidth, 0, 1);
265
263
  const ts = viewStart + ratio * (viewEnd - viewStart);
266
264
  const min = clamp(Math.round((ts - dayStart) / 6e4 / SNAP_MIN) * SNAP_MIN, 0, 24 * 60);
267
- setWorkTime((wt) => which === "start" ? { ...wt, startMin: Math.min(min, wt.endMin - SNAP_MIN) } : { ...wt, endMin: Math.max(min, wt.startMin + SNAP_MIN) });
265
+ next = which === "start" ? { ...workTime, startMin: Math.min(min, workTime.endMin - SNAP_MIN) } : { ...workTime, endMin: Math.max(min, workTime.startMin + SNAP_MIN) };
266
+ setWorkTime(next);
268
267
  };
269
268
  const onUp = () => {
270
269
  setDragging(null);
270
+ if (next) window.api.setWorkHours({ work_hours: { start: fmtHM(next.startMin), end: fmtHM(next.endMin) } });
271
271
  document.removeEventListener("mousemove", onMove);
272
272
  document.removeEventListener("mouseup", onUp);
273
273
  };
@@ -1189,6 +1189,7 @@ function SessionList({ sessions, selectedId, deepLink, onSelect }) {
1189
1189
  const filtered = q ? sessions.filter((s) => [
1190
1190
  s.customTitle,
1191
1191
  s.agentName,
1192
+ s.tag,
1192
1193
  s.aiTitle,
1193
1194
  s.summary,
1194
1195
  s.firstUserPrompt,
@@ -1296,6 +1297,10 @@ ${TIPS.time}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { size: 11
1296
1297
  ] }) : sessionLabel(s) })
1297
1298
  ] }),
1298
1299
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "session-time-bottom", children: [
1300
+ s.tag && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "session-tag", title: "Session tag", children: [
1301
+ "#",
1302
+ s.tag
1303
+ ] }),
1299
1304
  s.project && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "session-project", title: s.project, children: s.projectShort }),
1300
1305
  s.models?.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "session-model", children: s.models.map((m) => m.replace(/^claude-/, "")).join(", ") }),
1301
1306
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { title: format(s.lastActivityAt, "MMM d, HH:mm:ss"), children: fmtAgo(s.lastActivityAt) }),
@@ -1440,7 +1445,8 @@ function buildLinkComponents(basePath) {
1440
1445
  }
1441
1446
  };
1442
1447
  }
1443
- const Chunk = React.memo(({ text, components }) => /* @__PURE__ */ jsxRuntimeExports.jsx(Markdown$1, { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeHighlight], components, children: text }));
1448
+ const urlTransform = (url, key, node) => node.tagName === "img" && /^data:image\//i.test(url) ? url : defaultUrlTransform(url);
1449
+ const Chunk = React.memo(({ text, components }) => /* @__PURE__ */ jsxRuntimeExports.jsx(Markdown$1, { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeHighlight], components, urlTransform, children: text }));
1444
1450
  function Markdown({ text, basePath = null, autoFence = false, className = "", ...rest }) {
1445
1451
  const ref = reactExports.useRef(null);
1446
1452
  const findOpen = useFindActive();
@@ -1748,7 +1754,7 @@ function InstructionFile({ it, model, tokens: tokens2 }) {
1748
1754
  ] }),
1749
1755
  open && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "aux-body", children: [
1750
1756
  it.name && it.name !== it.file_path && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "instruction-path", children: it.file_path }),
1751
- /* @__PURE__ */ jsxRuntimeExports.jsx(Markdown, { className: "block-text", text: it.content || "", basePath: it.file_path })
1757
+ it.tools ? it.tools.map((t) => /* @__PURE__ */ jsxRuntimeExports.jsx(InstructionFile, { it: { file_path: t.name, content: t.content }, model, tokens: countTokens(t.content, it.model ?? model) }, t.name)) : /* @__PURE__ */ jsxRuntimeExports.jsx(Markdown, { className: "block-text", text: it.content || "", basePath: it.file_path })
1752
1758
  ] })
1753
1759
  ] });
1754
1760
  }
@@ -2087,7 +2093,8 @@ ${transcript}`;
2087
2093
  - Model: ${meta.models.join(", ") || "—"}
2088
2094
  - Service tier: ${meta.serviceTier || "—"}
2089
2095
  - Speed: ${meta.speed ? meta.speed === "fast" ? meta.fastPricingUnknown ? "fast (pricing unknown)" : "fast" : meta.speed : "—"}
2090
- - Git branch: ${meta.gitBranch || "—"}
2096
+ ${meta.tag ? `- Tag: ${meta.tag}
2097
+ ` : ""}- Git branch: ${meta.gitBranch || "—"}
2091
2098
  - Source: ${meta.source || meta.entrypoint || "—"}
2092
2099
  - Scheduled: ${meta.hasScheduledTask ? "yes" : "no"}
2093
2100
  - CLI version: ${meta.version || "—"}
@@ -2332,6 +2339,7 @@ ${body}`);
2332
2339
  /* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Model", value: modelLabel, mono: true }),
2333
2340
  meta.serviceTier && /* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Service tier", value: meta.serviceTier }),
2334
2341
  meta.speed && /* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Speed", value: meta.speed === "fast" ? meta.fastPricingUnknown ? "fast (pricing unknown)" : "fast" : meta.speed }),
2342
+ meta.tag && /* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Tag", value: meta.tag, mono: true }),
2335
2343
  /* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Git branch", value: meta.gitBranch || "—", mono: true }),
2336
2344
  /* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Source", value: meta.source || meta.entrypoint || "—" }),
2337
2345
  meta.hasScheduledTask && /* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Scheduled", value: "yes" }),
@@ -54069,16 +54069,17 @@ var vscodeTheme = {
54069
54069
  };
54070
54070
  var reactDomExports = requireReactDom();
54071
54071
  export {
54072
- qt as A,
54073
- nn as B,
54072
+ an as A,
54073
+ qt as B,
54074
54074
  Clock as C,
54075
54075
  DollarSign as D,
54076
- parse$1 as E,
54077
- delegate as F,
54076
+ nn as E,
54077
+ parse$1 as F,
54078
54078
  Gauge as G,
54079
54079
  Hourglass as H,
54080
- clientExports as I,
54080
+ delegate as I,
54081
54081
  JsonView as J,
54082
+ clientExports as K,
54082
54083
  MessageSquare as M,
54083
54084
  Qt as Q,
54084
54085
  React as R,
@@ -54106,8 +54107,8 @@ export {
54106
54107
  Markdown as t,
54107
54108
  rehypeHighlight as u,
54108
54109
  remarkGfm as v,
54109
- vscodeTheme as w,
54110
- tippy as x,
54111
- reactDomExports as y,
54112
- an as z
54110
+ defaultUrlTransform as w,
54111
+ vscodeTheme as x,
54112
+ tippy as y,
54113
+ reactDomExports as z
54113
54114
  };
@@ -6,10 +6,10 @@
6
6
  <meta charset="UTF-8" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>Discover</title>
9
- <script type="module" crossorigin src="./assets/index-CX9ZlZGo.js"></script>
10
- <link rel="modulepreload" crossorigin href="./assets/vendors-JM3FOKMR.js">
9
+ <script type="module" crossorigin src="./assets/index-FM6CSRHb.js"></script>
10
+ <link rel="modulepreload" crossorigin href="./assets/vendors-D2DMOduT.js">
11
11
  <link rel="stylesheet" crossorigin href="./assets/vendors-B4XMnGdr.css">
12
- <link rel="stylesheet" crossorigin href="./assets/index-DZ1Gj6hz.css">
12
+ <link rel="stylesheet" crossorigin href="./assets/index-DnYN00T3.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
package/package.json CHANGED
@@ -1,77 +1,77 @@
1
- {
2
- "name": "claude-discover",
3
- "version": "1.9.5",
4
- "type": "module",
5
- "description": "Claude Discover — a desktop tool to browse your local Claude Code sessions: conversations, token costs, and captured API traffic",
6
- "license": "MIT",
7
- "author": "dichev <dichev.lab@gmail.com>",
8
- "repository": "github:dichev/claude-discover",
9
- "engines": {
10
- "node": ">=22"
11
- },
12
- "main": "out/main/main.js",
13
- "bin": {
14
- "claude-discover": "bin/claude-discover.mjs"
15
- },
16
- "files": [
17
- "out",
18
- "bin"
19
- ],
20
- "scripts": {
21
- "dev": "electron-vite dev",
22
- "build": "electron-vite build",
23
- "start": "electron .",
24
- "test": "vitest run --reporter=tree",
25
- "proxy": "node bin/proxy.mjs --restart",
26
- "readme": "gh markdown-preview",
27
- "smoke": "npm pack && npx -y ./claude-discover-${npm_package_version}.tgz",
28
- "postinstall": "install-electron",
29
- "prepack": "npm run build",
30
- "prepublishOnly": "npm whoami && npm test"
31
- },
32
- "dependencies": {
33
- "@anthropic-ai/claude-agent-sdk": "^0.3.266",
34
- "@date-fns/tz": "^1.5.0",
35
- "chokidar": "^5.0.0",
36
- "date-fns": "^4.4.0",
37
- "electron": "43.2.0",
38
- "electron-context-menu": "^4.1.2",
39
- "electron-window-state": "^5.0.3",
40
- "gpt-tokenizer": "^3.4.0",
41
- "which": "^7.0.0",
42
- "write-file-atomic": "^8.0.0"
43
- },
44
- "devDependencies": {
45
- "@fontsource/inter": "^5.3.0",
46
- "@fontsource/jetbrains-mono": "^5.3.0",
47
- "@tomjs/electron-devtools-installer": "^4.0.1",
48
- "@uiw/react-json-view": "^2.0.0-alpha.43",
49
- "@vitejs/plugin-react": "^5.2.0",
50
- "electron-vite": "^5.0.0",
51
- "highlight.js": "^11.12.0",
52
- "lucide-react": "^1.43.0",
53
- "react": "^19.3.0",
54
- "react-dom": "^19.3.0",
55
- "react-markdown": "^10.1.0",
56
- "react-resizable-panels": "^4.12.4",
57
- "rehype-highlight": "^7.0.2",
58
- "remark-gfm": "^4.0.1",
59
- "tippy.js": "^6.3.7",
60
- "vite": "^7.3.6",
61
- "vitest": "^4.1.11"
62
- },
63
- "keywords": [
64
- "claude",
65
- "claude-code",
66
- "claude-code-proxy",
67
- "claude-code-inspect",
68
- "system-prompt",
69
- "session-browser",
70
- "session-profiler",
71
- "transcript-viewer",
72
- "token-usage",
73
- "token-cost",
74
- "usage-dashboard",
75
- "api-inspector"
76
- ]
77
- }
1
+ {
2
+ "name": "claude-discover",
3
+ "version": "1.10.1",
4
+ "type": "module",
5
+ "description": "Claude Discover — a desktop tool to browse your local Claude Code sessions: conversations, token costs, and captured API traffic",
6
+ "license": "MIT",
7
+ "author": "dichev <dichev.lab@gmail.com>",
8
+ "repository": "github:dichev/claude-discover",
9
+ "engines": {
10
+ "node": ">=22"
11
+ },
12
+ "main": "out/main/main.js",
13
+ "bin": {
14
+ "claude-discover": "bin/claude-discover.mjs"
15
+ },
16
+ "files": [
17
+ "out",
18
+ "bin"
19
+ ],
20
+ "scripts": {
21
+ "dev": "electron-vite dev",
22
+ "build": "electron-vite build",
23
+ "start": "electron .",
24
+ "test": "vitest run --reporter=tree",
25
+ "proxy": "node bin/proxy.mjs --restart",
26
+ "readme": "gh markdown-preview",
27
+ "smoke:local": "npm pack && npx -y ./claude-discover-${npm_package_version}.tgz",
28
+ "smoke:global": "npm pack && npm i -g ./claude-discover-${npm_package_version}.tgz && claude-discover",
29
+ "postinstall": "install-electron",
30
+ "prepack": "npm run build",
31
+ "prepublishOnly": "npm whoami && npm test"
32
+ },
33
+ "dependencies": {
34
+ "@anthropic-ai/claude-agent-sdk": "^0.3.266",
35
+ "@date-fns/tz": "^1.5.0",
36
+ "date-fns": "^4.4.0",
37
+ "electron": "43.2.0",
38
+ "electron-context-menu": "^4.1.2",
39
+ "electron-window-state": "^5.0.3",
40
+ "gpt-tokenizer": "^3.4.0",
41
+ "which": "^7.0.0",
42
+ "write-file-atomic": "^8.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@fontsource/inter": "^5.3.0",
46
+ "@fontsource/jetbrains-mono": "^5.3.0",
47
+ "@tomjs/electron-devtools-installer": "^4.0.1",
48
+ "@uiw/react-json-view": "^2.0.0-alpha.43",
49
+ "@vitejs/plugin-react": "^5.2.0",
50
+ "electron-vite": "^5.0.0",
51
+ "highlight.js": "^11.12.0",
52
+ "lucide-react": "^1.43.0",
53
+ "react": "^19.3.0",
54
+ "react-dom": "^19.3.0",
55
+ "react-markdown": "^10.1.0",
56
+ "react-resizable-panels": "^4.12.4",
57
+ "rehype-highlight": "^7.0.2",
58
+ "remark-gfm": "^4.0.1",
59
+ "tippy.js": "^6.3.7",
60
+ "vite": "^7.3.6",
61
+ "vitest": "^4.1.11"
62
+ },
63
+ "keywords": [
64
+ "claude",
65
+ "claude-code",
66
+ "claude-code-proxy",
67
+ "claude-code-inspect",
68
+ "system-prompt",
69
+ "session-browser",
70
+ "session-profiler",
71
+ "transcript-viewer",
72
+ "token-usage",
73
+ "token-cost",
74
+ "usage-dashboard",
75
+ "api-inspector"
76
+ ]
77
+ }