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 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` | `true` | status line entry |
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
- /** Show the status line entry. */
18
- showStatus: boolean;
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: true,
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: typeof r.showStatus === "boolean" ? r.showStatus : d.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,
@@ -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)) ?? []) if (!known(sym, mem)) (s.files ? names : symbols).add(sym);
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 hint = `"${list.join('", "')}" ${list.length === 1 ? "looks like a symbol" : "look like symbols"}: CodeGraph answers this structurally. Use ${suggestion(list)}.`;
330
- if (mode === "nudge") return { action: "nudge", hint: `[codegraph] ${hint}` };
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 {
@@ -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
  ];
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-graphnav",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "CodeGraph-first code navigation for pi: persistent index, auto init/sync, and a gate that steers structural searches to codegraph_* tools.",
5
5
  "keywords": [
6
6
  "pi-package",