@vanillagreen/pi-qol 1.7.0 → 1.7.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
@@ -13,6 +13,7 @@ Quality-of-life extension for Pi: compact statusline, multiline input, session n
13
13
  - Newline-insert in the editor (multi-line drafts without auto-submit), with a fallback binding for terminals that can't distinguish the primary key.
14
14
  - Sessions auto-name from your first prompt. `/rename` overrides anytime.
15
15
  - `/search` browses previous sessions with snippet previews; the configured shortcut opens it instantly.
16
+ - Session search reads prompt snippets line-by-line, so very large session JSONL files do not have to be materialized just to browse, search, or import context.
16
17
  - `/context` shows a Claude-style context-window breakdown.
17
18
  - `/handoff <goal>` drafts a focused prompt for a new session.
18
19
  - `/schedule 20m <message>` or `/schedule 1h45m <message>` sends a delayed prompt without invoking the model until the timer fires.
@@ -63,6 +64,8 @@ Arguments support autocomplete.
63
64
 
64
65
  Open `/extensions:settings`; settings appear under the **QOL** tab. Names below match the labels shown there.
65
66
 
67
+ Project settings in `.pi/settings.json` apply only after Pi marks the workspace trusted; before trust, vstack Pi extensions read user/global settings only.
68
+
66
69
  Glyph style: each package exposes `glyphStyle` (`unicode` default, `ascii` for terminal-safe chrome). `@vanillagreen/pi-tool-renderer.globalGlyphStyleOverride=ascii` forces ASCII chrome across vstack Pi extensions while leaving tool/model/user content unchanged.
67
70
 
68
71
  ### Statusline
@@ -180,7 +183,7 @@ Idle thresholds (token threshold, idle delay, fixed token limit, percent limit)
180
183
 
181
184
  ### Long-session budget guard
182
185
 
183
- For autonomous/Flightdeck-style runs the agent never goes idle, so idle compaction may never fire and the transcript can grow until provider/buffer limits hit. The budget guard runs on `agent_end` (not idle) and starts a compaction immediately when context usage crosses a percent of the model window or an absolute token limit. It fires once per threshold crossing — repeated `agent_end` events above the same threshold do not retrigger it, and the crossing key resets on a successful compaction or on a transient compaction failure (so the next `agent_end` retries).
186
+ For long autonomous runs the agent may not go idle, so idle compaction may never fire and the transcript can grow until provider/buffer limits hit. The budget guard runs on `agent_end` (not idle) and starts a compaction immediately when context usage crosses a percent of the model window or an absolute token limit. It fires once per threshold crossing — repeated `agent_end` events above the same threshold do not retrigger it, and the crossing key resets on a successful compaction or on a transient compaction failure (so the next `agent_end` retries).
184
187
 
185
188
  | Setting | What it does | Default |
186
189
  | --- | --- | --- |
@@ -195,7 +198,7 @@ When the budget guard fires it injects a sentinel into the compaction request so
195
198
 
196
199
  While budget-guard compaction is running, QOL keeps a persistent status line above the prompt (and in the normal status footer when the compact statusline is disabled). After Pi prints the compacted-summary block, the line changes to `QOL budget guard finalizing compaction…` until `ctx.compact()` reports completion, so long reload/finalization gaps do not look frozen.
197
200
 
198
- Recommended values for autonomous/Flightdeck runs:
201
+ Recommended values for long autonomous runs:
199
202
 
200
203
  - Keep **Long-session budget guard** on. Lower **Budget guard percent** to `75` if your provider buffers are tight.
201
204
  - Optionally turn **Custom compaction summaries** on so user-initiated and idle compactions also use the QOL chunked summarizer + handoff artifact. (Budget-guard-triggered compactions always use them regardless of this setting.)
@@ -24,6 +24,7 @@ function styleSlashAutocompleteHints(suggestions: AutocompleteSuggestions | null
24
24
  export function installAutocompleteHintStyling(ctx: ExtensionContext): void {
25
25
  if (!ctx.hasUI) return;
26
26
  ctx.ui.addAutocompleteProvider((current) => ({
27
+ ...current,
27
28
  async getSuggestions(lines, cursorLine, cursorCol, options) {
28
29
  return styleSlashAutocompleteHints(await current.getSuggestions(lines, cursorLine, cursorCol, options), ctx.ui.theme);
29
30
  },
@@ -26,9 +26,44 @@ function projectSettingsPath(cwd: string): string {
26
26
  }
27
27
  }
28
28
 
29
+ const PROJECT_TRUST_SYMBOL = Symbol.for("vstack.pi.project-trust");
30
+
31
+ interface ProjectTrustRegistry {
32
+ projectSettings?: Map<string, boolean>;
33
+ }
34
+
35
+ function projectTrustRegistry(): ProjectTrustRegistry {
36
+ const host = globalThis as unknown as Record<PropertyKey, ProjectTrustRegistry | undefined>;
37
+ const existing = host[PROJECT_TRUST_SYMBOL];
38
+ if (existing) return existing;
39
+ const created: ProjectTrustRegistry = {};
40
+ host[PROJECT_TRUST_SYMBOL] = created;
41
+ return created;
42
+ }
43
+
44
+ export function recordProjectTrust(ctx: { cwd?: string; isProjectTrusted?: () => boolean }): void {
45
+ if (!ctx.cwd) return;
46
+ let trusted = true;
47
+ try {
48
+ trusted = ctx.isProjectTrusted?.() === true;
49
+ } catch {
50
+ trusted = false;
51
+ }
52
+ const registry = projectTrustRegistry();
53
+ if (!registry.projectSettings) registry.projectSettings = new Map();
54
+ registry.projectSettings.set(projectSettingsPath(ctx.cwd), trusted);
55
+ }
56
+
57
+ function projectSettingsTrusted(settingsPath: string): boolean {
58
+ return projectTrustRegistry().projectSettings?.get(settingsPath) === true;
59
+ }
60
+
61
+
29
62
  function piSettingsPaths(cwd = process.cwd()): string[] {
30
63
  const userDir = resolve(expandHome(process.env.PI_CODING_AGENT_DIR?.trim() || "~/.pi/agent"));
31
- return [join(userDir, "settings.json"), projectSettingsPath(cwd)];
64
+ const user = join(userDir, "settings.json");
65
+ const project = projectSettingsPath(cwd);
66
+ return projectSettingsTrusted(project) ? [user, project] : [user];
32
67
  }
33
68
 
34
69
  function readPackageConfig(packageId: string, cwd?: string): Record<string, unknown> {
@@ -10,6 +10,7 @@ import {
10
10
  SESSION_SEARCH_STATUS_KEY,
11
11
  } from "../constants.js";
12
12
  import { expandHome, piSettingsPaths, settingBoolean, settingNumber, settingString, settingStringAllowEmpty } from "../settings.js";
13
+ import { forEachSessionJsonlLine } from "./jsonl.js";
13
14
  import type {
14
15
  QolSessionPaletteAction,
15
16
  QolSessionSearchPendingMessage,
@@ -166,15 +167,14 @@ export function sessionUserMessages(sessionPath: string): QolSessionUserMessage[
166
167
  if (cached) return cached;
167
168
  const messages: QolSessionUserMessage[] = [];
168
169
  try {
169
- const lines = readFileSync(sessionPath, "utf8").split(/\r?\n/);
170
- for (const line of lines) {
171
- if (!line.trim()) continue;
170
+ forEachSessionJsonlLine(sessionPath, (line) => {
171
+ if (!line.trim()) return;
172
172
  let entry: any;
173
- try { entry = JSON.parse(line); } catch { continue; }
173
+ try { entry = JSON.parse(line); } catch { return; }
174
174
  const message = entry?.type === "message" ? entry.message : undefined;
175
- if (!message || message.role !== "user") continue;
175
+ if (!message || message.role !== "user") return;
176
176
  const text = oneLine(messageContentText(message.content));
177
- if (!text) continue;
177
+ if (!text) return;
178
178
  messages.push({
179
179
  entryId: typeof entry.id === "string" ? entry.id : undefined,
180
180
  index: messages.length + 1,
@@ -182,7 +182,7 @@ export function sessionUserMessages(sessionPath: string): QolSessionUserMessage[
182
182
  text,
183
183
  timestamp: sessionMessageTimestamp(entry, message),
184
184
  });
185
- }
185
+ });
186
186
  } catch {
187
187
  // Ignore unreadable sessions; callers fall back to SessionInfo.firstMessage.
188
188
  }
@@ -1,4 +1,3 @@
1
- import { readFileSync } from "node:fs";
2
1
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
2
  import { SessionManager, type ExtensionAPI, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
4
3
  import { Text } from "@earendil-works/pi-tui";
@@ -13,6 +12,7 @@ import {
13
12
  import { settingNumber, settingString } from "../settings.js";
14
13
  import { sessionDisplayName } from "./cache.js";
15
14
  import { QolSessionSearchLoadingComponent } from "./component.js";
15
+ import { forEachSessionJsonlLine } from "./jsonl.js";
16
16
  import type { QolSessionSearchPendingMessage, QolSessionSearchResult } from "./types.js";
17
17
 
18
18
  export function trimSessionSummaryInput(text: string, cwd: string): string {
@@ -33,14 +33,13 @@ function messagesForSessionSummary(sessionPath: string): AgentMessage[] {
33
33
  // Fall back to a direct JSONL parse below.
34
34
  }
35
35
  try {
36
- const lines = readFileSync(sessionPath, "utf8").split(/\r?\n/);
37
36
  const messages: AgentMessage[] = [];
38
- for (const line of lines) {
39
- if (!line.trim()) continue;
37
+ forEachSessionJsonlLine(sessionPath, (line) => {
38
+ if (!line.trim()) return;
40
39
  let entry: any;
41
- try { entry = JSON.parse(line); } catch { continue; }
40
+ try { entry = JSON.parse(line); } catch { return; }
42
41
  if (entry?.type === "message" && entry.message) messages.push(entry.message);
43
- }
42
+ });
44
43
  return messages;
45
44
  } catch {
46
45
  return [];
@@ -0,0 +1,31 @@
1
+ import { closeSync, openSync, readSync } from "node:fs";
2
+ import { StringDecoder } from "node:string_decoder";
3
+
4
+ export function forEachSessionJsonlLine(sessionPath: string, onLine: (line: string) => void, chunkSize = 64 * 1024): void {
5
+ const fd = openSync(sessionPath, "r");
6
+ const buffer = Buffer.allocUnsafe(Math.max(1, Math.floor(chunkSize)));
7
+ const decoder = new StringDecoder("utf8");
8
+ let pending = "";
9
+ try {
10
+ for (;;) {
11
+ const bytesRead = readSync(fd, buffer, 0, buffer.length, null);
12
+ if (bytesRead === 0) break;
13
+ pending += decoder.write(buffer.subarray(0, bytesRead));
14
+ let start = 0;
15
+ for (;;) {
16
+ const newline = pending.indexOf("\n", start);
17
+ if (newline < 0) {
18
+ pending = pending.slice(start);
19
+ break;
20
+ }
21
+ const line = pending.slice(start, newline);
22
+ onLine(line.endsWith("\r") ? line.slice(0, -1) : line);
23
+ start = newline + 1;
24
+ }
25
+ }
26
+ pending += decoder.end();
27
+ if (pending.length > 0) onLine(pending.endsWith("\r") ? pending.slice(0, -1) : pending);
28
+ } finally {
29
+ closeSync(fd);
30
+ }
31
+ }
@@ -23,9 +23,44 @@ export function projectSettingsPath(cwd: string): string {
23
23
  }
24
24
  }
25
25
 
26
+ const PROJECT_TRUST_SYMBOL = Symbol.for("vstack.pi.project-trust");
27
+
28
+ interface ProjectTrustRegistry {
29
+ projectSettings?: Map<string, boolean>;
30
+ }
31
+
32
+ function projectTrustRegistry(): ProjectTrustRegistry {
33
+ const host = globalThis as unknown as Record<PropertyKey, ProjectTrustRegistry | undefined>;
34
+ const existing = host[PROJECT_TRUST_SYMBOL];
35
+ if (existing) return existing;
36
+ const created: ProjectTrustRegistry = {};
37
+ host[PROJECT_TRUST_SYMBOL] = created;
38
+ return created;
39
+ }
40
+
41
+ export function recordProjectTrust(ctx: { cwd?: string; isProjectTrusted?: () => boolean }): void {
42
+ if (!ctx.cwd) return;
43
+ let trusted = true;
44
+ try {
45
+ trusted = ctx.isProjectTrusted?.() === true;
46
+ } catch {
47
+ trusted = false;
48
+ }
49
+ const registry = projectTrustRegistry();
50
+ if (!registry.projectSettings) registry.projectSettings = new Map();
51
+ registry.projectSettings.set(projectSettingsPath(ctx.cwd), trusted);
52
+ }
53
+
54
+ function projectSettingsTrusted(settingsPath: string): boolean {
55
+ return projectTrustRegistry().projectSettings?.get(settingsPath) === true;
56
+ }
57
+
58
+
26
59
  export function piSettingsPaths(cwd = process.cwd()): string[] {
27
60
  const userDir = resolve(expandHome(process.env.PI_CODING_AGENT_DIR?.trim() || "~/.pi/agent"));
28
- return [join(userDir, "settings.json"), projectSettingsPath(cwd)];
61
+ const user = join(userDir, "settings.json");
62
+ const project = projectSettingsPath(cwd);
63
+ return projectSettingsTrusted(project) ? [user, project] : [user];
29
64
  }
30
65
 
31
66
  export function readVstackConfig(cwd?: string): VstackConfig {
package/extensions/qol.ts CHANGED
@@ -67,7 +67,7 @@ import {
67
67
  runSessionSearchResumeOrFork,
68
68
  sessionSearchShortcut,
69
69
  } from "./qol/session-search/index.js";
70
- import { settingBoolean, settingNumber, settingString } from "./qol/settings.js";
70
+ import { recordProjectTrust, settingBoolean, settingNumber, settingString } from "./qol/settings.js";
71
71
  import { statusMessage } from "./qol/status-message.js";
72
72
  import {
73
73
  formatTmuxSessionTitle,
@@ -560,6 +560,7 @@ export default function qol(pi: ExtensionAPI): void {
560
560
  });
561
561
 
562
562
  pi.on("session_start", (event, ctx) => {
563
+ recordProjectTrust(ctx);
563
564
  currentCtx = ctx;
564
565
  subscribeCavemanBridge();
565
566
  latestSystemPromptOptions = undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanillagreen/pi-qol",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "description": "Pi quality-of-life extension: compact statusline/π prompt, reliable multiline input, styled pasted-image chips, session naming/search/context import, scheduled prompts, handoff, permission prompts, notifications, custom compaction, and a collapsed-thinking timer.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -875,7 +875,7 @@
875
875
  {
876
876
  "key": "compaction.budgetGuardEnabled",
877
877
  "label": "Long-session budget guard",
878
- "description": "Trigger compaction on agent_end (no idle wait) when context usage crosses budgetPercent or budgetTokens. Designed for autonomous/Flightdeck runs so transcript growth cannot silently hit provider/buffer limits. Fires once per threshold crossing, not in a loop.",
878
+ "description": "Trigger compaction on agent_end (no idle wait) when context usage crosses budgetPercent or budgetTokens. Designed for long autonomous runs so transcript growth cannot silently hit provider/buffer limits. Fires once per threshold crossing, not in a loop.",
879
879
  "type": "boolean",
880
880
  "default": true,
881
881
  "category": "Compaction Budget Guard",