@pi-unipi/core 2.1.3 → 2.3.0

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/constants.ts CHANGED
@@ -39,6 +39,7 @@ export const MODULES = {
39
39
  UPDATER: "@pi-unipi/updater",
40
40
  INPUT_SHORTCUTS: "@pi-unipi/input-shortcuts",
41
41
  COCOINDEX: "@pi-unipi/cocoindex",
42
+ IMAGE: "@pi-unipi/image",
42
43
  } as const;
43
44
 
44
45
  /** Workflow command names */
@@ -192,6 +193,23 @@ export const ASK_USER_TOOLS = {
192
193
  ASK: "ask_user",
193
194
  } as const;
194
195
 
196
+ /** Image tool names */
197
+ export const IMAGE_TOOLS = {
198
+ GENERATE: "image_generate",
199
+ RECOGNIZE: "image_recognize",
200
+ } as const;
201
+
202
+ /** Image command names */
203
+ export const IMAGE_COMMANDS = {
204
+ SETTINGS: "image-settings",
205
+ } as const;
206
+
207
+ /** Image directory paths */
208
+ export const IMAGE_DIRS = {
209
+ CONFIG: "~/.unipi/config/image",
210
+ OUTPUT: "~/.unipi/images",
211
+ } as const;
212
+
195
213
  /** MCP directory paths */
196
214
  export const MCP_DIRS = {
197
215
  GLOBAL_CONFIG: "~/.unipi/config/mcp",
package/index.ts CHANGED
@@ -9,3 +9,4 @@ export * from "./events.js";
9
9
  export * from "./sandbox.js";
10
10
  export * from "./utils.js";
11
11
  export * from "./model-cache.js";
12
+ export * from "./tui-width.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/core",
3
- "version": "2.1.3",
3
+ "version": "2.3.0",
4
4
  "description": "Shared utilities, event types, and constants for Unipi extension suite",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/tui-width.ts ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @pi-unipi/core — TUI width helpers
3
+ *
4
+ * pi-tui's differential renderer throws when a rendered line is wider than the
5
+ * terminal (see `TUI.render` in @earendil-works/pi-tui — it writes
6
+ * `~/.pi/agent/pi-crash.log`, stops the TUI and rethrows). That makes any
7
+ * "minimum width" floor a crash waiting to happen on a narrow terminal:
8
+ *
9
+ * const innerWidth = Math.max(40, width - 2); // ← 42-col lines at width=20
10
+ *
11
+ * The invariant every component must hold is:
12
+ *
13
+ * for every returned line: visibleWidth(line) <= width
14
+ *
15
+ * These helpers make that invariant easy to satisfy. They are pure arithmetic
16
+ * so this module stays free of a pi-tui dependency.
17
+ */
18
+
19
+ /**
20
+ * Terminals narrower than this cannot usefully show a bordered box: two
21
+ * columns go to the border, leaving too little for content. Below the
22
+ * threshold, callers should render borderless (see {@link shouldRenderBorder}).
23
+ */
24
+ export const MIN_BORDERED_WIDTH = 12;
25
+
26
+ /** Smallest width any layout is asked to cope with. */
27
+ export const MIN_RENDER_WIDTH = 1;
28
+
29
+ /**
30
+ * Normalize an incoming render width. Guards against `0`, negative, `NaN`
31
+ * and fractional widths, all of which have been observed during terminal
32
+ * resize races.
33
+ */
34
+ export function normalizeWidth(width: number): number {
35
+ if (!Number.isFinite(width)) return MIN_RENDER_WIDTH;
36
+ return Math.max(MIN_RENDER_WIDTH, Math.floor(width));
37
+ }
38
+
39
+ /**
40
+ * Whether a bordered box fits at this width. When false, render the content
41
+ * without `│` side borders so the full width is usable.
42
+ */
43
+ export function shouldRenderBorder(width: number): boolean {
44
+ return normalizeWidth(width) >= MIN_BORDERED_WIDTH;
45
+ }
46
+
47
+ /**
48
+ * Content width inside a bordered box, i.e. the terminal width minus the two
49
+ * border columns, so that `│ + content + │` is `<= width`.
50
+ *
51
+ * Use this for components that always draw a border. Components that can drop
52
+ * the border on narrow terminals should branch on {@link shouldRenderBorder}
53
+ * and use {@link adaptiveInnerWidth} instead.
54
+ */
55
+ export function boxInnerWidth(width: number): number {
56
+ return Math.max(1, normalizeWidth(width) - 2);
57
+ }
58
+
59
+ /**
60
+ * Content width for components that drop their border on narrow terminals:
61
+ * the box inner width when a border fits, otherwise the full width.
62
+ *
63
+ * Pair with {@link shouldRenderBorder} to decide whether to emit the border
64
+ * characters. Together they guarantee every emitted line is `<= width` at any
65
+ * width down to 1.
66
+ */
67
+ export function adaptiveInnerWidth(width: number): number {
68
+ const w = normalizeWidth(width);
69
+ return shouldRenderBorder(w) ? boxInnerWidth(w) : w;
70
+ }
71
+
72
+ /**
73
+ * Width remaining after reserving `reserved` columns for a prefix, indent or
74
+ * gutter. Never returns less than 1, so it is safe to pass to wrapping and
75
+ * truncation helpers (which throw or misbehave on non-positive widths).
76
+ */
77
+ export function contentWidth(available: number, reserved: number): number {
78
+ return Math.max(1, normalizeWidth(available) - Math.max(0, Math.floor(reserved)));
79
+ }
80
+
81
+ /**
82
+ * Clamp a repeat count to a non-negative integer.
83
+ *
84
+ * `String.prototype.repeat` throws `RangeError: Invalid count value` for
85
+ * negative counts, which crashes the render pass.
86
+ */
87
+ export function safeRepeatCount(count: number): number {
88
+ if (!Number.isFinite(count)) return 0;
89
+ return Math.max(0, Math.floor(count));
90
+ }
91
+
92
+ /** `" ".repeat(n)` that cannot throw. */
93
+ export function safeRepeat(char: string, count: number): string {
94
+ return char.repeat(safeRepeatCount(count));
95
+ }
96
+
97
+ /**
98
+ * Width-keyed render cache.
99
+ *
100
+ * Components that cache their rendered lines must invalidate on width change.
101
+ * pi-tui's `requestRender()` does *not* call `invalidate()`, so a component
102
+ * that caches `string[]` without keying on width will return stale, over-wide
103
+ * lines after the terminal is made narrower — and the next differential frame
104
+ * throws.
105
+ */
106
+ export class WidthKeyedCache {
107
+ private lines: string[] | null = null;
108
+ private width = -1;
109
+
110
+ /** Cached lines for this width, or `null` on miss. */
111
+ get(width: number): string[] | null {
112
+ return this.lines !== null && this.width === normalizeWidth(width) ? this.lines : null;
113
+ }
114
+
115
+ /** Store lines for this width. Returns the lines for convenient chaining. */
116
+ set(width: number, lines: string[]): string[] {
117
+ this.lines = lines;
118
+ this.width = normalizeWidth(width);
119
+ return lines;
120
+ }
121
+
122
+ /** Drop the cache — call from `invalidate()` and on any state change. */
123
+ clear(): void {
124
+ this.lines = null;
125
+ this.width = -1;
126
+ }
127
+ }
package/utils.ts CHANGED
@@ -187,6 +187,43 @@ export function getInstalledPackageVersion(startDir: string, packageName: string
187
187
  return getPackageVersion(root);
188
188
  }
189
189
 
190
+ /** Cached pi version — resolved at most once per process. */
191
+ let cachedPiVersion: string | null = null;
192
+
193
+ /**
194
+ * Get the running Pi agent's version.
195
+ *
196
+ * Resolves by walking up from Pi's own entry point (`process.argv[1]`), which
197
+ * must be `realpath`'d first: the executable on PATH is typically a symlink
198
+ * (e.g. mise shims `~/.local/share/mise/installs/node/lts/bin/pi`), and the
199
+ * package.json lives next to the *real* `dist/cli.js`, not the link.
200
+ *
201
+ * Never spawns a subprocess. A previous implementation fell back to
202
+ * `execSync("pi --version")`, which cost ~350ms per call and still returned
203
+ * "unknown" because it matched against a `v` prefix that Pi no longer emits.
204
+ */
205
+ export function getPiVersion(): string {
206
+ if (cachedPiVersion !== null) return cachedPiVersion;
207
+
208
+ const PI_PACKAGE = "@earendil-works/pi-coding-agent";
209
+ const entry = process.argv[1];
210
+ if (entry) {
211
+ try {
212
+ const realEntry = fs.realpathSync(entry);
213
+ const root = findPackageRoot(path.dirname(realEntry), PI_PACKAGE);
214
+ if (root) {
215
+ cachedPiVersion = getPackageVersion(root);
216
+ return cachedPiVersion;
217
+ }
218
+ } catch {
219
+ // Fall through to "unknown".
220
+ }
221
+ }
222
+
223
+ cachedPiVersion = "unknown";
224
+ return cachedPiVersion;
225
+ }
226
+
190
227
  /**
191
228
  * Check if a module is available in node_modules.
192
229
  */