claude-discover 1.10.3 → 1.10.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/claude/statusline.mjs +125 -125
- package/bin/claude-discover.mjs +32 -1
- package/bin/proxy.mjs +18 -3
- package/bin/update.mjs +42 -42
- package/out/main/main.js +20 -90
- package/out/renderer/assets/{index-CTIYCpXd.css → index-BMh5255F.css} +41 -1
- package/out/renderer/assets/{index-By2RDJFe.js → index-CHnuzPLr.js} +70 -26
- package/out/renderer/index.html +2 -2
- package/package.json +11 -5
- package/out/preload/findPreload.cjs +0 -16
- package/out/renderer/assets/find-CxcUmpSm.css +0 -88
- package/out/renderer/assets/find-DSEw-KLs.js +0 -59
- package/out/renderer/find/find.html +0 -26
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Claude Code status line, installed by StatuslineSwitch. Reads the hook JSON from stdin and
|
|
3
|
-
// prints one colored line — model and effort, context usage, token totals, rate-limit windows:
|
|
4
|
-
// [Opus 4.8 high] Context: ▓▓▓░░░░░░░ 32% used (45.2k, 87% cached) | Tokens: 1.2M total (+45.2k, 3 turns) | Daily: 42% used (resets in 2h 30m)
|
|
5
|
-
|
|
6
|
-
import { readFileSync } from 'node:fs'
|
|
7
|
-
import { styleText } from 'node:util'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// Formatting
|
|
11
|
-
const THRESHOLDS = { // (white, yellow, red)
|
|
12
|
-
context: [0, 100_000, 250_000],
|
|
13
|
-
limit: [0, 60, 90],
|
|
14
|
-
cache: [90, 60, 0],
|
|
15
|
-
tokens: [0, 250_000, 1_000_000],
|
|
16
|
-
session: [0, 3_000_000, 8_000_000],
|
|
17
|
-
turns: [0, 10, 25],
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const NO_COLOR = process.env.NO_COLOR !== undefined // standard opt-out; styleText can't honor it for piped output, so gate here
|
|
21
|
-
|
|
22
|
-
function colorize(kind, text, value = 0) {
|
|
23
|
-
const bounds = THRESHOLDS[kind]
|
|
24
|
-
const n = bounds.filter(b => value >= b).length
|
|
25
|
-
const TIERS = [null, 'yellow', 'red'] // indexed by severity tier
|
|
26
|
-
const idx = bounds[0] < bounds.at(-1) ? n - 1 : bounds.length - n
|
|
27
|
-
const color = TIERS[idx]
|
|
28
|
-
// validateStream:false forces codes through the pipe (statusline output isn't a TTY)
|
|
29
|
-
return color && !NO_COLOR ? styleText(color, text, { validateStream: false }) : text
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// Collect stdin data
|
|
34
|
-
const data = JSON.parse(readFileSync(0, 'utf-8')) // fd 0 = stdin
|
|
35
|
-
const modelName = data.model?.display_name?.replace('Claude ', '') || '?'
|
|
36
|
-
const effort = data.effort?.level // absent when the model doesn't support effort
|
|
37
|
-
const parts = []
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// Context window
|
|
42
|
-
const ctx = data.context_window || {}
|
|
43
|
-
const total = ctx.context_window_size || 0
|
|
44
|
-
if (total) {
|
|
45
|
-
const usage = ctx.current_usage || {}
|
|
46
|
-
const hit = usage.cache_read_input_tokens || 0
|
|
47
|
-
const used = hit + (usage.cache_creation_input_tokens || 0) + (usage.input_tokens || 0)
|
|
48
|
-
const pct = Math.round((used / total) * 100)
|
|
49
|
-
const cpct = used ? Math.floor((hit * 100) / used) : 0
|
|
50
|
-
const filled = Math.floor(pct / 10)
|
|
51
|
-
const bar = '▓'.repeat(filled) + '░'.repeat(10 - filled)
|
|
52
|
-
const head = colorize('context', `${bar} ${pct}% used`, used)
|
|
53
|
-
const tokens = colorize('context', `${(used / 1000).toFixed(1)}k`, used)
|
|
54
|
-
const cached = used ? colorize('cache', `${cpct}% cached`, cpct) : `${cpct}% cached`
|
|
55
|
-
parts.push(`Context: ${head} (${tokens}, ${cached})`)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// Total tokens (per session and per reply loop)
|
|
61
|
-
function agentLoopUsage(transcriptPath) { // Read the transcript JSONL and sum API usage for the current agent loop and the whole session.
|
|
62
|
-
const lines = readFileSync(transcriptPath, 'utf-8').split(/\r?\n/).filter(Boolean)
|
|
63
|
-
let loop = { total: 0, turns: 0 } // last prompt-reply breakdown
|
|
64
|
-
const session = { total: 0, turns: 0 } // whole-session breakdown
|
|
65
|
-
let seen = new Set() // One API response spans multiple lines (one per content block), each repeating the usage block, so dedupe by message.id.
|
|
66
|
-
|
|
67
|
-
for (const line of lines) {
|
|
68
|
-
let e
|
|
69
|
-
try { e = JSON.parse(line) } catch { continue } // the transcript may be mid-append — skip a half-written line
|
|
70
|
-
const msg = e.message || {}
|
|
71
|
-
|
|
72
|
-
if (e.type === 'user' && !e.isMeta) {
|
|
73
|
-
const isToolReply = Array.isArray(msg.content) && msg.content.some(b => b.type === 'tool_result')
|
|
74
|
-
if (!isToolReply) { // real user prompt = new loop
|
|
75
|
-
loop = { total: 0, turns: 0 }
|
|
76
|
-
seen = new Set()
|
|
77
|
-
}
|
|
78
|
-
} else if (e.type === 'assistant' && !seen.has(msg.id)) {
|
|
79
|
-
seen.add(msg.id)
|
|
80
|
-
const u = msg.usage || {}
|
|
81
|
-
const tokens = (u.input_tokens || 0) + (u.output_tokens || 0) + (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0)
|
|
82
|
-
for (const d of [loop, session]) {
|
|
83
|
-
d.total += tokens
|
|
84
|
-
d.turns += 1
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return [loop, session]
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (data.transcript_path) {
|
|
92
|
-
const [loop, session] = agentLoopUsage(data.transcript_path)
|
|
93
|
-
const fmt = n => n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : `${(n / 1000).toFixed(1)}k`
|
|
94
|
-
const tokensS = colorize('tokens', fmt(loop.total), loop.total)
|
|
95
|
-
const sessS = colorize('session', fmt(session.total), session.total)
|
|
96
|
-
const turnsS = colorize('turns', `${loop.turns} turn${loop.turns !== 1 ? 's' : ''}`, loop.turns)
|
|
97
|
-
parts.push(`Tokens: ${sessS} total (+${tokensS}, ${turnsS})`)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// Rate limits (5-hour and weekly windows)
|
|
103
|
-
function fmtReset(resetsAt) { // Format remaining time until a unix-epoch reset, e.g. "2h 30m" (null if no timestamp).
|
|
104
|
-
if (!resetsAt) return null
|
|
105
|
-
const secs = Math.max(0, Math.floor(Number(resetsAt)) - Math.floor(Date.now() / 1000))
|
|
106
|
-
const min = Math.floor(secs / 60) % 60
|
|
107
|
-
const hrs = Math.floor(secs / 3600) % 24
|
|
108
|
-
const days = Math.floor(secs / 86400)
|
|
109
|
-
if (days) return `${days}d ${hrs}h`
|
|
110
|
-
if (hrs) return `${hrs}h ${min}m`
|
|
111
|
-
return `${min}m`
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const rl = data.rate_limits || {}
|
|
115
|
-
for (const [label, window] of [['Daily', rl.five_hour], ['Weekly', rl.seven_day]]) {
|
|
116
|
-
if (!window) continue
|
|
117
|
-
const pct = Math.floor(window.used_percentage || 0)
|
|
118
|
-
const reset = fmtReset(window.resets_at)
|
|
119
|
-
const resetStr = reset ? ` (resets in ${reset})` : ''
|
|
120
|
-
parts.push(`${label}: ${colorize('limit', `${pct}% used${resetStr}`, pct)}`)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// Status line
|
|
125
|
-
process.stdout.write(`[${modelName}${effort ? ` ${effort}` : ''}] ` + parts.join(' | ') + '\n')
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code status line, installed by StatuslineSwitch. Reads the hook JSON from stdin and
|
|
3
|
+
// prints one colored line — model and effort, context usage, token totals, rate-limit windows:
|
|
4
|
+
// [Opus 4.8 high] Context: ▓▓▓░░░░░░░ 32% used (45.2k, 87% cached) | Tokens: 1.2M total (+45.2k, 3 turns) | Daily: 42% used (resets in 2h 30m)
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
import { styleText } from 'node:util'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
// Formatting
|
|
11
|
+
const THRESHOLDS = { // (white, yellow, red)
|
|
12
|
+
context: [0, 100_000, 250_000],
|
|
13
|
+
limit: [0, 60, 90],
|
|
14
|
+
cache: [90, 60, 0],
|
|
15
|
+
tokens: [0, 250_000, 1_000_000],
|
|
16
|
+
session: [0, 3_000_000, 8_000_000],
|
|
17
|
+
turns: [0, 10, 25],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const NO_COLOR = process.env.NO_COLOR !== undefined // standard opt-out; styleText can't honor it for piped output, so gate here
|
|
21
|
+
|
|
22
|
+
function colorize(kind, text, value = 0) {
|
|
23
|
+
const bounds = THRESHOLDS[kind]
|
|
24
|
+
const n = bounds.filter(b => value >= b).length
|
|
25
|
+
const TIERS = [null, 'yellow', 'red'] // indexed by severity tier
|
|
26
|
+
const idx = bounds[0] < bounds.at(-1) ? n - 1 : bounds.length - n
|
|
27
|
+
const color = TIERS[idx]
|
|
28
|
+
// validateStream:false forces codes through the pipe (statusline output isn't a TTY)
|
|
29
|
+
return color && !NO_COLOR ? styleText(color, text, { validateStream: false }) : text
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
// Collect stdin data
|
|
34
|
+
const data = JSON.parse(readFileSync(0, 'utf-8')) // fd 0 = stdin
|
|
35
|
+
const modelName = data.model?.display_name?.replace('Claude ', '') || '?'
|
|
36
|
+
const effort = data.effort?.level // absent when the model doesn't support effort
|
|
37
|
+
const parts = []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
// Context window
|
|
42
|
+
const ctx = data.context_window || {}
|
|
43
|
+
const total = ctx.context_window_size || 0
|
|
44
|
+
if (total) {
|
|
45
|
+
const usage = ctx.current_usage || {}
|
|
46
|
+
const hit = usage.cache_read_input_tokens || 0
|
|
47
|
+
const used = hit + (usage.cache_creation_input_tokens || 0) + (usage.input_tokens || 0)
|
|
48
|
+
const pct = Math.round((used / total) * 100)
|
|
49
|
+
const cpct = used ? Math.floor((hit * 100) / used) : 0
|
|
50
|
+
const filled = Math.floor(pct / 10)
|
|
51
|
+
const bar = '▓'.repeat(filled) + '░'.repeat(10 - filled)
|
|
52
|
+
const head = colorize('context', `${bar} ${pct}% used`, used)
|
|
53
|
+
const tokens = colorize('context', `${(used / 1000).toFixed(1)}k`, used)
|
|
54
|
+
const cached = used ? colorize('cache', `${cpct}% cached`, cpct) : `${cpct}% cached`
|
|
55
|
+
parts.push(`Context: ${head} (${tokens}, ${cached})`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
// Total tokens (per session and per reply loop)
|
|
61
|
+
function agentLoopUsage(transcriptPath) { // Read the transcript JSONL and sum API usage for the current agent loop and the whole session.
|
|
62
|
+
const lines = readFileSync(transcriptPath, 'utf-8').split(/\r?\n/).filter(Boolean)
|
|
63
|
+
let loop = { total: 0, turns: 0 } // last prompt-reply breakdown
|
|
64
|
+
const session = { total: 0, turns: 0 } // whole-session breakdown
|
|
65
|
+
let seen = new Set() // One API response spans multiple lines (one per content block), each repeating the usage block, so dedupe by message.id.
|
|
66
|
+
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
let e
|
|
69
|
+
try { e = JSON.parse(line) } catch { continue } // the transcript may be mid-append — skip a half-written line
|
|
70
|
+
const msg = e.message || {}
|
|
71
|
+
|
|
72
|
+
if (e.type === 'user' && !e.isMeta) {
|
|
73
|
+
const isToolReply = Array.isArray(msg.content) && msg.content.some(b => b.type === 'tool_result')
|
|
74
|
+
if (!isToolReply) { // real user prompt = new loop
|
|
75
|
+
loop = { total: 0, turns: 0 }
|
|
76
|
+
seen = new Set()
|
|
77
|
+
}
|
|
78
|
+
} else if (e.type === 'assistant' && !seen.has(msg.id)) {
|
|
79
|
+
seen.add(msg.id)
|
|
80
|
+
const u = msg.usage || {}
|
|
81
|
+
const tokens = (u.input_tokens || 0) + (u.output_tokens || 0) + (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0)
|
|
82
|
+
for (const d of [loop, session]) {
|
|
83
|
+
d.total += tokens
|
|
84
|
+
d.turns += 1
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return [loop, session]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (data.transcript_path) {
|
|
92
|
+
const [loop, session] = agentLoopUsage(data.transcript_path)
|
|
93
|
+
const fmt = n => n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : `${(n / 1000).toFixed(1)}k`
|
|
94
|
+
const tokensS = colorize('tokens', fmt(loop.total), loop.total)
|
|
95
|
+
const sessS = colorize('session', fmt(session.total), session.total)
|
|
96
|
+
const turnsS = colorize('turns', `${loop.turns} turn${loop.turns !== 1 ? 's' : ''}`, loop.turns)
|
|
97
|
+
parts.push(`Tokens: ${sessS} total (+${tokensS}, ${turnsS})`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
// Rate limits (5-hour and weekly windows)
|
|
103
|
+
function fmtReset(resetsAt) { // Format remaining time until a unix-epoch reset, e.g. "2h 30m" (null if no timestamp).
|
|
104
|
+
if (!resetsAt) return null
|
|
105
|
+
const secs = Math.max(0, Math.floor(Number(resetsAt)) - Math.floor(Date.now() / 1000))
|
|
106
|
+
const min = Math.floor(secs / 60) % 60
|
|
107
|
+
const hrs = Math.floor(secs / 3600) % 24
|
|
108
|
+
const days = Math.floor(secs / 86400)
|
|
109
|
+
if (days) return `${days}d ${hrs}h`
|
|
110
|
+
if (hrs) return `${hrs}h ${min}m`
|
|
111
|
+
return `${min}m`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const rl = data.rate_limits || {}
|
|
115
|
+
for (const [label, window] of [['Daily', rl.five_hour], ['Weekly', rl.seven_day]]) {
|
|
116
|
+
if (!window) continue
|
|
117
|
+
const pct = Math.floor(window.used_percentage || 0)
|
|
118
|
+
const reset = fmtReset(window.resets_at)
|
|
119
|
+
const resetStr = reset ? ` (resets in ${reset})` : ''
|
|
120
|
+
parts.push(`${label}: ${colorize('limit', `${pct}% used${resetStr}`, pct)}`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
// Status line
|
|
125
|
+
process.stdout.write(`[${modelName}${effort ? ` ${effort}` : ''}] ` + parts.join(' | ') + '\n')
|
package/bin/claude-discover.mjs
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// Electron binary at the package root, whose package.json main is out/main/main.js.
|
|
4
4
|
import { spawn } from 'node:child_process'
|
|
5
5
|
import { existsSync } from 'node:fs'
|
|
6
|
+
import { access, constants } from 'node:fs/promises'
|
|
6
7
|
import { fileURLToPath } from 'node:url'
|
|
7
|
-
import electron from 'electron' // under plain node the electron package exports its binary path
|
|
8
8
|
|
|
9
9
|
const appDir = fileURLToPath(new URL('..', import.meta.url))
|
|
10
10
|
if (!existsSync(new URL('../out/main/main.js', import.meta.url))) {
|
|
@@ -12,5 +12,36 @@ if (!existsSync(new URL('../out/main/main.js', import.meta.url))) {
|
|
|
12
12
|
process.exit(1)
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
|
|
16
|
+
if (process.platform === 'linux') { // @linux
|
|
17
|
+
// Importing electron downloads its binary on first run, which can't unzip into a root-owned `sudo npm i -g` install,
|
|
18
|
+
// so it's imported dynamically, only after this check (a static import is hoisted and would run first)
|
|
19
|
+
|
|
20
|
+
const electronDir = new URL('.', import.meta.resolve('electron'))
|
|
21
|
+
const isWritable = await access(electronDir, constants.W_OK).then(() => true, () => false)
|
|
22
|
+
if (!existsSync(new URL('path.txt', electronDir)) && !isWritable) {
|
|
23
|
+
console.error(`
|
|
24
|
+
Electron can't finish installing — its folder isn't writable by your user.
|
|
25
|
+
|
|
26
|
+
Global npm packages shouldn't need sudo. The recommended setup is Node from a version
|
|
27
|
+
manager like nvm, which keeps global npm packages in your home folder:
|
|
28
|
+
|
|
29
|
+
sudo npm uninstall -g claude-discover
|
|
30
|
+
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash
|
|
31
|
+
|
|
32
|
+
Then open a new terminal and run:
|
|
33
|
+
|
|
34
|
+
nvm install --lts
|
|
35
|
+
npm install -g claude-discover
|
|
36
|
+
claude-discover
|
|
37
|
+
|
|
38
|
+
See https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally`)
|
|
39
|
+
process.exit(1)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
const { default: electron } = await import('electron') // under plain node the electron package exports its binary path
|
|
45
|
+
|
|
15
46
|
spawn(electron, [appDir, ...process.argv.slice(2)], { stdio: 'inherit' })
|
|
16
47
|
.on('close', code => process.exit(code ?? 0))
|
package/bin/proxy.mjs
CHANGED
|
@@ -16,20 +16,25 @@ import { parseArgs } from 'node:util'
|
|
|
16
16
|
|
|
17
17
|
import { HOST, PORT, PROXY_URL, UPSTREAM, EXIT_ROUTE, PING_ROUTE, PING_RESPONSE, REQUESTS_DIR, ERROR_LOG_PATH } from './proxy.config.js'
|
|
18
18
|
|
|
19
|
+
// How long an exiting proxy keeps serving in-flight responses — long replies stream for minutes
|
|
20
|
+
const DRAIN_TIMEOUT_MS = 10 * 60_000
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
// ── 1. Generic tee proxy — knows nothing about Claude ────────────────────────
|
|
21
24
|
// Forwards verbatim to `upstream` and pipes the response back while buffering both bodies.
|
|
22
25
|
// `onExchange(exchange)` fires exactly once per request, after piping (its response fields are
|
|
23
|
-
// undefined when upstream never responded); `errorBody(err)` shapes the 502 when it's unreachable
|
|
26
|
+
// undefined when upstream never responded); `errorBody(err)` shapes the 502 when it's unreachable;
|
|
27
|
+
// `onExit()` runs once the exit route has answered.
|
|
24
28
|
|
|
25
|
-
function createProxy({ upstream, onExchange, onError, errorBody }) {
|
|
29
|
+
function createProxy({ upstream, onExchange, onError, errorBody, onExit }) {
|
|
26
30
|
const mod = upstream.protocol === 'http:' ? http : https
|
|
27
31
|
|
|
28
32
|
return http.createServer(async (req, res) => {
|
|
29
33
|
if (req.method === 'POST' && req.url === EXIT_ROUTE) {
|
|
30
34
|
// Loopback-only control route. The restart client is a bare Node request; reject anything carrying browser fetch metadata so a web page can't CSRF the proxy into exiting.
|
|
31
35
|
if (req.headers.origin || req.headers.referer || req.headers['sec-fetch-site']) return res.writeHead(403).end('forbidden')
|
|
32
|
-
|
|
36
|
+
res.setHeader('connection', 'close')
|
|
37
|
+
return res.end('bye', onExit)
|
|
33
38
|
}
|
|
34
39
|
if (req.url === PING_ROUTE) return res.end(PING_RESPONSE)
|
|
35
40
|
const started = Date.now()
|
|
@@ -246,7 +251,17 @@ if (isMain) {
|
|
|
246
251
|
onExchange: logRequest,
|
|
247
252
|
onError: logError,
|
|
248
253
|
errorBody: err => JSON.stringify({ type: 'error', error: { type: 'api_error', message: `claude-discover proxy: upstream unreachable (${err.code || err.message})` } }),
|
|
254
|
+
onExit: () => drain(),
|
|
249
255
|
})
|
|
256
|
+
// Stop listening at once (frees the port for a successor) but let in-flight responses finish — exiting outright would cut a reply mid-stream
|
|
257
|
+
let draining = false
|
|
258
|
+
const drain = () => {
|
|
259
|
+
if (draining) return
|
|
260
|
+
draining = true
|
|
261
|
+
server.close(() => process.exit(0))
|
|
262
|
+
setTimeout(() => process.exit(0), DRAIN_TIMEOUT_MS)
|
|
263
|
+
}
|
|
264
|
+
process.on('SIGTERM', drain) // @macOS @linux launchd/systemd stop the service this way, right after ProxySwitch's exit request
|
|
250
265
|
if (args.values.restart) {
|
|
251
266
|
// Ask a previous instance (the port is loopback-only and assumed ours) to exit before listening.
|
|
252
267
|
const replaced = await new Promise(resolve => http.request(`${PROXY_URL}${EXIT_ROUTE}`, { method: 'POST' }, () => resolve(true)).on('error', () => resolve(false)).end())
|
package/bin/update.mjs
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
// Updates an `npm i -g` install from outside it. AutoUpdate starts it detached as the app quits:
|
|
2
|
-
// Windows locks the app's running electron.exe, so npm can only replace the package once the app is gone.
|
|
3
|
-
|
|
4
|
-
import { spawn, spawnSync } from 'node:child_process'
|
|
5
|
-
import path from 'node:path'
|
|
6
|
-
import { setTimeout as sleep } from 'node:timers/promises'
|
|
7
|
-
import { parseArgs } from 'node:util'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// ── 1. CLI, then wait for the app to exit ────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
const { values, positionals: [pid, version] } = parseArgs({
|
|
13
|
-
allowPositionals: true,
|
|
14
|
-
options: {
|
|
15
|
-
'no-reopen': { type: 'boolean' },
|
|
16
|
-
'help': { type: 'boolean', short: 'h' },
|
|
17
|
-
},
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
if (values.help || !version) {
|
|
21
|
-
console.error('Usage: node bin/update.mjs <pid> <version> [--no-reopen]')
|
|
22
|
-
process.exit(1)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
while (true) {
|
|
26
|
-
try {
|
|
27
|
-
process.kill(Number(pid), 0)
|
|
28
|
-
} catch (err) {
|
|
29
|
-
if (err.code !== 'EPERM') break // EPERM: alive, just not ours to signal
|
|
30
|
-
}
|
|
31
|
-
await sleep(200)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// ── 2. Update and reopen ─────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
const { status } = spawnSync(`npm i -g claude-discover@${version} --loglevel=http`, { shell: true, stdio: 'inherit' })
|
|
38
|
-
if (status !== 0) await sleep(5_000) // leaves npm's error readable before the window closes
|
|
39
|
-
|
|
40
|
-
// Reopen even after a failed update; dev passes --no-reopen, as its Vite renderer died with the app
|
|
41
|
-
const globalBin = path.join(spawnSync('npm root -g', { shell: true, encoding: 'utf8' }).stdout.trim(), 'claude-discover/bin/claude-discover.mjs')
|
|
42
|
-
if (!values['no-reopen']) spawn(process.execPath, [globalBin], { detached: true, stdio: 'ignore' }).unref()
|
|
1
|
+
// Updates an `npm i -g` install from outside it. AutoUpdate starts it detached as the app quits:
|
|
2
|
+
// Windows locks the app's running electron.exe, so npm can only replace the package once the app is gone.
|
|
3
|
+
|
|
4
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
7
|
+
import { parseArgs } from 'node:util'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
// ── 1. CLI, then wait for the app to exit ────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const { values, positionals: [pid, version] } = parseArgs({
|
|
13
|
+
allowPositionals: true,
|
|
14
|
+
options: {
|
|
15
|
+
'no-reopen': { type: 'boolean' },
|
|
16
|
+
'help': { type: 'boolean', short: 'h' },
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
if (values.help || !version) {
|
|
21
|
+
console.error('Usage: node bin/update.mjs <pid> <version> [--no-reopen]')
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
while (true) {
|
|
26
|
+
try {
|
|
27
|
+
process.kill(Number(pid), 0)
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err.code !== 'EPERM') break // EPERM: alive, just not ours to signal
|
|
30
|
+
}
|
|
31
|
+
await sleep(200)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
// ── 2. Update and reopen ─────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const { status } = spawnSync(`npm i -g claude-discover@${version} --loglevel=http`, { shell: true, stdio: 'inherit' })
|
|
38
|
+
if (status !== 0) await sleep(5_000) // leaves npm's error readable before the window closes
|
|
39
|
+
|
|
40
|
+
// Reopen even after a failed update; dev passes --no-reopen, as its Vite renderer died with the app
|
|
41
|
+
const globalBin = path.join(spawnSync('npm root -g', { shell: true, encoding: 'utf8' }).stdout.trim(), 'claude-discover/bin/claude-discover.mjs')
|
|
42
|
+
if (!values['no-reopen']) spawn(process.execPath, [globalBin], { detached: true, stdio: 'ignore' }).unref()
|
package/out/main/main.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { app, dialog, BrowserWindow, shell,
|
|
1
|
+
import { app, dialog, BrowserWindow, shell, nativeTheme, Menu, ipcMain } from "electron";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import os, { homedir } from "node:os";
|
|
4
4
|
import path, { join, dirname, basename } from "node:path";
|
|
@@ -14,6 +14,7 @@ import semver from "semver";
|
|
|
14
14
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
15
15
|
import windowStateKeeper from "electron-window-state";
|
|
16
16
|
import contextMenu from "electron-context-menu";
|
|
17
|
+
import { FindOverlay } from "electron-find-overlay";
|
|
17
18
|
class ConfigFile {
|
|
18
19
|
dataDir = join(homedir(), ".claude-discover");
|
|
19
20
|
configFile = join(this.dataDir, "config.json");
|
|
@@ -435,7 +436,7 @@ const MEMORY_LABELS = {
|
|
|
435
436
|
"user's private project instructions, not checked in": "Local",
|
|
436
437
|
"user's auto-memory, persists across conversations": "Auto"
|
|
437
438
|
};
|
|
438
|
-
const MEMORY_FILES = { User: "CLAUDE.md", Project: "CLAUDE.md", Local: "CLAUDE.local.md", Auto: "MEMORY.md" };
|
|
439
|
+
const MEMORY_FILES = { User: ["CLAUDE.md"], Project: ["CLAUDE.md", "AGENTS.md"], Local: ["CLAUDE.local.md"], Auto: ["MEMORY.md"] };
|
|
439
440
|
function splitClaudeMd(text) {
|
|
440
441
|
const marker = text.match(/\n# claudeMd\r?\n|Codebase and user instructions are shown below\./);
|
|
441
442
|
if (!marker) return { files: [], rest: text };
|
|
@@ -449,7 +450,7 @@ function splitClaudeMd(text) {
|
|
|
449
450
|
const base = h[1].split(/[\\/]/).pop();
|
|
450
451
|
return {
|
|
451
452
|
file_path: h[1],
|
|
452
|
-
name:
|
|
453
|
+
name: MEMORY_FILES[memory_type]?.includes(base) ? base : h[1],
|
|
453
454
|
memory_type,
|
|
454
455
|
content: region.slice(h.index + h[0].length, heads[i + 1]?.index ?? region.length).trim()
|
|
455
456
|
};
|
|
@@ -759,6 +760,7 @@ class SessionParser {
|
|
|
759
760
|
this.prevMessage = null;
|
|
760
761
|
this.tokenTotal = 0;
|
|
761
762
|
this.lastSeenTs = null;
|
|
763
|
+
this.modelId = null;
|
|
762
764
|
this.commandUuids = /* @__PURE__ */ new Set();
|
|
763
765
|
this.seenUuids = /* @__PURE__ */ new Set();
|
|
764
766
|
this.appliedById = /* @__PURE__ */ new Map();
|
|
@@ -774,6 +776,7 @@ class SessionParser {
|
|
|
774
776
|
if (t === "tag") meta.tag = obj.tag || null;
|
|
775
777
|
if (t === "queue-operation" && obj.content?.includes("<scheduled-task")) meta.hasScheduledTask = true;
|
|
776
778
|
if (t === "started" && obj.key && obj.agentId) meta.workflowAgents += 1;
|
|
779
|
+
if (t === "attachment" && obj.attachment?.type === "model") this.modelId = obj.attachment.identity?.modelId;
|
|
777
780
|
if (obj.cwd && !meta.project) meta.project = obj.cwd;
|
|
778
781
|
if (obj.gitBranch && !meta.gitBranch) meta.gitBranch = obj.gitBranch;
|
|
779
782
|
if (obj.entrypoint && !meta.entrypoint) meta.entrypoint = obj.entrypoint;
|
|
@@ -818,9 +821,10 @@ class SessionParser {
|
|
|
818
821
|
} else if (t === "assistant") {
|
|
819
822
|
const msg = obj.message;
|
|
820
823
|
if (!msg || msg.model === "<synthetic>") return true;
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
if (!meta.
|
|
824
|
+
const model = this.modelId?.replace(/\[.*\]$/, "") === msg.model ? this.modelId : msg.model;
|
|
825
|
+
if (model) {
|
|
826
|
+
if (!meta.model) meta.model = model;
|
|
827
|
+
if (!meta.models.includes(model)) meta.models.push(model);
|
|
824
828
|
}
|
|
825
829
|
const effort = obj.perTurnEffort ?? obj.effort;
|
|
826
830
|
if (effort && !meta.efforts.includes(effort)) meta.efforts.push(effort);
|
|
@@ -1608,21 +1612,6 @@ class ClaudeSettings {
|
|
|
1608
1612
|
}
|
|
1609
1613
|
}
|
|
1610
1614
|
}
|
|
1611
|
-
// Removes every hook (any event) whose command matches `pattern`, dropping groups and events
|
|
1612
|
-
// left empty. Returns the removed commands.
|
|
1613
|
-
removeHooks(pattern) {
|
|
1614
|
-
const matches = (h) => pattern.test(h.command ?? "");
|
|
1615
|
-
const removed = [];
|
|
1616
|
-
for (const [event, groups] of Object.entries(this.cfg.hooks ?? {})) {
|
|
1617
|
-
const hit = groups.flatMap((g) => (g.hooks ?? []).filter(matches).map((h) => h.command));
|
|
1618
|
-
if (!hit.length) continue;
|
|
1619
|
-
removed.push(...hit);
|
|
1620
|
-
for (const g of groups) g.hooks = (g.hooks ?? []).filter((h) => !matches(h));
|
|
1621
|
-
this.cfg.hooks[event] = groups.filter((g) => g.hooks.length);
|
|
1622
|
-
if (!this.cfg.hooks[event].length) delete this.cfg.hooks[event];
|
|
1623
|
-
}
|
|
1624
|
-
return removed;
|
|
1625
|
-
}
|
|
1626
1615
|
// Days Claude Code keeps transcripts before auto-deleting them; undefined when unset (its default is 30).
|
|
1627
1616
|
get cleanupPeriodDays() {
|
|
1628
1617
|
return this.cfg.cleanupPeriodDays;
|
|
@@ -1790,15 +1779,9 @@ WantedBy=default.target
|
|
|
1790
1779
|
}
|
|
1791
1780
|
}
|
|
1792
1781
|
const NODE = await which("node", { nothrow: true }) ?? "node";
|
|
1782
|
+
const SETTINGS_RELOAD_GRACE_MS = 3e3;
|
|
1793
1783
|
class ProxySwitch {
|
|
1794
1784
|
#service = new LoginService({ name: "claude-discover-proxy", command: [NODE, PROXY_PATH] });
|
|
1795
|
-
constructor() {
|
|
1796
|
-
try {
|
|
1797
|
-
const settings = new ClaudeSettings();
|
|
1798
|
-
if (settings.removeHooks(/claude-discover/).length) settings.save();
|
|
1799
|
-
} catch {
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
1785
|
async status() {
|
|
1803
1786
|
return {
|
|
1804
1787
|
running: await this.#running(),
|
|
@@ -1828,16 +1811,17 @@ class ProxySwitch {
|
|
|
1828
1811
|
settings.setEnv("ENABLE_TOOL_SEARCH", "true");
|
|
1829
1812
|
settings.save();
|
|
1830
1813
|
}
|
|
1831
|
-
//
|
|
1814
|
+
// Remove our env keys, then stop the proxy and remove the service — a foreign base URL is left alone
|
|
1832
1815
|
async deactivate() {
|
|
1833
|
-
await this.#exit();
|
|
1834
|
-
await this.#service.uninstall();
|
|
1835
1816
|
const settings = new ClaudeSettings();
|
|
1836
1817
|
if (settings.env?.ANTHROPIC_BASE_URL === PROXY_URL) {
|
|
1837
|
-
settings.
|
|
1818
|
+
settings.setEnv("ANTHROPIC_BASE_URL", "");
|
|
1838
1819
|
settings.deleteEnv("ENABLE_TOOL_SEARCH");
|
|
1839
1820
|
settings.save();
|
|
1821
|
+
await new Promise((r) => setTimeout(r, SETTINGS_RELOAD_GRACE_MS));
|
|
1840
1822
|
}
|
|
1823
|
+
await this.#exit();
|
|
1824
|
+
await this.#service.uninstall();
|
|
1841
1825
|
}
|
|
1842
1826
|
// Ask a running instance to exit via its control route and wait for the port to free up
|
|
1843
1827
|
async #exit() {
|
|
@@ -2052,61 +2036,6 @@ function lockNavigation(contents) {
|
|
|
2052
2036
|
contents.on("will-navigate", block);
|
|
2053
2037
|
contents.on("will-redirect", block);
|
|
2054
2038
|
}
|
|
2055
|
-
const __dirname$2 = path.dirname(fileURLToPath(import.meta.url));
|
|
2056
|
-
const DEV_URL$1 = process.env.ELECTRON_RENDERER_URL;
|
|
2057
|
-
const WIDTH = 320, HEIGHT = 56, MARGIN = 6;
|
|
2058
|
-
class FindBar {
|
|
2059
|
-
constructor(win) {
|
|
2060
|
-
this.win = win;
|
|
2061
|
-
this.view = new WebContentsView({
|
|
2062
|
-
webPreferences: {
|
|
2063
|
-
preload: path.join(__dirname$2, "../preload/findPreload.cjs"),
|
|
2064
|
-
contextIsolation: true,
|
|
2065
|
-
nodeIntegration: false,
|
|
2066
|
-
sandbox: true
|
|
2067
|
-
}
|
|
2068
|
-
});
|
|
2069
|
-
this.view.setBackgroundColor("#00000000");
|
|
2070
|
-
lockNavigation(this.view.webContents);
|
|
2071
|
-
win.contentView.addChildView(this.view);
|
|
2072
|
-
this.view.setVisible(false);
|
|
2073
|
-
if (DEV_URL$1) this.view.webContents.loadURL(`${DEV_URL$1}/find/find.html`);
|
|
2074
|
-
else this.view.webContents.loadFile(path.join(__dirname$2, "../renderer/find/find.html"));
|
|
2075
|
-
win.webContents.on("found-in-page", (_e, r) => {
|
|
2076
|
-
return this.view.webContents.send("find:result", { active: r.activeMatchOrdinal, total: r.matches, id: r.requestId });
|
|
2077
|
-
});
|
|
2078
|
-
win.on("resize", () => {
|
|
2079
|
-
if (this.visible) this.layout();
|
|
2080
|
-
});
|
|
2081
|
-
}
|
|
2082
|
-
get visible() {
|
|
2083
|
-
return this.view.getVisible();
|
|
2084
|
-
}
|
|
2085
|
-
layout() {
|
|
2086
|
-
const [w] = this.win.getContentSize();
|
|
2087
|
-
this.view.setBounds({ x: Math.max(0, w - WIDTH - MARGIN), y: MARGIN, width: WIDTH, height: HEIGHT });
|
|
2088
|
-
}
|
|
2089
|
-
query(text, options) {
|
|
2090
|
-
this.win.webContents.findInPage(text, options);
|
|
2091
|
-
}
|
|
2092
|
-
stop() {
|
|
2093
|
-
this.win.webContents.stopFindInPage("clearSelection");
|
|
2094
|
-
}
|
|
2095
|
-
show() {
|
|
2096
|
-
this.layout();
|
|
2097
|
-
this.view.setVisible(true);
|
|
2098
|
-
this.view.webContents.focus();
|
|
2099
|
-
this.view.webContents.send("find:open");
|
|
2100
|
-
this.win.webContents.send("find:active", true);
|
|
2101
|
-
}
|
|
2102
|
-
hide() {
|
|
2103
|
-
if (!this.visible) return;
|
|
2104
|
-
this.view.setVisible(false);
|
|
2105
|
-
this.stop();
|
|
2106
|
-
this.win.webContents.focus();
|
|
2107
|
-
this.win.webContents.send("find:active", false);
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2110
2039
|
const __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
|
|
2111
2040
|
const DEV_URL = process.env.ELECTRON_RENDERER_URL;
|
|
2112
2041
|
class MainWindow {
|
|
@@ -2114,6 +2043,7 @@ class MainWindow {
|
|
|
2114
2043
|
this.win = null;
|
|
2115
2044
|
}
|
|
2116
2045
|
create() {
|
|
2046
|
+
nativeTheme.themeSource = "dark";
|
|
2117
2047
|
const state = windowStateKeeper({ defaultWidth: 1500, defaultHeight: 900 });
|
|
2118
2048
|
this.win = new BrowserWindow({
|
|
2119
2049
|
x: state.x,
|
|
@@ -2139,7 +2069,9 @@ class MainWindow {
|
|
|
2139
2069
|
} else {
|
|
2140
2070
|
this.win.loadFile(path.join(__dirname$1, "../renderer/index.html"));
|
|
2141
2071
|
}
|
|
2142
|
-
this.findBar = new
|
|
2072
|
+
this.findBar = new FindOverlay(this.win);
|
|
2073
|
+
this.findBar.on("show", () => this.send("find:active", true));
|
|
2074
|
+
this.findBar.on("hide", () => this.send("find:active", false));
|
|
2143
2075
|
return this.win;
|
|
2144
2076
|
}
|
|
2145
2077
|
send(channel, payload) {
|
|
@@ -2185,8 +2117,6 @@ class Application {
|
|
|
2185
2117
|
ipcMain.handle("agent:run", (e, text) => agentRunner.run(text, e.sender));
|
|
2186
2118
|
ipcMain.handle("shell:open-link", (_e, href, baseFile) => openLinkSafely(href, baseFile));
|
|
2187
2119
|
ipcMain.handle("deeplink:take-pending", () => deepLink?.takePending() ?? null);
|
|
2188
|
-
ipcMain.on("find:query", (_e, text, options) => win.findBar?.query(text, options));
|
|
2189
|
-
ipcMain.on("find:stop", () => win.findBar?.stop());
|
|
2190
2120
|
ipcMain.on("find:close", () => win.findBar?.hide());
|
|
2191
2121
|
ipcMain.on("claude-settings:get", (e) => e.returnValue = { claudeDir: CLAUDE_DIR });
|
|
2192
2122
|
app.on("activate", () => {
|
|
@@ -1085,12 +1085,19 @@ button.msg-summary:hover { color: var(--text); background: var(--bg-elev-2); bor
|
|
|
1085
1085
|
/* Instruction files (system prompt / CLAUDE.md / memory) extracted from the captured API requests,
|
|
1086
1086
|
and the label over each group — one unbroken strip down the whole run. */
|
|
1087
1087
|
.instruction-file,
|
|
1088
|
-
.instruction-source
|
|
1088
|
+
.instruction-source,
|
|
1089
|
+
.instruction-missing {
|
|
1089
1090
|
border-left: 2px solid #a78bfa;
|
|
1090
1091
|
padding-left: 8px;
|
|
1091
1092
|
margin-left: -10px;
|
|
1092
1093
|
}
|
|
1093
1094
|
|
|
1095
|
+
/* Stands in for the strips when nothing was captured (see InstructionsMissing). */
|
|
1096
|
+
.instruction-missing {
|
|
1097
|
+
color: var(--text-faint);
|
|
1098
|
+
font-size: calc(11px * var(--font-scale, 1));
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1094
1101
|
/* Group label over the strips read from one part of the request (system prompt / tools / user message). */
|
|
1095
1102
|
.instruction-source {
|
|
1096
1103
|
font-family: var(--font-mono), monospace;
|
|
@@ -1280,6 +1287,20 @@ button.msg-summary:hover { color: var(--text); background: var(--bg-elev-2); bor
|
|
|
1280
1287
|
padding-top: 6px;
|
|
1281
1288
|
}
|
|
1282
1289
|
|
|
1290
|
+
.requests-list li.requests-missed {
|
|
1291
|
+
margin: 18px 0;
|
|
1292
|
+
padding: 6px 10px;
|
|
1293
|
+
background: var(--bg-elev);
|
|
1294
|
+
border-radius: 4px;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/* the wavy divider already separates the turns */
|
|
1298
|
+
.requests-list li.requests-missed + li.new-turn {
|
|
1299
|
+
border-top: 0;
|
|
1300
|
+
margin-top: 0;
|
|
1301
|
+
padding-top: 0;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1283
1304
|
.requests-list button {
|
|
1284
1305
|
display: flex;
|
|
1285
1306
|
align-items: center;
|
|
@@ -1471,6 +1492,25 @@ button.msg-summary:hover { color: var(--text); background: var(--bg-elev-2); bor
|
|
|
1471
1492
|
.requests-empty {
|
|
1472
1493
|
padding: 10px 20px;
|
|
1473
1494
|
}
|
|
1495
|
+
|
|
1496
|
+
/* Empty state: sells the Capture proxy instead of just saying nothing was recorded. */
|
|
1497
|
+
.requests-pitch {
|
|
1498
|
+
margin: 60px auto auto;
|
|
1499
|
+
max-width: 480px;
|
|
1500
|
+
text-align: center;
|
|
1501
|
+
padding: 24px 28px;
|
|
1502
|
+
border: 1px solid rgba(167, 139, 250, 0.35);
|
|
1503
|
+
border-radius: 8px;
|
|
1504
|
+
background: rgba(167, 139, 250, 0.06);
|
|
1505
|
+
color: var(--text-dim);
|
|
1506
|
+
line-height: 1.6;
|
|
1507
|
+
|
|
1508
|
+
& h3 { display: flex; align-items: center; justify-content: center; gap: 8px; margin: 0 0 8px; color: var(--text); font-size: 17px; }
|
|
1509
|
+
& p { margin: 0 0 6px; }
|
|
1510
|
+
& b { color: #a78bfa; }
|
|
1511
|
+
}
|
|
1512
|
+
.requests-pitch-icon { width: 22px; height: 22px; color: #a78bfa; }
|
|
1513
|
+
.requests-pitch-hint { color: var(--text); }
|
|
1474
1514
|
.agent-prompt-typing {
|
|
1475
1515
|
display: inline-flex;
|
|
1476
1516
|
align-items: center;
|
|
@@ -34,13 +34,13 @@ function useLocalStorage(key, initial) {
|
|
|
34
34
|
}
|
|
35
35
|
function clearOutdatedLocalStorage() {
|
|
36
36
|
const version = localStorage.getItem(VERSION_KEY);
|
|
37
|
-
if (version !== "1.10.
|
|
38
|
-
const [MAJOR, MINOR, PATCH] = "1.10.
|
|
37
|
+
if (version !== "1.10.5") {
|
|
38
|
+
const [MAJOR, MINOR, PATCH] = "1.10.5".split(".");
|
|
39
39
|
const [major, minor, patch] = (version ?? "").split(".");
|
|
40
40
|
if (MAJOR !== major || MINOR !== minor) {
|
|
41
41
|
localStorage.clear();
|
|
42
42
|
}
|
|
43
|
-
localStorage.setItem(VERSION_KEY, "1.10.
|
|
43
|
+
localStorage.setItem(VERSION_KEY, "1.10.5");
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
function useMouseFontScale(storageKey) {
|
|
@@ -892,7 +892,7 @@ function instructionTitle(it, model) {
|
|
|
892
892
|
return parts ? `${it.name ?? it.file_path} (${parts})` : it.name ?? it.file_path;
|
|
893
893
|
}
|
|
894
894
|
function compactTitle({ trigger, preTokens }) {
|
|
895
|
-
const detail = [trigger, preTokens && `${fmtCompact(preTokens)} tokens before`].filter(Boolean).join(" · ");
|
|
895
|
+
const detail = [trigger, preTokens && `${fmtCompact(preTokens)} context tokens before`].filter(Boolean).join(" · ");
|
|
896
896
|
return detail ? `Conversation compacted (${detail})` : "Conversation compacted";
|
|
897
897
|
}
|
|
898
898
|
const TIPS = {
|
|
@@ -1122,12 +1122,18 @@ function flatten(items, instructions = []) {
|
|
|
1122
1122
|
const companions = {};
|
|
1123
1123
|
const compactions = {};
|
|
1124
1124
|
const state = { mode: null, cwd: null, artifacts: /* @__PURE__ */ new Set() };
|
|
1125
|
+
const byUuid = new Map(items.map((it) => [it.uuid, it]));
|
|
1126
|
+
const boundaryOf = (it) => {
|
|
1127
|
+
let p = it.parentUuid;
|
|
1128
|
+
while (byUuid.get(p)?.type === "attachment") p = byUuid.get(p).parentUuid;
|
|
1129
|
+
return p ?? it.uuid;
|
|
1130
|
+
};
|
|
1125
1131
|
for (let it of items) {
|
|
1126
1132
|
if (it._relogged) continue;
|
|
1127
1133
|
const change = stateNote(it, state);
|
|
1128
1134
|
if (change) turns.push(noteTurn(it, change, `state-${turns.length}`));
|
|
1129
1135
|
if (it.subtype === "compact_boundary" || it.isCompactSummary) {
|
|
1130
|
-
const key = it.isCompactSummary ? it
|
|
1136
|
+
const key = it.isCompactSummary ? boundaryOf(it) : it.uuid;
|
|
1131
1137
|
let turn = compactions[key];
|
|
1132
1138
|
if (!turn) turns.push(turn = compactions[key] = metaTurn(it, "compact", [{ type: "compact" }]));
|
|
1133
1139
|
if (it.isCompactSummary) {
|
|
@@ -1530,7 +1536,7 @@ function useCollapsed(defaultOpen) {
|
|
|
1530
1536
|
}
|
|
1531
1537
|
const isCommandTurn = (t) => t.blocks.some((b) => b.type === "text" && parseCommand(b.text)?.name);
|
|
1532
1538
|
const isClearTurn = (t) => t.blocks.some((b) => b.type === "text" && parseCommand(b.text)?.name === "/clear");
|
|
1533
|
-
function ConversationView({ items, instructions = [], filePath = null, expandAll = null, continuesFrom = null, continuesTo = null, onShowPeriodOf }) {
|
|
1539
|
+
function ConversationView({ items, instructions = [], desktop = false, filePath = null, expandAll = null, continuesFrom = null, continuesTo = null, onShowPeriodOf }) {
|
|
1534
1540
|
const turns = reactExports.useMemo(() => flatten(items, instructions), [items, instructions]);
|
|
1535
1541
|
const groups = reactExports.useMemo(() => groupTurns(turns), [turns]);
|
|
1536
1542
|
const points = reactExports.useMemo(() => tokenPoints(groups), [groups]);
|
|
@@ -1550,6 +1556,7 @@ function ConversationView({ items, instructions = [], filePath = null, expandAll
|
|
|
1550
1556
|
"Continues from ",
|
|
1551
1557
|
format(continuesFrom, "MMM d, yyyy HH:mm")
|
|
1552
1558
|
] }),
|
|
1559
|
+
instructions.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "conv-row conv-row-instruction", children: /* @__PURE__ */ jsxRuntimeExports.jsx(InstructionsMissing, { desktop }) }),
|
|
1553
1560
|
groups.map((g, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [
|
|
1554
1561
|
i === branch?.index && branchDivider,
|
|
1555
1562
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `conv-row conv-row-${g.kind}`, children: [
|
|
@@ -1857,13 +1864,21 @@ function safeJson(x) {
|
|
|
1857
1864
|
function JsonBlock({ value }) {
|
|
1858
1865
|
return /* @__PURE__ */ jsxRuntimeExports.jsx(Markdown, { className: "block-text", text: "```json\n" + safeJson(value) + "\n```" });
|
|
1859
1866
|
}
|
|
1867
|
+
const ProxyHeader = ({ children }) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "msg-header msg-proxy", children: [
|
|
1868
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Globe, { className: "msg-icon" }),
|
|
1869
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "msg-author", children: "Proxy" }),
|
|
1870
|
+
children
|
|
1871
|
+
] });
|
|
1872
|
+
function InstructionsMissing({ desktop }) {
|
|
1873
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "instruction-missing", children: [
|
|
1874
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(ProxyHeader, { children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "msg-summary", children: "not captured" }) }),
|
|
1875
|
+
desktop ? "Not yet supported for Claude desktop app sessions." : "Turn on the Capture proxy to see the system prompt, tools and memory files."
|
|
1876
|
+
] });
|
|
1877
|
+
}
|
|
1860
1878
|
function InstructionRun({ turns, model }) {
|
|
1861
1879
|
const strips = turns.flatMap((t) => t.blocks).map(({ it }) => ({ ...it, tokens: countTokens(it.content, it.model ?? model) }));
|
|
1862
1880
|
return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
|
|
1863
|
-
/* @__PURE__ */ jsxRuntimeExports.
|
|
1864
|
-
/* @__PURE__ */ jsxRuntimeExports.jsx(Globe, { className: "msg-icon" }),
|
|
1865
|
-
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "msg-author", children: "Proxy" })
|
|
1866
|
-
] }),
|
|
1881
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(ProxyHeader, {}),
|
|
1867
1882
|
groupInstructions(strips).map(([label, list]) => /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [
|
|
1868
1883
|
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "aux instruction-source", children: label }),
|
|
1869
1884
|
list.map((it) => /* @__PURE__ */ jsxRuntimeExports.jsx(InstructionFile, { it, model, tokens: it.tokens }, `${it.hash ?? it.file_path}-${it.timestamp}`))
|
|
@@ -2112,7 +2127,7 @@ const Pane = React.memo(function Pane2({ value, headers, seen, expandAll }) {
|
|
|
2112
2127
|
] })
|
|
2113
2128
|
] });
|
|
2114
2129
|
});
|
|
2115
|
-
function RequestsView({ filePath, date, granularity = "day", fileSize = 0, expandAll = null }) {
|
|
2130
|
+
function RequestsView({ filePath, items, date, granularity = "day", fileSize = 0, desktop = false, expandAll = null }) {
|
|
2116
2131
|
const [records, setRecords] = reactExports.useState(null);
|
|
2117
2132
|
const [selected, setSelected] = reactExports.useState(0);
|
|
2118
2133
|
const [tab, setTab] = reactExports.useState("request");
|
|
@@ -2143,21 +2158,49 @@ function RequestsView({ filePath, date, granularity = "day", fileSize = 0, expan
|
|
|
2143
2158
|
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-view", children: error ? /* @__PURE__ */ jsxRuntimeExports.jsx(ErrorMessage, { title: "Couldn't load the captured requests", error }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-empty", children: "Loading…" }) });
|
|
2144
2159
|
}
|
|
2145
2160
|
if (!records.length) {
|
|
2146
|
-
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-view", children: /* @__PURE__ */ jsxRuntimeExports.
|
|
2161
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-view", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "requests-pitch", children: [
|
|
2162
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { children: [
|
|
2163
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Globe, { className: "requests-pitch-icon" }),
|
|
2164
|
+
"No API requests captured"
|
|
2165
|
+
] }),
|
|
2166
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "See the system prompt, tools and hidden calls behind every reply." }),
|
|
2167
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "requests-pitch-hint", children: desktop ? "Not yet supported for Claude desktop app sessions." : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
|
|
2168
|
+
"Turn on ",
|
|
2169
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("b", { children: "Capture proxy" }),
|
|
2170
|
+
" in the status bar."
|
|
2171
|
+
] }) })
|
|
2172
|
+
] }) });
|
|
2147
2173
|
}
|
|
2174
|
+
const captured = new Set(records.map((r) => r.responseHeaders?.["request-id"]));
|
|
2175
|
+
const missed = items?.filter((o) => o.requestId && !captured.has(o.requestId)) ?? [];
|
|
2176
|
+
const gapRow = (from = "", to) => {
|
|
2177
|
+
const n = new Set(missed.filter((o) => o.timestamp >= from && (!to || o.timestamp < to)).map((o) => o.requestId)).size;
|
|
2178
|
+
return n > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("li", { className: "requests-missed", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Divider, { children: [
|
|
2179
|
+
n,
|
|
2180
|
+
" ",
|
|
2181
|
+
n === 1 ? "reply" : "replies",
|
|
2182
|
+
" not captured (proxy was off)"
|
|
2183
|
+
] }) });
|
|
2184
|
+
};
|
|
2148
2185
|
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "requests-view", children: [
|
|
2149
|
-
/* @__PURE__ */ jsxRuntimeExports.
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
/* @__PURE__ */ jsxRuntimeExports.
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2186
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("ul", { className: "requests-list", children: [
|
|
2187
|
+
records.map((r, i) => {
|
|
2188
|
+
const [method, path] = splitUrl(r.url);
|
|
2189
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [
|
|
2190
|
+
gapRow(records[i - 1]?.timestamp, r.timestamp),
|
|
2191
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("li", { className: i > 0 && r.kind?.[0] === "main" ? "new-turn" : "", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { type: "button", className: i === selected ? "active" : "", onClick: () => setSelected(i), children: [
|
|
2192
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `requests-method ${method.toLowerCase()}`, children: method }),
|
|
2193
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "requests-url", children: path }),
|
|
2194
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `requests-status ${!r.status || r.status >= 400 ? "error" : "ok"}`, children: r.status ?? "—" }),
|
|
2195
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "requests-meta", children: time(r.timestamp) }),
|
|
2196
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "requests-meta requests-duration", children: duration(r.durationMs) }),
|
|
2197
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `requests-kind ${r.kind ? `kind-${r.kind[0]}` : ""}`, children: r.kind?.[1] }),
|
|
2198
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "requests-meta requests-tokens", title: "total tokens (input + output + cache)", children: tokens(totalTokens(r)) })
|
|
2199
|
+
] }) })
|
|
2200
|
+
] }, i);
|
|
2201
|
+
}),
|
|
2202
|
+
gapRow(records.at(-1).timestamp)
|
|
2203
|
+
] }),
|
|
2161
2204
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "requests-detail", children: [
|
|
2162
2205
|
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-tabs", children: ["request", "response"].map((t) => /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: t === tab ? "active" : "", onClick: () => setTab(t), children: t }, t)) }),
|
|
2163
2206
|
/* @__PURE__ */ jsxRuntimeExports.jsx(Pane, { value: body, headers, seen, expandAll })
|
|
@@ -2544,6 +2587,7 @@ function Session({ meta, missing, date, granularity = "day", onShowPeriodOf }) {
|
|
|
2544
2587
|
const [expandAll, setExpandAll] = reactExports.useState(null);
|
|
2545
2588
|
const filePath = meta?.filePath;
|
|
2546
2589
|
const fileSize = meta?.fileSize;
|
|
2590
|
+
const desktop = meta?.entrypoint === "claude-desktop";
|
|
2547
2591
|
const agent = useAgent(`${filePath}|${date}`);
|
|
2548
2592
|
reactExports.useEffect(() => {
|
|
2549
2593
|
setItems(null);
|
|
@@ -2594,7 +2638,7 @@ function Session({ meta, missing, date, granularity = "day", onShowPeriodOf }) {
|
|
|
2594
2638
|
}
|
|
2595
2639
|
)
|
|
2596
2640
|
] }),
|
|
2597
|
-
mode === "conversation" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "view-tab-pane-content", children: items ? /* @__PURE__ */ jsxRuntimeExports.jsx(ConversationView, { items, instructions, filePath: meta.parentFilePath ?? meta.filePath, expandAll, continuesFrom: meta.continuesFrom, continuesTo: meta.continuesTo, onShowPeriodOf }) : loadError || /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "empty", children: "Loading conversation…" }) }) : mode === "jsonl" ? !items && loadError || /* @__PURE__ */ jsxRuntimeExports.jsx(JsonlView, { items, expandAll }) : /* @__PURE__ */ jsxRuntimeExports.jsx(RequestsView, { filePath, date, granularity, fileSize, expandAll })
|
|
2641
|
+
mode === "conversation" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "view-tab-pane-content", children: items ? /* @__PURE__ */ jsxRuntimeExports.jsx(ConversationView, { items, instructions, desktop, filePath: meta.parentFilePath ?? meta.filePath, expandAll, continuesFrom: meta.continuesFrom, continuesTo: meta.continuesTo, onShowPeriodOf }) : loadError || /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "empty", children: "Loading conversation…" }) }) : mode === "jsonl" ? !items && loadError || /* @__PURE__ */ jsxRuntimeExports.jsx(JsonlView, { items, expandAll }) : /* @__PURE__ */ jsxRuntimeExports.jsx(RequestsView, { filePath, items, date, granularity, fileSize, desktop, expandAll })
|
|
2598
2642
|
] }),
|
|
2599
2643
|
/* @__PURE__ */ jsxRuntimeExports.jsx(SessionSummary, { meta, items, instructions, agent, granularity })
|
|
2600
2644
|
] }) }) }) }) });
|
|
@@ -2663,7 +2707,7 @@ function StatusSwitch({ service, on, warn, button, className = "", tooltip, chan
|
|
|
2663
2707
|
}
|
|
2664
2708
|
const proxyTooltip = /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
|
|
2665
2709
|
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "Captures Claude Code's raw API traffic - system prompt, tool definitions, injected reminders, responses." }),
|
|
2666
|
-
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "Applies
|
|
2710
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "Applies to running Claude Code sessions too." })
|
|
2667
2711
|
] });
|
|
2668
2712
|
const proxyChanges = /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
|
|
2669
2713
|
/* @__PURE__ */ jsxRuntimeExports.jsx("pre", { children: `"env": {
|
package/out/renderer/index.html
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
<meta charset="UTF-8" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
8
|
<title>Discover</title>
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-CHnuzPLr.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="./assets/vendors-CpMM-Zb0.js">
|
|
11
11
|
<link rel="stylesheet" crossorigin href="./assets/vendors-B4XMnGdr.css">
|
|
12
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
12
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BMh5255F.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<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.5",
|
|
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",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"bin"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
+
"predev": "kill-port 5555 9333",
|
|
21
22
|
"dev": "electron-vite dev",
|
|
22
23
|
"build": "electron-vite build",
|
|
23
24
|
"start": "electron .",
|
|
@@ -27,17 +28,18 @@
|
|
|
27
28
|
"smoke:local": "npm pack && npx -y ./claude-discover-${npm_package_version}.tgz",
|
|
28
29
|
"smoke:global": "npm pack && npm i -g ./claude-discover-${npm_package_version}.tgz && claude-discover",
|
|
29
30
|
"prepack": "npm run build",
|
|
30
|
-
"
|
|
31
|
+
"publish:patch": "npm version patch && git push --follow-tags"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
34
|
"@anthropic-ai/claude-agent-sdk": "^0.3.284",
|
|
34
35
|
"electron": "44.4.5",
|
|
35
|
-
"electron-context-menu": "^
|
|
36
|
+
"electron-context-menu": "^4.1.2",
|
|
37
|
+
"electron-find-overlay": "^1.0.0",
|
|
36
38
|
"electron-window-state": "^5.0.3",
|
|
37
39
|
"latest-version": "^9.0.0",
|
|
38
40
|
"semver": "^7.8.5",
|
|
39
|
-
"which": "^
|
|
40
|
-
"write-file-atomic": "^
|
|
41
|
+
"which": "^6.0.1",
|
|
42
|
+
"write-file-atomic": "^7.0.1"
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
|
43
45
|
"@date-fns/tz": "^1.5.0",
|
|
@@ -50,6 +52,7 @@
|
|
|
50
52
|
"electron-vite": "^5.0.0",
|
|
51
53
|
"gpt-tokenizer": "^4.0.0",
|
|
52
54
|
"highlight.js": "^11.12.0",
|
|
55
|
+
"kill-port": "^2.0.1",
|
|
53
56
|
"lucide-react": "^1.43.0",
|
|
54
57
|
"react": "^19.3.0",
|
|
55
58
|
"react-dom": "^19.3.0",
|
|
@@ -62,6 +65,9 @@
|
|
|
62
65
|
"vite": "^7.3.6",
|
|
63
66
|
"vitest": "^5.0.2"
|
|
64
67
|
},
|
|
68
|
+
"allowScripts": {
|
|
69
|
+
"esbuild": true
|
|
70
|
+
},
|
|
65
71
|
"keywords": [
|
|
66
72
|
"claude",
|
|
67
73
|
"claude-code",
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
const electron = require("electron");
|
|
3
|
-
const subscribe = (channel) => (cb) => {
|
|
4
|
-
const listener = (_e, payload) => cb(payload);
|
|
5
|
-
electron.ipcRenderer.on(channel, listener);
|
|
6
|
-
return () => {
|
|
7
|
-
electron.ipcRenderer.removeListener(channel, listener);
|
|
8
|
-
};
|
|
9
|
-
};
|
|
10
|
-
electron.contextBridge.exposeInMainWorld("api", {
|
|
11
|
-
findInPage: (text, options) => electron.ipcRenderer.send("find:query", text, options),
|
|
12
|
-
stopFind: () => electron.ipcRenderer.send("find:stop"),
|
|
13
|
-
findClose: () => electron.ipcRenderer.send("find:close"),
|
|
14
|
-
onFindOpen: subscribe("find:open"),
|
|
15
|
-
onFindResult: subscribe("find:result")
|
|
16
|
-
});
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
--bg-elev: #292a2d;
|
|
3
|
-
--border-strong: #3c4043;
|
|
4
|
-
--text: #e8eaed;
|
|
5
|
-
--text-dim: #9aa0a6;
|
|
6
|
-
}
|
|
7
|
-
* {
|
|
8
|
-
box-sizing: border-box;
|
|
9
|
-
}
|
|
10
|
-
html, body {
|
|
11
|
-
margin: 0;
|
|
12
|
-
height: 100%;
|
|
13
|
-
background: transparent;
|
|
14
|
-
overflow: hidden;
|
|
15
|
-
}
|
|
16
|
-
body {
|
|
17
|
-
font-family: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
18
|
-
font-size: 13px;
|
|
19
|
-
color: var(--text);
|
|
20
|
-
display: flex;
|
|
21
|
-
align-items: flex-start;
|
|
22
|
-
justify-content: flex-end;
|
|
23
|
-
padding: 4px;
|
|
24
|
-
}
|
|
25
|
-
.find-bar {
|
|
26
|
-
display: flex;
|
|
27
|
-
align-items: center;
|
|
28
|
-
width: 100%;
|
|
29
|
-
height: 40px;
|
|
30
|
-
padding: 0 4px 0 12px;
|
|
31
|
-
background: var(--bg-elev);
|
|
32
|
-
border: 1px solid var(--border-strong);
|
|
33
|
-
border-radius: 6px;
|
|
34
|
-
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
|
35
|
-
}
|
|
36
|
-
input {
|
|
37
|
-
flex: 1;
|
|
38
|
-
min-width: 0;
|
|
39
|
-
background: transparent;
|
|
40
|
-
color: var(--text);
|
|
41
|
-
border: none;
|
|
42
|
-
padding: 0;
|
|
43
|
-
font-size: 13px;
|
|
44
|
-
outline: none;
|
|
45
|
-
|
|
46
|
-
&::placeholder {
|
|
47
|
-
color: var(--text-dim);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
.count {
|
|
51
|
-
flex-shrink: 0;
|
|
52
|
-
padding: 0 10px;
|
|
53
|
-
font-size: 12px;
|
|
54
|
-
color: var(--text-dim);
|
|
55
|
-
white-space: nowrap;
|
|
56
|
-
font-variant-numeric: tabular-nums;
|
|
57
|
-
}
|
|
58
|
-
.divider {
|
|
59
|
-
width: 1px;
|
|
60
|
-
height: 20px;
|
|
61
|
-
margin: 0 2px;
|
|
62
|
-
background: var(--border-strong);
|
|
63
|
-
}
|
|
64
|
-
button {
|
|
65
|
-
display: flex;
|
|
66
|
-
align-items: center;
|
|
67
|
-
justify-content: center;
|
|
68
|
-
flex-shrink: 0;
|
|
69
|
-
width: 28px;
|
|
70
|
-
height: 28px;
|
|
71
|
-
margin: 0 1px;
|
|
72
|
-
background: transparent;
|
|
73
|
-
color: var(--text-dim);
|
|
74
|
-
border: none;
|
|
75
|
-
border-radius: 50%;
|
|
76
|
-
font-size: 15px;
|
|
77
|
-
line-height: 1;
|
|
78
|
-
cursor: pointer;
|
|
79
|
-
|
|
80
|
-
&:hover:not(:disabled) {
|
|
81
|
-
background: rgba(255, 255, 255, 0.1);
|
|
82
|
-
color: var(--text);
|
|
83
|
-
}
|
|
84
|
-
&:disabled {
|
|
85
|
-
opacity: 0.35;
|
|
86
|
-
cursor: default;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
class FindBar {
|
|
2
|
-
constructor() {
|
|
3
|
-
this.dom = {
|
|
4
|
-
input: document.getElementById("q"),
|
|
5
|
-
count: document.getElementById("count"),
|
|
6
|
-
prev: document.getElementById("prev"),
|
|
7
|
-
next: document.getElementById("next"),
|
|
8
|
-
close: document.getElementById("close")
|
|
9
|
-
};
|
|
10
|
-
this.lastId = 0;
|
|
11
|
-
this.timer = null;
|
|
12
|
-
window.api.onFindResult((r) => this.onResult(r));
|
|
13
|
-
window.api.onFindOpen(() => this.onOpen());
|
|
14
|
-
this.dom.input.addEventListener("input", () => this.onInput());
|
|
15
|
-
this.dom.input.addEventListener("keydown", (e) => this.onKeydown(e));
|
|
16
|
-
this.dom.prev.addEventListener("click", () => this.find(false));
|
|
17
|
-
this.dom.next.addEventListener("click", () => this.find(true));
|
|
18
|
-
this.dom.close.addEventListener("click", () => window.api.findClose());
|
|
19
|
-
}
|
|
20
|
-
render(active, total) {
|
|
21
|
-
this.dom.count.textContent = this.dom.input.value ? `${active} / ${total}` : "";
|
|
22
|
-
this.dom.prev.disabled = this.dom.next.disabled = !total;
|
|
23
|
-
}
|
|
24
|
-
onResult(r) {
|
|
25
|
-
if (r.id < this.lastId) return;
|
|
26
|
-
this.lastId = r.id;
|
|
27
|
-
this.render(r.active, r.total);
|
|
28
|
-
}
|
|
29
|
-
onOpen() {
|
|
30
|
-
this.lastId = 0;
|
|
31
|
-
this.dom.input.focus();
|
|
32
|
-
this.dom.input.select();
|
|
33
|
-
}
|
|
34
|
-
// Debounced so fast typing fires one search.
|
|
35
|
-
onInput() {
|
|
36
|
-
clearTimeout(this.timer);
|
|
37
|
-
if (!this.dom.input.value) {
|
|
38
|
-
window.api.stopFind();
|
|
39
|
-
this.render(0, 0);
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
this.timer = setTimeout(() => this.find(true), 150);
|
|
43
|
-
}
|
|
44
|
-
onKeydown(e) {
|
|
45
|
-
if (e.key === "Enter") {
|
|
46
|
-
e.preventDefault();
|
|
47
|
-
this.find(!e.shiftKey);
|
|
48
|
-
} else if (e.key === "Escape") {
|
|
49
|
-
e.preventDefault();
|
|
50
|
-
window.api.findClose();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
// findNext:true reports the first match of a new query (findNext:false searches silently) and
|
|
54
|
-
// continues the session on repeats — so it serves both the initial search and next/prev nav.
|
|
55
|
-
find(forward) {
|
|
56
|
-
if (this.dom.input.value) window.api.findInPage(this.dom.input.value, { forward, findNext: true });
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
new FindBar();
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none';">
|
|
5
|
-
|
|
6
|
-
<meta charset="utf-8" />
|
|
7
|
-
<script type="module" crossorigin src="../assets/find-DSEw-KLs.js"></script>
|
|
8
|
-
<link rel="stylesheet" crossorigin href="../assets/find-CxcUmpSm.css">
|
|
9
|
-
</head>
|
|
10
|
-
<body>
|
|
11
|
-
<div class="find-bar">
|
|
12
|
-
<input id="q" placeholder="Find" autofocus />
|
|
13
|
-
<span class="count" id="count"></span>
|
|
14
|
-
<span class="divider"></span>
|
|
15
|
-
<button id="prev" title="Previous (Shift+Enter)">
|
|
16
|
-
<svg viewBox="0 0 16 16" width="14" height="14"><path d="M3.5 10L8 5.5l4.5 4.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
17
|
-
</button>
|
|
18
|
-
<button id="next" title="Next (Enter)">
|
|
19
|
-
<svg viewBox="0 0 16 16" width="14" height="14"><path d="M3.5 6L8 10.5 12.5 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
20
|
-
</button>
|
|
21
|
-
<button id="close" title="Close (Esc)">
|
|
22
|
-
<svg viewBox="0 0 16 16" width="13" height="13"><path d="M4 4l8 8M12 4l-8 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
23
|
-
</button>
|
|
24
|
-
</div>
|
|
25
|
-
</body>
|
|
26
|
-
</html>
|