@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 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).
@@ -101,7 +101,25 @@ function syncQolEditorStatus(
101
101
  }
102
102
  }
103
103
 
104
- export class QolEditor extends CustomEditor {
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 CustomEditor {
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">): string {
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 projectChunk = `${git.projectName}${gitBadge(git, settingBoolean("showDirtyMarker", true, ctx.cwd))} ${formatModelName(ctx)}`;
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(leftPlain) - visibleWidth(rightPlain) - 2);
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
- // Working indicator mode.
221
- // The built-in pi-tui Loader ticks every 80ms during streaming. Each tick
222
- // mutates a line in statusContainer; once total rendered content exceeds the
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
- return settingBoolean("compactPrompt", true, ctx.cwd)
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.1.1",
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 row visible without the 80ms timer.",
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.80.4",
986
+ "@earendil-works/pi-coding-agent": ">=0.86.0",
978
987
  "@earendil-works/pi-tui": "*"
979
988
  },
980
989
  "files": [