@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 +9 -2
- package/extensions/qol.ts +80 -12
- package/package.json +3 -3
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,
|
|
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 = `
|
|
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 ?
|
|
1271
|
-
const contextSeparator = caveman ?
|
|
1272
|
-
const leftPlain = `${projectChunk}${thinkingChunk}${cavemanSegment}${contextSeparator}${contextChunk.trimStart()}`;
|
|
1273
|
-
const
|
|
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(
|
|
1277
|
-
: `${theme.fg("accent", projectChunk)}${theme.fg(THINKING_TOKEN[thinkingLevel], thinkingChunk)}${theme.fg("accent", contextChunk)}`;
|
|
1278
|
-
const right = theme.fg(percentColor,
|
|
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
|
|
2460
|
-
return ansiGreen(
|
|
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)
|
|
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
|
|
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": "
|
|
154
|
-
"description": "Render Pi's built-in steering/follow-up queue preview
|
|
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",
|