@vanillagreen/pi-qol 1.0.1 → 1.0.3
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/extensions/qol.ts +131 -16
- package/package.json +1 -1
package/extensions/qol.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { homedir } from "node:os";
|
|
|
9
9
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
10
10
|
|
|
11
11
|
const INSTALL_SYMBOL = Symbol.for("vstack.pi-qol.installed");
|
|
12
|
+
const CONFIG_ID = "@vanillagreen/pi-qol";
|
|
12
13
|
const STATUS_KEY = "qol-attachments";
|
|
13
14
|
const SESSION_SEARCH_STATUS_KEY = "qol-session-search";
|
|
14
15
|
const SESSION_SEARCH_CONTEXT_TYPE = "qol-session-context";
|
|
@@ -19,6 +20,27 @@ const IMAGE_PATH_PATTERN = /(^|[\s(\[{<"'`])(@?(?:~|\.\.?|\/)[^\s)\]}>"'`]+?\.(?
|
|
|
19
20
|
const QUESTION_SERVICE_SYMBOL = Symbol.for("vstack.pi-questions.service");
|
|
20
21
|
const QOL_NOTIFICATION_SERVICE_SYMBOL = Symbol.for("vstack.pi-qol.notification-service");
|
|
21
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
|
+
}
|
|
22
44
|
const THINKING_TIMER_STORE_SYMBOL = Symbol.for("vstack.pi-qol.thinking-timer.store");
|
|
23
45
|
const THINKING_TIMER_PATCH_SYMBOL = Symbol.for("vstack.pi-qol.thinking-timer.patch");
|
|
24
46
|
const SESSION_SEARCH_PENDING_SYMBOL = Symbol.for("vstack.pi-qol.session-search.pending-context");
|
|
@@ -44,6 +66,7 @@ const DEFAULT_SESSION_SEARCH_SHORTCUT = "f3";
|
|
|
44
66
|
const DEFAULT_SESSION_SEARCH_SUMMARY_INPUT_CHARS = 180_000;
|
|
45
67
|
const DEFAULT_SESSION_SEARCH_SUMMARY_MAX_TOKENS = 4096;
|
|
46
68
|
const DEFAULT_SESSION_SEARCH_CACHE_TTL_SECONDS = 0;
|
|
69
|
+
const SESSION_SEARCH_OVERLAY_HEIGHT_RATIO = 0.9;
|
|
47
70
|
const DEFAULT_AUTO_RENAME_MODEL = "openai-codex/gpt-5.4-mini";
|
|
48
71
|
const DEFAULT_AUTO_RENAME_FALLBACK_MODEL = "current";
|
|
49
72
|
const DEFAULT_AUTO_RENAME_INPUT_CHARS = 2000;
|
|
@@ -131,7 +154,7 @@ function readVstackConfig(cwd?: string): VstackConfig {
|
|
|
131
154
|
if (!existsSync(path)) continue;
|
|
132
155
|
try {
|
|
133
156
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
134
|
-
const config = parsed?.vstack?.extensionManager?.config?.[
|
|
157
|
+
const config = parsed?.vstack?.extensionManager?.config?.[CONFIG_ID];
|
|
135
158
|
if (config && typeof config === "object" && !Array.isArray(config)) Object.assign(merged, config);
|
|
136
159
|
} catch {
|
|
137
160
|
// Ignore malformed optional manager config.
|
|
@@ -938,6 +961,16 @@ function statusText(ctx: ExtensionContext, text: string): string | undefined {
|
|
|
938
961
|
}
|
|
939
962
|
|
|
940
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
|
+
}
|
|
941
974
|
const fallback = newlineFallbackKey(ctx.cwd);
|
|
942
975
|
const newlineEnabled = settingBoolean("newlineOnShiftEnter", true, ctx.cwd);
|
|
943
976
|
const isShiftEnter = matchesKey(data, "shift+enter") || matchesKey(data, "shift+return");
|
|
@@ -1066,9 +1099,9 @@ function repoNameFromRemote(remote: string): string | undefined {
|
|
|
1066
1099
|
return match?.[1];
|
|
1067
1100
|
}
|
|
1068
1101
|
|
|
1069
|
-
function
|
|
1102
|
+
function formatModelName(ctx: ExtensionContext): string {
|
|
1070
1103
|
const model = ctx.model;
|
|
1071
|
-
if (!model) return
|
|
1104
|
+
if (!model) return "no model";
|
|
1072
1105
|
let name = model.name || model.id;
|
|
1073
1106
|
name = name.replace(/^Claude\s+/i, "");
|
|
1074
1107
|
name = name.replace(/^claude[-_]/i, "");
|
|
@@ -1080,7 +1113,29 @@ function formatModel(ctx: ExtensionContext, pi: ExtensionAPI): string {
|
|
|
1080
1113
|
name = name.replace(/\bhaiku\b/i, "Haiku");
|
|
1081
1114
|
name = name.replace(/\s+/g, " ").trim();
|
|
1082
1115
|
name = name.replace(/\b(Opus|Sonnet|Haiku) (\d) (\d)\b/, "$1 $2.$3");
|
|
1083
|
-
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
|
+
}
|
|
1084
1139
|
}
|
|
1085
1140
|
|
|
1086
1141
|
function formatWindow(tokens: number | undefined): string {
|
|
@@ -1189,19 +1244,44 @@ function setTmuxWindowOption(target: string, option: string, value: string): voi
|
|
|
1189
1244
|
execFile("tmux", ["set-option", "-wq", "-t", target, option, value], { timeout: 1000 }, () => undefined);
|
|
1190
1245
|
}
|
|
1191
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
|
+
|
|
1192
1258
|
function renderStatusLine(width: number, ctx: ExtensionContext, git: GitState, pi: ExtensionAPI, theme: Pick<Theme, "fg">): string {
|
|
1193
1259
|
const { label: contextLabel, percent } = statuslineContextInfo(ctx);
|
|
1194
|
-
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()}`;
|
|
1195
1273
|
const rightPlain = percent === null ? "…%" : `${percent}%`;
|
|
1196
1274
|
const percentColor = percent === null ? "muted" : percent <= 15 ? "error" : percent <= 30 ? "warning" : "success";
|
|
1197
|
-
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)}`;
|
|
1198
1278
|
const right = theme.fg(percentColor, rightPlain);
|
|
1199
1279
|
const minimumGap = 1;
|
|
1200
1280
|
const gapWidth = Math.max(minimumGap, width - visibleWidth(leftPlain) - visibleWidth(rightPlain) - 2);
|
|
1201
1281
|
const filled = percent === null ? 0 : Math.round(gapWidth * (percent / 100));
|
|
1202
1282
|
const empty = Math.max(0, gapWidth - filled);
|
|
1203
1283
|
const bar = " ".repeat(empty) + theme.fg("warning", "─".repeat(filled));
|
|
1204
|
-
return truncateToWidth(`${
|
|
1284
|
+
return truncateToWidth(`${leftColored} ${bar} ${right}`, width, "");
|
|
1205
1285
|
}
|
|
1206
1286
|
|
|
1207
1287
|
type QolSummaryProfile = "concise" | "balanced" | "exhaustive";
|
|
@@ -3071,7 +3151,7 @@ class QolSessionSearchComponent {
|
|
|
3071
3151
|
|
|
3072
3152
|
constructor(
|
|
3073
3153
|
private readonly done: (action: QolSessionPaletteAction) => void,
|
|
3074
|
-
private readonly tui: { requestRender(): void },
|
|
3154
|
+
private readonly tui: { requestRender(): void; terminal?: { rows?: number } },
|
|
3075
3155
|
private readonly theme: Theme,
|
|
3076
3156
|
private readonly sessions: QolSessionSearchSession[],
|
|
3077
3157
|
private readonly cwd: string,
|
|
@@ -3089,6 +3169,30 @@ class QolSessionSearchComponent {
|
|
|
3089
3169
|
|
|
3090
3170
|
invalidate(): void {}
|
|
3091
3171
|
|
|
3172
|
+
private maxOverlayRows(): number {
|
|
3173
|
+
const terminalRows = Number(this.tui.terminal?.rows ?? process.stdout.rows ?? 30);
|
|
3174
|
+
const safeRows = Number.isFinite(terminalRows) && terminalRows > 0 ? terminalRows : 30;
|
|
3175
|
+
return Math.max(8, Math.floor(safeRows * SESSION_SEARCH_OVERLAY_HEIGHT_RATIO));
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
private searchMaxVisibleRows(): number {
|
|
3179
|
+
const configured = Math.max(1, Math.floor(settingNumber("sessionSearch.maxVisible", 8, this.cwd)));
|
|
3180
|
+
// Search results render as 3 content rows plus a divider between rows.
|
|
3181
|
+
// The remaining chrome is title, search box, help text, scroll status,
|
|
3182
|
+
// footer, and frame. Keep the rendered line count within overlay maxHeight
|
|
3183
|
+
// so Pi does not clip the bottom of the popup on short terminals.
|
|
3184
|
+
const responsive = Math.max(1, Math.floor((this.maxOverlayRows() - 12) / 4));
|
|
3185
|
+
return Math.max(1, Math.min(configured, responsive));
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
private messageMaxVisibleRows(): number {
|
|
3189
|
+
const configured = Math.max(1, Math.floor(settingNumber("sessionSearch.messageMaxVisible", 12, this.cwd)));
|
|
3190
|
+
// Message rows are one line each; reserve space for session metadata,
|
|
3191
|
+
// optional scroll status, footer, and frame.
|
|
3192
|
+
const responsive = Math.max(1, this.maxOverlayRows() - 14);
|
|
3193
|
+
return Math.max(1, Math.min(configured, responsive));
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3092
3196
|
render(width: number): string[] {
|
|
3093
3197
|
const configured = Math.max(70, Math.floor(settingNumber("sessionSearch.overlayWidth", 104, this.cwd)));
|
|
3094
3198
|
const renderWidth = Math.min(Math.max(48, width), configured);
|
|
@@ -3165,11 +3269,11 @@ class QolSessionSearchComponent {
|
|
|
3165
3269
|
return;
|
|
3166
3270
|
}
|
|
3167
3271
|
if (matchesKey(data, "-") || matchesKey(data, "pageup")) {
|
|
3168
|
-
state.selected = Math.max(0, state.selected -
|
|
3272
|
+
state.selected = Math.max(0, state.selected - this.searchMaxVisibleRows());
|
|
3169
3273
|
return;
|
|
3170
3274
|
}
|
|
3171
3275
|
if (matchesKey(data, "=") || matchesKey(data, "pagedown")) {
|
|
3172
|
-
state.selected = Math.min(state.results.length - 1, state.selected +
|
|
3276
|
+
state.selected = Math.min(state.results.length - 1, state.selected + this.searchMaxVisibleRows());
|
|
3173
3277
|
return;
|
|
3174
3278
|
}
|
|
3175
3279
|
if (matchesKey(data, "left")) {
|
|
@@ -3239,11 +3343,11 @@ class QolSessionSearchComponent {
|
|
|
3239
3343
|
return;
|
|
3240
3344
|
}
|
|
3241
3345
|
if (matchesKey(data, "-") || matchesKey(data, "pageup")) {
|
|
3242
|
-
state.selected = Math.max(0, state.selected -
|
|
3346
|
+
state.selected = Math.max(0, state.selected - this.messageMaxVisibleRows());
|
|
3243
3347
|
return;
|
|
3244
3348
|
}
|
|
3245
3349
|
if (matchesKey(data, "=") || matchesKey(data, "pagedown")) {
|
|
3246
|
-
state.selected = Math.min(state.messages.length - 1, state.selected +
|
|
3350
|
+
state.selected = Math.min(state.messages.length - 1, state.selected + this.messageMaxVisibleRows());
|
|
3247
3351
|
return;
|
|
3248
3352
|
}
|
|
3249
3353
|
if (matchesKey(data, "home") || matchesKey(data, "ctrl+a")) {
|
|
@@ -3365,7 +3469,7 @@ class QolSessionSearchComponent {
|
|
|
3365
3469
|
if (state.results.length === 0) {
|
|
3366
3470
|
lines.push(row(muted(state.query.trim() ? "No sessions match your search" : "No sessions found")), empty());
|
|
3367
3471
|
} else {
|
|
3368
|
-
const maxVisible =
|
|
3472
|
+
const maxVisible = this.searchMaxVisibleRows();
|
|
3369
3473
|
const start = Math.max(0, Math.min(state.selected - Math.floor(maxVisible / 2), state.results.length - maxVisible));
|
|
3370
3474
|
const end = Math.min(start + maxVisible, state.results.length);
|
|
3371
3475
|
const rowBudget = Math.max(10, inner);
|
|
@@ -3394,7 +3498,7 @@ class QolSessionSearchComponent {
|
|
|
3394
3498
|
if (state.results.length > maxVisible) lines.push(empty(), row(dim(`${state.selected + 1}/${state.results.length} ${state.query.trim() ? "matches" : "recent sessions"}`)));
|
|
3395
3499
|
}
|
|
3396
3500
|
lines.push(divider(), empty());
|
|
3397
|
-
lines.push(row(`${ansiYellow("
|
|
3501
|
+
lines.push(row(`${ansiYellow("-/=")} ${dim("page")} ${ansiYellow("enter")} ${dim("prompts")} ${ansiYellow("ctrl+u")} ${dim("clear")} ${ansiYellow("esc")} ${dim("close")}`));
|
|
3398
3502
|
lines.push(bottom());
|
|
3399
3503
|
return lines;
|
|
3400
3504
|
}
|
|
@@ -3419,7 +3523,7 @@ class QolSessionSearchComponent {
|
|
|
3419
3523
|
lines.push(row(dim(`${state.messages.length} user prompt${state.messages.length === 1 ? "" : "s"} · enter opens prompt actions · r resumes latest session state`)));
|
|
3420
3524
|
lines.push(empty(), divider(), empty());
|
|
3421
3525
|
|
|
3422
|
-
const maxVisible =
|
|
3526
|
+
const maxVisible = this.messageMaxVisibleRows();
|
|
3423
3527
|
const start = Math.max(0, Math.min(state.selected - Math.floor(maxVisible / 2), state.messages.length - maxVisible));
|
|
3424
3528
|
const end = Math.min(start + maxVisible, state.messages.length);
|
|
3425
3529
|
for (let i = start; i < end; i++) {
|
|
@@ -3434,7 +3538,7 @@ class QolSessionSearchComponent {
|
|
|
3434
3538
|
}
|
|
3435
3539
|
if (state.messages.length > maxVisible) lines.push(empty(), row(dim(`${state.selected + 1}/${state.messages.length} user prompts`)));
|
|
3436
3540
|
lines.push(divider(), empty());
|
|
3437
|
-
lines.push(row(`${ansiYellow("
|
|
3541
|
+
lines.push(row(`${ansiYellow("-/=")} ${dim("page")} ${ansiYellow("enter")} ${dim("prompt actions")} ${ansiYellow("r")} ${dim("resume session")} ${ansiYellow("esc")} ${dim("sessions")}`));
|
|
3438
3542
|
lines.push(bottom());
|
|
3439
3543
|
return lines;
|
|
3440
3544
|
}
|
|
@@ -4122,6 +4226,14 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
4122
4226
|
};
|
|
4123
4227
|
|
|
4124
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
|
+
};
|
|
4125
4237
|
const notificationService: QolNotificationService = {
|
|
4126
4238
|
notifyQuestionOpened(ctx, event) {
|
|
4127
4239
|
notifyQuestionOpened(ctx ?? currentCtx, event, "question");
|
|
@@ -4138,6 +4250,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
4138
4250
|
|
|
4139
4251
|
pi.on("session_start", (event, ctx) => {
|
|
4140
4252
|
currentCtx = ctx;
|
|
4253
|
+
subscribeCavemanBridge();
|
|
4141
4254
|
latestSystemPromptOptions = undefined;
|
|
4142
4255
|
resetAutoRename();
|
|
4143
4256
|
resetThinkingTimer(ctx);
|
|
@@ -4196,6 +4309,8 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
4196
4309
|
});
|
|
4197
4310
|
|
|
4198
4311
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
4312
|
+
cavemanUnsubscribe?.();
|
|
4313
|
+
cavemanUnsubscribe = undefined;
|
|
4199
4314
|
resetAutoRename();
|
|
4200
4315
|
clearIdleCompactionTimer();
|
|
4201
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.3",
|
|
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": [
|