@vanillagreen/pi-qol 1.3.0 → 1.4.0
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 +114 -71
- package/extensions/qol/session-rename.ts +60 -24
- package/extensions/qol.ts +102 -33
- package/package.json +15 -1
package/README.md
CHANGED
|
@@ -5,7 +5,21 @@
|
|
|
5
5
|

|
|
6
6
|

|
|
7
7
|
|
|
8
|
-
Quality-of-life extension for Pi.
|
|
8
|
+
Quality-of-life extension for Pi: compact statusline, multiline input, session naming and search, notifications, and a permission gate.
|
|
9
|
+
|
|
10
|
+
## Highlights
|
|
11
|
+
|
|
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.
|
|
14
|
+
- Sessions auto-name from your first prompt. `/rename` overrides anytime.
|
|
15
|
+
- `/search` browses previous sessions with snippet previews. F2 opens it instantly.
|
|
16
|
+
- `/context` shows a Claude-style context-window breakdown.
|
|
17
|
+
- `/handoff <goal>` drafts a focused prompt for a new session.
|
|
18
|
+
- Permission gate prompts before risky `bash` commands. Default match: `rm -Rf`.
|
|
19
|
+
- Notifications for ready, questions, blocked states, and task completion.
|
|
20
|
+
- Thinking timer next to collapsed `Thinking...` labels.
|
|
21
|
+
- Caveman badge and Alt+C mode cycling when `pi-caveman` is loaded.
|
|
22
|
+
- Subagent-name badge in `pi-agents-tmux` child panes.
|
|
9
23
|
|
|
10
24
|
## Install
|
|
11
25
|
|
|
@@ -24,108 +38,137 @@ vstack add vanillagreencom/vstack --pi-extension pi-qol --harness pi -y
|
|
|
24
38
|
|
|
25
39
|
Restart Pi after installation.
|
|
26
40
|
|
|
27
|
-
## What it provides
|
|
28
|
-
|
|
29
|
-
- Compact statusline and `π` prompt editor: shows repo/project, branch/dirty state, model, colorized thinking level, optional Caveman badge, context window size, remaining context percent, and (inside `pi-agents-tmux` child panes) an ANSI-background subagent-name badge 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. QOL can also style Pi's built-in steering/follow-up pending queue preview with an ANSI green left bar while leaving the restore hint muted. When `pi-caveman` is loaded, `Alt+C` cycles Caveman modes.
|
|
31
|
-
- Compact image placeholders: existing pasted image paths can collapse to `[Image #N]` aliases and are attached on submit.
|
|
32
|
-
- Session naming: `/rename [name]` sets or shows the friendly session name; automatic first-prompt naming is enabled by default.
|
|
33
|
-
- Context usage: `/context` prints an inline Claude-style context-window visualization with estimated Pi/model category breakdowns.
|
|
34
|
-
- Previous-session search: `/search` and optional `F2` overlay search prior sessions, preview snippets, resume, inject summarized context, or start a new session with summarized context.
|
|
35
|
-
- Handoff: `/handoff <goal>` drafts a focused prompt for a new session, preserving the latest compaction summary plus retained branch entries.
|
|
36
|
-
- Optional permission gate: when enabled, prompts before configured `bash` tool command fragments run; default match is `rm -Rf`.
|
|
37
|
-
- Notifications: terminal/tmux/native notifications for ready-for-input, questions, direction needed, task completion, and critical/blocked states.
|
|
38
|
-
- Optional custom compaction and idle compaction; disabled by default so Pi's compaction behavior is unchanged until enabled.
|
|
39
|
-
- Thinking timer next to collapsed `Thinking...` labels; enabled by default and falls back to Pi defaults if internals change.
|
|
40
|
-
|
|
41
41
|
## Commands
|
|
42
42
|
|
|
43
43
|
| Command | Action |
|
|
44
44
|
| --- | --- |
|
|
45
|
-
| `/qol` | Open
|
|
45
|
+
| `/qol` | Open settings (or print status if extension-manager isn't installed). |
|
|
46
46
|
| `/qol notify-test` | Send a test notification. |
|
|
47
|
-
| `/
|
|
48
|
-
| `/qol:rename
|
|
49
|
-
| `/rename
|
|
50
|
-
| `/context` | Show
|
|
51
|
-
| `/search [query]` | Open previous-session search
|
|
47
|
+
| `/rename [name]` | Set or show the current session's name. |
|
|
48
|
+
| `/qol:rename` | Regenerate the session name from the first prompt. |
|
|
49
|
+
| `/qol:rename:full` | Regenerate from the full conversation. |
|
|
50
|
+
| `/context` | Show context-window usage with category breakdown. |
|
|
51
|
+
| `/search [query]` | Open previous-session search. |
|
|
52
52
|
| `/search:refresh` | Refresh the session search cache. |
|
|
53
|
-
| `/
|
|
54
|
-
| `/handoff <goal>` | Draft a focused handoff prompt for a new session. |
|
|
53
|
+
| `/handoff <goal>` | Draft a handoff prompt for a new session. |
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
Arguments support autocomplete.
|
|
57
56
|
|
|
58
57
|
## Settings
|
|
59
58
|
|
|
60
|
-
|
|
59
|
+
All settings live in the extension manager under **QOL**. Names below match the labels shown there.
|
|
61
60
|
|
|
62
|
-
### Statusline
|
|
61
|
+
### Statusline
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
| Setting | What it does |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| Enable QOL editor helpers | Master toggle for QOL statusline, commands, notifications, search, compaction, and editor helpers. |
|
|
66
|
+
| Replace built-in footer | Hide Pi's default footer while the QOL statusline is active. |
|
|
67
|
+
| Use π prompt editor | Use the compact prompt editor. |
|
|
68
|
+
| Show session name title | Show the session name above the prompt and in the tmux pane title. |
|
|
69
|
+
| Sync session name to tmux window name | Rename the tmux window to `π <session>`. |
|
|
70
|
+
| Input bottom padding | Blank lines below the prompt. |
|
|
71
|
+
| Show dirty marker | Append `*` to the branch when the worktree is dirty. |
|
|
72
72
|
|
|
73
73
|
### Input
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
| Setting | What it does |
|
|
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. |
|
|
79
|
+
| Style pending queue preview | Highlight Pi's pending-queue preview with a green left bar. |
|
|
80
|
+
| Style image chips | Render `[Image #N]` placeholders as distinct chips. |
|
|
81
|
+
| Show attachment count | Show a status badge when the draft has image placeholders. |
|
|
78
82
|
|
|
79
|
-
### Session
|
|
83
|
+
### Session naming
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
85
|
+
| Setting | What it does |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| Enable /rename command | Register the `/rename` command. |
|
|
88
|
+
| Auto-name new sessions | Generate a friendly session name from the first prompt. |
|
|
89
|
+
| Auto-rename model | Model used for title generation. |
|
|
90
|
+
| Auto-rename fallback model | Model tried when the primary fails. |
|
|
91
|
+
| Deterministic fallback | Title-case words, truncated prompt, or none if all model calls fail. |
|
|
92
|
+
| Auto-rename prefix | Optional static prefix on every generated name. |
|
|
93
|
+
| Notify on auto-rename | Show a notification when auto-renaming. |
|
|
86
94
|
|
|
87
|
-
|
|
95
|
+
Advanced: input cap, title length, output tokens, timeout, custom prompt template, and debug logging.
|
|
88
96
|
|
|
89
|
-
|
|
90
|
-
- `sessionSearch.shortcutKey`: shortcut to open search; default `f2`, set `none` to disable.
|
|
91
|
-
- Search ordering: empty search shows prompts by recency; typed search sorts by match quality, then prompt recency.
|
|
92
|
-
- Result/layout knobs: `resultLimit`, `maxVisible`, `messageMaxVisible`, `previewSnippets`, `overlayWidth`, `cacheTtlSeconds`.
|
|
93
|
-
- Summary knobs: `summaryModel`, `summaryMaxTokens`, `summaryInputMaxChars`.
|
|
97
|
+
### Handoff
|
|
94
98
|
|
|
95
|
-
|
|
99
|
+
| Setting | What it does |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| Enable /handoff command | Register the `/handoff` command. |
|
|
102
|
+
| Review handoff prompt | Open an editor to edit the generated prompt before creating the session. |
|
|
103
|
+
|
|
104
|
+
### Context window
|
|
105
|
+
|
|
106
|
+
| Setting | What it does |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| Enable /context command | Register `/context`. |
|
|
96
109
|
|
|
97
|
-
###
|
|
110
|
+
### Session search
|
|
98
111
|
|
|
99
|
-
|
|
112
|
+
| Setting | What it does |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| Enable session search | Register `/search` and the overlay. |
|
|
115
|
+
| Session search shortcut | Shortcut to open search. Default `f2`; set to `none` to disable. |
|
|
116
|
+
| Result limit | Max matching prompts returned. |
|
|
117
|
+
| Visible session rows | Rows shown before scrolling. |
|
|
118
|
+
| Preview snippets | Matching snippets shown on the preview screen. |
|
|
119
|
+
| Session cache TTL | Seconds before the session list refreshes; `0` keeps it until you run `/search:refresh`. |
|
|
120
|
+
|
|
121
|
+
Summary settings (model, max tokens, input cap) tune the summarizer when you import context from a previous session.
|
|
100
122
|
|
|
101
123
|
### Notifications
|
|
102
124
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
125
|
+
Master toggle: **Enable notifications**.
|
|
126
|
+
|
|
127
|
+
Triggers (notify when): ready, direction needed, question popups, all tasks complete, critical/blocked.
|
|
128
|
+
|
|
129
|
+
Channels: terminal bell, native terminal notifications (OSC 777/99 or Windows toast), tmux `display-message`, tmux window marking, OSC passthrough, and an optional in-Pi UI notice.
|
|
130
|
+
|
|
131
|
+
Tuning: cooldown seconds, title, ready message, body length, tmux durations.
|
|
132
|
+
|
|
133
|
+
Notes:
|
|
134
|
+
|
|
135
|
+
- **Terminal notification protocol** picks between OSC 99 (Kitty) and OSC 777 automatically.
|
|
136
|
+
- **Bell when tmux window active** is off so you don't get bells while looking at Pi.
|
|
137
|
+
- **tmux native via client TTY** sends OSC notifications to attached tmux clients so notifications still appear when the Pi window is inactive.
|
|
107
138
|
|
|
108
|
-
Use `/qol notify-test` to verify your terminal/tmux
|
|
139
|
+
Use `/qol notify-test` to verify your terminal/tmux setup.
|
|
109
140
|
|
|
110
141
|
### Permission gate
|
|
111
142
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
143
|
+
| Setting | What it does |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| Prompt before risky bash commands | Ask before bash commands matching the command list. |
|
|
146
|
+
| Commands to prompt for | Comma-separated literal fragments or `/regex/flags`. |
|
|
147
|
+
| Approval preview lines | Cap the approval-prompt preview height. |
|
|
148
|
+
| Approval preview characters | Cap the approval-prompt preview width. |
|
|
115
149
|
|
|
116
|
-
|
|
150
|
+
Off by default. When enabled, non-interactive matches are blocked.
|
|
117
151
|
|
|
118
|
-
|
|
119
|
-
- `compaction.model`: summarizer model; default `google/gemini-2.5-flash`.
|
|
120
|
-
- `compaction.profile`: `concise`, `balanced`, or `exhaustive`.
|
|
121
|
-
- `compaction.remoteEnabled` / `compaction.remoteEndpoint`: call a remote summarizer with `{ systemPrompt, prompt, maxTokens }` and expect `{ summary }`.
|
|
122
|
-
- `compaction.branchSummaryEnabled`: use the same summarizer for requested `/tree` branch summaries.
|
|
123
|
-
- `compaction.idleEnabled` and related thresholds: extension-managed idle compaction trigger.
|
|
152
|
+
### Compaction
|
|
124
153
|
|
|
125
|
-
|
|
154
|
+
| Setting | What it does |
|
|
155
|
+
| --- | --- |
|
|
156
|
+
| Custom compaction summaries | Use QOL summaries instead of Pi's default. |
|
|
157
|
+
| Compaction model | Summarizer model. |
|
|
158
|
+
| Compaction detail profile | `concise`, `balanced`, or `exhaustive`. |
|
|
159
|
+
| Include previous summary | Pass the previous summary for iterative continuity. |
|
|
160
|
+
| Fallback to Pi default compaction | Run Pi's default compaction if QOL's fails. |
|
|
161
|
+
| Show compaction notifications | Notify on compaction start/fail/complete. |
|
|
162
|
+
| Custom branch summaries | Use the QOL summarizer for `/tree` branch summaries. |
|
|
163
|
+
| Remote compaction endpoint | Call a remote HTTP summarizer instead of a model. |
|
|
164
|
+
| Idle compaction trigger | Auto-compact after the session sits idle above a token threshold. |
|
|
126
165
|
|
|
127
|
-
|
|
166
|
+
Idle thresholds (token threshold, idle delay, fixed token limit, percent limit) tune when idle compaction fires.
|
|
128
167
|
|
|
129
|
-
|
|
168
|
+
### Thinking
|
|
130
169
|
|
|
131
|
-
|
|
170
|
+
| Setting | What it does |
|
|
171
|
+
| --- | --- |
|
|
172
|
+
| Hidden thinking label | Label shown when thinking blocks are hidden. |
|
|
173
|
+
| Show thinking timer | Show elapsed time next to collapsed `Thinking...` labels. |
|
|
174
|
+
| Working indicator mode | `animated` ticks every 80ms; switch to `static` if you see flashes when the chat overflows. |
|
|
@@ -30,6 +30,14 @@ export function autoRenameEnabled(cwd?: string): boolean {
|
|
|
30
30
|
return settingBoolean("sessionAutoRename.enabled", true, cwd);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// pi raises this exact prefix when a captured ExtensionContext is touched
|
|
34
|
+
// after newSession / fork / switchSession / reload. Recognising it lets us
|
|
35
|
+
// abort gracefully instead of crashing the host process.
|
|
36
|
+
export function isStaleCtxError(error: unknown): boolean {
|
|
37
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
38
|
+
return /extension ctx is stale/i.test(message);
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
function autoRenameCtxAlive(ctx: ExtensionContext): boolean {
|
|
34
42
|
// Auto-rename runs from a deferred timer / async path, so the captured
|
|
35
43
|
// extension context can be invalidated mid-flight by newSession / fork /
|
|
@@ -44,6 +52,16 @@ function autoRenameCtxAlive(ctx: ExtensionContext): boolean {
|
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
54
|
|
|
55
|
+
// Read a ctx property without crashing if the ctx has been replaced. Used
|
|
56
|
+
// for the cwd-derived setting reads that downstream helpers depend on.
|
|
57
|
+
function readCtxCwd(ctx: ExtensionContext): string | undefined {
|
|
58
|
+
try {
|
|
59
|
+
return ctx.cwd;
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
function autoRenameDebug(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error" = "info"): void {
|
|
48
66
|
if (!autoRenameCtxAlive(ctx)) return;
|
|
49
67
|
if (settingBoolean("sessionAutoRename.debug", false, ctx.cwd)) ctx.ui.notify(`[auto-rename] ${message}`, level);
|
|
@@ -230,7 +248,13 @@ function modelCost(model: any): number {
|
|
|
230
248
|
}
|
|
231
249
|
|
|
232
250
|
async function cheapestAvailableAutoRenameModel(ctx: ExtensionContext): Promise<AutoRenameAuth | undefined> {
|
|
233
|
-
|
|
251
|
+
let registry: any;
|
|
252
|
+
try {
|
|
253
|
+
registry = ctx.modelRegistry as any;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (isStaleCtxError(error)) return undefined;
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
234
258
|
const rawModels = typeof registry.getAvailable === "function" ? registry.getAvailable() : typeof registry.getAll === "function" ? registry.getAll() : [];
|
|
235
259
|
const models = Array.isArray(rawModels) ? rawModels.filter((model) => model?.input?.includes?.("text") ?? true) : [];
|
|
236
260
|
models.sort((a, b) => modelCost(a) - modelCost(b) || modelLabel(a).localeCompare(modelLabel(b)));
|
|
@@ -239,7 +263,8 @@ async function cheapestAvailableAutoRenameModel(ctx: ExtensionContext): Promise<
|
|
|
239
263
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
240
264
|
const headers = headerRecord(auth.headers);
|
|
241
265
|
if (auth.ok && (auth.apiKey || headers)) return { apiKey: auth.apiKey, headers, label: modelLabel(model), model, source: "cheapest" };
|
|
242
|
-
} catch {
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (isStaleCtxError(error)) return undefined;
|
|
243
268
|
// Try the next available model.
|
|
244
269
|
}
|
|
245
270
|
}
|
|
@@ -247,48 +272,58 @@ async function cheapestAvailableAutoRenameModel(ctx: ExtensionContext): Promise<
|
|
|
247
272
|
}
|
|
248
273
|
|
|
249
274
|
async function resolveAutoRenameModel(ctx: ExtensionContext, configured: string): Promise<AutoRenameAuth | undefined> {
|
|
250
|
-
if (configured.trim().toLowerCase() === "cheapest") return cheapestAvailableAutoRenameModel(ctx);
|
|
251
|
-
const model = resolveConfiguredModel(ctx, configured);
|
|
252
|
-
if (!model) {
|
|
253
|
-
autoRenameDebug(ctx, `model not found: ${configured}`, "warning");
|
|
254
|
-
return undefined;
|
|
255
|
-
}
|
|
256
275
|
try {
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
if (!
|
|
260
|
-
autoRenameDebug(ctx, `
|
|
276
|
+
if (configured.trim().toLowerCase() === "cheapest") return await cheapestAvailableAutoRenameModel(ctx);
|
|
277
|
+
const model = resolveConfiguredModel(ctx, configured);
|
|
278
|
+
if (!model) {
|
|
279
|
+
autoRenameDebug(ctx, `model not found: ${configured}`, "warning");
|
|
261
280
|
return undefined;
|
|
262
281
|
}
|
|
263
|
-
|
|
264
|
-
|
|
282
|
+
try {
|
|
283
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
284
|
+
const headers = headerRecord(auth.headers);
|
|
285
|
+
if (!auth.ok) {
|
|
286
|
+
autoRenameDebug(ctx, `auth unavailable for ${modelLabel(model)}: ${auth.error}`, "warning");
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
if (!auth.apiKey && !headers) {
|
|
290
|
+
autoRenameDebug(ctx, `no auth for ${modelLabel(model)}; use /login or models.json`, "warning");
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
return { apiKey: auth.apiKey, headers, label: modelLabel(model), model, source: configured };
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (isStaleCtxError(error)) return undefined;
|
|
296
|
+
autoRenameDebug(ctx, `auth failed for ${modelLabel(model)}: ${stringifyError(error)}`, "warning");
|
|
265
297
|
return undefined;
|
|
266
298
|
}
|
|
267
|
-
return { apiKey: auth.apiKey, headers, label: modelLabel(model), model, source: configured };
|
|
268
299
|
} catch (error) {
|
|
269
|
-
|
|
270
|
-
|
|
300
|
+
if (isStaleCtxError(error)) return undefined;
|
|
301
|
+
throw error;
|
|
271
302
|
}
|
|
272
303
|
}
|
|
273
304
|
|
|
274
305
|
export async function generateAutoRenameName(query: string, ctx: ExtensionContext, fullConversation = false): Promise<{ name?: string; source: string }> {
|
|
275
|
-
|
|
276
|
-
|
|
306
|
+
// Snapshot cwd once. If the ctx has already gone stale, fall through with
|
|
307
|
+
// undefined cwd — downstream settings reads handle that and the rename
|
|
308
|
+
// will abort with "none".
|
|
309
|
+
const cwd = readCtxCwd(ctx);
|
|
310
|
+
const maxNameChars = Math.max(20, Math.floor(settingNumber("sessionAutoRename.maxNameChars", DEFAULT_AUTO_RENAME_NAME_CHARS, cwd)));
|
|
311
|
+
const maxTokens = Math.max(16, Math.floor(settingNumber("sessionAutoRename.maxTokens", DEFAULT_AUTO_RENAME_MAX_TOKENS, cwd)));
|
|
277
312
|
const prompt = fullConversation
|
|
278
313
|
? `${AUTO_RENAME_SYSTEM_PROMPT}\n\nName this session using the conversation transcript below. Return only the title.\n\n${query}`
|
|
279
|
-
: autoRenamePrompt(query,
|
|
314
|
+
: autoRenamePrompt(query, cwd);
|
|
280
315
|
const message: Message = {
|
|
281
316
|
content: [{ text: prompt, type: "text" }],
|
|
282
317
|
role: "user",
|
|
283
318
|
timestamp: Date.now(),
|
|
284
319
|
};
|
|
285
320
|
|
|
286
|
-
for (const configured of autoRenameModelSettings(
|
|
321
|
+
for (const configured of autoRenameModelSettings(cwd)) {
|
|
287
322
|
const resolved = await resolveAutoRenameModel(ctx, configured);
|
|
288
323
|
if (!resolved) continue;
|
|
289
324
|
try {
|
|
290
325
|
const controller = new AbortController();
|
|
291
|
-
const timeoutMs = Math.max(1000, Math.floor(settingNumber("sessionAutoRename.timeoutMs", DEFAULT_AUTO_RENAME_TIMEOUT_MS,
|
|
326
|
+
const timeoutMs = Math.max(1000, Math.floor(settingNumber("sessionAutoRename.timeoutMs", DEFAULT_AUTO_RENAME_TIMEOUT_MS, cwd)));
|
|
292
327
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
293
328
|
timeout.unref?.();
|
|
294
329
|
try {
|
|
@@ -308,12 +343,13 @@ export async function generateAutoRenameName(query: string, ctx: ExtensionContex
|
|
|
308
343
|
clearTimeout(timeout);
|
|
309
344
|
}
|
|
310
345
|
} catch (error) {
|
|
346
|
+
if (isStaleCtxError(error)) return { source: "none" };
|
|
311
347
|
autoRenameDebug(ctx, `${resolved.label} failed: ${stringifyError(error)}`, "warning");
|
|
312
348
|
}
|
|
313
349
|
}
|
|
314
350
|
|
|
315
|
-
const deterministic = deterministicAutoRenameName(query,
|
|
316
|
-
return deterministic ? { name: deterministic, source: `fallback:${autoRenameFallbackMode(
|
|
351
|
+
const deterministic = deterministicAutoRenameName(query, cwd);
|
|
352
|
+
return deterministic ? { name: deterministic, source: `fallback:${autoRenameFallbackMode(cwd)}` } : { source: "none" };
|
|
317
353
|
}
|
|
318
354
|
|
|
319
355
|
export function withAutoRenamePrefix(name: string, cwd?: string): string {
|
package/extensions/qol.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
conversationTranscriptText,
|
|
51
51
|
firstUserMessageText,
|
|
52
52
|
generateAutoRenameName,
|
|
53
|
+
isStaleCtxError,
|
|
53
54
|
withAutoRenamePrefix,
|
|
54
55
|
} from "./qol/session-rename.js";
|
|
55
56
|
import {
|
|
@@ -122,34 +123,56 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
122
123
|
};
|
|
123
124
|
|
|
124
125
|
const attemptAutoRename = async (ctx: ExtensionContext, options: { force?: boolean; fullConversation?: boolean; notify?: boolean } = {}) => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const branch = ctx.sessionManager.getBranch?.() ?? [];
|
|
130
|
-
const maxInputChars = Math.max(200, Math.floor(settingNumber("sessionAutoRename.maxInputChars", 2000, ctx.cwd)));
|
|
131
|
-
const sourceText = options.fullConversation ? conversationTranscriptText(branch, maxInputChars) : firstUserMessageText(branch);
|
|
132
|
-
if (!sourceText) {
|
|
133
|
-
if (force) autoRenameNotify(ctx, "No user message found to name this session.", "warning", true);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const generation = autoRenameGeneration;
|
|
137
|
-
if (!force) autoRenameAttempted = true;
|
|
138
|
-
autoRenameInProgress = true;
|
|
126
|
+
// All ctx reads here can throw if the captured context has been replaced
|
|
127
|
+
// by newSession / fork / switchSession / reload. Wrap the whole body so
|
|
128
|
+
// a stale ctx aborts the rename quietly instead of crashing pi (and
|
|
129
|
+
// killing the in-flight tool dispatch that triggered agent_end).
|
|
139
130
|
try {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
131
|
+
const force = options.force === true;
|
|
132
|
+
let cwd: string | undefined;
|
|
133
|
+
try {
|
|
134
|
+
cwd = ctx.cwd;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (isStaleCtxError(error)) return;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
if (!force && !autoRenameEnabled(cwd)) return;
|
|
140
|
+
if (autoRenameInProgress) return;
|
|
141
|
+
if (!force && (autoRenameAttempted || pi.getSessionName())) return;
|
|
142
|
+
let branch;
|
|
143
|
+
try {
|
|
144
|
+
branch = ctx.sessionManager.getBranch?.() ?? [];
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (isStaleCtxError(error)) return;
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
const maxInputChars = Math.max(200, Math.floor(settingNumber("sessionAutoRename.maxInputChars", 2000, cwd)));
|
|
150
|
+
const sourceText = options.fullConversation ? conversationTranscriptText(branch, maxInputChars) : firstUserMessageText(branch);
|
|
151
|
+
if (!sourceText) {
|
|
152
|
+
if (force) autoRenameNotify(ctx, "No user message found to name this session.", "warning", true);
|
|
144
153
|
return;
|
|
145
154
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
const generation = autoRenameGeneration;
|
|
156
|
+
if (!force) autoRenameAttempted = true;
|
|
157
|
+
autoRenameInProgress = true;
|
|
158
|
+
try {
|
|
159
|
+
const result = await generateAutoRenameName(sourceText, ctx, options.fullConversation === true);
|
|
160
|
+
if (generation !== autoRenameGeneration) return;
|
|
161
|
+
if (!result.name) {
|
|
162
|
+
autoRenameNotify(ctx, "No session name generated.", "warning", options.notify === true || force);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (!force && pi.getSessionName()) return;
|
|
166
|
+
const name = withAutoRenamePrefix(result.name, cwd);
|
|
167
|
+
if (!name) return;
|
|
168
|
+
pi.setSessionName(name);
|
|
169
|
+
autoRenameNotify(ctx, `Session named: ${name} (${result.source})`, "info", options.notify === true || force);
|
|
170
|
+
} finally {
|
|
171
|
+
if (generation === autoRenameGeneration) autoRenameInProgress = false;
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (isStaleCtxError(error)) return;
|
|
175
|
+
throw error;
|
|
153
176
|
}
|
|
154
177
|
};
|
|
155
178
|
|
|
@@ -171,7 +194,10 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
171
194
|
|
|
172
195
|
const startThinkingTimerTicker = () => {
|
|
173
196
|
if (thinkingTimerTicker) return;
|
|
174
|
-
|
|
197
|
+
// 250ms cadence (was 100ms): user-visible precision of "thinking 1.2s" still feels live,
|
|
198
|
+
// but render rate drops 2.5x. Every tick mutates an inline chat label, which makes the
|
|
199
|
+
// pi-tui above-viewport diff (firstChanged < prevViewportTop) fire as full-screen redraws.
|
|
200
|
+
thinkingTimerTicker = setInterval(tickThinkingTimer, 250);
|
|
175
201
|
thinkingTimerTicker.unref?.();
|
|
176
202
|
};
|
|
177
203
|
|
|
@@ -185,6 +211,27 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
185
211
|
thinkingTimerStore.enabled = !!ctx?.hasUI && settingBoolean("thinkingTimer.enabled", true, ctx?.cwd);
|
|
186
212
|
};
|
|
187
213
|
|
|
214
|
+
// Working indicator mode.
|
|
215
|
+
// The built-in pi-tui Loader ticks every 80ms during streaming. Each tick
|
|
216
|
+
// mutates a line in statusContainer; once total rendered content exceeds the
|
|
217
|
+
// terminal viewport (overlay content like /tree, or chat overflow), every tick
|
|
218
|
+
// trips pi-tui's firstChanged < prevViewportTop branch and triggers a full
|
|
219
|
+
// screen + scrollback clear (visible flash). This setting lets the user trade
|
|
220
|
+
// the spinner animation away for a stable display in overflow scenarios.
|
|
221
|
+
// Implementation note: Loader.restartAnimation() bails out when frames.length
|
|
222
|
+
// is <= 1, so a single-frame indicator does NOT start the setInterval at all.
|
|
223
|
+
const applyWorkingIndicatorMode = (ctx: ExtensionContext): void => {
|
|
224
|
+
if (!ctx.hasUI) return;
|
|
225
|
+
const mode = settingString("workingIndicator.mode", "animated", ctx.cwd);
|
|
226
|
+
if (mode === "static") {
|
|
227
|
+
ctx.ui.setWorkingVisible(true);
|
|
228
|
+
ctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg("accent", "●")] });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
ctx.ui.setWorkingVisible(true);
|
|
232
|
+
ctx.ui.setWorkingIndicator(undefined);
|
|
233
|
+
};
|
|
234
|
+
|
|
188
235
|
const updateThinkingTimerEnabled = (ctx: ExtensionContext): boolean => {
|
|
189
236
|
thinkingTimerStore.cwd = ctx.cwd;
|
|
190
237
|
thinkingTimerStore.theme = ctx.ui.theme;
|
|
@@ -218,15 +265,36 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
218
265
|
const delayMs = Math.max(1, Math.floor(settingNumber("compaction.idleTimeoutSeconds", DEFAULT_IDLE_COMPACTION_SECONDS, ctx.cwd))) * 1000;
|
|
219
266
|
idleCompactionTimer = setTimeout(() => {
|
|
220
267
|
idleCompactionTimer = undefined;
|
|
221
|
-
|
|
268
|
+
// Same stale-ctx risk as auto-rename: the captured ctx may be invalidated
|
|
269
|
+
// by newSession/fork/switchSession/reload between schedule and fire.
|
|
270
|
+
// Bail silently if so; the next session_start will reschedule.
|
|
271
|
+
try {
|
|
272
|
+
if (!ctx.isIdle?.()) return;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (isStaleCtxError(error)) return;
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
222
277
|
const latestReason = compactionTriggerReason(ctx);
|
|
223
278
|
if (!latestReason) return;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
279
|
+
const notifySafely = (message: string, level: "info" | "error") => {
|
|
280
|
+
try {
|
|
281
|
+
if (ctx.hasUI && settingBoolean("compaction.notify", true, ctx.cwd)) ctx.ui.notify(message, level);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (isStaleCtxError(error)) return;
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
notifySafely(`QOL idle compaction starting: ${latestReason}`, "info");
|
|
288
|
+
try {
|
|
289
|
+
ctx.compact?.({
|
|
290
|
+
customInstructions: `QOL idle compaction triggered after inactivity because ${latestReason}. Preserve current task state, decisions, files, blockers, and next steps.`,
|
|
291
|
+
onComplete: () => notifySafely("QOL idle compaction completed.", "info"),
|
|
292
|
+
onError: (error: Error) => notifySafely(`QOL idle compaction failed: ${stringifyError(error)}`, "error"),
|
|
293
|
+
});
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (isStaleCtxError(error)) return;
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
230
298
|
}, delayMs);
|
|
231
299
|
idleCompactionTimer.unref?.();
|
|
232
300
|
};
|
|
@@ -447,6 +515,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
447
515
|
installPendingQueueThemePatch(ctx);
|
|
448
516
|
if (ctx.hasUI) {
|
|
449
517
|
ctx.ui.setHiddenThinkingLabel(hiddenThinkingLabel(ctx.ui.theme, ctx.cwd));
|
|
518
|
+
applyWorkingIndicatorMode(ctx);
|
|
450
519
|
gitState = makeFallbackGitState(ctx.cwd);
|
|
451
520
|
void refreshStatusline(ctx);
|
|
452
521
|
installSessionTitle(ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillagreen/pi-qol",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
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": [
|
|
@@ -800,6 +800,20 @@
|
|
|
800
800
|
"default": true,
|
|
801
801
|
"category": "Thinking",
|
|
802
802
|
"apply": "live"
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
"key": "workingIndicator.mode",
|
|
806
|
+
"label": "Working indicator mode",
|
|
807
|
+
"description": "Control Pi's built-in streaming spinner. The default animated spinner ticks every 80ms; in terminals where rendered content overflows the viewport (chat scrolled past the top, /tree open, etc.) each tick can trigger a pi-tui full-screen redraw and visible flash. Use 'static' for a single non-animating dot that keeps the working row visible without the 80ms timer.",
|
|
808
|
+
"type": "enum",
|
|
809
|
+
"enumValues": [
|
|
810
|
+
"animated",
|
|
811
|
+
"static"
|
|
812
|
+
],
|
|
813
|
+
"default": "animated",
|
|
814
|
+
"category": "Thinking",
|
|
815
|
+
"apply": "reload",
|
|
816
|
+
"requiresReload": true
|
|
803
817
|
}
|
|
804
818
|
]
|
|
805
819
|
}
|