pi-graphnav 0.2.1 → 0.3.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/README.md +9 -1
- package/extensions/command/parse.ts +17 -3
- package/extensions/core/config.ts +15 -4
- package/extensions/core/gate.ts +76 -7
- package/extensions/core/tools.ts +1 -1
- package/extensions/graphnav.ts +47 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,6 +33,13 @@ A [pi](https://github.com/earendil-works/pi-coding-agent) extension.
|
|
|
33
33
|
phrases, string literals or in docs and config (`*.md`, `--type yaml`, …) are never touched,
|
|
34
34
|
nor symbols already looked up with CodeGraph. Searches a codemode script makes with literal
|
|
35
35
|
`tools.bash({ command })` / `tools.grep({ pattern })` are gated the same way.
|
|
36
|
+
A pattern only counts as a symbol when it has code shape (`parseConfig`, `ParseConfig`,
|
|
37
|
+
`start_scan`, `Foo::new`) or symbol syntax (`fn summary`, `summary(`); plain words like
|
|
38
|
+
`summary`, `cache` or `Success` are text. Before blocking, the gate asks the index (one
|
|
39
|
+
`codegraph_search`, 2 s limit, fails open) and lets the search through when the index does
|
|
40
|
+
not define the name. When it does, the block message already says where (`path:line
|
|
41
|
+
(kind)`), so one blocked call still answers the question. A search inside named files
|
|
42
|
+
(`rg start_scan src/app.rs`) is only nudged: the agent has already found the code.
|
|
36
43
|
* **Nudges file-name lookups.** `fd UserService`, `find . -name '*ParseConfig*'` and pi's `find`
|
|
37
44
|
tool with a camelCase or snake_case name run, with a hint to `codegraph_search` (never blocked).
|
|
38
45
|
* **Hints after blind reading.** Reading several source files before any CodeGraph call adds
|
|
@@ -76,6 +83,7 @@ pi install npm:pi-graphnav
|
|
|
76
83
|
| `/codegraph` or `/codegraph status` | project, index state, mode, blocked/nudged counts |
|
|
77
84
|
| `/codegraph [on\|off]` | switch it all (remembered across sessions) |
|
|
78
85
|
| `/codegraph mode <mode>` | symbol searches: `enforce` (default), `nudge` or `off` |
|
|
86
|
+
| `/codegraph statusline <when>` | status line: `always` (default), `problems` or `never`; CodeGraph keeps working |
|
|
79
87
|
| `/codegraph sync` | sync the index with changed files |
|
|
80
88
|
| `/codegraph reindex` | rebuild the index from scratch |
|
|
81
89
|
| `/codegraph init` | create the index (for a directory that is not a git repo) |
|
|
@@ -85,7 +93,7 @@ Settings live in `<pi agent dir>/codegraph.json` (`PI_CODING_AGENT_DIR`, else `~
|
|
|
85
93
|
| Key | Default | |
|
|
86
94
|
|---|---|---|
|
|
87
95
|
| `enabled` | `true` | master switch |
|
|
88
|
-
| `showStatus` | `
|
|
96
|
+
| `showStatus` | `"always"` | status line entry: `"always"`, `"problems"` (only while indexing, or when the index is missing, failing or unavailable) or `"never"`; `true`/`false` still work |
|
|
89
97
|
| `mode` | `"enforce"` | `enforce`, `nudge` or `off` |
|
|
90
98
|
| `autoInit` | `true` | init a git repo that has no index |
|
|
91
99
|
| `autoSync` | `true` | sync or rebuild a stale index at session start |
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/** `/codegraph ...`: parsing and argument completion. Pure: the handler in graphnav.ts acts on the result. */
|
|
2
|
-
import { MODES, type Mode } from "../core/config.ts";
|
|
2
|
+
import { MODES, type Mode, STATUS_SHOWS, type StatusShow } from "../core/config.ts";
|
|
3
3
|
|
|
4
4
|
export type Command =
|
|
5
5
|
| { kind: "status" }
|
|
6
6
|
| { kind: "on" }
|
|
7
7
|
| { kind: "off" }
|
|
8
8
|
| { kind: "mode"; mode: Mode }
|
|
9
|
+
| { kind: "statusline"; show: StatusShow }
|
|
9
10
|
| { kind: "sync" }
|
|
10
11
|
| { kind: "reindex" }
|
|
11
12
|
| { kind: "init" }
|
|
12
13
|
| { kind: "invalid"; message: string };
|
|
13
14
|
|
|
14
|
-
export const USAGE = "Usage: /codegraph [status] · on · off · mode <enforce|nudge|off> · sync · reindex · init";
|
|
15
|
+
export const USAGE = "Usage: /codegraph [status] · on · off · mode <enforce|nudge|off> · statusline <always|problems|never> · sync · reindex · init";
|
|
15
16
|
|
|
16
17
|
export function parseCommand(args: string | undefined): Command {
|
|
17
18
|
const [cmd, ...rest] = (args ?? "").trim().split(/\s+/).filter(Boolean);
|
|
@@ -31,6 +32,12 @@ export function parseCommand(args: string | undefined): Command {
|
|
|
31
32
|
? { kind: "mode", mode: m as Mode }
|
|
32
33
|
: { kind: "invalid", message: "Usage: /codegraph mode <enforce|nudge|off>" };
|
|
33
34
|
}
|
|
35
|
+
case "statusline": {
|
|
36
|
+
const v = rest[0]?.toLowerCase();
|
|
37
|
+
return STATUS_SHOWS.includes(v as StatusShow) && rest.length === 1
|
|
38
|
+
? { kind: "statusline", show: v as StatusShow }
|
|
39
|
+
: { kind: "invalid", message: "Usage: /codegraph statusline <always|problems|never>" };
|
|
40
|
+
}
|
|
34
41
|
case "sync":
|
|
35
42
|
return { kind: "sync" };
|
|
36
43
|
case "reindex":
|
|
@@ -55,6 +62,7 @@ const TOP_LEVEL: CompletionItem[] = [
|
|
|
55
62
|
{ value: "on", label: "on", description: "Turn CodeGraph-first on" },
|
|
56
63
|
{ value: "off", label: "off", description: "Turn CodeGraph-first off" },
|
|
57
64
|
{ value: "mode ", label: "mode", description: "Symbol searches: enforce, nudge or off" },
|
|
65
|
+
{ value: "statusline ", label: "statusline", description: "Status line: always, only problems, or never" },
|
|
58
66
|
{ value: "sync", label: "sync", description: "Sync the index with changed files" },
|
|
59
67
|
{ value: "reindex", label: "reindex", description: "Rebuild the index from scratch" },
|
|
60
68
|
{ value: "init", label: "init", description: "Create the index for this project" },
|
|
@@ -66,10 +74,16 @@ const MODE_ITEMS: CompletionItem[] = [
|
|
|
66
74
|
{ value: "mode off", label: "off", description: "Leave text searches alone" },
|
|
67
75
|
];
|
|
68
76
|
|
|
77
|
+
const STATUSLINE_ITEMS: CompletionItem[] = [
|
|
78
|
+
{ value: "statusline always", label: "always", description: "Always show the CodeGraph status" },
|
|
79
|
+
{ value: "statusline problems", label: "problems", description: "Only while indexing, or when the index is missing, failing or unavailable" },
|
|
80
|
+
{ value: "statusline never", label: "never", description: "Never show it (CodeGraph keeps working)" },
|
|
81
|
+
];
|
|
82
|
+
|
|
69
83
|
/** `prefix` is the whole argument text typed so far (pi replaces it with `value`). */
|
|
70
84
|
export function completions(prefix: string): CompletionItem[] | null {
|
|
71
85
|
const text = (prefix ?? "").toLowerCase().replace(/^\s+/, "");
|
|
72
|
-
const pool = text.startsWith("mode ") ? MODE_ITEMS : TOP_LEVEL;
|
|
86
|
+
const pool = text.startsWith("mode ") ? MODE_ITEMS : text.startsWith("statusline ") ? STATUSLINE_ITEMS : TOP_LEVEL;
|
|
73
87
|
const out = pool.filter((i) => i.value.startsWith(text));
|
|
74
88
|
return out.length > 0 ? out : null;
|
|
75
89
|
}
|
|
@@ -11,11 +11,22 @@ import { join } from "node:path";
|
|
|
11
11
|
export type Mode = "enforce" | "nudge" | "off";
|
|
12
12
|
export const MODES: readonly Mode[] = ["enforce", "nudge", "off"];
|
|
13
13
|
|
|
14
|
+
/** Status line: always, only when something needs attention (indexing, missing, error, unavailable), or never. */
|
|
15
|
+
export type StatusShow = "always" | "problems" | "never";
|
|
16
|
+
export const STATUS_SHOWS: readonly StatusShow[] = ["always", "problems", "never"];
|
|
17
|
+
|
|
18
|
+
/** `showStatus` from a config file: one of STATUS_SHOWS, or the 0.2 booleans (true = always, false = never). */
|
|
19
|
+
export function parseStatusShow(v: unknown): StatusShow | undefined {
|
|
20
|
+
if (v === true) return "always";
|
|
21
|
+
if (v === false) return "never";
|
|
22
|
+
return STATUS_SHOWS.includes(v as StatusShow) ? (v as StatusShow) : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
export interface Config {
|
|
15
26
|
/** Master switch: when false the extension adds no guidance, gates nothing and indexes nothing. */
|
|
16
27
|
enabled: boolean;
|
|
17
|
-
/**
|
|
18
|
-
showStatus:
|
|
28
|
+
/** When to show the status line entry. */
|
|
29
|
+
showStatus: StatusShow;
|
|
19
30
|
/** Symbol searches with rg/grep: block once (`enforce`), only add a hint (`nudge`) or leave alone (`off`). */
|
|
20
31
|
mode: Mode;
|
|
21
32
|
/** Create the index (`codegraph init`) for a git repo that has none. */
|
|
@@ -32,7 +43,7 @@ export interface Config {
|
|
|
32
43
|
|
|
33
44
|
export const DEFAULT_CONFIG: Config = {
|
|
34
45
|
enabled: true,
|
|
35
|
-
showStatus:
|
|
46
|
+
showStatus: "always",
|
|
36
47
|
mode: "enforce",
|
|
37
48
|
autoInit: true,
|
|
38
49
|
autoSync: true,
|
|
@@ -49,7 +60,7 @@ export function sanitizeConfig(raw: unknown): Config {
|
|
|
49
60
|
const d = DEFAULT_CONFIG;
|
|
50
61
|
return {
|
|
51
62
|
enabled: typeof r.enabled === "boolean" ? r.enabled : d.enabled,
|
|
52
|
-
showStatus:
|
|
63
|
+
showStatus: parseStatusShow(r.showStatus) ?? d.showStatus,
|
|
53
64
|
mode: MODES.includes(r.mode as Mode) ? (r.mode as Mode) : d.mode,
|
|
54
65
|
autoInit: typeof r.autoInit === "boolean" ? r.autoInit : d.autoInit,
|
|
55
66
|
autoSync: typeof r.autoSync === "boolean" ? r.autoSync : d.autoSync,
|
package/extensions/core/gate.ts
CHANGED
|
@@ -228,7 +228,7 @@ const QUALIFIED = /^[A-Za-z_$][\w$]*(?:(?:::|\.|#|->)[A-Za-z_$][\w$]*)*$/;
|
|
|
228
228
|
const NOT_SYMBOLS = new Set(["todo", "fixme", "hack", "xxx", "note", "bug", "deprecated", "error", "warn", "warning", "info", "debug"]);
|
|
229
229
|
|
|
230
230
|
/** Strip regex syntax that does not change what is being looked for. */
|
|
231
|
-
function normalizeAlternative(raw: string): string {
|
|
231
|
+
function normalizeAlternative(raw: string): { s: string; declared: boolean } {
|
|
232
232
|
let s = raw.trim();
|
|
233
233
|
s = s.replace(/\\[bB<>]/g, "").replace(/\[\[:<:\]\]|\[\[:>:\]\]/g, "");
|
|
234
234
|
s = s.replace(/^\^/, "").replace(/\$$/, "");
|
|
@@ -236,10 +236,23 @@ function normalizeAlternative(raw: string): string {
|
|
|
236
236
|
prev = s;
|
|
237
237
|
s = s.replace(/^\((?:\?:)?/, "").replace(/\)$/, "");
|
|
238
238
|
}
|
|
239
|
+
const bare = s;
|
|
239
240
|
s = s.replace(DECL, "");
|
|
241
|
+
const declared = s !== bare;
|
|
240
242
|
s = s.replace(/(?:\\?\(|\\?<|\s*\\s\*\\?\(|\s*\\s\*)$/, "");
|
|
243
|
+
const called = s !== bare && !declared && /\(\s*$|\\\(/.test(bare);
|
|
241
244
|
s = s.replace(/^(?:\.\*|\\s\*|\\s\+)+|(?:\.\*|\\s\*|\\s\+)+$/g, "");
|
|
242
|
-
return s.trim();
|
|
245
|
+
return { s: s.trim(), declared: declared || called };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Does a bare identifier look like code rather than a word? A plain `summary`, `cache`,
|
|
250
|
+
* `Success` or `ERROR` is as likely prose or a log string as a symbol, so it only counts when
|
|
251
|
+
* the search says "symbol": a declaration keyword (`fn summary`) or call syntax (`summary(`).
|
|
252
|
+
* Otherwise it needs shape: camelCase/PascalCase humps, an inner underscore, or a qualifier.
|
|
253
|
+
*/
|
|
254
|
+
export function isSymbolShaped(s: string): boolean {
|
|
255
|
+
return /::|\.|#|->/.test(s) || /[a-z0-9][A-Z]/.test(s) || /^[A-Z][a-z0-9]+[A-Z]/.test(s) || /[A-Za-z0-9]_[A-Za-z0-9]/.test(s);
|
|
243
256
|
}
|
|
244
257
|
|
|
245
258
|
/**
|
|
@@ -252,11 +265,13 @@ export function symbolsIn(pattern: string | undefined): string[] | undefined {
|
|
|
252
265
|
const alternatives = pattern.split(/\\?\|/);
|
|
253
266
|
const out: string[] = [];
|
|
254
267
|
for (const alt of alternatives) {
|
|
255
|
-
const s = normalizeAlternative(alt);
|
|
268
|
+
const { s, declared } = normalizeAlternative(alt);
|
|
256
269
|
if (s.length < 3 || !QUALIFIED.test(s) || KEYWORDS.has(s)) return undefined;
|
|
270
|
+
if (!declared && !isSymbolShaped(s)) return undefined;
|
|
257
271
|
const parts = s.split(/::|\.|#|->/);
|
|
258
272
|
if (parts.some((p) => NOT_SYMBOLS.has(p.toLowerCase()))) return undefined;
|
|
259
273
|
if (/^[A-Z0-9_]+$/.test(s) && s.length <= 4) return undefined;
|
|
274
|
+
if (/^_+$|^[a-z]+_$/.test(s)) return undefined;
|
|
260
275
|
out.push(s);
|
|
261
276
|
}
|
|
262
277
|
return out.length > 0 ? out : undefined;
|
|
@@ -284,21 +299,64 @@ export interface GateMemory {
|
|
|
284
299
|
lookedUp: Set<string>;
|
|
285
300
|
/** Commands blocked once: running the same command again is the agent's explicit override. */
|
|
286
301
|
blocked: Set<string>;
|
|
302
|
+
/** Lower-cased words the index was asked about and does not define: text, not symbols. */
|
|
303
|
+
unindexed: Set<string>;
|
|
304
|
+
/** Lower-cased leaf → where the index defines it (`path:line (kind)`), from the gate's probe. */
|
|
305
|
+
defined: Map<string, string[]>;
|
|
287
306
|
}
|
|
288
307
|
|
|
289
|
-
export const newMemory = (): GateMemory => ({ lookedUp: new Set(), blocked: new Set() });
|
|
308
|
+
export const newMemory = (): GateMemory => ({ lookedUp: new Set(), blocked: new Set(), unindexed: new Set(), defined: new Map() });
|
|
309
|
+
|
|
310
|
+
/** Last segment of a qualified name, lower-cased: what the index is keyed by. */
|
|
311
|
+
export const leafOf = (symbol: string): string => symbol.toLowerCase().split(/::|\.|#|->/).pop()!;
|
|
290
312
|
|
|
291
313
|
const known = (symbol: string, mem: GateMemory) => {
|
|
292
314
|
const s = symbol.toLowerCase();
|
|
315
|
+
if (mem.unindexed.has(leafOf(s))) return true;
|
|
293
316
|
if (mem.lookedUp.has(s)) return true;
|
|
294
317
|
const last = s.split(/::|\.|#|->/).pop()!;
|
|
295
318
|
return mem.lookedUp.has(last);
|
|
296
319
|
};
|
|
297
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Does a `codegraph_search` answer define `symbol`? Reads the `**name** (kind)` headings and
|
|
323
|
+
* compares the last path segment, case-insensitively.
|
|
324
|
+
*/
|
|
325
|
+
export function indexDefines(searchText: string, symbol: string): boolean {
|
|
326
|
+
return definitionsIn(searchText, symbol).length > 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Where a `codegraph_search` answer defines `symbol`: `path:line (kind)`, at most `max`. */
|
|
330
|
+
export function definitionsIn(searchText: string, symbol: string, max = 5): string[] {
|
|
331
|
+
const leaf = leafOf(symbol);
|
|
332
|
+
const out: string[] = [];
|
|
333
|
+
for (const m of searchText.matchAll(/^\*\*(.+?)\*\* \(([^)\n]*)\)[ \t]*\n([^\n`*]*)/gm)) {
|
|
334
|
+
if (m[1]!.toLowerCase() !== leaf) continue;
|
|
335
|
+
const at = m[3]!.trim();
|
|
336
|
+
out.push(at ? `${at} (${m[2]})` : `(${m[2]})`);
|
|
337
|
+
if (out.length >= max) break;
|
|
338
|
+
}
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Does every search name only files (not directories), so it reads code already pinned down? */
|
|
343
|
+
const onlyNamedFiles = (s: Search): boolean => s.paths.length > 0 && s.paths.every((p) => extOf(p) !== undefined && !/[*?{[]/.test(p));
|
|
344
|
+
|
|
345
|
+
/** Symbols a search set would be gated on (not yet looked up, not known to be text). */
|
|
346
|
+
export function candidateSymbols(searches: Search[], mem: GateMemory): string[] {
|
|
347
|
+
const out = new Set<string>();
|
|
348
|
+
for (const s of searches) {
|
|
349
|
+
if (s.files || targetsOnlyNonCode(s)) continue;
|
|
350
|
+
for (const sym of symbolsIn(s.pattern) ?? []) if (!known(sym, mem)) out.add(sym);
|
|
351
|
+
}
|
|
352
|
+
return [...out];
|
|
353
|
+
}
|
|
354
|
+
|
|
298
355
|
export function suggestion(symbols: string[]): string {
|
|
299
356
|
const q = symbols.join(" ");
|
|
300
357
|
const one = symbols[0]!;
|
|
301
358
|
return [
|
|
359
|
+
`codegraph_node({ symbol: "${one}", includeCode: true }) for its source`,
|
|
302
360
|
`codegraph_search({ query: "${one}" }) for the definition`,
|
|
303
361
|
`codegraph_callers({ symbol: "${one}" }) for usages`,
|
|
304
362
|
`codegraph_explore({ query: "${q}" }) for the code around it`,
|
|
@@ -313,9 +371,14 @@ export function decide(mode: Mode, searches: Search[], key: string, mem: GateMem
|
|
|
313
371
|
if (mode === "off") return { action: "allow" };
|
|
314
372
|
const symbols = new Set<string>();
|
|
315
373
|
const names = new Set<string>();
|
|
374
|
+
let scoped = true;
|
|
316
375
|
for (const s of searches) {
|
|
317
376
|
if (targetsOnlyNonCode(s)) continue;
|
|
318
|
-
for (const sym of (s.files ? fileSymbolIn(s.pattern) : symbolsIn(s.pattern)) ?? [])
|
|
377
|
+
for (const sym of (s.files ? fileSymbolIn(s.pattern) : symbolsIn(s.pattern)) ?? []) {
|
|
378
|
+
if (known(sym, mem)) continue;
|
|
379
|
+
(s.files ? names : symbols).add(sym);
|
|
380
|
+
if (!s.files && !onlyNamedFiles(s)) scoped = false;
|
|
381
|
+
}
|
|
319
382
|
}
|
|
320
383
|
if (symbols.size === 0 && names.size === 0) return { action: "allow" };
|
|
321
384
|
if (symbols.size === 0) {
|
|
@@ -326,8 +389,14 @@ export function decide(mode: Mode, searches: Search[], key: string, mem: GateMem
|
|
|
326
389
|
};
|
|
327
390
|
}
|
|
328
391
|
const list = [...symbols];
|
|
329
|
-
const
|
|
330
|
-
|
|
392
|
+
const where = list.flatMap((sym) => {
|
|
393
|
+
const at = mem.defined.get(leafOf(sym));
|
|
394
|
+
return at && at.length > 0 ? [`${sym} is defined at ${at.join(", ")}`] : [];
|
|
395
|
+
});
|
|
396
|
+
const found = where.length > 0 ? ` ${where.join("; ")}.` : "";
|
|
397
|
+
const hint = `"${list.join('", "')}" ${list.length === 1 ? "looks like a symbol" : "look like symbols"}: CodeGraph answers this structurally.${found} Use ${suggestion(list)}.`;
|
|
398
|
+
// A search inside named files reads code the agent already pinned down: hint, do not block.
|
|
399
|
+
if (mode === "nudge" || scoped) return { action: "nudge", hint: `[codegraph] ${hint}` };
|
|
331
400
|
if (mem.blocked.has(key)) return { action: "allow", override: true };
|
|
332
401
|
mem.blocked.add(key);
|
|
333
402
|
return {
|
package/extensions/core/tools.ts
CHANGED
|
@@ -114,7 +114,7 @@ export const isCodeGraphTool = (name: string): boolean => TOOL_NAMES.includes(na
|
|
|
114
114
|
|
|
115
115
|
/** Guidelines added to the system prompt while the tools are active (static: they never break the prompt cache). */
|
|
116
116
|
export const GUIDELINES = [
|
|
117
|
-
"Understand code through CodeGraph first: codegraph_explore for how something works or where to change it, codegraph_search to find a definition, codegraph_callers/codegraph_callees for relations, codegraph_impact before changing a signature, codegraph_files for structure.",
|
|
117
|
+
"Understand code through CodeGraph first: codegraph_explore for how something works or where to change it, codegraph_search to find a definition, codegraph_node with includeCode for a symbol's body (instead of rg -A), codegraph_callers/codegraph_callees for relations, codegraph_impact before changing a signature, codegraph_files for structure.",
|
|
118
118
|
"Do not grep (rg, grep, git grep) for symbol names: such searches are redirected to CodeGraph. Text search is for string literals, comments, config and docs, or after CodeGraph came up empty.",
|
|
119
119
|
"Read a file once CodeGraph has told you where to look, with offset/limit around the reported lines.",
|
|
120
120
|
];
|
package/extensions/graphnav.ts
CHANGED
|
@@ -14,13 +14,16 @@ import { homedir } from "node:os";
|
|
|
14
14
|
import { resolve } from "node:path";
|
|
15
15
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { completions, parseCommand } from "./command/parse.ts";
|
|
17
|
-
import { type Config, DEFAULT_CONFIG, type ProjectOverrides, readConfig, readProjectConfig, saveConfig } from "./core/config.ts";
|
|
17
|
+
import { type Config, DEFAULT_CONFIG, type ProjectOverrides, readConfig, readProjectConfig, saveConfig, type StatusShow } from "./core/config.ts";
|
|
18
18
|
import {
|
|
19
19
|
codemodeSearches,
|
|
20
|
+
candidateSymbols,
|
|
20
21
|
decide,
|
|
21
22
|
findFileSearches,
|
|
22
23
|
findSearches,
|
|
23
24
|
type GateMemory,
|
|
25
|
+
definitionsIn,
|
|
26
|
+
leafOf,
|
|
24
27
|
isSourceFile,
|
|
25
28
|
lookupWords,
|
|
26
29
|
newMemory,
|
|
@@ -51,6 +54,13 @@ export interface Status {
|
|
|
51
54
|
error?: string;
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
/** Does the status line show for this setting? `problems`: only when the user may need to act or wait. */
|
|
58
|
+
export function showsStatus(show: StatusShow, s: Status): boolean {
|
|
59
|
+
if (show === "never") return false;
|
|
60
|
+
if (show === "always") return true;
|
|
61
|
+
return !!s.unavailable || s.state === "indexing" || s.state === "missing" || s.state === "error";
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
export function statusText(s: Status): string {
|
|
55
65
|
if (!s.config.enabled) return "codegraph: off";
|
|
56
66
|
if (s.unavailable) return `codegraph: unavailable · ${s.config.mode}`;
|
|
@@ -131,6 +141,15 @@ export interface Deps {
|
|
|
131
141
|
breaker?: { threshold: number; resetMs: number };
|
|
132
142
|
}
|
|
133
143
|
|
|
144
|
+
const withTimeout = <T>(p: Promise<T>, ms: number): Promise<T> =>
|
|
145
|
+
new Promise((resolve, reject) => {
|
|
146
|
+
const t = setTimeout(() => reject(new Error("timeout")), ms);
|
|
147
|
+
p.then(
|
|
148
|
+
(v) => (clearTimeout(t), resolve(v)),
|
|
149
|
+
(e) => (clearTimeout(t), reject(e)),
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
134
153
|
export function createExtension(deps: Deps = {}) {
|
|
135
154
|
const run = deps.run ?? defaultRunner;
|
|
136
155
|
return function (pi: ExtensionAPI) {
|
|
@@ -158,7 +177,7 @@ export function createExtension(deps: Deps = {}) {
|
|
|
158
177
|
const guardNow = new Map<string, Guard>();
|
|
159
178
|
const unavailable = (root = project?.root) => (root ? (guardNow.get(root)?.open ?? false) : false);
|
|
160
179
|
const snapshot = (): Status => ({ config, project, state, index, error: lastError, ...counts, unavailable: unavailable(), overrides });
|
|
161
|
-
const refresh = () => ui?.setStatus("codegraph", config.enabled && config.showStatus ? statusText(snapshot()) : "");
|
|
180
|
+
const refresh = () => ui?.setStatus("codegraph", config.enabled && showsStatus(config.showStatus, snapshot()) ? statusText(snapshot()) : "");
|
|
162
181
|
|
|
163
182
|
const server = (root: string) => {
|
|
164
183
|
let s = servers.get(root);
|
|
@@ -198,6 +217,9 @@ export function createExtension(deps: Deps = {}) {
|
|
|
198
217
|
index = status;
|
|
199
218
|
lastError = error;
|
|
200
219
|
state = error ? "error" : status.initialized ? "ready" : "missing";
|
|
220
|
+
// the index changed: earlier probe answers may be stale
|
|
221
|
+
mem.unindexed.clear();
|
|
222
|
+
mem.defined.clear();
|
|
201
223
|
})
|
|
202
224
|
.finally(() => {
|
|
203
225
|
job = undefined;
|
|
@@ -300,6 +322,24 @@ export function createExtension(deps: Deps = {}) {
|
|
|
300
322
|
if (!config.enabled || config.mode === "off" || state !== "ready" || unavailable()) return;
|
|
301
323
|
const searches = searchesOf(event.toolName, event.input ?? {});
|
|
302
324
|
if (searches.length === 0) return;
|
|
325
|
+
// Gate only what the index defines: a word it has never heard of is text, not a symbol.
|
|
326
|
+
const root = project?.root;
|
|
327
|
+
if (root) {
|
|
328
|
+
const unknown = candidateSymbols(searches, mem).filter((sym) => !mem.defined.has(leafOf(sym)));
|
|
329
|
+
await Promise.all(
|
|
330
|
+
unknown.map(async (sym) => {
|
|
331
|
+
try {
|
|
332
|
+
const res = await withTimeout(server(root).call("codegraph_search", { query: leafOf(sym), limit: 25, projectPath: root }), 2000);
|
|
333
|
+
if (res.isError) return;
|
|
334
|
+
const at = definitionsIn(resultText(res), sym);
|
|
335
|
+
if (at.length === 0) mem.unindexed.add(leafOf(sym));
|
|
336
|
+
else mem.defined.set(leafOf(sym), at);
|
|
337
|
+
} catch {
|
|
338
|
+
// index unreachable or slow: fail open to the usual gate rather than guess
|
|
339
|
+
}
|
|
340
|
+
}),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
303
343
|
const d = decide(config.mode, searches, `${event.toolName}\u0000${JSON.stringify(event.input)}`, mem);
|
|
304
344
|
if (d.action === "block") {
|
|
305
345
|
counts.blocked++;
|
|
@@ -388,6 +428,11 @@ export function createExtension(deps: Deps = {}) {
|
|
|
388
428
|
await persist();
|
|
389
429
|
ctx.ui.notify(`codegraph: mode ${cmd.mode}`, "info");
|
|
390
430
|
return;
|
|
431
|
+
case "statusline":
|
|
432
|
+
config.showStatus = base.showStatus = cmd.show;
|
|
433
|
+
await persist();
|
|
434
|
+
ctx.ui.notify(`codegraph: status line ${cmd.show}`, "info");
|
|
435
|
+
return;
|
|
391
436
|
case "sync":
|
|
392
437
|
case "reindex":
|
|
393
438
|
case "init": {
|
package/package.json
CHANGED