@alucard_24/pi-codeindex 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +345 -2
- package/config.schema.json +16 -0
- package/eval/queries.jsonl +8 -0
- package/package.json +33 -3
- package/scripts/setup-gpu.sh +65 -0
- package/scripts/setup-vulkan.mjs +140 -0
- package/scripts/vulkan-sources.json +29 -0
- package/src/backend.js +120 -0
- package/src/chunker.js +276 -0
- package/src/cli.js +196 -0
- package/src/config-ui.js +35 -0
- package/src/config.js +126 -0
- package/src/doctor.js +96 -0
- package/src/embed.js +88 -0
- package/src/eval.js +72 -0
- package/src/extension.js +485 -0
- package/src/graph.js +306 -0
- package/src/indexer.js +302 -0
- package/src/model.js +71 -0
- package/src/present.js +177 -0
- package/src/probe.js +20 -0
- package/src/search.js +189 -0
- package/src/store.js +142 -0
- package/src/sync.js +106 -0
- package/src/util.js +108 -0
- package/src/vulkan.js +151 -0
package/src/extension.js
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
// Adapter pi (Fase 3): search_code, outline su `read`, riga di stato, /codeindex.
|
|
2
|
+
//
|
|
3
|
+
// Qui c'è solo colla: tutta la logica sta nei moduli puri (chunker, indexer,
|
|
4
|
+
// search, present). Nessuna eccezione esce da questo file: un indice assente,
|
|
5
|
+
// un modello non scaricabile o un reindex fallito degradano il tool, non la
|
|
6
|
+
// sessione.
|
|
7
|
+
|
|
8
|
+
import { defineTool } from '@earendil-works/pi-coding-agent';
|
|
9
|
+
import { Type } from '@earendil-works/pi-ai';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { indexRepo } from './indexer.js';
|
|
13
|
+
import { embeddingStatus } from './embed.js';
|
|
14
|
+
import { formatProfile } from './backend.js';
|
|
15
|
+
import { configCommand } from './config.js';
|
|
16
|
+
import { configure } from './config-ui.js';
|
|
17
|
+
import { doctor, formatDoctor, parseDoctorArgs } from './doctor.js';
|
|
18
|
+
import { loadIndex } from './store.js';
|
|
19
|
+
import { invalidateSearcher, loadSearcher, search } from './search.js';
|
|
20
|
+
import { buildBundle, buildGraph } from './graph.js';
|
|
21
|
+
import { createDebouncedQueue, filterDirty, parsePorcelainStatus } from './sync.js';
|
|
22
|
+
import { fmtBytes, git, headCommit, repoRoot } from './util.js';
|
|
23
|
+
import { formatBundle, formatHits, outlineText, promptLine, shouldAnnotateRead, statusLine, toRepoRel } from './present.js';
|
|
24
|
+
|
|
25
|
+
const STATUS_KEY = 'codeindex';
|
|
26
|
+
const DEFAULT_K = 8;
|
|
27
|
+
const MAX_K = 30;
|
|
28
|
+
const MAX_CAPS = { maxFiles: 5000, maxBytes: 200 * 1024 * 1024 };
|
|
29
|
+
const CONFIG_ENTRY = 'codeindex-config';
|
|
30
|
+
const DEBOUNCE_MS = 800;
|
|
31
|
+
const MAX_SWEEP_PATHS = 200;
|
|
32
|
+
|
|
33
|
+
let state = null; // { repo, enabled, indexing, progress, index, error, abort, queue, watcher, rerun }
|
|
34
|
+
const running = new Map(); // repo -> promise (single-flight per repo)
|
|
35
|
+
|
|
36
|
+
function readEnabled(ctx) {
|
|
37
|
+
try {
|
|
38
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
39
|
+
if (entry.type === 'custom' && entry.customType === CONFIG_ENTRY) {
|
|
40
|
+
const data = entry.data ?? {};
|
|
41
|
+
if (typeof data.enabled === 'boolean') return data.enabled;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
/* nessuno stato salvato: default attivo */
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function showStatus(ctx) {
|
|
51
|
+
// Spento significa spento: niente riga nel footer, come se l'estensione non ci fosse.
|
|
52
|
+
if (!state || state.enabled === false) {
|
|
53
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const profile = embeddingStatus().profile;
|
|
57
|
+
ctx.ui.setStatus(STATUS_KEY, statusLine({
|
|
58
|
+
repo: state.repo,
|
|
59
|
+
enabled: state.enabled,
|
|
60
|
+
indexing: state.indexing,
|
|
61
|
+
progress: state.progress,
|
|
62
|
+
chunks: state.index?.chunks?.length ?? 0,
|
|
63
|
+
}) + (profile ? ` · ${formatProfile(profile)}` : ''));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Legge solo i metadati (per l'outline e la riga di stato): niente vettori. */
|
|
67
|
+
async function refreshMeta() {
|
|
68
|
+
try {
|
|
69
|
+
const { index } = await loadIndex(state.repo);
|
|
70
|
+
state.index = index;
|
|
71
|
+
} catch {
|
|
72
|
+
state.index = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Indicizzazione in background. Riconcilia sempre (un file non committato non
|
|
78
|
+
* cambia HEAD) ma riusa i vettori: su un repo già indicizzato costa secondi.
|
|
79
|
+
* Single-flight: due session_start, un evento del watcher e un /codeindex
|
|
80
|
+
* reindex non si sovrappongono. Una richiesta arrivata durante un run viene
|
|
81
|
+
* riaccodata (`rerun`) invece di essere persa.
|
|
82
|
+
*/
|
|
83
|
+
function runIndex(ctx, { force = false } = {}) {
|
|
84
|
+
const st = state;
|
|
85
|
+
const repo = st?.repo;
|
|
86
|
+
if (!repo || !st.enabled) return Promise.resolve(null);
|
|
87
|
+
const inflight = running.get(repo);
|
|
88
|
+
if (inflight && !force) {
|
|
89
|
+
st.rerun = true;
|
|
90
|
+
return inflight;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const p = (async () => {
|
|
94
|
+
st.indexing = true;
|
|
95
|
+
st.rerun = false;
|
|
96
|
+
st.progress = null;
|
|
97
|
+
st.error = null;
|
|
98
|
+
showStatus(ctx);
|
|
99
|
+
try {
|
|
100
|
+
const r = await indexRepo({
|
|
101
|
+
root: repo,
|
|
102
|
+
...MAX_CAPS,
|
|
103
|
+
force,
|
|
104
|
+
signal: st.abort.signal,
|
|
105
|
+
log: () => {}, // niente rumore su stderr dentro la TUI
|
|
106
|
+
onProgress: (p) => {
|
|
107
|
+
if (state !== st) return; // sessione sostituita: non toccarne lo stato
|
|
108
|
+
st.progress = { done: p.done, total: p.total };
|
|
109
|
+
showStatus(ctx);
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
st.index = r.index;
|
|
113
|
+
const runtime = embeddingStatus();
|
|
114
|
+
if (state === st && st.enabled && !st.backendWarning && runtime.attempts.some((a) => !a.ok)) {
|
|
115
|
+
st.backendWarning = true;
|
|
116
|
+
ctx.ui.notify(`codeindex: selezionato ${formatProfile(runtime.profile)}; tentativi falliti: ${runtime.attempts.filter((a) => !a.ok).map((a) => `${a.device}: ${a.error}`).join('; ')}`, 'warning');
|
|
117
|
+
}
|
|
118
|
+
st.graph = null; // il grafo è derivato dall'indice: va ricostruito
|
|
119
|
+
invalidateSearcher(repo);
|
|
120
|
+
return r;
|
|
121
|
+
} catch (e) {
|
|
122
|
+
if (st.abort.signal.aborted) return null;
|
|
123
|
+
st.error = e.message ?? String(e);
|
|
124
|
+
if (state === st && st.enabled && !/abort/i.test(st.error)) {
|
|
125
|
+
// Degradazione silenziosa: il tool resta disponibile e risponde con lo stato.
|
|
126
|
+
ctx.ui.notify(`codeindex: indicizzazione non riuscita (${st.error})`, 'warning');
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
} finally {
|
|
130
|
+
st.indexing = false;
|
|
131
|
+
st.progress = null;
|
|
132
|
+
running.delete(repo);
|
|
133
|
+
if (state === st) {
|
|
134
|
+
showStatus(ctx);
|
|
135
|
+
if (st.rerun && st.enabled && !st.abort.signal.aborted) {
|
|
136
|
+
st.rerun = false;
|
|
137
|
+
void runIndex(ctx);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
})();
|
|
142
|
+
running.set(repo, p);
|
|
143
|
+
return p;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Grafo L2 in cache finché l'indice non cambia (costruirlo su Strata: ~120 ms). */
|
|
147
|
+
async function ensureGraph(index) {
|
|
148
|
+
const stamp = `${index.updatedAt}:${index.chunks.length}`;
|
|
149
|
+
if (state.graph?.stamp === stamp) return state.graph.value;
|
|
150
|
+
const value = await buildGraph(state.repo, index);
|
|
151
|
+
state.graph = { stamp, value };
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Spegne tutto: watcher, coda, indicizzazione in corso. Riaccendibile con enable. */
|
|
156
|
+
function stopBackground() {
|
|
157
|
+
if (!state) return;
|
|
158
|
+
state.abort.abort();
|
|
159
|
+
state.queue?.cancel();
|
|
160
|
+
state.rerun = false;
|
|
161
|
+
try {
|
|
162
|
+
state.watcher?.close();
|
|
163
|
+
} catch {
|
|
164
|
+
/* già chiuso */
|
|
165
|
+
}
|
|
166
|
+
state.watcher = null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Path toccati da watcher/hook: passa dal debounce e poi riconcilia. */
|
|
170
|
+
function markDirty(paths) {
|
|
171
|
+
if (!state?.queue) return;
|
|
172
|
+
const rel = filterDirty(paths, state.repo);
|
|
173
|
+
if (rel.size === 0) return;
|
|
174
|
+
state.queue.add(rel);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** `git status --porcelain -z` dopo un bash: cattura le modifiche indirette. */
|
|
178
|
+
function sweepGitStatus() {
|
|
179
|
+
if (!state?.enabled || !state.repo) return;
|
|
180
|
+
// Costa ~50–100 ms su un repo grande: il watcher è il segnale primario, questo
|
|
181
|
+
// è la rete di sicurezza. Un giro ogni 1,5 s basta.
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
if (now - (state.lastSweep ?? 0) < 1500) return;
|
|
184
|
+
state.lastSweep = now;
|
|
185
|
+
const out = git(state.repo, ['status', '--porcelain', '-z']);
|
|
186
|
+
if (!out) return;
|
|
187
|
+
const paths = parsePorcelainStatus(out).slice(0, MAX_SWEEP_PATHS);
|
|
188
|
+
if (paths.length) markDirty(paths);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Watcher ricorsivo; se il sistema non lo consente si degrada senza watcher. */
|
|
192
|
+
function startWatcher(ctx) {
|
|
193
|
+
try {
|
|
194
|
+
const watcher = fs.watch(state.repo, { recursive: true }, (_event, filename) => {
|
|
195
|
+
if (filename) markDirty([String(filename)]);
|
|
196
|
+
});
|
|
197
|
+
watcher.on('error', () => {
|
|
198
|
+
try {
|
|
199
|
+
watcher.close();
|
|
200
|
+
} catch {
|
|
201
|
+
/* già chiuso */
|
|
202
|
+
}
|
|
203
|
+
if (state) state.watcher = null;
|
|
204
|
+
});
|
|
205
|
+
state.watcher = watcher;
|
|
206
|
+
} catch (e) {
|
|
207
|
+
state.watcher = null;
|
|
208
|
+
ctx.ui.notify(`codeindex: watcher non disponibile (${e.message})`, 'warning');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function unavailable(reason) {
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: 'text', text: `search_code non disponibile: ${reason}` }],
|
|
215
|
+
details: { status: 'unavailable', reason },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const searchCode = defineTool({
|
|
220
|
+
name: 'search_code',
|
|
221
|
+
label: 'Search code (semantic)',
|
|
222
|
+
description:
|
|
223
|
+
'Semantic search over a local embedding index of this repository. Returns the most relevant ' +
|
|
224
|
+
'code chunks as path:start-end with the enclosing symbol, score and a short preview. ' +
|
|
225
|
+
'Use it to locate code when you do not know the file or the identifiers; grep remains better ' +
|
|
226
|
+
'for exact strings and search_code does not replace it.',
|
|
227
|
+
promptSnippet: 'Semantic search over the local code index (path:lines + symbol)',
|
|
228
|
+
promptGuidelines: [
|
|
229
|
+
'Use search_code before grep when you need to find where a behavior lives and you do not know the names.',
|
|
230
|
+
'Use grep for exact identifiers, error strings and regex; search_code finds concepts, not literals.',
|
|
231
|
+
'Do not repeat the same search_code query: if the hits are wrong, rephrase or switch to grep.',
|
|
232
|
+
],
|
|
233
|
+
parameters: Type.Object({
|
|
234
|
+
query: Type.String({ description: 'What you are looking for, in natural language (Italian or English).' }),
|
|
235
|
+
k: Type.Optional(Type.Number({
|
|
236
|
+
description: `How many chunks to return (default ${DEFAULT_K}, max ${MAX_K}).`,
|
|
237
|
+
minimum: 1,
|
|
238
|
+
maximum: MAX_K,
|
|
239
|
+
})),
|
|
240
|
+
path_prefix: Type.Optional(Type.String({
|
|
241
|
+
description: 'Restrict results to paths starting with this prefix, e.g. "src/extension/".',
|
|
242
|
+
})),
|
|
243
|
+
context: Type.Optional(Type.String({
|
|
244
|
+
description:
|
|
245
|
+
'snippets (default) = matches with previews. bundle = also imports, importers and ' +
|
|
246
|
+
'heuristic callers of the first 3 matches, in one call (slower, saves round-trips).',
|
|
247
|
+
enum: ['snippets', 'bundle'],
|
|
248
|
+
})),
|
|
249
|
+
}),
|
|
250
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
251
|
+
if (!state?.repo) return unavailable('la directory corrente non è un repository git');
|
|
252
|
+
if (!state.enabled) return unavailable('indice disattivato (/codeindex enable per riattivarlo)');
|
|
253
|
+
|
|
254
|
+
const k = Math.min(MAX_K, Math.max(1, Math.round(params.k ?? DEFAULT_K)));
|
|
255
|
+
const tCall = performance.now();
|
|
256
|
+
try {
|
|
257
|
+
const res = await search(params.query, {
|
|
258
|
+
root: state.repo,
|
|
259
|
+
k,
|
|
260
|
+
pathPrefix: params.path_prefix ?? null,
|
|
261
|
+
checkStale: true, // L1: se il file è cambiato dopo l'indicizzazione, dillo
|
|
262
|
+
});
|
|
263
|
+
if (signal?.aborted) return unavailable('richiesta annullata');
|
|
264
|
+
|
|
265
|
+
const bundle = (params.context ?? 'snippets') === 'bundle';
|
|
266
|
+
if (bundle) {
|
|
267
|
+
const { index } = await loadSearcher(state.repo);
|
|
268
|
+
const graph = await ensureGraph(index);
|
|
269
|
+
await buildBundle(state.repo, index, res.results, graph, { signal });
|
|
270
|
+
}
|
|
271
|
+
const tookMsTotal = Math.round((performance.now() - tCall) * 10) / 10;
|
|
272
|
+
return {
|
|
273
|
+
content: [{ type: 'text', text: bundle ? formatBundle(params.query, res) : formatHits(params.query, res) }],
|
|
274
|
+
details: {
|
|
275
|
+
status: 'ok',
|
|
276
|
+
context: bundle ? 'bundle' : 'snippets',
|
|
277
|
+
repo: res.repo,
|
|
278
|
+
chunks: res.chunks,
|
|
279
|
+
tookMs: res.tookMs,
|
|
280
|
+
tookMsTotal,
|
|
281
|
+
graph: bundle ? state.graph?.value?.stats : undefined,
|
|
282
|
+
results: res.results,
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
} catch (e) {
|
|
286
|
+
const building = state.indexing
|
|
287
|
+
? ` indicizzazione in corso (${state.progress?.done ?? 0}/${state.progress?.total ?? '?'} chunk)`
|
|
288
|
+
: '';
|
|
289
|
+
return unavailable(`${e.message}${building}`);
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
/** Aggiunge l'outline a una `read` completa di un file lungo. */
|
|
295
|
+
async function annotateRead(event, ctx) {
|
|
296
|
+
if (!state?.enabled || !state.repo) return undefined;
|
|
297
|
+
const first = event.content?.find((c) => c.type === 'text');
|
|
298
|
+
if (!first) return undefined;
|
|
299
|
+
|
|
300
|
+
// `details.truncation.totalLines` è il conteggio vero; il testo può essere troncato a 50 KB.
|
|
301
|
+
const lineCount = event.details?.truncation?.totalLines ?? first.text.split('\n').length;
|
|
302
|
+
if (!shouldAnnotateRead(event.input, lineCount)) return undefined;
|
|
303
|
+
|
|
304
|
+
const rel = toRepoRel(state.repo, path.resolve(ctx.cwd, String(event.input.path ?? '')));
|
|
305
|
+
if (!rel) return undefined;
|
|
306
|
+
|
|
307
|
+
if (!state.index) await refreshMeta();
|
|
308
|
+
const outline = outlineText(rel, lineCount, state.index?.chunks);
|
|
309
|
+
if (!outline) return undefined;
|
|
310
|
+
|
|
311
|
+
return { content: [{ type: 'text', text: outline }, ...event.content] };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function statusReport() {
|
|
315
|
+
if (!state) return 'codeindex: nessun repository git nella directory corrente';
|
|
316
|
+
const i = state.index;
|
|
317
|
+
const lines = [
|
|
318
|
+
`repo : ${state.repo}`,
|
|
319
|
+
`stato : ${state.enabled ? 'attivo' : 'disattivato'}${state.indexing ? ' · indicizzazione in corso' : ''}`,
|
|
320
|
+
];
|
|
321
|
+
if (state.progress) lines.push(`progresso : ${state.progress.done}/${state.progress.total} chunk`);
|
|
322
|
+
if (i) {
|
|
323
|
+
lines.push(`file : ${i.stats?.files ?? '?'} (${fmtBytes(i.stats?.bytes ?? 0)})`);
|
|
324
|
+
lines.push(`chunk : ${i.chunks.length} · ${i.dims}d · ${i.model} (${i.dtype})`);
|
|
325
|
+
lines.push(`head : ${i.head ?? '?'}`);
|
|
326
|
+
lines.push(`aggiornato : ${i.updatedAt}`);
|
|
327
|
+
} else {
|
|
328
|
+
lines.push('indice : assente (usa /codeindex reindex)');
|
|
329
|
+
}
|
|
330
|
+
const runtime = embeddingStatus();
|
|
331
|
+
lines.push(`runtime : ${runtime.profile ? formatProfile(runtime.profile) : 'non caricato'}`);
|
|
332
|
+
if (i?.device) lines.push(`profilo indice: ${formatProfile(i)}`);
|
|
333
|
+
if (state.error) lines.push(`ultimo errore: ${state.error}`);
|
|
334
|
+
return lines.join('\n');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export default function codeindexExtension(pi) {
|
|
338
|
+
pi.registerTool(searchCode);
|
|
339
|
+
|
|
340
|
+
pi.registerCommand('codeindex', {
|
|
341
|
+
description: 'Indice e embedding locali (status|reindex|enable|disable|config|configure|doctor)',
|
|
342
|
+
handler: async (args, ctx) => {
|
|
343
|
+
const [sub = 'status', ...rest] = (args ?? '').trim().split(/\s+/).filter(Boolean);
|
|
344
|
+
// Configurazione/diagnostica funzionano anche fuori da git o con indice spento.
|
|
345
|
+
try {
|
|
346
|
+
if (sub === 'config') {
|
|
347
|
+
ctx.ui.notify(await configCommand(rest), 'info');
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (sub === 'configure') {
|
|
351
|
+
const result = await configure(ctx);
|
|
352
|
+
if (result) ctx.ui.notify(result, 'info');
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (sub === 'doctor') {
|
|
356
|
+
const options = parseDoctorArgs(rest);
|
|
357
|
+
if (options.probe) ctx.ui.notify('codeindex: prova isolata del modello (può scaricare i pesi, timeout 120 s per backend)…', 'info');
|
|
358
|
+
const report = await doctor(options);
|
|
359
|
+
ctx.ui.notify(options.json ? JSON.stringify(report) : formatDoctor(report), report.ok ? 'info' : 'warning');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
} catch (e) {
|
|
363
|
+
ctx.ui.notify(`codeindex: ${e.message}`, 'error');
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (!state) {
|
|
367
|
+
ctx.ui.notify('codeindex: nessun repository git nella directory corrente', 'warning');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (sub === 'status') {
|
|
371
|
+
// La sessione può chiedere lo stato prima che il refresh in background
|
|
372
|
+
// abbia finito: meglio aspettare che mentire ("indice assente").
|
|
373
|
+
if (!state.index) await refreshMeta();
|
|
374
|
+
showStatus(ctx);
|
|
375
|
+
ctx.ui.notify(statusReport(), 'info');
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (sub === 'reindex') {
|
|
379
|
+
if (state.indexing) {
|
|
380
|
+
ctx.ui.notify('codeindex: indicizzazione già in corso', 'warning');
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
ctx.ui.notify('codeindex: reindex avviato in background', 'info');
|
|
384
|
+
void runIndex(ctx, { force: (args ?? '').includes('--force') });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (sub === 'enable' || sub === 'disable') {
|
|
388
|
+
state.enabled = sub === 'enable';
|
|
389
|
+
if (state.enabled) {
|
|
390
|
+
// Un abort precedente resta sul vecchio controller: senza rinnovarlo
|
|
391
|
+
// ogni run futuro uscirebbe subito.
|
|
392
|
+
state.abort = new AbortController();
|
|
393
|
+
state.error = null;
|
|
394
|
+
if (!state.watcher) startWatcher(ctx);
|
|
395
|
+
} else {
|
|
396
|
+
stopBackground(); // watcher, coda, run in corso e rerun pendente
|
|
397
|
+
}
|
|
398
|
+
showStatus(ctx);
|
|
399
|
+
try {
|
|
400
|
+
pi.appendEntry(CONFIG_ENTRY, { enabled: state.enabled });
|
|
401
|
+
} catch {
|
|
402
|
+
/* stato non persistito: vale per questa sessione */
|
|
403
|
+
}
|
|
404
|
+
if (state.enabled) void runIndex(ctx);
|
|
405
|
+
ctx.ui.notify(`codeindex: ${state.enabled ? 'attivo' : 'disattivato'}`, 'info');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
ctx.ui.notify('uso: /codeindex status|reindex [--force]|enable|disable|config [show|set CHIAVE VALORE|unset CHIAVE]|configure|doctor [--probe] [--all] [--device DEVICE] [--webgpu-backend auto|vulkan]', 'warning');
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Risorse di sessione: watcher e timer non vanno creati nel factory.
|
|
413
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
414
|
+
const repo = repoRoot(ctx.cwd);
|
|
415
|
+
if (!repo) {
|
|
416
|
+
state = null;
|
|
417
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const st = {
|
|
421
|
+
repo,
|
|
422
|
+
enabled: readEnabled(ctx),
|
|
423
|
+
indexing: false,
|
|
424
|
+
progress: null,
|
|
425
|
+
index: null,
|
|
426
|
+
error: null,
|
|
427
|
+
rerun: false,
|
|
428
|
+
abort: new AbortController(),
|
|
429
|
+
queue: null,
|
|
430
|
+
watcher: null,
|
|
431
|
+
graph: null,
|
|
432
|
+
};
|
|
433
|
+
st.queue = createDebouncedQueue(() => {
|
|
434
|
+
if (state === st && st.enabled) void runIndex(ctx);
|
|
435
|
+
}, { delayMs: DEBOUNCE_MS });
|
|
436
|
+
state = st;
|
|
437
|
+
showStatus(ctx);
|
|
438
|
+
// Non bloccare l'avvio: metadati, watcher e indice corrono in background.
|
|
439
|
+
void (async () => {
|
|
440
|
+
await refreshMeta();
|
|
441
|
+
if (state !== st) return;
|
|
442
|
+
showStatus(ctx);
|
|
443
|
+
if (!st.enabled) return;
|
|
444
|
+
startWatcher(ctx);
|
|
445
|
+
void runIndex(ctx);
|
|
446
|
+
})();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
pi.on('session_shutdown', async () => {
|
|
450
|
+
stopBackground();
|
|
451
|
+
invalidateSearcher();
|
|
452
|
+
state = null;
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Una riga stabile nel system prompt rende il tool scopribile; i conteggi
|
|
456
|
+
// esatti restano nel footer per non invalidare la prompt cache a ogni run.
|
|
457
|
+
pi.on('before_agent_start', async (event, ctx) => {
|
|
458
|
+
showStatus(ctx);
|
|
459
|
+
if (!state?.enabled) return undefined; // spento: nessuna riga nel prompt
|
|
460
|
+
// Un checkout può cambiare HEAD senza eventi utili del watcher.
|
|
461
|
+
if (!state.indexing && state.index?.head && headCommit(state.repo) !== state.index.head) {
|
|
462
|
+
void runIndex(ctx);
|
|
463
|
+
}
|
|
464
|
+
const line = promptLine(state?.index?.chunks?.length ?? 0);
|
|
465
|
+
if (!line) return undefined;
|
|
466
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${line}` };
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Invalidazione diretta: `write`/`edit` sanno esattamente che path hanno toccato.
|
|
470
|
+
pi.on('tool_result', async (event, ctx) => {
|
|
471
|
+
if (event.isError || !state?.enabled) return undefined;
|
|
472
|
+
try {
|
|
473
|
+
if (event.toolName === 'read') return await annotateRead(event, ctx);
|
|
474
|
+
if (event.toolName === 'write' || event.toolName === 'edit') {
|
|
475
|
+
markDirty([String(event.input?.path ?? '')]);
|
|
476
|
+
return undefined;
|
|
477
|
+
}
|
|
478
|
+
// Dopo un bash il filesystem può essere cambiato in modo invisibile agli hook.
|
|
479
|
+
if (event.toolName === 'bash') sweepGitStatus();
|
|
480
|
+
} catch {
|
|
481
|
+
return undefined; // un hook non deve mai rompere un tool
|
|
482
|
+
}
|
|
483
|
+
return undefined;
|
|
484
|
+
});
|
|
485
|
+
}
|