@janvitos/pi-plan-build 0.1.13 → 0.1.15

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 +25 -4
  3. package/package.json +1 -1
  4. package/utils.ts +41 -13
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
- - The composer uses OpenCode prompt-inspired blue/orange mode colors on a rounded `╭─` top border and `│` rail, plus a mode/model/thinking metadata row and Pi's bottom border. The model is shown as `model-id [provider]` (for example, `gpt-5.6-luna [openai]`), with the model ID inheriting the terminal foreground like unselected entries in `/model` and the provider using the footer's dim text color. The footer keeps the remaining path and usage stats without duplicating model metadata.
11
+ - The composer uses OpenCode prompt-inspired blue/orange mode colors on the top and left borders, complemented by Pi's border color on the right and bottom borders; rounded corners close the frame while matching their adjoining horizontal border colors. It also includes a mode/model/thinking metadata row; cycling the thinking level updates this row without adding a duplicate status above the composer. The model is shown as `model-id [provider]` (for example, `gpt-5.6-luna [openai]`), with the model ID inheriting the terminal foreground like unselected entries in `/model` and the provider using the footer's dim text color. The footer keeps the remaining path and usage stats without duplicating model metadata.
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, truncateToWidth } from "@earendil-works/pi-tui";
5
+ import { Key, Markdown, matchesKey, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
6
6
  import { Type } from "typebox";
7
7
  import { registerQuestionTool } from "./question-ui.ts";
8
8
  import {
@@ -28,6 +28,7 @@ import {
28
28
  isAllowedPlanMutation,
29
29
  makePlanPath,
30
30
  nextMode,
31
+ nextThinkingLevel,
31
32
  PLAN_EXIT_APPROVE_CHOICE,
32
33
  PLAN_EXIT_FRESH_CHOICE,
33
34
  PLAN_EXIT_STAY_ACKNOWLEDGEMENT,
@@ -477,6 +478,8 @@ export default function planBuildModes(pi: ExtensionAPI): void {
477
478
  const promptHistory = event.reason === "startup" ? [] : extractPromptHistory(ctx.sessionManager.getBranch());
478
479
  class ModeEditor extends CustomEditor {
479
480
  onCycle?: () => void;
481
+ onCycleThinking?: () => void;
482
+ matchesThinkingCycle?: (data: string) => boolean;
480
483
 
481
484
  requestModeRender(): void {
482
485
  this.tui.requestRender();
@@ -490,20 +493,29 @@ export default function planBuildModes(pi: ExtensionAPI): void {
490
493
  const lines = super.render(width);
491
494
  if (paddingWidth !== railWidth) return lines;
492
495
 
493
- const rail = `${formatModeRail(selectedMode)} `;
496
+ const leftRail = `${formatModeRail(selectedMode)} `;
497
+ const rightRail = this.borderColor("│");
494
498
  const metadata = truncateToWidth(
495
499
  formatModeMetadata(selectedMode, pi.getThinkingLevel(), ctx.ui.theme, this.borderColor, {
496
500
  modelName: ctx.model?.id ?? "no-model",
497
501
  modelProvider: ctx.model?.provider,
498
502
  }),
499
- width,
503
+ width - 1,
500
504
  "",
501
505
  );
502
506
  const topBorder = formatModeTopBorder(selectedMode, width);
503
- return renderModeComposer(lines, topBorder, rail, metadata, railWidth);
507
+ return renderModeComposer(lines, topBorder, leftRail, rightRail, metadata, railWidth, width, {
508
+ truncate: (line, maxWidth) => truncateToWidth(line, maxWidth, ""),
509
+ measure: visibleWidth,
510
+ });
504
511
  }
505
512
 
506
513
  override handleInput(data: string): void {
514
+ if (this.matchesThinkingCycle?.(data)) {
515
+ if (this.onExtensionShortcut?.(data)) return;
516
+ this.onCycleThinking?.();
517
+ return;
518
+ }
507
519
  if (matchesKey(data, Key.tab) && !this.isShowingAutocomplete()) {
508
520
  this.onCycle?.();
509
521
  return;
@@ -518,6 +530,15 @@ export default function planBuildModes(pi: ExtensionAPI): void {
518
530
  editor.onCycle = () => {
519
531
  if (currentContext) void selectMode(nextMode(selectedMode), currentContext, "manual");
520
532
  };
533
+ editor.matchesThinkingCycle = (data) =>
534
+ keybindings.matches(data, "app.thinking.cycle") &&
535
+ !keybindings.matches(data, "tui.editor.historyPrevious") &&
536
+ !keybindings.matches(data, "tui.editor.historyNext");
537
+ editor.onCycleThinking = () => {
538
+ const level = nextThinkingLevel(pi.getThinkingLevel(), ctx.model);
539
+ if (level) pi.setThinkingLevel(level);
540
+ editor.requestModeRender();
541
+ };
521
542
  return editor;
522
543
  });
523
544
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@janvitos/pi-plan-build",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
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
@@ -9,7 +9,6 @@ const MODE_LABELS: Record<Mode, { color: string; text: string }> = {
9
9
  };
10
10
 
11
11
  export interface ModeStatusTheme {
12
- bold(text: string): string;
13
12
  fg(color: "dim", text: string): string;
14
13
  }
15
14
 
@@ -18,6 +17,23 @@ export interface PromptMetadataOptions {
18
17
  modelProvider?: string;
19
18
  }
20
19
 
20
+ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
21
+ const THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
22
+
23
+ export function nextThinkingLevel(
24
+ current: ThinkingLevel,
25
+ model: { reasoning: boolean; thinkingLevelMap?: Partial<Record<ThinkingLevel, unknown>> } | undefined,
26
+ ): ThinkingLevel | undefined {
27
+ if (!model?.reasoning) return undefined;
28
+ const available = THINKING_LEVELS.filter((level) => {
29
+ const mapped = model.thinkingLevelMap?.[level];
30
+ if (mapped === null) return false;
31
+ return level !== "xhigh" && level !== "max" || mapped !== undefined;
32
+ });
33
+ const currentIndex = available.indexOf(current);
34
+ return available[(currentIndex + 1) % available.length];
35
+ }
36
+
21
37
  function formatModeColor(mode: Mode, text: string): string {
22
38
  return `\x1b[${MODE_LABELS[mode].color}m${text}${ANSI_RESET}`;
23
39
  }
@@ -27,8 +43,8 @@ export function formatModeRail(mode: Mode): string {
27
43
  }
28
44
 
29
45
  export function formatModeTopBorder(mode: Mode, width: number): string {
30
- if (width <= 0) return "";
31
- return formatModeColor(mode, `╭${"─".repeat(width - 1)}`);
46
+ if (width <= 1) return "";
47
+ return formatModeColor(mode, `╭${"─".repeat(width - 2)}╮`);
32
48
  }
33
49
 
34
50
  export function formatModeMetadata(
@@ -39,7 +55,7 @@ export function formatModeMetadata(
39
55
  options?: PromptMetadataOptions,
40
56
  ): string {
41
57
  const label = MODE_LABELS[mode];
42
- const modeText = `\x1b[${label.color}m${theme.bold(label.text)}${ANSI_RESET}`;
58
+ const modeText = `\x1b[${label.color}m${label.text}${ANSI_RESET}`;
43
59
  const modelText = options
44
60
  ? `${theme.fg("dim", " • ")}${options.modelName}${
45
61
  options.modelProvider ? theme.fg("dim", ` [${options.modelProvider}]`) : ""
@@ -68,29 +84,41 @@ export function formatFooterCwd(cwd: string, home: string | undefined): string {
68
84
  return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
69
85
  }
70
86
 
87
+ export interface LineWidthTools {
88
+ truncate(line: string, width: number): string;
89
+ measure(line: string): number;
90
+ }
91
+
71
92
  export function renderModeComposer(
72
93
  lines: string[],
73
94
  topBorder: string,
74
- railPrefix: string,
95
+ leftRailPrefix: string,
96
+ rightRail: string,
75
97
  metadata: string,
76
98
  reservedWidth: number,
99
+ width: number,
100
+ lineWidth: LineWidthTools,
77
101
  ): string[] {
78
- if (reservedWidth <= 0 || lines.length < 3) return lines;
102
+ if (reservedWidth <= 0 || width <= 1 || lines.length < 3) return lines;
79
103
  const reservedPrefix = " ".repeat(reservedWidth);
80
104
  const bottomBorderIndex = lines.findIndex((line, index) => index > 0 && !line.startsWith(reservedPrefix));
81
105
  if (bottomBorderIndex < 2) return lines;
106
+
107
+ const addRightRail = (line: string): string => {
108
+ const content = lineWidth.truncate(line, width - 1);
109
+ return `${content}${" ".repeat(Math.max(0, width - 1 - lineWidth.measure(content)))}${rightRail}`;
110
+ };
82
111
  const promptLines = lines
83
112
  .slice(1, bottomBorderIndex)
84
- .map((line) => railPrefix + line.slice(reservedPrefix.length));
85
- const bottomBorder = lines[bottomBorderIndex]!.replace(
86
- /^((?:\x1b\[[0-?]*[ -/]*[@-~])*)./u,
87
- "$1 ",
88
- );
113
+ .map((line) => addRightRail(leftRailPrefix + line.slice(reservedPrefix.length)));
114
+ const bottomBorder = lineWidth.truncate(lines[bottomBorderIndex]!, width)
115
+ .replace(/^((?:\x1b\[[0-?]*[ -/]*[@-~])*)./u, "$1╰")
116
+ .replace(/.(?=(?:\x1b\[[0-?]*[ -/]*[@-~])*$)/u, "╯");
89
117
  return [
90
118
  topBorder,
91
119
  ...promptLines,
92
- railPrefix,
93
- metadata,
120
+ addRightRail(leftRailPrefix),
121
+ addRightRail(metadata),
94
122
  bottomBorder,
95
123
  "",
96
124
  ...lines.slice(bottomBorderIndex + 1),