Skip to content

Commit e0367e9

Browse files
Pi extensions to make life easier
NOTE: These have been slopped out by clankers. Use caution.
1 parent 8d72e6b commit e0367e9

3 files changed

Lines changed: 650 additions & 0 deletions

File tree

mappings/common.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
["glide.desktop", "~/.local/share/applications/glide.desktop"],
88
["scripts/*", "~/scripts"],
99
["nvim/*", "~/.config/nvim"],
10+
["pi/answer-extension.ts", "~/.pi/agent/extensions/answer-extension.ts"],
11+
["pi/cursor-rules-extension.ts", "~/.pi/agent/extensions/cursor-rules-extension.ts"],
1012
["pi/save-last-extension.ts", "~/.pi/agent/extensions/save-last-extension.ts"],
1113
["pi/settings.json", "~/.pi/agent/settings.json"],
1214
["tmux.conf", "~/.tmux.conf"],

pi/answer-extension.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/**
2+
* Answer Extension
3+
*
4+
* Parses the previous AI response for questions, asks them interactively
5+
* to the user one at a time, then feeds the Q&A pairs back to the AI.
6+
*
7+
* Usage:
8+
* /answer - extract questions from the last assistant message and answer them
9+
*/
10+
11+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
12+
13+
function getLastAssistantText(ctx: ExtensionCommandContext): string | null {
14+
const branch = ctx.sessionManager.getBranch();
15+
for (let i = branch.length - 1; i >= 0; i--) {
16+
const entry = branch[i];
17+
if (entry.type !== "message") continue;
18+
const msg = entry.message;
19+
if (!("role" in msg) || msg.role !== "assistant") continue;
20+
const textParts = msg.content
21+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
22+
.map((c) => c.text);
23+
if (textParts.length > 0) return textParts.join("\n");
24+
}
25+
return null;
26+
}
27+
28+
/**
29+
* Extract questions from text.
30+
*
31+
* Strategy: find any sentence ending with "?". Works across list items,
32+
* numbered items, inline prose. Strips leading bullet/number markers and
33+
* common markdown emphasis so the prompt reads cleanly.
34+
*/
35+
function extractQuestions(text: string): string[] {
36+
const questions: string[] = [];
37+
const seen = new Set<string>();
38+
39+
// Split into "sentences" by terminal punctuation while keeping the '?' tokens.
40+
// Regex captures text up to and including ? . or ! (or end-of-string).
41+
const sentenceRe = /[^.!?\n]*\?/g;
42+
let match: RegExpExecArray | null;
43+
while ((match = sentenceRe.exec(text)) !== null) {
44+
let q = match[0].trim();
45+
if (!q.endsWith("?")) continue;
46+
47+
// Strip leading list/number markers: "- ", "* ", "1. ", "1) ", "**1.** "
48+
q = q.replace(/^[-*+]\s+/, "");
49+
q = q.replace(/^\*+\s*/, "");
50+
q = q.replace(/^\d+[.)]\s+/, "");
51+
q = q.replace(/^\*\*[^*]+\*\*\s*[:\-]?\s*/, "");
52+
53+
// Strip surrounding markdown emphasis
54+
q = q.replace(/\*\*/g, "").replace(/__/g, "").replace(/`/g, "");
55+
56+
// Collapse whitespace
57+
q = q.replace(/\s+/g, " ").trim();
58+
59+
if (q.length < 3) continue;
60+
if (seen.has(q)) continue;
61+
seen.add(q);
62+
questions.push(q);
63+
}
64+
65+
return questions;
66+
}
67+
68+
function formatQA(pairs: Array<{ question: string; answer: string }>): string {
69+
const lines: string[] = [
70+
"Here are my answers to the questions you asked:",
71+
"",
72+
];
73+
pairs.forEach((p, i) => {
74+
lines.push(`${i + 1}. ${p.question}`);
75+
lines.push(` ${p.answer}`);
76+
lines.push("");
77+
});
78+
lines.push("Please continue based on these answers.");
79+
return lines.join("\n");
80+
}
81+
82+
export default function (pi: ExtensionAPI) {
83+
pi.registerCommand("answer", {
84+
description: "Extract questions from the last AI response and answer them interactively",
85+
handler: async (_args, ctx) => {
86+
if (!ctx.hasUI) {
87+
ctx.ui.notify("/answer requires interactive mode", "error");
88+
return;
89+
}
90+
91+
const lastText = getLastAssistantText(ctx);
92+
if (!lastText) {
93+
ctx.ui.notify("No previous assistant message found", "error");
94+
return;
95+
}
96+
97+
const questions = extractQuestions(lastText);
98+
if (questions.length === 0) {
99+
ctx.ui.notify("No questions found in the last AI response", "warning");
100+
return;
101+
}
102+
103+
ctx.ui.notify(`Found ${questions.length} question${questions.length === 1 ? "" : "s"}`, "info");
104+
105+
const pairs: Array<{ question: string; answer: string }> = [];
106+
for (let i = 0; i < questions.length; i++) {
107+
const q = questions[i];
108+
const title = `Question ${i + 1}/${questions.length}: ${q}`;
109+
const answer = await ctx.ui.input(title, "Type your answer...");
110+
111+
if (answer === null || answer === undefined) {
112+
ctx.ui.notify("Cancelled", "info");
113+
return;
114+
}
115+
116+
pairs.push({ question: q, answer: answer.trim() || "(no answer)" });
117+
}
118+
119+
const reply = formatQA(pairs);
120+
121+
if (ctx.isIdle()) {
122+
pi.sendUserMessage(reply);
123+
} else {
124+
pi.sendUserMessage(reply, { deliverAs: "followUp" });
125+
}
126+
127+
ctx.ui.notify("Answers sent to AI", "info");
128+
},
129+
});
130+
}

0 commit comments

Comments
 (0)