@vanillagreen/pi-qol 2.0.1 → 2.1.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
@@ -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`, `replaceFooter`, `compactPrompt`, `showSessionNameTitle`, `showSessionNameWindow`, `inputBottomPaddingLines`, `gitRefreshTimeoutMs`, `showDirtyMarker`, `newlineOnShiftEnter`, `newlineFallbackKey`, `pendingQueue.asciiGreen`, `showImageChips`, `showAttachmentCountInStatus`.
39
+ - Statusline and editor: `statusline.enabled`, `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).
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  CAVEMAN_BRIDGE_SYMBOL,
3
+ CLAUDE_BILLING_IDENTITY_SYMBOL,
3
4
  PI_AGENTS_STATUSLINE_SYMBOL,
4
5
  QUESTION_SERVICE_SYMBOL,
5
6
  KENDEX_MODAL_LOCK_SYMBOL,
@@ -21,6 +22,18 @@ export interface PiAgentsStatuslineBridge {
21
22
  getCurrentSubagent(cwd?: string): { name: string; color?: string } | undefined;
22
23
  }
23
24
 
25
+ /** The Claude bridge's published account surface. It answers with a login
26
+ * email only when the SDK confirmed one for the child that ran the request:
27
+ * an API key or a Bedrock, Vertex, Foundry, Anthropic-AWS or Mantle backend
28
+ * authenticates as something no config directory names, and a companion
29
+ * account router can rotate the profile per request without the environment
30
+ * changing. Reading a config directory here instead would name the wrong
31
+ * account in every one of those cases. */
32
+ export interface ClaudeBillingIdentityBridge {
33
+ version: 1;
34
+ currentLoginEmail(sessionId: string | undefined): string | undefined;
35
+ }
36
+
24
37
  export interface QuestionRequestLike {
25
38
  header?: string;
26
39
  question?: string;
@@ -47,6 +60,12 @@ export function readCavemanBridge(): CavemanBridge | undefined {
47
60
  return value && typeof value === "object" ? (value as CavemanBridge) : undefined;
48
61
  }
49
62
 
63
+ export function readClaudeBillingIdentityBridge(): ClaudeBillingIdentityBridge | undefined {
64
+ const host = globalThis as unknown as Record<PropertyKey, unknown>;
65
+ const value = host[CLAUDE_BILLING_IDENTITY_SYMBOL] as ClaudeBillingIdentityBridge | undefined;
66
+ return value?.version === 1 && typeof value.currentLoginEmail === "function" ? value : undefined;
67
+ }
68
+
50
69
  export function readPiAgentsStatuslineBridge(): PiAgentsStatuslineBridge | undefined {
51
70
  const host = globalThis as unknown as Record<PropertyKey, unknown>;
52
71
  const value = host[PI_AGENTS_STATUSLINE_SYMBOL];
@@ -15,6 +15,7 @@ export const QOL_NOTIFICATION_SERVICE_SYMBOL = Symbol.for("kendex.pi-qol.notific
15
15
  export const KENDEX_MODAL_LOCK_SYMBOL = Symbol.for("kendex.pi.modal-lock");
16
16
  export const CAVEMAN_BRIDGE_SYMBOL = Symbol.for("kendex.pi.caveman");
17
17
  export const PI_AGENTS_STATUSLINE_SYMBOL = Symbol.for("kendex.pi-agents-tmux.statusline");
18
+ export const CLAUDE_BILLING_IDENTITY_SYMBOL = Symbol.for("kendex.pi.claude-bridge.billing-identity.v1");
18
19
 
19
20
  export const CAVEMAN_ICON_ACTIVE = "\uee9a";
20
21
  export const CAVEMAN_ICON_INACTIVE = "\u{f19e0}";
@@ -4,7 +4,7 @@ import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-c
4
4
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
5
5
  import { subagentStatuslineMarker } from "./agent-statusline.js";
6
6
  import { stripAnsi } from "./ansi.js";
7
- import { readCavemanBridge } from "./bridges.js";
7
+ import { readCavemanBridge, readClaudeBillingIdentityBridge } from "./bridges.js";
8
8
  import { glyphs } from "./glyphs.js";
9
9
  import { CAVEMAN_ICON_ACTIVE, CAVEMAN_ICON_INACTIVE } from "./constants.js";
10
10
  import { settingBoolean, settingNumber } from "./settings.js";
@@ -89,6 +89,17 @@ function statuslineContextInfo(ctx: ExtensionContext): { label: string; percent:
89
89
  return { label: formatWindow(contextWindow), percent: 100 - usedPercent };
90
90
  }
91
91
 
92
+ /** The account segment for an active Claude-bridge model, or "" otherwise.
93
+ * A lane can retain its last Claude identity after the user selects another
94
+ * provider, which does not bill that account. The Claude bridge owns the
95
+ * judgement of what counts as a confirmed login; this reads its answer and
96
+ * displays it only beside its model. */
97
+ function accountLabel(ctx: ExtensionContext): string {
98
+ if (!settingBoolean("statusline.showAccount", true, ctx.cwd)) return "";
99
+ if (ctx.model?.provider !== "pi-claude") return "";
100
+ return readClaudeBillingIdentityBridge()?.currentLoginEmail(ctx.sessionManager.getSessionId()) ?? "";
101
+ }
102
+
92
103
  function gitBadge(state: GitState, showDirtyMarker: boolean): string {
93
104
  if (!state.branch) return "";
94
105
  const icon = state.inLinkedWorktree || state.branch !== "main" ? `🌳 ${state.branch}` : "🦀";
@@ -200,6 +211,8 @@ export function renderStatusLine(width: number, ctx: ExtensionContext, git: GitS
200
211
  const thinkingLevel = normalizeThinkingLevel(pi.getThinkingLevel());
201
212
  const thinkingChunk = thinkingLevel;
202
213
  const contextChunk = ` ${contextLabel}`;
214
+ const account = accountLabel(ctx);
215
+ const accountSegment = account ? `${statusSeparator}${account}` : "";
203
216
  const cavemanBridge = readCavemanBridge();
204
217
  const cavemanVisible = !!cavemanBridge && (cavemanBridge.isStatusBadgeEnabled?.(ctx.cwd) ?? true);
205
218
  const caveman = cavemanVisible ? cavemanBridge : undefined;
@@ -208,16 +221,17 @@ export function renderStatusLine(width: number, ctx: ExtensionContext, git: GitS
208
221
  const cavemanTone = cavemanIconTone(caveman?.getMode() ?? "off", cavemanActive);
209
222
  const cavemanSegment = caveman ? `${statusSeparator}${cavemanGlyph}` : "";
210
223
  const contextSeparator = caveman ? ` ${statusSeparator}` : "";
211
- const leftPlain = `${projectChunk}${statusSeparator}${thinkingChunk}${cavemanSegment}${contextSeparator}${contextChunk.trimStart()}`;
224
+ const leftPlain = `${projectChunk}${statusSeparator}${thinkingChunk}${cavemanSegment}${contextSeparator}${contextChunk.trimStart()}${accountSegment}`;
212
225
  const percentPlain = percent === null ? `${glyphs(ctx.cwd).ellipsis}%` : `${percent}%`;
213
226
  const subagentMarker = subagentStatuslineMarker(ctx.cwd);
214
227
  const rightPlain = subagentMarker ? `${percentPlain} ${subagentMarker.plain}` : percentPlain;
215
228
  const percentColor = percent === null ? "muted" : percent <= 15 ? "error" : percent <= 30 ? "warning" : "success";
216
229
  const separatorColored = theme.fg("muted", statusSeparator);
217
230
  const thinkingToken = thinkingThemeToken(thinkingLevel) as Parameters<Theme["fg"]>[0];
231
+ const accountColored = account ? `${separatorColored}${theme.fg("muted", account)}` : "";
218
232
  const leftColored = caveman
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)}`;
233
+ ? `${theme.fg("accent", projectChunk)}${separatorColored}${theme.fg(thinkingToken, thinkingChunk)}${separatorColored}${theme.fg(cavemanTone, cavemanGlyph)}${theme.fg("muted", contextSeparator)}${theme.fg("accent", contextChunk.trimStart())}${accountColored}`
234
+ : `${theme.fg("accent", projectChunk)}${separatorColored}${theme.fg(thinkingToken, thinkingChunk)}${theme.fg("accent", contextChunk)}${accountColored}`;
221
235
  const right = subagentMarker ? `${theme.fg(percentColor, percentPlain)} ${subagentMarker.styled}` : theme.fg(percentColor, percentPlain);
222
236
  const minimumGap = 1;
223
237
  const gapWidth = Math.max(minimumGap, width - visibleWidth(leftPlain) - visibleWidth(rightPlain) - 2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanillagreen/pi-qol",
3
- "version": "2.0.1",
3
+ "version": "2.1.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.showAccount",
61
+ "label": "Show account email",
62
+ "description": "Append the Claude login this session authenticated as, as the Claude bridge reports it. Nothing is shown before the first turn, or when the request used an API key or a third-party backend such as Bedrock or Vertex.",
63
+ "type": "boolean",
64
+ "default": true,
65
+ "category": "Statusline",
66
+ "apply": "live"
67
+ },
59
68
  {
60
69
  "key": "replaceFooter",
61
70
  "label": "Replace built-in footer",