pi-hypercharm-provider 1.3.29 → 1.3.31

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/README.md CHANGED
@@ -186,12 +186,13 @@ Non-interactive toggles:
186
186
 
187
187
  `glyphs: "auto"` replaces the footer glyphs (bolt, gem, warning, auth arrow,
188
188
  separator) with ASCII equivalents on legacy terminals (mintty/Cygwin), whose
189
- cell-width tables disagree with the extension's width math. There a
190
- full-width widget line can wrap physically, which desynchronizes pi's
191
- renderer and leaves stale rows behind. The widget also never paints the
192
- terminal's last column, and clamps an explicit `unicode` choice to ASCII on
193
- legacy terminals; the statusbar is not edge-padded and always honors the
194
- exact choice.
189
+ cell-width tables disagree with the extension's width math. There a glyph the
190
+ terminal renders wider than the extension measures overflows the widget line,
191
+ which desynchronizes pi's renderer and leaves stale rows behind. The widget
192
+ also right-aligns the account side flush with the terminal's last column, the
193
+ way pi's built-in footer does, and clamps an explicit `unicode` choice to
194
+ ASCII on legacy terminals; the statusbar is not edge-padded and always honors
195
+ the exact choice.
195
196
 
196
197
 
197
198
  ## Authentication
package/index.ts CHANGED
@@ -77,10 +77,11 @@
77
77
  *
78
78
  * - glyphs "auto" swaps the emoji footer glyphs for ASCII on legacy
79
79
  * terminals (mintty/Cygwin), whose cell-width tables disagree with the
80
- * width math and wrap the full-width widget line. "unicode"/"ascii"
81
- * force a set. The widget never paints the terminal's last column, and
82
- * clamps an explicit "unicode" to ASCII on legacy terminals; the
83
- * statusbar is not edge-padded and honors the exact choice.
80
+ * width math and can overflow the widget line. "unicode"/"ascii"
81
+ * force a set. The widget right-aligns the account side flush with the
82
+ * terminal's last column (as pi's built-in footer does) and clamps an
83
+ * explicit "unicode" to ASCII on legacy terminals; the statusbar is
84
+ * not edge-padded and honors the exact choice.
84
85
  * /hypercharm-status refresh (re-fetch balance/team now)
85
86
  * /hypercharm-status reset
86
87
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hypercharm-provider",
3
- "version": "1.3.29",
3
+ "version": "1.3.31",
4
4
  "description": "HyperCharm provider extension for pi - Access DeepSeek, GLM, Kimi, Qwen, MiniMax, Gemma, and GPT-OSS models through the Charm Hyper API",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -33,9 +33,9 @@
33
33
  "@earendil-works/pi-tui": "*"
34
34
  },
35
35
  "devDependencies": {
36
- "@earendil-works/pi-ai": "0.85.1",
37
- "@earendil-works/pi-coding-agent": "0.85.1",
38
- "@earendil-works/pi-tui": "0.85.1",
36
+ "@earendil-works/pi-ai": "0.86.0",
37
+ "@earendil-works/pi-coding-agent": "0.86.0",
38
+ "@earendil-works/pi-tui": "0.86.0",
39
39
  "@types/node": "^24.10.1",
40
40
  "jiti": "2.7.0",
41
41
  "typescript": "6.0.3"
package/status.ts CHANGED
@@ -21,10 +21,13 @@
21
21
  * or when the glyphs config asks for it (see GlyphMode). Older mintty
22
22
  * builds measure these glyphs with cell tables that disagree with the
23
23
  * ones above; the statusbar absorbs that (its lines are not edge-padded)
24
- * but the full-width widget line wraps, which desyncs pi's row
25
- * bookkeeping and leaves ghost rows behind.
26
- * - render() budgets width − 1, so the widget never paints the terminal's
27
- * last column (the classic pending-wrap hazard).
24
+ * but a glyph the terminal renders wider than the math above measures it
25
+ * overflows the widget line, which desyncs pi's row bookkeeping and
26
+ * leaves ghost rows behind.
27
+ * - render() budgets the terminal's full width, so the right zone ends
28
+ * flush with the last column. Overlong lines are what pi rejects — its
29
+ * renderer throws when a line's visible width exceeds the terminal — so
30
+ * the width math above must never undercount a glyph.
28
31
  */
29
32
 
30
33
  export type DisplayMode = "widget" | "statusbar" | "off";
@@ -299,10 +302,16 @@ export function termVisWidth(str: string): number {
299
302
  export function truncateAnsi(str: string, maxCols: number, ellipsis = "…"): string {
300
303
  if (maxCols <= 0) return "";
301
304
  if (termVisWidth(str) <= maxCols) return str;
305
+ // Reserve the ellipsis's own width, not a fixed column: ASCII "..." is
306
+ // three cells, and a line that overshoots the terminal is what pi's
307
+ // renderer rejects — it throws instead of wrapping. An ellipsis too wide
308
+ // for the budget is itself cut to it.
309
+ const ellipsisWidth = termVisWidth(ellipsis);
310
+ if (ellipsisWidth >= maxCols) return truncateAnsi(ellipsis, maxCols, "");
302
311
  let result = "";
303
312
  let visWidth = 0;
304
313
  let i = 0;
305
- const target = maxCols - 1;
314
+ const target = maxCols - ellipsisWidth;
306
315
  while (i < str.length) {
307
316
  const code = str.charCodeAt(i);
308
317
  if (code === 0x1b && i + 1 < str.length && str.charCodeAt(i + 1) === 0x5b) {
@@ -360,10 +369,14 @@ export class StatusLineWidget {
360
369
  invalidate(): void {}
361
370
 
362
371
  render(width: number): string[] {
363
- // Never paint the terminal's last column: writing the final cell marks a
364
- // pending wrap on legacy terminals, and any real-vs-table width
365
- // disagreement then scrolls the frame and desyncs pi's row bookkeeping.
366
- const w = Math.max(1, width - 1);
372
+ // Budget the terminal's full width, so the right zone ends flush with the
373
+ // last column. pi's built-in footer right-aligns its model/thinking text
374
+ // the same way and both rows share one frame, so holding a column back
375
+ // reads as a gap under a flush right edge. pi clears each line before
376
+ // writing it and separates rows with CR/LF, so ending on the final column
377
+ // carries no wrap hazard — the lines its renderer rejects are the
378
+ // overlong ones.
379
+ const w = Math.max(1, width);
367
380
  const leftVis = termVisWidth(this.leftRaw);
368
381
  if (leftVis > w) {
369
382
  return [this.theme.fg("dim", truncateAnsi(this.leftRaw, w, this.glyphs.ellipsis))];
@@ -101,38 +101,40 @@ const left = buildSessionLine({ requests: 7, spendHc: 1.24 })!;
101
101
  assert.ok(left.startsWith("\u26A1 "), "unicode glyph set is the default");
102
102
  const widget = new StatusLineWidget(fakeTheme, left, tiers, false);
103
103
 
104
- // Wide: full tier, left-right justified at width − 1 (the widget never
105
- // paints the terminal's last column — legacy terminals treat that cell as a
106
- // pending wrap).
104
+ // Wide: full tier, left-right justified to the terminal's last column
105
+ // (pi's built-in footer right-aligns its model text the same way, so both
106
+ // rows end flush).
107
107
  const wide = widget.render(80);
108
108
  assert.equal(wide.length, 1);
109
- assert.equal(termVisWidth(wide[0]), 79);
109
+ assert.equal(termVisWidth(wide[0]), 80);
110
110
  assert.ok(stripAnsi(wide[0]).startsWith("⚡ 1.24 hc"));
111
111
  assert.ok(stripAnsi(wide[0]).endsWith("⟳ 29d"));
112
112
 
113
- // Medium: drops to a compressed tier, still exactly width − 1
113
+ // Medium: drops to a compressed tier, still exactly width
114
114
  const med = widget.render(52);
115
- assert.equal(termVisWidth(med[0]), 51);
115
+ assert.equal(termVisWidth(med[0]), 52);
116
116
  assert.ok(!stripAnsi(med[0]).includes("⟳"), "compressed tiers drop auth first");
117
117
 
118
118
  // Narrow: no tier fits → left only, padded
119
119
  const narrow = widget.render(termVisWidth(left) + 3);
120
- assert.equal(termVisWidth(narrow[0]), termVisWidth(left) + 2);
120
+ assert.equal(termVisWidth(narrow[0]), termVisWidth(left) + 3);
121
121
  assert.ok(stripAnsi(narrow[0]).startsWith("⚡"));
122
122
  assert.ok(!stripAnsi(narrow[0]).includes("◆"));
123
123
 
124
124
  // Narrower than left itself: truncation never overflows (crash guard)
125
125
  const tiny = widget.render(10);
126
- assert.equal(termVisWidth(tiny[0]), 9);
126
+ assert.equal(termVisWidth(tiny[0]), 10);
127
127
 
128
128
  // Left empty (session gated) → right-aligned account line
129
129
  const rightOnly = new StatusLineWidget(fakeTheme, "", tiers, false);
130
130
  const ro = rightOnly.render(70);
131
- assert.equal(termVisWidth(ro[0]), 69);
131
+ assert.equal(termVisWidth(ro[0]), 70, "account side hugs the last column");
132
+ assert.equal(stripAnsi(ro[0]).length - stripAnsi(ro[0]).trimStart().length, 70 - termVisWidth(tiers[0]),
133
+ "left zone stays empty: account text starts where the tier width demands");
132
134
  assert.ok(stripAnsi(ro[0]).endsWith("⟳ 29d"));
133
135
 
134
136
  // No data at all
135
- assert.deepEqual(new StatusLineWidget(fakeTheme, "", [], false).render(40), [fakeTheme.fg("dim", "") + " ".repeat(39)]);
137
+ assert.deepEqual(new StatusLineWidget(fakeTheme, "", [], false).render(40), [fakeTheme.fg("dim", "") + " ".repeat(40)]);
136
138
 
137
139
  // Warning color wired through
138
140
  const warn = new StatusLineWidget(fakeTheme, "", buildAccountTiers(acc({ balance: 10 }), true), true);
@@ -197,11 +199,25 @@ assert.ok(asciiTiers[0].endsWith("~ 29d"), `got ${asciiTiers[0]}`);
197
199
  const asciiWidget = new StatusLineWidget(fakeTheme, asciiLine, asciiTiers, true, ASCII_GLYPHS).render(60)[0];
198
200
  assert.equal([...stripAnsi(asciiWidget)].every((c) => c.charCodeAt(0) < 128), true);
199
201
 
202
+ // ASCII truncation must fit the width too: the three-cell "..." used to
203
+ // overshoot by two columns on a narrow terminal, and pi's renderer throws
204
+ // on an overlong line instead of wrapping it.
205
+ for (const width of [10, 14, 17]) {
206
+ const line = new StatusLineWidget(fakeTheme, asciiLine, asciiTiers, true, ASCII_GLYPHS).render(width)[0];
207
+ assert.equal(termVisWidth(line), width, `ascii truncation must fit width ${width}`);
208
+ assert.equal([...stripAnsi(line)].every((c) => c.charCodeAt(0) < 128), true, `ascii truncation stays ascii at ${width}`);
209
+ }
210
+
200
211
  // Unicode mode keeps the glyphs (regression guard for the default path)
201
212
  assert.ok(buildAccountTiers(acc({ balance: 10 }), true)[0].includes("\u26A0 \u25C6"));
202
213
 
203
214
  // Truncation takes the caller's ellipsis so ASCII mode stays ASCII
204
- assert.equal(truncateAnsi("abcdefghij", 5, "..."), "abcd...");
215
+ // The ellipsis is billed its own width: ASCII "..." (3 cells) leaves 2 for text
216
+ assert.equal(truncateAnsi("abcdefghij", 5, "..."), "ab...");
217
+ assert.equal(termVisWidth(truncateAnsi("abcdefghij", 5, "...")), 5);
218
+ // An ellipsis wider than the whole budget is itself cut to it
219
+ assert.equal(truncateAnsi("abcdefghij", 2, "..."), "..");
220
+ assert.equal(truncateAnsi("abcdefghij", 1, "..."), ".");
205
221
  assert.ok(truncateAnsi("abcdefghij", 5).endsWith("\u2026"));
206
222
 
207
223
  assert.equal(coerceStatusConfig({ glyphs: "bogus" }).glyphs, "auto");
package/tsconfig.json CHANGED
@@ -4,6 +4,7 @@
4
4
  "module": "ESNext",
5
5
  "moduleResolution": "bundler",
6
6
  "lib": ["ES2022", "DOM"],
7
+ "types": ["node"],
7
8
  "esModuleInterop": true,
8
9
  "strict": false,
9
10
  "skipLibCheck": true,