@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.
Files changed (2) hide show
  1. package/extensions/qol.ts +95 -6
  2. 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 formatModel(ctx: ExtensionContext, pi: ExtensionAPI): string {
1102
+ function formatModelName(ctx: ExtensionContext): string {
1072
1103
  const model = ctx.model;
1073
- if (!model) return `no model / ${pi.getThinkingLevel()}`;
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 `${name} / ${pi.getThinkingLevel()}`;
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 leftPlain = `${git.projectName}${gitBadge(git, settingBoolean("showDirtyMarker", true, ctx.cwd))} ${formatModel(ctx, pi)} (${contextLabel})`;
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 left = theme.fg("accent", leftPlain);
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(`${left} ${bar} ${right}`, width, "");
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.2",
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": [