claude-discover 1.10.9 → 1.10.10

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/README.md CHANGED
@@ -39,7 +39,7 @@ claude-discover
39
39
  The same session exactly as Claude Code stores it on disk, one JSON line at a time. For when you don't want the story, you want the evidence.
40
40
 
41
41
  - **API request inspector**\
42
- A Postman-like view of every API request and response, captured by an optional local proxy (one click on, one click off). This is where the secrets live: the system prompt, your CLAUDE.md and memory files, tool definitions, injected reminders - all the stuff session logs politely never record.
42
+ A Postman-like view of every API request and response, captured by an optional local proxy (one click on, one click off). This is where the secrets live: the system prompt, your CLAUDE.md and memory files, tool definitions, injected reminders - plus the side requests (title, auto-mode checks, …) session logs never record.
43
43
 
44
44
  - **Session summary**\
45
45
  Token usage and cost for every session, and totals for the whole period. Computed from your local transcripts with current model rates - accurate to the cent.
@@ -16,4 +16,5 @@ export const PING_RESPONSE = 'claude-discover-proxy' // the ping body — proves
16
16
 
17
17
  export const DATA_DIR = process.env.CLAUDE_DISCOVER_DIR || path.join(os.homedir(), '.claude-discover') // global on purpose — session ids are unique, so one flat dir serves every Claude dir (env override used only by tests)
18
18
  export const REQUESTS_DIR = path.join(DATA_DIR, 'requests') // logs are kept forever — nothing deletes them
19
+ export const UNATTRIBUTED = 'unattributed' // the log name for requests without a session-id header
19
20
  export const ERROR_LOG_PATH = path.join(DATA_DIR, 'proxy.error.log') // capture errors — capture must never fail a request
package/bin/proxy.mjs CHANGED
@@ -14,7 +14,7 @@ import crypto from 'node:crypto'
14
14
  import { pathToFileURL } from 'node:url'
15
15
  import { parseArgs } from 'node:util'
16
16
 
17
- import { HOST, PORT, PROXY_URL, UPSTREAM, EXIT_ROUTE, PING_ROUTE, PING_RESPONSE, REQUESTS_DIR, ERROR_LOG_PATH } from './proxy.config.js'
17
+ import { HOST, PORT, PROXY_URL, UPSTREAM, EXIT_ROUTE, PING_ROUTE, PING_RESPONSE, REQUESTS_DIR, UNATTRIBUTED, ERROR_LOG_PATH } from './proxy.config.js'
18
18
 
19
19
  // How long an exiting proxy keeps serving in-flight responses — long replies stream for minutes
20
20
  const DRAIN_TIMEOUT_MS = 10 * 60_000
@@ -107,7 +107,7 @@ function decode(encoding, buf) {
107
107
 
108
108
  // ── 2. Claude Code capture ───────────────────────────────────────────────────
109
109
  // One JSON record per exchange in ~/.claude-discover/requests/<sessionId>.requests.jsonl
110
- // (session id from the X-Claude-Code-Session-Id header).
110
+ // (session id from the X-Claude-Code-Session-Id header; requests without a usable one go to the UNATTRIBUTED log).
111
111
 
112
112
  // Keep the header name (useful signal) but never the credential.
113
113
  function redactHeaders(headers, keys) {
@@ -116,8 +116,9 @@ function redactHeaders(headers, keys) {
116
116
  }
117
117
 
118
118
  function logRequest({ req, requestBody, status, responseHeaders, responseBody, timestamp, durationMs }) {
119
- const sessionId = req.headers['x-claude-code-session-id']
120
- if (!sessionId || !/^[\w-]+$/.test(sessionId)) return // header value becomes a filename — accept only safe ids
119
+ const header = req.headers['x-claude-code-session-id']
120
+ if (!header && req.method === 'HEAD') return // Claude Code's connectivity probe (HEAD /api/hello), every few seconds at times
121
+ const sessionId = /^[\w-]+$/.test(header ?? '') ? header : UNATTRIBUTED // header value becomes a filename — accept only safe ids
121
122
  const logPath = path.join(REQUESTS_DIR, `${sessionId}.requests.jsonl`)
122
123
  const record = { type: 'api-request', timestamp, url: `${req.method} ${req.url}`, status, durationMs }
123
124
  record.requestHeaders = redactHeaders(req.headers, ['authorization', 'x-api-key', 'proxy-authorization', 'cookie'])
package/out/main/main.js CHANGED
@@ -672,6 +672,7 @@ var EXIT_ROUTE = "/claude-discover/exit";
672
672
  var PING_ROUTE = "/claude-discover/ping";
673
673
  var DATA_DIR = process.env.CLAUDE_DISCOVER_DIR || path.join(os.homedir(), ".claude-discover");
674
674
  var REQUESTS_DIR = path.join(DATA_DIR, "requests");
675
+ var UNATTRIBUTED = "unattributed";
675
676
  var ERROR_LOG_PATH = path.join(DATA_DIR, "proxy.error.log");
676
677
  //#endregion
677
678
  //#region src/main/requests/claudeMarkers.js
@@ -690,7 +691,7 @@ var classifyRequest = (req, url) => {
690
691
  if (req.max_tokens === 1 || user.trim() === "quota") return ["quota", "Quota probe"];
691
692
  if (sysText.includes("You are a security monitor") || user.startsWith("<transcript>")) return ["security", "Auto-mode check"];
692
693
  if (sysText.includes("Generate a concise, sentence-case title") || sysText.includes("You are naming a coding session")) return ["title", "Session title"];
693
- if (sysText.includes("performing a web search")) return ["web", "Web search"];
694
+ if (sysText.includes("performing a web search") || sysText.includes("You run a web search")) return ["web", "Web search"];
694
695
  if (user.startsWith("[SUGGESTION MODE")) return ["suggest", "Suggestions"];
695
696
  if (user.startsWith("Web page content")) return ["web", "Web fetch"];
696
697
  if (user.startsWith("Describe your most recent action")) return ["status", "Status blurb"];
@@ -775,12 +776,9 @@ function splitClaudeMd(text) {
775
776
  var jsonBytes = (x) => x == null ? 0 : Buffer.byteLength(JSON.stringify(x));
776
777
  var contentHash = (s) => crypto.createHash("sha256").update(s).digest("base64url").slice(0, 16);
777
778
  var isMcp = (name) => name.startsWith("mcp__");
778
- var stripKind = (req) => {
779
- const kind = classifyRequest(req);
780
- return !kind || kind[0] === "main" || kind[0] === "tools" ? ["main", null] : kind;
781
- };
779
+ var stripKind = ({ kind }) => !kind || kind[0] === "main" || kind[0] === "tools" ? ["main", null] : kind;
782
780
  var toStrip = (rec, source, file_path, label, content) => {
783
- const [kind, kindLabel] = stripKind(rec.request);
781
+ const [kind, kindLabel] = stripKind(rec);
784
782
  return {
785
783
  source,
786
784
  file_path,
@@ -798,6 +796,22 @@ var RequestParser = class {
798
796
  this.hashes = /* @__PURE__ */ new Map();
799
797
  this.seenTools = /* @__PURE__ */ new Set();
800
798
  this.deferred = null;
799
+ this.seenStrips = /* @__PURE__ */ new Set();
800
+ }
801
+ strips(rec) {
802
+ const files = this.memoryFiles(rec);
803
+ return [
804
+ this.systemPrompt(rec),
805
+ ...files.filter((f) => f.source === "system"),
806
+ ...this.systemTools(rec),
807
+ this.deferredStrip(rec),
808
+ ...files.filter((f) => f.source === "message"),
809
+ ...this.reminderStrips(rec)
810
+ ].filter((f) => {
811
+ if (!f || this.seenStrips.has(f.hash)) return false;
812
+ this.seenStrips.add(f.hash);
813
+ return true;
814
+ });
801
815
  }
802
816
  resolve(v, path = "", seen = null) {
803
817
  if (v?.$hash) {
@@ -888,7 +902,8 @@ var RequestParser = class {
888
902
  const reminders = injectedTexts(req).filter((t) => t.includes("<system-reminder>"));
889
903
  const from = (source, list) => list.flatMap((t) => splitClaudeMd(t).files).map((f) => ({
890
904
  source,
891
- ...f
905
+ ...f,
906
+ hash: contentHash(`${f.file_path}\n${f.content}`)
892
907
  }));
893
908
  return [...from("system", texts(req?.system)), ...from("message", reminders)];
894
909
  }
@@ -937,26 +952,53 @@ var RequestFile = class {
937
952
  }
938
953
  async readInstructions(range = null) {
939
954
  const parser = new RequestParser();
940
- const files = /* @__PURE__ */ new Map();
941
- let start = null;
955
+ const strips = [];
942
956
  for (const rec of await this.read()) {
943
957
  if (!rec.request || rec.kind?.[0] === "count") continue;
944
- const loop = isLoop(rec.kind);
945
- start ??= loop ? rec : null;
946
- const record = (f) => ({
958
+ const found = parser.strips(rec);
959
+ if (!isLoop(rec.kind) && inRange(rec.timestamp, range)) strips.push(...found.map((f) => ({
947
960
  timestamp: rec.timestamp,
948
- ...f,
949
- side: !loop
950
- });
951
- const sys = parser.systemPrompt(rec);
952
- if (sys && !files.has(sys.hash)) files.set(sys.hash, record(sys));
953
- const deferred = parser.deferredStrip(rec);
954
- if (deferred && !files.has(deferred.hash)) files.set(deferred.hash, record(deferred));
955
- for (const tools of parser.systemTools(rec)) files.set(tools.hash, record(tools));
956
- for (const f of parser.memoryFiles(rec)) if (!files.has(f.file_path)) files.set(f.file_path, record(f));
957
- for (const r of parser.reminderStrips(rec)) if (!files.has(r.hash)) files.set(r.hash, record(r));
961
+ ...f
962
+ })));
958
963
  }
959
- return [...files.values()].filter((f) => f.timestamp === start?.timestamp || inRange(f.timestamp, range));
964
+ return strips;
965
+ }
966
+ };
967
+ //#endregion
968
+ //#region src/main/requests/TranscriptInstructions.js
969
+ var TranscriptInstructions = class {
970
+ constructor() {
971
+ this.parser = new RequestParser();
972
+ this.shape = null;
973
+ this.blobs = [];
974
+ }
975
+ static listed(obj, keys) {
976
+ if (obj.attachment?.type === "prompt_snapshot") return keys.size > 0;
977
+ const texts = (obj.rendered ?? []).map((r) => r.content).filter((t) => typeof t === "string");
978
+ const found = new RequestParser().strips({ request: { messages: [{
979
+ role: "user",
980
+ content: texts.map((text) => ({
981
+ type: "text",
982
+ text
983
+ }))
984
+ }] } });
985
+ return found.length > 0 && found.every((f) => keys.has(f.hash));
986
+ }
987
+ feed(obj) {
988
+ if (obj.type === "api-request-shape") this.shape = obj.shape;
989
+ if (obj.type === "api-request-blob") this.blobs.push(obj.message);
990
+ if (obj.type !== "api-request") return [];
991
+ const rec = { request: {
992
+ ...this.shape,
993
+ model: obj.params?.model,
994
+ messages: this.blobs
995
+ } };
996
+ this.shape = null;
997
+ this.blobs = [];
998
+ return this.parser.strips(rec).map((f) => ({
999
+ timestamp: obj.timestamp,
1000
+ ...f
1001
+ }));
960
1002
  }
961
1003
  };
962
1004
  //#endregion
@@ -1911,21 +1953,46 @@ var SessionsService = class extends EventEmitter {
1911
1953
  range
1912
1954
  });
1913
1955
  const items = [];
1914
- await reader.stream((obj) => {
1915
- if (parser.feed(obj)) items.push(obj);
1916
- });
1956
+ const transcript = new TranscriptInstructions();
1957
+ const loop = [];
1958
+ let start = null;
1959
+ const [, side] = await Promise.all([reader.stream((obj) => {
1960
+ const strips = transcript.feed(obj);
1961
+ if (obj.type === "api-request") start ??= strips;
1962
+ if (parser.feed(obj)) {
1963
+ items.push(obj);
1964
+ loop.push(...strips);
1965
+ }
1966
+ }), this.#requestFile(reader.sessionId, reader.parentSessionId).readInstructions(range)]);
1967
+ if (start?.length && !loop.includes(start[0])) loop.unshift(...start);
1917
1968
  items.sort((a, b) => a._ts - b._ts);
1918
- for (const o of items) delete o._ts;
1969
+ const keys = new Set(loop.map((s) => s.hash));
1970
+ for (const o of items) {
1971
+ delete o._ts;
1972
+ if (o.type === "attachment" && TranscriptInstructions.listed(o, keys)) o._listed = true;
1973
+ }
1919
1974
  return {
1920
1975
  items,
1921
- instructions: await this.#requestFile(reader.sessionId, reader.parentSessionId).readInstructions(range)
1976
+ instructions: {
1977
+ loop,
1978
+ side
1979
+ }
1922
1980
  };
1923
1981
  }
1924
1982
  async readRequests(filePath, date = null, granularity = "day") {
1925
1983
  const meta = this.metaCache.get(filePath);
1926
1984
  if (!meta || !/^[\w-]+$/.test(meta.sessionId)) return [];
1927
1985
  const range = date ? periodBounds(date, granularity) : null;
1928
- return this.#requestFile(meta.sessionId, meta.parentSessionId).read(range);
1986
+ const own = await this.#requestFile(meta.sessionId, meta.parentSessionId).read(range);
1987
+ const span = {
1988
+ start: meta.startedAt,
1989
+ end: meta.lastActivityAt
1990
+ };
1991
+ const unattributed = (await new RequestFile(UNATTRIBUTED).read(span)).map((r) => ({
1992
+ ...r,
1993
+ unattributed: true
1994
+ }));
1995
+ return [...own, ...unattributed].sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
1929
1996
  }
1930
1997
  avatarPath(filePath) {
1931
1998
  return this.metaCache.get(filePath)?.avatar ?? null;
@@ -2437,7 +2504,6 @@ var ProxySwitch = class {
2437
2504
  throw new Error(`Proxy did not start — see ${ERROR_LOG_PATH}`);
2438
2505
  }
2439
2506
  settings.setEnv("ANTHROPIC_BASE_URL", PROXY_URL);
2440
- settings.setEnv("ENABLE_TOOL_SEARCH", "true");
2441
2507
  settings.save();
2442
2508
  }
2443
2509
  async deactivate() {
@@ -2747,7 +2813,7 @@ var MainWindow = class {
2747
2813
  //#region src/main/updater.js
2748
2814
  var updater = new NpmUpdater({
2749
2815
  name: "claude-discover",
2750
- version: "1.10.9",
2816
+ version: "1.10.10",
2751
2817
  bin: { "claude-discover": "bin/claude-discover.mjs" }
2752
2818
  }, { dev: false });
2753
2819
  //#endregion
@@ -734,7 +734,13 @@
734
734
  width: 28px;
735
735
  height: 28px;
736
736
  border-radius: 50%;
737
- object-fit: cover;
737
+ overflow: hidden;
738
+ }
739
+
740
+ /* Clipped by the box rather than object-fit/object-view-box, which downscale with a blockier filter */
741
+ .session-avatar img {
742
+ display: block;
743
+ width: 100%;
738
744
  }
739
745
 
740
746
  .subagent-tag,
@@ -871,7 +877,7 @@ button.divider {
871
877
  }
872
878
 
873
879
  .conv-row + .conv-row { margin-top: 15px; }
874
- /* The session's one instruction row (see flatten) — the gap keeps its strips off the first message. */
880
+ /* A side request's Proxy block — the gap keeps its strips off the next message. */
875
881
  .conv-row-instruction + .conv-row { margin-top: 20px; }
876
882
  .conv-row-assistant + .conv-row-user { margin-top: 60px; }
877
883
  .conv-row-assistant + .conv-row-assistant { margin-top: 0; }
@@ -1079,6 +1085,7 @@ button.msg-summary:hover { color: var(--text); background: var(--bg-elev-2); bor
1079
1085
 
1080
1086
  .turn-system {
1081
1087
  & .turn-blocks > .aux { align-self: flex-start; }
1088
+ & .turn-blocks > .instruction-file { align-self: stretch; } /* full width, so its token count lines up right */
1082
1089
  & .turn-blocks > .block-text {
1083
1090
  border: 1px dashed var(--border);
1084
1091
  border-radius: 6px;
@@ -1226,24 +1233,17 @@ button.msg-summary:hover { color: var(--text); background: var(--bg-elev-2); bor
1226
1233
  }
1227
1234
  }
1228
1235
 
1229
- /* Instruction files (system prompt / CLAUDE.md / memory) extracted from the captured API requests,
1230
- and the label over each group — one unbroken strip down the whole run. */
1236
+ /* Instruction files (system prompt / CLAUDE.md / memory) a side request sent, and the label over
1237
+ each group — one unbroken strip down the whole Proxy block. */
1231
1238
  .instruction-file,
1232
- .instruction-source,
1233
- .instruction-missing {
1239
+ .instruction-source {
1234
1240
  border-left: 2px solid #a78bfa;
1235
1241
  padding-left: 8px;
1236
1242
  margin-left: -10px;
1237
1243
  }
1238
-
1239
- /* Stands in for the strips when nothing was captured (see InstructionsMissing). */
1240
- .instruction-missing {
1241
- color: var(--text-faint);
1242
- font-size: calc(11px * var(--font-scale, 1));
1243
- }
1244
- .instruction-missing.pending :is(.msg-summary, .instruction-missing-text) {
1245
- visibility: hidden;
1246
- }
1244
+ /* The loop's own system prompt and tools sit in a System row, among its directives */
1245
+ .instruction-file.directive { border-left: none; padding-left: 0; margin-left: 0; }
1246
+ .instruction-file.directive .aux-toggle { color: #b9b9b9 }
1247
1247
 
1248
1248
  /* Group label over the strips read from one part of the request (system prompt / tools / user message). */
1249
1249
  .instruction-source {
@@ -1474,6 +1474,14 @@ button.msg-summary:hover { color: var(--text); background: var(--bg-elev-2); bor
1474
1474
  white-space: nowrap;
1475
1475
  }
1476
1476
 
1477
+ /* sent without a session id — tinted so it stands out from the session's own requests */
1478
+ .requests-list li.unattributed button {
1479
+ background: color-mix(in srgb, var(--warn) 10%, transparent);
1480
+
1481
+ &:hover { background: color-mix(in srgb, var(--warn) 16%, transparent); }
1482
+ &.active { background: color-mix(in srgb, var(--warn) 22%, transparent); }
1483
+ }
1484
+
1477
1485
  /* category label, last column — fixed width so the labels line up left-aligned,
1478
1486
  set off from the metrics by a full-height vertical rule; colors come from the
1479
1487
  shared .kind-* palette in styles.css */
@@ -2440,6 +2448,7 @@ button:hover { background: var(--border); }
2440
2448
  .kind-suggest { color: #6ea882; }
2441
2449
  .kind-web { color: #5fa8ad; }
2442
2450
  .kind-security { color: #c4937b; }
2451
+ .kind-unattributed { color: var(--warn); }
2443
2452
  .kind-quota, .kind-status, .kind-count, .kind-aux { color: var(--text-faint); }
2444
2453
 
2445
2454
  /* Pill meter styled like .button-primary: a strong tone border over a tinted track */
@@ -46,11 +46,11 @@ function useLocalStorage(key, initial) {
46
46
  }
47
47
  function clearOutdatedLocalStorage() {
48
48
  const version = localStorage.getItem(VERSION_KEY);
49
- if (version !== "1.10.9") {
50
- const [MAJOR, MINOR, PATCH] = "1.10.9".split(".");
49
+ if (version !== "1.10.10") {
50
+ const [MAJOR, MINOR, PATCH] = "1.10.10".split(".");
51
51
  const [major, minor, patch] = (version ?? "").split(".");
52
52
  if (MAJOR !== major || MINOR !== minor) localStorage.clear();
53
- localStorage.setItem(VERSION_KEY, "1.10.9");
53
+ localStorage.setItem(VERSION_KEY, "1.10.10");
54
54
  }
55
55
  }
56
56
  //#endregion
@@ -1437,6 +1437,7 @@ function persistedOutput(text) {
1437
1437
  href: saved.split(/[\\/]/).slice(-3).map(encodeURIComponent).join("/")
1438
1438
  } : null;
1439
1439
  }
1440
+ var SIDE_NOTE = "separate session, not in context";
1440
1441
  var SOURCE_LABELS = {
1441
1442
  system: "system prompt",
1442
1443
  tools: "tools",
@@ -1445,7 +1446,6 @@ var SOURCE_LABELS = {
1445
1446
  function groupInstructions(strips) {
1446
1447
  return Object.entries(SOURCE_LABELS).map(([source, label]) => [label, strips.filter((it) => it.source === source)]).filter(([, list]) => list.length);
1447
1448
  }
1448
- var sideChannelNote = (strips) => strips.every((it) => it.side) ? "separate session, not in context" : null;
1449
1449
  function instructionTitle(it, model) {
1450
1450
  const parts = [it.memory_type, it.model && it.model !== model && it.model].filter(Boolean).join(", ");
1451
1451
  return parts ? `${it.name ?? it.file_path} (${parts})` : it.name ?? it.file_path;
@@ -1478,10 +1478,12 @@ var avatars = /* @__PURE__ */ new Map();
1478
1478
  var Avatar = ({ session }) => {
1479
1479
  if (!avatars.has(session.avatar)) avatars.set(session.avatar, window.api.readAvatar(session.filePath));
1480
1480
  const src = (0, import_react.use)(avatars.get(session.avatar));
1481
- return src && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
1481
+ return src && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1482
1482
  className: "session-avatar",
1483
- src,
1484
- alt: ""
1483
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
1484
+ src,
1485
+ alt: ""
1486
+ })
1485
1487
  });
1486
1488
  };
1487
1489
  function SessionList({ sessions, selectedId, deepLink, onSelect }) {
@@ -1763,10 +1765,18 @@ function SessionList({ sessions, selectedId, deepLink, onSelect }) {
1763
1765
  }
1764
1766
  //#endregion
1765
1767
  //#region src/renderer/sessions/view/transcript.js
1768
+ var lengths = /* @__PURE__ */ new Map();
1769
+ var tokenLength = (text) => {
1770
+ if (!lengths.has(text)) {
1771
+ if (lengths.size >= 1e3) lengths.clear();
1772
+ lengths.set(text, encode(text).length);
1773
+ }
1774
+ return lengths.get(text);
1775
+ };
1766
1776
  var countTokens = (text, model = "") => {
1767
1777
  if (!text) return 0;
1768
1778
  const scale = /-(3|4)(-|$)/.test(model) ? 1.13 : 1.5;
1769
- return Math.round(encode(text).length * scale);
1779
+ return Math.round(tokenLength(text) * scale);
1770
1780
  };
1771
1781
  var queuedPrompt = (it) => it.type === "attachment" && it.attachment?.type === "queued_command" && it.attachment.commandMode === "prompt" && (it.attachment.origin?.kind ?? "human") === "human" && it.attachment.prompt?.trim() ? it.attachment.prompt : null;
1772
1782
  var USER_ATTACHMENTS = /* @__PURE__ */ new Set([
@@ -1865,7 +1875,7 @@ var noteTurn = (it, note, uuid) => metaTurn(it, "system", [{
1865
1875
  folded: !note.level,
1866
1876
  ...note
1867
1877
  }], uuid);
1868
- function flatten(items, instructions = []) {
1878
+ function flatten(items, { loop = [], side = [] } = {}) {
1869
1879
  const turns = [];
1870
1880
  const results = {};
1871
1881
  const companions = {};
@@ -1883,6 +1893,7 @@ function flatten(items, instructions = []) {
1883
1893
  };
1884
1894
  for (let it of items) {
1885
1895
  if (it._relogged) continue;
1896
+ if (it._listed) continue;
1886
1897
  const change = stateNote(it, state);
1887
1898
  if (change) turns.push(noteTurn(it, change, `state-${turns.length}`));
1888
1899
  if (it.subtype === "compact_boundary" || it.isCompactSummary) {
@@ -2018,22 +2029,36 @@ function flatten(items, instructions = []) {
2018
2029
  result: withCompanion(results[b.id], companions[b.id])
2019
2030
  } : b).filter((b) => !(b.type === "tool_result" && calls.has(b.tool_use_id)));
2020
2031
  const out = turns.filter((t) => t.blocks.length > 0);
2021
- for (const [ts, list] of Object.entries(Object.groupBy(instructions, (it) => it.timestamp))) {
2022
- const turn = {
2023
- uuid: `instructions-${ts}`,
2024
- role: "instruction",
2025
- ts: Date.parse(ts) - 500,
2026
- model: null,
2027
- usage: null,
2028
- tokenDelta: null,
2029
- tokenTotal: null,
2030
- blocks: list.map((it) => ({
2031
- type: "instruction",
2032
- it
2033
- }))
2034
- };
2032
+ const slot = (turn) => {
2035
2033
  const i = out.findIndex((t) => t.ts != null && t.ts > turn.ts);
2036
2034
  i === -1 ? out.push(turn) : out.splice(i, 0, turn);
2035
+ };
2036
+ for (const [ts, list] of Object.entries(Object.groupBy(side, (it) => it.timestamp))) {
2037
+ const blocks = list.map((it) => ({
2038
+ type: "instruction",
2039
+ it
2040
+ }));
2041
+ slot({
2042
+ ...metaTurn({ timestamp: ts }, "instruction", blocks, `instructions-${ts}`),
2043
+ ts: Date.parse(ts) - 500
2044
+ });
2045
+ }
2046
+ const model = currentModel(out);
2047
+ for (const [ts, list] of Object.entries(Object.groupBy(loop, (it) => it.timestamp))) {
2048
+ const blocks = list.map((it) => ({
2049
+ type: "instruction",
2050
+ it: {
2051
+ ...it,
2052
+ model: it.model ?? model
2053
+ }
2054
+ }));
2055
+ const turn = metaTurn({ timestamp: ts }, "system", blocks, `prompt-${ts}`);
2056
+ const i = out.findLastIndex((t) => t.ts != null && t.ts <= turn.ts && [
2057
+ "user",
2058
+ "assistant",
2059
+ "tool"
2060
+ ].includes(t.role) && !isContextTurn(t));
2061
+ out[i]?.role === "user" ? out.splice(i, 0, turn) : slot(turn);
2037
2062
  }
2038
2063
  return out;
2039
2064
  }
@@ -2452,7 +2477,7 @@ function useCollapsed(defaultOpen) {
2452
2477
  }
2453
2478
  var isCommandTurn = (t) => t.blocks.some((b) => b.type === "text" && parseCommand(b.text)?.name);
2454
2479
  var isClearTurn = (t) => t.blocks.some((b) => b.type === "text" && parseCommand(b.text)?.name === "/clear");
2455
- function ConversationView({ items, instructions = [], desktop = false, filePath = null, expandAll = null, continuesFrom = null, continuesTo = null, onShowPeriodOf }) {
2480
+ function ConversationView({ items, instructions = {}, filePath = null, expandAll = null, continuesFrom = null, continuesTo = null, onShowPeriodOf }) {
2456
2481
  const turns = (0, import_react.useMemo)(() => flatten(items, instructions), [items, instructions]);
2457
2482
  const groups = (0, import_react.useMemo)(() => groupTurns(turns), [turns]);
2458
2483
  const points = (0, import_react.useMemo)(() => tokenPoints(groups), [groups]);
@@ -2483,13 +2508,6 @@ function ConversationView({ items, instructions = [], desktop = false, filePath
2483
2508
  title: "Show the earlier period",
2484
2509
  children: ["Continues from ", format(continuesFrom, "MMM d, yyyy HH:mm")]
2485
2510
  }),
2486
- instructions.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
2487
- className: "conv-row conv-row-instruction",
2488
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InstructionsMissing, {
2489
- desktop,
2490
- pending: !groups.some((g) => g.kind === "assistant")
2491
- })
2492
- }),
2493
2511
  groups.map((g, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react.Fragment, { children: [
2494
2512
  i === branch?.index && branchDivider,
2495
2513
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
@@ -2881,6 +2899,13 @@ function Block({ block, speaker = null }) {
2881
2899
  folded: block.folded,
2882
2900
  speaker
2883
2901
  });
2902
+ if (block.type === "instruction") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InstructionFile, {
2903
+ it: block.it,
2904
+ model: block.it.model,
2905
+ tokens: countTokens(block.it.content, block.it.model),
2906
+ speaker,
2907
+ className: "directive"
2908
+ });
2884
2909
  if (block.type === "system") {
2885
2910
  const className = `system-note ${block.level ?? ""} ${block.kind ?? ""}`;
2886
2911
  if (!block.body) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, {
@@ -3075,41 +3100,27 @@ function JsonBlock({ value }) {
3075
3100
  text: "```json\n" + safeJson(value) + "\n```"
3076
3101
  });
3077
3102
  }
3078
- var ProxyHeader = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
3079
- className: "msg-header msg-proxy",
3080
- children: [
3081
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Globe, { className: "msg-icon" }),
3082
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
3083
- className: "msg-author",
3084
- children: "Proxy"
3085
- }),
3086
- children
3087
- ]
3088
- });
3089
- function InstructionsMissing({ desktop, pending }) {
3090
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
3091
- className: `instruction-missing${pending ? " pending" : ""}`,
3092
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProxyHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
3093
- className: "msg-summary",
3094
- children: "not captured"
3095
- }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
3096
- className: "instruction-missing-text",
3097
- children: desktop ? "Not yet supported for Claude desktop app sessions." : "Turn on the Capture proxy to see the system prompt, tools and memory files."
3098
- })]
3099
- });
3100
- }
3101
3103
  function InstructionRun({ turns, model }) {
3102
3104
  const strips = turns.flatMap((t) => t.blocks).map(({ it }) => ({
3103
3105
  ...it,
3104
3106
  tokens: countTokens(it.content, it.model ?? model)
3105
3107
  }));
3106
- const note = sideChannelNote(strips);
3107
3108
  const groups = groupInstructions(strips);
3108
3109
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
3109
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProxyHeader, { children: note && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
3110
- className: "msg-summary",
3111
- children: note
3112
- }) }),
3110
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
3111
+ className: "msg-header msg-proxy",
3112
+ children: [
3113
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Globe, { className: "msg-icon" }),
3114
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
3115
+ className: "msg-author",
3116
+ children: "Proxy"
3117
+ }),
3118
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
3119
+ className: "msg-summary",
3120
+ children: SIDE_NOTE
3121
+ })
3122
+ ]
3123
+ }),
3113
3124
  groups.map(([label, list]) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react.Fragment, { children: [groups.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
3114
3125
  className: "aux instruction-source",
3115
3126
  children: label
@@ -3117,7 +3128,7 @@ function InstructionRun({ turns, model }) {
3117
3128
  it,
3118
3129
  model,
3119
3130
  tokens: it.tokens
3120
- }, `${it.hash ?? it.file_path}-${it.timestamp}`))] }, label)),
3131
+ }, `${it.hash}-${it.timestamp}`))] }, label)),
3121
3132
  strips.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
3122
3133
  className: "aux instruction-total",
3123
3134
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
@@ -3127,11 +3138,11 @@ function InstructionRun({ turns, model }) {
3127
3138
  })
3128
3139
  ] });
3129
3140
  }
3130
- function InstructionFile({ it, model, tokens }) {
3141
+ function InstructionFile({ it, model, tokens, speaker = null, className = "" }) {
3131
3142
  const [open, setOpen] = useCollapsed(false);
3132
3143
  const title = instructionTitle(it, model);
3133
3144
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
3134
- className: "aux instruction-file",
3145
+ className: `aux instruction-file ${className}`,
3135
3146
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
3136
3147
  className: "aux-toggle",
3137
3148
  onClick: () => setOpen((v) => !v),
@@ -3144,7 +3155,7 @@ function InstructionFile({ it, model, tokens }) {
3144
3155
  className: "instruction-title",
3145
3156
  "data-tooltip-content": title,
3146
3157
  "data-tip-if-truncated": true,
3147
- children: title
3158
+ children: said(speaker, title)
3148
3159
  }),
3149
3160
  tokens > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
3150
3161
  className: "instruction-tokens",
@@ -3611,8 +3622,9 @@ function RequestsView({ filePath, items, date, granularity = "day", fileSize = 0
3611
3622
  className: "requests-list",
3612
3623
  children: [records.map((r, i) => {
3613
3624
  const [method, path] = splitUrl(r.url);
3625
+ const kind = r.unattributed ? ["unattributed", "No session id"] : r.kind;
3614
3626
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react.Fragment, { children: [gapRow(records[i - 1]?.timestamp, r.timestamp), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", {
3615
- className: i > 0 && r.kind?.[0] === "main" ? "new-turn" : "",
3627
+ className: r.unattributed ? "unattributed" : i > 0 && kind?.[0] === "main" ? "new-turn" : "",
3616
3628
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
3617
3629
  type: "button",
3618
3630
  className: i === selected ? "active" : "",
@@ -3639,8 +3651,9 @@ function RequestsView({ filePath, items, date, granularity = "day", fileSize = 0
3639
3651
  children: duration(r.durationMs)
3640
3652
  }),
3641
3653
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
3642
- className: `requests-kind ${r.kind ? `kind-${r.kind[0]}` : ""}`,
3643
- children: r.kind?.[1]
3654
+ className: `requests-kind ${kind ? `kind-${kind[0]}` : ""}`,
3655
+ "data-tooltip-content": r.unattributed ? "Sent without a session id, so it might not be from this session - every session active at the time lists it" : void 0,
3656
+ children: kind?.[1]
3644
3657
  }),
3645
3658
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
3646
3659
  className: "requests-meta requests-tokens",
@@ -4081,8 +4094,7 @@ function renderBlock(b) {
4081
4094
  function renderTurn(t) {
4082
4095
  if (t.role === "instruction") {
4083
4096
  const strips = t.blocks.map((b) => b.it);
4084
- const note = sideChannelNote(strips);
4085
- return `**Proxy${note ? ` (${note})` : ""}:**\n\n` + groupInstructions(strips).map(([label, list]) => `**Instructions loaded (${label}):**\n\n${list.map((it) => renderBlock({
4097
+ return `**Proxy (${SIDE_NOTE}):**\n\n` + groupInstructions(strips).map(([label, list]) => `**Instructions loaded (${label}):**\n\n${list.map((it) => renderBlock({
4086
4098
  type: "instruction",
4087
4099
  it
4088
4100
  })).join("\n\n")}`).join("\n\n");
@@ -4093,7 +4105,7 @@ function renderTurn(t) {
4093
4105
  }
4094
4106
  return `**${t.role === "system" ? "System" : t.role === "user" ? t.queued ? "User (queued)" : "User" : `Assistant`}${t.tokenTotal > 0 && t.tokenDelta != null ? ` (${t.tokenDelta >= 0 ? "+" : ""}${fmtCompact(t.tokenDelta)} / ${fmtCompact(t.tokenTotal)} tokens)` : ""}:**\n\n${t.blocks.map(renderBlock).join("\n\n")}`;
4095
4107
  }
4096
- function markdownSession(meta, items, instructions = []) {
4108
+ function markdownSession(meta, items, instructions = {}) {
4097
4109
  const t = meta.tokens;
4098
4110
  const stu = meta.serverToolUse;
4099
4111
  const wallDuration = meta.lastActivityAt - meta.startedAt;
@@ -4157,7 +4169,7 @@ ${meta.subdir ? `- Subfolder: ${meta.subdir}\n` : ""}${meta.worktreePath ? `- Wo
4157
4169
  ${conversation}
4158
4170
  </transcript>`.trim() };
4159
4171
  }
4160
- var PROMPT = String("# Role\r\n\r\nYou are reviewing a Claude Code session and giving the user practical advice on how to run a similar session more cheaply next time.\r\n\r\nThe user controls the prompts and the request settings around them: **model** (Opus / Sonnet / Haiku), **prompt caching**, enabled **system tools** (web search, web fetch, bash, MCP, etc.), context-window size, and CLAUDE.md. They cannot change Claude's internal reasoning. Look for missed opportunities to apply Anthropic's official Claude Code guidance.\r\n\r\n# Levers\r\n\r\nThe list below covers the most common waste patterns and should be your starting point — but it is **not exhaustive**. If you spot a different concrete inefficiency in this session that the user could change next time, raise it. Don't force-fit a session into a listed lever, and don't ignore real waste just because it isn't named here.\r\n\r\n**Out of scope — do NOT suggest these.** Anything about *how Claude itself works internally* is not under user control and must not appear in the output:\r\n\r\n - How Claude batches or sequences tool calls (e.g. \"group edits per file\", \"batch related changes into one pass\", \"make parallel tool calls\").\r\n - How Claude searches, reads, or explores (e.g. \"use Grep before Read\", \"use Glob to narrow first\").\r\n - How Claude reasons, plans, or thinks.\r\n - Anything the user would have to micromanage Claude into doing turn-by-turn — even if technically prompt-able, it isn't a realistic next-time change.\r\n\r\nStick to changes the user makes in *their* prompt, *their* settings, or *their* session structure.\r\n\r\n### Better initial prompt\r\nThe single biggest lever — vague prompts cause exploration, wrong-problem code, and correction rounds.\r\n\r\n - Scope the task, name the file (`@path/to/file`), describe the symptom and likely location, reference an existing pattern.\r\n - Provide verification criteria upfront: tests, expected outputs, screenshots, error messages.\r\n - Paste rich content directly (errors, logs, screenshots, URLs) instead of asking Claude to reproduce, search, or guess.\r\n - Use plan mode for uncertain / multi-file work; skip it for trivial fixes.\r\n\r\n### Context management\r\nEach later turn re-pays the full prior context.\r\n\r\n - `/clear` between unrelated tasks (\"kitchen sink session\" anti-pattern) — often the biggest win.\r\n - `/compact <focus>` with explicit preserve-instructions before context drifts past ~50%.\r\n - `/rewind` to redirect early. After two failed corrections, `/clear` and restart with a better prompt.\r\n - Subagents for investigation — they explore in a separate context and report a summary.\r\n - Bloated CLAUDE.md is loaded every turn. If the session shows Claude ignoring a rule that's *in* CLAUDE.md, the file is probably too long.\r\n\r\n\r\n### Request settings\r\nVisible in `<summary>`.\r\n\r\n - **Model**: Opus used for work Sonnet or Haiku could have done.\r\n - **Caching**: a low cache-hit ratio means re-paying for the same context every turn. Bursty sessions, mid-session CLAUDE.md edits, tool-list changes, or long idle gaps bust the cache.\r\n - **5m vs 1h cache**: 5m is the right default for almost all sessions and should be used most of the time. A 1h write costs ~2× a 5m write, so it only pays off in narrow cases — when the session reliably has gaps >5 min between turns (scheduled / background tasks, long manual review breaks, sessions resumed hours later). Most interactive coding does NOT qualify. Only flag 1h-caching as a fix when the activity timestamps in `<summary>` clearly show long gaps AND the cache-hit ratio is poor. Conversely, if the session is tight (<5 min gaps) but `Cache write (1h)` is non-trivial, that's overspend — recommend dropping back to 5m.\r\n - **Unused system tools** (web search/fetch, MCP, bash, etc.) add tokens every turn even when they don't fire — disable what wasn't needed. Expensive web-search/fetch loops can also be short-circuited by pasting a URL or doc.\r\n - **Context cap**: 1M-context for work that fits in 200k makes every cached token more expensive.\r\n\r\n\r\n\r\n# Input\r\n\r\n`<summary>` is rich — the app pre-computes it for you. Read every field and reason from numbers, not vibes\r\n`<transcript>` shows the actual turns, labeled with per-turn token deltas like `(+12k / 340k tokens)`. Use the deltas to find expensive moments and to ground each recommendation in a concrete event. **Instructions loaded** rows are CLAUDE.md / memory files Claude Code injected into the system prompt — they re-pay on every turn, so their size and relevance are a direct cost lever.\r\n{{TRUNCATION_NOTE}}\r\n\r\n# How to analyze\r\nSilently — don't show your work.\r\n\r\n 1. **Read `<summary>` first.** Several recommendations come straight from these numbers without needing the transcript.\r\n 2. **Then scan `<transcript>`** for the 2–4 most expensive moments via token deltas. What was happening? Was the prompt vague? Was context already large?\r\n 3. Cross-reference: do the transcript moments confirm or contradict the `<summary>` signals? E.g. low cache-hit ratio + bursty activity timestamps + small mid-session edits → cache-busting pattern.\r\n 4. Match findings against the levers above, OR identify other concrete inefficiencies the user could fix. Pick up to 3 changes that would have saved the most tokens *for this specific session*.\r\n 5. Drop any item you can't tie to a concrete signal (a number from `<summary>` or a moment in `<transcript>`).\r\n 6. Estimate a single combined savings figure (conservative). No per-item dollars.\r\n\r\n# Output\r\n\r\n## Rules\r\n - Audience: a technical user in a hurry. Skimmable in ~10 seconds. No preamble, no closing remarks, no hedging. Avoid jargon, model internals, and acronyms.\r\n - Each item: short instruction and why, tied to this session. No second sentence, no dollar figure.\r\n - Rank by impact. Fewer items are fine — one strong item beats many weak ones.\r\n - If the session was already efficient (no meaningful waste), output the one-line summary, then `I don't see any meaningful savings.` on its own line, and stop. Do not invent items to fill the list.\r\n\r\n## Format\r\n\r\n<one-line session summary, ≤20 words>\r\n\r\n**How to optimize?**\r\n\r\nN. **<instruction, ≤8 words>** — <why, tied to this session, ≤10 words>\r\n\r\n**Estimated savings: X% (about $X)**\r\n").replace("{{TRUNCATION_NOTE}}", ` - Note: long content was truncated for display (lines >200 chars, blocks >10 lines). The original session had the full content — base advice on what was clearly happening, not on the truncation.`);
4172
+ var PROMPT = String("# Role\r\n\r\nYou are reviewing a Claude Code session and giving the user practical advice on how to run a similar session more cheaply next time.\r\n\r\nThe user controls the prompts and the request settings around them: **model** (Opus / Sonnet / Haiku), **prompt caching**, enabled **system tools** (web search, web fetch, bash, MCP, etc.), context-window size, and CLAUDE.md. They cannot change Claude's internal reasoning. Look for missed opportunities to apply Anthropic's official Claude Code guidance.\r\n\r\n# Levers\r\n\r\nThe list below covers the most common waste patterns and should be your starting point — but it is **not exhaustive**. If you spot a different concrete inefficiency in this session that the user could change next time, raise it. Don't force-fit a session into a listed lever, and don't ignore real waste just because it isn't named here.\r\n\r\n**Out of scope — do NOT suggest these.** Anything about *how Claude itself works internally* is not under user control and must not appear in the output:\r\n\r\n - How Claude batches or sequences tool calls (e.g. \"group edits per file\", \"batch related changes into one pass\", \"make parallel tool calls\").\r\n - How Claude searches, reads, or explores (e.g. \"use Grep before Read\", \"use Glob to narrow first\").\r\n - How Claude reasons, plans, or thinks.\r\n - Anything the user would have to micromanage Claude into doing turn-by-turn — even if technically prompt-able, it isn't a realistic next-time change.\r\n\r\nStick to changes the user makes in *their* prompt, *their* settings, or *their* session structure.\r\n\r\n### Better initial prompt\r\nThe single biggest lever — vague prompts cause exploration, wrong-problem code, and correction rounds.\r\n\r\n - Scope the task, name the file (`@path/to/file`), describe the symptom and likely location, reference an existing pattern.\r\n - Provide verification criteria upfront: tests, expected outputs, screenshots, error messages.\r\n - Paste rich content directly (errors, logs, screenshots, URLs) instead of asking Claude to reproduce, search, or guess.\r\n - Use plan mode for uncertain / multi-file work; skip it for trivial fixes.\r\n\r\n### Context management\r\nEach later turn re-pays the full prior context.\r\n\r\n - `/clear` between unrelated tasks (\"kitchen sink session\" anti-pattern) — often the biggest win.\r\n - `/compact <focus>` with explicit preserve-instructions before context drifts past ~50%.\r\n - `/rewind` to redirect early. After two failed corrections, `/clear` and restart with a better prompt.\r\n - Subagents for investigation — they explore in a separate context and report a summary.\r\n - Bloated CLAUDE.md is loaded every turn. If the session shows Claude ignoring a rule that's *in* CLAUDE.md, the file is probably too long.\r\n\r\n\r\n### Request settings\r\nVisible in `<summary>`.\r\n\r\n - **Model**: Opus used for work Sonnet or Haiku could have done.\r\n - **Caching**: a low cache-hit ratio means re-paying for the same context every turn. Bursty sessions, mid-session CLAUDE.md edits, tool-list changes, or long idle gaps bust the cache.\r\n - **5m vs 1h cache**: 5m is the right default for almost all sessions and should be used most of the time. A 1h write costs ~2× a 5m write, so it only pays off in narrow cases — when the session reliably has gaps >5 min between turns (scheduled / background tasks, long manual review breaks, sessions resumed hours later). Most interactive coding does NOT qualify. Only flag 1h-caching as a fix when the activity timestamps in `<summary>` clearly show long gaps AND the cache-hit ratio is poor. Conversely, if the session is tight (<5 min gaps) but `Cache write (1h)` is non-trivial, that's overspend — recommend dropping back to 5m.\r\n - **Unused system tools** (web search/fetch, MCP, bash, etc.) add tokens every turn even when they don't fire — disable what wasn't needed. Expensive web-search/fetch loops can also be short-circuited by pasting a URL or doc.\r\n - **Context cap**: 1M-context for work that fits in 200k makes every cached token more expensive.\r\n\r\n\r\n\r\n# Input\r\n\r\n`<summary>` is rich — the app pre-computes it for you. Read every field and reason from numbers, not vibes\r\n`<transcript>` shows the actual turns, labeled with per-turn token deltas like `(+12k / 340k tokens)`. Use the deltas to find expensive moments and to ground each recommendation in a concrete event. **System** rows listing the system prompt, tools and CLAUDE.md / memory files are what Claude Code sent along with the conversation — they re-pay on every turn, so their size and relevance are a direct cost lever. **Proxy** blocks are side requests (title, auto-mode check, …): separate sessions, not in the conversation's context.\r\n{{TRUNCATION_NOTE}}\r\n\r\n# How to analyze\r\nSilently — don't show your work.\r\n\r\n 1. **Read `<summary>` first.** Several recommendations come straight from these numbers without needing the transcript.\r\n 2. **Then scan `<transcript>`** for the 2–4 most expensive moments via token deltas. What was happening? Was the prompt vague? Was context already large?\r\n 3. Cross-reference: do the transcript moments confirm or contradict the `<summary>` signals? E.g. low cache-hit ratio + bursty activity timestamps + small mid-session edits → cache-busting pattern.\r\n 4. Match findings against the levers above, OR identify other concrete inefficiencies the user could fix. Pick up to 3 changes that would have saved the most tokens *for this specific session*.\r\n 5. Drop any item you can't tie to a concrete signal (a number from `<summary>` or a moment in `<transcript>`).\r\n 6. Estimate a single combined savings figure (conservative). No per-item dollars.\r\n\r\n# Output\r\n\r\n## Rules\r\n - Audience: a technical user in a hurry. Skimmable in ~10 seconds. No preamble, no closing remarks, no hedging. Avoid jargon, model internals, and acronyms.\r\n - Each item: short instruction and why, tied to this session. No second sentence, no dollar figure.\r\n - Rank by impact. Fewer items are fine — one strong item beats many weak ones.\r\n - If the session was already efficient (no meaningful waste), output the one-line summary, then `I don't see any meaningful savings.` on its own line, and stop. Do not invent items to fill the list.\r\n\r\n## Format\r\n\r\n<one-line session summary, ≤20 words>\r\n\r\n**How to optimize?**\r\n\r\nN. **<instruction, ≤8 words>** — <why, tied to this session, ≤10 words>\r\n\r\n**Estimated savings: X% (about $X)**\r\n").replace("{{TRUNCATION_NOTE}}", ` - Note: long content was truncated for display (lines >200 chars, blocks >10 lines). The original session had the full content — base advice on what was clearly happening, not on the truncation.`);
4161
4173
  function useAgent(resetKey) {
4162
4174
  const [running, setRunning] = (0, import_react.useState)(false);
4163
4175
  const [output, setOutput] = (0, import_react.useState)("");
@@ -4207,7 +4219,7 @@ var TABS = [
4207
4219
  ];
4208
4220
  function Session({ meta, missing, date, granularity = "day", onShowPeriodOf }) {
4209
4221
  const [items, setItems] = (0, import_react.useState)(null);
4210
- const [instructions, setInstructions] = (0, import_react.useState)([]);
4222
+ const [instructions, setInstructions] = (0, import_react.useState)({});
4211
4223
  const [error, setError] = (0, import_react.useState)(null);
4212
4224
  const [mode, setMode] = useLocalStorage("session.view-mode", "conversation");
4213
4225
  const [expandAll, setExpandAll] = (0, import_react.useState)(null);
@@ -4217,7 +4229,7 @@ function Session({ meta, missing, date, granularity = "day", onShowPeriodOf }) {
4217
4229
  const agent = useAgent(`${filePath}|${date}`);
4218
4230
  (0, import_react.useEffect)(() => {
4219
4231
  setItems(null);
4220
- setInstructions([]);
4232
+ setInstructions({});
4221
4233
  setError(null);
4222
4234
  }, [
4223
4235
  filePath,
@@ -4230,7 +4242,7 @@ function Session({ meta, missing, date, granularity = "day", onShowPeriodOf }) {
4230
4242
  window.api.readSession(filePath, date || null, granularity).then((res) => {
4231
4243
  if (cancelled || !res) return;
4232
4244
  setItems(res.items);
4233
- setInstructions(res.instructions || []);
4245
+ setInstructions(res.instructions || {});
4234
4246
  setError(null);
4235
4247
  }, (err) => {
4236
4248
  if (!cancelled) setError(err);
@@ -4290,7 +4302,6 @@ function Session({ meta, missing, date, granularity = "day", onShowPeriodOf }) {
4290
4302
  children: items ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ConversationView, {
4291
4303
  items,
4292
4304
  instructions,
4293
- desktop,
4294
4305
  filePath: meta.parentFilePath ?? meta.filePath,
4295
4306
  expandAll,
4296
4307
  continuesFrom: meta.continuesFrom,
@@ -4421,11 +4432,10 @@ function StatusSwitch({ service, on, warn, button, className = "", tooltip, chan
4421
4432
  }
4422
4433
  //#endregion
4423
4434
  //#region src/renderer/ui/StatusBar.jsx
4424
- var proxyTooltip = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Captures Claude Code's raw API traffic - system prompt, tool definitions, injected reminders, responses." }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Applies to running Claude Code sessions too." })] });
4435
+ var proxyTooltip = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Captures Claude Code's raw API traffic - requests and responses, including the side requests (title, auto-mode checks, …) the transcript doesn't log." }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Applies to running Claude Code sessions too." })] });
4425
4436
  var proxyChanges = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
4426
4437
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { children: `"env": {
4427
- "ANTHROPIC_BASE_URL": "http://127.0.0.1:41414",
4428
- "ENABLE_TOOL_SEARCH": "true"
4438
+ "ANTHROPIC_BASE_URL": "http://127.0.0.1:41414"
4429
4439
  }` }),
4430
4440
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Also registers the proxy as a login service so it survives restarts." }),
4431
4441
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: [
@@ -6,11 +6,11 @@
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-C318t5Qw.js"></script>
9
+ <script type="module" crossorigin src="./assets/index-CUHQx2Tz.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-Bk_YeHJf.js">
11
11
  <link rel="modulepreload" crossorigin href="./assets/vendors-DZb2Jdmj.js">
12
12
  <link rel="stylesheet" crossorigin href="./assets/vendors-CXTqG1IV.css">
13
- <link rel="stylesheet" crossorigin href="./assets/index-CE80Hk93.css">
13
+ <link rel="stylesheet" crossorigin href="./assets/index-BrlHmtTw.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-discover",
3
- "version": "1.10.9",
3
+ "version": "1.10.10",
4
4
  "type": "module",
5
5
  "description": "Claude Discover — a desktop tool to browse your local Claude Code sessions: conversations, token costs, and captured API traffic",
6
6
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  "publish:patch": "npm test && npm version patch && git push --follow-tags && echo Approve once CI is green: https://www.npmjs.com/settings/g.dichev/staged-packages"
32
32
  },
33
33
  "dependencies": {
34
- "@anthropic-ai/claude-agent-sdk": "^0.3.302",
34
+ "@anthropic-ai/claude-agent-sdk": "^0.3.304",
35
35
  "electron": "44.8.0",
36
36
  "electron-context-menu": "^4.1.2",
37
37
  "electron-find-overlay": "^1.0.1",