@vanillagreen/pi-qol 1.8.0 → 2.0.1
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 +31 -102
- package/extensions/qol/bridges.ts +5 -5
- package/extensions/qol/budget-guard-runtime.ts +1 -1
- package/extensions/qol/compaction-handoff.ts +10 -9
- package/extensions/qol/compaction.ts +5 -0
- package/extensions/qol/constants.ts +12 -12
- package/extensions/qol/editor.ts +1 -1
- package/extensions/qol/glyphs.ts +1 -76
- package/extensions/qol/handoff.ts +2 -2
- package/extensions/qol/rate-limit-auto-resume.ts +1 -1
- package/extensions/qol/session-search/context.ts +2 -2
- package/extensions/qol/session-search/index.ts +2 -2
- package/extensions/qol/settings.ts +24 -14
- package/extensions/qol.ts +1 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -1,120 +1,49 @@
|
|
|
1
|
-
# pi-qol
|
|
1
|
+
# @vanillagreen/pi-qol
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-

|
|
5
|
-

|
|
3
|
+
A Pi extension for session controls, prompt editing and notifications. Users can configure each feature separately.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## Highlights
|
|
10
|
-
|
|
11
|
-
- Compact statusline with repo, branch, model, thinking level, and context percent; can be disabled.
|
|
12
|
-
- Newline-insert in the editor (multi-line drafts without auto-submit), with a fallback binding for terminals that can't distinguish the primary key.
|
|
13
|
-
- Sessions auto-name from your first prompt. `/rename` overrides anytime.
|
|
14
|
-
- Session search reads prompt snippets line-by-line, so very large session JSONL files do not have to be materialized just to browse, search, or import context.
|
|
15
|
-
- Optional rate-limit auto-resume sends a configurable continuation after reset.
|
|
16
|
-
- Permission gate prompts before risky `bash` commands. Default match: `rm -Rf`.
|
|
17
|
-
- Notifications for ready, questions, blocked states, and task completion.
|
|
18
|
-
- Thinking timer next to collapsed `Thinking...` labels.
|
|
19
|
-
- Caveman badge and a mode-cycling shortcut when `pi-caveman` is loaded.
|
|
20
|
-
- Subagent-name badge in `pi-agents-tmux` child panes.
|
|
5
|
+
  
|
|
21
6
|
|
|
22
7
|
## Install
|
|
23
8
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Via [npm](https://www.npmjs.com/package/@vanillagreen/pi-qol):
|
|
9
|
+
- npm: `pi install npm:@vanillagreen/pi-qol`.
|
|
10
|
+
- kendex: add the declaration below to the project's `kendex.toml`, or to `~/.config/kendex/kendex.toml` for user scope. Run `kendex update-pi`.
|
|
27
11
|
|
|
28
|
-
```
|
|
29
|
-
pi
|
|
12
|
+
```toml
|
|
13
|
+
[pi-extensions."@vanillagreen/pi-qol"]
|
|
14
|
+
source = "kendex"
|
|
30
15
|
```
|
|
31
16
|
|
|
32
|
-
|
|
17
|
+
Restart Pi after installation. Use `kendex update-pi --check` to preview the installation.
|
|
33
18
|
|
|
34
|
-
|
|
35
|
-
cargo install --git https://github.com/vanillagreencom/vstack.git vstack
|
|
36
|
-
vstack add vanillagreencom/vstack --pi-extension pi-qol --harness pi -y
|
|
37
|
-
```
|
|
19
|
+
## Features
|
|
38
20
|
|
|
39
|
-
|
|
21
|
+
- Show repository, model and context information beside the editor.
|
|
22
|
+
- Name and search sessions.
|
|
23
|
+
- Schedule prompts and prepare handoff drafts.
|
|
24
|
+
- Ask before configured shell commands run.
|
|
25
|
+
- Send terminal and desktop notifications.
|
|
26
|
+
- Configure summaries and compaction for long sessions.
|
|
40
27
|
|
|
41
|
-
|
|
42
|
-
| --- | --- |
|
|
43
|
-
| `/qol` | Open settings (or print status if extension-manager isn't installed). |
|
|
44
|
-
| `/qol notify-test` | Send a test notification. |
|
|
45
|
-
| `/rename [name]` | Set or show the current session's name. |
|
|
46
|
-
| `/qol:rename` / `/qol:rename:full` | Regenerate the session name from the first prompt, or from the full conversation. |
|
|
47
|
-
| `/context` | Show a Claude-style context-window usage breakdown by category. |
|
|
48
|
-
| `/search [query]` | Open previous-session search with snippet previews; the configured shortcut opens it instantly. |
|
|
49
|
-
| `/search:refresh` | Refresh the session cache. |
|
|
50
|
-
| `/handoff <goal>` | Draft a focused handoff prompt for a new session. |
|
|
51
|
-
| `/schedule <delay> <message>` | Send a user message after a timer without invoking the model now. Example: `/schedule 1h45m retry the previous request`. |
|
|
28
|
+
## How it works
|
|
52
29
|
|
|
53
|
-
|
|
30
|
+
The extension reads your enabled features when Pi starts. It adds their editor controls, commands and event handlers. Session actions update the session or queue messages for the agent. Notifications report events through your selected channels. Compaction settings control when long conversations are summarized.
|
|
54
31
|
|
|
55
32
|
## Settings
|
|
56
33
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
| Group | Setting | What it does |
|
|
60
|
-
| --- | --- | --- |
|
|
61
|
-
| Statusline | Enable QOL editor helpers | Master toggle for QOL statusline, commands, notifications, search, compaction, and editor helpers. |
|
|
62
|
-
| Statusline | Show compact statusline | Render or disable the QOL statusline row. |
|
|
63
|
-
| Statusline | Replace built-in footer | Hide Pi's default footer while the QOL statusline is enabled. |
|
|
64
|
-
| Statusline | Use π prompt editor | Use the compact prompt editor. |
|
|
65
|
-
| Statusline | Show session name title | Show the session name above the prompt and in the tmux pane title; refreshes as soon as Pi reports a session metadata change. |
|
|
66
|
-
| Statusline | Sync session name to tmux window name | Rename the tmux window to `π <session>`. |
|
|
67
|
-
| Statusline | Input bottom padding | Blank lines below the prompt. |
|
|
68
|
-
| Statusline | Show dirty marker | Append `*` to the branch when the worktree is dirty. |
|
|
69
|
-
| Input | shift+enter inserts newline | Insert a newline instead of submitting. |
|
|
70
|
-
| Input | Fallback newline key | Alternate binding for terminals that can't send the primary one. |
|
|
71
|
-
| Input | Style pending queue preview | Highlight Pi's pending-queue preview with a green left bar. |
|
|
72
|
-
| Input | Style image chips | Render `[Image #N]` placeholders as distinct chips. |
|
|
73
|
-
| Input | Show attachment count | Show a status badge when the draft has image placeholders. |
|
|
74
|
-
| Session, Handoff, Context window, Session search | Enable /rename, /schedule, /handoff, /context, session search | One toggle each: register that command. Session search also registers its overlay; `/schedule` is useful for retrying after rate limits reset. |
|
|
75
|
-
| Session | Auto-resume after rate limits | Send the configured continuation after a detected reset; cancels on newer turn. |
|
|
76
|
-
| Session | Auto-name new sessions | Generate a friendly session name from the first prompt. |
|
|
77
|
-
| Session | Auto-rename model / fallback model | Model used for title generation, and the model tried when the primary fails. |
|
|
78
|
-
| Session | Deterministic fallback | Title-case words, truncated prompt, or none if all model calls fail. |
|
|
79
|
-
| Session | Auto-rename prefix | Optional static prefix on every generated name. |
|
|
80
|
-
| Session | Notify on auto-rename | Show a notification when auto-renaming. |
|
|
81
|
-
| Handoff | Review handoff prompt | Open an editor to edit the generated prompt before creating the session. |
|
|
82
|
-
| Session search | Session search shortcut | Configurable; set to `none` to disable. |
|
|
83
|
-
| Session search | Result limit | Max matching prompts returned. |
|
|
84
|
-
| Session search | Visible session rows | Rows shown before scrolling; defaults to `8`. |
|
|
85
|
-
| Session search | Preview snippets | Matching snippets shown on the preview screen. |
|
|
86
|
-
| Session search | Session cache TTL | Seconds before the session list refreshes; `0` keeps it until you run `/search:refresh`. |
|
|
87
|
-
| Permission gate | Prompt before risky bash commands | Ask before bash commands matching the command list. Off by default; when enabled, non-interactive matches are blocked. |
|
|
88
|
-
| Permission gate | Commands to prompt for | Comma-separated literal fragments or `/regex/flags`. |
|
|
89
|
-
| Permission gate | Approval preview lines / characters | Cap the approval-prompt preview height and width. |
|
|
90
|
-
| Compaction | Custom compaction summaries | Use QOL summaries instead of Pi's default, with Pi-standard transient provider retries when available. |
|
|
91
|
-
| Compaction | Compaction model | Summarizer model. Defaults to `current`, meaning Pi's active model; set a provider/model when you want a dedicated larger-context summarizer. Thinking suffixes through `:max` are accepted, and Pi-resolved header/environment authentication is forwarded. |
|
|
92
|
-
| Compaction | Compaction detail profile | `concise`, `balanced`, or `exhaustive`. |
|
|
93
|
-
| Compaction | Include previous summary | Pass the previous summary for iterative continuity. |
|
|
94
|
-
| Compaction | Fallback to Pi default compaction | Run Pi's default compaction if QOL's fails. |
|
|
95
|
-
| Compaction | Show compaction notifications | Notify on compaction start/fail/complete. |
|
|
96
|
-
| Compaction | Custom branch summaries | Use QOL's chunked summarizer for `/tree` branch summaries; branch summaries do not write handoff artifacts. |
|
|
97
|
-
| Compaction | Remote compaction endpoint | Call a remote HTTP summarizer instead of a model. |
|
|
98
|
-
| Compaction | Idle compaction trigger | Auto-compact after the session sits idle above a token threshold. Idle thresholds (token threshold, idle delay, fixed token limit, percent limit) tune when it fires. |
|
|
99
|
-
| Long-session budget guard | Long-session budget guard | Master toggle for threshold-triggered bounded compaction. Default on. |
|
|
100
|
-
| Long-session budget guard | Budget guard percent / token limit | Context-window percentage that fires the guard (default `85`; `-1` disables percent-based firing), and the absolute token count that fires it (default `-1`, meaning percent only). |
|
|
101
|
-
| Long-session budget guard | Chunked compaction input cap | Max serialized characters per summarization request; long transcripts are chunked and summarized in bounded pieces so the compaction call itself cannot exceed provider buffer limits. `0` disables chunking. Default `240000`. |
|
|
102
|
-
| Long-session budget guard | Write pre-compaction handoff artifact | Write a pre-compaction handoff artifact (previous summary, last task state, referenced files and artifacts) for budget-guard and enabled custom session compactions; branch summaries never write one. Write failures surface as a QOL warning notification. Default on. |
|
|
103
|
-
| Long-session budget guard | Transcript-risk warn budget (chars) | `/context` warns when the serialized payload of messages-to-send exceeds this many characters, even if tokens are still below the context window. `0` disables. Default `600000`. |
|
|
104
|
-
| Thinking | Hidden thinking label | Label shown when thinking blocks are hidden. |
|
|
105
|
-
| Thinking | Show thinking timer | Show elapsed time next to collapsed `Thinking...` labels. |
|
|
106
|
-
| Thinking | Working indicator mode | `animated` ticks every 80ms; switch to `static` if you see flashes when the chat overflows. |
|
|
107
|
-
|
|
108
|
-
Advanced auto-rename settings (input cap, title length, output tokens, timeout, custom prompt template, debug logging) and session-search summary settings (model, max tokens, input cap — they tune the summarizer when you import context from a previous session) sit alongside the rows above.
|
|
109
|
-
|
|
110
|
-
`/context` also estimates the serialized payload size of the messages that would be sent on the next request. When that payload crosses **Transcript-risk warn budget (chars)** a `Transcript risk` block appears below the compact buffer section even if token count is still under the context window — useful for catching large blob-shaped tool outputs that inflate the request long before token count alone would page anyone.
|
|
111
|
-
|
|
112
|
-
### Notifications
|
|
113
|
-
|
|
114
|
-
Master toggle: **Enable notifications**. Triggers (notify when): ready, direction needed, question popups, all tasks complete, critical/blocked. Channels: terminal bell, **Mute bell sound**, native terminal notifications (OSC 777/99 or Windows toast), tmux `display-message`, tmux window marking, OSC passthrough, and an optional in-Pi UI notice. Tuning: cooldown seconds, title, ready message, body length, tmux durations. **Terminal notification protocol** picks between OSC 99 (Kitty) and OSC 777 automatically. **Bell when tmux window active** is off so you don't get bells while looking at Pi. **Mute bell sound** keeps notification routing enabled but suppresses QOL-emitted BEL bytes and uses ST terminators for OSC 777/99 where supported; terminals or operating systems may still play their own sound for native notifications outside QOL control. **tmux native via client TTY** sends OSC notifications to attached tmux clients so notifications still appear when the Pi window is inactive. Use `/qol notify-test` to verify your terminal/tmux setup, including silent behavior with **Mute bell sound** enabled.
|
|
34
|
+
The settings editor writes project values to `.pi/settings.json`. The default user file is `~/.pi/agent/settings.json`. `PI_CODING_AGENT_DIR` changes the user directory. Package values are stored under `kendex.extensionManager.config["@vanillagreen/pi-qol"]`.
|
|
115
35
|
|
|
116
|
-
|
|
36
|
+
Open `/extensions:settings`; settings appear under the **QOL** tab. Project settings in `.pi/settings.json` apply only after Pi marks the workspace trusted. `glyphStyle` picks `unicode` or `ascii` symbols, and `@vanillagreen/pi-tool-renderer`'s `globalGlyphStyleOverride` wins when set.
|
|
117
37
|
|
|
118
|
-
|
|
38
|
+
- `enabled`: package toggle for everything below.
|
|
39
|
+
- Statusline and editor: `statusline.enabled`, `replaceFooter`, `compactPrompt`, `showSessionNameTitle`, `showSessionNameWindow`, `inputBottomPaddingLines`, `gitRefreshTimeoutMs`, `showDirtyMarker`, `newlineOnShiftEnter`, `newlineFallbackKey`, `pendingQueue.asciiGreen`, `showImageChips`, `showAttachmentCountInStatus`.
|
|
40
|
+
- Commands: `enableSessionNameCommand`, `enableHandoffCommand`, `enableScheduleCommand`, `enableContextCommand`, `handoffReviewPrompt`.
|
|
41
|
+
- Session naming: `sessionAutoRename.*` (model, fallback model, deterministic fallback, prefix, prompt, limits, notify, debug).
|
|
42
|
+
- Session search: `sessionSearch.*` (shortcut, result and row limits, snippets, overlay width, cache TTL, summary model and limits).
|
|
43
|
+
- Rate limits: `rateLimitAutoResume.*`.
|
|
44
|
+
- Permission gate: `permissionGate.enabled`, `permissionGate.commands` (comma-separated literal fragments or `/regex/flags`), `permissionGate.previewLines`, `permissionGate.previewChars`.
|
|
45
|
+
- Notifications: `notification.*` (triggers, channels, tmux options, protocol, cooldown, title and body).
|
|
46
|
+
- Compaction and budget guard: `compaction.*` (custom summaries, model, profile, remote endpoint, branch summaries, idle trigger, budget guard thresholds, chunk input cap, handoff artifact, transcript-risk budget).
|
|
47
|
+
- Thinking: `thinkingLabel.text`, `thinkingTimer.enabled`, `workingIndicator.mode` (`static` if the animated indicator flashes).
|
|
119
48
|
|
|
120
|
-
|
|
49
|
+
Maintainer notes are in [DEVELOPMENT.md](DEVELOPMENT.md).
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
CAVEMAN_BRIDGE_SYMBOL,
|
|
3
3
|
PI_AGENTS_STATUSLINE_SYMBOL,
|
|
4
4
|
QUESTION_SERVICE_SYMBOL,
|
|
5
|
-
|
|
5
|
+
KENDEX_MODAL_LOCK_SYMBOL,
|
|
6
6
|
} from "./constants.js";
|
|
7
7
|
|
|
8
8
|
export interface CavemanBridge {
|
|
@@ -37,7 +37,7 @@ export interface QuestionServiceLike {
|
|
|
37
37
|
subscribe(listener: (event: any) => void): () => void;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
interface
|
|
40
|
+
interface kendexModalLock {
|
|
41
41
|
depth: number;
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -63,11 +63,11 @@ export function getQuestionService(): QuestionServiceLike | undefined {
|
|
|
63
63
|
return undefined;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
export function
|
|
66
|
+
export function acquirekendexModalLock(): () => void {
|
|
67
67
|
const host = globalThis as unknown as Record<PropertyKey, unknown>;
|
|
68
|
-
const existing = host[
|
|
68
|
+
const existing = host[KENDEX_MODAL_LOCK_SYMBOL] as kendexModalLock | undefined;
|
|
69
69
|
const lock = existing && typeof existing.depth === "number" ? existing : { depth: 0 };
|
|
70
|
-
host[
|
|
70
|
+
host[KENDEX_MODAL_LOCK_SYMBOL] = lock;
|
|
71
71
|
lock.depth += 1;
|
|
72
72
|
let released = false;
|
|
73
73
|
return () => {
|
|
@@ -116,7 +116,7 @@ export class BudgetGuardDriver {
|
|
|
116
116
|
return true;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
/** Dispatch a trigger
|
|
119
|
+
/** Dispatch a trigger staged by agent_end. */
|
|
120
120
|
dispatchPending(input: GuardPendingDispatchInput): GuardDispatchResult {
|
|
121
121
|
const completed = (outcome: DispatchOutcome): GuardDispatchResult => ({ completion: Promise.resolve(), outcome });
|
|
122
122
|
const generation = input.generation ?? this.generation;
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
// so it can be unit-tested without the pi-coding-agent / pi-ai peer deps.
|
|
6
6
|
|
|
7
7
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { homedir } from "node:os";
|
|
9
8
|
import { basename, dirname, join, resolve } from "node:path";
|
|
10
9
|
|
|
11
10
|
import { QOL_BUDGET_HANDOFF_FOLDER, QOL_BUDGET_HANDOFF_LATEST } from "./constants.js";
|
|
11
|
+
import { expandHome } from "./settings.js";
|
|
12
12
|
|
|
13
13
|
export interface QolBudgetHandoff {
|
|
14
14
|
reason: string;
|
|
@@ -22,14 +22,15 @@ export interface QolBudgetHandoff {
|
|
|
22
22
|
model?: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
25
|
+
/** Root-anchored as `crates/core/src/harness/pi.rs::pi_root_is_absolute_for`
|
|
26
|
+
* means it, which `isAbsolute` is not: it calls a driveless `\root` absolute
|
|
27
|
+
* where the renderer does not, putting the two on different roots. Hoisted, so
|
|
28
|
+
* a circular import cannot reach it inside a temporal dead zone. */
|
|
29
|
+
function rootAnchored(path: string, windows: boolean): boolean { return windows ? /^(?:[A-Za-z]:[\\/]|[\\/]{2}[^\\/]+[\\/][^\\/]+)/.test(path) : path.startsWith("/"); }
|
|
30
30
|
|
|
31
31
|
export function piUserDir(): string {
|
|
32
|
-
|
|
32
|
+
const override = expandHome(process.env.PI_CODING_AGENT_DIR?.trim() || "");
|
|
33
|
+
return resolve(rootAnchored(override, process.platform === "win32") ? override : expandHome("~/.pi/agent"));
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export function safeFileName(value: string): string {
|
|
@@ -83,7 +84,7 @@ const ARTIFACT_REF_PATTERN = /(?:^|\s|["'`(\[<])((?:\.{1,2}\/|\/|~\/)?[\w.\-+@/]
|
|
|
83
84
|
|
|
84
85
|
/**
|
|
85
86
|
* Collects up to `maxRefs` file path-shaped strings from message contents,
|
|
86
|
-
* walking newest-first.
|
|
87
|
+
* walking newest-first. Gives the handoff artifact a quick "if you
|
|
87
88
|
* are recovering, here are recent files" pointer list.
|
|
88
89
|
*/
|
|
89
90
|
export function collectArtifactRefs(branch: unknown[], maxRefs = 20): string[] {
|
|
@@ -151,7 +152,7 @@ export function handoffStampedPath(baseDir: string, timestamp: number): string {
|
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
export function handoffBaseDir(sessionId: string, root: string = piUserDir()): string {
|
|
154
|
-
return join(root, "
|
|
155
|
+
return join(root, "kendex", "sessions", safeFileName(sessionId), QOL_BUDGET_HANDOFF_FOLDER);
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
export interface WriteHandoffOptions {
|
|
@@ -186,6 +186,11 @@ async function singleShotSummary(ctx: ExtensionContext, request: SummarizeReques
|
|
|
186
186
|
if (response.stopReason === "error" || response.stopReason === "aborted") {
|
|
187
187
|
throw new Error(response.errorMessage || `Summary request stopped: ${response.stopReason}`);
|
|
188
188
|
}
|
|
189
|
+
// A length stop carries text, but the text ends wherever the cap fell; as a
|
|
190
|
+
// continuation checkpoint it would silently drop the rest of the context.
|
|
191
|
+
if (response.stopReason === "length") {
|
|
192
|
+
throw new Error("Summary generation hit the token cap and the summary is incomplete");
|
|
193
|
+
}
|
|
189
194
|
const summary = response.content
|
|
190
195
|
.filter((content): content is { type: "text"; text: string } => content.type === "text")
|
|
191
196
|
.map((content) => content.text)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const INSTALL_SYMBOL = Symbol.for("
|
|
1
|
+
export const INSTALL_SYMBOL = Symbol.for("kendex.pi-qol.installed");
|
|
2
2
|
export const CONFIG_ID = "@vanillagreen/pi-qol";
|
|
3
3
|
|
|
4
4
|
export const STATUS_KEY = "qol-attachments";
|
|
@@ -10,22 +10,22 @@ export const SESSION_MANAGER_STATUS_KEY = "session-manager";
|
|
|
10
10
|
export const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tif", ".tiff", ".heic", ".heif"]);
|
|
11
11
|
export const IMAGE_PATH_PATTERN = /(^|[\s(\[{<"'`])(@?(?:~|\.\.?|\/)[^\s)\]}>"'`]+?\.(?:png|jpe?g|gif|webp|bmp|tiff?|heic|heif))(?=$|[\s)\]}>"'`,.;:!?])/gi;
|
|
12
12
|
|
|
13
|
-
export const QUESTION_SERVICE_SYMBOL = Symbol.for("
|
|
14
|
-
export const QOL_NOTIFICATION_SERVICE_SYMBOL = Symbol.for("
|
|
15
|
-
export const
|
|
16
|
-
export const CAVEMAN_BRIDGE_SYMBOL = Symbol.for("
|
|
17
|
-
export const PI_AGENTS_STATUSLINE_SYMBOL = Symbol.for("
|
|
13
|
+
export const QUESTION_SERVICE_SYMBOL = Symbol.for("kendex.pi-questions.service");
|
|
14
|
+
export const QOL_NOTIFICATION_SERVICE_SYMBOL = Symbol.for("kendex.pi-qol.notification-service");
|
|
15
|
+
export const KENDEX_MODAL_LOCK_SYMBOL = Symbol.for("kendex.pi.modal-lock");
|
|
16
|
+
export const CAVEMAN_BRIDGE_SYMBOL = Symbol.for("kendex.pi.caveman");
|
|
17
|
+
export const PI_AGENTS_STATUSLINE_SYMBOL = Symbol.for("kendex.pi-agents-tmux.statusline");
|
|
18
18
|
|
|
19
19
|
export const CAVEMAN_ICON_ACTIVE = "\uee9a";
|
|
20
20
|
export const CAVEMAN_ICON_INACTIVE = "\u{f19e0}";
|
|
21
21
|
|
|
22
|
-
export const THINKING_TIMER_STORE_SYMBOL = Symbol.for("
|
|
23
|
-
export const THINKING_TIMER_PATCH_SYMBOL = Symbol.for("
|
|
24
|
-
export const SESSION_SEARCH_PENDING_SYMBOL = Symbol.for("
|
|
25
|
-
export const PENDING_QUEUE_THEME_PATCH_SYMBOL = Symbol.for("
|
|
26
|
-
export const STATUS_TEXT_ALIGNMENT_PATCH_SYMBOL = Symbol.for("
|
|
22
|
+
export const THINKING_TIMER_STORE_SYMBOL = Symbol.for("kendex.pi-qol.thinking-timer.store");
|
|
23
|
+
export const THINKING_TIMER_PATCH_SYMBOL = Symbol.for("kendex.pi-qol.thinking-timer.patch");
|
|
24
|
+
export const SESSION_SEARCH_PENDING_SYMBOL = Symbol.for("kendex.pi-qol.session-search.pending-context");
|
|
25
|
+
export const PENDING_QUEUE_THEME_PATCH_SYMBOL = Symbol.for("kendex.pi-qol.pending-queue.theme-patch");
|
|
26
|
+
export const STATUS_TEXT_ALIGNMENT_PATCH_SYMBOL = Symbol.for("kendex.pi-qol.status-text-alignment-patch");
|
|
27
27
|
|
|
28
|
-
export const QUESTION_OPENED_EVENT = "
|
|
28
|
+
export const QUESTION_OPENED_EVENT = "kendex:pi-questions:opened";
|
|
29
29
|
export const QUESTION_NOTIFY_DEDUP_MS = 2000;
|
|
30
30
|
|
|
31
31
|
export const DEFAULT_NOTIFICATION_TITLE = "Pi";
|
package/extensions/qol/editor.ts
CHANGED
|
@@ -85,7 +85,7 @@ function handleQolEditorInput(editor: CustomEditor, ctx: ExtensionContext, data:
|
|
|
85
85
|
/// Push the latest attachment-count status only when it actually changes.
|
|
86
86
|
/// Called from render() so we follow the rainbow-editor pattern (pull from
|
|
87
87
|
/// render, don't push from every keystroke) and avoid the per-keystroke
|
|
88
|
-
/// setStatus side effect that
|
|
88
|
+
/// setStatus side effect that can cause resize and input races.
|
|
89
89
|
function syncQolEditorStatus(
|
|
90
90
|
ctx: ExtensionContext,
|
|
91
91
|
text: string,
|
package/extensions/qol/glyphs.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
1
|
+
import { readPackageConfig } from "./settings.js";
|
|
4
2
|
|
|
5
3
|
export type GlyphStyle = "unicode" | "ascii";
|
|
6
4
|
export type GlobalGlyphStyleOverride = "inherit" | GlyphStyle;
|
|
@@ -8,79 +6,6 @@ export type GlobalGlyphStyleOverride = "inherit" | GlyphStyle;
|
|
|
8
6
|
const LOCAL_CONFIG_ID = "@vanillagreen/pi-qol";
|
|
9
7
|
const GLOBAL_CONFIG_ID = "@vanillagreen/pi-tool-renderer";
|
|
10
8
|
|
|
11
|
-
function expandHome(input: string): string {
|
|
12
|
-
if (input === "~") return homedir();
|
|
13
|
-
if (input.startsWith("~/")) return join(homedir(), input.slice(2));
|
|
14
|
-
return input;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function projectSettingsPath(cwd: string): string {
|
|
18
|
-
let current = resolve(cwd);
|
|
19
|
-
while (true) {
|
|
20
|
-
const candidate = join(current, ".pi", "settings.json");
|
|
21
|
-
if (existsSync(candidate)) return candidate;
|
|
22
|
-
if (existsSync(join(current, ".pi")) || existsSync(join(current, ".git")) || existsSync(join(current, ".vstack-lock.json"))) return candidate;
|
|
23
|
-
const parent = dirname(current);
|
|
24
|
-
if (parent === current) return join(resolve(cwd), ".pi", "settings.json");
|
|
25
|
-
current = parent;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const PROJECT_TRUST_SYMBOL = Symbol.for("vstack.pi.project-trust");
|
|
30
|
-
|
|
31
|
-
interface ProjectTrustRegistry {
|
|
32
|
-
projectSettings?: Map<string, boolean>;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function projectTrustRegistry(): ProjectTrustRegistry {
|
|
36
|
-
const host = globalThis as unknown as Record<PropertyKey, ProjectTrustRegistry | undefined>;
|
|
37
|
-
const existing = host[PROJECT_TRUST_SYMBOL];
|
|
38
|
-
if (existing) return existing;
|
|
39
|
-
const created: ProjectTrustRegistry = {};
|
|
40
|
-
host[PROJECT_TRUST_SYMBOL] = created;
|
|
41
|
-
return created;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function recordProjectTrust(ctx: { cwd?: string; isProjectTrusted?: () => boolean }): void {
|
|
45
|
-
if (!ctx.cwd) return;
|
|
46
|
-
let trusted = true;
|
|
47
|
-
try {
|
|
48
|
-
trusted = ctx.isProjectTrusted?.() === true;
|
|
49
|
-
} catch {
|
|
50
|
-
trusted = false;
|
|
51
|
-
}
|
|
52
|
-
const registry = projectTrustRegistry();
|
|
53
|
-
if (!registry.projectSettings) registry.projectSettings = new Map();
|
|
54
|
-
registry.projectSettings.set(projectSettingsPath(ctx.cwd), trusted);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function projectSettingsTrusted(settingsPath: string): boolean {
|
|
58
|
-
return projectTrustRegistry().projectSettings?.get(settingsPath) === true;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
function piSettingsPaths(cwd = process.cwd()): string[] {
|
|
63
|
-
const userDir = resolve(expandHome(process.env.PI_CODING_AGENT_DIR?.trim() || "~/.pi/agent"));
|
|
64
|
-
const user = join(userDir, "settings.json");
|
|
65
|
-
const project = projectSettingsPath(cwd);
|
|
66
|
-
return projectSettingsTrusted(project) ? [user, project] : [user];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function readPackageConfig(packageId: string, cwd?: string): Record<string, unknown> {
|
|
70
|
-
const merged: Record<string, unknown> = {};
|
|
71
|
-
for (const settingsPath of piSettingsPaths(cwd)) {
|
|
72
|
-
if (!existsSync(settingsPath)) continue;
|
|
73
|
-
try {
|
|
74
|
-
const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
75
|
-
const config = parsed?.vstack?.extensionManager?.config?.[packageId];
|
|
76
|
-
if (config && typeof config === "object" && !Array.isArray(config)) Object.assign(merged, config);
|
|
77
|
-
} catch {
|
|
78
|
-
// Ignore malformed optional manager config.
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return merged;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
9
|
function asGlyphStyle(value: unknown): GlyphStyle | undefined {
|
|
85
10
|
return value === "unicode" || value === "ascii" ? value : undefined;
|
|
86
11
|
}
|
|
@@ -2,7 +2,7 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
|
2
2
|
import type { Message } from "@earendil-works/pi-ai";
|
|
3
3
|
import { complete } from "./pi-ai-compat.js";
|
|
4
4
|
import { BorderedLoader, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import {
|
|
5
|
+
import { acquirekendexModalLock } from "./bridges.js";
|
|
6
6
|
import { serializeMessagesForSummary } from "./compaction.js";
|
|
7
7
|
import { HANDOFF_SYSTEM_PROMPT } from "./constants.js";
|
|
8
8
|
import { settingBoolean } from "./settings.js";
|
|
@@ -40,7 +40,7 @@ export async function runHandoff(args: string, ctx: ExtensionCommandContext): Pr
|
|
|
40
40
|
const conversationText = serializeMessagesForSummary(messages);
|
|
41
41
|
const currentSessionFile = ctx.sessionManager.getSessionFile();
|
|
42
42
|
|
|
43
|
-
const releaseModalLock =
|
|
43
|
+
const releaseModalLock = acquirekendexModalLock();
|
|
44
44
|
let result: string | null = null;
|
|
45
45
|
let generationError: unknown;
|
|
46
46
|
try {
|
|
@@ -4,7 +4,7 @@ import { ansiRed } from "./ansi.js";
|
|
|
4
4
|
import { boundedSettingNumber, settingBoolean, settingString } from "./settings.js";
|
|
5
5
|
import { stringifyError } from "./util.js";
|
|
6
6
|
|
|
7
|
-
export const RATE_LIMIT_AUTO_RESUME_EVENT = "
|
|
7
|
+
export const RATE_LIMIT_AUTO_RESUME_EVENT = "kendex:rate-limit";
|
|
8
8
|
export const DEFAULT_RATE_LIMIT_RESUME_MESSAGE = "Hit API rate limit. It has been reset. Continue";
|
|
9
9
|
|
|
10
10
|
const MAX_TIMER_DELAY_MS = 2_147_000_000;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { SessionManager, type ExtensionAPI, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Text } from "@earendil-works/pi-tui";
|
|
4
|
-
import {
|
|
4
|
+
import { acquirekendexModalLock } from "../bridges.js";
|
|
5
5
|
import { generateQolSummary, serializeMessagesForSummary } from "../compaction.js";
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_SESSION_SEARCH_SUMMARY_INPUT_CHARS,
|
|
@@ -89,7 +89,7 @@ export async function buildSessionSearchContextMessageWithLoader(ctx: ExtensionC
|
|
|
89
89
|
const title = sessionDisplayName(result);
|
|
90
90
|
if (!ctx.hasUI) return buildSessionSearchContextMessage(ctx, result, customPrompt);
|
|
91
91
|
ctx.ui.setStatus(SESSION_SEARCH_STATUS_KEY, `Summarizing ${title}...`);
|
|
92
|
-
const releaseModalLock =
|
|
92
|
+
const releaseModalLock = acquirekendexModalLock();
|
|
93
93
|
let failure: unknown;
|
|
94
94
|
try {
|
|
95
95
|
const message = await ctx.ui.custom<QolSessionSearchPendingMessage | null>((tui: any, theme: any, _kb: any, done: (value: QolSessionSearchPendingMessage | null) => void) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import { acquirekendexModalLock } from "../bridges.js";
|
|
3
3
|
import { SESSION_SEARCH_CONTEXT_TYPE, SESSION_SEARCH_STATUS_KEY } from "../constants.js";
|
|
4
4
|
import { settingNumber } from "../settings.js";
|
|
5
5
|
import { stringifyError } from "../util.js";
|
|
@@ -122,7 +122,7 @@ export async function openQolSessionSearch(pi: ExtensionAPI, ctx: ExtensionConte
|
|
|
122
122
|
return;
|
|
123
123
|
}
|
|
124
124
|
const sessions = await refreshQolSessionSearchCache(ctx);
|
|
125
|
-
const releaseModalLock =
|
|
125
|
+
const releaseModalLock = acquirekendexModalLock();
|
|
126
126
|
let action: QolSessionPaletteAction | undefined;
|
|
127
127
|
try {
|
|
128
128
|
const currentModel = ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined;
|
|
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { CONFIG_ID } from "./constants.js";
|
|
5
5
|
|
|
6
|
-
export type
|
|
6
|
+
export type kendexConfig = Record<string, unknown>;
|
|
7
7
|
|
|
8
8
|
export function expandHome(input: string): string {
|
|
9
9
|
if (input === "~") return homedir();
|
|
@@ -16,14 +16,14 @@ export function projectSettingsPath(cwd: string): string {
|
|
|
16
16
|
while (true) {
|
|
17
17
|
const candidate = join(current, ".pi", "settings.json");
|
|
18
18
|
if (existsSync(candidate)) return candidate;
|
|
19
|
-
if (existsSync(join(current, ".pi")) || existsSync(join(current, ".git")) || existsSync(join(current, ".
|
|
19
|
+
if (existsSync(join(current, ".pi")) || existsSync(join(current, ".git")) || existsSync(join(current, ".kendex-lock.json"))) return candidate;
|
|
20
20
|
const parent = dirname(current);
|
|
21
21
|
if (parent === current) return join(resolve(cwd), ".pi", "settings.json");
|
|
22
22
|
current = parent;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const PROJECT_TRUST_SYMBOL = Symbol.for("
|
|
26
|
+
const PROJECT_TRUST_SYMBOL = Symbol.for("kendex.pi.project-trust");
|
|
27
27
|
|
|
28
28
|
interface ProjectTrustRegistry {
|
|
29
29
|
projectSettings?: Map<string, boolean>;
|
|
@@ -55,21 +55,27 @@ function projectSettingsTrusted(settingsPath: string): boolean {
|
|
|
55
55
|
return projectTrustRegistry().projectSettings?.get(settingsPath) === true;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/** Root-anchored as `crates/core/src/harness/pi.rs::pi_root_is_absolute_for`
|
|
59
|
+
* means it, which `isAbsolute` is not: it calls a driveless `\root` absolute
|
|
60
|
+
* where the renderer does not, putting the two on different roots. Hoisted, so
|
|
61
|
+
* a circular import cannot reach it inside a temporal dead zone. */
|
|
62
|
+
function rootAnchored(path: string, windows: boolean): boolean { return windows ? /^(?:[A-Za-z]:[\\/]|[\\/]{2}[^\\/]+[\\/][^\\/]+)/.test(path) : path.startsWith("/"); }
|
|
58
63
|
|
|
59
64
|
export function piSettingsPaths(cwd = process.cwd()): string[] {
|
|
60
|
-
const
|
|
65
|
+
const override = expandHome(process.env.PI_CODING_AGENT_DIR?.trim() || "");
|
|
66
|
+
const userDir = resolve(rootAnchored(override, process.platform === "win32") ? override : expandHome("~/.pi/agent"));
|
|
61
67
|
const user = join(userDir, "settings.json");
|
|
62
68
|
const project = projectSettingsPath(cwd);
|
|
63
69
|
return projectSettingsTrusted(project) ? [user, project] : [user];
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
export function
|
|
67
|
-
const merged:
|
|
68
|
-
for (const
|
|
69
|
-
if (!existsSync(
|
|
72
|
+
export function readPackageConfig(packageId: string, cwd?: string): Record<string, unknown> {
|
|
73
|
+
const merged: Record<string, unknown> = {};
|
|
74
|
+
for (const settingsPath of piSettingsPaths(cwd)) {
|
|
75
|
+
if (!existsSync(settingsPath)) continue;
|
|
70
76
|
try {
|
|
71
|
-
const parsed = JSON.parse(readFileSync(
|
|
72
|
-
const config = parsed?.
|
|
77
|
+
const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
78
|
+
const config = parsed?.kendex?.extensionManager?.config?.[packageId];
|
|
73
79
|
if (config && typeof config === "object" && !Array.isArray(config)) Object.assign(merged, config);
|
|
74
80
|
} catch {
|
|
75
81
|
// Ignore malformed optional manager config.
|
|
@@ -78,18 +84,22 @@ export function readVstackConfig(cwd?: string): VstackConfig {
|
|
|
78
84
|
return merged;
|
|
79
85
|
}
|
|
80
86
|
|
|
87
|
+
export function readkendexConfig(cwd?: string): kendexConfig {
|
|
88
|
+
return readPackageConfig(CONFIG_ID, cwd) as kendexConfig;
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
export function settingBoolean(key: string, fallback: boolean, cwd?: string): boolean {
|
|
82
|
-
const value =
|
|
92
|
+
const value = readkendexConfig(cwd)[key];
|
|
83
93
|
return typeof value === "boolean" ? value : fallback;
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
export function settingString(key: string, fallback: string, cwd?: string): string {
|
|
87
|
-
const value =
|
|
97
|
+
const value = readkendexConfig(cwd)[key];
|
|
88
98
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
|
|
89
99
|
}
|
|
90
100
|
|
|
91
101
|
export function settingStringAllowEmpty(key: string, fallback: string, cwd?: string): string {
|
|
92
|
-
const value =
|
|
102
|
+
const value = readkendexConfig(cwd)[key];
|
|
93
103
|
return typeof value === "string" ? value.trim() : fallback;
|
|
94
104
|
}
|
|
95
105
|
|
|
@@ -99,7 +109,7 @@ export function newlineFallbackKey(cwd?: string): "ctrl+j" | "none" {
|
|
|
99
109
|
}
|
|
100
110
|
|
|
101
111
|
export function settingNumber(key: string, fallback: number, cwd?: string): number {
|
|
102
|
-
const value =
|
|
112
|
+
const value = readkendexConfig(cwd)[key];
|
|
103
113
|
const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
104
114
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
105
115
|
}
|
package/extensions/qol.ts
CHANGED
|
@@ -924,7 +924,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
924
924
|
|
|
925
925
|
const tryOpenExtensionManagerSettings = async (ctx: ExtensionCommandContext): Promise<boolean> => {
|
|
926
926
|
const host = globalThis as unknown as Record<PropertyKey, unknown>;
|
|
927
|
-
const openQuickSettings = host[Symbol.for("
|
|
927
|
+
const openQuickSettings = host[Symbol.for("kendex.pi.extension-manager.open-quick-settings")];
|
|
928
928
|
if (typeof openQuickSettings !== "function") return false;
|
|
929
929
|
try {
|
|
930
930
|
await (openQuickSettings as (ctx: ExtensionCommandContext, hint?: string) => Promise<void>)(ctx, "pi-qol");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillagreen/pi-qol",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Pi quality-of-life extension: compact statusline/π prompt, reliable multiline input, styled pasted-image chips, session naming/search/context import, scheduled prompts, handoff, permission prompts, notifications, custom compaction, and a collapsed-thinking timer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
"extensions": [
|
|
15
15
|
"./extensions/qol.ts"
|
|
16
16
|
],
|
|
17
|
-
"image": "https://raw.githubusercontent.com/vanillagreencom/
|
|
17
|
+
"image": "https://raw.githubusercontent.com/vanillagreencom/kendex/main/pi-extensions/pi-qol/assets/settings-panel.png"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"test": "bun test ./tests"
|
|
21
21
|
},
|
|
22
|
-
"
|
|
22
|
+
"kendex": {
|
|
23
23
|
"extensionManager": {
|
|
24
24
|
"displayName": "QOL",
|
|
25
25
|
"settings": [
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
{
|
|
37
37
|
"key": "glyphStyle",
|
|
38
38
|
"label": "Glyph style",
|
|
39
|
-
"description": "Glyphs for
|
|
39
|
+
"description": "Glyphs for kendex-owned UI chrome. Unicode is the default; ASCII uses plain box/status/separator characters for terminal compatibility. pi-tool-renderer's global override wins when set.",
|
|
40
40
|
"type": "enum",
|
|
41
41
|
"enumValues": [
|
|
42
42
|
"unicode",
|
|
@@ -911,7 +911,7 @@
|
|
|
911
911
|
{
|
|
912
912
|
"key": "compaction.handoffArtifactEnabled",
|
|
913
913
|
"label": "Write pre-compaction handoff artifact",
|
|
914
|
-
"description": "When enabled, write a JSON handoff artifact under ~/.pi/agent/
|
|
914
|
+
"description": "When enabled, write a JSON handoff artifact under ~/.pi/agent/kendex/sessions/<session>/pi-qol/handoff/ containing the previous summary, last task state, and recently-referenced file/artifact paths. Each write creates a stamped file and a latest.json pointer. Applies to every budget-guard compaction (regardless of compaction.customEnabled), and to manual/idle compactions only when compaction.customEnabled is on. Write failures surface a QOL warning and a handoffArtifactError field in the compaction details.",
|
|
915
915
|
"type": "boolean",
|
|
916
916
|
"default": true,
|
|
917
917
|
"category": "Compaction Budget Guard",
|
|
@@ -976,12 +976,12 @@
|
|
|
976
976
|
],
|
|
977
977
|
"repository": {
|
|
978
978
|
"type": "git",
|
|
979
|
-
"url": "git+https://github.com/vanillagreencom/
|
|
979
|
+
"url": "git+https://github.com/vanillagreencom/kendex.git",
|
|
980
980
|
"directory": "pi-extensions/pi-qol"
|
|
981
981
|
},
|
|
982
|
-
"homepage": "https://github.com/vanillagreencom/
|
|
982
|
+
"homepage": "https://github.com/vanillagreencom/kendex/tree/main/pi-extensions/pi-qol",
|
|
983
983
|
"bugs": {
|
|
984
|
-
"url": "https://github.com/vanillagreencom/
|
|
984
|
+
"url": "https://github.com/vanillagreencom/kendex/issues"
|
|
985
985
|
},
|
|
986
986
|
"author": "vanillagreen",
|
|
987
987
|
"publishConfig": {
|