@vanillagreen/pi-qol 2.4.0 → 2.5.1
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 +18 -13
- package/extensions/qol/compaction.ts +22 -14
- package/extensions/qol/gateway-privacy.ts +59 -0
- package/extensions/qol/handoff.ts +2 -1
- package/extensions/qol/pi-ai-compat.ts +20 -4
- package/extensions/qol/session-rename.ts +23 -7
- package/extensions/qol/status-message.ts +4 -2
- package/extensions/qol/thinking-timer.ts +8 -4
- package/extensions/qol.ts +2 -0
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -4,6 +4,15 @@ A Pi extension for session controls, prompt editing and notifications. Users can
|
|
|
4
4
|
|
|
5
5
|
  
|
|
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
|
-
##
|
|
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). `sessionAutoRename.maxTokens`
|
|
55
|
+
- Session naming: `sessionAutoRename.*` (model, fallback model, deterministic fallback, prefix, prompt, limits, notify, debug). `sessionAutoRename.maxTokens` includes the naming model's reasoning tokens. The naming request asks for the lowest reasoning effort the model accepts, and turns reasoning off where the model allows it. `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). `compaction.maxTokens`
|
|
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`. `workingIndicator.mode` defaults to `static`, a dot with no animation timer; `animated` restores Pi's spinner.
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
//
|
|
267
|
-
|
|
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
|
|
273
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -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;
|
|
@@ -18,11 +18,27 @@ type RetryAssistantCall = (
|
|
|
18
18
|
},
|
|
19
19
|
) => Promise<any>;
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
if (typeof
|
|
21
|
+
async function dispatchCompletion(name: "complete" | "completeSimple", model: unknown, context: unknown, options: unknown, deps: PiAiCompatDeps): Promise<any> {
|
|
22
|
+
const rootFunction = (deps.root ?? piAi as any)[name];
|
|
23
|
+
if (typeof rootFunction === "function") return await rootFunction(model, context, options);
|
|
24
24
|
const compat = deps.loadCompat ? await deps.loadCompat() : await dynamicImport("@earendil-works/pi-ai/compat") as any;
|
|
25
|
-
return await compat
|
|
25
|
+
return await compat[name](model, context, options);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function complete(model: unknown, context: unknown, options?: unknown, deps: PiAiCompatDeps = {}): Promise<any> {
|
|
29
|
+
return await dispatchCompletion("complete", model, context, options, deps);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function completeSimple(model: unknown, context: unknown, options?: unknown, deps: PiAiCompatDeps = {}): Promise<any> {
|
|
33
|
+
return await dispatchCompletion("completeSimple", model, context, options, deps);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The lowest reasoning level the model accepts, as `SimpleStreamOptions.reasoning`
|
|
37
|
+
// takes it. Simple options express "off" by leaving the level out, so a model that
|
|
38
|
+
// can turn reasoning off gets undefined. Levels come from pi-ai's model catalog.
|
|
39
|
+
export function lowestReasoningLevel(model: unknown, deps: { root?: any } = {}): string | undefined {
|
|
40
|
+
const [lowest] = (deps.root ?? piAi as any).getSupportedThinkingLevels(model) as string[];
|
|
41
|
+
return lowest === "off" ? undefined : lowest;
|
|
26
42
|
}
|
|
27
43
|
|
|
28
44
|
export async function retryAssistantCallCompat(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
-
import {
|
|
2
|
+
import { completeSimple, lowestReasoningLevel } 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";
|
|
@@ -247,6 +248,21 @@ export function headerRecord(headers: unknown): Record<string, string | null> |
|
|
|
247
248
|
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
248
249
|
}
|
|
249
250
|
|
|
251
|
+
// Pi's agent path sends a request to the base URL the credential resolves, not the
|
|
252
|
+
// catalog's: a Copilot enterprise token names the enterprise proxy host
|
|
253
|
+
// (pi-ai auth/oauth/github-copilot.js `toAuth`, pi-coding-agent
|
|
254
|
+
// core/agent-session.js `_getRequiredRequestAuth`).
|
|
255
|
+
function autoRenameAuth(model: any, auth: { apiKey?: string; baseUrl?: string; env?: Record<string, string>; headers?: unknown }, source: string): AutoRenameAuth {
|
|
256
|
+
return {
|
|
257
|
+
apiKey: auth.apiKey,
|
|
258
|
+
env: auth.env,
|
|
259
|
+
headers: headerRecord(auth.headers),
|
|
260
|
+
label: modelLabel(model),
|
|
261
|
+
model: auth.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model,
|
|
262
|
+
source,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
250
266
|
function modelCost(model: any): number {
|
|
251
267
|
const cost = model?.cost;
|
|
252
268
|
return Number(cost?.input ?? 0) + Number(cost?.output ?? 0);
|
|
@@ -266,8 +282,7 @@ async function cheapestAvailableAutoRenameModel(ctx: ExtensionContext): Promise<
|
|
|
266
282
|
for (const model of models) {
|
|
267
283
|
try {
|
|
268
284
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
269
|
-
|
|
270
|
-
if (auth.ok) return { apiKey: auth.apiKey, env: auth.env, headers, label: modelLabel(model), model, source: "cheapest" };
|
|
285
|
+
if (auth.ok) return autoRenameAuth(model, auth, "cheapest");
|
|
271
286
|
} catch (error) {
|
|
272
287
|
if (isStaleCtxError(error)) return undefined;
|
|
273
288
|
// Try the next available model.
|
|
@@ -286,12 +301,11 @@ async function resolveAutoRenameModel(ctx: ExtensionContext, configured: string)
|
|
|
286
301
|
}
|
|
287
302
|
try {
|
|
288
303
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
289
|
-
const headers = headerRecord(auth.headers);
|
|
290
304
|
if (!auth.ok) {
|
|
291
305
|
autoRenameDebug(ctx, `auth unavailable for ${modelLabel(model)}: ${auth.error}`, "warning");
|
|
292
306
|
return undefined;
|
|
293
307
|
}
|
|
294
|
-
return
|
|
308
|
+
return autoRenameAuth(model, auth, configured);
|
|
295
309
|
} catch (error) {
|
|
296
310
|
if (isStaleCtxError(error)) return undefined;
|
|
297
311
|
autoRenameDebug(ctx, `auth failed for ${modelLabel(model)}: ${stringifyError(error)}`, "warning");
|
|
@@ -328,10 +342,12 @@ export async function generateAutoRenameName(query: string, ctx: ExtensionContex
|
|
|
328
342
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
329
343
|
timeout.unref?.();
|
|
330
344
|
try {
|
|
331
|
-
|
|
345
|
+
// A reasoning model left at its default effort can spend the whole token
|
|
346
|
+
// cap reasoning and write no title.
|
|
347
|
+
const response = await completeSimple(
|
|
332
348
|
resolved.model,
|
|
333
349
|
{ messages: [message], systemPrompt: AUTO_RENAME_SYSTEM_PROMPT },
|
|
334
|
-
{ apiKey: resolved.apiKey, env: resolved.env, headers: resolved.headers, maxTokens, signal: controller.signal },
|
|
350
|
+
{ apiKey: resolved.apiKey, env: resolved.env, headers: resolved.headers, maxTokens, reasoning: lowestReasoningLevel(resolved.model), signal: controller.signal, onPayload: gatewayPrivacyOnPayload(ctx) },
|
|
335
351
|
);
|
|
336
352
|
if (response.stopReason === "error") {
|
|
337
353
|
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"}`,
|
|
@@ -71,11 +71,15 @@ export function installThinkingTimerPatch(): void {
|
|
|
71
71
|
const labelComponents: Text[] = [];
|
|
72
72
|
for (const child of this.contentContainer.children as any[]) {
|
|
73
73
|
if (!child || typeof child !== "object") continue;
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
// Pi's SDK setHiddenThinkingLabel applies one label globally, not per block.
|
|
75
|
+
// Until it supports per-block labels, depend on the private contentContainer
|
|
76
|
+
// and MouseRegion.child (verified at Pi 0.86.0 and 1.0.1).
|
|
77
|
+
const label = child.child ?? child;
|
|
78
|
+
if (typeof label.setText !== "function") continue;
|
|
79
|
+
if (typeof label.text !== "string") continue;
|
|
76
80
|
const expectedLabel = thinkingLabel(store.cwd);
|
|
77
|
-
if (!
|
|
78
|
-
labelComponents.push(
|
|
81
|
+
if (!label.text.includes(expectedLabel) && !label.text.includes("Thinking...")) continue;
|
|
82
|
+
labelComponents.push(label as Text);
|
|
79
83
|
}
|
|
80
84
|
if (labelComponents.length === 0) return;
|
|
81
85
|
|
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 = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillagreen/pi-qol",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
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",
|
|
@@ -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",
|