@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 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
- - `[Build]` and `[Plan]` status indicators 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 {
@@ -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 updateStatus(ctx: ExtensionContext): void {
97
- const label = selectedMode === "plan"
98
- ? ctx.ui.theme.fg("warning", "Plan")
99
- : ctx.ui.theme.fg("border", "Build");
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
- updateStatus(ctx);
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
- updateStatus(ctx);
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
- updateStatus(ctx);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@janvitos/pi-plan-build",
3
- "version": "0.1.1",
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
@@ -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";