@vanillagreen/pi-qol 2.3.4 → 2.5.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
@@ -4,6 +4,15 @@ A Pi extension for session controls, prompt editing and notifications. Users can
4
4
 
5
5
  ![QOL extension settings panel](https://raw.githubusercontent.com/vanillagreencom/kendex/main/pi-extensions/pi-qol/assets/settings-panel.png) ![Session search popup](https://raw.githubusercontent.com/vanillagreencom/kendex/main/pi-extensions/pi-qol/assets/session-search.gif) ![/context usage breakdown](https://raw.githubusercontent.com/vanillagreencom/kendex/main/pi-extensions/pi-qol/assets/context-usage.png)
6
6
 
7
+ ## Features
8
+
9
+ - Show repository, model and context information beside the editor, with the working spinner before the project name.
10
+ - Name and search sessions.
11
+ - Schedule prompts and prepare handoff drafts.
12
+ - Ask before configured shell commands run.
13
+ - Send terminal and desktop notifications.
14
+ - Configure summaries and compaction for long sessions.
15
+
7
16
  ## Install
8
17
 
9
18
  - npm: `pi install npm:@vanillagreen/pi-qol`.
@@ -16,15 +25,6 @@ source = "kendex"
16
25
 
17
26
  Requires Pi 0.86.0 or newer. Restart Pi after installation. Use `kendex update-pi --check` to preview the installation.
18
27
 
19
- ## Features
20
-
21
- - Show repository, model and context information beside the editor, with the working spinner before the project name.
22
- - Name and search sessions.
23
- - Schedule prompts and prepare handoff drafts.
24
- - Ask before configured shell commands run.
25
- - Send terminal and desktop notifications.
26
- - Configure summaries and compaction for long sessions.
27
-
28
28
  ## How it works
29
29
 
30
30
  The extension reads your enabled features when Pi starts. It adds their editor controls, commands and event handlers. Session actions update the session or queue messages for the agent. Notifications report events through your selected channels. Compaction settings control when long conversations are summarized.
@@ -42,24 +42,29 @@ The extension reads your enabled features when Pi starts. It adds their editor c
42
42
 
43
43
  At session start, the extension deletes budget handoff files whose recorded working directory no longer exists. It also deletes files older than 5 days. Both timestamped snapshots and `latest.json` follow this rule.
44
44
 
45
- ## Settings
45
+ ## Setup
46
46
 
47
47
  The settings editor writes project values to `.pi/settings.json`. The default user file is `~/.pi/agent/settings.json`. `PI_CODING_AGENT_DIR` changes the user directory. Package values are stored under `kendex.extensionManager.config["@vanillagreen/pi-qol"]`.
48
48
 
49
49
  Open `/extensions:settings`; settings appear under the **QOL** tab. Project settings in `.pi/settings.json` apply only after Pi marks the workspace trusted. `glyphStyle` picks `unicode` or `ascii` symbols, and `@vanillagreen/pi-tool-renderer`'s `globalGlyphStyleOverride` wins when set.
50
50
 
51
51
  - `enabled`: package toggle for everything below.
52
+ - `gatewayPrivacy.enabled`: require zero data retention and no prompt training for main-agent and QOL model requests through OpenRouter and Vercel AI Gateway. Covers QOL compaction, branch summaries, imported-session summaries, handoff and session naming. Keeps other routing options and overrides weaker privacy values. Pi's built-in compaction and branch summaries cannot carry these fields, so on a gateway session model QOL writes them in place of Pi's, whatever `compaction.customEnabled` and `compaction.branchSummaryEnabled` say, and a failed or refused summary cancels the compaction or tree move instead of falling back to Pi's. Custom remote compaction endpoints keep their own privacy policy.
52
53
  - Statusline and editor: `statusline.enabled`, `statusline.showProvider`, `statusline.showAccount`, `replaceFooter`, `compactPrompt`, `showSessionNameTitle`, `showSessionNameWindow`, `inputBottomPaddingLines`, `gitRefreshTimeoutMs`, `showDirtyMarker`, `newlineOnShiftEnter`, `newlineFallbackKey`, `pendingQueue.asciiGreen`, `showImageChips`, `showAttachmentCountInStatus`.
53
54
  - Commands: `enableSessionNameCommand`, `enableHandoffCommand`, `enableScheduleCommand`, `enableContextCommand`, `handoffReviewPrompt`.
54
- - Session naming: `sessionAutoRename.*` (model, fallback model, deterministic fallback, prefix, prompt, limits, notify, debug).
55
+ - Session naming: `sessionAutoRename.*` (model, fallback model, deterministic fallback, prefix, prompt, limits, notify, debug). `sessionAutoRename.maxTokens` includes the naming model's reasoning tokens. `sessionAutoRename.maxNameChars` limits the title length.
55
56
  - Session search: `sessionSearch.*` (shortcut, result and row limits, snippets, overlay width, cache TTL, summary model and limits).
56
57
  - Rate limits: `rateLimitAutoResume.*`.
57
58
  - Permission gate: `permissionGate.enabled`, `permissionGate.commands` (comma-separated literal fragments or `/regex/flags`), `permissionGate.previewLines`, `permissionGate.previewChars`.
58
59
  - Notifications: `notification.*` (triggers, channels, tmux options, protocol, cooldown, title and body).
59
- - Compaction and budget guard: `compaction.*` (custom summaries, model, profile, remote endpoint, branch summaries, idle trigger, budget guard thresholds, chunk input cap, handoff artifact, transcript-risk budget).
60
+ - Compaction and budget guard: `compaction.*` (custom summaries, model, profile, remote endpoint, branch summaries, idle trigger, budget guard thresholds, chunk input cap, handoff artifact, transcript-risk budget). `compaction.maxTokens` caps budget-guard, custom and branch summaries.
60
61
  - Pi's own `compaction.enabled` (the top-level `compaction` object in the same settings files, not a QOL key) turns off every automatic compaction: when it is `false`, neither the budget guard nor the idle trigger starts one, and `/qol` reports both as disabled by that key. A manual `/compact` still runs. While it is `true` or absent, both keep the defaults and thresholds above: the budget guard on at `compaction.budgetPercent` 85, the idle trigger off until `compaction.idleEnabled` is set.
61
- - Thinking: `thinkingLabel.text`, `thinkingTimer.enabled`, `workingIndicator.mode` (`static` if the animated indicator flashes).
62
+ - Thinking: `thinkingLabel.text`, `thinkingTimer.enabled`, `workingIndicator.mode`. `workingIndicator.mode` defaults to `static`, a dot with no animation timer; `animated` restores Pi's spinner. `thinkingTimer.enabled` controls the thinking duration display.
62
63
 
63
64
  **Show model provider** displays a readable provider name before the model, such as `Copilot / GPT 6 Astra`. It is on by default; changes apply on the next render without reloading. Disabling the QOL statusline leaves Pi's standalone working indicator in place.
64
65
 
65
66
  Maintainer notes are in [DEVELOPMENT.md](DEVELOPMENT.md).
67
+
68
+ ## Licence
69
+
70
+ [MIT](https://github.com/vanillagreencom/kendex/blob/main/LICENSE)
@@ -29,12 +29,16 @@ import {
29
29
  } from "./compaction-handoff.js";
30
30
  import { piCompactionEnabled, settingBoolean, settingNumber, settingString } from "./settings.js";
31
31
  import { stringifyError } from "./util.js";
32
+ import { gatewayPrivacyBlocksNativeSummary, gatewayPrivacyOnPayload } from "./gateway-privacy.js";
32
33
 
33
34
  export type QolSummaryProfile = "concise" | "balanced" | "exhaustive";
34
35
  export type QolSummaryPurpose = "compaction" | "branch-summary" | "session-search";
35
36
 
36
- export function compactionNotify(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error" = "info"): void {
37
- if (ctx.hasUI && settingBoolean("compaction.notify", true, ctx.cwd)) ctx.ui.notify(message, level);
37
+ export function compactionNotify(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error" = "info", required = false): void {
38
+ if (ctx.hasUI && (required || settingBoolean("compaction.notify", true, ctx.cwd))) ctx.ui.notify(message, level);
39
+ // Print, JSON and RPC callers read stderr for diagnostics. A cancelled
40
+ // protected summary has no Pi error event to carry its provider failure.
41
+ if (!ctx.hasUI && required) console.error(JSON.stringify({ source: "pi-qol", level, message }));
38
42
  }
39
43
 
40
44
  export function compactionProfile(cwd: string): QolSummaryProfile {
@@ -174,7 +178,7 @@ async function singleShotSummary(ctx: ExtensionContext, request: SummarizeReques
174
178
  () => complete(
175
179
  model,
176
180
  { messages: [message], systemPrompt: QOL_COMPACTION_SYSTEM_PROMPT },
177
- { apiKey: auth.apiKey, env: auth.env, headers: auth.headers, maxTokens: options.maxTokens, signal: options.signal },
181
+ { apiKey: auth.apiKey, env: auth.env, headers: auth.headers, maxTokens: options.maxTokens, signal: options.signal, onPayload: gatewayPrivacyOnPayload(ctx) },
178
182
  ),
179
183
  options.signal,
180
184
  {
@@ -261,16 +265,19 @@ export function writeBudgetHandoffArtifact(ctx: ExtensionContext, handoff: QolBu
261
265
 
262
266
  export async function handleQolCompaction(event: any, ctx: ExtensionContext): Promise<any> {
263
267
  const isBudgetGuard = isBudgetGuardCompaction(event?.customInstructions);
264
- // Budget-guard-triggered compactions force the QOL bounded summarizer even
265
- // when compaction.customEnabled is off. The handoff writer still honors its
266
- // independent compaction.handoffArtifactEnabled toggle.
267
- if (!isBudgetGuard && !settingBoolean("compaction.customEnabled", false, ctx.cwd)) return undefined;
268
+ const nativeBlocked = gatewayPrivacyBlocksNativeSummary(ctx);
269
+ const customEnabled = settingBoolean("compaction.customEnabled", false, ctx.cwd);
270
+ // Budget-guard-triggered and privacy-covered compactions force the QOL
271
+ // summarizer even when compaction.customEnabled is off. The handoff writer
272
+ // still honors its independent compaction.handoffArtifactEnabled toggle.
273
+ if (!isBudgetGuard && !nativeBlocked && !customEnabled) return undefined;
268
274
  const preparation = event.preparation ?? {};
269
275
  const messages = [...(preparation.messagesToSummarize ?? []), ...(preparation.turnPrefixMessages ?? [])];
270
276
  if (messages.length === 0) return undefined;
271
277
  const tokensBefore = typeof preparation.tokensBefore === "number" ? preparation.tokensBefore : 0;
272
- const handoff = buildBudgetHandoff(ctx, { preparation, reason: event.customInstructions ?? "session_before_compact" });
273
- const handoffResult = writeBudgetHandoffArtifact(ctx, handoff);
278
+ const handoffResult: HandoffWriteResult = isBudgetGuard || customEnabled
279
+ ? writeBudgetHandoffArtifact(ctx, buildBudgetHandoff(ctx, { preparation, reason: event.customInstructions ?? "session_before_compact" }))
280
+ : {};
274
281
  const sourceLabel = isBudgetGuard ? "pi-qol budget-guard" : "pi-qol";
275
282
  compactionNotify(ctx, `QOL compaction: summarizing ${messages.length} message(s), ${tokensBefore.toLocaleString()} token(s)${isBudgetGuard ? " (budget guard)" : ""}.`, "info");
276
283
  try {
@@ -308,8 +315,8 @@ export async function handleQolCompaction(event: any, ctx: ExtensionContext): Pr
308
315
  };
309
316
  } catch (error) {
310
317
  if (event.signal?.aborted) return undefined;
311
- compactionNotify(ctx, `QOL compaction failed: ${stringifyError(error)}`, "error");
312
- return settingBoolean("compaction.fallbackToDefault", true, ctx.cwd) ? undefined : { cancel: true };
318
+ compactionNotify(ctx, `QOL compaction failed: ${stringifyError(error)}`, "error", nativeBlocked);
319
+ return !nativeBlocked && settingBoolean("compaction.fallbackToDefault", true, ctx.cwd) ? undefined : { cancel: true };
313
320
  }
314
321
  }
315
322
 
@@ -322,7 +329,8 @@ function summarizeEntryForBranch(entry: any): string[] {
322
329
  }
323
330
 
324
331
  export async function handleQolBranchSummary(event: any, ctx: ExtensionContext): Promise<any> {
325
- if (!settingBoolean("compaction.branchSummaryEnabled", false, ctx.cwd)) return undefined;
332
+ const nativeBlocked = gatewayPrivacyBlocksNativeSummary(ctx);
333
+ if (!nativeBlocked && !settingBoolean("compaction.branchSummaryEnabled", false, ctx.cwd)) return undefined;
326
334
  const preparation = event.preparation ?? {};
327
335
  if (preparation.userWantsSummary !== true) return undefined;
328
336
  const entries = Array.isArray(preparation.entriesToSummarize) ? preparation.entriesToSummarize : [];
@@ -345,8 +353,8 @@ export async function handleQolBranchSummary(event: any, ctx: ExtensionContext):
345
353
  };
346
354
  } catch (error) {
347
355
  if (event.signal?.aborted) return undefined;
348
- compactionNotify(ctx, `QOL branch summary failed: ${stringifyError(error)}`, "error");
349
- return undefined;
356
+ compactionNotify(ctx, `QOL branch summary failed: ${stringifyError(error)}`, "error", nativeBlocked);
357
+ return nativeBlocked ? { cancel: true } : undefined;
350
358
  }
351
359
  }
352
360
 
@@ -92,7 +92,7 @@ export const DEFAULT_AUTO_RENAME_MODEL = "openai-codex/gpt-5.4-mini";
92
92
  export const DEFAULT_AUTO_RENAME_FALLBACK_MODEL = "current";
93
93
  export const DEFAULT_AUTO_RENAME_INPUT_CHARS = 2000;
94
94
  export const DEFAULT_AUTO_RENAME_NAME_CHARS = 80;
95
- export const DEFAULT_AUTO_RENAME_MAX_TOKENS = 96;
95
+ export const DEFAULT_AUTO_RENAME_MAX_TOKENS = 128;
96
96
  export const DEFAULT_AUTO_RENAME_TIMEOUT_MS = 12_000;
97
97
  export const DEFAULT_INPUT_BOTTOM_PADDING_LINES = 0;
98
98
 
@@ -0,0 +1,59 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { recordProjectTrust } from "./package-config.js";
3
+ import { settingBoolean } from "./settings.js";
4
+
5
+ function isRecord(value: unknown): value is Record<string, unknown> {
6
+ return value !== null && typeof value === "object" && !Array.isArray(value);
7
+ }
8
+
9
+ function gatewayPrivacyCovers(model: { provider: string } | undefined, cwd: string): boolean {
10
+ const provider = model?.provider;
11
+ return (provider === "openrouter" || provider === "vercel-ai-gateway") && settingBoolean("gatewayPrivacy.enabled", false, cwd);
12
+ }
13
+
14
+ /** Pi sends its own compaction and branch summaries to the session model with
15
+ * no payload callback, so for a covered model QOL writes the summary and never
16
+ * falls back to Pi's. */
17
+ export function gatewayPrivacyBlocksNativeSummary(ctx: ExtensionContext): boolean {
18
+ recordProjectTrust(ctx);
19
+ return gatewayPrivacyCovers(ctx.model, ctx.cwd);
20
+ }
21
+
22
+ export function gatewayPrivacyOnPayload(ctx: ExtensionContext): (payload: unknown, model: { provider: string } | undefined) => unknown {
23
+ const cwd = ctx.cwd;
24
+ recordProjectTrust(ctx);
25
+ return (payload, model) => {
26
+ if (!gatewayPrivacyCovers(model, cwd)) return undefined;
27
+ const body = payload;
28
+ if (!isRecord(body)) return undefined;
29
+
30
+ // These request filters are opt-in package policy. Caller routing keys
31
+ // remain, but weaker privacy values must not disable an enabled filter.
32
+ if (model?.provider === "openrouter") {
33
+ return {
34
+ ...body,
35
+ provider: {
36
+ ...(isRecord(body.provider) ? body.provider : {}),
37
+ zdr: true,
38
+ data_collection: "deny",
39
+ },
40
+ };
41
+ }
42
+ const options = isRecord(body.providerOptions) ? body.providerOptions : {};
43
+ return {
44
+ ...body,
45
+ providerOptions: {
46
+ ...options,
47
+ gateway: {
48
+ ...(isRecord(options.gateway) ? options.gateway : {}),
49
+ zeroDataRetention: true,
50
+ disallowPromptTraining: true,
51
+ },
52
+ },
53
+ };
54
+ };
55
+ }
56
+
57
+ export function registerGatewayPrivacy(pi: ExtensionAPI): void {
58
+ pi.on("before_provider_request", (event, ctx) => gatewayPrivacyOnPayload(ctx)(event.payload, ctx.model));
59
+ }
@@ -6,6 +6,7 @@ import { acquirekendexModalLock } from "./bridges.js";
6
6
  import { serializeMessagesForSummary } from "./compaction.js";
7
7
  import { HANDOFF_SYSTEM_PROMPT } from "./constants.js";
8
8
  import { settingBoolean } from "./settings.js";
9
+ import { gatewayPrivacyOnPayload } from "./gateway-privacy.js";
9
10
 
10
11
  function errorMessage(error: unknown): string {
11
12
  const message = error instanceof Error ? error.message : String(error);
@@ -66,7 +67,7 @@ export async function runHandoff(args: string, ctx: ExtensionCommandContext): Pr
66
67
  const response = await complete(
67
68
  ctx.model!,
68
69
  { systemPrompt: HANDOFF_SYSTEM_PROMPT, messages: [userMessage] },
69
- { apiKey: auth.apiKey, env: auth.env, headers: auth.headers, signal: loader.signal },
70
+ { apiKey: auth.apiKey, env: auth.env, headers: auth.headers, signal: loader.signal, onPayload: gatewayPrivacyOnPayload(ctx) },
70
71
  );
71
72
 
72
73
  if (response.stopReason === "aborted") return null;
@@ -1,5 +1,6 @@
1
1
  import type { Message } from "@earendil-works/pi-ai";
2
2
  import { complete } from "./pi-ai-compat.js";
3
+ import { gatewayPrivacyOnPayload } from "./gateway-privacy.js";
3
4
  import type { ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
4
5
  import { textFromContent } from "./agent-end.js";
5
6
  import { oneLine } from "./ansi.js";
@@ -331,7 +332,7 @@ export async function generateAutoRenameName(query: string, ctx: ExtensionContex
331
332
  const response = await complete(
332
333
  resolved.model,
333
334
  { messages: [message], systemPrompt: AUTO_RENAME_SYSTEM_PROMPT },
334
- { apiKey: resolved.apiKey, env: resolved.env, headers: resolved.headers, maxTokens, signal: controller.signal },
335
+ { apiKey: resolved.apiKey, env: resolved.env, headers: resolved.headers, maxTokens, signal: controller.signal, onPayload: gatewayPrivacyOnPayload(ctx) },
335
336
  );
336
337
  if (response.stopReason === "error") {
337
338
  autoRenameDebug(ctx, `${resolved.label} failed: ${response.errorMessage ?? "unknown error"}`, "warning");
@@ -12,6 +12,7 @@ import {
12
12
  import { currentEditorText } from "./editor.js";
13
13
  import { glyphs } from "./glyphs.js";
14
14
  import { attachmentLabels } from "./images.js";
15
+ import { gatewayPrivacyBlocksNativeSummary } from "./gateway-privacy.js";
15
16
  import { permissionGateCommands } from "./permission-gate.js";
16
17
  import { autoRenameEnabled } from "./session-rename.js";
17
18
  import { sessionSearchShortcut } from "./session-search/index.js";
@@ -36,6 +37,7 @@ export function statusMessage(ctx: ExtensionContext): string {
36
37
  const labels = attachmentLabels(currentEditorText(ctx), ctx.cwd);
37
38
  const searchShortcut = sessionSearchShortcut(ctx.cwd);
38
39
  const statuslineEnabled = settingBoolean("statusline.enabled", true, ctx.cwd);
40
+ const nativeBlocked = gatewayPrivacyBlocksNativeSummary(ctx);
39
41
  const statuslineMode = statuslineEnabled
40
42
  ? (settingBoolean("replaceFooter", true, ctx.cwd) ? "enabled, replaces footer" : "enabled, footer preserved")
41
43
  : "disabled";
@@ -53,10 +55,10 @@ export function statusMessage(ctx: ExtensionContext): string {
53
55
  `Schedule command: ${settingBoolean("enableScheduleCommand", true, ctx.cwd) ? "enabled (/schedule)" : "disabled"}`,
54
56
  `Handoff prompt review: ${settingBoolean("handoffReviewPrompt", true, ctx.cwd) ? "enabled" : "disabled"}`,
55
57
  `Session search: ${settingBoolean("sessionSearch.enabled", true, ctx.cwd) ? `enabled (/search${searchShortcut ? `, ${searchShortcut}` : ""})` : "disabled"}`,
56
- `Custom compaction: ${settingBoolean("compaction.customEnabled", false, ctx.cwd) ? `enabled (${settingString("compaction.model", DEFAULT_COMPACTION_MODEL, ctx.cwd)}, ${compactionProfile(ctx.cwd)})` : "disabled (Pi default)"}`,
58
+ `Custom compaction: ${settingBoolean("compaction.customEnabled", false, ctx.cwd) ? `enabled (${settingString("compaction.model", DEFAULT_COMPACTION_MODEL, ctx.cwd)}, ${compactionProfile(ctx.cwd)})` : nativeBlocked ? "enabled by gateway privacy" : "disabled (Pi default)"}`,
57
59
  `Budget guard: ${autoCompactionStatus(autoCompactionGate("budgetGuard", ctx.cwd), () => `enabled (budgetPercent=${settingNumber("compaction.budgetPercent", DEFAULT_BUDGET_GUARD_PERCENT, ctx.cwd)}, budgetTokens=${settingNumber("compaction.budgetTokens", DEFAULT_BUDGET_GUARD_TOKENS, ctx.cwd)})`)}`,
58
60
  `Idle compaction: ${autoCompactionStatus(autoCompactionGate("idle", ctx.cwd), () => `enabled after ${Math.max(1, Math.floor(settingNumber("compaction.idleTimeoutSeconds", DEFAULT_IDLE_COMPACTION_SECONDS, ctx.cwd)))}s idle`)}`,
59
- `Branch summary override: ${settingBoolean("compaction.branchSummaryEnabled", false, ctx.cwd) ? "enabled" : "disabled"}`,
61
+ `Branch summary override: ${settingBoolean("compaction.branchSummaryEnabled", false, ctx.cwd) ? "enabled" : nativeBlocked ? "enabled by gateway privacy" : "disabled"}`,
60
62
  `Notifications: ${settingBoolean("notification.enabled", true, ctx.cwd) ? `enabled (bell=${settingBoolean("notification.bell", true, ctx.cwd)}, muteBell=${settingBoolean("notification.muteBellSound", false, ctx.cwd)}, native=${settingBoolean("notification.native", true, ctx.cwd)}, tmuxClientTty=${settingBoolean("notification.tmuxNativeClientTty", true, ctx.cwd)}, tmuxMessage=${settingBoolean("notification.tmux", false, ctx.cwd)})` : "disabled"}`,
61
63
  `Permission gate: ${settingBoolean("permissionGate.enabled", false, ctx.cwd) ? `enabled (${permissionGateCommands(ctx.cwd).join(", ") || "none configured"}; preview ${boundedSettingNumber("permissionGate.previewLines", DEFAULT_PERMISSION_GATE_PREVIEW_LINES, 4, 40, ctx.cwd)} lines/${boundedSettingNumber("permissionGate.previewChars", DEFAULT_PERMISSION_GATE_PREVIEW_CHARS, 200, 5000, ctx.cwd)} chars)` : "disabled"}`,
62
64
  `Thinking timer: ${settingBoolean("thinkingTimer.enabled", true, ctx.cwd) ? "enabled" : "disabled"}`,
package/extensions/qol.ts CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  QolEditor,
40
40
  } from "./qol/editor.js";
41
41
  import { runHandoff } from "./qol/handoff.js";
42
+ import { registerGatewayPrivacy } from "./qol/gateway-privacy.js";
42
43
  import { imageContentsForPaths, resolveSubmittedImagePaths } from "./qol/images.js";
43
44
  import {
44
45
  clearTmuxWindowMark,
@@ -109,6 +110,7 @@ export default function qol(pi: ExtensionAPI): void {
109
110
  if (guard[INSTALL_SYMBOL]) return;
110
111
  guard[INSTALL_SYMBOL] = true;
111
112
  if (!settingBoolean("enabled", true)) return;
113
+ registerGatewayPrivacy(pi);
112
114
 
113
115
  installThinkingTimerPatch();
114
116
  const thinkingTimerStore: ThinkingTimerStore = {
@@ -226,12 +228,12 @@ export default function qol(pi: ExtensionAPI): void {
226
228
  thinkingTimerStore.enabled = !!ctx?.hasUI && settingBoolean("thinkingTimer.enabled", true, ctx?.cwd);
227
229
  };
228
230
 
229
- // Pi's Loader starts no animation timer for a single-frame indicator.
230
- // Keep static mode available for terminals where frequent redraws flash,
231
- // whether the indicator is embedded in QOL or in Pi's standalone row.
231
+ // Pi's Loader starts no animation timer for a single-frame indicator, so
232
+ // static mode, the default, draws no frames of its own whether the
233
+ // indicator is embedded in QOL or in Pi's standalone row.
232
234
  const applyWorkingIndicatorMode = (ctx: ExtensionContext): void => {
233
235
  if (!ctx.hasUI) return;
234
- const mode = settingString("workingIndicator.mode", "animated", ctx.cwd);
236
+ const mode = settingString("workingIndicator.mode", "static", ctx.cwd);
235
237
  if (mode === "static") {
236
238
  ctx.ui.setWorkingVisible(true);
237
239
  ctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg("accent", "●")] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanillagreen/pi-qol",
3
- "version": "2.3.4",
3
+ "version": "2.5.0",
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": [
@@ -33,6 +33,15 @@
33
33
  "apply": "reload",
34
34
  "requiresReload": true
35
35
  },
36
+ {
37
+ "key": "gatewayPrivacy.enabled",
38
+ "label": "Require gateway privacy controls",
39
+ "description": "Require zero data retention and no prompt training for main-agent and QOL model requests through OpenRouter and Vercel AI Gateway. Overrides weaker privacy values and keeps other routing options. On a gateway session model, QOL writes compaction and branch summaries in place of Pi's built-in ones, which cannot carry these controls, and a failed summary cancels instead of falling back to Pi's.",
40
+ "type": "boolean",
41
+ "default": false,
42
+ "category": "Privacy",
43
+ "apply": "live"
44
+ },
36
45
  {
37
46
  "key": "glyphStyle",
38
47
  "label": "Glyph style",
@@ -310,9 +319,9 @@
310
319
  {
311
320
  "key": "sessionAutoRename.maxTokens",
312
321
  "label": "Auto-rename output tokens",
313
- "description": "Maximum output tokens requested from the naming model.",
322
+ "description": "Maximum output tokens requested from the naming model, reasoning tokens included.",
314
323
  "type": "number",
315
- "default": 96,
324
+ "default": 128,
316
325
  "category": "Session Auto-Rename",
317
326
  "apply": "live"
318
327
  },
@@ -803,7 +812,7 @@
803
812
  {
804
813
  "key": "compaction.fallbackToDefault",
805
814
  "label": "Fallback to Pi default compaction",
806
- "description": "If custom compaction fails, let Pi run its default compaction instead of cancelling the compaction.",
815
+ "description": "If custom compaction fails, let Pi run its default compaction instead of cancelling the compaction. Ignored while gatewayPrivacy.enabled covers the session model, since Pi's compaction would send the transcript without the privacy controls.",
807
816
  "type": "boolean",
808
817
  "default": true,
809
818
  "category": "Compaction",
@@ -966,13 +975,13 @@
966
975
  {
967
976
  "key": "workingIndicator.mode",
968
977
  "label": "Working indicator mode",
969
- "description": "Control Pi's built-in streaming spinner. The default animated spinner ticks every 80ms; in terminals where rendered content overflows the viewport (chat scrolled past the top, /tree open, etc.) each tick can trigger a pi-tui full-screen redraw and visible flash. Use 'static' for a single non-animating dot that keeps the working indicator visible without the 80ms timer. It appears before the project name when the QOL statusline is enabled, or in Pi's working row otherwise.",
978
+ "description": "Control Pi's built-in streaming spinner. The default 'static' shows a single non-animating dot that keeps the working indicator visible without a timer. 'animated' restores Pi's spinner, which ticks every 80ms; in terminals where rendered content overflows the viewport (chat scrolled past the top, /tree open, etc.) each tick can trigger a pi-tui full-screen redraw and visible flash. It appears before the project name when the QOL statusline is enabled, or in Pi's working row otherwise.",
970
979
  "type": "enum",
971
980
  "enumValues": [
972
981
  "animated",
973
982
  "static"
974
983
  ],
975
- "default": "animated",
984
+ "default": "static",
976
985
  "category": "Thinking",
977
986
  "apply": "reload",
978
987
  "requiresReload": true