@quandev104/pi-style 0.2.6 → 0.2.9

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.
@@ -567,9 +567,21 @@ function isSpacerChild(child: unknown): boolean {
567
567
  * rendered content is empty. Duck-typed on the public shape because
568
568
  * pi-coding-agent may resolve its own nested pi-tui copy, so `instanceof`
569
569
  * across that module boundary is unreliable.
570
+ *
571
+ * Pi 0.85.0 wraps every thinking-run component (the hidden label Text or the
572
+ * visible thinking Markdown) in a `MouseRegion` for click-to-toggle visibility.
573
+ * `MouseRegion` is render-transparent, so the placeholder check unwraps it first
574
+ * (duck-typed: a `handleMouse` function plus a `child`), keeping the same
575
+ * Text detection for the pre-0.85 bare-Text layout.
570
576
  */
577
+ function unwrapMouseRegion(child: unknown): unknown {
578
+ const candidate = child as { handleMouse?: unknown; child?: unknown } | undefined;
579
+ if (typeof candidate?.handleMouse !== "function" || candidate.child === undefined) return child;
580
+ return candidate.child;
581
+ }
582
+
571
583
  function isBlankTextChild(child: unknown): boolean {
572
- const candidate = child as
584
+ const candidate = unwrapMouseRegion(child) as
573
585
  | { setCustomBgFn?: unknown; render?: (width: number) => string[]; text?: unknown }
574
586
  | undefined;
575
587
  if (typeof candidate?.setCustomBgFn !== "function" || typeof candidate.render !== "function") return false;
@@ -605,7 +617,8 @@ function markChildrenScanned(instance: object, children: readonly unknown[]): vo
605
617
  *
606
618
  * Native `AssistantMessageComponent.updateContent` renders the thinking block as
607
619
  * `Text(theme.italic(theme.fg("thinkingText", label)), outputPad, 0)` plus a
608
- * trailing `Spacer(1)`. An empty label is still wrapped in ANSI SGR codes, so
620
+ * trailing `Spacer(1)` wrapped in a render-transparent `MouseRegion` since
621
+ * Pi 0.85.0. An empty label is still wrapped in ANSI SGR codes, so
609
622
  * `Text.render` cannot treat it as empty (its check is `text.trim() === ""`,
610
623
  * and trim does not strip escape sequences) and emits one full-width invisible
611
624
  * line. That invisible row plus the surrounding spacers is the "gap" left when
@@ -2,8 +2,9 @@ import type { Component, OverlayHandle, OverlayOptions } from "@earendil-works/p
2
2
  import type { NormalizedPiStyleConfig } from "../../domain/config-types.js";
3
3
  import type { StatusSnapshot } from "../../domain/status.js";
4
4
  import { type ActiveTheme, type ResolvedTheme, resolveTheme } from "../../domain/theme.js";
5
- import { fitAnsiWidth, truncateAnsi, visibleWidth } from "../../shared/ansi.js";
6
- import { compactLogoHeader } from "./logo.js";
5
+ import { fitAnsiWidth, fitAnsiWidthTail, truncateAnsi, visibleWidth } from "../../shared/ansi.js";
6
+ import { shortenPath } from "../../shared/box.js";
7
+ import { compactLogoHeader, logoDetailWidth } from "./logo.js";
7
8
 
8
9
  export type StartupReason = "startup" | "reload" | "new" | "resume" | "fork";
9
10
 
@@ -302,6 +303,16 @@ const STARTUP_PADDING_TOP = 2;
302
303
  /** Blank rows below the block, separating it from the editor / chat. */
303
304
  const STARTUP_PADDING_BOTTOM = 2;
304
305
 
306
+ /**
307
+ * Startup heading title: the working directory of the session (home-contracted
308
+ * to `~`) — i.e. the path of the repo currently being worked in. Falls back to
309
+ * the directory basename, then the brand, when the runtime reports no cwd.
310
+ */
311
+ function startupProjectTitle(snapshot: StartupSnapshot): string {
312
+ if (snapshot.cwd) return shortenPath(snapshot.cwd);
313
+ return snapshot.project ?? "pi-style";
314
+ }
315
+
305
316
  function styledLines(
306
317
  theme: ActiveTheme,
307
318
  config: NormalizedPiStyleConfig,
@@ -319,13 +330,22 @@ function styledLines(
319
330
  // Breathing room above the block (separates it from the status line / terminal top).
320
331
  lines.push(...Array.from({ length: STARTUP_PADDING_TOP }, () => ""));
321
332
 
322
- const logoTitle = resolved.mode === "ascii" ? "pi-style" : `${resolved.glyph("pi")} pi-style`;
333
+ // Heading: the π glyph followed by the current project path (never the
334
+ // package name — the heading identifies WHERE you are, not what styles it).
335
+ // The path is tail-fitted so the repo name survives narrow terminals.
336
+ const asciiMode = resolved.mode === "ascii";
337
+ const glyph = resolved.glyph("pi");
338
+ const projectTitle = startupProjectTitle(snapshot);
339
+ const titleBudget = Math.max(0, logoDetailWidth(bodyWidth) - (asciiMode ? 0 : visibleWidth(glyph) + 1));
340
+ const fittedTitle = fitAnsiWidthTail(projectTitle, titleBudget, asciiMode ? "..." : "…");
341
+ const logoTitle = asciiMode ? fittedTitle : `${glyph} ${fittedTitle}`;
323
342
  lines.push(
324
343
  ...compactLogoHeader(
325
344
  resolved,
326
345
  [
327
346
  resolved.apply("accent", logoTitle),
328
- resolved.apply("muted", "/ commands · ! bash"),
347
+ resolved.apply("muted", "/ commands"),
348
+ resolved.apply("muted", "! bash"),
329
349
  resolved.apply("success", "● ready"),
330
350
  ],
331
351
  bodyWidth,
@@ -9,15 +9,14 @@ import { fitAnsiWidth, parseAnsiFgToRgb, visibleWidth } from "../../shared/ansi.
9
9
  */
10
10
 
11
11
  export const PI_LOGO_LINES = [
12
- "████████████╗",
13
- "████████████║",
14
- "████╔═══████║",
15
- "████║ ████║",
16
- "████████╬═══████╗",
17
- "████████║ ████║ ",
18
- "████╔═══╝ ████║",
19
- "████║ ████║",
20
- "╚═══╝ ╚═══╝",
12
+ "████████████",
13
+ "████████████",
14
+ "████ ████",
15
+ "████ ████",
16
+ "████████ ████",
17
+ "████████ ████",
18
+ "████ ████",
19
+ "████ ████",
21
20
  ] as const;
22
21
 
23
22
  const LOGO_PALETTE_STEPS = 24;
@@ -117,20 +116,25 @@ export function styledLogoLines(resolved: ResolvedTheme): string[] {
117
116
  return logoGradientCacheLines;
118
117
  }
119
118
 
119
+ /** Width of the side-detail column next to the block-art logo at a given content width. */
120
+ export function logoDetailWidth(width: number): number {
121
+ const logoWidth = Math.max(...PI_LOGO_LINES.map((line) => visibleWidth(line)));
122
+ return Math.max(0, width - logoWidth - visibleWidth(LOGO_GAP));
123
+ }
124
+
120
125
  /**
121
126
  * Assemble the compact startup header: gradient logo with side details when
122
127
  * wide enough, stacked logo + details next, and a minimal title/status pair
123
- * for very narrow terminals. Every returned line fits within `width`.
128
+ * for very narrow terminals. `details` is a column of detail rows — the first
129
+ * is the title, the last is the status, and the rows between are hints — each
130
+ * rendered on its own line, vertically centered beside the logo. Every
131
+ * returned line fits within `width`.
124
132
  */
125
- export function compactLogoHeader(
126
- resolved: ResolvedTheme,
127
- details: readonly [title: string, hints: string, status: string],
128
- width: number,
129
- ): string[] {
133
+ export function compactLogoHeader(resolved: ResolvedTheme, details: readonly string[], width: number): string[] {
130
134
  const logoLines = styledLogoLines(resolved);
131
- const logoWidth = Math.max(...PI_LOGO_LINES.map((line) => visibleWidth(line)));
132
135
  const safeWidth = Math.max(1, width);
133
- const detailWidth = safeWidth - logoWidth - visibleWidth(LOGO_GAP);
136
+ const logoWidth = Math.max(...PI_LOGO_LINES.map((line) => visibleWidth(line)));
137
+ const detailWidth = logoDetailWidth(safeWidth);
134
138
 
135
139
  if (detailWidth >= LOGO_SIDE_DETAIL_MIN_WIDTH) {
136
140
  const detailStartRow = Math.max(0, Math.floor((PI_LOGO_LINES.length - details.length) / 2));
@@ -147,5 +151,5 @@ export function compactLogoHeader(
147
151
  return [...logoLines, ...details.map((detail) => fitAnsiWidth(detail, safeWidth))];
148
152
  }
149
153
 
150
- return [details[0], details[2]].map((detail) => fitAnsiWidth(detail, safeWidth));
154
+ return [details[0] ?? "", details[details.length - 1] ?? ""].map((detail) => fitAnsiWidth(detail, safeWidth));
151
155
  }
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import * as piCodingAgentPackage from "@earendil-works/pi-coding-agent";
4
5
  import {
5
6
  AssistantMessageComponent,
6
7
  BashExecutionComponent,
@@ -34,8 +35,16 @@ import {
34
35
  * changes the identity degrades that single surface to its native fallback while
35
36
  * every other surface continues.
36
37
  */
37
- export const SUPPORTED_VERSION_RANGE = ">=0.83.0 <0.85.0";
38
- export const SUPPORTED_PI_VERSIONS: readonly string[] = Object.freeze(["0.83.0", "0.84.0"]);
38
+ export const SUPPORTED_VERSION_RANGE = ">=0.83.0 <0.86.0";
39
+ export const SUPPORTED_PI_VERSIONS: readonly string[] = Object.freeze([
40
+ "0.83.0",
41
+ "0.84.0",
42
+ "0.84.1",
43
+ "0.84.2",
44
+ "0.84.3",
45
+ "0.84.4",
46
+ "0.85.0",
47
+ ]);
39
48
 
40
49
  /** A recorded native identity for one certified surface. */
41
50
  export interface KnownNativeIdentity {
@@ -53,6 +62,28 @@ export interface KnownNativeIdentity {
53
62
  * A matching name/arity/hash is evidence of the shipped native method, not a
54
63
  * cryptographic proof: code loaded before this module could spoof the same
55
64
  * function source. Unrecorded identities therefore fall back natively per surface.
65
+ *
66
+ * Identities are recorded per ARTIFACT FAMILY, not just per version: through Pi
67
+ * 0.84.2 the Node CLI ran the modular `dist/` build and extensions received the
68
+ * pretty-printed dist classes; 0.84.3 switches the CLI entrypoint to a minified
69
+ * bundled runtime (`dist/bundle/chunks/*`, see Pi 0.84.3 CHANGELOG "load a bundled
70
+ * runtime") whose jiti virtual-module map hands extensions the in-bundle class
71
+ * objects. Minification rewrites every method's `Function.prototype.toString()`
72
+ * text while behavior stays identical, so each surface carries BOTH the modular
73
+ * identity (0.83.0–0.84.2) and the bundled identity (0.84.3). The modular
74
+ * `dist/index.js` itself is unchanged in 0.84.3 — the switch is which class
75
+ * objects the running CLI actually serves to extensions.
76
+ *
77
+ * 0.84.4 rebuilds both artifact families byte-identically for every surface
78
+ * below, so it shares the 0.84.3 identities. 0.85.0 drifts exactly three
79
+ * surfaces (in both families) while preserving name/arity and the adapter
80
+ * contracts, so each carries a re-recorded 0.85.0 identity:
81
+ * - `AssistantMessageComponent.updateContent` adds per-run thinking visibility
82
+ * overrides (`thinkingVisibilityOverrides` + a `MouseRegion` click toggle per
83
+ * thinking run; the `isStreaming` default parameter and arity 1 are unchanged).
84
+ * - `ToolExecutionComponent.getCallRenderer`/`getResultRenderer` drop the
85
+ * `builtInToolDefinition` fallback branches and simply return
86
+ * `this.toolDefinition?.renderCall`/`renderResult` (still renderer-or-undefined).
56
87
  */
57
88
  export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNativeIdentity[]>> = Object.freeze({
58
89
  "native-assistant-message:render": Object.freeze([
@@ -60,7 +91,13 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
60
91
  name: "render",
61
92
  arity: 1,
62
93
  fingerprint: "2a39243f",
63
- versions: Object.freeze(["0.83.0", "0.84.0"]),
94
+ versions: Object.freeze(["0.83.0", "0.84.0", "0.84.1", "0.84.2", "0.84.4", "0.85.0"]),
95
+ }),
96
+ Object.freeze({
97
+ name: "render",
98
+ arity: 1,
99
+ fingerprint: "a9be09a3",
100
+ versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0"]),
64
101
  }),
65
102
  ]),
66
103
  "native-assistant-message:updateContent": Object.freeze([
@@ -77,7 +114,32 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
77
114
  name: "updateContent",
78
115
  arity: 1,
79
116
  fingerprint: "d2114491",
80
- versions: Object.freeze(["0.84.0"]),
117
+ versions: Object.freeze(["0.84.0", "0.84.1", "0.84.2", "0.84.4"]),
118
+ }),
119
+ // 0.84.3: the CLI loads a minified bundled runtime and extensions receive
120
+ // the in-bundle class objects, so this method's toString() is the minified
121
+ // text. See the registry docblock (artifact families) — the modular dist
122
+ // build of 0.84.3 still carries the fingerprint above.
123
+ Object.freeze({
124
+ name: "updateContent",
125
+ arity: 1,
126
+ fingerprint: "356b7e83",
127
+ versions: Object.freeze(["0.84.3", "0.84.4"]),
128
+ }),
129
+ // 0.85.0 modular: adds per-run thinking visibility overrides and a
130
+ // MouseRegion click toggle per thinking run (arity stays 1).
131
+ Object.freeze({
132
+ name: "updateContent",
133
+ arity: 1,
134
+ fingerprint: "80e338d2",
135
+ versions: Object.freeze(["0.85.0"]),
136
+ }),
137
+ // 0.85.0 bundled: same drift, minified.
138
+ Object.freeze({
139
+ name: "updateContent",
140
+ arity: 1,
141
+ fingerprint: "c3d72f2b",
142
+ versions: Object.freeze(["0.85.0"]),
81
143
  }),
82
144
  ]),
83
145
  "native-compaction-message:updateDisplay": Object.freeze([
@@ -85,7 +147,13 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
85
147
  name: "updateDisplay",
86
148
  arity: 0,
87
149
  fingerprint: "f8c44e78",
88
- versions: Object.freeze(["0.83.0", "0.84.0"]),
150
+ versions: Object.freeze(["0.83.0", "0.84.0", "0.84.1", "0.84.2", "0.84.4", "0.85.0"]),
151
+ }),
152
+ Object.freeze({
153
+ name: "updateDisplay",
154
+ arity: 0,
155
+ fingerprint: "5118a51d",
156
+ versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0"]),
89
157
  }),
90
158
  ]),
91
159
  "native-branch-message:updateDisplay": Object.freeze([
@@ -93,7 +161,13 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
93
161
  name: "updateDisplay",
94
162
  arity: 0,
95
163
  fingerprint: "415d57b7",
96
- versions: Object.freeze(["0.83.0", "0.84.0"]),
164
+ versions: Object.freeze(["0.83.0", "0.84.0", "0.84.1", "0.84.2", "0.84.4", "0.85.0"]),
165
+ }),
166
+ Object.freeze({
167
+ name: "updateDisplay",
168
+ arity: 0,
169
+ fingerprint: "2185274e",
170
+ versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0"]),
97
171
  }),
98
172
  ]),
99
173
  "native-skill-message:updateDisplay": Object.freeze([
@@ -101,7 +175,13 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
101
175
  name: "updateDisplay",
102
176
  arity: 0,
103
177
  fingerprint: "48099ea6",
104
- versions: Object.freeze(["0.83.0", "0.84.0"]),
178
+ versions: Object.freeze(["0.83.0", "0.84.0", "0.84.1", "0.84.2", "0.84.4", "0.85.0"]),
179
+ }),
180
+ Object.freeze({
181
+ name: "updateDisplay",
182
+ arity: 0,
183
+ fingerprint: "4051fd65",
184
+ versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0"]),
105
185
  }),
106
186
  ]),
107
187
  "native-custom-message:rebuild": Object.freeze([
@@ -109,7 +189,13 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
109
189
  name: "rebuild",
110
190
  arity: 0,
111
191
  fingerprint: "76ae2e3a",
112
- versions: Object.freeze(["0.83.0", "0.84.0"]),
192
+ versions: Object.freeze(["0.83.0", "0.84.0", "0.84.1", "0.84.2", "0.84.4", "0.85.0"]),
193
+ }),
194
+ Object.freeze({
195
+ name: "rebuild",
196
+ arity: 0,
197
+ fingerprint: "b89987cc",
198
+ versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0"]),
113
199
  }),
114
200
  ]),
115
201
  "tool-call-renderer:getCallRenderer": Object.freeze([
@@ -117,7 +203,28 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
117
203
  name: "getCallRenderer",
118
204
  arity: 0,
119
205
  fingerprint: "951ea0e0",
120
- versions: Object.freeze(["0.83.0", "0.84.0"]),
206
+ versions: Object.freeze(["0.83.0", "0.84.0", "0.84.1", "0.84.2", "0.84.4"]),
207
+ }),
208
+ Object.freeze({
209
+ name: "getCallRenderer",
210
+ arity: 0,
211
+ fingerprint: "e50613b7",
212
+ versions: Object.freeze(["0.84.3", "0.84.4"]),
213
+ }),
214
+ // 0.85.0 modular: drops the `builtInToolDefinition` fallback branches
215
+ // and returns `this.toolDefinition?.renderCall` directly.
216
+ Object.freeze({
217
+ name: "getCallRenderer",
218
+ arity: 0,
219
+ fingerprint: "e0a9ed86",
220
+ versions: Object.freeze(["0.85.0"]),
221
+ }),
222
+ // 0.85.0 bundled: same drift, minified.
223
+ Object.freeze({
224
+ name: "getCallRenderer",
225
+ arity: 0,
226
+ fingerprint: "73116365",
227
+ versions: Object.freeze(["0.85.0"]),
121
228
  }),
122
229
  ]),
123
230
  "tool-result-renderer:getResultRenderer": Object.freeze([
@@ -125,7 +232,28 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
125
232
  name: "getResultRenderer",
126
233
  arity: 0,
127
234
  fingerprint: "8a25cd71",
128
- versions: Object.freeze(["0.83.0", "0.84.0"]),
235
+ versions: Object.freeze(["0.83.0", "0.84.0", "0.84.1", "0.84.2", "0.84.4"]),
236
+ }),
237
+ Object.freeze({
238
+ name: "getResultRenderer",
239
+ arity: 0,
240
+ fingerprint: "28c4dc22",
241
+ versions: Object.freeze(["0.84.3", "0.84.4"]),
242
+ }),
243
+ // 0.85.0 modular: drops the `builtInToolDefinition` fallback branches
244
+ // and returns `this.toolDefinition?.renderResult` directly.
245
+ Object.freeze({
246
+ name: "getResultRenderer",
247
+ arity: 0,
248
+ fingerprint: "1567dcf4",
249
+ versions: Object.freeze(["0.85.0"]),
250
+ }),
251
+ // 0.85.0 bundled: same drift, minified.
252
+ Object.freeze({
253
+ name: "getResultRenderer",
254
+ arity: 0,
255
+ fingerprint: "d613a2a3",
256
+ versions: Object.freeze(["0.85.0"]),
129
257
  }),
130
258
  ]),
131
259
  "native-bash-execution:render": Object.freeze([
@@ -137,7 +265,13 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
137
265
  name: "BashExecutionComponent",
138
266
  arity: 2,
139
267
  fingerprint: "a5b5abca",
140
- versions: Object.freeze(["0.83.0", "0.84.0"]),
268
+ versions: Object.freeze(["0.83.0", "0.84.0", "0.84.1", "0.84.2", "0.84.4", "0.85.0"]),
269
+ }),
270
+ Object.freeze({
271
+ name: "BashExecutionComponent",
272
+ arity: 2,
273
+ fingerprint: "98d22d96",
274
+ versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0"]),
141
275
  }),
142
276
  ]),
143
277
  });
@@ -267,8 +401,19 @@ function trustedNativeIdentity(spec: TargetSpec): unknown {
267
401
  export interface PiVersionResolution {
268
402
  resolvePackageEntry?: (packageName: string) => string;
269
403
  readFile?: (path: string) => string;
404
+ /** Test-only override for the runtime VERSION-export fallback source. */
405
+ runtimeVersion?: string;
270
406
  }
271
407
 
408
+ /**
409
+ * The running host build's own VERSION constant. Inside the packaged Pi
410
+ * process extensions are loaded through jiti, where `import.meta.resolve`
411
+ * cannot see the host package at all — this export is aliased to the exact
412
+ * host build and is the authoritative version source there. Read defensively
413
+ * through a namespace import so hosts without the export still load.
414
+ */
415
+ const RUNTIME_PI_VERSION = (piCodingAgentPackage as { readonly VERSION?: unknown }).VERSION;
416
+
272
417
  export function detectPiVersion(resolution: PiVersionResolution = {}): {
273
418
  version: string | undefined;
274
419
  diagnostic?: string;
@@ -285,6 +430,7 @@ export function detectPiVersion(resolution: PiVersionResolution = {}): {
285
430
  }
286
431
  });
287
432
  const readFile = resolution.readFile ?? ((path: string) => readFileSync(path, "utf8"));
433
+ let walkDiagnostic: string | undefined;
288
434
  try {
289
435
  const entry = resolvePackageEntry("@earendil-works/pi-coding-agent");
290
436
  let directory = dirname(entry);
@@ -301,13 +447,13 @@ export function detectPiVersion(resolution: PiVersionResolution = {}): {
301
447
  if (parent === directory) break;
302
448
  directory = parent;
303
449
  }
304
- return { version: undefined, diagnostic: "Pi package version was not found" };
450
+ walkDiagnostic = "Pi package version was not found";
305
451
  } catch (error) {
306
- return {
307
- version: undefined,
308
- diagnostic: `Pi package version detection failed: ${error instanceof Error ? error.message : String(error)}`,
309
- };
452
+ walkDiagnostic = `Pi package version detection failed: ${error instanceof Error ? error.message : String(error)}`;
310
453
  }
454
+ const runtimeVersion = resolution.runtimeVersion !== undefined ? resolution.runtimeVersion : RUNTIME_PI_VERSION;
455
+ if (typeof runtimeVersion === "string" && runtimeVersion.trim() !== "") return { version: runtimeVersion };
456
+ return { version: undefined, diagnostic: walkDiagnostic };
311
457
  }
312
458
 
313
459
  function descriptorSnapshot(descriptor: PropertyDescriptor | undefined): CompatibilityDescriptorSnapshot | undefined {
@@ -108,6 +108,51 @@ export function resetAnsi(value: string): string {
108
108
  export function fitAnsiWidth(value: string, width: number, ellipsis = "…"): string {
109
109
  return visibleWidth(value) <= width ? value : truncateAnsi(value, width, ellipsis);
110
110
  }
111
+ /**
112
+ * Fit a string by dropping the HEAD (keeping the tail): for path-like values
113
+ * whose meaningful part is the end (the repo name). ANSI-aware — an escape run
114
+ * belongs to the visible character it precedes, so dropped characters drop
115
+ * their styling with them and the first kept character keeps its own escape.
116
+ */
117
+ export function fitAnsiWidthTail(value: string, width: number, ellipsis = "…"): string {
118
+ if (width <= 0) return "";
119
+ if (visibleWidth(value) <= width) return resetAnsi(value);
120
+ const ellipsisWidth = visibleWidth(ellipsis);
121
+ if (width <= ellipsisWidth) return resetAnsi(ellipsis);
122
+ type Unit = { ansi: string; char: string; charWidth: number };
123
+ const units: Unit[] = [];
124
+ let pending = "";
125
+ for (let i = 0; i < value.length; i++) {
126
+ const code = value.charCodeAt(i);
127
+ if (code === 27) {
128
+ const start = i;
129
+ i++;
130
+ while (i + 1 < value.length && !isFinal(value[i + 1] ?? "")) i++;
131
+ i++;
132
+ pending += value.slice(start, i + 1);
133
+ continue;
134
+ }
135
+ let char = value[i] ?? "";
136
+ // Keep surrogate pairs (emoji etc.) as one unit.
137
+ if (code >= 0xd800 && code <= 0xdbff && i + 1 < value.length) char += value[++i];
138
+ units.push({ ansi: pending, char, charWidth: visibleWidth(char) || 1 });
139
+ pending = "";
140
+ }
141
+ let kept = 0;
142
+ let used = ellipsisWidth;
143
+ while (kept < units.length) {
144
+ const unit = units[units.length - 1 - kept];
145
+ if (!unit || used + unit.charWidth > width) break;
146
+ used += unit.charWidth;
147
+ kept++;
148
+ }
149
+ let output = ellipsis;
150
+ for (let index = units.length - kept; index < units.length; index++) {
151
+ const unit = units[index];
152
+ if (unit) output += unit.ansi + unit.char;
153
+ }
154
+ return resetAnsi(output);
155
+ }
111
156
  export function truncateAnsi(value: string, width: number, ellipsis = "…"): string {
112
157
  if (width <= 0) return "";
113
158
  if (visibleWidth(value) <= width) return resetAnsi(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quandev104/pi-style",
3
- "version": "0.2.6",
3
+ "version": "0.2.9",
4
4
  "description": "A native-layout, cohesive visual style package for Pi.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,18 +42,19 @@
42
42
  "check": "npm run typecheck && npm run lint && npm run depcruise && npm run test && npm run build && npm run package:smoke"
43
43
  },
44
44
  "peerDependencies": {
45
- "@earendil-works/pi-agent-core": ">=0.83.0 <0.85.0",
46
- "@earendil-works/pi-ai": ">=0.83.0 <0.85.0",
47
- "@earendil-works/pi-coding-agent": ">=0.83.0 <0.85.0",
48
- "@earendil-works/pi-tui": ">=0.83.0 <0.85.0",
45
+ "@earendil-works/pi-agent-core": ">=0.83.0 <0.86.0",
46
+ "@earendil-works/pi-ai": ">=0.83.0 <0.86.0",
47
+ "@earendil-works/pi-coding-agent": ">=0.83.0 <0.86.0",
48
+ "@earendil-works/pi-tui": ">=0.83.0 <0.86.0",
49
49
  "typebox": ">=1.3.9 <2.0.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@biomejs/biome": "^2.5.0",
53
- "@earendil-works/pi-agent-core": "^0.84.0",
54
- "@earendil-works/pi-ai": "^0.84.0",
55
- "@earendil-works/pi-coding-agent": "^0.84.0",
56
- "@earendil-works/pi-tui": "^0.84.0",
53
+ "@earendil-works/pi-agent-core": "^0.85.0",
54
+ "@earendil-works/pi-ai": "^0.85.0",
55
+ "@earendil-works/pi-coding-agent": "^0.85.0",
56
+ "@earendil-works/pi-server": "^0.85.0",
57
+ "@earendil-works/pi-tui": "^0.85.0",
57
58
  "@types/node": "^24.0.0",
58
59
  "dependency-cruiser": "^17.4.3",
59
60
  "git-cliff": "^2.13.1",