@janvitos/pi-plan-build 0.1.1 → 0.1.3
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 +1 -1
- package/index.ts +32 -9
- package/package.json +1 -1
- package/utils.ts +17 -0
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** and replaces normal Tab autocomplete.
|
|
11
|
-
-
|
|
11
|
+
- Bracketed **build** (blue) and **plan** (yellow) indicators rendered as bold prompt prefixes inside Pi's text input box.
|
|
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.
|
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 } from "@earendil-works/pi-tui";
|
|
5
|
+
import { Key, Markdown, matchesKey, Text, visibleWidth } from "@earendil-works/pi-tui";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
7
|
import { registerQuestionTool } from "./question-ui.ts";
|
|
8
8
|
import {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
buildPlanReviewMessage,
|
|
20
20
|
classifyPlanExitChoice,
|
|
21
21
|
decodeModeState,
|
|
22
|
+
formatModeStatus,
|
|
22
23
|
isAllowedPlanMutation,
|
|
23
24
|
makePlanPath,
|
|
24
25
|
nextMode,
|
|
@@ -63,6 +64,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
63
64
|
let planPath = "";
|
|
64
65
|
let toolsBeforeModes: string[] = [];
|
|
65
66
|
let currentContext: ExtensionContext | undefined;
|
|
67
|
+
let requestEditorRender: (() => void) | undefined;
|
|
66
68
|
let freshImplementationPlan: string | undefined;
|
|
67
69
|
|
|
68
70
|
pi.registerFlag("plan", {
|
|
@@ -93,11 +95,10 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
93
95
|
pi.appendEntry(STATE_TYPE, stateData());
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
ctx.ui.setStatus(STATUS_KEY, `[${label}]`);
|
|
98
|
+
function updateModeIndicator(ctx: ExtensionContext): void {
|
|
99
|
+
// Clear the legacy footer status; the mode is now rendered inside the editor.
|
|
100
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
101
|
+
requestEditorRender?.();
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
function discoverUnmanagedTools(): void {
|
|
@@ -136,7 +137,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
136
137
|
pendingReminder = undefined;
|
|
137
138
|
applyTools(mode);
|
|
138
139
|
}
|
|
139
|
-
|
|
140
|
+
updateModeIndicator(ctx);
|
|
140
141
|
persist();
|
|
141
142
|
}
|
|
142
143
|
|
|
@@ -362,7 +363,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
362
363
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
363
364
|
runMode = undefined;
|
|
364
365
|
applyTools(selectedMode);
|
|
365
|
-
|
|
366
|
+
updateModeIndicator(ctx);
|
|
366
367
|
});
|
|
367
368
|
|
|
368
369
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -385,11 +386,31 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
385
386
|
planPath = makePlanPath(path.join(getAgentDir(), "plans"), ctx.sessionManager.getSessionId());
|
|
386
387
|
if (selectedMode === "plan") await ensurePlanDirectory();
|
|
387
388
|
applyTools(selectedMode);
|
|
388
|
-
|
|
389
|
+
updateModeIndicator(ctx);
|
|
389
390
|
|
|
390
391
|
if (ctx.mode === "tui") {
|
|
391
392
|
class ModeEditor extends CustomEditor {
|
|
392
393
|
onCycle?: () => void;
|
|
394
|
+
|
|
395
|
+
requestModeRender(): void {
|
|
396
|
+
this.tui.requestRender();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
override render(width: number): string[] {
|
|
400
|
+
const badge = formatModeStatus(selectedMode, ctx.ui.theme);
|
|
401
|
+
const prompt = `${badge} `;
|
|
402
|
+
const promptWidth = visibleWidth(prompt);
|
|
403
|
+
const paddingWidth = Math.min(promptWidth, Math.max(0, Math.floor((width - 1) / 2)));
|
|
404
|
+
if (this.getPaddingX() !== paddingWidth) this.setPaddingX(paddingWidth);
|
|
405
|
+
|
|
406
|
+
const lines = super.render(width);
|
|
407
|
+
const reservedPrefix = " ".repeat(paddingWidth);
|
|
408
|
+
if (paddingWidth === promptWidth && lines[1]?.startsWith(reservedPrefix)) {
|
|
409
|
+
lines[1] = prompt + lines[1].slice(reservedPrefix.length);
|
|
410
|
+
}
|
|
411
|
+
return lines;
|
|
412
|
+
}
|
|
413
|
+
|
|
393
414
|
override handleInput(data: string): void {
|
|
394
415
|
if (matchesKey(data, Key.tab)) {
|
|
395
416
|
this.onCycle?.();
|
|
@@ -400,6 +421,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
400
421
|
}
|
|
401
422
|
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
402
423
|
const editor = new ModeEditor(tui, theme, keybindings);
|
|
424
|
+
requestEditorRender = () => editor.requestModeRender();
|
|
403
425
|
editor.onCycle = () => {
|
|
404
426
|
if (currentContext) void selectMode(nextMode(selectedMode), currentContext, "manual");
|
|
405
427
|
};
|
|
@@ -411,6 +433,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
411
433
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
412
434
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
413
435
|
ctx.ui.setEditorComponent(undefined);
|
|
436
|
+
requestEditorRender = undefined;
|
|
414
437
|
currentContext = undefined;
|
|
415
438
|
});
|
|
416
439
|
}
|
package/package.json
CHANGED
package/utils.ts
CHANGED
|
@@ -2,6 +2,23 @@ import path from "node:path";
|
|
|
2
2
|
|
|
3
3
|
export type Mode = "build" | "plan";
|
|
4
4
|
|
|
5
|
+
const ANSI_RESET = "\x1b[0m";
|
|
6
|
+
const MODE_LABELS: Record<Mode, { color: string; text: string }> = {
|
|
7
|
+
plan: { color: "38;2;255;215;0", text: "plan" },
|
|
8
|
+
build: { color: "38;2;59;130;246", text: "build" },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface ModeStatusTheme {
|
|
12
|
+
fg(color: "dim", text: string): string;
|
|
13
|
+
bold(text: string): string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatModeStatus(mode: Mode, theme: ModeStatusTheme): string {
|
|
17
|
+
const label = MODE_LABELS[mode];
|
|
18
|
+
const modeText = `\x1b[${label.color}m${theme.bold(label.text)}${ANSI_RESET}`;
|
|
19
|
+
return theme.fg("dim", "[") + modeText + theme.fg("dim", "]");
|
|
20
|
+
}
|
|
21
|
+
|
|
5
22
|
export const PLAN_EXIT_APPROVE_CHOICE = "Switch to Build and implement here";
|
|
6
23
|
export const PLAN_EXIT_FRESH_CHOICE = "Start fresh and implement";
|
|
7
24
|
export const PLAN_EXIT_STAY_CHOICE = "Stay in Plan mode";
|