@henryqw/pi-codegraph 0.3.0 → 0.3.2

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
@@ -16,7 +16,7 @@ Requires only the `codegraph` executable on Pi's `PATH`. No MCP server or MCP co
16
16
  | Package | Relationship | Purpose |
17
17
  | --- | --- | --- |
18
18
  | [`@colbymchenry/codegraph`](https://github.com/colbymchenry/codegraph) | Required | Builds the indexes and answers `codegraph explore` queries. |
19
- | [`@henryqw/pi-footer`](https://pi.henry.wang/extensions/pi-footer) | Improves | Shows a compact CodeGraph index badge and tool activity when loaded. |
19
+ | [`@henryqw/pi-footer`](https://pi.henry.wang/extensions/pi-footer) | Improves | Shows quiet CodeGraph setup progress and direct tool activity when loaded. |
20
20
 
21
21
  ## Use
22
22
 
@@ -31,18 +31,20 @@ Then launch `pi` in a linked worktree. When the primary checkout has an index, p
31
31
  | Surface | Type | Purpose |
32
32
  | --- | --- | --- |
33
33
  | `codegraph_explore` | tool | Lets agents explore the indexed code. It returns the source of the relevant symbols grouped by file, plus the call path between them. |
34
- | CodeGraph setup status | ui | Shows users setup progress and errors, and confirms a new worktree index is ready. |
34
+ | CodeGraph setup status | ui | Shows a dim footer spinner and elapsed time during setup. Clears silently when done; setup errors remain visible with recovery instructions. |
35
35
 
36
36
  `codegraph_explore` takes a `query`, an optional `maxFiles` (default 12), and an optional `projectPath` (defaults to Pi's working directory). It runs `codegraph explore --path <projectPath> --max-files <maxFiles> <query>` and returns the output as-is. If the command exits with an error or runs longer than 120 seconds, the tool call fails and shows the end of CodeGraph's error output.
37
37
 
38
38
  ## Flow
39
39
 
40
40
  - A missing worktree index is built only when the primary checkout already has `.codegraph/codegraph.db`.
41
- - Initialization happens when Pi launches, not when Git creates the worktree.
41
+ - Initialization happens when Pi launches, not when Git creates the worktree. In TUI mode it runs in the background so the footer and later startup handlers can load.
42
42
  - Each linked worktree keeps its own `.codegraph`; databases are never copied or shared between branches.
43
43
  - Non-Git directories and repositories without a primary index are not initialized.
44
- - The extension reports `checking index…` and `indexing…` during setup, `indexed` when a database file exists, `missing` when no index was built, `prerequisites missing` when setup was skipped, and `setup failed` on an error. `indexed` does not guarantee database health.
45
- - If `pi-footer` is also loaded, it places a compact `CG` badge first on its third line: `✓ CG` indexed, `○ CG` missing, `◐ CG` checking or indexing, `! CG` setup problem, or `? CG` unknown state. It temporarily shows `● CG` only during `codegraph_explore` calls.
44
+ - Setup shows a dim footer status such as `⠋ codegraph · indexing 8s`, or `waiting for index` when another session holds the initialization lock. Only TUI mode animates the spinner. Progress disappears when setup finishes; there is no success message or idle index badge.
45
+ - Missing prerequisites and setup failures remain visible with a theme-colored `!` icon, meaningful text, and an actionable notification.
46
+ - Creating, resuming, or forking a session waits for in-flight worktree initialization instead of interrupting it. Repeated startup events reuse that initialization. Quitting or reloading Pi cancels the initializer and every Git probe; interrupted initialization keeps the lock for explicit recovery.
47
+ - If `pi-footer` is also loaded, it preserves the setup status on its third line and temporarily shows `● CG` during direct `codegraph_explore` calls.
46
48
 
47
49
  ## State and storage
48
50
 
@@ -7,8 +7,31 @@ import { Type } from "typebox";
7
7
  const LOCK_WAIT_MS = 5 * 60_000;
8
8
  const INIT_TIMEOUT_MS = 10 * 60_000;
9
9
  const EXPLORE_TIMEOUT_MS = 120_000;
10
- const WIDGET_KEY = "pi-codegraph";
11
- const SUCCESS_TTL_MS = 5000;
10
+ const STATUS_KEY = "pi-codegraph";
11
+ const SPINNER_INTERVAL_MS = 100;
12
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
13
+
14
+ function showProgress(ctx: ExtensionContext, action: string, signal: AbortSignal): () => void {
15
+ if (!ctx.hasUI) return () => {};
16
+ signal.throwIfAborted();
17
+ const startedAt = Date.now();
18
+ const update = () => {
19
+ const frame = SPINNER_FRAMES[Math.floor(Date.now() / SPINNER_INTERVAL_MS) % SPINNER_FRAMES.length]!;
20
+ const seconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1000));
21
+ const elapsed = seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`;
22
+ ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("dim", `${frame} codegraph · ${action} ${elapsed}`));
23
+ };
24
+ update();
25
+ const timer = ctx.mode === "tui" ? setInterval(update, SPINNER_INTERVAL_MS) : undefined;
26
+ timer?.unref();
27
+ const stop = () => {
28
+ if (timer) clearInterval(timer);
29
+ signal.removeEventListener("abort", stop);
30
+ ctx.ui.setStatus(STATUS_KEY, undefined);
31
+ };
32
+ signal.addEventListener("abort", stop, { once: true });
33
+ return stop;
34
+ }
12
35
 
13
36
  async function hasIndex(root: string): Promise<boolean> {
14
37
  try {
@@ -19,26 +42,29 @@ async function hasIndex(root: string): Promise<boolean> {
19
42
  }
20
43
  }
21
44
 
22
- async function git(pi: ExtensionAPI, cwd: string, args: string[]): Promise<string> {
23
- const result = await pi.exec("git", args, { cwd, timeout: 10_000 });
45
+ async function git(pi: ExtensionAPI, cwd: string, args: string[], signal: AbortSignal): Promise<string> {
46
+ const result = await pi.exec("git", args, { cwd, timeout: 10_000, signal });
47
+ signal.throwIfAborted();
24
48
  if (result.code !== 0 || result.killed) {
25
49
  throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || `exit ${result.code}`}`);
26
50
  }
27
51
  return result.stdout.replace(/\r?\n$/, "");
28
52
  }
29
53
 
30
- async function initialize(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
31
- const version = await pi.exec("codegraph", ["--version"], { cwd: ctx.cwd, timeout: 10_000 });
54
+ async function initialize(pi: ExtensionAPI, ctx: ExtensionContext, signal: AbortSignal): Promise<void> {
55
+ ctx.ui.setStatus(STATUS_KEY, undefined);
56
+ const version = await pi.exec("codegraph", ["--version"], { cwd: ctx.cwd, timeout: 10_000, signal });
57
+ signal.throwIfAborted();
32
58
  if (version.code !== 0 || version.killed) {
33
59
  const detail = version.killed ? "timed out or killed" : version.stderr.trim().slice(-1000) || `exit ${version.code}`;
34
60
  const message = `pi-codegraph: setup skipped.\ncodegraph --version failed (${detail}). Install CodeGraph: npm install -g @colbymchenry/codegraph. If already installed, check that codegraph runs on Pi's PATH.\nThen restart Pi or /reload.`;
35
- ctx.ui.setStatus("pi-codegraph", "pi-codegraph: prerequisites missing");
61
+ ctx.ui.setStatus(STATUS_KEY, `${ctx.ui.theme.fg("warning", "!")} pi-codegraph: prerequisites missing`);
36
62
  if (ctx.hasUI) ctx.ui.notify(message, "warning");
37
63
  else console.warn(message);
38
64
  return;
39
65
  }
40
- ctx.ui.setStatus("pi-codegraph", "pi-codegraph: missing");
41
- const rootResult = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd: ctx.cwd, timeout: 10_000 });
66
+ const rootResult = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd: ctx.cwd, timeout: 10_000, signal });
67
+ signal.throwIfAborted();
42
68
  if (rootResult.code !== 0 || rootResult.killed) {
43
69
  if (!rootResult.killed && rootResult.stderr.includes("not a git repository")) return;
44
70
  throw new Error(`Cannot locate Git worktree: ${rootResult.stderr.trim() || `exit ${rootResult.code}`}`);
@@ -48,24 +74,29 @@ async function initialize(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void
48
74
  if (process.env.CODEGRAPH_DIR && process.env.CODEGRAPH_DIR !== ".codegraph") {
49
75
  throw new Error("pi-codegraph requires the default CODEGRAPH_DIR (.codegraph). Unset CODEGRAPH_DIR before launching Pi.");
50
76
  }
51
- const gitDir = await git(pi, root, ["rev-parse", "--absolute-git-dir"]);
77
+ const gitDir = await git(pi, root, ["rev-parse", "--absolute-git-dir"], signal);
52
78
  if (!gitDir) throw new Error("Git returned an empty metadata directory.");
53
79
  const lock = join(gitDir, "pi-codegraph-init.lock");
54
80
  // Check the lock before the database: an in-progress/failed init can leave a partial DB.
55
81
  const recovery = `Inspect CodeGraph in ${root}. If no initializer is running, run codegraph index in that directory, then remove ${lock} with rmdir and /reload.`;
56
82
  const deadline = Date.now() + LOCK_WAIT_MS;
57
- ctx.ui.setStatus("pi-codegraph", "pi-codegraph: checking index…");
83
+ let stopProgress = () => {};
84
+ let waiting = false;
58
85
  let indexed = false;
59
86
  try {
60
87
  while (true) {
88
+ signal.throwIfAborted();
61
89
  try {
62
90
  await mkdir(lock);
63
91
  break;
64
92
  } catch (error) {
65
93
  if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
66
94
  if (Date.now() >= deadline) throw new Error(`Timed out waiting for CodeGraph initialization. ${recovery}`);
67
- ctx.ui.setStatus("pi-codegraph", "pi-codegraph: indexing…");
68
- await delay(250);
95
+ if (!waiting) {
96
+ stopProgress = showProgress(ctx, "waiting for index", signal);
97
+ waiting = true;
98
+ }
99
+ await delay(250, undefined, { signal });
69
100
  }
70
101
  }
71
102
  let attempted = false;
@@ -74,24 +105,20 @@ async function initialize(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void
74
105
  indexed = true;
75
106
  return;
76
107
  }
77
- const worktrees = await git(pi, root, ["worktree", "list", "--porcelain", "-z"]);
108
+ const worktrees = await git(pi, root, ["worktree", "list", "--porcelain", "-z"], signal);
78
109
  const first = worktrees.split("\0", 1)[0];
79
110
  if (!first?.startsWith("worktree ")) throw new Error("Git returned an invalid primary worktree.");
80
111
  const primary = first.slice("worktree ".length);
81
112
  if (!primary || primary === root || !(await hasIndex(primary))) return;
82
- ctx.ui.setStatus("pi-codegraph", "pi-codegraph: indexing…");
113
+ stopProgress();
114
+ stopProgress = showProgress(ctx, "indexing", signal);
83
115
  attempted = true;
84
- const result = await pi.exec("codegraph", ["init", "--yes", root], { cwd: root, timeout: INIT_TIMEOUT_MS });
116
+ const result = await pi.exec("codegraph", ["init", "--yes", root], { cwd: root, timeout: INIT_TIMEOUT_MS, signal });
117
+ signal.throwIfAborted();
85
118
  if (result.code !== 0 || result.killed || !(await hasIndex(root))) {
86
119
  throw new Error(`CodeGraph init failed (${result.killed ? "timed out or killed" : `exit ${result.code}`}): ${(result.stderr || result.stdout).trim().slice(-2000)}`);
87
120
  }
88
121
  indexed = true;
89
- if (ctx.hasUI) {
90
- ctx.ui.setWidget(WIDGET_KEY, ["pi-codegraph: index ready"]);
91
- setTimeout(() => ctx.ui.setWidget(WIDGET_KEY, undefined), SUCCESS_TTL_MS);
92
- } else {
93
- ctx.ui.notify("CodeGraph worktree index ready.", "info");
94
- }
95
122
  } catch (error) {
96
123
  if (attempted) throw new Error(`${error instanceof Error ? error.message : String(error)}\n${recovery}`, { cause: error });
97
124
  throw error;
@@ -100,11 +127,21 @@ async function initialize(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void
100
127
  if (!attempted || indexed) await rmdir(lock);
101
128
  }
102
129
  } finally {
103
- ctx.ui.setStatus("pi-codegraph", indexed ? "pi-codegraph: indexed" : "pi-codegraph: missing");
130
+ if (!signal.aborted) {
131
+ stopProgress();
132
+ ctx.ui.setStatus(STATUS_KEY, undefined);
133
+ }
104
134
  }
105
135
  }
106
136
 
107
137
  export default function codegraphExtension(pi: ExtensionAPI): void {
138
+ let initializer: AbortController | undefined;
139
+ let setup: Promise<void> | undefined;
140
+ pi.on("session_shutdown", (event) => {
141
+ // Session replacement waits for worktree-scoped indexing instead of abandoning its lock.
142
+ if (event.reason !== "new" && event.reason !== "resume" && event.reason !== "fork") initializer?.abort();
143
+ return setup;
144
+ });
108
145
  pi.registerTool({
109
146
  name: "codegraph_explore",
110
147
  label: "CodeGraph explore",
@@ -124,14 +161,19 @@ export default function codegraphExtension(pi: ExtensionAPI): void {
124
161
  return { content: [{ type: "text", text: result.stdout }], details: undefined };
125
162
  },
126
163
  });
127
- pi.on("session_start", async (_event, ctx) => {
128
- try {
129
- await initialize(pi, ctx);
130
- } catch (error) {
164
+ pi.on("session_start", (_event, ctx) => {
165
+ if (setup) return ctx.mode === "tui" ? undefined : setup;
166
+ const controller = initializer = new AbortController();
167
+ setup = initialize(pi, ctx, controller.signal).catch((error) => {
168
+ if (controller.signal.aborted) return;
131
169
  const message = `pi-codegraph: ${error instanceof Error ? error.message : String(error)}`;
132
- ctx.ui.setStatus("pi-codegraph", "pi-codegraph: setup failed");
170
+ ctx.ui.setStatus(STATUS_KEY, `${ctx.ui.theme.fg("error", "!")} pi-codegraph: setup failed`);
133
171
  if (ctx.hasUI) ctx.ui.notify(message, "error");
134
172
  else throw new Error(message, { cause: error });
135
- }
173
+ }).finally(() => {
174
+ setup = undefined;
175
+ initializer = undefined;
176
+ });
177
+ if (ctx.mode !== "tui") return setup;
136
178
  });
137
179
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-codegraph",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Initialize CodeGraph indexes in opted-in Git worktrees and expose codegraph_explore as a Pi tool.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -24,11 +24,11 @@
24
24
  "pack:check": "npm pack --dry-run"
25
25
  },
26
26
  "peerDependencies": {
27
- "@earendil-works/pi-coding-agent": ">=0.87.0 <0.100.0",
27
+ "@earendil-works/pi-coding-agent": ">=0.87.0 <0.100.0 || ^1.0.0",
28
28
  "typebox": "^1.3.15"
29
29
  },
30
30
  "devDependencies": {
31
- "@earendil-works/pi-coding-agent": "0.99.1"
31
+ "@earendil-works/pi-coding-agent": "1.0.0"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",