claude-code-session-manager 0.11.1 → 0.12.0
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/dist/assets/{TiptapBody-CRmtVlnv.js → TiptapBody-DS6BmNu7.js} +1 -1
- package/dist/assets/{cssMode-jKB9FuI1.js → cssMode-Bz8SFtEY.js} +1 -1
- package/dist/assets/{freemarker2-BPjwx9LW.js → freemarker2-DgDtRkId.js} +1 -1
- package/dist/assets/{handlebars-BLoV3dqv.js → handlebars-Dihbt_Mk.js} +1 -1
- package/dist/assets/{html-D-6YItp_.js → html-YEK2Ukg4.js} +1 -1
- package/dist/assets/{htmlMode-Dtc92cqk.js → htmlMode-Dg-MiMFK.js} +1 -1
- package/dist/assets/{index-DG1rozxP.js → index-3WBXI5kq.js} +872 -872
- package/dist/assets/{index-4x5C_duH.css → index-DJuzPa27.css} +1 -1
- package/dist/assets/{javascript-BQcvFYdn.js → javascript-CQXKrXCl.js} +1 -1
- package/dist/assets/{jsonMode-CnFmKTUr.js → jsonMode-IRogHtiE.js} +1 -1
- package/dist/assets/{liquid-ogD2CQU1.js → liquid-BMmGufls.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-DKVkL-Ro.js → lspLanguageFeatures-CjGavzGi.js} +1 -1
- package/dist/assets/{mdx-BHbTIp7G.js → mdx-BBqtWGs6.js} +1 -1
- package/dist/assets/{python-fa-8uToX.js → python-Bqt0Xd-X.js} +1 -1
- package/dist/assets/{razor-D4OJJrW7.js → razor-6-o8bJo5.js} +1 -1
- package/dist/assets/{tsMode-J9ZwPyiC.js → tsMode-Cg0nI_Eq.js} +1 -1
- package/dist/assets/{typescript-1H3_b_hk.js → typescript-DiEzfesU.js} +1 -1
- package/dist/assets/{xml-Djys1Yq_.js → xml--sJWdh5o.js} +1 -1
- package/dist/assets/{yaml-CAZ0iMcZ.js → yaml-D3hKgUit.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/main/index.cjs +4 -0
- package/src/main/ipcSchemas.cjs +5 -0
- package/src/main/repoAnalyzer.cjs +346 -0
- package/src/main/search.cjs +337 -0
- package/src/preload/api.d.ts +48 -0
- package/src/preload/index.cjs +7 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search IPC — fuzzy file path search + content grep.
|
|
3
|
+
*
|
|
4
|
+
* - Prefers ripgrep (`rg`) when present on PATH for both fast file enumeration
|
|
5
|
+
* (`rg --files`) and content search (`rg --json`). Falls back to a Node
|
|
6
|
+
* fs-based recursive walk capped at depth 10 when rg is missing.
|
|
7
|
+
* - All cwds are routed through `config.cjs`'s `validatePath` so the renderer
|
|
8
|
+
* can never search outside the home directory.
|
|
9
|
+
* - 10s wall-clock ceiling per call — search runs that overflow are killed
|
|
10
|
+
* and return partial results.
|
|
11
|
+
* - Ignored dirs (both modes): .git / node_modules / dist / build / .next /
|
|
12
|
+
* .cache / coverage / .turbo.
|
|
13
|
+
*
|
|
14
|
+
* Designed to back two renderer modals: QuickOpen (Cmd+P) and GlobalSearch
|
|
15
|
+
* (Cmd+Shift+F).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { ipcMain } = require('electron');
|
|
19
|
+
const { spawn } = require('node:child_process');
|
|
20
|
+
const fsp = require('node:fs/promises');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
const os = require('node:os');
|
|
23
|
+
const { z } = require('zod');
|
|
24
|
+
|
|
25
|
+
const { validatePath } = require('./config.cjs');
|
|
26
|
+
|
|
27
|
+
const TIMEOUT_MS = 10_000;
|
|
28
|
+
const MAX_FILES = 5000;
|
|
29
|
+
const MAX_TEXT_MATCHES = 500;
|
|
30
|
+
const MAX_WALK_DEPTH = 10;
|
|
31
|
+
const IGNORED_DIRS = new Set([
|
|
32
|
+
'.git', 'node_modules', 'dist', 'build', '.next',
|
|
33
|
+
'.cache', 'coverage', '.turbo', '.parcel-cache', '.pnpm-store',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
// ── ripgrep detection ─────────────────────────────────────────────────────
|
|
37
|
+
let _rgAvailable = null; // null = unknown, true/false = cached
|
|
38
|
+
function probeRipgrep() {
|
|
39
|
+
if (_rgAvailable !== null) return _rgAvailable;
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
let resolved = false;
|
|
42
|
+
const done = (val) => { if (!resolved) { resolved = true; _rgAvailable = val; resolve(val); } };
|
|
43
|
+
try {
|
|
44
|
+
const proc = spawn('rg', ['--version'], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
45
|
+
proc.on('error', () => done(false));
|
|
46
|
+
proc.on('exit', (code) => done(code === 0));
|
|
47
|
+
// Hard timeout: probe shouldn't take more than 1s.
|
|
48
|
+
setTimeout(() => { try { proc.kill('SIGKILL'); } catch { /* noop */ } done(false); }, 1000);
|
|
49
|
+
} catch {
|
|
50
|
+
done(false);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── helpers ───────────────────────────────────────────────────────────────
|
|
56
|
+
function expandHome(p) {
|
|
57
|
+
if (!p) return p;
|
|
58
|
+
if (p === '~') return os.homedir();
|
|
59
|
+
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
|
|
60
|
+
return p;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Spawn rg with argv (no shell), capture stdout/stderr, enforce TIMEOUT_MS.
|
|
65
|
+
* Resolves to { ok, stdout, stderr, exitCode, timedOut }.
|
|
66
|
+
*/
|
|
67
|
+
function runRg(args, cwd) {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
let stdout = '';
|
|
70
|
+
let stderr = '';
|
|
71
|
+
let timedOut = false;
|
|
72
|
+
let proc;
|
|
73
|
+
try {
|
|
74
|
+
proc = spawn('rg', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
75
|
+
} catch (e) {
|
|
76
|
+
resolve({ ok: false, stdout: '', stderr: e.message, exitCode: -1, timedOut: false });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const timer = setTimeout(() => {
|
|
80
|
+
timedOut = true;
|
|
81
|
+
try { proc.kill('SIGTERM'); } catch { /* noop */ }
|
|
82
|
+
setTimeout(() => { try { proc.kill('SIGKILL'); } catch { /* noop */ } }, 500);
|
|
83
|
+
}, TIMEOUT_MS);
|
|
84
|
+
|
|
85
|
+
proc.stdout.on('data', (chunk) => {
|
|
86
|
+
stdout += chunk.toString('utf8');
|
|
87
|
+
// Soft cap on buffer growth — if rg yields more than 10 MB of stdout
|
|
88
|
+
// something is wrong with the query; just kill it.
|
|
89
|
+
if (stdout.length > 10 * 1024 * 1024) {
|
|
90
|
+
try { proc.kill('SIGTERM'); } catch { /* noop */ }
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
proc.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
|
|
94
|
+
proc.on('error', (e) => {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
resolve({ ok: false, stdout, stderr: e.message, exitCode: -1, timedOut });
|
|
97
|
+
});
|
|
98
|
+
proc.on('exit', (code) => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
// rg exits 1 when zero matches found — that's still "ok" for us.
|
|
101
|
+
resolve({ ok: code === 0 || code === 1, stdout, stderr, exitCode: code ?? -1, timedOut });
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Recursive fs walk capped at MAX_WALK_DEPTH and ignoring IGNORED_DIRS.
|
|
108
|
+
* Returns absolute paths. Bails out early at MAX_FILES.
|
|
109
|
+
*/
|
|
110
|
+
async function fsWalk(rootAbs) {
|
|
111
|
+
const out = [];
|
|
112
|
+
const startedAt = Date.now();
|
|
113
|
+
async function walk(dir, depth) {
|
|
114
|
+
if (depth > MAX_WALK_DEPTH) return;
|
|
115
|
+
if (out.length >= MAX_FILES) return;
|
|
116
|
+
if (Date.now() - startedAt > TIMEOUT_MS) return;
|
|
117
|
+
let entries;
|
|
118
|
+
try {
|
|
119
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
120
|
+
} catch {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
if (out.length >= MAX_FILES) return;
|
|
125
|
+
if (entry.name.startsWith('.') && entry.name !== '.env') {
|
|
126
|
+
// Skip dotfiles by default — matches rg's defaults closely enough
|
|
127
|
+
// for our use case. .env is a common exception users may want.
|
|
128
|
+
if (entry.isDirectory()) continue;
|
|
129
|
+
}
|
|
130
|
+
if (entry.isDirectory()) {
|
|
131
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
132
|
+
await walk(path.join(dir, entry.name), depth + 1);
|
|
133
|
+
} else if (entry.isFile()) {
|
|
134
|
+
out.push(path.join(dir, entry.name));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
await walk(rootAbs, 0);
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Naive content grep for the rg-fallback path. Reads each file up to 1 MB,
|
|
144
|
+
* splits on newlines, returns the first MAX_TEXT_MATCHES occurrences. Skips
|
|
145
|
+
* binary files heuristically (NUL byte in first 4 KiB).
|
|
146
|
+
*/
|
|
147
|
+
async function fsGrep(rootAbs, needle, opts) {
|
|
148
|
+
const caseSensitive = !!opts.caseSensitive;
|
|
149
|
+
const limit = Math.max(1, Math.min(MAX_TEXT_MATCHES, opts.limit ?? MAX_TEXT_MATCHES));
|
|
150
|
+
const haystack = caseSensitive ? needle : needle.toLowerCase();
|
|
151
|
+
const startedAt = Date.now();
|
|
152
|
+
const files = await fsWalk(rootAbs);
|
|
153
|
+
const matches = [];
|
|
154
|
+
for (const file of files) {
|
|
155
|
+
if (matches.length >= limit) break;
|
|
156
|
+
if (Date.now() - startedAt > TIMEOUT_MS) break;
|
|
157
|
+
let buf;
|
|
158
|
+
try {
|
|
159
|
+
const st = await fsp.stat(file);
|
|
160
|
+
if (st.size > 1024 * 1024) continue;
|
|
161
|
+
buf = await fsp.readFile(file);
|
|
162
|
+
} catch { continue; }
|
|
163
|
+
// Binary sniff.
|
|
164
|
+
const sniff = buf.slice(0, 4096);
|
|
165
|
+
if (sniff.includes(0)) continue;
|
|
166
|
+
const text = buf.toString('utf8');
|
|
167
|
+
const lines = text.split('\n');
|
|
168
|
+
for (let i = 0; i < lines.length; i++) {
|
|
169
|
+
if (matches.length >= limit) break;
|
|
170
|
+
const line = lines[i];
|
|
171
|
+
const target = caseSensitive ? line : line.toLowerCase();
|
|
172
|
+
const idx = target.indexOf(haystack);
|
|
173
|
+
if (idx !== -1) {
|
|
174
|
+
matches.push({
|
|
175
|
+
path: file,
|
|
176
|
+
line: i + 1,
|
|
177
|
+
column: idx + 1,
|
|
178
|
+
text: line.length > 400 ? line.slice(0, 400) + '…' : line,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return matches;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── public API: search:files ──────────────────────────────────────────────
|
|
187
|
+
async function searchFiles({ cwd, query, opts }) {
|
|
188
|
+
const limit = Math.max(1, Math.min(MAX_FILES, opts?.limit ?? MAX_FILES));
|
|
189
|
+
let resolved;
|
|
190
|
+
try { resolved = validatePath(expandHome(cwd)); }
|
|
191
|
+
catch (e) { return { ok: false, files: [], error: e.message, usedRipgrep: false }; }
|
|
192
|
+
|
|
193
|
+
const usedRipgrep = await probeRipgrep();
|
|
194
|
+
let absPaths = [];
|
|
195
|
+
if (usedRipgrep) {
|
|
196
|
+
// `rg --files` enumerates the tree honoring .gitignore. We add explicit
|
|
197
|
+
// --glob '!dir' exclusions on top in case the repo isn't a git repo.
|
|
198
|
+
const args = [
|
|
199
|
+
'--files', '--hidden', '--follow',
|
|
200
|
+
'--max-filesize', '5M',
|
|
201
|
+
];
|
|
202
|
+
for (const dir of IGNORED_DIRS) {
|
|
203
|
+
args.push('--glob', `!**/${dir}/**`);
|
|
204
|
+
}
|
|
205
|
+
const result = await runRg(args, resolved);
|
|
206
|
+
if (result.timedOut) {
|
|
207
|
+
return { ok: false, files: [], error: 'search timeout (10s)', usedRipgrep: true };
|
|
208
|
+
}
|
|
209
|
+
if (!result.ok) {
|
|
210
|
+
return { ok: false, files: [], error: result.stderr || `rg exit ${result.exitCode}`, usedRipgrep: true };
|
|
211
|
+
}
|
|
212
|
+
absPaths = result.stdout.split('\n')
|
|
213
|
+
.filter((l) => l.length > 0)
|
|
214
|
+
.map((rel) => path.join(resolved, rel));
|
|
215
|
+
} else {
|
|
216
|
+
absPaths = await fsWalk(resolved);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Fuzzy filter / sort happens in renderer. Main returns the candidate set.
|
|
220
|
+
// When query is provided we do a coarse case-insensitive substring filter
|
|
221
|
+
// to cut payload size — renderer's fuzzy ranker handles the precise scoring.
|
|
222
|
+
if (query && query.length > 0) {
|
|
223
|
+
const q = query.toLowerCase();
|
|
224
|
+
absPaths = absPaths.filter((p) => p.toLowerCase().includes(q));
|
|
225
|
+
}
|
|
226
|
+
absPaths = absPaths.slice(0, limit);
|
|
227
|
+
|
|
228
|
+
// Shape each entry like files:list so QuickOpen can reuse FileNode types.
|
|
229
|
+
const files = absPaths.map((abs) => ({
|
|
230
|
+
name: path.basename(abs),
|
|
231
|
+
path: abs,
|
|
232
|
+
isDirectory: false,
|
|
233
|
+
isFile: true,
|
|
234
|
+
}));
|
|
235
|
+
return { ok: true, files, error: null, usedRipgrep };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── public API: search:text ───────────────────────────────────────────────
|
|
239
|
+
async function searchText({ cwd, query, opts }) {
|
|
240
|
+
const limit = Math.max(1, Math.min(MAX_TEXT_MATCHES, opts?.limit ?? MAX_TEXT_MATCHES));
|
|
241
|
+
const caseSensitive = !!opts?.caseSensitive;
|
|
242
|
+
if (!query || query.length === 0) {
|
|
243
|
+
return { ok: true, matches: [], error: null, usedRipgrep: false };
|
|
244
|
+
}
|
|
245
|
+
let resolved;
|
|
246
|
+
try { resolved = validatePath(expandHome(cwd)); }
|
|
247
|
+
catch (e) { return { ok: false, matches: [], error: e.message, usedRipgrep: false }; }
|
|
248
|
+
|
|
249
|
+
const usedRipgrep = await probeRipgrep();
|
|
250
|
+
if (!usedRipgrep) {
|
|
251
|
+
const matches = await fsGrep(resolved, query, { caseSensitive, limit });
|
|
252
|
+
return { ok: true, matches, error: null, usedRipgrep: false };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// rg --json gives us { type, data: { ... } } lines. We use --fixed-strings
|
|
256
|
+
// so users don't accidentally trigger regex on `.` or `(`. Renderer can
|
|
257
|
+
// toggle regex later if we ever surface that option.
|
|
258
|
+
const args = [
|
|
259
|
+
'--json', '--fixed-strings', '--hidden', '--follow',
|
|
260
|
+
'--max-filesize', '5M',
|
|
261
|
+
'--max-count', String(limit),
|
|
262
|
+
];
|
|
263
|
+
if (!caseSensitive) args.push('--ignore-case');
|
|
264
|
+
for (const dir of IGNORED_DIRS) {
|
|
265
|
+
args.push('--glob', `!**/${dir}/**`);
|
|
266
|
+
}
|
|
267
|
+
args.push('--', query);
|
|
268
|
+
|
|
269
|
+
const result = await runRg(args, resolved);
|
|
270
|
+
if (result.timedOut) {
|
|
271
|
+
return { ok: false, matches: [], error: 'search timeout (10s)', usedRipgrep: true };
|
|
272
|
+
}
|
|
273
|
+
if (!result.ok) {
|
|
274
|
+
return { ok: false, matches: [], error: result.stderr || `rg exit ${result.exitCode}`, usedRipgrep: true };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const matches = [];
|
|
278
|
+
const lines = result.stdout.split('\n');
|
|
279
|
+
for (const raw of lines) {
|
|
280
|
+
if (matches.length >= limit) break;
|
|
281
|
+
if (!raw) continue;
|
|
282
|
+
let evt;
|
|
283
|
+
try { evt = JSON.parse(raw); } catch { continue; }
|
|
284
|
+
if (evt.type !== 'match') continue;
|
|
285
|
+
const data = evt.data;
|
|
286
|
+
if (!data) continue;
|
|
287
|
+
const rel = data.path?.text;
|
|
288
|
+
if (!rel) continue;
|
|
289
|
+
const abs = path.isAbsolute(rel) ? rel : path.join(resolved, rel);
|
|
290
|
+
const lineNum = data.line_number ?? 0;
|
|
291
|
+
let text = data.lines?.text ?? '';
|
|
292
|
+
if (text.endsWith('\n')) text = text.slice(0, -1);
|
|
293
|
+
if (text.length > 400) text = text.slice(0, 400) + '…';
|
|
294
|
+
const column = (data.submatches && data.submatches[0]?.start != null)
|
|
295
|
+
? data.submatches[0].start + 1
|
|
296
|
+
: 1;
|
|
297
|
+
matches.push({ path: abs, line: lineNum, column, text });
|
|
298
|
+
}
|
|
299
|
+
return { ok: true, matches, error: null, usedRipgrep: true };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── IPC registration ──────────────────────────────────────────────────────
|
|
303
|
+
const searchFilesSchema = z.object({
|
|
304
|
+
cwd: z.string().min(1).max(4096),
|
|
305
|
+
query: z.string().max(1024).optional().default(''),
|
|
306
|
+
opts: z.object({
|
|
307
|
+
limit: z.number().int().min(1).max(MAX_FILES).optional(),
|
|
308
|
+
}).optional(),
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const searchTextSchema = z.object({
|
|
312
|
+
cwd: z.string().min(1).max(4096),
|
|
313
|
+
query: z.string().min(1).max(1024),
|
|
314
|
+
opts: z.object({
|
|
315
|
+
limit: z.number().int().min(1).max(MAX_TEXT_MATCHES).optional(),
|
|
316
|
+
caseSensitive: z.boolean().optional(),
|
|
317
|
+
}).optional(),
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
function registerSearchHandlers() {
|
|
321
|
+
ipcMain.handle('search:files', (_e, payload) => {
|
|
322
|
+
const parsed = searchFilesSchema.parse(payload);
|
|
323
|
+
return searchFiles(parsed);
|
|
324
|
+
});
|
|
325
|
+
ipcMain.handle('search:text', (_e, payload) => {
|
|
326
|
+
const parsed = searchTextSchema.parse(payload);
|
|
327
|
+
return searchText(parsed);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
module.exports = {
|
|
332
|
+
registerSearchHandlers,
|
|
333
|
+
// exported for tests
|
|
334
|
+
searchFiles,
|
|
335
|
+
searchText,
|
|
336
|
+
probeRipgrep,
|
|
337
|
+
};
|
package/src/preload/api.d.ts
CHANGED
|
@@ -417,6 +417,47 @@ export interface FilesRenameResult { ok: boolean; newPath?: string; error: strin
|
|
|
417
417
|
export interface FilesDeleteResult { ok: boolean; error: string | null }
|
|
418
418
|
export interface FilesShellResult { ok: boolean; error?: string }
|
|
419
419
|
|
|
420
|
+
export interface SearchFileEntry {
|
|
421
|
+
name: string;
|
|
422
|
+
path: string;
|
|
423
|
+
isDirectory: false;
|
|
424
|
+
isFile: true;
|
|
425
|
+
}
|
|
426
|
+
export interface SearchFilesResult {
|
|
427
|
+
ok: boolean;
|
|
428
|
+
files: SearchFileEntry[];
|
|
429
|
+
error: string | null;
|
|
430
|
+
usedRipgrep: boolean;
|
|
431
|
+
}
|
|
432
|
+
export interface SearchTextMatch {
|
|
433
|
+
path: string;
|
|
434
|
+
line: number;
|
|
435
|
+
column: number;
|
|
436
|
+
text: string;
|
|
437
|
+
}
|
|
438
|
+
export interface SearchTextResult {
|
|
439
|
+
ok: boolean;
|
|
440
|
+
matches: SearchTextMatch[];
|
|
441
|
+
error: string | null;
|
|
442
|
+
usedRipgrep: boolean;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export interface RepoLanguageStats { files: number; lines: number }
|
|
446
|
+
export interface RepoTopDirectory { path: string; fileCount: number; totalLines: number }
|
|
447
|
+
export interface RepoGitStatusSummary { uncommitted: number; branch: string | null }
|
|
448
|
+
export interface RepoAnalyzeResult {
|
|
449
|
+
ok: true;
|
|
450
|
+
cwd: string;
|
|
451
|
+
totalFiles: number;
|
|
452
|
+
totalLines: number;
|
|
453
|
+
languageBreakdown: Record<string, RepoLanguageStats>;
|
|
454
|
+
topDirectories: RepoTopDirectory[];
|
|
455
|
+
gitStatus: RepoGitStatusSummary;
|
|
456
|
+
truncated: boolean;
|
|
457
|
+
durationMs: number;
|
|
458
|
+
}
|
|
459
|
+
export interface RepoAnalyzeError { ok: false; error: string }
|
|
460
|
+
|
|
420
461
|
export interface WatcherInfo {
|
|
421
462
|
watcherId: string;
|
|
422
463
|
tabId: string;
|
|
@@ -788,6 +829,13 @@ export interface SessionManagerAPI {
|
|
|
788
829
|
openExternal: (path: string) => Promise<FilesShellResult>;
|
|
789
830
|
showInFinder: (path: string) => Promise<FilesShellResult>;
|
|
790
831
|
};
|
|
832
|
+
search: {
|
|
833
|
+
files: (cwd: string, query?: string, opts?: { limit?: number }) => Promise<SearchFilesResult>;
|
|
834
|
+
text: (cwd: string, query: string, opts?: { limit?: number; caseSensitive?: boolean }) => Promise<SearchTextResult>;
|
|
835
|
+
};
|
|
836
|
+
repo: {
|
|
837
|
+
analyze: (cwd: string) => Promise<RepoAnalyzeResult | RepoAnalyzeError>;
|
|
838
|
+
};
|
|
791
839
|
history: {
|
|
792
840
|
aggregate: (req?: HistoryAggregateRequest) => Promise<HistoryAggregateResult>;
|
|
793
841
|
listConversations: () => Promise<ListConversationsResult>;
|
package/src/preload/index.cjs
CHANGED
|
@@ -163,6 +163,13 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
163
163
|
openExternal: (path) => ipcRenderer.invoke('files:open-external', { path }),
|
|
164
164
|
showInFinder: (path) => ipcRenderer.invoke('files:show-in-finder', { path }),
|
|
165
165
|
},
|
|
166
|
+
search: {
|
|
167
|
+
files: (cwd, query, opts) => ipcRenderer.invoke('search:files', { cwd, query, opts }),
|
|
168
|
+
text: (cwd, query, opts) => ipcRenderer.invoke('search:text', { cwd, query, opts }),
|
|
169
|
+
},
|
|
170
|
+
repo: {
|
|
171
|
+
analyze: (cwd) => ipcRenderer.invoke('repo:analyze', { cwd }),
|
|
172
|
+
},
|
|
166
173
|
schedule: {
|
|
167
174
|
state: () => ipcRenderer.invoke('schedule:state'),
|
|
168
175
|
setConfig: (partial) => ipcRenderer.invoke('schedule:set-config', partial),
|