@vanillagreen/pi-qol 1.0.2 → 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 +95 -6
- package/package.json +1 -1
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.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": [
|