@milanglacier/pi-plan-mode 0.5.1 → 0.5.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milanglacier/pi-plan-mode",
3
- "version": "0.5.1",
3
+ "version": "0.5.5",
4
4
  "description": "Planning mode extension for pi with persistent plan files and branch-aware planning.",
5
5
  "keywords": [
6
6
  "branch-planning",
@@ -34,7 +34,8 @@
34
34
  ],
35
35
  "type": "module",
36
36
  "scripts": {
37
- "test": "vitest run"
37
+ "test": "vitest run",
38
+ "check": "npm test"
38
39
  },
39
40
  "devDependencies": {
40
41
  "vitest": "^4.1.8"
package/qna/index.ts CHANGED
@@ -1,3 +1,2 @@
1
- export * from "./pi-tui-loader.js";
2
1
  export * from "./qna-tui.js";
3
2
  export * from "./scroll-select.js";
package/qna/qna-tui.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { requirePiTuiModule } from "./pi-tui-loader.js";
1
+ import { Editor, Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
2
 
3
3
  type Component = {
4
4
  handleInput: (data: string) => void;
@@ -18,35 +18,6 @@ type EditorTheme = {
18
18
  };
19
19
  };
20
20
 
21
- function getPiTui() {
22
- return requirePiTuiModule() as {
23
- Editor: new (
24
- tui: TUI,
25
- theme: EditorTheme,
26
- ) => {
27
- disableSubmit?: boolean;
28
- onChange?: () => void;
29
- setText: (text: string) => void;
30
- getText: () => string;
31
- render: (width: number) => string[];
32
- handleInput: (data: string) => void;
33
- };
34
- Key: {
35
- enter: string;
36
- tab: string;
37
- escape: string;
38
- up: string;
39
- down: string;
40
- ctrl: (key: string) => string;
41
- shift: (key: string) => string;
42
- };
43
- matchesKey: (input: string, key: string) => boolean;
44
- truncateToWidth: (text: string, width: number) => string;
45
- visibleWidth: (text: string) => number;
46
- wrapTextWithAnsi: (text: string, width: number) => string[];
47
- };
48
- }
49
-
50
21
  export interface QnAOption {
51
22
  label: string;
52
23
  description: string;
@@ -341,8 +312,18 @@ export class QnATuiComponent<TQuestion extends QnAQuestion> implements Component
341
312
  },
342
313
  };
343
314
 
344
- const { Editor } = getPiTui();
345
- this.editor = new Editor(tui, editorTheme);
315
+ const TuiEditor = Editor as unknown as new (
316
+ tui: TUI,
317
+ theme: EditorTheme,
318
+ ) => {
319
+ disableSubmit?: boolean;
320
+ onChange?: () => void;
321
+ setText: (text: string) => void;
322
+ getText: () => string;
323
+ render: (width: number) => string[];
324
+ handleInput: (data: string) => void;
325
+ };
326
+ this.editor = new TuiEditor(tui, editorTheme);
346
327
  this.editor.disableSubmit = true;
347
328
  this.editor.onChange = () => {
348
329
  this.saveCurrentResponse();
@@ -510,8 +491,6 @@ export class QnATuiComponent<TQuestion extends QnAQuestion> implements Component
510
491
  }
511
492
 
512
493
  handleInput(data: string): void {
513
- const { Key, matchesKey } = getPiTui();
514
-
515
494
  if (this.showingConfirmation) {
516
495
  if (matchesKey(data, Key.enter)) {
517
496
  this.submit();
@@ -640,8 +619,6 @@ export class QnATuiComponent<TQuestion extends QnAQuestion> implements Component
640
619
  }
641
620
 
642
621
  render(width: number): string[] {
643
- const { truncateToWidth, visibleWidth, wrapTextWithAnsi } = getPiTui();
644
-
645
622
  if (this.cachedLines && this.cachedWidth === width) {
646
623
  return this.cachedLines;
647
624
  }
@@ -1,41 +1,4 @@
1
- import { requirePiTuiModule } from "./pi-tui-loader.js";
2
-
3
- let cachedPiTui:
4
- | {
5
- Key: {
6
- enter: string;
7
- escape: string;
8
- up: string;
9
- down: string;
10
- ctrl: (key: string) => string;
11
- };
12
- matchesKey: (input: string, key: string) => boolean;
13
- truncateToWidth: (text: string, width: number) => string;
14
- visibleWidth: (text: string) => number;
15
- wrapTextWithAnsi: (text: string, width: number) => string[];
16
- }
17
- | undefined;
18
-
19
- function getPiTui() {
20
- if (cachedPiTui) {
21
- return cachedPiTui;
22
- }
23
-
24
- cachedPiTui = requirePiTuiModule() as {
25
- Key: {
26
- enter: string;
27
- escape: string;
28
- up: string;
29
- down: string;
30
- ctrl: (key: string) => string;
31
- };
32
- matchesKey: (input: string, key: string) => boolean;
33
- truncateToWidth: (text: string, width: number) => string;
34
- visibleWidth: (text: string) => number;
35
- wrapTextWithAnsi: (text: string, width: number) => string[];
36
- };
37
- return cachedPiTui;
38
- }
1
+ import { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
39
2
 
40
3
  export interface ScrollSelectOption<T> {
41
4
  value: T;
@@ -132,7 +95,6 @@ class ScrollSelectComponent<T> {
132
95
  }
133
96
 
134
97
  render(width: number): string[] {
135
- const { truncateToWidth, visibleWidth, wrapTextWithAnsi } = getPiTui();
136
98
  const safeWidth = Math.max(20, width);
137
99
  const contentWidth = Math.max(12, safeWidth - 2);
138
100
  const lines: string[] = [];
@@ -193,8 +155,6 @@ class ScrollSelectComponent<T> {
193
155
  }
194
156
 
195
157
  handleInput(data: string): void {
196
- const { Key, matchesKey } = getPiTui();
197
-
198
158
  if (matchesKey(data, Key.escape) || data === "q") {
199
159
  this.done(null);
200
160
  return;
@@ -225,7 +185,6 @@ class ScrollSelectComponent<T> {
225
185
  dispose(): void {}
226
186
 
227
187
  private renderSurfaceLine(line: string, contentWidth: number, options: { selected?: boolean } = {}): string {
228
- const { visibleWidth } = getPiTui();
229
188
  const paddedLine = `${line}${" ".repeat(Math.max(0, contentWidth - visibleWidth(line)))}`;
230
189
  const boxedLine = ` ${paddedLine} `;
231
190
  if (typeof this.theme.bg !== "function") {
@@ -2,7 +2,8 @@ import type { QnAResponse, QnAResult } from "./qna";
2
2
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
3
3
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
 
5
- import { QnATuiComponent, requirePiTuiModule } from "./qna";
5
+ import { Text } from "@earendil-works/pi-tui";
6
+ import { QnATuiComponent } from "./qna";
6
7
 
7
8
  import type {
8
9
  NormalizedRequestUserInputQuestion,
@@ -16,9 +17,6 @@ import type {
16
17
  import { findDuplicateId } from "./utils";
17
18
 
18
19
  function createText(text: string) {
19
- const { Text } = requirePiTuiModule() as {
20
- Text: new (text: string, x: number, y: number) => unknown;
21
- };
22
20
  return new Text(text, 0, 0);
23
21
  }
24
22
 
package/state.ts CHANGED
@@ -1,19 +1,12 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
 
3
- import { requirePiTuiModule } from "./qna";
3
+ import { truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
4
4
 
5
5
  import type { PlanModeState } from "./types";
6
6
 
7
7
  import { resolveActivePlanFilePath } from "./plan-files";
8
8
  import { createInactivePlanModeState, isPlanModeState } from "./utils";
9
9
 
10
- function getPiTui() {
11
- return requirePiTuiModule() as {
12
- truncateToWidth: (text: string, width: number) => string;
13
- wrapTextWithAnsi: (text: string, width: number) => string[];
14
- };
15
- }
16
-
17
10
  export const STATE_ENTRY_TYPE = "pi-plan:state";
18
11
  export const CONTEXT_ENTRY_TYPE = "pi-plan:context";
19
12
  const BANNER_WIDGET_KEY = "pi-plan-banner";
@@ -104,7 +97,6 @@ export function createPlanModeStateManager(pi: ExtensionAPI) {
104
97
  (_tui, theme) => ({
105
98
  invalidate: () => {},
106
99
  render: (width: number) => {
107
- const { truncateToWidth, wrapTextWithAnsi } = getPiTui();
108
100
  const safeWidth = Math.max(1, width);
109
101
  const activePlanFilePath = resolveActivePlanFilePath(ctx, state.planFilePath);
110
102
  const lines = [
@@ -1,92 +0,0 @@
1
- /**
2
- <!-- {=sharedQnaPiTuiLoaderOverview} -->
3
-
4
- `local qna helpers` centralizes `@earendil-works/pi-tui` loading so first-party packages reuse one
5
- fallback strategy instead of embedding Bun-global lookup logic in multiple runtime modules.
6
-
7
- The shared loader returns pi's extension-provided `@earendil-works/pi-tui` module by default.
8
- Custom require functions can still exercise the normal package resolution path and Bun global
9
- fallback locations for tests or standalone callers running outside pi's extension loader.
10
-
11
- <!-- {/sharedQnaPiTuiLoaderOverview} -->
12
- */
13
- import * as piTuiModule from "@earendil-works/pi-tui";
14
- import { createRequire } from "node:module";
15
- import os from "node:os";
16
- import path from "node:path";
17
-
18
- export type PiTuiRequire = (specifier: string) => unknown;
19
-
20
- export interface PiTuiLoaderOptions {
21
- homeDir?: string;
22
- bunInstallDir?: string | undefined;
23
- requireFn?: PiTuiRequire;
24
- }
25
-
26
- /**
27
- <!-- {=sharedQnaGetPiTuiFallbackPathsDocs} -->
28
-
29
- Return the ordered list of Bun global fallback paths to try for `@earendil-works/pi-tui`.
30
-
31
- The list prefers an explicit `BUN_INSTALL` root when provided and always includes the default
32
- `~/.bun/install/global/node_modules/@earendil-works/pi-tui` fallback without duplicates.
33
-
34
- <!-- {/sharedQnaGetPiTuiFallbackPathsDocs} -->
35
- */
36
- export function getPiTuiFallbackPaths(options: Omit<PiTuiLoaderOptions, "requireFn"> = {}): string[] {
37
- const homeDir = options.homeDir ?? os.homedir();
38
- const roots = new Set<string>();
39
- if (options.bunInstallDir) {
40
- roots.add(options.bunInstallDir);
41
- }
42
- roots.add(path.join(homeDir, ".bun"));
43
- return [...roots].map((root) => path.join(root, "install", "global", "node_modules", "@earendil-works", "pi-tui"));
44
- }
45
-
46
- /**
47
- <!-- {=sharedQnaRequirePiTuiModuleDocs} -->
48
-
49
- Load `@earendil-works/pi-tui` with a shared fallback strategy.
50
-
51
- Pi exposes `@earendil-works/pi-tui` as an available extension import. Returning the static import
52
- keeps us on pi's module resolver; using `createRequire(import.meta.url)` in that path bypasses pi's
53
- resolver and can crash render callbacks when the peer dependency is not locally installed.
54
-
55
- When a custom `requireFn` or fallback path options are provided, the loader keeps the older
56
- standalone behavior: try normal package resolution, then walk Bun-global fallback locations, and
57
- finally throw a helpful error that names every checked location when none resolve.
58
-
59
- <!-- {/sharedQnaRequirePiTuiModuleDocs} -->
60
- */
61
- export function requirePiTuiModule(options: PiTuiLoaderOptions = {}): unknown {
62
- if (!options.requireFn && !options.homeDir && !options.bunInstallDir) {
63
- return piTuiModule;
64
- }
65
-
66
- const requireFn = options.requireFn ?? createRequire(import.meta.url);
67
- try {
68
- return requireFn("@earendil-works/pi-tui");
69
- } catch (error) {
70
- const code = (error as { code?: string }).code;
71
- if (code !== "MODULE_NOT_FOUND") {
72
- throw error;
73
- }
74
-
75
- const fallbackPaths = getPiTuiFallbackPaths(options);
76
- for (const fallbackPath of fallbackPaths) {
77
- try {
78
- return requireFn(fallbackPath);
79
- } catch (fallbackError) {
80
- const fallbackCode = (fallbackError as { code?: string }).code;
81
- if (fallbackCode !== "MODULE_NOT_FOUND") {
82
- throw fallbackError;
83
- }
84
- }
85
- }
86
-
87
- throw new Error(
88
- `Unable to load @earendil-works/pi-tui. Checked the local dependency and Bun global fallbacks: ${fallbackPaths.join(", ")}`,
89
- { cause: error },
90
- );
91
- }
92
- }