feat: avoid duplicate project-map hint injection by checking context
The before_agent_start handler now inspects the current session context and skips injection if a pi-project-map-hint custom message is already present in the active branch. This prevents duplicate hints on every prompt while still re-injecting after compaction or tree navigation. Also removes the unused hooks/on-prompt.ts prompt-text injector.
This commit is contained in:
+2
-3
@@ -291,8 +291,6 @@ pi-project-map/
|
||||
│ ├── merge.ts # Merge AST + LLM outputs
|
||||
│ ├── format.ts # Dense markdown formatter
|
||||
│ └── config.ts # Skill configuration (thresholds, ignore patterns)
|
||||
├── hooks/
|
||||
│ └── on-prompt.ts # Injects maintenance command into prompts
|
||||
└── README.md # Setup and usage for humans
|
||||
```
|
||||
|
||||
@@ -303,8 +301,9 @@ pi-project-map/
|
||||
- `project-map:reinit [path]` — Force re-initialization of entire project or subtree.
|
||||
|
||||
### Prompt Hook
|
||||
- On every prompt, the skill appends a lightweight instruction:
|
||||
- On each prompt, the skill injects a lightweight custom message **only if it is not already present in the current branch of context**:
|
||||
> "If you modify any source file, run `project-map:patch <path>` to update the analysis. If you suspect staleness, run `project-map:validate`."
|
||||
- The extension checks `ctx.sessionManager.buildSessionContext()` for an existing `pi-project-map-hint` custom message and skips injection when one is found. This prevents duplicate hints after steering, follow-up messages, or multi-turn conversations. The hint is automatically re-injected after compaction or `/tree` navigation removes it from the active path.
|
||||
|
||||
## 9. Risks and Tradeoffs
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// Pi skill prompt hook
|
||||
// Injected into every prompt to remind the agent to maintain .pi-map.md files
|
||||
|
||||
export const MAINTENANCE_INSTRUCTION = `
|
||||
If you modify any source file, run \`project-map:patch <file-path>\` to update the analysis.
|
||||
If you suspect staleness, run \`project-map:validate\`.
|
||||
`;
|
||||
|
||||
export function injectPrompt(originalPrompt: string): string {
|
||||
return `${originalPrompt}\n\n---\n${MAINTENANCE_INSTRUCTION}`;
|
||||
}
|
||||
+24
-2
@@ -50,6 +50,25 @@ function isDirty(content: string): boolean {
|
||||
return content.includes("## dirty") && !content.includes("## dirty\n-");
|
||||
}
|
||||
|
||||
const HINT_CUSTOM_TYPE = "pi-project-map-hint";
|
||||
|
||||
function hintAlreadyInContext(ctx: any): boolean {
|
||||
const manager = ctx?.sessionManager;
|
||||
if (!manager || typeof manager.buildSessionContext !== "function") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { messages } = manager.buildSessionContext();
|
||||
if (!Array.isArray(messages)) return false;
|
||||
|
||||
return messages.some(
|
||||
(m: any) =>
|
||||
m &&
|
||||
m.role === "custom" &&
|
||||
m.customType === HINT_CUSTOM_TYPE,
|
||||
);
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.registerTool({
|
||||
name: "project_map_init",
|
||||
@@ -236,14 +255,17 @@ export default function (pi: ExtensionAPI) {
|
||||
}
|
||||
});
|
||||
|
||||
// Inject maintenance instructions before agent starts
|
||||
// Inject maintenance instructions before agent starts, but only once
|
||||
// within the current branch of context. Re-inject after compaction or
|
||||
// tree navigation removes the hint from the active path.
|
||||
pi.on("before_agent_start", async (_event, _ctx) => {
|
||||
const mapFiles = findPiMapFiles(_ctx.cwd);
|
||||
if (mapFiles.length === 0) return {};
|
||||
if (hintAlreadyInContext(_ctx)) return {};
|
||||
|
||||
return {
|
||||
message: {
|
||||
customType: "pi-project-map-hint",
|
||||
customType: HINT_CUSTOM_TYPE,
|
||||
content:
|
||||
"📋 Project map active: If you modify any source file, run `project_map_patch` with the file path. If you suspect staleness, run `project_map_validate`.",
|
||||
display: false,
|
||||
|
||||
@@ -48,6 +48,9 @@ describe("pi-extension", () => {
|
||||
getApiKeyAndHeaders: vi.fn(async () => ({ apiKey: "test-key", headers: {} })),
|
||||
},
|
||||
ui: { notify: mockNotify },
|
||||
sessionManager: {
|
||||
buildSessionContext: vi.fn(() => ({ messages: [] })),
|
||||
},
|
||||
};
|
||||
|
||||
const mockPi = {
|
||||
@@ -225,5 +228,44 @@ describe("pi-extension", () => {
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("does not inject hint when it is already in context", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTest\n");
|
||||
mockCtx.cwd = dir;
|
||||
mockCtx.sessionManager.buildSessionContext = vi.fn(() => ({
|
||||
messages: [
|
||||
{
|
||||
role: "custom",
|
||||
customType: "pi-project-map-hint",
|
||||
content: "existing hint",
|
||||
display: false,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
const handler = registeredEvents.before_agent_start;
|
||||
const result = await handler(null, mockCtx);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("injects hint again when previous context did not contain it", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTest\n");
|
||||
mockCtx.cwd = dir;
|
||||
mockCtx.sessionManager.buildSessionContext = vi.fn(() => ({
|
||||
messages: [
|
||||
{ role: "user", content: "hello" },
|
||||
{ role: "assistant", content: [{ type: "text", text: "hi" }] },
|
||||
],
|
||||
}));
|
||||
|
||||
const handler = registeredEvents.before_agent_start;
|
||||
const result = await handler(null, mockCtx);
|
||||
|
||||
expect(result).toHaveProperty("message");
|
||||
expect(result.message.customType).toBe("pi-project-map-hint");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user