pi-provider-status 0.2.0 → 0.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/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
  Balance CNY 128.40
13
13
  ```
14
14
 
15
- The panel opens immediately; each section fills in as its request settles. Press `r` to refresh, `Esc`/`q` to close. In non-interactive mode the report is printed as a plain notification instead.
15
+ The panel opens immediately as a scrollable overlay; each section fills in as its request settles. Scroll with `↑`/`↓` (or `j`/`k`), `PgUp`/`PgDn` by page, `Ctrl+u`/`Ctrl+d` by half page, `Home`/`End` (or `g`/`G`) to jump to top/bottom; the mouse wheel also works over the panel in fullscreen mode. Press `r` to refresh, `Esc`/`q` to close. In non-interactive mode the report is printed as a plain notification instead.
16
16
 
17
17
  ## Install
18
18
 
@@ -48,10 +48,14 @@
48
48
  *
49
49
  * The panel opens immediately; each section fills in as its
50
50
  * request settles. Press r to refresh, Esc (or q) to close.
51
+ * The panel is a clamped, scrollable overlay in both TUI modes:
52
+ * ↑/↓/j/k line, PgUp/PgDn page, ctrl+u/d half page, Home/End
53
+ * (g/G) top/bottom, plus mouse wheel in fullscreen; it stays
54
+ * pinned to the bottom as sections settle.
51
55
  */
52
56
 
53
57
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
54
- import { Key, matchesKey, wrapTextWithAnsi } from "@earendil-works/pi-tui";
58
+ import { Key, matchesKey, wrapTextWithAnsi, type TUI } from "@earendil-works/pi-tui";
55
59
  import { execFileSync } from "node:child_process";
56
60
  import { existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
57
61
  import { homedir, platform } from "node:os";
@@ -1000,7 +1004,7 @@ export default function (pi: ExtensionAPI) {
1000
1004
  })
1001
1005
  .join("\n\n");
1002
1006
  const updated = lastUpdated ? `updated ${new Date(lastUpdated).toLocaleTimeString()}` : "";
1003
- const footer = st.dim(interactive ? `r refresh · Esc/q close · ${updated}` : updated);
1007
+ const footer = st.dim(interactive ? `↑↓/PgUp scroll · r refresh · Esc/q close · ${updated}` : updated);
1004
1008
  return `${body}\n\n${footer}`;
1005
1009
  };
1006
1010
 
@@ -1010,7 +1014,16 @@ export default function (pi: ExtensionAPI) {
1010
1014
  return;
1011
1015
  }
1012
1016
 
1013
- await ctx.ui.custom<void>((tui, theme, _kb, done) => {
1017
+ // The panel opens as a capturing overlay in both TUI modes: in
1018
+ // fullscreen the input-dock slot clips a tall plain component and
1019
+ // the alt screen routes arrows/PgUp/PgDn/wheel to the chat transcript,
1020
+ // and in regular mode an overlay taller than the terminal gets sliced
1021
+ // by the compositor (content beyond maxHeight is dropped, scrollback
1022
+ // never sees it). A focused overlay receives viewport keys in both
1023
+ // modes, so the panel clamps itself to the terminal height and scrolls
1024
+ // itself. (Wheel only reaches us in fullscreen; regular mode leaves
1025
+ // the mouse to the terminal.)
1026
+ const tuiFactory = async (tui: TUI, theme: any, _kb: unknown, done: () => void) => {
1014
1027
  const st: Styler = {
1015
1028
  bold: (s) => theme.bold(s),
1016
1029
  dim: (s) => theme.fg("dim", s),
@@ -1019,6 +1032,8 @@ export default function (pi: ExtensionAPI) {
1019
1032
  };
1020
1033
  let cachedWidth = 0;
1021
1034
  let cachedLines: string[] = [];
1035
+ let scrollTop = 0;
1036
+ let stickyBottom = true;
1022
1037
  const refresh = (): void => {
1023
1038
  cachedWidth = 0;
1024
1039
  tui.requestRender();
@@ -1026,25 +1041,79 @@ export default function (pi: ExtensionAPI) {
1026
1041
  onUpdate = refresh;
1027
1042
  // Show the panel immediately; each section fills in as its query settles.
1028
1043
  runAll();
1044
+
1045
+ // ── Scrolling (overlay is clamped to the terminal height) ──
1046
+ // SGR wheel sequences reach handleInput in fullscreen because a
1047
+ // focused overlay defers viewport input; 64 = wheel up, 65 = down.
1048
+ const wheelRe = /^\x1b\[<(6[45]);/;
1049
+ // Cap one line below the terminal so the 100% overlay never slices us.
1050
+ const viewportH = (): number => Math.max(4, tui.terminal.rows - 1);
1051
+ const scrollable = (): boolean => cachedLines.length > viewportH();
1052
+ const maxScrollTop = (): number => Math.max(0, cachedLines.length - Math.max(1, viewportH() - 2));
1053
+ const applyScroll = (top: number): void => {
1054
+ if (!scrollable()) return;
1055
+ const max = maxScrollTop();
1056
+ scrollTop = Math.max(0, Math.min(max, top));
1057
+ stickyBottom = scrollTop >= max;
1058
+ tui.requestRender();
1059
+ };
1060
+ const scrollBy = (delta: number): void => applyScroll((stickyBottom ? maxScrollTop() : scrollTop) + delta);
1061
+
1029
1062
  return {
1030
1063
  render(width: number): string[] {
1031
1064
  if (width !== cachedWidth) {
1032
1065
  cachedWidth = width;
1033
1066
  cachedLines = wrapTextWithAnsi(renderReport(st, true), width);
1034
1067
  }
1035
- return cachedLines;
1068
+ const viewH = viewportH();
1069
+ if (cachedLines.length <= viewH) return cachedLines;
1070
+ const bodyH = Math.max(1, viewH - 2);
1071
+ const max = maxScrollTop();
1072
+ scrollTop = stickyBottom ? max : Math.max(0, Math.min(scrollTop, max));
1073
+ stickyBottom = scrollTop >= max;
1074
+ const out: string[] = [
1075
+ st.dim(scrollTop > 0 ? ` ↑ ${scrollTop} line${scrollTop === 1 ? "" : "s"} above` : ""),
1076
+ ];
1077
+ out.push(...cachedLines.slice(scrollTop, scrollTop + bodyH));
1078
+ const below = cachedLines.length - scrollTop - bodyH;
1079
+ out.push(st.dim(below > 0 ? ` ↓ ${below} line${below === 1 ? "" : "s"} below` : ""));
1080
+ return out;
1036
1081
  },
1037
1082
  handleInput(data: string): void {
1038
- if (matchesKey(data, Key.escape) || data === "q") done();
1039
- else if (data === "r") {
1083
+ if (matchesKey(data, Key.escape) || data === "q") {
1084
+ done();
1085
+ return;
1086
+ }
1087
+ if (data === "r") {
1040
1088
  runAll();
1041
1089
  refresh();
1090
+ return;
1091
+ }
1092
+ const wheel = wheelRe.exec(data);
1093
+ if (wheel) {
1094
+ scrollBy(wheel[1] === "65" ? 3 : -3);
1095
+ return;
1042
1096
  }
1097
+ const page = Math.max(1, viewportH() - 2);
1098
+ const half = Math.max(1, Math.floor(viewportH() / 2));
1099
+ if (matchesKey(data, Key.up) || data === "k") scrollBy(-1);
1100
+ else if (matchesKey(data, Key.down) || data === "j") scrollBy(1);
1101
+ else if (matchesKey(data, Key.pageUp)) scrollBy(-page);
1102
+ else if (matchesKey(data, Key.pageDown)) scrollBy(page);
1103
+ else if (matchesKey(data, Key.ctrl("u"))) scrollBy(-half);
1104
+ else if (matchesKey(data, Key.ctrl("d"))) scrollBy(half);
1105
+ else if (matchesKey(data, Key.home) || data === "g") applyScroll(0);
1106
+ else if (matchesKey(data, Key.end) || data === "G") applyScroll(Number.MAX_SAFE_INTEGER);
1043
1107
  },
1044
1108
  invalidate(): void {
1045
1109
  cachedWidth = 0;
1046
1110
  },
1047
1111
  };
1112
+ };
1113
+
1114
+ await ctx.ui.custom<void>(tuiFactory, {
1115
+ overlay: true,
1116
+ overlayOptions: { width: "100%", maxHeight: "100%" },
1048
1117
  });
1049
1118
  },
1050
1119
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-status",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Show remaining quota and balance for LLM providers in pi — /status panel for Kimi Coding, Zhipu ZAI, MiniMax, DeepSeek, OpenRouter, Claude and Codex subscription windows",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -18,7 +18,9 @@
18
18
  "license": "MIT",
19
19
  "author": "cyzlmh",
20
20
  "pi": {
21
- "extensions": ["./extensions"]
21
+ "extensions": [
22
+ "./extensions"
23
+ ]
22
24
  },
23
25
  "repository": {
24
26
  "type": "git",