@janvitos/pi-plan-build 0.1.7 → 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.
Files changed (4) hide show
  1. package/README.md +1 -1
  2. package/index.ts +18 -12
  3. package/package.json +1 -1
  4. package/utils.ts +52 -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
- - Bracketed **build** (blue) and **plan** (yellow) indicators rendered as bold prompt prefixes inside Pi's text input box.
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.
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, visibleWidth } from "@earendil-works/pi-tui";
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 {
@@ -20,7 +20,9 @@ import {
20
20
  classifyPlanExitChoice,
21
21
  decodeModeState,
22
22
  extractPromptHistory,
23
- formatModeStatus,
23
+ formatModeMetadata,
24
+ formatModeRail,
25
+ formatModeTopBorder,
24
26
  isAllowedPlanMutation,
25
27
  makePlanPath,
26
28
  nextMode,
@@ -28,6 +30,7 @@ import {
28
30
  PLAN_EXIT_FRESH_CHOICE,
29
31
  PLAN_EXIT_STAY_ACKNOWLEDGEMENT,
30
32
  PLAN_EXIT_STAY_CHOICE,
33
+ renderModeComposer,
31
34
  type Mode,
32
35
  unique,
33
36
  } from "./utils.ts";
@@ -400,18 +403,21 @@ export default function planBuildModes(pi: ExtensionAPI): void {
400
403
  }
401
404
 
402
405
  override render(width: number): string[] {
403
- const badge = formatModeStatus(selectedMode, ctx.ui.theme, this.borderColor);
404
- const prompt = `${badge} `;
405
- const promptWidth = visibleWidth(prompt);
406
- const paddingWidth = Math.min(promptWidth, Math.max(0, Math.floor((width - 1) / 2)));
407
- 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);
408
409
 
409
410
  const lines = super.render(width);
410
- const reservedPrefix = " ".repeat(paddingWidth);
411
- if (paddingWidth === promptWidth && lines[1]?.startsWith(reservedPrefix)) {
412
- lines[1] = prompt + lines[1].slice(reservedPrefix.length);
413
- }
414
- return lines;
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);
415
421
  }
416
422
 
417
423
  override handleInput(data: string): void {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@janvitos/pi-plan-build",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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
@@ -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;255;215;0", text: "plan" },
8
- build: { color: "38;2;59;130;246", text: "build" },
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
- export function formatModeStatus(mode: Mode, theme: ModeStatusTheme, borderColor: (text: string) => string): string {
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 borderColor("[") + modeText + borderColor("]");
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";