@janvitos/pi-plan-build 0.1.6 → 0.1.8
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 +3 -2
- package/index.ts +23 -13
- package/package.json +1 -1
- package/utils.ts +80 -4
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ A global [Pi coding agent](https://github.com/badlogic/pi-mono) extension that a
|
|
|
8
8
|
|
|
9
9
|
- New sessions start in **Build** mode.
|
|
10
10
|
- Bare `Tab` cycles **Build → Plan → Build**, while active autocomplete dropdowns retain Pi's normal Tab completion.
|
|
11
|
-
-
|
|
11
|
+
- The composer uses OpenCode prompt-inspired blue/orange mode colors on a rounded `╭─` top border and `│` rail, plus a mode/thinking metadata row and Pi's bottom border.
|
|
12
12
|
- `/plan`, `/build`, and the `--plan` startup flag.
|
|
13
13
|
- Per-session plans at `~/.pi/agent/plans/<session-id>.md`.
|
|
14
14
|
- In Plan mode, built-in `edit` and `write` are restricted to the exact plan file.
|
|
@@ -21,6 +21,7 @@ A global [Pi coding agent](https://github.com/badlogic/pi-mono) extension that a
|
|
|
21
21
|
- **Stay in Plan mode**
|
|
22
22
|
- Staying in Plan mode produces a durable acknowledgement and stops the run until the user responds.
|
|
23
23
|
- 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.
|
|
24
25
|
|
|
25
26
|
## Requirements
|
|
26
27
|
|
|
@@ -110,7 +111,7 @@ npm test
|
|
|
110
111
|
npm pack --dry-run
|
|
111
112
|
```
|
|
112
113
|
|
|
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.
|
|
114
115
|
|
|
115
116
|
### Publishing
|
|
116
117
|
|
package/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { CustomEditor, getAgentDir, getMarkdownTheme, type EntryRenderer, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { Key, Markdown, matchesKey, Text,
|
|
5
|
+
import { Key, Markdown, matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
7
|
import { registerQuestionTool } from "./question-ui.ts";
|
|
8
8
|
import {
|
|
@@ -19,7 +19,10 @@ import {
|
|
|
19
19
|
buildPlanReviewMessage,
|
|
20
20
|
classifyPlanExitChoice,
|
|
21
21
|
decodeModeState,
|
|
22
|
-
|
|
22
|
+
extractPromptHistory,
|
|
23
|
+
formatModeMetadata,
|
|
24
|
+
formatModeRail,
|
|
25
|
+
formatModeTopBorder,
|
|
23
26
|
isAllowedPlanMutation,
|
|
24
27
|
makePlanPath,
|
|
25
28
|
nextMode,
|
|
@@ -27,6 +30,7 @@ import {
|
|
|
27
30
|
PLAN_EXIT_FRESH_CHOICE,
|
|
28
31
|
PLAN_EXIT_STAY_ACKNOWLEDGEMENT,
|
|
29
32
|
PLAN_EXIT_STAY_CHOICE,
|
|
33
|
+
renderModeComposer,
|
|
30
34
|
type Mode,
|
|
31
35
|
unique,
|
|
32
36
|
} from "./utils.ts";
|
|
@@ -366,7 +370,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
366
370
|
updateModeIndicator(ctx);
|
|
367
371
|
});
|
|
368
372
|
|
|
369
|
-
pi.on("session_start", async (
|
|
373
|
+
pi.on("session_start", async (event, ctx) => {
|
|
370
374
|
currentContext = ctx;
|
|
371
375
|
const entries = ctx.sessionManager.getEntries();
|
|
372
376
|
const latest = entries
|
|
@@ -389,6 +393,8 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
389
393
|
updateModeIndicator(ctx);
|
|
390
394
|
|
|
391
395
|
if (ctx.mode === "tui") {
|
|
396
|
+
// Startup history is populated after session_start; replacement flows recreate the editor after that step.
|
|
397
|
+
const promptHistory = event.reason === "startup" ? [] : extractPromptHistory(ctx.sessionManager.getBranch());
|
|
392
398
|
class ModeEditor extends CustomEditor {
|
|
393
399
|
onCycle?: () => void;
|
|
394
400
|
|
|
@@ -397,18 +403,21 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
397
403
|
}
|
|
398
404
|
|
|
399
405
|
override render(width: number): string[] {
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
const paddingWidth = Math.min(promptWidth, Math.max(0, Math.floor((width - 1) / 2)));
|
|
404
|
-
if (this.getPaddingX() !== paddingWidth) this.setPaddingX(paddingWidth);
|
|
406
|
+
const railWidth = 2;
|
|
407
|
+
const paddingWidth = Math.min(railWidth, Math.max(0, Math.floor((width - 1) / 2)));
|
|
408
|
+
if (this.getPaddingX() !== railWidth) this.setPaddingX(railWidth);
|
|
405
409
|
|
|
406
410
|
const lines = super.render(width);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
411
|
+
if (paddingWidth !== railWidth) return lines;
|
|
412
|
+
|
|
413
|
+
const rail = `${formatModeRail(selectedMode)} `;
|
|
414
|
+
const metadata = truncateToWidth(
|
|
415
|
+
formatModeMetadata(selectedMode, pi.getThinkingLevel(), ctx.ui.theme, this.borderColor),
|
|
416
|
+
width,
|
|
417
|
+
"",
|
|
418
|
+
);
|
|
419
|
+
const topBorder = formatModeTopBorder(selectedMode, width);
|
|
420
|
+
return renderModeComposer(lines, topBorder, rail, metadata, railWidth);
|
|
412
421
|
}
|
|
413
422
|
|
|
414
423
|
override handleInput(data: string): void {
|
|
@@ -421,6 +430,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
421
430
|
}
|
|
422
431
|
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
423
432
|
const editor = new ModeEditor(tui, theme, keybindings);
|
|
433
|
+
for (const prompt of promptHistory) editor.addToHistory(prompt);
|
|
424
434
|
requestEditorRender = () => editor.requestModeRender();
|
|
425
435
|
editor.onCycle = () => {
|
|
426
436
|
if (currentContext) void selectMode(nextMode(selectedMode), currentContext, "manual");
|
package/package.json
CHANGED
package/utils.ts
CHANGED
|
@@ -4,18 +4,66 @@ export type Mode = "build" | "plan";
|
|
|
4
4
|
|
|
5
5
|
const ANSI_RESET = "\x1b[0m";
|
|
6
6
|
const MODE_LABELS: Record<Mode, { color: string; text: string }> = {
|
|
7
|
-
plan: { color: "38;2;
|
|
8
|
-
build: { color: "38;2;
|
|
7
|
+
plan: { color: "38;2;245;167;66", text: "plan" },
|
|
8
|
+
build: { color: "38;2;92;156;245", text: "build" },
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export interface ModeStatusTheme {
|
|
12
12
|
bold(text: string): string;
|
|
13
|
+
fg(color: "dim", text: string): string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
function formatModeColor(mode: Mode, text: string): string {
|
|
17
|
+
return `\x1b[${MODE_LABELS[mode].color}m${text}${ANSI_RESET}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatModeRail(mode: Mode): string {
|
|
21
|
+
return formatModeColor(mode, "│");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatModeTopBorder(mode: Mode, width: number): string {
|
|
25
|
+
if (width <= 0) return "";
|
|
26
|
+
return formatModeColor(mode, `╭${"─".repeat(width - 1)}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatModeMetadata(
|
|
30
|
+
mode: Mode,
|
|
31
|
+
thinkingLevel: string,
|
|
32
|
+
theme: ModeStatusTheme,
|
|
33
|
+
thinkingColor: (text: string) => string,
|
|
34
|
+
): string {
|
|
16
35
|
const label = MODE_LABELS[mode];
|
|
17
36
|
const modeText = `\x1b[${label.color}m${theme.bold(label.text)}${ANSI_RESET}`;
|
|
18
|
-
return
|
|
37
|
+
return `${formatModeRail(mode)} ${modeText}${theme.fg("dim", " · ")}${thinkingColor(thinkingLevel)}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function renderModeComposer(
|
|
41
|
+
lines: string[],
|
|
42
|
+
topBorder: string,
|
|
43
|
+
railPrefix: string,
|
|
44
|
+
metadata: string,
|
|
45
|
+
reservedWidth: number,
|
|
46
|
+
): string[] {
|
|
47
|
+
if (reservedWidth <= 0 || lines.length < 3) return lines;
|
|
48
|
+
const reservedPrefix = " ".repeat(reservedWidth);
|
|
49
|
+
const bottomBorderIndex = lines.findIndex((line, index) => index > 0 && !line.startsWith(reservedPrefix));
|
|
50
|
+
if (bottomBorderIndex < 2) return lines;
|
|
51
|
+
const promptLines = lines
|
|
52
|
+
.slice(1, bottomBorderIndex)
|
|
53
|
+
.map((line) => railPrefix + line.slice(reservedPrefix.length));
|
|
54
|
+
const bottomBorder = lines[bottomBorderIndex]!.replace(
|
|
55
|
+
/^((?:\x1b\[[0-?]*[ -/]*[@-~])*)./u,
|
|
56
|
+
"$1 ",
|
|
57
|
+
);
|
|
58
|
+
return [
|
|
59
|
+
topBorder,
|
|
60
|
+
...promptLines,
|
|
61
|
+
railPrefix,
|
|
62
|
+
metadata,
|
|
63
|
+
bottomBorder,
|
|
64
|
+
"",
|
|
65
|
+
...lines.slice(bottomBorderIndex + 1),
|
|
66
|
+
];
|
|
19
67
|
}
|
|
20
68
|
|
|
21
69
|
export const PLAN_EXIT_APPROVE_CHOICE = "Switch to Build and implement here";
|
|
@@ -121,6 +169,34 @@ export function formatQuestionAnswers(answers: QuestionAnswerData[]): string {
|
|
|
121
169
|
return answers.map((answer) => `"${answer.question}"="${answer.answers.length ? answer.answers.join(", ") : "Unanswered"}"`).join(", ");
|
|
122
170
|
}
|
|
123
171
|
|
|
172
|
+
export function extractPromptHistory(entries: readonly unknown[], limit = 100): string[] {
|
|
173
|
+
const prompts: string[] = [];
|
|
174
|
+
for (const entry of entries) {
|
|
175
|
+
if (!entry || typeof entry !== "object") continue;
|
|
176
|
+
const candidate = entry as {
|
|
177
|
+
type?: unknown;
|
|
178
|
+
message?: { role?: unknown; content?: unknown };
|
|
179
|
+
};
|
|
180
|
+
if (candidate.type !== "message" || candidate.message?.role !== "user") continue;
|
|
181
|
+
|
|
182
|
+
const content = candidate.message.content;
|
|
183
|
+
const text = typeof content === "string"
|
|
184
|
+
? content
|
|
185
|
+
: Array.isArray(content)
|
|
186
|
+
? content
|
|
187
|
+
.filter((block): block is { type: "text"; text: string } =>
|
|
188
|
+
!!block && typeof block === "object" && (block as { type?: unknown }).type === "text" && typeof (block as { text?: unknown }).text === "string")
|
|
189
|
+
.map((block) => block.text)
|
|
190
|
+
.join("")
|
|
191
|
+
: "";
|
|
192
|
+
const trimmed = text.trim();
|
|
193
|
+
if (!trimmed || prompts.at(-1) === trimmed) continue;
|
|
194
|
+
prompts.push(trimmed);
|
|
195
|
+
}
|
|
196
|
+
const maxEntries = Math.max(0, Math.floor(limit));
|
|
197
|
+
return maxEntries === 0 ? [] : prompts.slice(-maxEntries);
|
|
198
|
+
}
|
|
199
|
+
|
|
124
200
|
export function nextMode(mode: Mode): Mode {
|
|
125
201
|
return mode === "build" ? "plan" : "build";
|
|
126
202
|
}
|