@janvitos/pi-plan-build 0.1.2 → 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 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
- - Fixed-width, bold, color-coded **Build** (blue) and **Plan** (yellow) status badges in Pi's footer.
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 {
@@ -64,6 +64,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
64
64
  let planPath = "";
65
65
  let toolsBeforeModes: string[] = [];
66
66
  let currentContext: ExtensionContext | undefined;
67
+ let requestEditorRender: (() => void) | undefined;
67
68
  let freshImplementationPlan: string | undefined;
68
69
 
69
70
  pi.registerFlag("plan", {
@@ -94,8 +95,10 @@ export default function planBuildModes(pi: ExtensionAPI): void {
94
95
  pi.appendEntry(STATE_TYPE, stateData());
95
96
  }
96
97
 
97
- function updateStatus(ctx: ExtensionContext): void {
98
- ctx.ui.setStatus(STATUS_KEY, formatModeStatus(selectedMode));
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?.();
99
102
  }
100
103
 
101
104
  function discoverUnmanagedTools(): void {
@@ -134,7 +137,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
134
137
  pendingReminder = undefined;
135
138
  applyTools(mode);
136
139
  }
137
- updateStatus(ctx);
140
+ updateModeIndicator(ctx);
138
141
  persist();
139
142
  }
140
143
 
@@ -360,7 +363,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
360
363
  pi.on("agent_settled", async (_event, ctx) => {
361
364
  runMode = undefined;
362
365
  applyTools(selectedMode);
363
- updateStatus(ctx);
366
+ updateModeIndicator(ctx);
364
367
  });
365
368
 
366
369
  pi.on("session_start", async (_event, ctx) => {
@@ -383,11 +386,31 @@ export default function planBuildModes(pi: ExtensionAPI): void {
383
386
  planPath = makePlanPath(path.join(getAgentDir(), "plans"), ctx.sessionManager.getSessionId());
384
387
  if (selectedMode === "plan") await ensurePlanDirectory();
385
388
  applyTools(selectedMode);
386
- updateStatus(ctx);
389
+ updateModeIndicator(ctx);
387
390
 
388
391
  if (ctx.mode === "tui") {
389
392
  class ModeEditor extends CustomEditor {
390
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
+
391
414
  override handleInput(data: string): void {
392
415
  if (matchesKey(data, Key.tab)) {
393
416
  this.onCycle?.();
@@ -398,6 +421,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
398
421
  }
399
422
  ctx.ui.setEditorComponent((tui, theme, keybindings) => {
400
423
  const editor = new ModeEditor(tui, theme, keybindings);
424
+ requestEditorRender = () => editor.requestModeRender();
401
425
  editor.onCycle = () => {
402
426
  if (currentContext) void selectMode(nextMode(selectedMode), currentContext, "manual");
403
427
  };
@@ -409,6 +433,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
409
433
  pi.on("session_shutdown", async (_event, ctx) => {
410
434
  ctx.ui.setStatus(STATUS_KEY, undefined);
411
435
  ctx.ui.setEditorComponent(undefined);
436
+ requestEditorRender = undefined;
412
437
  currentContext = undefined;
413
438
  });
414
439
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@janvitos/pi-plan-build",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Plan safely, approve explicitly, then implement here or in a clean session.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/utils.ts CHANGED
@@ -3,17 +3,20 @@ import path from "node:path";
3
3
  export type Mode = "build" | "plan";
4
4
 
5
5
  const ANSI_RESET = "\x1b[0m";
6
- const BOLD_BLACK_FOREGROUND = "1;38;2;0;0;0";
7
- const MODE_BADGES: Record<Mode, { background: string; text: string; reservedSpace: string }> = {
8
- // A trailing reset keeps Pi's status sanitizer from trimming the uncolored reserved cell.
9
- plan: { background: "48;2;255;215;0", text: " PLAN ", reservedSpace: "\u00a0" },
10
- build: { background: "48;2;59;130;246", text: " BUILD ", reservedSpace: "" },
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" },
11
9
  };
12
10
 
13
- export function formatModeStatus(mode: Mode): string {
14
- const badge = MODE_BADGES[mode];
15
- const reservedSuffix = badge.reservedSpace ? `${badge.reservedSpace}${ANSI_RESET}` : "";
16
- return `\x1b[${BOLD_BLACK_FOREGROUND};${badge.background}m${badge.text}${ANSI_RESET}${reservedSuffix}`;
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", "]");
17
20
  }
18
21
 
19
22
  export const PLAN_EXIT_APPROVE_CHOICE = "Switch to Build and implement here";