@vanillagreen/pi-qol 1.4.0 → 1.4.2

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
@@ -10,15 +10,15 @@ Quality-of-life extension for Pi: compact statusline, multiline input, session n
10
10
  ## Highlights
11
11
 
12
12
  - Compact statusline with repo, branch, model, thinking level, and context percent.
13
- - Shift+Enter inserts a newline. Ctrl+J fallback for terminals that can't distinguish it.
13
+ - Newline-insert in the editor (multi-line drafts without auto-submit), with a fallback binding for terminals that can't distinguish the primary key.
14
14
  - Sessions auto-name from your first prompt. `/rename` overrides anytime.
15
- - `/search` browses previous sessions with snippet previews. F2 opens it instantly.
15
+ - `/search` browses previous sessions with snippet previews; the configured shortcut opens it instantly.
16
16
  - `/context` shows a Claude-style context-window breakdown.
17
17
  - `/handoff <goal>` drafts a focused prompt for a new session.
18
18
  - Permission gate prompts before risky `bash` commands. Default match: `rm -Rf`.
19
19
  - Notifications for ready, questions, blocked states, and task completion.
20
20
  - Thinking timer next to collapsed `Thinking...` labels.
21
- - Caveman badge and Alt+C mode cycling when `pi-caveman` is loaded.
21
+ - Caveman badge and a mode-cycling shortcut when `pi-caveman` is loaded.
22
22
  - Subagent-name badge in `pi-agents-tmux` child panes.
23
23
 
24
24
  ## Install
@@ -56,7 +56,7 @@ Arguments support autocomplete.
56
56
 
57
57
  ## Settings
58
58
 
59
- All settings live in the extension manager under **QOL**. Names below match the labels shown there.
59
+ Open `/extensions:settings`; settings appear under the **QOL** tab. Names below match the labels shown there.
60
60
 
61
61
  ### Statusline
62
62
 
@@ -74,8 +74,8 @@ All settings live in the extension manager under **QOL**. Names below match the
74
74
 
75
75
  | Setting | What it does |
76
76
  | --- | --- |
77
- | Shift+Enter inserts newline | Insert a newline instead of submitting on Shift+Enter. |
78
- | Fallback newline key | Alternate newline key for terminals that can't send Shift+Enter. |
77
+ | Newline-insert binding | Insert a newline instead of submitting. |
78
+ | Fallback newline binding | Alternate binding for terminals that can't send the primary one. |
79
79
  | Style pending queue preview | Highlight Pi's pending-queue preview with a green left bar. |
80
80
  | Style image chips | Render `[Image #N]` placeholders as distinct chips. |
81
81
  | Show attachment count | Show a status badge when the draft has image placeholders. |
@@ -112,7 +112,7 @@ Advanced: input cap, title length, output tokens, timeout, custom prompt templat
112
112
  | Setting | What it does |
113
113
  | --- | --- |
114
114
  | Enable session search | Register `/search` and the overlay. |
115
- | Session search shortcut | Shortcut to open search. Default `f2`; set to `none` to disable. |
115
+ | Session search shortcut | Configurable; set to `none` to disable. |
116
116
  | Result limit | Max matching prompts returned. |
117
117
  | Visible session rows | Rows shown before scrolling. |
118
118
  | Preview snippets | Matching snippets shown on the preview screen. |
@@ -6,6 +6,11 @@ import { serializeMessagesForSummary } from "./compaction.js";
6
6
  import { HANDOFF_SYSTEM_PROMPT } from "./constants.js";
7
7
  import { settingBoolean } from "./settings.js";
8
8
 
9
+ function errorMessage(error: unknown): string {
10
+ const message = error instanceof Error ? error.message : String(error);
11
+ return message.length > 180 ? `${message.slice(0, 179)}…` : message;
12
+ }
13
+
9
14
  export async function runHandoff(args: string, ctx: ExtensionCommandContext): Promise<void> {
10
15
  if (!ctx.hasUI) {
11
16
  ctx.ui.notify("handoff requires interactive mode", "error");
@@ -36,56 +41,58 @@ export async function runHandoff(args: string, ctx: ExtensionCommandContext): Pr
36
41
 
37
42
  const releaseModalLock = acquireVstackModalLock();
38
43
  let result: string | null = null;
44
+ let generationError: unknown;
39
45
  try {
40
- result = await ctx.ui.custom<string | null>((tui: any, theme: any, _kb: any, done: (value: string | null) => void) => {
41
- const loader = new BorderedLoader(tui, theme, "Generating handoff prompt...");
42
- loader.onAbort = () => done(null);
43
-
44
- const doGenerate = async () => {
45
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model!);
46
- if (!auth.ok || !auth.apiKey) {
47
- throw new Error(auth.ok ? `No API key for ${ctx.model!.provider}` : auth.error);
48
- }
49
-
50
- const userMessage: Message = {
51
- role: "user",
52
- content: [
53
- {
54
- type: "text",
55
- text: `## Conversation History\n\n${conversationText}\n\n## User's Goal for New Thread\n\n${goal}`,
56
- },
57
- ],
58
- timestamp: Date.now(),
46
+ result = await ctx.ui.custom<string | null>((tui: any, theme: any, _kb: any, done: (value: string | null) => void) => {
47
+ const loader = new BorderedLoader(tui, theme, "Generating handoff prompt...");
48
+ loader.onAbort = () => done(null);
49
+
50
+ const doGenerate = async () => {
51
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model!);
52
+ if (!auth.ok || !auth.apiKey) {
53
+ throw new Error(auth.ok ? `No API key for ${ctx.model!.provider}` : auth.error);
54
+ }
55
+
56
+ const userMessage: Message = {
57
+ role: "user",
58
+ content: [
59
+ {
60
+ type: "text",
61
+ text: `## Conversation History\n\n${conversationText}\n\n## User's Goal for New Thread\n\n${goal}`,
62
+ },
63
+ ],
64
+ timestamp: Date.now(),
65
+ };
66
+
67
+ const response = await complete(
68
+ ctx.model!,
69
+ { systemPrompt: HANDOFF_SYSTEM_PROMPT, messages: [userMessage] },
70
+ { apiKey: auth.apiKey, headers: auth.headers, signal: loader.signal },
71
+ );
72
+
73
+ if (response.stopReason === "aborted") return null;
74
+ return response.content
75
+ .filter((content): content is { type: "text"; text: string } => content.type === "text")
76
+ .map((content) => content.text)
77
+ .join("\n");
59
78
  };
60
79
 
61
- const response = await complete(
62
- ctx.model!,
63
- { systemPrompt: HANDOFF_SYSTEM_PROMPT, messages: [userMessage] },
64
- { apiKey: auth.apiKey, headers: auth.headers, signal: loader.signal },
65
- );
66
-
67
- if (response.stopReason === "aborted") return null;
68
- return response.content
69
- .filter((content): content is { type: "text"; text: string } => content.type === "text")
70
- .map((content) => content.text)
71
- .join("\n");
72
- };
73
-
74
- doGenerate()
75
- .then(done)
76
- .catch((error) => {
77
- console.error("Handoff generation failed:", error);
78
- done(null);
79
- });
80
-
81
- return loader;
82
- });
80
+ doGenerate()
81
+ .then(done)
82
+ .catch((error) => {
83
+ generationError = error;
84
+ done(null);
85
+ });
86
+
87
+ return loader;
88
+ });
83
89
  } finally {
84
90
  releaseModalLock();
85
91
  }
86
92
 
87
93
  if (result === null) {
88
- ctx.ui.notify("Cancelled", "info");
94
+ if (generationError) ctx.ui.notify(`Handoff generation failed: ${errorMessage(generationError)}`, "error");
95
+ else ctx.ui.notify("Cancelled", "info");
89
96
  return;
90
97
  }
91
98
 
@@ -146,7 +146,7 @@ export function renderSessionSearchContextMessage(message: any, options: any, th
146
146
  const title = raw.match(/^## Session Search Context:\s*(.+)$/m)?.[1]?.trim() || "session";
147
147
  const date = raw.match(/\*\*Date:\*\*\s*([^|\n]+)/)?.[1]?.trim() || "";
148
148
  const header = `${theme.fg("customMessageLabel", theme.bold("Session context: "))}${theme.fg("accent", title)}${date ? theme.fg("muted", ` (${date})`) : ""}`;
149
- if (!options?.expanded) return new Text(`${header}${theme.fg("dim", " ctrl+o expand")}`, 0, 0);
149
+ if (!options?.expanded) return new Text(`${header}${theme.fg("dim", " ctrl+o to expand")}`, 0, 0);
150
150
  const body = raw.slice(raw.indexOf("\n") + 1).trim();
151
151
  return new Text(body ? `${header}\n\n${body}` : header, 0, 0);
152
152
  }
@@ -34,7 +34,7 @@ function queueSessionSearchCommandAction(ctx: ExtensionContext, action: QolSessi
34
34
  const id = nextSessionSearchPendingActionId();
35
35
  qolSessionSearchPendingActions.set(id, action);
36
36
  ctx.ui.setEditorText(`/search:resume-pending ${id}`);
37
- ctx.ui.notify(`${sessionDisplayName(action.result!)} — press Enter to ${action.type === "fork" ? "fork" : "resume"}`, "info");
37
+ ctx.ui.notify(`${sessionDisplayName(action.result!)} — press enter to ${action.type === "fork" ? "fork" : "resume"}`, "info");
38
38
  }
39
39
 
40
40
  export async function runSessionSearchResumeOrFork(pi: ExtensionAPI, ctx: ExtensionContext, action: QolSessionPaletteAction): Promise<boolean> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanillagreen/pi-qol",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
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": [
@@ -136,8 +136,8 @@
136
136
  },
137
137
  {
138
138
  "key": "newlineOnShiftEnter",
139
- "label": "Shift+Enter inserts newline",
140
- "description": "Intercept distinguishable Shift+Enter/Shift+Return in the prompt editor and insert a newline instead of submitting.",
139
+ "label": "shift+enter inserts newline",
140
+ "description": "Intercept distinguishable shift+enter/shift+return in the prompt editor and insert a newline instead of submitting.",
141
141
  "type": "boolean",
142
142
  "default": true,
143
143
  "category": "Input",
@@ -147,7 +147,7 @@
147
147
  {
148
148
  "key": "newlineFallbackKey",
149
149
  "label": "Fallback newline key",
150
- "description": "Alternate newline key for terminals that cannot distinguish Shift+Enter. Alt+Enter is reserved for Pi follow-up messages.",
150
+ "description": "Alternate newline key for terminals that cannot distinguish shift+enter. alt+enter is reserved for Pi follow-up messages.",
151
151
  "type": "enum",
152
152
  "enumValues": [
153
153
  "ctrl+j",
@@ -161,7 +161,7 @@
161
161
  {
162
162
  "key": "pendingQueue.asciiGreen",
163
163
  "label": "Style pending queue preview",
164
- "description": "Render Pi's built-in steering/follow-up queue preview in ANSI green with a heavy left queue bar, while keeping the Alt+Up restore hint in the theme hint color with a two-space indent.",
164
+ "description": "Render Pi's built-in steering/follow-up queue preview in ANSI green with a heavy left queue bar, while keeping the alt+up restore hint in the theme hint color with a two-space indent.",
165
165
  "type": "boolean",
166
166
  "default": true,
167
167
  "category": "Input",
@@ -350,7 +350,7 @@
350
350
  {
351
351
  "key": "sessionSearch.shortcutKey",
352
352
  "label": "Session search shortcut",
353
- "description": "Keyboard shortcut to open the session search overlay. Default f2 avoids Pi's built-in Ctrl+F and Alt+F editor bindings. Set to none to disable.",
353
+ "description": "Keyboard shortcut to open the session search overlay. Default f2 avoids Pi's built-in ctrl+f and alt+f editor bindings. Set to none to disable.",
354
354
  "type": "string",
355
355
  "default": "f2",
356
356
  "category": "Session Search",