@vanillagreen/pi-qol 2.2.0 → 2.3.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 +11 -0
- package/extensions/qol/compaction-handoff.ts +6 -19
- package/extensions/qol/compaction.ts +1 -1
- package/extensions/qol/constants.ts +14 -1
- package/extensions/qol/glyphs.ts +1 -1
- package/extensions/qol/images.ts +2 -1
- package/extensions/qol/notifications.ts +172 -97
- package/extensions/qol/package-config.ts +445 -0
- package/extensions/qol/pending-queue.ts +49 -4
- package/extensions/qol/session-search/cache.ts +68 -17
- package/extensions/qol/session-search/component.ts +19 -5
- package/extensions/qol/session-search/index.ts +17 -9
- package/extensions/qol/session-search/types.ts +7 -0
- package/extensions/qol/settings.ts +5 -100
- package/extensions/qol.ts +50 -19
- package/package.json +4 -3
- package/scripts/lane-retention.ts +187 -0
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
QolSessionMessagesState,
|
|
30
30
|
QolSessionPaletteAction,
|
|
31
31
|
QolSessionSearchHit,
|
|
32
|
+
QolSessionSearchLoad,
|
|
32
33
|
QolSessionSearchResult,
|
|
33
34
|
QolSessionSearchScope,
|
|
34
35
|
QolSessionSearchSession,
|
|
@@ -108,7 +109,8 @@ export class QolSessionSearchComponent {
|
|
|
108
109
|
private readonly done: (action: QolSessionPaletteAction) => void;
|
|
109
110
|
private readonly tui: { requestRender(): void; terminal?: { rows?: number } };
|
|
110
111
|
private readonly theme: Theme;
|
|
111
|
-
|
|
112
|
+
/** The index, while it loads, once loaded, or the load's failure. */
|
|
113
|
+
private sessions: QolSessionSearchLoad;
|
|
112
114
|
private readonly cwd: string;
|
|
113
115
|
private readonly currentModel: QolModelInfo | undefined;
|
|
114
116
|
private screen: "search" | "messages" | "actions" | "confirmContext" | "confirmFork" | "confirmModel" = "search";
|
|
@@ -125,7 +127,7 @@ export class QolSessionSearchComponent {
|
|
|
125
127
|
done: (action: QolSessionPaletteAction) => void,
|
|
126
128
|
tui: { requestRender(): void; terminal?: { rows?: number } },
|
|
127
129
|
theme: Theme,
|
|
128
|
-
sessions:
|
|
130
|
+
sessions: QolSessionSearchLoad,
|
|
129
131
|
cwd: string,
|
|
130
132
|
initialQuery = "",
|
|
131
133
|
currentModel: QolModelInfo | undefined = undefined,
|
|
@@ -151,6 +153,13 @@ export class QolSessionSearchComponent {
|
|
|
151
153
|
|
|
152
154
|
invalidate(): void {}
|
|
153
155
|
|
|
156
|
+
/** Show the index once its load settles; the overlay opens before that. */
|
|
157
|
+
setSessions(sessions: QolSessionSearchLoad): void {
|
|
158
|
+
this.sessions = sessions;
|
|
159
|
+
this.updateResults(false);
|
|
160
|
+
this.tui.requestRender();
|
|
161
|
+
}
|
|
162
|
+
|
|
154
163
|
private maxOverlayRows(): number {
|
|
155
164
|
const terminalRows = Number(this.tui.terminal?.rows ?? process.stdout.rows ?? 30);
|
|
156
165
|
const safeRows = Number.isFinite(terminalRows) && terminalRows > 0 ? terminalRows : 30;
|
|
@@ -213,8 +222,9 @@ export class QolSessionSearchComponent {
|
|
|
213
222
|
}
|
|
214
223
|
|
|
215
224
|
private sessionsForScope(scope = this.searchState.scope): QolSessionSearchSession[] {
|
|
216
|
-
if (
|
|
217
|
-
|
|
225
|
+
if (this.sessions.status !== "ready") return [];
|
|
226
|
+
if (scope === "all") return this.sessions.sessions;
|
|
227
|
+
return this.sessions.sessions.filter((session) => sameSessionSearchProject(session.cwd, this.cwd));
|
|
218
228
|
}
|
|
219
229
|
|
|
220
230
|
private updateResults(resetSelection = true): void {
|
|
@@ -614,7 +624,11 @@ export class QolSessionSearchComponent {
|
|
|
614
624
|
if (!compact) lines.push(row(dim(`tokens · re:<pattern> regex · "phrase" exact`)));
|
|
615
625
|
lines.push(divider());
|
|
616
626
|
|
|
617
|
-
if (
|
|
627
|
+
if (this.sessions.status === "loading") {
|
|
628
|
+
lines.push(row(muted("Loading sessions…")));
|
|
629
|
+
} else if (this.sessions.status === "failed") {
|
|
630
|
+
lines.push(row(muted(`Could not load sessions: ${this.sessions.error.split("\n")[0]}`)));
|
|
631
|
+
} else if (state.results.length === 0) {
|
|
618
632
|
lines.push(row(muted(state.query.trim() ? "No prompts match your search" : "No prompts found")));
|
|
619
633
|
} else {
|
|
620
634
|
lines.push(...this.renderSearchHitPane(inner, row, dim, muted, accent));
|
|
@@ -5,9 +5,8 @@ import { settingNumber } from "../settings.js";
|
|
|
5
5
|
import { stringifyError } from "../util.js";
|
|
6
6
|
import {
|
|
7
7
|
getPendingSessionSearchMessage,
|
|
8
|
-
nextSessionSearchPendingActionId,
|
|
9
8
|
pinSessionModel,
|
|
10
|
-
|
|
9
|
+
queueSessionSearchPendingAction,
|
|
11
10
|
refreshQolSessionSearchCache,
|
|
12
11
|
sessionDisplayName,
|
|
13
12
|
setPendingSessionSearchMessage,
|
|
@@ -17,12 +16,14 @@ import {
|
|
|
17
16
|
buildSessionSearchContextMessageWithLoader,
|
|
18
17
|
injectSessionSearchContext,
|
|
19
18
|
} from "./context.js";
|
|
20
|
-
import type { QolSessionPaletteAction, QolSessionSearchResult } from "./types.js";
|
|
19
|
+
import type { QolSessionPaletteAction, QolSessionSearchLoad, QolSessionSearchResult } from "./types.js";
|
|
21
20
|
|
|
22
21
|
export {
|
|
23
|
-
|
|
22
|
+
clearSessionSearchPendingAction,
|
|
24
23
|
refreshQolSessionSearchCache,
|
|
24
|
+
releaseQolSessionSearchCache,
|
|
25
25
|
sessionSearchShortcut,
|
|
26
|
+
takeSessionSearchPendingAction,
|
|
26
27
|
} from "./cache.js";
|
|
27
28
|
export { renderSessionSearchContextMessage } from "./context.js";
|
|
28
29
|
|
|
@@ -31,8 +32,7 @@ function asCommandContext(ctx: ExtensionContext): (ExtensionContext & Partial<Ex
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
function queueSessionSearchCommandAction(ctx: ExtensionContext, action: QolSessionPaletteAction): void {
|
|
34
|
-
const id =
|
|
35
|
-
qolSessionSearchPendingActions.set(id, action);
|
|
35
|
+
const id = queueSessionSearchPendingAction(action);
|
|
36
36
|
ctx.ui.setEditorText(`/search:resume-pending ${id}`);
|
|
37
37
|
ctx.ui.notify(`${sessionDisplayName(action.result!)} — press enter to ${action.type === "fork" ? "fork" : "resume"}`, "info");
|
|
38
38
|
}
|
|
@@ -121,13 +121,21 @@ export async function openQolSessionSearch(pi: ExtensionAPI, ctx: ExtensionConte
|
|
|
121
121
|
ctx.ui.notify("Session search requires interactive UI", "warning");
|
|
122
122
|
return;
|
|
123
123
|
}
|
|
124
|
-
|
|
124
|
+
// The overlay opens at once and fills in when the index is loaded: a
|
|
125
|
+
// released index takes a full read of every session file to load again.
|
|
126
|
+
const loading: Promise<QolSessionSearchLoad> = refreshQolSessionSearchCache(ctx).then(
|
|
127
|
+
(sessions) => ({ status: "ready", sessions }),
|
|
128
|
+
(error) => ({ status: "failed", error: stringifyError(error) }),
|
|
129
|
+
);
|
|
125
130
|
const releaseModalLock = acquirekendexModalLock();
|
|
126
131
|
let action: QolSessionPaletteAction | undefined;
|
|
127
132
|
try {
|
|
128
133
|
const currentModel = ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined;
|
|
129
|
-
action = await ctx.ui.custom<QolSessionPaletteAction>((tui, theme, _keybindings, done) =>
|
|
130
|
-
new QolSessionSearchComponent(done, tui, theme,
|
|
134
|
+
action = await ctx.ui.custom<QolSessionPaletteAction>((tui, theme, _keybindings, done) => {
|
|
135
|
+
const component = new QolSessionSearchComponent(done, tui, theme, { status: "loading" }, ctx.cwd, initialQuery, currentModel);
|
|
136
|
+
void loading.then((load) => component.setSessions(load));
|
|
137
|
+
return component;
|
|
138
|
+
}, {
|
|
131
139
|
overlay: true,
|
|
132
140
|
overlayOptions: {
|
|
133
141
|
anchor: "center",
|
|
@@ -13,6 +13,13 @@ export interface QolSessionSearchSession {
|
|
|
13
13
|
path: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/** The session-search index as the overlay sees it: still loading, loaded,
|
|
17
|
+
* or failed with the load's error. */
|
|
18
|
+
export type QolSessionSearchLoad =
|
|
19
|
+
| { status: "loading" }
|
|
20
|
+
| { status: "ready"; sessions: QolSessionSearchSession[] }
|
|
21
|
+
| { status: "failed"; error: string };
|
|
22
|
+
|
|
16
23
|
export interface QolSessionSearchResult extends QolSessionSearchSession {
|
|
17
24
|
rank: number;
|
|
18
25
|
snippets: string[];
|
|
@@ -1,113 +1,18 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
1
|
import { CONFIG_ID } from "./constants.js";
|
|
2
|
+
import { piSettingsPaths, readPackageConfig, readSettingsFiles } from "./package-config.js";
|
|
5
3
|
|
|
6
4
|
export type kendexConfig = Record<string, unknown>;
|
|
7
5
|
|
|
8
|
-
export function expandHome(input: string): string {
|
|
9
|
-
if (input === "~") return homedir();
|
|
10
|
-
if (input.startsWith("~/")) return join(homedir(), input.slice(2));
|
|
11
|
-
return input;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function projectSettingsPath(cwd: string): string {
|
|
15
|
-
let current = resolve(cwd);
|
|
16
|
-
while (true) {
|
|
17
|
-
const candidate = join(current, ".pi", "settings.json");
|
|
18
|
-
if (existsSync(candidate)) return candidate;
|
|
19
|
-
if (existsSync(join(current, ".pi")) || existsSync(join(current, ".git")) || existsSync(join(current, ".kendex-lock.json"))) return candidate;
|
|
20
|
-
const parent = dirname(current);
|
|
21
|
-
if (parent === current) return join(resolve(cwd), ".pi", "settings.json");
|
|
22
|
-
current = parent;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const PROJECT_TRUST_SYMBOL = Symbol.for("kendex.pi.project-trust");
|
|
27
|
-
|
|
28
|
-
interface ProjectTrustRegistry {
|
|
29
|
-
projectSettings?: Map<string, boolean>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function projectTrustRegistry(): ProjectTrustRegistry {
|
|
33
|
-
const host = globalThis as unknown as Record<PropertyKey, ProjectTrustRegistry | undefined>;
|
|
34
|
-
const existing = host[PROJECT_TRUST_SYMBOL];
|
|
35
|
-
if (existing) return existing;
|
|
36
|
-
const created: ProjectTrustRegistry = {};
|
|
37
|
-
host[PROJECT_TRUST_SYMBOL] = created;
|
|
38
|
-
return created;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function recordProjectTrust(ctx: { cwd?: string; isProjectTrusted?: () => boolean }): void {
|
|
42
|
-
if (!ctx.cwd) return;
|
|
43
|
-
let trusted = true;
|
|
44
|
-
try {
|
|
45
|
-
trusted = ctx.isProjectTrusted?.() === true;
|
|
46
|
-
} catch {
|
|
47
|
-
trusted = false;
|
|
48
|
-
}
|
|
49
|
-
const registry = projectTrustRegistry();
|
|
50
|
-
if (!registry.projectSettings) registry.projectSettings = new Map();
|
|
51
|
-
registry.projectSettings.set(projectSettingsPath(ctx.cwd), trusted);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function projectSettingsTrusted(settingsPath: string): boolean {
|
|
55
|
-
return projectTrustRegistry().projectSettings?.get(settingsPath) === true;
|
|
56
|
-
}
|
|
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("/"); }
|
|
63
|
-
|
|
64
|
-
export function piSettingsPaths(cwd = process.cwd()): string[] {
|
|
65
|
-
const override = expandHome(process.env.PI_CODING_AGENT_DIR?.trim() || "");
|
|
66
|
-
const userDir = resolve(rootAnchored(override, process.platform === "win32") ? override : expandHome("~/.pi/agent"));
|
|
67
|
-
const user = join(userDir, "settings.json");
|
|
68
|
-
const project = projectSettingsPath(cwd);
|
|
69
|
-
return projectSettingsTrusted(project) ? [user, project] : [user];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
73
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Each settings file `piSettingsPaths` names that exists and parses, later
|
|
77
|
-
* files overriding earlier ones. A malformed file is skipped, as Pi's own
|
|
78
|
-
* settings load treats one as empty. */
|
|
79
|
-
function readPiSettingsFiles(cwd?: string): Record<string, any>[] {
|
|
80
|
-
const files: Record<string, any>[] = [];
|
|
81
|
-
for (const settingsPath of piSettingsPaths(cwd)) {
|
|
82
|
-
if (!existsSync(settingsPath)) continue;
|
|
83
|
-
try {
|
|
84
|
-
const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
85
|
-
if (isRecord(parsed)) files.push(parsed);
|
|
86
|
-
} catch {
|
|
87
|
-
// Ignore a malformed settings file.
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return files;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function readPackageConfig(packageId: string, cwd?: string): Record<string, unknown> {
|
|
94
|
-
const merged: Record<string, unknown> = {};
|
|
95
|
-
for (const settings of readPiSettingsFiles(cwd)) {
|
|
96
|
-
const config = settings.kendex?.extensionManager?.config?.[packageId];
|
|
97
|
-
if (isRecord(config)) Object.assign(merged, config);
|
|
98
|
-
}
|
|
99
|
-
return merged;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
6
|
/** Pi core's `compaction.enabled` over the files QOL reads its own config
|
|
103
7
|
* from, resolved as Pi's `SettingsManager.getCompactionEnabled` resolves it:
|
|
104
8
|
* the last file that sets the key decides, a key no file sets is Pi's default,
|
|
105
9
|
* true, and any falsy value turns compaction off. */
|
|
106
10
|
export function piCompactionEnabled(cwd?: string): boolean {
|
|
107
11
|
let enabled: unknown;
|
|
108
|
-
for (const
|
|
109
|
-
|
|
110
|
-
|
|
12
|
+
for (const file of readSettingsFiles(piSettingsPaths(cwd))) {
|
|
13
|
+
if (file.kind !== "parsed") continue;
|
|
14
|
+
const compaction = file.settings.compaction as { enabled?: unknown } | null | undefined;
|
|
15
|
+
if (compaction && typeof compaction === "object" && !Array.isArray(compaction) && compaction.enabled !== undefined) enabled = compaction.enabled;
|
|
111
16
|
}
|
|
112
17
|
return Boolean(enabled ?? true);
|
|
113
18
|
}
|
package/extensions/qol.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { pruneLanes } from "../scripts/lane-retention.js";
|
|
2
4
|
import { truncateToWidth, type TUI } from "@earendil-works/pi-tui";
|
|
3
5
|
import { criticalInfo, lastAssistantTextFromAgentEnd, needsDirection, taskStats } from "./qol/agent-end.js";
|
|
4
6
|
import { getQuestionService, readCavemanBridge, type QuestionOpenedEventLike } from "./qol/bridges.js";
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
DEFAULT_IDLE_COMPACTION_SECONDS,
|
|
17
19
|
DEFAULT_INPUT_BOTTOM_PADDING_LINES,
|
|
18
20
|
INSTALL_SYMBOL,
|
|
21
|
+
QOL_BUDGET_HANDOFF_FOLDER,
|
|
19
22
|
QOL_NOTIFICATION_SERVICE_SYMBOL,
|
|
20
23
|
QUESTION_OPENED_EVENT,
|
|
21
24
|
SESSION_MANAGER_STATUS_KEY,
|
|
@@ -23,6 +26,7 @@ import {
|
|
|
23
26
|
SESSION_SEARCH_STATUS_KEY,
|
|
24
27
|
SESSION_TITLE_SYNC_INTERVAL_MS,
|
|
25
28
|
STATUS_KEY,
|
|
29
|
+
THINKING_TIMER_MAX_DURATIONS,
|
|
26
30
|
THINKING_TIMER_STORE_SYMBOL,
|
|
27
31
|
TMUX_SESSION_TITLE_BORDER_FORMAT,
|
|
28
32
|
} from "./qol/constants.js";
|
|
@@ -39,6 +43,7 @@ import { imageContentForPath, resolveSubmittedImagePaths } from "./qol/images.js
|
|
|
39
43
|
import {
|
|
40
44
|
clearTmuxWindowMark,
|
|
41
45
|
notifyQuestionOpened,
|
|
46
|
+
resetQolNotificationCooldowns,
|
|
42
47
|
sendQolNotification,
|
|
43
48
|
type QolNotificationService,
|
|
44
49
|
} from "./qol/notifications.js";
|
|
@@ -46,6 +51,7 @@ import {
|
|
|
46
51
|
installPendingQueueThemePatch,
|
|
47
52
|
installStatusTextAlignmentPatch,
|
|
48
53
|
restorePendingQueueThemePatch,
|
|
54
|
+
restoreStatusTextAlignmentPatch,
|
|
49
55
|
} from "./qol/pending-queue.js";
|
|
50
56
|
import { permissionGateMatch, permissionGatePrompt } from "./qol/permission-gate.js";
|
|
51
57
|
import { createRateLimitAutoResumeController, RATE_LIMIT_AUTO_RESUME_EVENT } from "./qol/rate-limit-auto-resume.js";
|
|
@@ -60,15 +66,18 @@ import {
|
|
|
60
66
|
} from "./qol/session-rename.js";
|
|
61
67
|
import { createScheduleController, getScheduleArgumentCompletions } from "./qol/schedule.js";
|
|
62
68
|
import {
|
|
69
|
+
clearSessionSearchPendingAction,
|
|
63
70
|
consumePendingSessionSearchContext,
|
|
64
71
|
openQolSessionSearch,
|
|
65
|
-
qolSessionSearchPendingActions,
|
|
66
72
|
refreshQolSessionSearchCache,
|
|
73
|
+
releaseQolSessionSearchCache,
|
|
67
74
|
renderSessionSearchContextMessage,
|
|
68
75
|
runSessionSearchResumeOrFork,
|
|
69
76
|
sessionSearchShortcut,
|
|
77
|
+
takeSessionSearchPendingAction,
|
|
70
78
|
} from "./qol/session-search/index.js";
|
|
71
|
-
import {
|
|
79
|
+
import { installSettingsCacheRefresh, piUserDir, recordProjectTrust } from "./qol/package-config.js";
|
|
80
|
+
import { settingBoolean, settingNumber, settingString } from "./qol/settings.js";
|
|
72
81
|
import { statusMessage } from "./qol/status-message.js";
|
|
73
82
|
import {
|
|
74
83
|
formatTmuxSessionTitle,
|
|
@@ -102,7 +111,6 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
102
111
|
if (!settingBoolean("enabled", true)) return;
|
|
103
112
|
|
|
104
113
|
installThinkingTimerPatch();
|
|
105
|
-
installStatusTextAlignmentPatch();
|
|
106
114
|
const thinkingTimerStore: ThinkingTimerStore = {
|
|
107
115
|
enabled: false,
|
|
108
116
|
starts: new Map(),
|
|
@@ -248,11 +256,24 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
248
256
|
const duration = Math.max(0, endTimeMs - start);
|
|
249
257
|
thinkingTimerStore.starts.delete(key);
|
|
250
258
|
thinkingTimerStore.durations.set(key, duration);
|
|
259
|
+
for (const oldest of thinkingTimerStore.durations.keys()) {
|
|
260
|
+
if (thinkingTimerStore.durations.size <= THINKING_TIMER_MAX_DURATIONS) break;
|
|
261
|
+
thinkingTimerStore.durations.delete(oldest);
|
|
262
|
+
}
|
|
251
263
|
const label = thinkingTimerStore.labels.get(key);
|
|
252
264
|
if (label) label.setText(thinkingTimerLabel(thinkingTimerStore.theme, duration, thinkingTimerStore.cwd));
|
|
253
265
|
if (thinkingTimerStore.starts.size === 0) stopThinkingTimerTicker();
|
|
254
266
|
};
|
|
255
267
|
|
|
268
|
+
// A label is held only to tick it while its block runs. A finished block's
|
|
269
|
+
// label already shows its final time, and a redraw of that message labels
|
|
270
|
+
// it again from `durations`.
|
|
271
|
+
const releaseFinishedThinkingLabels = () => {
|
|
272
|
+
for (const key of thinkingTimerStore.labels.keys()) {
|
|
273
|
+
if (!thinkingTimerStore.starts.has(key)) thinkingTimerStore.labels.delete(key);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
256
277
|
const clearIdleCompactionTimer = () => {
|
|
257
278
|
if (idleCompactionTimer) clearTimeout(idleCompactionTimer);
|
|
258
279
|
idleCompactionTimer = undefined;
|
|
@@ -381,7 +402,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
381
402
|
if (!service) return false;
|
|
382
403
|
questionUnsubscribe = service.subscribe((event: any) => {
|
|
383
404
|
if (event?.action !== "opened") return;
|
|
384
|
-
notifyQuestionOpened(ctx, { requestId: event.requestId, request: event.request, source: event.source }, "question");
|
|
405
|
+
notifyQuestionOpened(pi, ctx, { requestId: event.requestId, request: event.request, source: event.source }, "question");
|
|
385
406
|
});
|
|
386
407
|
return true;
|
|
387
408
|
};
|
|
@@ -570,7 +591,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
570
591
|
};
|
|
571
592
|
const notificationService: QolNotificationService = {
|
|
572
593
|
notifyQuestionOpened(ctx, event) {
|
|
573
|
-
notifyQuestionOpened(ctx ?? currentCtx, event, "question");
|
|
594
|
+
notifyQuestionOpened(pi, ctx ?? currentCtx, event, "question");
|
|
574
595
|
return true;
|
|
575
596
|
},
|
|
576
597
|
};
|
|
@@ -579,15 +600,20 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
579
600
|
pi.events.on(QUESTION_OPENED_EVENT, (data: unknown) => {
|
|
580
601
|
if (!data || typeof data !== "object") return;
|
|
581
602
|
const event = data as QuestionOpenedEventLike;
|
|
582
|
-
notifyQuestionOpened(currentCtx, event, "question");
|
|
603
|
+
notifyQuestionOpened(pi, currentCtx, event, "question");
|
|
583
604
|
});
|
|
584
605
|
pi.events.on(RATE_LIMIT_AUTO_RESUME_EVENT, (payload: unknown) => {
|
|
585
606
|
if (!currentCtx) return;
|
|
586
607
|
rateLimitAutoResumeController.noteExternalRateLimitEvent(payload, currentCtx);
|
|
587
608
|
});
|
|
588
609
|
|
|
610
|
+
installSettingsCacheRefresh(pi);
|
|
589
611
|
pi.on("session_start", (event, ctx) => {
|
|
590
612
|
recordProjectTrust(ctx);
|
|
613
|
+
const pruned = pruneLanes(join(piUserDir(), "kendex", "sessions"), QOL_BUDGET_HANDOFF_FOLDER.split("/"));
|
|
614
|
+
for (const failure of pruned.failed) {
|
|
615
|
+
console.warn(`handoff-prune-error: ${failure.path}\n${failure.error}`);
|
|
616
|
+
}
|
|
591
617
|
currentCtx = ctx;
|
|
592
618
|
subscribeCavemanBridge();
|
|
593
619
|
latestSystemPromptOptions = undefined;
|
|
@@ -601,6 +627,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
601
627
|
void consumePendingSessionSearchContext(pi, ctx, event.reason);
|
|
602
628
|
installAutocompleteHintStyling(ctx);
|
|
603
629
|
installPendingQueueThemePatch(ctx);
|
|
630
|
+
installStatusTextAlignmentPatch(ctx);
|
|
604
631
|
if (ctx.hasUI) {
|
|
605
632
|
ctx.ui.setHiddenThinkingLabel(hiddenThinkingLabel(ctx.ui.theme, ctx.cwd));
|
|
606
633
|
applyWorkingIndicatorMode(ctx);
|
|
@@ -647,7 +674,8 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
647
674
|
}
|
|
648
675
|
startQuestionSubscription(ctx);
|
|
649
676
|
void attemptAutoRename(ctx);
|
|
650
|
-
|
|
677
|
+
// A headless session has no search overlay to open, so it loads no index.
|
|
678
|
+
if (ctx.hasUI && settingBoolean("sessionSearch.enabled", true, ctx.cwd)) {
|
|
651
679
|
if (sessionSearchWarmupTimer) clearTimeout(sessionSearchWarmupTimer);
|
|
652
680
|
sessionSearchWarmupTimer = setTimeout(() => {
|
|
653
681
|
sessionSearchWarmupTimer = undefined;
|
|
@@ -672,8 +700,11 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
672
700
|
clearQuestionSubscribeTimer();
|
|
673
701
|
if (sessionSearchWarmupTimer) clearTimeout(sessionSearchWarmupTimer);
|
|
674
702
|
sessionSearchWarmupTimer = undefined;
|
|
703
|
+
releaseQolSessionSearchCache();
|
|
704
|
+
clearSessionSearchPendingAction();
|
|
675
705
|
resetThinkingTimer(undefined);
|
|
676
|
-
clearTmuxWindowMark();
|
|
706
|
+
clearTmuxWindowMark(pi);
|
|
707
|
+
resetQolNotificationCooldowns();
|
|
677
708
|
questionUnsubscribe?.();
|
|
678
709
|
questionUnsubscribe = undefined;
|
|
679
710
|
currentCtx = undefined;
|
|
@@ -683,6 +714,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
683
714
|
ctx.ui.setStatus(SESSION_SEARCH_STATUS_KEY, undefined);
|
|
684
715
|
ctx.ui.setStatus("qol-budget-guard", undefined);
|
|
685
716
|
restorePendingQueueThemePatch(ctx);
|
|
717
|
+
restoreStatusTextAlignmentPatch();
|
|
686
718
|
resetStatuslineUi(ctx);
|
|
687
719
|
ctx.ui.setEditorComponent(undefined);
|
|
688
720
|
});
|
|
@@ -704,7 +736,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
704
736
|
});
|
|
705
737
|
pi.on("agent_start", (_event, ctx) => {
|
|
706
738
|
clearIdleCompactionTimer();
|
|
707
|
-
clearTmuxWindowMark();
|
|
739
|
+
clearTmuxWindowMark(pi);
|
|
708
740
|
rateLimitAutoResumeController.noteAgentStart(ctx);
|
|
709
741
|
if (ctx.hasUI) {
|
|
710
742
|
void refreshStatusline(ctx);
|
|
@@ -742,6 +774,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
742
774
|
}
|
|
743
775
|
});
|
|
744
776
|
pi.on("agent_end", (event, ctx) => {
|
|
777
|
+
releaseFinishedThinkingLabels();
|
|
745
778
|
if (ctx.hasUI) {
|
|
746
779
|
void refreshStatusline(ctx);
|
|
747
780
|
requestRender();
|
|
@@ -754,21 +787,21 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
754
787
|
const critical = criticalInfo(text);
|
|
755
788
|
if (critical) {
|
|
756
789
|
pendingTaskCompleteNotification = undefined;
|
|
757
|
-
sendQolNotification(ctx, "critical", `Critical: ${critical}`, "error", `critical:${critical.slice(0, 80)}`);
|
|
790
|
+
void sendQolNotification(pi, ctx, "critical", `Critical: ${critical}`, "error", `critical:${critical.slice(0, 80)}`);
|
|
758
791
|
return;
|
|
759
792
|
}
|
|
760
793
|
if (pendingTaskCompleteNotification) {
|
|
761
794
|
const body = pendingTaskCompleteNotification;
|
|
762
795
|
pendingTaskCompleteNotification = undefined;
|
|
763
|
-
sendQolNotification(ctx, "task-complete", body, "info", "task-complete");
|
|
796
|
+
void sendQolNotification(pi, ctx, "task-complete", body, "info", "task-complete");
|
|
764
797
|
return;
|
|
765
798
|
}
|
|
766
799
|
if (ctx.hasPendingMessages?.()) return;
|
|
767
800
|
if (needsDirection(text)) {
|
|
768
|
-
sendQolNotification(ctx, "direction", "Pi is awaiting your direction.", "warning", "direction");
|
|
801
|
+
void sendQolNotification(pi, ctx, "direction", "Pi is awaiting your direction.", "warning", "direction");
|
|
769
802
|
return;
|
|
770
803
|
}
|
|
771
|
-
sendQolNotification(ctx, "ready", settingString("notification.readyMessage", "Ready for input", ctx.cwd), "info", "ready");
|
|
804
|
+
void sendQolNotification(pi, ctx, "ready", settingString("notification.readyMessage", "Ready for input", ctx.cwd), "info", "ready");
|
|
772
805
|
});
|
|
773
806
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
774
807
|
await fireStagedBudgetGuard(ctx);
|
|
@@ -796,7 +829,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
796
829
|
pi.on("session_before_tree", (event, ctx) => handleQolBranchSummary(event, ctx));
|
|
797
830
|
pi.on("tool_call", async (event: any, ctx) => {
|
|
798
831
|
if (event?.toolName === "question") {
|
|
799
|
-
notifyQuestionOpened(ctx, { requestId: event.input?.id ?? event.toolCallId, request: event.input, source: "tool_call" }, "question");
|
|
832
|
+
notifyQuestionOpened(pi, ctx, { requestId: event.input?.id ?? event.toolCallId, request: event.input, source: "tool_call" }, "question");
|
|
800
833
|
return undefined;
|
|
801
834
|
}
|
|
802
835
|
if (!settingBoolean("permissionGate.enabled", false, ctx.cwd)) return undefined;
|
|
@@ -815,7 +848,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
815
848
|
});
|
|
816
849
|
|
|
817
850
|
pi.on("input", async (event) => {
|
|
818
|
-
clearTmuxWindowMark();
|
|
851
|
+
clearTmuxWindowMark(pi);
|
|
819
852
|
if (event.source === "extension") return { action: "continue" };
|
|
820
853
|
const text = event.text ?? "";
|
|
821
854
|
const paths = currentCtx?.cwd ? resolveSubmittedImagePaths(text, currentCtx.cwd) : [];
|
|
@@ -901,13 +934,11 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
901
934
|
pi.registerCommand("search:resume-pending", {
|
|
902
935
|
description: "Run a pending session-search resume or fork action",
|
|
903
936
|
handler: async (args, ctx) => {
|
|
904
|
-
const
|
|
905
|
-
const action = qolSessionSearchPendingActions.get(id);
|
|
937
|
+
const action = takeSessionSearchPendingAction(args.trim());
|
|
906
938
|
if (!action) {
|
|
907
939
|
ctx.ui.notify("No pending session-search resume/fork action found.", "warning");
|
|
908
940
|
return;
|
|
909
941
|
}
|
|
910
|
-
qolSessionSearchPendingActions.delete(id);
|
|
911
942
|
if (!(await runSessionSearchResumeOrFork(pi, ctx, action))) ctx.ui.notify("Session resume/fork is unavailable in this context.", "error");
|
|
912
943
|
},
|
|
913
944
|
});
|
|
@@ -952,7 +983,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
952
983
|
return;
|
|
953
984
|
}
|
|
954
985
|
if (sub === "notify-test") {
|
|
955
|
-
sendQolNotification(ctx, "test", "QOL notification test", "info", `test:${Date.now()}`);
|
|
986
|
+
void sendQolNotification(pi, ctx, "test", "QOL notification test", "info", `test:${Date.now()}`);
|
|
956
987
|
ctx.ui.notify("Sent QOL notification test.", "info");
|
|
957
988
|
return;
|
|
958
989
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillagreen/pi-qol",
|
|
3
|
-
"version": "2.2
|
|
3
|
+
"version": "2.3.2",
|
|
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": [
|
|
@@ -504,9 +504,9 @@
|
|
|
504
504
|
{
|
|
505
505
|
"key": "sessionSearch.cacheTtlSeconds",
|
|
506
506
|
"label": "Session cache TTL",
|
|
507
|
-
"description": "Seconds
|
|
507
|
+
"description": "Seconds a loaded session index is kept in memory before it is released; the next search opens at once and reloads it. 0 keeps the index until the session ends.",
|
|
508
508
|
"type": "number",
|
|
509
|
-
"default":
|
|
509
|
+
"default": 300,
|
|
510
510
|
"category": "Session Search",
|
|
511
511
|
"apply": "live"
|
|
512
512
|
},
|
|
@@ -988,6 +988,7 @@
|
|
|
988
988
|
},
|
|
989
989
|
"files": [
|
|
990
990
|
"extensions/",
|
|
991
|
+
"scripts/",
|
|
991
992
|
"assets/",
|
|
992
993
|
"README.md",
|
|
993
994
|
"package.json"
|