@vanillagreen/pi-qol 1.7.1 → 1.7.3
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 -4
- package/extensions/qol/compaction.ts +2 -1
- package/extensions/qol/editor.ts +1 -0
- package/extensions/qol/glyphs.ts +36 -1
- package/extensions/qol/handoff.ts +2 -1
- package/extensions/qol/pi-ai-compat.ts +15 -0
- package/extensions/qol/session-rename.ts +2 -1
- package/extensions/qol/settings.ts +36 -1
- package/extensions/qol/statusline.ts +13 -5
- package/extensions/qol.ts +2 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -64,6 +64,8 @@ Arguments support autocomplete.
|
|
|
64
64
|
|
|
65
65
|
Open `/extensions:settings`; settings appear under the **QOL** tab. Names below match the labels shown there.
|
|
66
66
|
|
|
67
|
+
Project settings in `.pi/settings.json` apply only after Pi marks the workspace trusted; before trust, vstack Pi extensions read user/global settings only.
|
|
68
|
+
|
|
67
69
|
Glyph style: each package exposes `glyphStyle` (`unicode` default, `ascii` for terminal-safe chrome). `@vanillagreen/pi-tool-renderer.globalGlyphStyleOverride=ascii` forces ASCII chrome across vstack Pi extensions while leaving tool/model/user content unchanged.
|
|
68
70
|
|
|
69
71
|
### Statusline
|
|
@@ -83,8 +85,8 @@ Glyph style: each package exposes `glyphStyle` (`unicode` default, `ascii` for t
|
|
|
83
85
|
|
|
84
86
|
| Setting | What it does |
|
|
85
87
|
| --- | --- |
|
|
86
|
-
|
|
|
87
|
-
| 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. |
|
|
88
90
|
| Style pending queue preview | Highlight Pi's pending-queue preview with a green left bar. |
|
|
89
91
|
| Style image chips | Render `[Image #N]` placeholders as distinct chips. |
|
|
90
92
|
| Show attachment count | Show a status badge when the draft has image placeholders. |
|
|
@@ -181,7 +183,7 @@ Idle thresholds (token threshold, idle delay, fixed token limit, percent limit)
|
|
|
181
183
|
|
|
182
184
|
### Long-session budget guard
|
|
183
185
|
|
|
184
|
-
For autonomous
|
|
186
|
+
For long autonomous runs the agent may not go idle, so idle compaction may never fire and the transcript can grow until provider/buffer limits hit. The budget guard runs on `agent_end` (not idle) and starts a compaction immediately when context usage crosses a percent of the model window or an absolute token limit. It fires once per threshold crossing — repeated `agent_end` events above the same threshold do not retrigger it, and the crossing key resets on a successful compaction or on a transient compaction failure (so the next `agent_end` retries).
|
|
185
187
|
|
|
186
188
|
| Setting | What it does | Default |
|
|
187
189
|
| --- | --- | --- |
|
|
@@ -196,7 +198,7 @@ When the budget guard fires it injects a sentinel into the compaction request so
|
|
|
196
198
|
|
|
197
199
|
While budget-guard compaction is running, QOL keeps a persistent status line above the prompt (and in the normal status footer when the compact statusline is disabled). After Pi prints the compacted-summary block, the line changes to `QOL budget guard finalizing compaction…` until `ctx.compact()` reports completion, so long reload/finalization gaps do not look frozen.
|
|
198
200
|
|
|
199
|
-
Recommended values for autonomous
|
|
201
|
+
Recommended values for long autonomous runs:
|
|
200
202
|
|
|
201
203
|
- Keep **Long-session budget guard** on. Lower **Budget guard percent** to `75` if your provider buffers are tight.
|
|
202
204
|
- Optionally turn **Custom compaction summaries** on so user-initiated and idle compactions also use the QOL chunked summarizer + handoff artifact. (Budget-guard-triggered compactions always use them regardless of this setting.)
|
|
@@ -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 { convertToLlm, serializeConversation, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import {
|
|
5
6
|
DEFAULT_BUDGET_GUARD_PERCENT,
|
package/extensions/qol/editor.ts
CHANGED
|
@@ -24,6 +24,7 @@ function styleSlashAutocompleteHints(suggestions: AutocompleteSuggestions | null
|
|
|
24
24
|
export function installAutocompleteHintStyling(ctx: ExtensionContext): void {
|
|
25
25
|
if (!ctx.hasUI) return;
|
|
26
26
|
ctx.ui.addAutocompleteProvider((current) => ({
|
|
27
|
+
...current,
|
|
27
28
|
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
28
29
|
return styleSlashAutocompleteHints(await current.getSuggestions(lines, cursorLine, cursorCol, options), ctx.ui.theme);
|
|
29
30
|
},
|
package/extensions/qol/glyphs.ts
CHANGED
|
@@ -26,9 +26,44 @@ function projectSettingsPath(cwd: string): string {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
const PROJECT_TRUST_SYMBOL = Symbol.for("vstack.pi.project-trust");
|
|
30
|
+
|
|
31
|
+
interface ProjectTrustRegistry {
|
|
32
|
+
projectSettings?: Map<string, boolean>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function projectTrustRegistry(): ProjectTrustRegistry {
|
|
36
|
+
const host = globalThis as unknown as Record<PropertyKey, ProjectTrustRegistry | undefined>;
|
|
37
|
+
const existing = host[PROJECT_TRUST_SYMBOL];
|
|
38
|
+
if (existing) return existing;
|
|
39
|
+
const created: ProjectTrustRegistry = {};
|
|
40
|
+
host[PROJECT_TRUST_SYMBOL] = created;
|
|
41
|
+
return created;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function recordProjectTrust(ctx: { cwd?: string; isProjectTrusted?: () => boolean }): void {
|
|
45
|
+
if (!ctx.cwd) return;
|
|
46
|
+
let trusted = true;
|
|
47
|
+
try {
|
|
48
|
+
trusted = ctx.isProjectTrusted?.() === true;
|
|
49
|
+
} catch {
|
|
50
|
+
trusted = false;
|
|
51
|
+
}
|
|
52
|
+
const registry = projectTrustRegistry();
|
|
53
|
+
if (!registry.projectSettings) registry.projectSettings = new Map();
|
|
54
|
+
registry.projectSettings.set(projectSettingsPath(ctx.cwd), trusted);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function projectSettingsTrusted(settingsPath: string): boolean {
|
|
58
|
+
return projectTrustRegistry().projectSettings?.get(settingsPath) === true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
29
62
|
function piSettingsPaths(cwd = process.cwd()): string[] {
|
|
30
63
|
const userDir = resolve(expandHome(process.env.PI_CODING_AGENT_DIR?.trim() || "~/.pi/agent"));
|
|
31
|
-
|
|
64
|
+
const user = join(userDir, "settings.json");
|
|
65
|
+
const project = projectSettingsPath(cwd);
|
|
66
|
+
return projectSettingsTrusted(project) ? [user, project] : [user];
|
|
32
67
|
}
|
|
33
68
|
|
|
34
69
|
function readPackageConfig(packageId: string, cwd?: string): Record<string, unknown> {
|
|
@@ -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";
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
export async function complete(model: unknown, context: unknown, options?: unknown, deps: PiAiCompatDeps = {}): Promise<any> {
|
|
11
|
+
const rootComplete = (deps.root ?? piAi as any).complete;
|
|
12
|
+
if (typeof rootComplete === "function") return await rootComplete(model, context, options);
|
|
13
|
+
const compat = deps.loadCompat ? await deps.loadCompat() : await dynamicImport("@earendil-works/pi-ai/compat") as any;
|
|
14
|
+
return await compat.complete(model, context, options);
|
|
15
|
+
}
|
|
@@ -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";
|
|
@@ -23,9 +23,44 @@ export function projectSettingsPath(cwd: string): string {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
const PROJECT_TRUST_SYMBOL = Symbol.for("vstack.pi.project-trust");
|
|
27
|
+
|
|
28
|
+
interface ProjectTrustRegistry {
|
|
29
|
+
projectSettings?: Map<string, boolean>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function projectTrustRegistry(): ProjectTrustRegistry {
|
|
33
|
+
const host = globalThis as unknown as Record<PropertyKey, ProjectTrustRegistry | undefined>;
|
|
34
|
+
const existing = host[PROJECT_TRUST_SYMBOL];
|
|
35
|
+
if (existing) return existing;
|
|
36
|
+
const created: ProjectTrustRegistry = {};
|
|
37
|
+
host[PROJECT_TRUST_SYMBOL] = created;
|
|
38
|
+
return created;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function recordProjectTrust(ctx: { cwd?: string; isProjectTrusted?: () => boolean }): void {
|
|
42
|
+
if (!ctx.cwd) return;
|
|
43
|
+
let trusted = true;
|
|
44
|
+
try {
|
|
45
|
+
trusted = ctx.isProjectTrusted?.() === true;
|
|
46
|
+
} catch {
|
|
47
|
+
trusted = false;
|
|
48
|
+
}
|
|
49
|
+
const registry = projectTrustRegistry();
|
|
50
|
+
if (!registry.projectSettings) registry.projectSettings = new Map();
|
|
51
|
+
registry.projectSettings.set(projectSettingsPath(ctx.cwd), trusted);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function projectSettingsTrusted(settingsPath: string): boolean {
|
|
55
|
+
return projectTrustRegistry().projectSettings?.get(settingsPath) === true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
26
59
|
export function piSettingsPaths(cwd = process.cwd()): string[] {
|
|
27
60
|
const userDir = resolve(expandHome(process.env.PI_CODING_AGENT_DIR?.trim() || "~/.pi/agent"));
|
|
28
|
-
|
|
61
|
+
const user = join(userDir, "settings.json");
|
|
62
|
+
const project = projectSettingsPath(cwd);
|
|
63
|
+
return projectSettingsTrusted(project) ? [user, project] : [user];
|
|
29
64
|
}
|
|
30
65
|
|
|
31
66
|
export function readVstackConfig(cwd?: string): VstackConfig {
|
|
@@ -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
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
runSessionSearchResumeOrFork,
|
|
68
68
|
sessionSearchShortcut,
|
|
69
69
|
} from "./qol/session-search/index.js";
|
|
70
|
-
import { settingBoolean, settingNumber, settingString } from "./qol/settings.js";
|
|
70
|
+
import { recordProjectTrust, settingBoolean, settingNumber, settingString } from "./qol/settings.js";
|
|
71
71
|
import { statusMessage } from "./qol/status-message.js";
|
|
72
72
|
import {
|
|
73
73
|
formatTmuxSessionTitle,
|
|
@@ -560,6 +560,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
560
560
|
});
|
|
561
561
|
|
|
562
562
|
pi.on("session_start", (event, ctx) => {
|
|
563
|
+
recordProjectTrust(ctx);
|
|
563
564
|
currentCtx = ctx;
|
|
564
565
|
subscribeCavemanBridge();
|
|
565
566
|
latestSystemPromptOptions = undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillagreen/pi-qol",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
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": [
|
|
@@ -875,7 +875,7 @@
|
|
|
875
875
|
{
|
|
876
876
|
"key": "compaction.budgetGuardEnabled",
|
|
877
877
|
"label": "Long-session budget guard",
|
|
878
|
-
"description": "Trigger compaction on agent_end (no idle wait) when context usage crosses budgetPercent or budgetTokens. Designed for autonomous
|
|
878
|
+
"description": "Trigger compaction on agent_end (no idle wait) when context usage crosses budgetPercent or budgetTokens. Designed for long autonomous runs so transcript growth cannot silently hit provider/buffer limits. Fires once per threshold crossing, not in a loop.",
|
|
879
879
|
"type": "boolean",
|
|
880
880
|
"default": true,
|
|
881
881
|
"category": "Compaction Budget Guard",
|