@vanillagreen/pi-qol 2.1.0 → 2.2.0
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 -3
- package/extensions/qol/compaction.ts +18 -2
- package/extensions/qol/editor.ts +22 -4
- package/extensions/qol/settings.ts +33 -5
- package/extensions/qol/status-message.ts +20 -2
- package/extensions/qol/statusline.ts +34 -7
- package/extensions/qol.ts +10 -12
- package/package.json +14 -5
package/README.md
CHANGED
|
@@ -14,11 +14,11 @@ A Pi extension for session controls, prompt editing and notifications. Users can
|
|
|
14
14
|
source = "kendex"
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Restart Pi after installation. Use `kendex update-pi --check` to preview the installation.
|
|
17
|
+
Requires Pi 0.86.0 or newer. Restart Pi after installation. Use `kendex update-pi --check` to preview the installation.
|
|
18
18
|
|
|
19
19
|
## Features
|
|
20
20
|
|
|
21
|
-
- Show repository, model and context information beside the editor.
|
|
21
|
+
- Show repository, model and context information beside the editor, with the working spinner before the project name.
|
|
22
22
|
- Name and search sessions.
|
|
23
23
|
- Schedule prompts and prepare handoff drafts.
|
|
24
24
|
- Ask before configured shell commands run.
|
|
@@ -36,7 +36,7 @@ The settings editor writes project values to `.pi/settings.json`. The default us
|
|
|
36
36
|
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.
|
|
37
37
|
|
|
38
38
|
- `enabled`: package toggle for everything below.
|
|
39
|
-
- Statusline and editor: `statusline.enabled`, `statusline.showAccount`, `replaceFooter`, `compactPrompt`, `showSessionNameTitle`, `showSessionNameWindow`, `inputBottomPaddingLines`, `gitRefreshTimeoutMs`, `showDirtyMarker`, `newlineOnShiftEnter`, `newlineFallbackKey`, `pendingQueue.asciiGreen`, `showImageChips`, `showAttachmentCountInStatus`.
|
|
39
|
+
- Statusline and editor: `statusline.enabled`, `statusline.showProvider`, `statusline.showAccount`, `replaceFooter`, `compactPrompt`, `showSessionNameTitle`, `showSessionNameWindow`, `inputBottomPaddingLines`, `gitRefreshTimeoutMs`, `showDirtyMarker`, `newlineOnShiftEnter`, `newlineFallbackKey`, `pendingQueue.asciiGreen`, `showImageChips`, `showAttachmentCountInStatus`.
|
|
40
40
|
- Commands: `enableSessionNameCommand`, `enableHandoffCommand`, `enableScheduleCommand`, `enableContextCommand`, `handoffReviewPrompt`.
|
|
41
41
|
- Session naming: `sessionAutoRename.*` (model, fallback model, deterministic fallback, prefix, prompt, limits, notify, debug).
|
|
42
42
|
- Session search: `sessionSearch.*` (shortcut, result and row limits, snippets, overlay width, cache TTL, summary model and limits).
|
|
@@ -44,6 +44,9 @@ Open `/extensions:settings`; settings appear under the **QOL** tab. Project sett
|
|
|
44
44
|
- Permission gate: `permissionGate.enabled`, `permissionGate.commands` (comma-separated literal fragments or `/regex/flags`), `permissionGate.previewLines`, `permissionGate.previewChars`.
|
|
45
45
|
- Notifications: `notification.*` (triggers, channels, tmux options, protocol, cooldown, title and body).
|
|
46
46
|
- 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).
|
|
47
|
+
- 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.
|
|
47
48
|
- Thinking: `thinkingLabel.text`, `thinkingTimer.enabled`, `workingIndicator.mode` (`static` if the animated indicator flashes).
|
|
48
49
|
|
|
50
|
+
**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.
|
|
51
|
+
|
|
49
52
|
Maintainer notes are in [DEVELOPMENT.md](DEVELOPMENT.md).
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
type HandoffWriteResult,
|
|
28
28
|
type QolBudgetHandoff,
|
|
29
29
|
} from "./compaction-handoff.js";
|
|
30
|
-
import { settingBoolean, settingNumber, settingString } from "./settings.js";
|
|
30
|
+
import { piCompactionEnabled, settingBoolean, settingNumber, settingString } from "./settings.js";
|
|
31
31
|
import { stringifyError } from "./util.js";
|
|
32
32
|
|
|
33
33
|
export type QolSummaryProfile = "concise" | "balanced" | "exhaustive";
|
|
@@ -373,6 +373,22 @@ export function compactionTriggerReason(ctx: ExtensionContext): string | undefin
|
|
|
373
373
|
return undefined;
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
/** Whether a QOL automatic compaction trigger may fire, and what turned it
|
|
377
|
+
* off. Pi core's `compaction.enabled` is read first, so that one key stops
|
|
378
|
+
* every automatic compaction in Pi; a manual /compact never passes here. */
|
|
379
|
+
export type AutoCompactionGate = "enabled" | "disabled" | "pi-compaction-disabled";
|
|
380
|
+
|
|
381
|
+
const AUTO_COMPACTION_SWITCHES = {
|
|
382
|
+
budgetGuard: { key: "compaction.budgetGuardEnabled", fallback: true },
|
|
383
|
+
idle: { key: "compaction.idleEnabled", fallback: false },
|
|
384
|
+
} as const;
|
|
385
|
+
|
|
386
|
+
export function autoCompactionGate(trigger: keyof typeof AUTO_COMPACTION_SWITCHES, cwd: string): AutoCompactionGate {
|
|
387
|
+
if (!piCompactionEnabled(cwd)) return "pi-compaction-disabled";
|
|
388
|
+
const { key, fallback } = AUTO_COMPACTION_SWITCHES[trigger];
|
|
389
|
+
return settingBoolean(key, fallback, cwd) ? "enabled" : "disabled";
|
|
390
|
+
}
|
|
391
|
+
|
|
376
392
|
export type BudgetGuardTrigger = BudgetTrigger;
|
|
377
393
|
export type TranscriptRiskState = TranscriptRiskResult;
|
|
378
394
|
|
|
@@ -383,7 +399,7 @@ export type TranscriptRiskState = TranscriptRiskResult;
|
|
|
383
399
|
* repeated triggers while usage stays above the threshold.
|
|
384
400
|
*/
|
|
385
401
|
export function budgetGuardTrigger(ctx: ExtensionContext): BudgetGuardTrigger | undefined {
|
|
386
|
-
if (
|
|
402
|
+
if (autoCompactionGate("budgetGuard", ctx.cwd) !== "enabled") return undefined;
|
|
387
403
|
const usage = contextUsage(ctx);
|
|
388
404
|
if (!usage) return undefined;
|
|
389
405
|
return computeBudgetTrigger({
|
package/extensions/qol/editor.ts
CHANGED
|
@@ -101,7 +101,25 @@ function syncQolEditorStatus(
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
type WorkingStatusIndicator = Parameters<CustomEditor["setWorkingStatusIndicator"]>[0];
|
|
105
|
+
|
|
106
|
+
// Pi owns the indicator's animation and lifetime. Opt into its embedded-status
|
|
107
|
+
// delivery, but draw in the statusline widget rather than the editor border.
|
|
108
|
+
class QolStatusEditor extends CustomEditor {
|
|
109
|
+
private statuslineIndicator: WorkingStatusIndicator;
|
|
110
|
+
|
|
111
|
+
override setWorkingStatusIndicator(indicator: WorkingStatusIndicator): void {
|
|
112
|
+
this.statuslineIndicator = indicator;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
renderWorkingStatus(width: number): string {
|
|
116
|
+
const indicator = this.statuslineIndicator;
|
|
117
|
+
if (!indicator || !this.embedWorkingStatus) return "";
|
|
118
|
+
return indicator.kind === "working" ? indicator.renderSpinnerInBorder(width) : indicator.renderInBorder(width);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class QolEditor extends QolStatusEditor {
|
|
105
123
|
private readonly statusCache: { last?: string } = {};
|
|
106
124
|
private readonly ctx: ExtensionContext;
|
|
107
125
|
|
|
@@ -111,7 +129,7 @@ export class QolEditor extends CustomEditor {
|
|
|
111
129
|
keybindings: KeybindingsManager,
|
|
112
130
|
ctx: ExtensionContext,
|
|
113
131
|
) {
|
|
114
|
-
super(tui, editorTheme, keybindings);
|
|
132
|
+
super(tui, editorTheme, keybindings, { embedWorkingStatus: settingBoolean("statusline.enabled", true, ctx.cwd) });
|
|
115
133
|
this.ctx = ctx;
|
|
116
134
|
}
|
|
117
135
|
|
|
@@ -130,7 +148,7 @@ function isEditorBorderLine(line: string): boolean {
|
|
|
130
148
|
return visible.length > 0 && /^[─━╭╮╰╯┌┐└┘]+$/.test(visible);
|
|
131
149
|
}
|
|
132
150
|
|
|
133
|
-
export class QolCompactPromptEditor extends
|
|
151
|
+
export class QolCompactPromptEditor extends QolStatusEditor {
|
|
134
152
|
private readonly statusCache: { last?: string } = {};
|
|
135
153
|
private readonly inputBottomPaddingLines: number;
|
|
136
154
|
private readonly ctx: ExtensionContext;
|
|
@@ -142,7 +160,7 @@ export class QolCompactPromptEditor extends CustomEditor {
|
|
|
142
160
|
inputBottomPaddingLines: number,
|
|
143
161
|
ctx: ExtensionContext,
|
|
144
162
|
) {
|
|
145
|
-
super(tui, editorTheme, keybindings, { paddingX: 0 });
|
|
163
|
+
super(tui, editorTheme, keybindings, { paddingX: 0, embedWorkingStatus: settingBoolean("statusline.enabled", true, ctx.cwd) });
|
|
146
164
|
this.inputBottomPaddingLines = inputBottomPaddingLines;
|
|
147
165
|
this.ctx = ctx;
|
|
148
166
|
}
|
|
@@ -69,21 +69,49 @@ export function piSettingsPaths(cwd = process.cwd()): string[] {
|
|
|
69
69
|
return projectSettingsTrusted(project) ? [user, project] : [user];
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
73
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Each settings file `piSettingsPaths` names that exists and parses, later
|
|
77
|
+
* files overriding earlier ones. A malformed file is skipped, as Pi's own
|
|
78
|
+
* settings load treats one as empty. */
|
|
79
|
+
function readPiSettingsFiles(cwd?: string): Record<string, any>[] {
|
|
80
|
+
const files: Record<string, any>[] = [];
|
|
74
81
|
for (const settingsPath of piSettingsPaths(cwd)) {
|
|
75
82
|
if (!existsSync(settingsPath)) continue;
|
|
76
83
|
try {
|
|
77
84
|
const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
78
|
-
|
|
79
|
-
if (config && typeof config === "object" && !Array.isArray(config)) Object.assign(merged, config);
|
|
85
|
+
if (isRecord(parsed)) files.push(parsed);
|
|
80
86
|
} catch {
|
|
81
|
-
// Ignore malformed
|
|
87
|
+
// Ignore a malformed settings file.
|
|
82
88
|
}
|
|
83
89
|
}
|
|
90
|
+
return files;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function readPackageConfig(packageId: string, cwd?: string): Record<string, unknown> {
|
|
94
|
+
const merged: Record<string, unknown> = {};
|
|
95
|
+
for (const settings of readPiSettingsFiles(cwd)) {
|
|
96
|
+
const config = settings.kendex?.extensionManager?.config?.[packageId];
|
|
97
|
+
if (isRecord(config)) Object.assign(merged, config);
|
|
98
|
+
}
|
|
84
99
|
return merged;
|
|
85
100
|
}
|
|
86
101
|
|
|
102
|
+
/** Pi core's `compaction.enabled` over the files QOL reads its own config
|
|
103
|
+
* from, resolved as Pi's `SettingsManager.getCompactionEnabled` resolves it:
|
|
104
|
+
* the last file that sets the key decides, a key no file sets is Pi's default,
|
|
105
|
+
* true, and any falsy value turns compaction off. */
|
|
106
|
+
export function piCompactionEnabled(cwd?: string): boolean {
|
|
107
|
+
let enabled: unknown;
|
|
108
|
+
for (const settings of readPiSettingsFiles(cwd)) {
|
|
109
|
+
const compaction = settings.compaction;
|
|
110
|
+
if (isRecord(compaction) && compaction.enabled !== undefined) enabled = compaction.enabled;
|
|
111
|
+
}
|
|
112
|
+
return Boolean(enabled ?? true);
|
|
113
|
+
}
|
|
114
|
+
|
|
87
115
|
export function readkendexConfig(cwd?: string): kendexConfig {
|
|
88
116
|
return readPackageConfig(CONFIG_ID, cwd) as kendexConfig;
|
|
89
117
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { compactionProfile } from "./compaction.js";
|
|
2
|
+
import { autoCompactionGate, compactionProfile, type AutoCompactionGate } from "./compaction.js";
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_AUTO_RENAME_MODEL,
|
|
5
|
+
DEFAULT_BUDGET_GUARD_PERCENT,
|
|
6
|
+
DEFAULT_BUDGET_GUARD_TOKENS,
|
|
5
7
|
DEFAULT_COMPACTION_MODEL,
|
|
6
8
|
DEFAULT_IDLE_COMPACTION_SECONDS,
|
|
7
9
|
DEFAULT_PERMISSION_GATE_PREVIEW_CHARS,
|
|
@@ -15,6 +17,21 @@ import { autoRenameEnabled } from "./session-rename.js";
|
|
|
15
17
|
import { sessionSearchShortcut } from "./session-search/index.js";
|
|
16
18
|
import { boundedSettingNumber, newlineFallbackKey, settingBoolean, settingNumber, settingString } from "./settings.js";
|
|
17
19
|
|
|
20
|
+
function autoCompactionStatus(gate: AutoCompactionGate, enabled: () => string): string {
|
|
21
|
+
switch (gate) {
|
|
22
|
+
case "enabled":
|
|
23
|
+
return enabled();
|
|
24
|
+
case "disabled":
|
|
25
|
+
return "disabled";
|
|
26
|
+
case "pi-compaction-disabled":
|
|
27
|
+
return "disabled by Pi compaction.enabled=false";
|
|
28
|
+
default: {
|
|
29
|
+
const unknownGate: never = gate;
|
|
30
|
+
throw new Error(`pi-qol: unknown auto-compaction gate ${String(unknownGate)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
export function statusMessage(ctx: ExtensionContext): string {
|
|
19
36
|
const labels = attachmentLabels(currentEditorText(ctx), ctx.cwd);
|
|
20
37
|
const searchShortcut = sessionSearchShortcut(ctx.cwd);
|
|
@@ -37,7 +54,8 @@ export function statusMessage(ctx: ExtensionContext): string {
|
|
|
37
54
|
`Handoff prompt review: ${settingBoolean("handoffReviewPrompt", true, ctx.cwd) ? "enabled" : "disabled"}`,
|
|
38
55
|
`Session search: ${settingBoolean("sessionSearch.enabled", true, ctx.cwd) ? `enabled (/search${searchShortcut ? `, ${searchShortcut}` : ""})` : "disabled"}`,
|
|
39
56
|
`Custom compaction: ${settingBoolean("compaction.customEnabled", false, ctx.cwd) ? `enabled (${settingString("compaction.model", DEFAULT_COMPACTION_MODEL, ctx.cwd)}, ${compactionProfile(ctx.cwd)})` : "disabled (Pi default)"}`,
|
|
40
|
-
`
|
|
57
|
+
`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
|
+
`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`)}`,
|
|
41
59
|
`Branch summary override: ${settingBoolean("compaction.branchSummaryEnabled", false, ctx.cwd) ? "enabled" : "disabled"}`,
|
|
42
60
|
`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"}`,
|
|
43
61
|
`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"}`,
|
|
@@ -39,6 +39,34 @@ function formatModelName(ctx: ExtensionContext): string {
|
|
|
39
39
|
return name;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
const PROVIDER_NAMES: Record<string, string> = {
|
|
43
|
+
"github-copilot": "Copilot",
|
|
44
|
+
"openai": "OpenAI",
|
|
45
|
+
"openai-codex": "Codex",
|
|
46
|
+
"anthropic": "Anthropic",
|
|
47
|
+
"pi-claude": "Claude",
|
|
48
|
+
"google": "Google",
|
|
49
|
+
"google-gemini-cli": "Gemini CLI",
|
|
50
|
+
"google-antigravity": "Antigravity",
|
|
51
|
+
"google-vertex": "Vertex AI",
|
|
52
|
+
"amazon-bedrock": "Bedrock",
|
|
53
|
+
"azure-openai-responses": "Azure OpenAI",
|
|
54
|
+
"openrouter": "OpenRouter",
|
|
55
|
+
"vercel-ai-gateway": "Vercel AI Gateway",
|
|
56
|
+
"xai": "xAI",
|
|
57
|
+
"groq": "Groq",
|
|
58
|
+
"cerebras": "Cerebras",
|
|
59
|
+
"mistral": "Mistral",
|
|
60
|
+
"deepseek": "DeepSeek",
|
|
61
|
+
"zai": "Z.AI",
|
|
62
|
+
"ollama": "Ollama",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function formatProviderName(provider: string): string {
|
|
66
|
+
const name = stripAnsi(provider).replace(/[\x00-\x1f\x7f]/g, " ").trim();
|
|
67
|
+
return Object.hasOwn(PROVIDER_NAMES, name) ? PROVIDER_NAMES[name] : name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).replace(/\s+/g, " ").trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
42
70
|
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
43
71
|
type ThinkingThemeToken = "thinkingOff" | "thinkingMinimal" | "thinkingLow" | "thinkingMedium" | "thinkingHigh" | "thinkingXhigh" | "thinkingMax";
|
|
44
72
|
const THINKING_TOKEN: Record<ThinkingLevel, ThinkingThemeToken> = {
|
|
@@ -204,24 +232,23 @@ function cavemanIconTone(mode: string, active: boolean): "muted" | "text" | "suc
|
|
|
204
232
|
}
|
|
205
233
|
}
|
|
206
234
|
|
|
207
|
-
export function renderStatusLine(width: number, ctx: ExtensionContext, git: GitState, pi: ExtensionAPI, theme: Pick<Theme, "fg"
|
|
235
|
+
export function renderStatusLine(width: number, ctx: ExtensionContext, git: GitState, pi: ExtensionAPI, theme: Pick<Theme, "fg">, workingIndicator = ""): string {
|
|
208
236
|
const { label: contextLabel, percent } = statuslineContextInfo(ctx);
|
|
209
|
-
const
|
|
237
|
+
const provider = settingBoolean("statusline.showProvider", true, ctx.cwd) && ctx.model ? formatProviderName(ctx.model.provider) : "";
|
|
238
|
+
const projectChunk = `${git.projectName}${gitBadge(git, settingBoolean("showDirtyMarker", true, ctx.cwd))} ${provider ? `${provider} / ` : ""}${formatModelName(ctx)}`;
|
|
239
|
+
const workingPrefix = workingIndicator ? `${workingIndicator} ` : "";
|
|
210
240
|
const statusSeparator = " / ";
|
|
211
241
|
const thinkingLevel = normalizeThinkingLevel(pi.getThinkingLevel());
|
|
212
242
|
const thinkingChunk = thinkingLevel;
|
|
213
243
|
const contextChunk = ` ${contextLabel}`;
|
|
214
244
|
const account = accountLabel(ctx);
|
|
215
|
-
const accountSegment = account ? `${statusSeparator}${account}` : "";
|
|
216
245
|
const cavemanBridge = readCavemanBridge();
|
|
217
246
|
const cavemanVisible = !!cavemanBridge && (cavemanBridge.isStatusBadgeEnabled?.(ctx.cwd) ?? true);
|
|
218
247
|
const caveman = cavemanVisible ? cavemanBridge : undefined;
|
|
219
248
|
const cavemanActive = caveman?.isActive() ?? false;
|
|
220
249
|
const cavemanGlyph = caveman ? (cavemanActive ? CAVEMAN_ICON_ACTIVE : CAVEMAN_ICON_INACTIVE) : "";
|
|
221
250
|
const cavemanTone = cavemanIconTone(caveman?.getMode() ?? "off", cavemanActive);
|
|
222
|
-
const cavemanSegment = caveman ? `${statusSeparator}${cavemanGlyph}` : "";
|
|
223
251
|
const contextSeparator = caveman ? ` ${statusSeparator}` : "";
|
|
224
|
-
const leftPlain = `${projectChunk}${statusSeparator}${thinkingChunk}${cavemanSegment}${contextSeparator}${contextChunk.trimStart()}${accountSegment}`;
|
|
225
252
|
const percentPlain = percent === null ? `${glyphs(ctx.cwd).ellipsis}%` : `${percent}%`;
|
|
226
253
|
const subagentMarker = subagentStatuslineMarker(ctx.cwd);
|
|
227
254
|
const rightPlain = subagentMarker ? `${percentPlain} ${subagentMarker.plain}` : percentPlain;
|
|
@@ -234,9 +261,9 @@ export function renderStatusLine(width: number, ctx: ExtensionContext, git: GitS
|
|
|
234
261
|
: `${theme.fg("accent", projectChunk)}${separatorColored}${theme.fg(thinkingToken, thinkingChunk)}${theme.fg("accent", contextChunk)}${accountColored}`;
|
|
235
262
|
const right = subagentMarker ? `${theme.fg(percentColor, percentPlain)} ${subagentMarker.styled}` : theme.fg(percentColor, percentPlain);
|
|
236
263
|
const minimumGap = 1;
|
|
237
|
-
const gapWidth = Math.max(minimumGap, width - visibleWidth(
|
|
264
|
+
const gapWidth = Math.max(minimumGap, width - visibleWidth(workingPrefix + leftColored) - visibleWidth(rightPlain) - 2);
|
|
238
265
|
const filled = percent === null ? 0 : Math.round(gapWidth * (percent / 100));
|
|
239
266
|
const empty = Math.max(0, gapWidth - filled);
|
|
240
267
|
const bar = " ".repeat(empty) + theme.fg("warning", glyphs(ctx.cwd).line.repeat(filled));
|
|
241
|
-
return truncateToWidth(`${leftColored} ${bar} ${right}`, width, "");
|
|
268
|
+
return truncateToWidth(`${workingPrefix}${leftColored} ${bar} ${right}`, width, "");
|
|
242
269
|
}
|
package/extensions/qol.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { getQuestionService, readCavemanBridge, type QuestionOpenedEventLike } f
|
|
|
5
5
|
import { BudgetGuardDriver } from "./qol/budget-guard-runtime.js";
|
|
6
6
|
import { ansiGreen } from "./qol/ansi.js";
|
|
7
7
|
import {
|
|
8
|
+
autoCompactionGate,
|
|
8
9
|
budgetGuardTrigger,
|
|
9
10
|
compactionTriggerReason,
|
|
10
11
|
handleQolBranchSummary,
|
|
@@ -120,6 +121,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
120
121
|
let autoRenameInProgress = false;
|
|
121
122
|
let autoRenameGeneration = 0;
|
|
122
123
|
let latestSystemPromptOptions: unknown;
|
|
124
|
+
let statusEditor: QolEditor | QolCompactPromptEditor | undefined;
|
|
123
125
|
|
|
124
126
|
const resetAutoRename = () => {
|
|
125
127
|
autoRenameAttempted = false;
|
|
@@ -216,15 +218,9 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
216
218
|
thinkingTimerStore.enabled = !!ctx?.hasUI && settingBoolean("thinkingTimer.enabled", true, ctx?.cwd);
|
|
217
219
|
};
|
|
218
220
|
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
// terminal viewport (overlay content like /tree, or chat overflow), every tick
|
|
223
|
-
// trips pi-tui's firstChanged < prevViewportTop branch and triggers a full
|
|
224
|
-
// screen + scrollback clear (visible flash). This setting lets the user trade
|
|
225
|
-
// the spinner animation away for a stable display in overflow scenarios.
|
|
226
|
-
// Implementation note: Loader.restartAnimation() bails out when frames.length
|
|
227
|
-
// is <= 1, so a single-frame indicator does NOT start the setInterval at all.
|
|
221
|
+
// Pi's Loader starts no animation timer for a single-frame indicator.
|
|
222
|
+
// Keep static mode available for terminals where frequent redraws flash,
|
|
223
|
+
// whether the indicator is embedded in QOL or in Pi's standalone row.
|
|
228
224
|
const applyWorkingIndicatorMode = (ctx: ExtensionContext): void => {
|
|
229
225
|
if (!ctx.hasUI) return;
|
|
230
226
|
const mode = settingString("workingIndicator.mode", "animated", ctx.cwd);
|
|
@@ -334,7 +330,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
334
330
|
|
|
335
331
|
const scheduleIdleCompaction = (ctx: ExtensionContext) => {
|
|
336
332
|
clearIdleCompactionTimer();
|
|
337
|
-
if (
|
|
333
|
+
if (autoCompactionGate("idle", ctx.cwd) !== "enabled") return;
|
|
338
334
|
const reason = compactionTriggerReason(ctx);
|
|
339
335
|
if (!reason) return;
|
|
340
336
|
const delayMs = Math.max(1, Math.floor(settingNumber("compaction.idleTimeoutSeconds", DEFAULT_IDLE_COMPACTION_SECONDS, ctx.cwd))) * 1000;
|
|
@@ -546,6 +542,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
546
542
|
ctx.ui.setHeader(undefined);
|
|
547
543
|
ctx.ui.setFooter(undefined);
|
|
548
544
|
activeTui = undefined;
|
|
545
|
+
statusEditor = undefined;
|
|
549
546
|
};
|
|
550
547
|
|
|
551
548
|
const maybeNotifyTaskCompletion = (_ctx: ExtensionContext, state: any) => {
|
|
@@ -614,9 +611,10 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
614
611
|
installSessionTitleSync(ctx);
|
|
615
612
|
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
616
613
|
activeTui = tui;
|
|
617
|
-
|
|
614
|
+
statusEditor = settingBoolean("compactPrompt", true, ctx.cwd)
|
|
618
615
|
? new QolCompactPromptEditor(tui, theme, keybindings, Math.max(0, Math.floor(settingNumber("inputBottomPaddingLines", DEFAULT_INPUT_BOTTOM_PADDING_LINES, ctx.cwd))), ctx)
|
|
619
616
|
: new QolEditor(tui, theme, keybindings, ctx);
|
|
617
|
+
return statusEditor;
|
|
620
618
|
});
|
|
621
619
|
if (showStatusline || scheduleEnabled || rateLimitAutoResumeEnabled) {
|
|
622
620
|
const statusWidgetTimer = setTimeout(() => {
|
|
@@ -628,7 +626,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
628
626
|
const lines = rateLimitAutoResumeController.enabled(ctx) ? rateLimitAutoResumeController.renderPreviewLines(width) : [];
|
|
629
627
|
if (scheduleEnabled) lines.push(...scheduleController.renderPreviewLines(width));
|
|
630
628
|
if (budgetGuardStatus) lines.push(truncateToWidth(ansiGreen(`┃ ${budgetGuardStatus}`), width, ""));
|
|
631
|
-
if (showStatusline) lines.push(renderStatusLine(width, ctx, gitState ?? makeFallbackGitState(ctx.cwd), pi, theme));
|
|
629
|
+
if (showStatusline) lines.push(renderStatusLine(width, ctx, gitState ?? makeFallbackGitState(ctx.cwd), pi, theme, statusEditor?.renderWorkingStatus(width)));
|
|
632
630
|
return lines;
|
|
633
631
|
},
|
|
634
632
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillagreen/pi-qol",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
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": [
|
|
@@ -56,6 +56,15 @@
|
|
|
56
56
|
"apply": "reload",
|
|
57
57
|
"requiresReload": true
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
"key": "statusline.showProvider",
|
|
61
|
+
"label": "Show model provider",
|
|
62
|
+
"description": "Show a readable provider name before the model, for example Copilot / GPT 6 Astra.",
|
|
63
|
+
"type": "boolean",
|
|
64
|
+
"default": true,
|
|
65
|
+
"category": "Statusline",
|
|
66
|
+
"apply": "live"
|
|
67
|
+
},
|
|
59
68
|
{
|
|
60
69
|
"key": "statusline.showAccount",
|
|
61
70
|
"label": "Show account email",
|
|
@@ -839,7 +848,7 @@
|
|
|
839
848
|
{
|
|
840
849
|
"key": "compaction.idleEnabled",
|
|
841
850
|
"label": "Idle compaction trigger",
|
|
842
|
-
"description": "Trigger Pi compaction after the session sits idle above the configured token threshold. Inspired by oh-my-pi idle compaction.",
|
|
851
|
+
"description": "Trigger Pi compaction after the session sits idle above the configured token threshold. Never fires while Pi's own compaction.enabled is false. Inspired by oh-my-pi idle compaction.",
|
|
843
852
|
"type": "boolean",
|
|
844
853
|
"default": false,
|
|
845
854
|
"category": "Compaction Idle",
|
|
@@ -884,7 +893,7 @@
|
|
|
884
893
|
{
|
|
885
894
|
"key": "compaction.budgetGuardEnabled",
|
|
886
895
|
"label": "Long-session budget guard",
|
|
887
|
-
"description": "Start bounded compaction when an active session crosses the configured context budget, while giving Pi's built-in compaction priority. Designed for long autonomous runs so transcript growth cannot silently hit provider or buffer limits. Fires once per threshold crossing, not in a loop.",
|
|
896
|
+
"description": "Start bounded compaction when an active session crosses the configured context budget, while giving Pi's built-in compaction priority. Designed for long autonomous runs so transcript growth cannot silently hit provider or buffer limits. Fires once per threshold crossing, not in a loop. Never fires while Pi's own compaction.enabled is false.",
|
|
888
897
|
"type": "boolean",
|
|
889
898
|
"default": true,
|
|
890
899
|
"category": "Compaction Budget Guard",
|
|
@@ -957,7 +966,7 @@
|
|
|
957
966
|
{
|
|
958
967
|
"key": "workingIndicator.mode",
|
|
959
968
|
"label": "Working indicator mode",
|
|
960
|
-
"description": "Control Pi's built-in streaming spinner. The default animated spinner ticks every 80ms; in terminals where rendered content overflows the viewport (chat scrolled past the top, /tree open, etc.) each tick can trigger a pi-tui full-screen redraw and visible flash. Use 'static' for a single non-animating dot that keeps the working
|
|
969
|
+
"description": "Control Pi's built-in streaming spinner. The default animated spinner ticks every 80ms; in terminals where rendered content overflows the viewport (chat scrolled past the top, /tree open, etc.) each tick can trigger a pi-tui full-screen redraw and visible flash. Use 'static' for a single non-animating dot that keeps the working indicator visible without the 80ms timer. It appears before the project name when the QOL statusline is enabled, or in Pi's working row otherwise.",
|
|
961
970
|
"type": "enum",
|
|
962
971
|
"enumValues": [
|
|
963
972
|
"animated",
|
|
@@ -974,7 +983,7 @@
|
|
|
974
983
|
"peerDependencies": {
|
|
975
984
|
"@earendil-works/pi-agent-core": "*",
|
|
976
985
|
"@earendil-works/pi-ai": "*",
|
|
977
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
986
|
+
"@earendil-works/pi-coding-agent": ">=0.86.0",
|
|
978
987
|
"@earendil-works/pi-tui": "*"
|
|
979
988
|
},
|
|
980
989
|
"files": [
|