@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 complete(
175
- model,
176
- { messages: [message], systemPrompt: QOL_COMPACTION_SYSTEM_PROMPT },
177
- { apiKey: auth.apiKey, headers: auth.headers, maxTokens: options.maxTokens, signal: options.signal },
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 || !auth.apiKey) {
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 && (auth.apiKey || headers)) return { apiKey: auth.apiKey, headers, label: modelLabel(model), model, source: "cheapest" };
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
- if (!auth.apiKey && !headers) {
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",
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": 10,
455
+ "default": 8,
456
456
  "category": "Session Search",
457
457
  "apply": "live"
458
458
  },