@henryqw/pi-codegraph 0.3.1 → 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 +7 -5
- package/extensions/codegraph.ts +71 -29
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
-
|
|
45
|
-
-
|
|
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
|
|
package/extensions/codegraph.ts
CHANGED
|
@@ -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
|
|
11
|
-
const
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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("
|
|
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
|
}
|