@vanillagreen/pi-qol 2.1.1 → 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 +5 -3
- package/extensions/qol/editor.ts +22 -4
- package/extensions/qol/statusline.ts +34 -7
- package/extensions/qol.ts +8 -11
- package/package.json +12 -3
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).
|
|
@@ -47,4 +47,6 @@ Open `/extensions:settings`; settings appear under the **QOL** tab. Project sett
|
|
|
47
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.
|
|
48
48
|
- Thinking: `thinkingLabel.text`, `thinkingTimer.enabled`, `workingIndicator.mode` (`static` if the animated indicator flashes).
|
|
49
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
|
+
|
|
50
52
|
Maintainer notes are in [DEVELOPMENT.md](DEVELOPMENT.md).
|
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
|
}
|
|
@@ -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
|
@@ -121,6 +121,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
121
121
|
let autoRenameInProgress = false;
|
|
122
122
|
let autoRenameGeneration = 0;
|
|
123
123
|
let latestSystemPromptOptions: unknown;
|
|
124
|
+
let statusEditor: QolEditor | QolCompactPromptEditor | undefined;
|
|
124
125
|
|
|
125
126
|
const resetAutoRename = () => {
|
|
126
127
|
autoRenameAttempted = false;
|
|
@@ -217,15 +218,9 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
217
218
|
thinkingTimerStore.enabled = !!ctx?.hasUI && settingBoolean("thinkingTimer.enabled", true, ctx?.cwd);
|
|
218
219
|
};
|
|
219
220
|
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
// terminal viewport (overlay content like /tree, or chat overflow), every tick
|
|
224
|
-
// trips pi-tui's firstChanged < prevViewportTop branch and triggers a full
|
|
225
|
-
// screen + scrollback clear (visible flash). This setting lets the user trade
|
|
226
|
-
// the spinner animation away for a stable display in overflow scenarios.
|
|
227
|
-
// Implementation note: Loader.restartAnimation() bails out when frames.length
|
|
228
|
-
// 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.
|
|
229
224
|
const applyWorkingIndicatorMode = (ctx: ExtensionContext): void => {
|
|
230
225
|
if (!ctx.hasUI) return;
|
|
231
226
|
const mode = settingString("workingIndicator.mode", "animated", ctx.cwd);
|
|
@@ -547,6 +542,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
547
542
|
ctx.ui.setHeader(undefined);
|
|
548
543
|
ctx.ui.setFooter(undefined);
|
|
549
544
|
activeTui = undefined;
|
|
545
|
+
statusEditor = undefined;
|
|
550
546
|
};
|
|
551
547
|
|
|
552
548
|
const maybeNotifyTaskCompletion = (_ctx: ExtensionContext, state: any) => {
|
|
@@ -615,9 +611,10 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
615
611
|
installSessionTitleSync(ctx);
|
|
616
612
|
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
617
613
|
activeTui = tui;
|
|
618
|
-
|
|
614
|
+
statusEditor = settingBoolean("compactPrompt", true, ctx.cwd)
|
|
619
615
|
? new QolCompactPromptEditor(tui, theme, keybindings, Math.max(0, Math.floor(settingNumber("inputBottomPaddingLines", DEFAULT_INPUT_BOTTOM_PADDING_LINES, ctx.cwd))), ctx)
|
|
620
616
|
: new QolEditor(tui, theme, keybindings, ctx);
|
|
617
|
+
return statusEditor;
|
|
621
618
|
});
|
|
622
619
|
if (showStatusline || scheduleEnabled || rateLimitAutoResumeEnabled) {
|
|
623
620
|
const statusWidgetTimer = setTimeout(() => {
|
|
@@ -629,7 +626,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
629
626
|
const lines = rateLimitAutoResumeController.enabled(ctx) ? rateLimitAutoResumeController.renderPreviewLines(width) : [];
|
|
630
627
|
if (scheduleEnabled) lines.push(...scheduleController.renderPreviewLines(width));
|
|
631
628
|
if (budgetGuardStatus) lines.push(truncateToWidth(ansiGreen(`┃ ${budgetGuardStatus}`), width, ""));
|
|
632
|
-
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)));
|
|
633
630
|
return lines;
|
|
634
631
|
},
|
|
635
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",
|
|
@@ -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": [
|