claude-discover 1.10.4 → 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.
@@ -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')
@@ -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/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, WebContentsView, Menu, ipcMain } from "electron";
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");
@@ -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
- if (msg.model) {
822
- if (!meta.model) meta.model = msg.model;
823
- if (!meta.models.includes(msg.model)) meta.models.push(msg.model);
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);
@@ -2032,61 +2036,6 @@ function lockNavigation(contents) {
2032
2036
  contents.on("will-navigate", block);
2033
2037
  contents.on("will-redirect", block);
2034
2038
  }
2035
- const __dirname$2 = path.dirname(fileURLToPath(import.meta.url));
2036
- const DEV_URL$1 = process.env.ELECTRON_RENDERER_URL;
2037
- const WIDTH = 320, HEIGHT = 56, MARGIN = 6;
2038
- class FindBar {
2039
- constructor(win) {
2040
- this.win = win;
2041
- this.view = new WebContentsView({
2042
- webPreferences: {
2043
- preload: path.join(__dirname$2, "../preload/findPreload.cjs"),
2044
- contextIsolation: true,
2045
- nodeIntegration: false,
2046
- sandbox: true
2047
- }
2048
- });
2049
- this.view.setBackgroundColor("#00000000");
2050
- lockNavigation(this.view.webContents);
2051
- win.contentView.addChildView(this.view);
2052
- this.view.setVisible(false);
2053
- if (DEV_URL$1) this.view.webContents.loadURL(`${DEV_URL$1}/find/find.html`);
2054
- else this.view.webContents.loadFile(path.join(__dirname$2, "../renderer/find/find.html"));
2055
- win.webContents.on("found-in-page", (_e, r) => {
2056
- return this.view.webContents.send("find:result", { active: r.activeMatchOrdinal, total: r.matches, id: r.requestId });
2057
- });
2058
- win.on("resize", () => {
2059
- if (this.visible) this.layout();
2060
- });
2061
- }
2062
- get visible() {
2063
- return this.view.getVisible();
2064
- }
2065
- layout() {
2066
- const [w] = this.win.getContentSize();
2067
- this.view.setBounds({ x: Math.max(0, w - WIDTH - MARGIN), y: MARGIN, width: WIDTH, height: HEIGHT });
2068
- }
2069
- query(text, options) {
2070
- this.win.webContents.findInPage(text, options);
2071
- }
2072
- stop() {
2073
- this.win.webContents.stopFindInPage("clearSelection");
2074
- }
2075
- show() {
2076
- this.layout();
2077
- this.view.setVisible(true);
2078
- this.view.webContents.focus();
2079
- this.view.webContents.send("find:open");
2080
- this.win.webContents.send("find:active", true);
2081
- }
2082
- hide() {
2083
- if (!this.visible) return;
2084
- this.view.setVisible(false);
2085
- this.stop();
2086
- this.win.webContents.focus();
2087
- this.win.webContents.send("find:active", false);
2088
- }
2089
- }
2090
2039
  const __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
2091
2040
  const DEV_URL = process.env.ELECTRON_RENDERER_URL;
2092
2041
  class MainWindow {
@@ -2094,6 +2043,7 @@ class MainWindow {
2094
2043
  this.win = null;
2095
2044
  }
2096
2045
  create() {
2046
+ nativeTheme.themeSource = "dark";
2097
2047
  const state = windowStateKeeper({ defaultWidth: 1500, defaultHeight: 900 });
2098
2048
  this.win = new BrowserWindow({
2099
2049
  x: state.x,
@@ -2119,7 +2069,9 @@ class MainWindow {
2119
2069
  } else {
2120
2070
  this.win.loadFile(path.join(__dirname$1, "../renderer/index.html"));
2121
2071
  }
2122
- this.findBar = new FindBar(this.win);
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));
2123
2075
  return this.win;
2124
2076
  }
2125
2077
  send(channel, payload) {
@@ -2165,8 +2117,6 @@ class Application {
2165
2117
  ipcMain.handle("agent:run", (e, text) => agentRunner.run(text, e.sender));
2166
2118
  ipcMain.handle("shell:open-link", (_e, href, baseFile) => openLinkSafely(href, baseFile));
2167
2119
  ipcMain.handle("deeplink:take-pending", () => deepLink?.takePending() ?? null);
2168
- ipcMain.on("find:query", (_e, text, options) => win.findBar?.query(text, options));
2169
- ipcMain.on("find:stop", () => win.findBar?.stop());
2170
2120
  ipcMain.on("find:close", () => win.findBar?.hide());
2171
2121
  ipcMain.on("claude-settings:get", (e) => e.returnValue = { claudeDir: CLAUDE_DIR });
2172
2122
  app.on("activate", () => {