@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 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 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.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": [