@vanillagreen/pi-qol 1.7.2 → 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 +6 -6
- package/extensions/qol/compaction.ts +19 -7
- package/extensions/qol/handoff.ts +4 -5
- package/extensions/qol/pi-ai-compat.ts +37 -0
- package/extensions/qol/session-rename.ts +6 -8
- package/extensions/qol/statusline.ts +13 -5
- package/extensions/qol.ts +7 -0
- package/package.json +3 -3
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. |
|
|
@@ -85,8 +85,8 @@ Glyph style: each package exposes `glyphStyle` (`unicode` default, `ascii` for t
|
|
|
85
85
|
|
|
86
86
|
| Setting | What it does |
|
|
87
87
|
| --- | --- |
|
|
88
|
-
|
|
|
89
|
-
| Fallback newline
|
|
88
|
+
| shift+enter inserts newline | Insert a newline instead of submitting. |
|
|
89
|
+
| Fallback newline key | Alternate binding for terminals that can't send the primary one. |
|
|
90
90
|
| Style pending queue preview | Highlight Pi's pending-queue preview with a green left bar. |
|
|
91
91
|
| Style image chips | Render `[Image #N]` placeholders as distinct chips. |
|
|
92
92
|
| Show attachment count | Show a status badge when the draft has image placeholders. |
|
|
@@ -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,5 +1,6 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import {
|
|
2
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
3
|
+
import { complete, retryAssistantCallCompat } from "./pi-ai-compat.js";
|
|
3
4
|
import { convertToLlm, serializeConversation, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import {
|
|
5
6
|
DEFAULT_BUDGET_GUARD_PERCENT,
|
|
@@ -115,7 +116,7 @@ async function summarizeWithRemote(endpoint: string, systemPrompt: string, promp
|
|
|
115
116
|
|
|
116
117
|
export function resolveConfiguredModel(ctx: ExtensionContext, configured: string): any | undefined {
|
|
117
118
|
if (!configured || configured.trim().toLowerCase() === "current") return ctx.model;
|
|
118
|
-
const withoutThinking = configured.replace(/:(off|minimal|low|medium|high|xhigh)$/i, "");
|
|
119
|
+
const withoutThinking = configured.replace(/:(off|minimal|low|medium|high|xhigh|max)$/i, "");
|
|
119
120
|
const slash = withoutThinking.indexOf("/");
|
|
120
121
|
if (slash > 0) return ctx.modelRegistry.find(withoutThinking.slice(0, slash), withoutThinking.slice(slash + 1));
|
|
121
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");
|
|
@@ -163,23 +164,34 @@ async function singleShotSummary(ctx: ExtensionContext, request: SummarizeReques
|
|
|
163
164
|
if (!model) throw new Error(`Summary model not found: ${configuredModel}`);
|
|
164
165
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
165
166
|
if (!auth.ok) throw new Error(auth.error);
|
|
166
|
-
if (!auth.apiKey) throw new Error(`No API key for ${model.provider}`);
|
|
167
167
|
|
|
168
168
|
const message: Message = {
|
|
169
169
|
content: [{ text: promptText, type: "text" }],
|
|
170
170
|
role: "user",
|
|
171
171
|
timestamp: Date.now(),
|
|
172
172
|
};
|
|
173
|
-
const response = await
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
},
|
|
177
185
|
);
|
|
186
|
+
if (response.stopReason === "error" || response.stopReason === "aborted") {
|
|
187
|
+
throw new Error(response.errorMessage || `Summary request stopped: ${response.stopReason}`);
|
|
188
|
+
}
|
|
178
189
|
const summary = response.content
|
|
179
190
|
.filter((content): content is { type: "text"; text: string } => content.type === "text")
|
|
180
191
|
.map((content) => content.text)
|
|
181
192
|
.join("\n")
|
|
182
193
|
.trim();
|
|
194
|
+
if (!summary) throw new Error("Summary model returned no text");
|
|
183
195
|
return { model: modelLabel(model), summary, via: "model" };
|
|
184
196
|
}
|
|
185
197
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import {
|
|
2
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
3
|
+
import { complete } from "./pi-ai-compat.js";
|
|
3
4
|
import { BorderedLoader, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { acquireVstackModalLock } from "./bridges.js";
|
|
5
6
|
import { serializeMessagesForSummary } from "./compaction.js";
|
|
@@ -49,9 +50,7 @@ export async function runHandoff(args: string, ctx: ExtensionCommandContext): Pr
|
|
|
49
50
|
|
|
50
51
|
const doGenerate = async () => {
|
|
51
52
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model!);
|
|
52
|
-
if (!auth.ok
|
|
53
|
-
throw new Error(auth.ok ? `No API key for ${ctx.model!.provider}` : auth.error);
|
|
54
|
-
}
|
|
53
|
+
if (!auth.ok) throw new Error(auth.error);
|
|
55
54
|
|
|
56
55
|
const userMessage: Message = {
|
|
57
56
|
role: "user",
|
|
@@ -67,7 +66,7 @@ export async function runHandoff(args: string, ctx: ExtensionCommandContext): Pr
|
|
|
67
66
|
const response = await complete(
|
|
68
67
|
ctx.model!,
|
|
69
68
|
{ systemPrompt: HANDOFF_SYSTEM_PROMPT, messages: [userMessage] },
|
|
70
|
-
{ apiKey: auth.apiKey, headers: auth.headers, signal: loader.signal },
|
|
69
|
+
{ apiKey: auth.apiKey, env: auth.env, headers: auth.headers, signal: loader.signal },
|
|
71
70
|
);
|
|
72
71
|
|
|
73
72
|
if (response.stopReason === "aborted") return null;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as piAi from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
const dynamicImport = (specifier: string) => import(specifier);
|
|
4
|
+
|
|
5
|
+
export interface PiAiCompatDeps {
|
|
6
|
+
root?: any;
|
|
7
|
+
loadCompat?: () => Promise<any>;
|
|
8
|
+
}
|
|
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
|
+
|
|
21
|
+
export async function complete(model: unknown, context: unknown, options?: unknown, deps: PiAiCompatDeps = {}): Promise<any> {
|
|
22
|
+
const rootComplete = (deps.root ?? piAi as any).complete;
|
|
23
|
+
if (typeof rootComplete === "function") return await rootComplete(model, context, options);
|
|
24
|
+
const compat = deps.loadCompat ? await deps.loadCompat() : await dynamicImport("@earendil-works/pi-ai/compat") as any;
|
|
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);
|
|
37
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
+
import { complete } from "./pi-ai-compat.js";
|
|
2
3
|
import type { ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import { textFromContent } from "./agent-end.js";
|
|
4
5
|
import { oneLine } from "./ansi.js";
|
|
@@ -20,6 +21,7 @@ type AutoRenameFallbackMode = "none" | "truncate" | "words";
|
|
|
20
21
|
|
|
21
22
|
interface AutoRenameAuth {
|
|
22
23
|
apiKey?: string;
|
|
24
|
+
env?: Record<string, string>;
|
|
23
25
|
headers?: Record<string, string>;
|
|
24
26
|
label: string;
|
|
25
27
|
model: any;
|
|
@@ -262,7 +264,7 @@ async function cheapestAvailableAutoRenameModel(ctx: ExtensionContext): Promise<
|
|
|
262
264
|
try {
|
|
263
265
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
264
266
|
const headers = headerRecord(auth.headers);
|
|
265
|
-
if (auth.ok
|
|
267
|
+
if (auth.ok) return { apiKey: auth.apiKey, env: auth.env, headers, label: modelLabel(model), model, source: "cheapest" };
|
|
266
268
|
} catch (error) {
|
|
267
269
|
if (isStaleCtxError(error)) return undefined;
|
|
268
270
|
// Try the next available model.
|
|
@@ -286,11 +288,7 @@ async function resolveAutoRenameModel(ctx: ExtensionContext, configured: string)
|
|
|
286
288
|
autoRenameDebug(ctx, `auth unavailable for ${modelLabel(model)}: ${auth.error}`, "warning");
|
|
287
289
|
return undefined;
|
|
288
290
|
}
|
|
289
|
-
|
|
290
|
-
autoRenameDebug(ctx, `no auth for ${modelLabel(model)}; use /login or models.json`, "warning");
|
|
291
|
-
return undefined;
|
|
292
|
-
}
|
|
293
|
-
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 };
|
|
294
292
|
} catch (error) {
|
|
295
293
|
if (isStaleCtxError(error)) return undefined;
|
|
296
294
|
autoRenameDebug(ctx, `auth failed for ${modelLabel(model)}: ${stringifyError(error)}`, "warning");
|
|
@@ -330,7 +328,7 @@ export async function generateAutoRenameName(query: string, ctx: ExtensionContex
|
|
|
330
328
|
const response = await complete(
|
|
331
329
|
resolved.model,
|
|
332
330
|
{ messages: [message], systemPrompt: AUTO_RENAME_SYSTEM_PROMPT },
|
|
333
|
-
{ apiKey: resolved.apiKey, headers: resolved.headers, maxTokens, signal: controller.signal },
|
|
331
|
+
{ apiKey: resolved.apiKey, env: resolved.env, headers: resolved.headers, maxTokens, signal: controller.signal },
|
|
334
332
|
);
|
|
335
333
|
if (response.stopReason === "error") {
|
|
336
334
|
autoRenameDebug(ctx, `${resolved.label} failed: ${response.errorMessage ?? "unknown error"}`, "warning");
|
|
@@ -39,17 +39,19 @@ function formatModelName(ctx: ExtensionContext): string {
|
|
|
39
39
|
return name;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
43
|
-
|
|
42
|
+
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
43
|
+
type ThinkingThemeToken = "thinkingOff" | "thinkingMinimal" | "thinkingLow" | "thinkingMedium" | "thinkingHigh" | "thinkingXhigh" | "thinkingMax";
|
|
44
|
+
const THINKING_TOKEN: Record<ThinkingLevel, ThinkingThemeToken> = {
|
|
44
45
|
off: "thinkingOff",
|
|
45
46
|
minimal: "thinkingMinimal",
|
|
46
47
|
low: "thinkingLow",
|
|
47
48
|
medium: "thinkingMedium",
|
|
48
49
|
high: "thinkingHigh",
|
|
49
50
|
xhigh: "thinkingXhigh",
|
|
51
|
+
max: "thinkingMax",
|
|
50
52
|
};
|
|
51
53
|
|
|
52
|
-
function normalizeThinkingLevel(value: string | undefined): ThinkingLevel {
|
|
54
|
+
export function normalizeThinkingLevel(value: string | undefined): ThinkingLevel {
|
|
53
55
|
switch ((value ?? "").toLowerCase()) {
|
|
54
56
|
case "off": return "off";
|
|
55
57
|
case "minimal": return "minimal";
|
|
@@ -57,10 +59,15 @@ function normalizeThinkingLevel(value: string | undefined): ThinkingLevel {
|
|
|
57
59
|
case "medium": return "medium";
|
|
58
60
|
case "high": return "high";
|
|
59
61
|
case "xhigh": return "xhigh";
|
|
62
|
+
case "max": return "max";
|
|
60
63
|
default: return "off";
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
export function thinkingThemeToken(value: string | undefined): ThinkingThemeToken {
|
|
68
|
+
return THINKING_TOKEN[normalizeThinkingLevel(value)];
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
function formatWindow(tokens: number | undefined): string {
|
|
65
72
|
if (!tokens || tokens <= 0) return "?";
|
|
66
73
|
if (tokens >= 1_000_000) {
|
|
@@ -207,9 +214,10 @@ export function renderStatusLine(width: number, ctx: ExtensionContext, git: GitS
|
|
|
207
214
|
const rightPlain = subagentMarker ? `${percentPlain} ${subagentMarker.plain}` : percentPlain;
|
|
208
215
|
const percentColor = percent === null ? "muted" : percent <= 15 ? "error" : percent <= 30 ? "warning" : "success";
|
|
209
216
|
const separatorColored = theme.fg("muted", statusSeparator);
|
|
217
|
+
const thinkingToken = thinkingThemeToken(thinkingLevel) as Parameters<Theme["fg"]>[0];
|
|
210
218
|
const leftColored = caveman
|
|
211
|
-
? `${theme.fg("accent", projectChunk)}${separatorColored}${theme.fg(
|
|
212
|
-
: `${theme.fg("accent", projectChunk)}${separatorColored}${theme.fg(
|
|
219
|
+
? `${theme.fg("accent", projectChunk)}${separatorColored}${theme.fg(thinkingToken, thinkingChunk)}${separatorColored}${theme.fg(cavemanTone, cavemanGlyph)}${theme.fg("muted", contextSeparator)}${theme.fg("accent", contextChunk.trimStart())}`
|
|
220
|
+
: `${theme.fg("accent", projectChunk)}${separatorColored}${theme.fg(thinkingToken, thinkingChunk)}${theme.fg("accent", contextChunk)}`;
|
|
213
221
|
const right = subagentMarker ? `${theme.fg(percentColor, percentPlain)} ${subagentMarker.styled}` : theme.fg(percentColor, percentPlain);
|
|
214
222
|
const minimumGap = 1;
|
|
215
223
|
const gapWidth = Math.max(minimumGap, width - visibleWidth(leftPlain) - visibleWidth(rightPlain) - 2);
|
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
|
},
|