@vanillagreen/pi-qol 1.7.3 → 1.7.4
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
|
@@ -76,7 +76,7 @@ Glyph style: each package exposes `glyphStyle` (`unicode` default, `ascii` for t
|
|
|
76
76
|
| Show compact statusline | Render or disable the QOL statusline row. |
|
|
77
77
|
| Replace built-in footer | Hide Pi's default footer while the QOL statusline is enabled. |
|
|
78
78
|
| Use π prompt editor | Use the compact prompt editor. |
|
|
79
|
-
| Show session name title | Show the session name above the prompt and in the tmux pane title. |
|
|
79
|
+
| Show session name title | Show the session name above the prompt and in the tmux pane title; refreshes as soon as Pi reports a session metadata change. |
|
|
80
80
|
| Sync session name to tmux window name | Rename the tmux window to `π <session>`. |
|
|
81
81
|
| Input bottom padding | Blank lines below the prompt. |
|
|
82
82
|
| Show dirty marker | Append `*` to the branch when the worktree is dirty. |
|
|
@@ -129,7 +129,7 @@ Advanced: input cap, title length, output tokens, timeout, custom prompt templat
|
|
|
129
129
|
| Enable session search | Register `/search` and the overlay. |
|
|
130
130
|
| Session search shortcut | Configurable; set to `none` to disable. |
|
|
131
131
|
| Result limit | Max matching prompts returned. |
|
|
132
|
-
| Visible session rows | Rows shown before scrolling
|
|
132
|
+
| Visible session rows | Rows shown before scrolling; defaults to `8`. |
|
|
133
133
|
| Preview snippets | Matching snippets shown on the preview screen. |
|
|
134
134
|
| Session cache TTL | Seconds before the session list refreshes; `0` keeps it until you run `/search:refresh`. |
|
|
135
135
|
|
|
@@ -169,8 +169,8 @@ Off by default. When enabled, non-interactive matches are blocked.
|
|
|
169
169
|
|
|
170
170
|
| Setting | What it does |
|
|
171
171
|
| --- | --- |
|
|
172
|
-
| Custom compaction summaries | Use QOL summaries instead of Pi's default. |
|
|
173
|
-
| Compaction model | Summarizer model. Defaults to `current`, meaning Pi's active model; set a provider/model when you want a dedicated larger-context summarizer. |
|
|
172
|
+
| Custom compaction summaries | Use QOL summaries instead of Pi's default, with Pi-standard transient provider retries when available. |
|
|
173
|
+
| Compaction model | Summarizer model. Defaults to `current`, meaning Pi's active model; set a provider/model when you want a dedicated larger-context summarizer. Thinking suffixes through `:max` are accepted, and Pi-resolved header/environment authentication is forwarded. |
|
|
174
174
|
| Compaction detail profile | `concise`, `balanced`, or `exhaustive`. |
|
|
175
175
|
| Include previous summary | Pass the previous summary for iterative continuity. |
|
|
176
176
|
| Fallback to Pi default compaction | Run Pi's default compaction if QOL's fails. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { Message } from "@earendil-works/pi-ai";
|
|
3
|
-
import { complete } from "./pi-ai-compat.js";
|
|
3
|
+
import { complete, retryAssistantCallCompat } from "./pi-ai-compat.js";
|
|
4
4
|
import { convertToLlm, serializeConversation, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_BUDGET_GUARD_PERCENT,
|
|
@@ -116,7 +116,7 @@ async function summarizeWithRemote(endpoint: string, systemPrompt: string, promp
|
|
|
116
116
|
|
|
117
117
|
export function resolveConfiguredModel(ctx: ExtensionContext, configured: string): any | undefined {
|
|
118
118
|
if (!configured || configured.trim().toLowerCase() === "current") return ctx.model;
|
|
119
|
-
const withoutThinking = configured.replace(/:(off|minimal|low|medium|high|xhigh)$/i, "");
|
|
119
|
+
const withoutThinking = configured.replace(/:(off|minimal|low|medium|high|xhigh|max)$/i, "");
|
|
120
120
|
const slash = withoutThinking.indexOf("/");
|
|
121
121
|
if (slash > 0) return ctx.modelRegistry.find(withoutThinking.slice(0, slash), withoutThinking.slice(slash + 1));
|
|
122
122
|
const providers = [ctx.model?.provider, "google", "openai", "anthropic", "mistral", "moonshot", "cloudflare-ai-gateway", "cloudflare-workers-ai"].filter((value): value is string => typeof value === "string");
|
|
@@ -164,23 +164,34 @@ async function singleShotSummary(ctx: ExtensionContext, request: SummarizeReques
|
|
|
164
164
|
if (!model) throw new Error(`Summary model not found: ${configuredModel}`);
|
|
165
165
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
166
166
|
if (!auth.ok) throw new Error(auth.error);
|
|
167
|
-
if (!auth.apiKey) throw new Error(`No API key for ${model.provider}`);
|
|
168
167
|
|
|
169
168
|
const message: Message = {
|
|
170
169
|
content: [{ text: promptText, type: "text" }],
|
|
171
170
|
role: "user",
|
|
172
171
|
timestamp: Date.now(),
|
|
173
172
|
};
|
|
174
|
-
const response = await
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
173
|
+
const response = await retryAssistantCallCompat(
|
|
174
|
+
() => complete(
|
|
175
|
+
model,
|
|
176
|
+
{ messages: [message], systemPrompt: QOL_COMPACTION_SYSTEM_PROMPT },
|
|
177
|
+
{ apiKey: auth.apiKey, env: auth.env, headers: auth.headers, maxTokens: options.maxTokens, signal: options.signal },
|
|
178
|
+
),
|
|
179
|
+
options.signal,
|
|
180
|
+
{
|
|
181
|
+
onRetryScheduled: (attempt, maxAttempts, delayMs, errorMessage) => {
|
|
182
|
+
compactionNotify(ctx, `QOL compaction retry ${attempt}/${maxAttempts} in ${Math.ceil(delayMs / 1000)}s: ${errorMessage}`, "warning");
|
|
183
|
+
},
|
|
184
|
+
},
|
|
178
185
|
);
|
|
186
|
+
if (response.stopReason === "error" || response.stopReason === "aborted") {
|
|
187
|
+
throw new Error(response.errorMessage || `Summary request stopped: ${response.stopReason}`);
|
|
188
|
+
}
|
|
179
189
|
const summary = response.content
|
|
180
190
|
.filter((content): content is { type: "text"; text: string } => content.type === "text")
|
|
181
191
|
.map((content) => content.text)
|
|
182
192
|
.join("\n")
|
|
183
193
|
.trim();
|
|
194
|
+
if (!summary) throw new Error("Summary model returned no text");
|
|
184
195
|
return { model: modelLabel(model), summary, via: "model" };
|
|
185
196
|
}
|
|
186
197
|
|
|
@@ -50,9 +50,7 @@ export async function runHandoff(args: string, ctx: ExtensionCommandContext): Pr
|
|
|
50
50
|
|
|
51
51
|
const doGenerate = async () => {
|
|
52
52
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model!);
|
|
53
|
-
if (!auth.ok
|
|
54
|
-
throw new Error(auth.ok ? `No API key for ${ctx.model!.provider}` : auth.error);
|
|
55
|
-
}
|
|
53
|
+
if (!auth.ok) throw new Error(auth.error);
|
|
56
54
|
|
|
57
55
|
const userMessage: Message = {
|
|
58
56
|
role: "user",
|
|
@@ -68,7 +66,7 @@ export async function runHandoff(args: string, ctx: ExtensionCommandContext): Pr
|
|
|
68
66
|
const response = await complete(
|
|
69
67
|
ctx.model!,
|
|
70
68
|
{ systemPrompt: HANDOFF_SYSTEM_PROMPT, messages: [userMessage] },
|
|
71
|
-
{ apiKey: auth.apiKey, headers: auth.headers, signal: loader.signal },
|
|
69
|
+
{ apiKey: auth.apiKey, env: auth.env, headers: auth.headers, signal: loader.signal },
|
|
72
70
|
);
|
|
73
71
|
|
|
74
72
|
if (response.stopReason === "aborted") return null;
|
|
@@ -7,9 +7,31 @@ export interface PiAiCompatDeps {
|
|
|
7
7
|
loadCompat?: () => Promise<any>;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
type RetryAssistantCall = (
|
|
11
|
+
produce: () => Promise<any>,
|
|
12
|
+
policy: { enabled: boolean; maxRetries: number; baseDelayMs: number },
|
|
13
|
+
signal: AbortSignal | undefined,
|
|
14
|
+
callbacks?: {
|
|
15
|
+
onRetryScheduled?: (attempt: number, maxAttempts: number, delayMs: number, errorMessage: string) => void | Promise<void>;
|
|
16
|
+
onRetryAttemptStart?: () => void | Promise<void>;
|
|
17
|
+
onRetryFinished?: (success: boolean, attempt: number, finalError?: string) => void | Promise<void>;
|
|
18
|
+
},
|
|
19
|
+
) => Promise<any>;
|
|
20
|
+
|
|
10
21
|
export async function complete(model: unknown, context: unknown, options?: unknown, deps: PiAiCompatDeps = {}): Promise<any> {
|
|
11
22
|
const rootComplete = (deps.root ?? piAi as any).complete;
|
|
12
23
|
if (typeof rootComplete === "function") return await rootComplete(model, context, options);
|
|
13
24
|
const compat = deps.loadCompat ? await deps.loadCompat() : await dynamicImport("@earendil-works/pi-ai/compat") as any;
|
|
14
25
|
return await compat.complete(model, context, options);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function retryAssistantCallCompat(
|
|
29
|
+
produce: () => Promise<any>,
|
|
30
|
+
signal: AbortSignal | undefined,
|
|
31
|
+
callbacks?: Parameters<RetryAssistantCall>[3],
|
|
32
|
+
deps: { root?: any } = {},
|
|
33
|
+
): Promise<any> {
|
|
34
|
+
const candidate = (deps.root ?? piAi as any).retryAssistantCall as RetryAssistantCall | undefined;
|
|
35
|
+
if (typeof candidate !== "function") return produce();
|
|
36
|
+
return candidate(produce, { enabled: true, maxRetries: 3, baseDelayMs: 2000 }, signal, callbacks);
|
|
15
37
|
}
|
|
@@ -21,6 +21,7 @@ type AutoRenameFallbackMode = "none" | "truncate" | "words";
|
|
|
21
21
|
|
|
22
22
|
interface AutoRenameAuth {
|
|
23
23
|
apiKey?: string;
|
|
24
|
+
env?: Record<string, string>;
|
|
24
25
|
headers?: Record<string, string>;
|
|
25
26
|
label: string;
|
|
26
27
|
model: any;
|
|
@@ -263,7 +264,7 @@ async function cheapestAvailableAutoRenameModel(ctx: ExtensionContext): Promise<
|
|
|
263
264
|
try {
|
|
264
265
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
265
266
|
const headers = headerRecord(auth.headers);
|
|
266
|
-
if (auth.ok
|
|
267
|
+
if (auth.ok) return { apiKey: auth.apiKey, env: auth.env, headers, label: modelLabel(model), model, source: "cheapest" };
|
|
267
268
|
} catch (error) {
|
|
268
269
|
if (isStaleCtxError(error)) return undefined;
|
|
269
270
|
// Try the next available model.
|
|
@@ -287,11 +288,7 @@ async function resolveAutoRenameModel(ctx: ExtensionContext, configured: string)
|
|
|
287
288
|
autoRenameDebug(ctx, `auth unavailable for ${modelLabel(model)}: ${auth.error}`, "warning");
|
|
288
289
|
return undefined;
|
|
289
290
|
}
|
|
290
|
-
|
|
291
|
-
autoRenameDebug(ctx, `no auth for ${modelLabel(model)}; use /login or models.json`, "warning");
|
|
292
|
-
return undefined;
|
|
293
|
-
}
|
|
294
|
-
return { apiKey: auth.apiKey, headers, label: modelLabel(model), model, source: configured };
|
|
291
|
+
return { apiKey: auth.apiKey, env: auth.env, headers, label: modelLabel(model), model, source: configured };
|
|
295
292
|
} catch (error) {
|
|
296
293
|
if (isStaleCtxError(error)) return undefined;
|
|
297
294
|
autoRenameDebug(ctx, `auth failed for ${modelLabel(model)}: ${stringifyError(error)}`, "warning");
|
|
@@ -331,7 +328,7 @@ export async function generateAutoRenameName(query: string, ctx: ExtensionContex
|
|
|
331
328
|
const response = await complete(
|
|
332
329
|
resolved.model,
|
|
333
330
|
{ messages: [message], systemPrompt: AUTO_RENAME_SYSTEM_PROMPT },
|
|
334
|
-
{ apiKey: resolved.apiKey, headers: resolved.headers, maxTokens, signal: controller.signal },
|
|
331
|
+
{ apiKey: resolved.apiKey, env: resolved.env, headers: resolved.headers, maxTokens, signal: controller.signal },
|
|
335
332
|
);
|
|
336
333
|
if (response.stopReason === "error") {
|
|
337
334
|
autoRenameDebug(ctx, `${resolved.label} failed: ${response.errorMessage ?? "unknown error"}`, "warning");
|
package/extensions/qol.ts
CHANGED
|
@@ -668,6 +668,13 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
668
668
|
pi.on("thinking_level_select", (_event, ctx) => {
|
|
669
669
|
if (ctx.hasUI) requestRender();
|
|
670
670
|
});
|
|
671
|
+
// Renames land immediately instead of waiting for the SESSION_TITLE_SYNC_INTERVAL_MS
|
|
672
|
+
// poll installed by installSessionTitleSync; the poll stays as the backstop for
|
|
673
|
+
// name changes Pi does not announce.
|
|
674
|
+
pi.on("session_info_changed", (_event, ctx) => {
|
|
675
|
+
if (!ctx.hasUI) return;
|
|
676
|
+
syncSessionTitle(ctx);
|
|
677
|
+
});
|
|
671
678
|
pi.on("agent_start", (_event, ctx) => {
|
|
672
679
|
clearIdleCompactionTimer();
|
|
673
680
|
clearTmuxWindowMark();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillagreen/pi-qol",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.4",
|
|
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": [
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
{
|
|
80
80
|
"key": "showSessionNameTitle",
|
|
81
81
|
"label": "Show session name title",
|
|
82
|
-
"description": "Show the named session above the prompt. In tmux, update the current pane title to \"π <session>\" and enable a pane border title padded with non-breaking spaces so tmux does not trim it.",
|
|
82
|
+
"description": "Show the named session above the prompt. In tmux, update the current pane title to \"π <session>\" and enable a pane border title padded with non-breaking spaces so tmux does not trim it. The title refreshes as soon as Pi reports a session metadata change.",
|
|
83
83
|
"type": "boolean",
|
|
84
84
|
"default": true,
|
|
85
85
|
"category": "Statusline",
|
|
@@ -452,7 +452,7 @@
|
|
|
452
452
|
"label": "Visible session rows",
|
|
453
453
|
"description": "Maximum number of sessions shown at once in the overlay before scrolling.",
|
|
454
454
|
"type": "number",
|
|
455
|
-
"default":
|
|
455
|
+
"default": 8,
|
|
456
456
|
"category": "Session Search",
|
|
457
457
|
"apply": "live"
|
|
458
458
|
},
|