Skip to content

Commit cea797c

Browse files
committed
Release v0.1.7
1 parent fd8bf04 commit cea797c

5 files changed

Lines changed: 72 additions & 3 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ A global [Pi coding agent](https://github.com/badlogic/pi-mono) extension that a
2121
- **Stay in Plan mode**
2222
- Staying in Plan mode produces a durable acknowledgement and stops the run until the user responds.
2323
- Mode state survives reloads, resumes, and forks.
24+
- When Pi recreates the custom editor, the latest 100 user prompts from the active session branch are restored for Up/Down history navigation.
2425

2526
## Requirements
2627

@@ -110,7 +111,7 @@ npm test
110111
npm pack --dry-run
111112
```
112113

113-
The tests cover state decoding, safe plan paths, mutation restrictions, deferred transitions, complete plan rendering, approval decisions, stop behavior, fresh-session handoff content, and question formatting.
114+
The tests cover state decoding, safe plan paths, mutation restrictions, deferred transitions, mode rendering, session-based prompt history restoration, complete plan rendering, approval decisions, stop behavior, fresh-session handoff content, and question formatting.
114115

115116
### Publishing
116117

index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
buildPlanReviewMessage,
2020
classifyPlanExitChoice,
2121
decodeModeState,
22+
extractPromptHistory,
2223
formatModeStatus,
2324
isAllowedPlanMutation,
2425
makePlanPath,
@@ -366,7 +367,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
366367
updateModeIndicator(ctx);
367368
});
368369

369-
pi.on("session_start", async (_event, ctx) => {
370+
pi.on("session_start", async (event, ctx) => {
370371
currentContext = ctx;
371372
const entries = ctx.sessionManager.getEntries();
372373
const latest = entries
@@ -389,6 +390,8 @@ export default function planBuildModes(pi: ExtensionAPI): void {
389390
updateModeIndicator(ctx);
390391

391392
if (ctx.mode === "tui") {
393+
// Startup history is populated after session_start; replacement flows recreate the editor after that step.
394+
const promptHistory = event.reason === "startup" ? [] : extractPromptHistory(ctx.sessionManager.getBranch());
392395
class ModeEditor extends CustomEditor {
393396
onCycle?: () => void;
394397

@@ -421,6 +424,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
421424
}
422425
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
423426
const editor = new ModeEditor(tui, theme, keybindings);
427+
for (const prompt of promptHistory) editor.addToHistory(prompt);
424428
requestEditorRender = () => editor.requestModeRender();
425429
editor.onCycle = () => {
426430
if (currentContext) void selectMode(nextMode(selectedMode), currentContext, "manual");

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@janvitos/pi-plan-build",
3-
"version": "0.1.6",
3+
"version": "0.1.7",
44
"description": "Plan safely, approve explicitly, then implement here or in a clean session.",
55
"type": "module",
66
"license": "MIT",

utils.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
buildPlanReviewMessage,
1212
classifyPlanExitChoice,
1313
decodeModeState,
14+
extractPromptHistory,
1415
formatModeStatus,
1516
formatQuestionAnswers,
1617
isAllowedPlanMutation,
@@ -157,6 +158,41 @@ test("plan guidance answers informational questions without creating a plan", ()
157158
assert.match(PLAN_EXIT_DESCRIPTION, /After directly answering an informational question/);
158159
});
159160

161+
test("prompt history restores normalized user text in chronological order", () => {
162+
const entries = [
163+
{ type: "message", message: { role: "assistant", content: [{ type: "text", text: "ignore" }] } },
164+
{ type: "message", message: { role: "user", content: " first prompt " } },
165+
{ type: "custom_message", content: "ignore injected context" },
166+
{
167+
type: "message",
168+
message: {
169+
role: "user",
170+
content: [
171+
{ type: "text", text: "second " },
172+
{ type: "image", data: "...", mimeType: "image/png" },
173+
{ type: "text", text: "prompt" },
174+
],
175+
},
176+
},
177+
{ type: "message", message: { role: "user", content: "second prompt" } },
178+
{ type: "message", message: { role: "user", content: [{ type: "image", data: "..." }] } },
179+
];
180+
assert.deepEqual(extractPromptHistory(entries), ["first prompt", "second prompt"]);
181+
});
182+
183+
test("prompt history keeps the latest 100 entries", () => {
184+
const entries = Array.from({ length: 105 }, (_, index) => ({
185+
type: "message",
186+
message: { role: "user", content: `prompt ${index}` },
187+
}));
188+
const history = extractPromptHistory(entries);
189+
assert.equal(history.length, 100);
190+
assert.equal(history[0], "prompt 5");
191+
assert.equal(history.at(-1), "prompt 104");
192+
assert.deepEqual(extractPromptHistory(entries, 2), ["prompt 103", "prompt 104"]);
193+
assert.deepEqual(extractPromptHistory(entries, 0), []);
194+
});
195+
160196
test("question answers use stable model-visible formatting", () => {
161197
assert.equal(
162198
formatQuestionAnswers([

utils.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,34 @@ export function formatQuestionAnswers(answers: QuestionAnswerData[]): string {
121121
return answers.map((answer) => `"${answer.question}"="${answer.answers.length ? answer.answers.join(", ") : "Unanswered"}"`).join(", ");
122122
}
123123

124+
export function extractPromptHistory(entries: readonly unknown[], limit = 100): string[] {
125+
const prompts: string[] = [];
126+
for (const entry of entries) {
127+
if (!entry || typeof entry !== "object") continue;
128+
const candidate = entry as {
129+
type?: unknown;
130+
message?: { role?: unknown; content?: unknown };
131+
};
132+
if (candidate.type !== "message" || candidate.message?.role !== "user") continue;
133+
134+
const content = candidate.message.content;
135+
const text = typeof content === "string"
136+
? content
137+
: Array.isArray(content)
138+
? content
139+
.filter((block): block is { type: "text"; text: string } =>
140+
!!block && typeof block === "object" && (block as { type?: unknown }).type === "text" && typeof (block as { text?: unknown }).text === "string")
141+
.map((block) => block.text)
142+
.join("")
143+
: "";
144+
const trimmed = text.trim();
145+
if (!trimmed || prompts.at(-1) === trimmed) continue;
146+
prompts.push(trimmed);
147+
}
148+
const maxEntries = Math.max(0, Math.floor(limit));
149+
return maxEntries === 0 ? [] : prompts.slice(-maxEntries);
150+
}
151+
124152
export function nextMode(mode: Mode): Mode {
125153
return mode === "build" ? "plan" : "build";
126154
}

0 commit comments

Comments
 (0)