@vanillagreen/pi-qol 1.0.0 → 1.0.2

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
@@ -7,6 +7,23 @@
7
7
 
8
8
  Quality-of-life extension for Pi.
9
9
 
10
+ ## Install
11
+
12
+ Via [npm](https://www.npmjs.com/package/@vanillagreen/pi-qol):
13
+
14
+ ```bash
15
+ pi install npm:@vanillagreen/pi-qol
16
+ ```
17
+
18
+ Via [vstack](https://github.com/vanillagreencom/vstack):
19
+
20
+ ```bash
21
+ cargo install --git https://github.com/vanillagreencom/vstack.git vstack
22
+ vstack add vanillagreencom/vstack --pi-extension pi-qol --harness pi -y
23
+ ```
24
+
25
+ Restart Pi after installation.
26
+
10
27
  ## What it provides
11
28
 
12
29
  - Compact statusline and `π` prompt editor: shows repo/project, branch/dirty state, model, thinking level, context window size, and remaining context percent while replacing Pi's default footer/editor chrome.
package/extensions/qol.ts CHANGED
@@ -9,6 +9,7 @@ import { homedir } from "node:os";
9
9
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
10
10
 
11
11
  const INSTALL_SYMBOL = Symbol.for("vstack.pi-qol.installed");
12
+ const CONFIG_ID = "@vanillagreen/pi-qol";
12
13
  const STATUS_KEY = "qol-attachments";
13
14
  const SESSION_SEARCH_STATUS_KEY = "qol-session-search";
14
15
  const SESSION_SEARCH_CONTEXT_TYPE = "qol-session-context";
@@ -44,6 +45,7 @@ const DEFAULT_SESSION_SEARCH_SHORTCUT = "f3";
44
45
  const DEFAULT_SESSION_SEARCH_SUMMARY_INPUT_CHARS = 180_000;
45
46
  const DEFAULT_SESSION_SEARCH_SUMMARY_MAX_TOKENS = 4096;
46
47
  const DEFAULT_SESSION_SEARCH_CACHE_TTL_SECONDS = 0;
48
+ const SESSION_SEARCH_OVERLAY_HEIGHT_RATIO = 0.9;
47
49
  const DEFAULT_AUTO_RENAME_MODEL = "openai-codex/gpt-5.4-mini";
48
50
  const DEFAULT_AUTO_RENAME_FALLBACK_MODEL = "current";
49
51
  const DEFAULT_AUTO_RENAME_INPUT_CHARS = 2000;
@@ -131,7 +133,7 @@ function readVstackConfig(cwd?: string): VstackConfig {
131
133
  if (!existsSync(path)) continue;
132
134
  try {
133
135
  const parsed = JSON.parse(readFileSync(path, "utf8"));
134
- const config = parsed?.vstack?.extensionManager?.config?.["pi-qol"];
136
+ const config = parsed?.vstack?.extensionManager?.config?.[CONFIG_ID];
135
137
  if (config && typeof config === "object" && !Array.isArray(config)) Object.assign(merged, config);
136
138
  } catch {
137
139
  // Ignore malformed optional manager config.
@@ -3071,7 +3073,7 @@ class QolSessionSearchComponent {
3071
3073
 
3072
3074
  constructor(
3073
3075
  private readonly done: (action: QolSessionPaletteAction) => void,
3074
- private readonly tui: { requestRender(): void },
3076
+ private readonly tui: { requestRender(): void; terminal?: { rows?: number } },
3075
3077
  private readonly theme: Theme,
3076
3078
  private readonly sessions: QolSessionSearchSession[],
3077
3079
  private readonly cwd: string,
@@ -3089,6 +3091,30 @@ class QolSessionSearchComponent {
3089
3091
 
3090
3092
  invalidate(): void {}
3091
3093
 
3094
+ private maxOverlayRows(): number {
3095
+ const terminalRows = Number(this.tui.terminal?.rows ?? process.stdout.rows ?? 30);
3096
+ const safeRows = Number.isFinite(terminalRows) && terminalRows > 0 ? terminalRows : 30;
3097
+ return Math.max(8, Math.floor(safeRows * SESSION_SEARCH_OVERLAY_HEIGHT_RATIO));
3098
+ }
3099
+
3100
+ private searchMaxVisibleRows(): number {
3101
+ const configured = Math.max(1, Math.floor(settingNumber("sessionSearch.maxVisible", 8, this.cwd)));
3102
+ // Search results render as 3 content rows plus a divider between rows.
3103
+ // The remaining chrome is title, search box, help text, scroll status,
3104
+ // footer, and frame. Keep the rendered line count within overlay maxHeight
3105
+ // so Pi does not clip the bottom of the popup on short terminals.
3106
+ const responsive = Math.max(1, Math.floor((this.maxOverlayRows() - 12) / 4));
3107
+ return Math.max(1, Math.min(configured, responsive));
3108
+ }
3109
+
3110
+ private messageMaxVisibleRows(): number {
3111
+ const configured = Math.max(1, Math.floor(settingNumber("sessionSearch.messageMaxVisible", 12, this.cwd)));
3112
+ // Message rows are one line each; reserve space for session metadata,
3113
+ // optional scroll status, footer, and frame.
3114
+ const responsive = Math.max(1, this.maxOverlayRows() - 14);
3115
+ return Math.max(1, Math.min(configured, responsive));
3116
+ }
3117
+
3092
3118
  render(width: number): string[] {
3093
3119
  const configured = Math.max(70, Math.floor(settingNumber("sessionSearch.overlayWidth", 104, this.cwd)));
3094
3120
  const renderWidth = Math.min(Math.max(48, width), configured);
@@ -3165,11 +3191,11 @@ class QolSessionSearchComponent {
3165
3191
  return;
3166
3192
  }
3167
3193
  if (matchesKey(data, "-") || matchesKey(data, "pageup")) {
3168
- state.selected = Math.max(0, state.selected - 10);
3194
+ state.selected = Math.max(0, state.selected - this.searchMaxVisibleRows());
3169
3195
  return;
3170
3196
  }
3171
3197
  if (matchesKey(data, "=") || matchesKey(data, "pagedown")) {
3172
- state.selected = Math.min(state.results.length - 1, state.selected + 10);
3198
+ state.selected = Math.min(state.results.length - 1, state.selected + this.searchMaxVisibleRows());
3173
3199
  return;
3174
3200
  }
3175
3201
  if (matchesKey(data, "left")) {
@@ -3239,11 +3265,11 @@ class QolSessionSearchComponent {
3239
3265
  return;
3240
3266
  }
3241
3267
  if (matchesKey(data, "-") || matchesKey(data, "pageup")) {
3242
- state.selected = Math.max(0, state.selected - 10);
3268
+ state.selected = Math.max(0, state.selected - this.messageMaxVisibleRows());
3243
3269
  return;
3244
3270
  }
3245
3271
  if (matchesKey(data, "=") || matchesKey(data, "pagedown")) {
3246
- state.selected = Math.min(state.messages.length - 1, state.selected + 10);
3272
+ state.selected = Math.min(state.messages.length - 1, state.selected + this.messageMaxVisibleRows());
3247
3273
  return;
3248
3274
  }
3249
3275
  if (matchesKey(data, "home") || matchesKey(data, "ctrl+a")) {
@@ -3365,7 +3391,7 @@ class QolSessionSearchComponent {
3365
3391
  if (state.results.length === 0) {
3366
3392
  lines.push(row(muted(state.query.trim() ? "No sessions match your search" : "No sessions found")), empty());
3367
3393
  } else {
3368
- const maxVisible = Math.max(3, Math.floor(settingNumber("sessionSearch.maxVisible", 8, this.cwd)));
3394
+ const maxVisible = this.searchMaxVisibleRows();
3369
3395
  const start = Math.max(0, Math.min(state.selected - Math.floor(maxVisible / 2), state.results.length - maxVisible));
3370
3396
  const end = Math.min(start + maxVisible, state.results.length);
3371
3397
  const rowBudget = Math.max(10, inner);
@@ -3394,7 +3420,7 @@ class QolSessionSearchComponent {
3394
3420
  if (state.results.length > maxVisible) lines.push(empty(), row(dim(`${state.selected + 1}/${state.results.length} ${state.query.trim() ? "matches" : "recent sessions"}`)));
3395
3421
  }
3396
3422
  lines.push(divider(), empty());
3397
- lines.push(row(`${ansiYellow("↑↓")} ${dim("sessions")} ${ansiYellow("-/=")} ${dim("page")} ${ansiYellow("enter")} ${dim("prompts")} ${ansiYellow("ctrl+u")} ${dim("clear")} ${ansiYellow("esc")} ${dim("close")}`));
3423
+ lines.push(row(`${ansiYellow("-/=")} ${dim("page")} ${ansiYellow("enter")} ${dim("prompts")} ${ansiYellow("ctrl+u")} ${dim("clear")} ${ansiYellow("esc")} ${dim("close")}`));
3398
3424
  lines.push(bottom());
3399
3425
  return lines;
3400
3426
  }
@@ -3419,7 +3445,7 @@ class QolSessionSearchComponent {
3419
3445
  lines.push(row(dim(`${state.messages.length} user prompt${state.messages.length === 1 ? "" : "s"} · enter opens prompt actions · r resumes latest session state`)));
3420
3446
  lines.push(empty(), divider(), empty());
3421
3447
 
3422
- const maxVisible = Math.max(6, Math.floor(settingNumber("sessionSearch.messageMaxVisible", 12, this.cwd)));
3448
+ const maxVisible = this.messageMaxVisibleRows();
3423
3449
  const start = Math.max(0, Math.min(state.selected - Math.floor(maxVisible / 2), state.messages.length - maxVisible));
3424
3450
  const end = Math.min(start + maxVisible, state.messages.length);
3425
3451
  for (let i = start; i < end; i++) {
@@ -3434,7 +3460,7 @@ class QolSessionSearchComponent {
3434
3460
  }
3435
3461
  if (state.messages.length > maxVisible) lines.push(empty(), row(dim(`${state.selected + 1}/${state.messages.length} user prompts`)));
3436
3462
  lines.push(divider(), empty());
3437
- lines.push(row(`${ansiYellow("↑↓")} ${dim("prompts")} ${ansiYellow("-/=")} ${dim("page")} ${ansiYellow("enter")} ${dim("prompt actions")} ${ansiYellow("r")} ${dim("resume session")} ${ansiYellow("esc")} ${dim("sessions")}`));
3463
+ lines.push(row(`${ansiYellow("-/=")} ${dim("page")} ${ansiYellow("enter")} ${dim("prompt actions")} ${ansiYellow("r")} ${dim("resume session")} ${ansiYellow("esc")} ${dim("sessions")}`));
3438
3464
  lines.push(bottom());
3439
3465
  return lines;
3440
3466
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanillagreen/pi-qol",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Pi quality-of-life extension: compact statusline/π prompt, reliable multiline input, styled pasted-image chips, session naming/search/context import, handoff, permission prompts, notifications, custom compaction, and a collapsed-thinking timer.",
5
5
  "license": "MIT",
6
6
  "keywords": [