pi-hypercharm-provider 1.3.28 → 1.3.30
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 +14 -11
- package/index.ts +26 -16
- package/package.json +1 -1
- package/status.ts +22 -9
- package/tests/provider.integration.test.ts +72 -3
- package/tests/status.smoke.ts +27 -11
package/README.md
CHANGED
|
@@ -138,11 +138,13 @@ DeepSeek V4 models use the `deepseek` thinking format — the same native format
|
|
|
138
138
|
|
|
139
139
|
## Footer Status
|
|
140
140
|
|
|
141
|
-
A Neuralwatt-style status line sits below the editor. It appears
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
A Neuralwatt-style status line sits below the editor. It appears as soon as a
|
|
142
|
+
HyperCharm model is selected — the account side (team, balance, rate limits)
|
|
143
|
+
renders when the session-start or model-select credits fetch lands, and the
|
|
144
|
+
session side joins it after the first HyperCharm turn completes. Selecting
|
|
145
|
+
another provider hides it (`hideOnOtherProvider` defaults to `true`), the
|
|
146
|
+
balance refreshes when the agent run fully settles, and sessions that never
|
|
147
|
+
use HyperCharm make no status-related API calls:
|
|
146
148
|
|
|
147
149
|
```
|
|
148
150
|
⚡ 1.24 hc · 7 req Xu's Team ◆ 249 hc · 996/1k/h · 29d
|
|
@@ -184,12 +186,13 @@ Non-interactive toggles:
|
|
|
184
186
|
|
|
185
187
|
`glyphs: "auto"` replaces the footer glyphs (bolt, gem, warning, auth arrow,
|
|
186
188
|
separator) with ASCII equivalents on legacy terminals (mintty/Cygwin), whose
|
|
187
|
-
cell-width tables disagree with the extension's width math. There a
|
|
188
|
-
|
|
189
|
-
renderer and leaves stale rows behind. The widget
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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.
|
|
193
196
|
|
|
194
197
|
|
|
195
198
|
## Authentication
|
package/index.ts
CHANGED
|
@@ -33,12 +33,15 @@
|
|
|
33
33
|
* The right side compresses across progressive tiers as the terminal
|
|
34
34
|
* narrows. The balance flips to a ⚠ warning at/below lowBalanceHc.
|
|
35
35
|
*
|
|
36
|
-
* Lifecycle (mirrors pi-neuralwatt-provider):
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* the
|
|
41
|
-
*
|
|
36
|
+
* Lifecycle (mirrors pi-neuralwatt-provider): selecting a HyperCharm model
|
|
37
|
+
* shows the line — the account side renders as soon as the credits/team
|
|
38
|
+
* prefetch lands, and the session side (spend/requests) joins it on the
|
|
39
|
+
* first completed turn. hideOnOtherProvider (default true) clears
|
|
40
|
+
* everything the moment the active model belongs to another provider.
|
|
41
|
+
* Credits/team are prefetched on session start or model select when a
|
|
42
|
+
* HyperCharm model is active, so the first turn ends with data already
|
|
43
|
+
* cached. The balance is polled again on pi's agent_settled event (fires
|
|
44
|
+
* only once no automatic retry,
|
|
42
45
|
* compaction, or queued continuation can follow) — and nowhere else, so
|
|
43
46
|
* sessions without HyperCharm turns make zero status-related API calls.
|
|
44
47
|
* Between polls the balance moves optimistically: each turn's
|
|
@@ -74,10 +77,11 @@
|
|
|
74
77
|
*
|
|
75
78
|
* - glyphs "auto" swaps the emoji footer glyphs for ASCII on legacy
|
|
76
79
|
* terminals (mintty/Cygwin), whose cell-width tables disagree with the
|
|
77
|
-
* width math and
|
|
78
|
-
* force a set. The widget
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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.
|
|
81
85
|
* /hypercharm-status refresh (re-fetch balance/team now)
|
|
82
86
|
* /hypercharm-status reset
|
|
83
87
|
*
|
|
@@ -827,10 +831,13 @@ function renderStatus(ctx: ExtensionContext): void {
|
|
|
827
831
|
widgetGlyphClampNotified = true;
|
|
828
832
|
ctx.ui.notify("HyperCharm: widget glyphs stay ASCII on this terminal — unicode glyphs overflow legacy mintty/Cygwin cell widths. Statusbar is unaffected.", "info");
|
|
829
833
|
}
|
|
830
|
-
// Show
|
|
831
|
-
//
|
|
832
|
-
//
|
|
833
|
-
|
|
834
|
+
// Show while HyperCharm is the selected provider — the account side renders
|
|
835
|
+
// as soon as the session_start/model_select credits fetch lands, with no
|
|
836
|
+
// need to wait for a turn — and once this session recorded HyperCharm
|
|
837
|
+
// activity, which is what keeps the line alive after a switch when
|
|
838
|
+
// hideOnOtherProvider is false. The default true clears it on the switch.
|
|
839
|
+
const visible = hasActivity || provider === PROVIDER_ID;
|
|
840
|
+
const accountVisible = statusConfig.account !== "off" && accountHasData(account) && visible;
|
|
834
841
|
const lowBalance =
|
|
835
842
|
statusConfig.lowBalanceHc !== null && account.balance !== null && account.balance <= statusConfig.lowBalanceHc;
|
|
836
843
|
const sessionLine = statusConfig.session !== "off" ? buildSessionLine(sessionStats, glyphs) : undefined;
|
|
@@ -1151,7 +1158,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1151
1158
|
|
|
1152
1159
|
loadStatusConfig();
|
|
1153
1160
|
resetStatusState();
|
|
1154
|
-
updateStatus(ctx); // clears any carryover;
|
|
1161
|
+
updateStatus(ctx); // clears any carryover; the account side lands with the credits fetch
|
|
1155
1162
|
// Re-register so our identity (custom api + streamSimple) always wins
|
|
1156
1163
|
// over anything that touched provider registration during load.
|
|
1157
1164
|
pi.registerProvider(PROVIDER_ID, makeProviderConfig());
|
|
@@ -1187,8 +1194,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1187
1194
|
updateStatus(ctx);
|
|
1188
1195
|
const model: any = (event as any).model;
|
|
1189
1196
|
if (model?.provider === PROVIDER_ID && cachedApiKey) {
|
|
1197
|
+
// Both refreshes repaint when they land: selection alone must fill in
|
|
1198
|
+
// the account side (balance now, team/auth atoms a moment later)
|
|
1199
|
+
// instead of leaving a bare gem until the next turn.
|
|
1190
1200
|
updateStatusAfter(refreshCredits(cachedApiKey, statusAbort?.signal ?? undefined, false), ctx);
|
|
1191
|
-
|
|
1201
|
+
updateStatusAfter(refreshAccountMeta(cachedApiKey, statusAbort?.signal ?? undefined), ctx);
|
|
1192
1202
|
}
|
|
1193
1203
|
});
|
|
1194
1204
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hypercharm-provider",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.30",
|
|
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",
|
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
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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 -
|
|
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
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
|
|
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))];
|
|
@@ -80,6 +80,11 @@ function captureUI(runner) {
|
|
|
80
80
|
const notifications = [];
|
|
81
81
|
const statusKeys = [];
|
|
82
82
|
const widgetKeys = [];
|
|
83
|
+
// Install (component factory) vs clear (undefined), split so a test can
|
|
84
|
+
// assert both "the widget popped up" and "the widget went away".
|
|
85
|
+
const widgetInstalls = [];
|
|
86
|
+
const widgetClears = [];
|
|
87
|
+
const widgetRenders = [];
|
|
83
88
|
const base = runner.createContext().ui ?? {};
|
|
84
89
|
const ui = {
|
|
85
90
|
...base,
|
|
@@ -90,12 +95,24 @@ function captureUI(runner) {
|
|
|
90
95
|
setStatus: (key) => {
|
|
91
96
|
statusKeys.push(String(key));
|
|
92
97
|
},
|
|
93
|
-
setWidget: (key) => {
|
|
94
|
-
|
|
98
|
+
setWidget: (key, value) => {
|
|
99
|
+
const name = String(key);
|
|
100
|
+
widgetKeys.push(name);
|
|
101
|
+
if (value === undefined) {
|
|
102
|
+
widgetClears.push(name);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
widgetInstalls.push(name);
|
|
106
|
+
try {
|
|
107
|
+
const component = value(undefined, ui.theme);
|
|
108
|
+
widgetRenders.push(component?.render ? component.render(80).join("\n") : "");
|
|
109
|
+
} catch {
|
|
110
|
+
widgetRenders.push("");
|
|
111
|
+
}
|
|
95
112
|
},
|
|
96
113
|
};
|
|
97
114
|
runner.setUIContext(ui, "tui");
|
|
98
|
-
return { notifications, statusKeys, widgetKeys };
|
|
115
|
+
return { notifications, statusKeys, widgetKeys, widgetInstalls, widgetClears, widgetRenders };
|
|
99
116
|
}
|
|
100
117
|
|
|
101
118
|
async function load(options = {}) {
|
|
@@ -414,3 +431,55 @@ test("co-installs with the official identifier surface without interference", as
|
|
|
414
431
|
harness.session.dispose();
|
|
415
432
|
}
|
|
416
433
|
});
|
|
434
|
+
|
|
435
|
+
test("shows the footer widget on model selection and clears it when the provider is deselected", async () => {
|
|
436
|
+
const requests = [];
|
|
437
|
+
const json = (body) => new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } });
|
|
438
|
+
const accountApi = async (input) => {
|
|
439
|
+
const url = String(input);
|
|
440
|
+
requests.push(url);
|
|
441
|
+
if (url.endsWith("/credits")) return json({ balance: 249 });
|
|
442
|
+
if (url.endsWith("/teams")) return json({ items: [{ name: "Xu's Team" }] });
|
|
443
|
+
if (url.endsWith("/devices")) return json({ items: [] });
|
|
444
|
+
if (url.endsWith("/provider")) return json({ models: [FIXTURE_MODEL] });
|
|
445
|
+
throw new Error("unexpected request: " + url);
|
|
446
|
+
};
|
|
447
|
+
const harness = await load({ extensionPaths: [extensionPath, officialSurfacePath], fetchImpl: accountApi });
|
|
448
|
+
try {
|
|
449
|
+
// The model_select handler only refreshes credits once session_start has
|
|
450
|
+
// cached a key, so wait for the catalog refresh that follows resolution.
|
|
451
|
+
await waitFor("session-start catalog refresh", () =>
|
|
452
|
+
requests.some((url) => url.endsWith("/provider")) ? true : undefined,
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const ui = captureUI(harness.runner);
|
|
456
|
+
const ours = harness.runtime.getModel("hypercharm", "deepseek-v4-flash");
|
|
457
|
+
assert.ok(ours, "embedded catalog serves deepseek-v4-flash");
|
|
458
|
+
await harness.session.setModel(ours);
|
|
459
|
+
// No turn_end is ever emitted in this test: selection alone must install
|
|
460
|
+
// the widget, with the account side already filled in.
|
|
461
|
+
await waitFor("widget installed on selection", () => (ui.widgetInstalls.includes("hypercharm") ? true : undefined));
|
|
462
|
+
const filled = await waitFor("account side in the widget", () =>
|
|
463
|
+
ui.widgetRenders.find((line) => line.includes("Xu's Team")),
|
|
464
|
+
);
|
|
465
|
+
assert.match(filled, /249 hc/, "balance renders from the credits prefetch");
|
|
466
|
+
|
|
467
|
+
const installsWhileSelected = ui.widgetInstalls.length;
|
|
468
|
+
await harness.credentials.modify("hyper", async () => ({ type: "api_key", key: "fixture-official-key" }));
|
|
469
|
+
const official = harness.runtime.getModel("hyper", "glm-5.3");
|
|
470
|
+
assert.ok(official, "the official-surface fixture serves glm-5.3");
|
|
471
|
+
await harness.session.setModel(official);
|
|
472
|
+
await waitFor("widget cleared on deselect", () => (ui.widgetClears.includes("hypercharm") ? true : undefined));
|
|
473
|
+
|
|
474
|
+
// Deselected stays deselected: another provider's turn must not bring the
|
|
475
|
+
// line back (hideOnOtherProvider defaults to true).
|
|
476
|
+
await harness.runner.emit({ type: "turn_end", turnIndex: 0, message: assistantMessage(), toolResults: [] });
|
|
477
|
+
assert.equal(
|
|
478
|
+
ui.widgetInstalls.length,
|
|
479
|
+
installsWhileSelected,
|
|
480
|
+
"no reinstall while another provider's model is active",
|
|
481
|
+
);
|
|
482
|
+
} finally {
|
|
483
|
+
harness.session.dispose();
|
|
484
|
+
}
|
|
485
|
+
});
|
package/tests/status.smoke.ts
CHANGED
|
@@ -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
|
|
105
|
-
//
|
|
106
|
-
//
|
|
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]),
|
|
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
|
|
113
|
+
// Medium: drops to a compressed tier, still exactly width
|
|
114
114
|
const med = widget.render(52);
|
|
115
|
-
assert.equal(termVisWidth(med[0]),
|
|
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) +
|
|
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]),
|
|
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]),
|
|
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(
|
|
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
|
-
|
|
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");
|