@vanillagreen/pi-qol 1.0.4 → 1.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
@@ -26,8 +26,8 @@ Restart Pi after installation.
26
26
 
27
27
  ## What it provides
28
28
 
29
- - Compact statusline and `π` prompt editor: shows repo/project, branch/dirty state, model, colorized thinking level, optional Caveman badge, context window size, and remaining context percent while replacing Pi's default footer/editor chrome.
30
- - Reliable multiline input: `Shift+Enter` / `Shift+Return` inserts a newline when the terminal reports it distinctly; `ctrl+j` is the default fallback newline key. `Alt+Enter` is reserved for Pi follow-up messages. When `pi-caveman` is loaded, `Alt+C` cycles Caveman modes.
29
+ - Compact statusline and `π` prompt editor: shows repo/project, branch/dirty state, model, colorized thinking level, optional Caveman badge, context window size, remaining context percent, and (inside `pi-agents-tmux` child panes) an ANSI-background subagent-name badge while replacing Pi's default footer/editor chrome.
30
+ - Reliable multiline input: `Shift+Enter` / `Shift+Return` inserts a newline when the terminal reports it distinctly; `ctrl+j` is the default fallback newline key. `Alt+Enter` is reserved for Pi follow-up messages. QOL can also style Pi's built-in steering/follow-up pending queue preview with an ANSI green left bar while leaving the restore hint muted. When `pi-caveman` is loaded, `Alt+C` cycles Caveman modes.
31
31
  - Compact image placeholders: existing pasted image paths can collapse to `[Image #N]` aliases and are attached on submit.
32
32
  - Session naming: `/rename [name]` sets or shows the friendly session name; automatic first-prompt naming is enabled by default.
33
33
  - Context usage: `/context` prints an inline Claude-style context-window visualization with estimated Pi/model category breakdowns.
@@ -66,6 +66,13 @@ Settings are exposed through `pi-extension-manager` under **QOL**.
66
66
  - `inputBottomPaddingLines`: blank lines below the compact prompt; default `0`.
67
67
  - Git display knobs: `gitRefreshTimeoutMs`, `showDirtyMarker`.
68
68
  - If `pi-caveman` is loaded, QOL reads its bridge to show a Caveman icon in the statusline and to make `Alt+C` cycle Caveman modes. The Caveman package's `showStatusBadge` setting also controls this QOL badge.
69
+ - If `pi-agents-tmux` is loaded inside a child pane, QOL reads its bridge/env metadata to show a trailing subagent-name badge after the context percent. The badge uses the agent `color:` frontmatter (`red`, `green`, `yellow`, `blue`, `magenta`, `cyan`; aliases `orange`, `purple`/`violet`, `teal`) or falls back to automatic color cycling.
70
+
71
+ ### Input
72
+
73
+ - `newlineOnShiftEnter`: intercept distinguishable `Shift+Enter` / `Shift+Return` and insert a newline; default on.
74
+ - `newlineFallbackKey`: alternate newline key for terminals that cannot distinguish shifted enter; default `ctrl+j`, or `none` to disable.
75
+ - `pendingQueue.asciiGreen`: style Pi's built-in steering/follow-up pending queue preview with an ANSI green heavy left bar while keeping the Alt+Up restore hint in the theme hint color with a two-space indent; default on.
69
76
 
70
77
  ### Session auto-rename
71
78
 
package/extensions/qol.ts CHANGED
@@ -21,9 +21,26 @@ const QUESTION_SERVICE_SYMBOL = Symbol.for("vstack.pi-questions.service");
21
21
  const QOL_NOTIFICATION_SERVICE_SYMBOL = Symbol.for("vstack.pi-qol.notification-service");
22
22
  const VSTACK_MODAL_LOCK_SYMBOL = Symbol.for("vstack.pi.modal-lock");
23
23
  const CAVEMAN_BRIDGE_SYMBOL = Symbol.for("vstack.pi.caveman");
24
+ const PI_AGENTS_STATUSLINE_SYMBOL = Symbol.for("vstack.pi-agents-tmux.statusline");
24
25
  const CAVEMAN_ICON_ACTIVE = "\uee9a";
25
26
  const CAVEMAN_ICON_INACTIVE = "\u{f19e0}";
26
27
 
28
+ type AgentAsciiColor = "red" | "green" | "yellow" | "blue" | "magenta" | "cyan";
29
+
30
+ interface PiAgentsStatuslineBridge {
31
+ getCurrentSubagent(cwd?: string): { name: string; color?: string } | undefined;
32
+ }
33
+
34
+ const AGENT_ASCII_COLOR_SEQUENCE: AgentAsciiColor[] = ["magenta", "green", "blue", "cyan", "yellow", "red"];
35
+ const AGENT_ASCII_BG: Record<AgentAsciiColor, number> = {
36
+ red: 41,
37
+ green: 42,
38
+ yellow: 43,
39
+ blue: 44,
40
+ magenta: 45,
41
+ cyan: 46,
42
+ };
43
+
27
44
  interface CavemanBridge {
28
45
  isActive(): boolean;
29
46
  getMode(): string;
@@ -41,6 +58,52 @@ function readCavemanBridge(): CavemanBridge | undefined {
41
58
  const value = host[CAVEMAN_BRIDGE_SYMBOL];
42
59
  return value && typeof value === "object" ? (value as CavemanBridge) : undefined;
43
60
  }
61
+
62
+ function readPiAgentsStatuslineBridge(): PiAgentsStatuslineBridge | undefined {
63
+ const host = globalThis as unknown as Record<PropertyKey, unknown>;
64
+ const value = host[PI_AGENTS_STATUSLINE_SYMBOL];
65
+ return value && typeof value === "object" && typeof (value as PiAgentsStatuslineBridge).getCurrentSubagent === "function"
66
+ ? (value as PiAgentsStatuslineBridge)
67
+ : undefined;
68
+ }
69
+
70
+ function normalizeAgentAsciiColor(value: string | undefined): AgentAsciiColor | undefined {
71
+ const normalized = value?.trim().toLowerCase().replace(/[^a-z]/g, "");
72
+ switch (normalized) {
73
+ case "red": return "red";
74
+ case "green": return "green";
75
+ case "yellow":
76
+ case "orange": return "yellow";
77
+ case "blue": return "blue";
78
+ case "magenta":
79
+ case "purple":
80
+ case "violet": return "magenta";
81
+ case "cyan":
82
+ case "teal": return "cyan";
83
+ default: return undefined;
84
+ }
85
+ }
86
+
87
+ function fallbackAgentAsciiColor(name: string): AgentAsciiColor {
88
+ let hash = 0;
89
+ for (const char of name) hash = ((hash * 31) + char.charCodeAt(0)) >>> 0;
90
+ return AGENT_ASCII_COLOR_SEQUENCE[hash % AGENT_ASCII_COLOR_SEQUENCE.length] ?? "magenta";
91
+ }
92
+
93
+ function ansiAgentBg(color: AgentAsciiColor, text: string): string {
94
+ return `\x1b[30;${AGENT_ASCII_BG[color]}m${text}\x1b[39;49m`;
95
+ }
96
+
97
+ function subagentStatuslineMarker(cwd: string, maxInnerWidth = 24): { plain: string; styled: string } | undefined {
98
+ const bridgeInfo = readPiAgentsStatuslineBridge()?.getCurrentSubagent(cwd);
99
+ const envName = process.env.PI_SUBAGENT_CHILD_AGENT?.trim();
100
+ const rawName = bridgeInfo?.name?.trim() || envName;
101
+ if (!rawName) return undefined;
102
+ const color = normalizeAgentAsciiColor(bridgeInfo?.color) ?? normalizeAgentAsciiColor(process.env.PI_SUBAGENT_CHILD_COLOR) ?? fallbackAgentAsciiColor(rawName);
103
+ const inner = truncateToWidth(rawName, Math.max(1, maxInnerWidth), "…");
104
+ const plain = ` ${inner} `;
105
+ return { plain, styled: ansiAgentBg(color, plain) };
106
+ }
44
107
  const THINKING_TIMER_STORE_SYMBOL = Symbol.for("vstack.pi-qol.thinking-timer.store");
45
108
  const THINKING_TIMER_PATCH_SYMBOL = Symbol.for("vstack.pi-qol.thinking-timer.patch");
46
109
  const SESSION_SEARCH_PENDING_SYMBOL = Symbol.for("vstack.pi-qol.session-search.pending-context");
@@ -1257,25 +1320,29 @@ function cavemanIconTone(mode: string, active: boolean): "muted" | "text" | "suc
1257
1320
 
1258
1321
  function renderStatusLine(width: number, ctx: ExtensionContext, git: GitState, pi: ExtensionAPI, theme: Pick<Theme, "fg">): string {
1259
1322
  const { label: contextLabel, percent } = statuslineContextInfo(ctx);
1260
- const projectChunk = `${git.projectName}${gitBadge(git, settingBoolean("showDirtyMarker", true, ctx.cwd))} ${formatModelName(ctx)} / `;
1323
+ const projectChunk = `${git.projectName}${gitBadge(git, settingBoolean("showDirtyMarker", true, ctx.cwd))} ${formatModelName(ctx)}`;
1324
+ const statusSeparator = " / ";
1261
1325
  const thinkingLevel = normalizeThinkingLevel(pi.getThinkingLevel());
1262
1326
  const thinkingChunk = thinkingLevel;
1263
- const contextChunk = ` (${contextLabel})`;
1327
+ const contextChunk = ` ${contextLabel}`;
1264
1328
  const cavemanBridge = readCavemanBridge();
1265
1329
  const cavemanVisible = !!cavemanBridge && (cavemanBridge.isStatusBadgeEnabled?.(ctx.cwd) ?? true);
1266
1330
  const caveman = cavemanVisible ? cavemanBridge : undefined;
1267
1331
  const cavemanActive = caveman?.isActive() ?? false;
1268
1332
  const cavemanGlyph = caveman ? (cavemanActive ? CAVEMAN_ICON_ACTIVE : CAVEMAN_ICON_INACTIVE) : "";
1269
1333
  const cavemanTone = cavemanIconTone(caveman?.getMode() ?? "off", cavemanActive);
1270
- const cavemanSegment = caveman ? ` / ${cavemanGlyph}` : "";
1271
- const contextSeparator = caveman ? " / " : "";
1272
- const leftPlain = `${projectChunk}${thinkingChunk}${cavemanSegment}${contextSeparator}${contextChunk.trimStart()}`;
1273
- const rightPlain = percent === null ? "…%" : `${percent}%`;
1334
+ const cavemanSegment = caveman ? `${statusSeparator}${cavemanGlyph}` : "";
1335
+ const contextSeparator = caveman ? ` ${statusSeparator}` : "";
1336
+ const leftPlain = `${projectChunk}${statusSeparator}${thinkingChunk}${cavemanSegment}${contextSeparator}${contextChunk.trimStart()}`;
1337
+ const percentPlain = percent === null ? "…%" : `${percent}%`;
1338
+ const subagentMarker = subagentStatuslineMarker(ctx.cwd);
1339
+ const rightPlain = subagentMarker ? `${percentPlain} ${subagentMarker.plain}` : percentPlain;
1274
1340
  const percentColor = percent === null ? "muted" : percent <= 15 ? "error" : percent <= 30 ? "warning" : "success";
1341
+ const separatorColored = theme.fg("muted", statusSeparator);
1275
1342
  const leftColored = caveman
1276
- ? `${theme.fg("accent", projectChunk)}${theme.fg(THINKING_TOKEN[thinkingLevel], thinkingChunk)}${theme.fg("accent", " / ")}${theme.fg(cavemanTone, cavemanGlyph)}${theme.fg("accent", `${contextSeparator}${contextChunk.trimStart()}`)}`
1277
- : `${theme.fg("accent", projectChunk)}${theme.fg(THINKING_TOKEN[thinkingLevel], thinkingChunk)}${theme.fg("accent", contextChunk)}`;
1278
- const right = theme.fg(percentColor, rightPlain);
1343
+ ? `${theme.fg("accent", projectChunk)}${separatorColored}${theme.fg(THINKING_TOKEN[thinkingLevel], thinkingChunk)}${separatorColored}${theme.fg(cavemanTone, cavemanGlyph)}${theme.fg("muted", contextSeparator)}${theme.fg("accent", contextChunk.trimStart())}`
1344
+ : `${theme.fg("accent", projectChunk)}${separatorColored}${theme.fg(THINKING_TOKEN[thinkingLevel], thinkingChunk)}${theme.fg("accent", contextChunk)}`;
1345
+ const right = subagentMarker ? `${theme.fg(percentColor, percentPlain)} ${subagentMarker.styled}` : theme.fg(percentColor, percentPlain);
1279
1346
  const minimumGap = 1;
1280
1347
  const gapWidth = Math.max(minimumGap, width - visibleWidth(leftPlain) - visibleWidth(rightPlain) - 2);
1281
1348
  const filled = percent === null ? 0 : Math.round(gapWidth * (percent / 100));
@@ -2456,8 +2523,8 @@ function isPendingQueueHintText(text: string): boolean {
2456
2523
  return plain.startsWith("↳ ") && plain.includes("queued messages");
2457
2524
  }
2458
2525
 
2459
- function pendingQueueLine(text: string): string {
2460
- return ansiGreen(`| ${text}`);
2526
+ function pendingQueuePreviewLine(text: string): string {
2527
+ return ansiGreen(`┃ ${text}`);
2461
2528
  }
2462
2529
 
2463
2530
  function isQueuedMessageStatusText(text: string): boolean {
@@ -2500,7 +2567,8 @@ function installPendingQueueThemePatch(ctx: ExtensionContext): void {
2500
2567
  proto[PENDING_QUEUE_THEME_PATCH_SYMBOL] = patch;
2501
2568
  proto.fg = function patchedQolFg(this: Theme, token: string, text: string): string {
2502
2569
  if (token === "dim" && typeof text === "string" && settingBoolean("pendingQueue.asciiGreen", true, patch.cwd)) {
2503
- if (isPendingQueuePreviewText(text) || isPendingQueueHintText(text)) return pendingQueueLine(text);
2570
+ if (isPendingQueuePreviewText(text)) return pendingQueuePreviewLine(text);
2571
+ if (isPendingQueueHintText(text)) return (patch.originalFg as (this: Theme, token: string, text: string) => string).call(this, token, ` ${text}`);
2504
2572
  }
2505
2573
  return (patch.originalFg as (this: Theme, token: string, text: string) => string).call(this, token, text);
2506
2574
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanillagreen/pi-qol",
3
- "version": "1.0.4",
3
+ "version": "1.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, handoff, permission prompts, notifications, custom compaction, and a collapsed-thinking timer.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -150,8 +150,8 @@
150
150
  },
151
151
  {
152
152
  "key": "pendingQueue.asciiGreen",
153
- "label": "Green pending queue preview",
154
- "description": "Render Pi's built-in steering/follow-up queue preview and Alt+Up hint in ANSI green with a left | queue bar instead of the dim hint color.",
153
+ "label": "Style pending queue preview",
154
+ "description": "Render Pi's built-in steering/follow-up queue preview in ANSI green with a heavy left queue bar, while keeping the Alt+Up restore hint in the theme hint color with a two-space indent.",
155
155
  "type": "boolean",
156
156
  "default": true,
157
157
  "category": "Input",