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 +1 -1
- package/bin/proxy.config.js +1 -0
- package/bin/proxy.mjs +5 -4
- package/out/main/main.js +97 -31
- package/out/renderer/assets/{index-CE80Hk93.css → index-BrlHmtTw.css} +24 -15
- package/out/renderer/assets/{index-C318t5Qw.js → index-CUHQx2Tz.js} +87 -77
- package/out/renderer/index.html +2 -2
- package/package.json +2 -2
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 -
|
|
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.
|
package/bin/proxy.config.js
CHANGED
|
@@ -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
|
|
120
|
-
if (!
|
|
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 = (
|
|
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
|
|
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
|
|
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
|
|
945
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1915
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
/*
|
|
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)
|
|
1230
|
-
|
|
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
|
-
|
|
1240
|
-
.instruction-
|
|
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.
|
|
50
|
-
const [MAJOR, MINOR, PATCH] = "1.10.
|
|
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.
|
|
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)("
|
|
1481
|
+
return src && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1482
1482
|
className: "session-avatar",
|
|
1483
|
-
|
|
1484
|
-
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
3110
|
-
className: "msg-
|
|
3111
|
-
children:
|
|
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
|
|
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:
|
|
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 &&
|
|
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 ${
|
|
3643
|
-
|
|
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
|
-
|
|
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. **
|
|
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 -
|
|
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: [
|
package/out/renderer/index.html
CHANGED
|
@@ -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-
|
|
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-
|
|
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.
|
|
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.
|
|
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",
|