pi-hypercharm-provider 1.3.28 → 1.3.29
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 +7 -5
- package/index.ts +21 -12
- package/package.json +1 -1
- package/tests/provider.integration.test.ts +72 -3
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
|
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
|
|
@@ -827,10 +830,13 @@ function renderStatus(ctx: ExtensionContext): void {
|
|
|
827
830
|
widgetGlyphClampNotified = true;
|
|
828
831
|
ctx.ui.notify("HyperCharm: widget glyphs stay ASCII on this terminal — unicode glyphs overflow legacy mintty/Cygwin cell widths. Statusbar is unaffected.", "info");
|
|
829
832
|
}
|
|
830
|
-
// Show
|
|
831
|
-
//
|
|
832
|
-
//
|
|
833
|
-
|
|
833
|
+
// Show while HyperCharm is the selected provider — the account side renders
|
|
834
|
+
// as soon as the session_start/model_select credits fetch lands, with no
|
|
835
|
+
// need to wait for a turn — and once this session recorded HyperCharm
|
|
836
|
+
// activity, which is what keeps the line alive after a switch when
|
|
837
|
+
// hideOnOtherProvider is false. The default true clears it on the switch.
|
|
838
|
+
const visible = hasActivity || provider === PROVIDER_ID;
|
|
839
|
+
const accountVisible = statusConfig.account !== "off" && accountHasData(account) && visible;
|
|
834
840
|
const lowBalance =
|
|
835
841
|
statusConfig.lowBalanceHc !== null && account.balance !== null && account.balance <= statusConfig.lowBalanceHc;
|
|
836
842
|
const sessionLine = statusConfig.session !== "off" ? buildSessionLine(sessionStats, glyphs) : undefined;
|
|
@@ -1151,7 +1157,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1151
1157
|
|
|
1152
1158
|
loadStatusConfig();
|
|
1153
1159
|
resetStatusState();
|
|
1154
|
-
updateStatus(ctx); // clears any carryover;
|
|
1160
|
+
updateStatus(ctx); // clears any carryover; the account side lands with the credits fetch
|
|
1155
1161
|
// Re-register so our identity (custom api + streamSimple) always wins
|
|
1156
1162
|
// over anything that touched provider registration during load.
|
|
1157
1163
|
pi.registerProvider(PROVIDER_ID, makeProviderConfig());
|
|
@@ -1187,8 +1193,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1187
1193
|
updateStatus(ctx);
|
|
1188
1194
|
const model: any = (event as any).model;
|
|
1189
1195
|
if (model?.provider === PROVIDER_ID && cachedApiKey) {
|
|
1196
|
+
// Both refreshes repaint when they land: selection alone must fill in
|
|
1197
|
+
// the account side (balance now, team/auth atoms a moment later)
|
|
1198
|
+
// instead of leaving a bare gem until the next turn.
|
|
1190
1199
|
updateStatusAfter(refreshCredits(cachedApiKey, statusAbort?.signal ?? undefined, false), ctx);
|
|
1191
|
-
|
|
1200
|
+
updateStatusAfter(refreshAccountMeta(cachedApiKey, statusAbort?.signal ?? undefined), ctx);
|
|
1192
1201
|
}
|
|
1193
1202
|
});
|
|
1194
1203
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hypercharm-provider",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.29",
|
|
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",
|
|
@@ -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
|
+
});
|