@vanillagreen/pi-qol 1.0.2 → 1.0.4
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 +3 -2
- package/extensions/qol.ts +95 -6
- package/package.json +1 -1
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, thinking level, 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.
|
|
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.
|
|
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.
|
|
@@ -65,6 +65,7 @@ Settings are exposed through `pi-extension-manager` under **QOL**.
|
|
|
65
65
|
- `showSessionNameTitle`: show the named session above the prompt, or in tmux's pane title/border; default on.
|
|
66
66
|
- `inputBottomPaddingLines`: blank lines below the compact prompt; default `0`.
|
|
67
67
|
- Git display knobs: `gitRefreshTimeoutMs`, `showDirtyMarker`.
|
|
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.
|
|
68
69
|
|
|
69
70
|
### Session auto-rename
|
|
70
71
|
|
package/extensions/qol.ts
CHANGED
|
@@ -20,6 +20,27 @@ const IMAGE_PATH_PATTERN = /(^|[\s(\[{<"'`])(@?(?:~|\.\.?|\/)[^\s)\]}>"'`]+?\.(?
|
|
|
20
20
|
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
|
+
const CAVEMAN_BRIDGE_SYMBOL = Symbol.for("vstack.pi.caveman");
|
|
24
|
+
const CAVEMAN_ICON_ACTIVE = "\uee9a";
|
|
25
|
+
const CAVEMAN_ICON_INACTIVE = "\u{f19e0}";
|
|
26
|
+
|
|
27
|
+
interface CavemanBridge {
|
|
28
|
+
isActive(): boolean;
|
|
29
|
+
getMode(): string;
|
|
30
|
+
getConfiguredMode?(cwd?: string): string;
|
|
31
|
+
getLastActiveMode(): string;
|
|
32
|
+
hasSessionOverride?(): boolean;
|
|
33
|
+
isStatusBadgeEnabled?(cwd?: string): boolean;
|
|
34
|
+
cycleMode?(cwd?: string): string;
|
|
35
|
+
setMode?(mode: string, cwd?: string): string | undefined;
|
|
36
|
+
subscribe(listener: () => void): () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readCavemanBridge(): CavemanBridge | undefined {
|
|
40
|
+
const host = globalThis as unknown as Record<PropertyKey, unknown>;
|
|
41
|
+
const value = host[CAVEMAN_BRIDGE_SYMBOL];
|
|
42
|
+
return value && typeof value === "object" ? (value as CavemanBridge) : undefined;
|
|
43
|
+
}
|
|
23
44
|
const THINKING_TIMER_STORE_SYMBOL = Symbol.for("vstack.pi-qol.thinking-timer.store");
|
|
24
45
|
const THINKING_TIMER_PATCH_SYMBOL = Symbol.for("vstack.pi-qol.thinking-timer.patch");
|
|
25
46
|
const SESSION_SEARCH_PENDING_SYMBOL = Symbol.for("vstack.pi-qol.session-search.pending-context");
|
|
@@ -940,6 +961,16 @@ function statusText(ctx: ExtensionContext, text: string): string | undefined {
|
|
|
940
961
|
}
|
|
941
962
|
|
|
942
963
|
function handleQolEditorInput(editor: CustomEditor, ctx: ExtensionContext, data: string): void {
|
|
964
|
+
if (matchesKey(data, "alt+c")) {
|
|
965
|
+
const caveman = readCavemanBridge();
|
|
966
|
+
if (caveman?.cycleMode) {
|
|
967
|
+
const next = caveman.cycleMode(ctx.cwd);
|
|
968
|
+
if (typeof next === "string") {
|
|
969
|
+
try { ctx.ui.notify(next === "off" ? "Caveman off." : `Caveman ${next} active.`, "info"); } catch { /* notify is best-effort */ }
|
|
970
|
+
}
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
943
974
|
const fallback = newlineFallbackKey(ctx.cwd);
|
|
944
975
|
const newlineEnabled = settingBoolean("newlineOnShiftEnter", true, ctx.cwd);
|
|
945
976
|
const isShiftEnter = matchesKey(data, "shift+enter") || matchesKey(data, "shift+return");
|
|
@@ -1068,9 +1099,9 @@ function repoNameFromRemote(remote: string): string | undefined {
|
|
|
1068
1099
|
return match?.[1];
|
|
1069
1100
|
}
|
|
1070
1101
|
|
|
1071
|
-
function
|
|
1102
|
+
function formatModelName(ctx: ExtensionContext): string {
|
|
1072
1103
|
const model = ctx.model;
|
|
1073
|
-
if (!model) return
|
|
1104
|
+
if (!model) return "no model";
|
|
1074
1105
|
let name = model.name || model.id;
|
|
1075
1106
|
name = name.replace(/^Claude\s+/i, "");
|
|
1076
1107
|
name = name.replace(/^claude[-_]/i, "");
|
|
@@ -1082,7 +1113,29 @@ function formatModel(ctx: ExtensionContext, pi: ExtensionAPI): string {
|
|
|
1082
1113
|
name = name.replace(/\bhaiku\b/i, "Haiku");
|
|
1083
1114
|
name = name.replace(/\s+/g, " ").trim();
|
|
1084
1115
|
name = name.replace(/\b(Opus|Sonnet|Haiku) (\d) (\d)\b/, "$1 $2.$3");
|
|
1085
|
-
return
|
|
1116
|
+
return name;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
1120
|
+
const THINKING_TOKEN: Record<ThinkingLevel, "thinkingOff" | "thinkingMinimal" | "thinkingLow" | "thinkingMedium" | "thinkingHigh" | "thinkingXhigh"> = {
|
|
1121
|
+
off: "thinkingOff",
|
|
1122
|
+
minimal: "thinkingMinimal",
|
|
1123
|
+
low: "thinkingLow",
|
|
1124
|
+
medium: "thinkingMedium",
|
|
1125
|
+
high: "thinkingHigh",
|
|
1126
|
+
xhigh: "thinkingXhigh",
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
function normalizeThinkingLevel(value: string | undefined): ThinkingLevel {
|
|
1130
|
+
switch ((value ?? "").toLowerCase()) {
|
|
1131
|
+
case "off": return "off";
|
|
1132
|
+
case "minimal": return "minimal";
|
|
1133
|
+
case "low": return "low";
|
|
1134
|
+
case "medium": return "medium";
|
|
1135
|
+
case "high": return "high";
|
|
1136
|
+
case "xhigh": return "xhigh";
|
|
1137
|
+
default: return "off";
|
|
1138
|
+
}
|
|
1086
1139
|
}
|
|
1087
1140
|
|
|
1088
1141
|
function formatWindow(tokens: number | undefined): string {
|
|
@@ -1191,19 +1244,44 @@ function setTmuxWindowOption(target: string, option: string, value: string): voi
|
|
|
1191
1244
|
execFile("tmux", ["set-option", "-wq", "-t", target, option, value], { timeout: 1000 }, () => undefined);
|
|
1192
1245
|
}
|
|
1193
1246
|
|
|
1247
|
+
function cavemanIconTone(mode: string, active: boolean): "muted" | "text" | "success" | "thinkingHigh" | "error" {
|
|
1248
|
+
if (!active) return "muted";
|
|
1249
|
+
switch (mode) {
|
|
1250
|
+
case "micro": return "text";
|
|
1251
|
+
case "lite": return "success";
|
|
1252
|
+
case "full": return "thinkingHigh";
|
|
1253
|
+
case "ultra": return "error";
|
|
1254
|
+
default: return "muted";
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1194
1258
|
function renderStatusLine(width: number, ctx: ExtensionContext, git: GitState, pi: ExtensionAPI, theme: Pick<Theme, "fg">): string {
|
|
1195
1259
|
const { label: contextLabel, percent } = statuslineContextInfo(ctx);
|
|
1196
|
-
const
|
|
1260
|
+
const projectChunk = `${git.projectName}${gitBadge(git, settingBoolean("showDirtyMarker", true, ctx.cwd))} ${formatModelName(ctx)} / `;
|
|
1261
|
+
const thinkingLevel = normalizeThinkingLevel(pi.getThinkingLevel());
|
|
1262
|
+
const thinkingChunk = thinkingLevel;
|
|
1263
|
+
const contextChunk = ` (${contextLabel})`;
|
|
1264
|
+
const cavemanBridge = readCavemanBridge();
|
|
1265
|
+
const cavemanVisible = !!cavemanBridge && (cavemanBridge.isStatusBadgeEnabled?.(ctx.cwd) ?? true);
|
|
1266
|
+
const caveman = cavemanVisible ? cavemanBridge : undefined;
|
|
1267
|
+
const cavemanActive = caveman?.isActive() ?? false;
|
|
1268
|
+
const cavemanGlyph = caveman ? (cavemanActive ? CAVEMAN_ICON_ACTIVE : CAVEMAN_ICON_INACTIVE) : "";
|
|
1269
|
+
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()}`;
|
|
1197
1273
|
const rightPlain = percent === null ? "…%" : `${percent}%`;
|
|
1198
1274
|
const percentColor = percent === null ? "muted" : percent <= 15 ? "error" : percent <= 30 ? "warning" : "success";
|
|
1199
|
-
const
|
|
1275
|
+
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)}`;
|
|
1200
1278
|
const right = theme.fg(percentColor, rightPlain);
|
|
1201
1279
|
const minimumGap = 1;
|
|
1202
1280
|
const gapWidth = Math.max(minimumGap, width - visibleWidth(leftPlain) - visibleWidth(rightPlain) - 2);
|
|
1203
1281
|
const filled = percent === null ? 0 : Math.round(gapWidth * (percent / 100));
|
|
1204
1282
|
const empty = Math.max(0, gapWidth - filled);
|
|
1205
1283
|
const bar = " ".repeat(empty) + theme.fg("warning", "─".repeat(filled));
|
|
1206
|
-
return truncateToWidth(`${
|
|
1284
|
+
return truncateToWidth(`${leftColored} ${bar} ${right}`, width, "");
|
|
1207
1285
|
}
|
|
1208
1286
|
|
|
1209
1287
|
type QolSummaryProfile = "concise" | "balanced" | "exhaustive";
|
|
@@ -4148,6 +4226,14 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
4148
4226
|
};
|
|
4149
4227
|
|
|
4150
4228
|
let currentCtx: ExtensionContext | undefined;
|
|
4229
|
+
let cavemanUnsubscribe: (() => void) | undefined;
|
|
4230
|
+
const subscribeCavemanBridge = () => {
|
|
4231
|
+
cavemanUnsubscribe?.();
|
|
4232
|
+
cavemanUnsubscribe = undefined;
|
|
4233
|
+
const bridge = readCavemanBridge();
|
|
4234
|
+
if (!bridge) return;
|
|
4235
|
+
cavemanUnsubscribe = bridge.subscribe(() => requestRender());
|
|
4236
|
+
};
|
|
4151
4237
|
const notificationService: QolNotificationService = {
|
|
4152
4238
|
notifyQuestionOpened(ctx, event) {
|
|
4153
4239
|
notifyQuestionOpened(ctx ?? currentCtx, event, "question");
|
|
@@ -4164,6 +4250,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
4164
4250
|
|
|
4165
4251
|
pi.on("session_start", (event, ctx) => {
|
|
4166
4252
|
currentCtx = ctx;
|
|
4253
|
+
subscribeCavemanBridge();
|
|
4167
4254
|
latestSystemPromptOptions = undefined;
|
|
4168
4255
|
resetAutoRename();
|
|
4169
4256
|
resetThinkingTimer(ctx);
|
|
@@ -4222,6 +4309,8 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
4222
4309
|
});
|
|
4223
4310
|
|
|
4224
4311
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
4312
|
+
cavemanUnsubscribe?.();
|
|
4313
|
+
cavemanUnsubscribe = undefined;
|
|
4225
4314
|
resetAutoRename();
|
|
4226
4315
|
clearIdleCompactionTimer();
|
|
4227
4316
|
clearQuestionSubscribeTimer();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillagreen/pi-qol",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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": [
|