@vanillagreen/pi-qol 1.0.4 → 1.1.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 +13 -6
- package/assets/session-search.gif +0 -0
- package/extensions/qol.ts +710 -259
- package/package.json +10 -23
- package/assets/session-search.png +0 -0
package/extensions/qol.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { AgentMessage } from "@
|
|
2
|
-
import { complete, type Message } from "@
|
|
3
|
-
import { AssistantMessageComponent, BorderedLoader, convertToLlm, CustomEditor, serializeConversation, SessionManager, Theme, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type KeybindingsManager, type SessionEntry } from "@
|
|
4
|
-
import type { AutocompleteItem, AutocompleteSuggestions, EditorTheme, TUI } from "@
|
|
5
|
-
import { matchesKey, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@
|
|
1
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { complete, type Message } from "@earendil-works/pi-ai";
|
|
3
|
+
import { AssistantMessageComponent, BorderedLoader, convertToLlm, CustomEditor, serializeConversation, SessionManager, Theme, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type KeybindingsManager, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { AutocompleteItem, AutocompleteSuggestions, EditorTheme, TUI } from "@earendil-works/pi-tui";
|
|
5
|
+
import { matchesKey, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
6
6
|
import { execFile, execFileSync } from "node:child_process";
|
|
7
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
10
10
|
|
|
@@ -21,9 +21,26 @@ const QUESTION_SERVICE_SYMBOL = Symbol.for("vstack.pi-questions.service");
|
|
|
21
21
|
const QOL_NOTIFICATION_SERVICE_SYMBOL = Symbol.for("vstack.pi-qol.notification-service");
|
|
22
22
|
const VSTACK_MODAL_LOCK_SYMBOL = Symbol.for("vstack.pi.modal-lock");
|
|
23
23
|
const CAVEMAN_BRIDGE_SYMBOL = Symbol.for("vstack.pi.caveman");
|
|
24
|
+
const PI_AGENTS_STATUSLINE_SYMBOL = Symbol.for("vstack.pi-agents-tmux.statusline");
|
|
24
25
|
const CAVEMAN_ICON_ACTIVE = "\uee9a";
|
|
25
26
|
const CAVEMAN_ICON_INACTIVE = "\u{f19e0}";
|
|
26
27
|
|
|
28
|
+
type AgentAsciiColor = "red" | "green" | "yellow" | "blue" | "magenta" | "cyan";
|
|
29
|
+
|
|
30
|
+
interface PiAgentsStatuslineBridge {
|
|
31
|
+
getCurrentSubagent(cwd?: string): { name: string; color?: string } | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const AGENT_ASCII_COLOR_SEQUENCE: AgentAsciiColor[] = ["magenta", "green", "blue", "cyan", "yellow", "red"];
|
|
35
|
+
const AGENT_ASCII_BG: Record<AgentAsciiColor, number> = {
|
|
36
|
+
red: 41,
|
|
37
|
+
green: 42,
|
|
38
|
+
yellow: 43,
|
|
39
|
+
blue: 44,
|
|
40
|
+
magenta: 45,
|
|
41
|
+
cyan: 46,
|
|
42
|
+
};
|
|
43
|
+
|
|
27
44
|
interface CavemanBridge {
|
|
28
45
|
isActive(): boolean;
|
|
29
46
|
getMode(): string;
|
|
@@ -41,6 +58,52 @@ function readCavemanBridge(): CavemanBridge | undefined {
|
|
|
41
58
|
const value = host[CAVEMAN_BRIDGE_SYMBOL];
|
|
42
59
|
return value && typeof value === "object" ? (value as CavemanBridge) : undefined;
|
|
43
60
|
}
|
|
61
|
+
|
|
62
|
+
function readPiAgentsStatuslineBridge(): PiAgentsStatuslineBridge | undefined {
|
|
63
|
+
const host = globalThis as unknown as Record<PropertyKey, unknown>;
|
|
64
|
+
const value = host[PI_AGENTS_STATUSLINE_SYMBOL];
|
|
65
|
+
return value && typeof value === "object" && typeof (value as PiAgentsStatuslineBridge).getCurrentSubagent === "function"
|
|
66
|
+
? (value as PiAgentsStatuslineBridge)
|
|
67
|
+
: undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeAgentAsciiColor(value: string | undefined): AgentAsciiColor | undefined {
|
|
71
|
+
const normalized = value?.trim().toLowerCase().replace(/[^a-z]/g, "");
|
|
72
|
+
switch (normalized) {
|
|
73
|
+
case "red": return "red";
|
|
74
|
+
case "green": return "green";
|
|
75
|
+
case "yellow":
|
|
76
|
+
case "orange": return "yellow";
|
|
77
|
+
case "blue": return "blue";
|
|
78
|
+
case "magenta":
|
|
79
|
+
case "purple":
|
|
80
|
+
case "violet": return "magenta";
|
|
81
|
+
case "cyan":
|
|
82
|
+
case "teal": return "cyan";
|
|
83
|
+
default: return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fallbackAgentAsciiColor(name: string): AgentAsciiColor {
|
|
88
|
+
let hash = 0;
|
|
89
|
+
for (const char of name) hash = ((hash * 31) + char.charCodeAt(0)) >>> 0;
|
|
90
|
+
return AGENT_ASCII_COLOR_SEQUENCE[hash % AGENT_ASCII_COLOR_SEQUENCE.length] ?? "magenta";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function ansiAgentBg(color: AgentAsciiColor, text: string): string {
|
|
94
|
+
return `\x1b[30;${AGENT_ASCII_BG[color]}m${text}\x1b[39;49m`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function subagentStatuslineMarker(cwd: string, maxInnerWidth = 24): { plain: string; styled: string } | undefined {
|
|
98
|
+
const bridgeInfo = readPiAgentsStatuslineBridge()?.getCurrentSubagent(cwd);
|
|
99
|
+
const envName = process.env.PI_SUBAGENT_CHILD_AGENT?.trim();
|
|
100
|
+
const rawName = bridgeInfo?.name?.trim() || envName;
|
|
101
|
+
if (!rawName) return undefined;
|
|
102
|
+
const color = normalizeAgentAsciiColor(bridgeInfo?.color) ?? normalizeAgentAsciiColor(process.env.PI_SUBAGENT_CHILD_COLOR) ?? fallbackAgentAsciiColor(rawName);
|
|
103
|
+
const inner = truncateToWidth(rawName, Math.max(1, maxInnerWidth), "…");
|
|
104
|
+
const plain = ` ${inner} `;
|
|
105
|
+
return { plain, styled: ansiAgentBg(color, plain) };
|
|
106
|
+
}
|
|
44
107
|
const THINKING_TIMER_STORE_SYMBOL = Symbol.for("vstack.pi-qol.thinking-timer.store");
|
|
45
108
|
const THINKING_TIMER_PATCH_SYMBOL = Symbol.for("vstack.pi-qol.thinking-timer.patch");
|
|
46
109
|
const SESSION_SEARCH_PENDING_SYMBOL = Symbol.for("vstack.pi-qol.session-search.pending-context");
|
|
@@ -62,7 +125,7 @@ const DEFAULT_PERMISSION_GATE_PREVIEW_CHARS = 1200;
|
|
|
62
125
|
const DEFAULT_PERMISSION_GATE_PREVIEW_LINE_WIDTH = 120;
|
|
63
126
|
const DEFAULT_SESSION_SEARCH_LIMIT = 40;
|
|
64
127
|
const DEFAULT_SESSION_SEARCH_PREVIEW_SNIPPETS = 6;
|
|
65
|
-
const DEFAULT_SESSION_SEARCH_SHORTCUT = "
|
|
128
|
+
const DEFAULT_SESSION_SEARCH_SHORTCUT = "f2";
|
|
66
129
|
const DEFAULT_SESSION_SEARCH_SUMMARY_INPUT_CHARS = 180_000;
|
|
67
130
|
const DEFAULT_SESSION_SEARCH_SUMMARY_MAX_TOKENS = 4096;
|
|
68
131
|
const DEFAULT_SESSION_SEARCH_CACHE_TTL_SECONDS = 0;
|
|
@@ -1257,25 +1320,29 @@ function cavemanIconTone(mode: string, active: boolean): "muted" | "text" | "suc
|
|
|
1257
1320
|
|
|
1258
1321
|
function renderStatusLine(width: number, ctx: ExtensionContext, git: GitState, pi: ExtensionAPI, theme: Pick<Theme, "fg">): string {
|
|
1259
1322
|
const { label: contextLabel, percent } = statuslineContextInfo(ctx);
|
|
1260
|
-
const projectChunk = `${git.projectName}${gitBadge(git, settingBoolean("showDirtyMarker", true, ctx.cwd))} ${formatModelName(ctx)}
|
|
1323
|
+
const projectChunk = `${git.projectName}${gitBadge(git, settingBoolean("showDirtyMarker", true, ctx.cwd))} ${formatModelName(ctx)}`;
|
|
1324
|
+
const statusSeparator = " / ";
|
|
1261
1325
|
const thinkingLevel = normalizeThinkingLevel(pi.getThinkingLevel());
|
|
1262
1326
|
const thinkingChunk = thinkingLevel;
|
|
1263
|
-
const contextChunk = `
|
|
1327
|
+
const contextChunk = ` ${contextLabel}`;
|
|
1264
1328
|
const cavemanBridge = readCavemanBridge();
|
|
1265
1329
|
const cavemanVisible = !!cavemanBridge && (cavemanBridge.isStatusBadgeEnabled?.(ctx.cwd) ?? true);
|
|
1266
1330
|
const caveman = cavemanVisible ? cavemanBridge : undefined;
|
|
1267
1331
|
const cavemanActive = caveman?.isActive() ?? false;
|
|
1268
1332
|
const cavemanGlyph = caveman ? (cavemanActive ? CAVEMAN_ICON_ACTIVE : CAVEMAN_ICON_INACTIVE) : "";
|
|
1269
1333
|
const cavemanTone = cavemanIconTone(caveman?.getMode() ?? "off", cavemanActive);
|
|
1270
|
-
const cavemanSegment = caveman ?
|
|
1271
|
-
const contextSeparator = caveman ?
|
|
1272
|
-
const leftPlain = `${projectChunk}${thinkingChunk}${cavemanSegment}${contextSeparator}${contextChunk.trimStart()}`;
|
|
1273
|
-
const
|
|
1334
|
+
const cavemanSegment = caveman ? `${statusSeparator}${cavemanGlyph}` : "";
|
|
1335
|
+
const contextSeparator = caveman ? ` ${statusSeparator}` : "";
|
|
1336
|
+
const leftPlain = `${projectChunk}${statusSeparator}${thinkingChunk}${cavemanSegment}${contextSeparator}${contextChunk.trimStart()}`;
|
|
1337
|
+
const percentPlain = percent === null ? "…%" : `${percent}%`;
|
|
1338
|
+
const subagentMarker = subagentStatuslineMarker(ctx.cwd);
|
|
1339
|
+
const rightPlain = subagentMarker ? `${percentPlain} ${subagentMarker.plain}` : percentPlain;
|
|
1274
1340
|
const percentColor = percent === null ? "muted" : percent <= 15 ? "error" : percent <= 30 ? "warning" : "success";
|
|
1341
|
+
const separatorColored = theme.fg("muted", statusSeparator);
|
|
1275
1342
|
const leftColored = caveman
|
|
1276
|
-
? `${theme.fg("accent", projectChunk)}${theme.fg(THINKING_TOKEN[thinkingLevel], thinkingChunk)}${theme.fg(
|
|
1277
|
-
: `${theme.fg("accent", projectChunk)}${theme.fg(THINKING_TOKEN[thinkingLevel], thinkingChunk)}${theme.fg("accent", contextChunk)}`;
|
|
1278
|
-
const right = theme.fg(percentColor,
|
|
1343
|
+
? `${theme.fg("accent", projectChunk)}${separatorColored}${theme.fg(THINKING_TOKEN[thinkingLevel], thinkingChunk)}${separatorColored}${theme.fg(cavemanTone, cavemanGlyph)}${theme.fg("muted", contextSeparator)}${theme.fg("accent", contextChunk.trimStart())}`
|
|
1344
|
+
: `${theme.fg("accent", projectChunk)}${separatorColored}${theme.fg(THINKING_TOKEN[thinkingLevel], thinkingChunk)}${theme.fg("accent", contextChunk)}`;
|
|
1345
|
+
const right = subagentMarker ? `${theme.fg(percentColor, percentPlain)} ${subagentMarker.styled}` : theme.fg(percentColor, percentPlain);
|
|
1279
1346
|
const minimumGap = 1;
|
|
1280
1347
|
const gapWidth = Math.max(minimumGap, width - visibleWidth(leftPlain) - visibleWidth(rightPlain) - 2);
|
|
1281
1348
|
const filled = percent === null ? 0 : Math.round(gapWidth * (percent / 100));
|
|
@@ -2205,7 +2272,7 @@ function statusMessage(ctx: ExtensionContext): string {
|
|
|
2205
2272
|
return [
|
|
2206
2273
|
"Pi QOL status",
|
|
2207
2274
|
`Statusline: ${settingBoolean("replaceFooter", true, ctx.cwd) ? "replaces footer" : "footer preserved"}; prompt=${settingBoolean("compactPrompt", true, ctx.cwd) ? "π compact" : "default chrome"}`,
|
|
2208
|
-
`
|
|
2275
|
+
`shift+enter newline: ${settingBoolean("newlineOnShiftEnter", true, ctx.cwd) ? "enabled" : "disabled"}`,
|
|
2209
2276
|
`Fallback newline key: ${newlineFallbackKey(ctx.cwd)}`,
|
|
2210
2277
|
`Pending queue preview: ${settingBoolean("pendingQueue.asciiGreen", true, ctx.cwd) ? "ANSI green" : "Pi default"}`,
|
|
2211
2278
|
`Image chips: ${settingBoolean("showImageChips", true, ctx.cwd) ? "filled (placeholders and existing image paths)" : "off"}`,
|
|
@@ -2221,23 +2288,12 @@ function statusMessage(ctx: ExtensionContext): string {
|
|
|
2221
2288
|
`Notifications: ${settingBoolean("notification.enabled", true, ctx.cwd) ? `enabled (bell=${settingBoolean("notification.bell", true, ctx.cwd)}, native=${settingBoolean("notification.native", true, ctx.cwd)}, tmuxClientTty=${settingBoolean("notification.tmuxNativeClientTty", true, ctx.cwd)}, tmuxMessage=${settingBoolean("notification.tmux", false, ctx.cwd)})` : "disabled"}`,
|
|
2222
2289
|
`Permission gate: ${settingBoolean("permissionGate.enabled", false, ctx.cwd) ? `enabled (${permissionGateCommands(ctx.cwd).join(", ") || "none configured"}; preview ${boundedSettingNumber("permissionGate.previewLines", DEFAULT_PERMISSION_GATE_PREVIEW_LINES, 4, 40, ctx.cwd)} lines/${boundedSettingNumber("permissionGate.previewChars", DEFAULT_PERMISSION_GATE_PREVIEW_CHARS, 200, 5000, ctx.cwd)} chars)` : "disabled"}`,
|
|
2223
2290
|
`Thinking timer: ${settingBoolean("thinkingTimer.enabled", true, ctx.cwd) ? "enabled" : "disabled"}`,
|
|
2224
|
-
"If
|
|
2291
|
+
"If shift+enter still submits, configure your terminal/tmux to send a distinct shift+enter sequence or use the fallback key.",
|
|
2225
2292
|
].join("\n");
|
|
2226
2293
|
}
|
|
2227
2294
|
|
|
2228
2295
|
|
|
2229
|
-
type
|
|
2230
|
-
|
|
2231
|
-
type QolSessionAction = "resume" | "copy" | "summarize" | "newSession" | "back";
|
|
2232
|
-
|
|
2233
|
-
const QOL_SESSION_ACTIONS: QolSessionAction[] = ["resume", "copy", "summarize", "newSession", "back"];
|
|
2234
|
-
const QOL_SESSION_ACTION_LABELS: Record<QolSessionAction, string> = {
|
|
2235
|
-
resume: "Resume Session From Here",
|
|
2236
|
-
copy: "Copy Prompt",
|
|
2237
|
-
summarize: "Inject Context",
|
|
2238
|
-
newSession: "New Session + Context",
|
|
2239
|
-
back: "Back",
|
|
2240
|
-
};
|
|
2296
|
+
type QolSessionSearchScope = "current" | "all";
|
|
2241
2297
|
|
|
2242
2298
|
interface QolSessionSearchSession {
|
|
2243
2299
|
allMessagesText: string;
|
|
@@ -2257,6 +2313,13 @@ interface QolSessionSearchResult extends QolSessionSearchSession {
|
|
|
2257
2313
|
snippets: string[];
|
|
2258
2314
|
}
|
|
2259
2315
|
|
|
2316
|
+
interface QolSessionSearchHit {
|
|
2317
|
+
message: QolSessionUserMessage;
|
|
2318
|
+
rank: number;
|
|
2319
|
+
result: QolSessionSearchResult;
|
|
2320
|
+
snippet: string;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2260
2323
|
interface QolParsedSessionQuery {
|
|
2261
2324
|
mode: "tokens" | "regex";
|
|
2262
2325
|
tokens: Array<{ kind: "fuzzy" | "phrase"; value: string }>;
|
|
@@ -2267,8 +2330,9 @@ interface QolParsedSessionQuery {
|
|
|
2267
2330
|
interface QolSessionSearchState {
|
|
2268
2331
|
cursor: number;
|
|
2269
2332
|
query: string;
|
|
2270
|
-
results:
|
|
2333
|
+
results: QolSessionSearchHit[];
|
|
2271
2334
|
selected: number;
|
|
2335
|
+
scope: QolSessionSearchScope;
|
|
2272
2336
|
total: number;
|
|
2273
2337
|
}
|
|
2274
2338
|
|
|
@@ -2287,26 +2351,31 @@ interface QolSessionMessagesState {
|
|
|
2287
2351
|
}
|
|
2288
2352
|
|
|
2289
2353
|
interface QolSessionActionState {
|
|
2290
|
-
actionIndex: number;
|
|
2291
2354
|
message: QolSessionUserMessage;
|
|
2292
2355
|
result: QolSessionSearchResult;
|
|
2293
2356
|
}
|
|
2294
2357
|
|
|
2295
|
-
interface
|
|
2296
|
-
cursor: number;
|
|
2358
|
+
interface QolSessionContextConfirmState {
|
|
2297
2359
|
message: QolSessionUserMessage;
|
|
2298
|
-
prompt: string;
|
|
2299
2360
|
result: QolSessionSearchResult;
|
|
2361
|
+
returnScreen: "search" | "messages" | "actions";
|
|
2300
2362
|
type: "summarize" | "newSession";
|
|
2301
2363
|
}
|
|
2302
2364
|
|
|
2365
|
+
interface QolSessionForkConfirmState {
|
|
2366
|
+
messages: QolSessionUserMessage[];
|
|
2367
|
+
result: QolSessionSearchResult;
|
|
2368
|
+
returnScreen: "search" | "messages" | "actions";
|
|
2369
|
+
selected: number;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2303
2372
|
interface QolSessionSearchPendingMessage {
|
|
2304
2373
|
content: string;
|
|
2305
2374
|
details: Record<string, unknown>;
|
|
2306
2375
|
}
|
|
2307
2376
|
|
|
2308
2377
|
interface QolSessionPaletteAction {
|
|
2309
|
-
type: "cancel" | "resume" | "copy" | "summarize" | "newSession";
|
|
2378
|
+
type: "cancel" | "resume" | "fork" | "copy" | "summarize" | "newSession";
|
|
2310
2379
|
customPrompt?: string;
|
|
2311
2380
|
message?: QolSessionUserMessage;
|
|
2312
2381
|
result?: QolSessionSearchResult;
|
|
@@ -2316,6 +2385,8 @@ let qolSessionSearchCache: QolSessionSearchSession[] = [];
|
|
|
2316
2385
|
let qolSessionSearchLoadedAt = 0;
|
|
2317
2386
|
let qolSessionSearchLoading: Promise<QolSessionSearchSession[]> | undefined;
|
|
2318
2387
|
const qolSessionUserMessagesCache = new Map<string, QolSessionUserMessage[]>();
|
|
2388
|
+
const qolSessionSearchPendingActions = new Map<string, QolSessionPaletteAction>();
|
|
2389
|
+
let qolSessionSearchPendingActionCounter = 0;
|
|
2319
2390
|
|
|
2320
2391
|
function getPendingSessionSearchMessage(): QolSessionSearchPendingMessage | undefined {
|
|
2321
2392
|
return (globalThis as unknown as Record<PropertyKey, unknown>)[SESSION_SEARCH_PENDING_SYMBOL] as QolSessionSearchPendingMessage | undefined;
|
|
@@ -2355,11 +2426,12 @@ function resolveSettingsRelativePath(value: string, settingsPath: string): strin
|
|
|
2355
2426
|
}
|
|
2356
2427
|
|
|
2357
2428
|
function sessionSearchShortcut(cwd?: string): string | undefined {
|
|
2358
|
-
// Legacy escape hatch from the original
|
|
2429
|
+
// Legacy escape hatch from the original ctrl+f setting. If users disabled it,
|
|
2359
2430
|
// keep shortcuts disabled even though the default shortcut is now conflict-free.
|
|
2360
2431
|
if (!settingBoolean("sessionSearch.ctrlFShortcut", true, cwd)) return undefined;
|
|
2361
2432
|
const shortcut = settingStringAllowEmpty("sessionSearch.shortcutKey", DEFAULT_SESSION_SEARCH_SHORTCUT, cwd).trim().toLowerCase();
|
|
2362
2433
|
if (!shortcut || shortcut === "none" || shortcut === "off" || shortcut === "false") return undefined;
|
|
2434
|
+
if (shortcut === "f3") return DEFAULT_SESSION_SEARCH_SHORTCUT;
|
|
2363
2435
|
return shortcut;
|
|
2364
2436
|
}
|
|
2365
2437
|
|
|
@@ -2381,6 +2453,23 @@ function configuredSessionDir(cwd: string): string | undefined {
|
|
|
2381
2453
|
return configured;
|
|
2382
2454
|
}
|
|
2383
2455
|
|
|
2456
|
+
function canonicalPathForSessionSearch(path: string | undefined): string | undefined {
|
|
2457
|
+
if (!path) return undefined;
|
|
2458
|
+
try {
|
|
2459
|
+
return realpathSync.native(path);
|
|
2460
|
+
} catch {
|
|
2461
|
+
return resolve(path);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
function sameSessionSearchProject(sessionCwd: string | undefined, cwd: string): boolean {
|
|
2466
|
+
return canonicalPathForSessionSearch(sessionCwd) === canonicalPathForSessionSearch(cwd);
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
function defaultSessionSearchScope(cwd?: string): QolSessionSearchScope {
|
|
2470
|
+
return settingString("sessionSearch.defaultScope", "current", cwd).toLowerCase() === "all" ? "all" : "current";
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2384
2473
|
async function loadQolSessionSearchSessions(ctx: ExtensionContext, onProgress?: (loaded: number, total: number) => void): Promise<QolSessionSearchSession[]> {
|
|
2385
2474
|
const customSessionDir = configuredSessionDir(ctx.cwd);
|
|
2386
2475
|
const infos = customSessionDir
|
|
@@ -2456,8 +2545,8 @@ function isPendingQueueHintText(text: string): boolean {
|
|
|
2456
2545
|
return plain.startsWith("↳ ") && plain.includes("queued messages");
|
|
2457
2546
|
}
|
|
2458
2547
|
|
|
2459
|
-
function
|
|
2460
|
-
return ansiGreen(
|
|
2548
|
+
function pendingQueuePreviewLine(text: string): string {
|
|
2549
|
+
return ansiGreen(`┃ ${text}`);
|
|
2461
2550
|
}
|
|
2462
2551
|
|
|
2463
2552
|
function isQueuedMessageStatusText(text: string): boolean {
|
|
@@ -2500,7 +2589,8 @@ function installPendingQueueThemePatch(ctx: ExtensionContext): void {
|
|
|
2500
2589
|
proto[PENDING_QUEUE_THEME_PATCH_SYMBOL] = patch;
|
|
2501
2590
|
proto.fg = function patchedQolFg(this: Theme, token: string, text: string): string {
|
|
2502
2591
|
if (token === "dim" && typeof text === "string" && settingBoolean("pendingQueue.asciiGreen", true, patch.cwd)) {
|
|
2503
|
-
if (isPendingQueuePreviewText(text)
|
|
2592
|
+
if (isPendingQueuePreviewText(text)) return pendingQueuePreviewLine(text);
|
|
2593
|
+
if (isPendingQueueHintText(text)) return (patch.originalFg as (this: Theme, token: string, text: string) => string).call(this, token, ` ${text}`);
|
|
2504
2594
|
}
|
|
2505
2595
|
return (patch.originalFg as (this: Theme, token: string, text: string) => string).call(this, token, text);
|
|
2506
2596
|
};
|
|
@@ -2864,6 +2954,16 @@ function messageContentText(content: unknown): string {
|
|
|
2864
2954
|
}).filter(Boolean).join(" ");
|
|
2865
2955
|
}
|
|
2866
2956
|
|
|
2957
|
+
function sessionMessageTimestamp(entry: any, message: any): number | undefined {
|
|
2958
|
+
if (typeof message?.timestamp === "number" && Number.isFinite(message.timestamp)) return message.timestamp;
|
|
2959
|
+
if (typeof entry?.timestamp === "number" && Number.isFinite(entry.timestamp)) return entry.timestamp;
|
|
2960
|
+
if (typeof entry?.timestamp === "string") {
|
|
2961
|
+
const parsed = new Date(entry.timestamp).getTime();
|
|
2962
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
2963
|
+
}
|
|
2964
|
+
return undefined;
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2867
2967
|
function sessionUserMessages(sessionPath: string): QolSessionUserMessage[] {
|
|
2868
2968
|
const cached = qolSessionUserMessagesCache.get(sessionPath);
|
|
2869
2969
|
if (cached) return cached;
|
|
@@ -2883,7 +2983,7 @@ function sessionUserMessages(sessionPath: string): QolSessionUserMessage[] {
|
|
|
2883
2983
|
index: messages.length + 1,
|
|
2884
2984
|
parentId: typeof entry.parentId === "string" || entry.parentId === null ? entry.parentId : undefined,
|
|
2885
2985
|
text,
|
|
2886
|
-
timestamp:
|
|
2986
|
+
timestamp: sessionMessageTimestamp(entry, message),
|
|
2887
2987
|
});
|
|
2888
2988
|
}
|
|
2889
2989
|
} catch {
|
|
@@ -2974,21 +3074,28 @@ function parseSessionSearchQuery(query: string): QolParsedSessionQuery {
|
|
|
2974
3074
|
return { mode: "tokens", tokens };
|
|
2975
3075
|
}
|
|
2976
3076
|
|
|
2977
|
-
function
|
|
2978
|
-
|
|
2979
|
-
|
|
3077
|
+
function searchWordLengthPenalty(word: string, query: string): number {
|
|
3078
|
+
return Math.max(0, word.length - query.length);
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
function searchStringScore(needle: string, haystack: string): number | undefined {
|
|
3082
|
+
const query = normalizeSearchText(needle);
|
|
3083
|
+
const text = normalizeSearchText(haystack);
|
|
2980
3084
|
if (!query) return 0;
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
const
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
3085
|
+
let best: number | undefined;
|
|
3086
|
+
const record = (score: number) => {
|
|
3087
|
+
best = best === undefined ? score : Math.min(best, score);
|
|
3088
|
+
};
|
|
3089
|
+
if (/^[a-z0-9_]+$/i.test(query)) {
|
|
3090
|
+
for (const match of text.matchAll(/[a-z0-9_]+/gi)) {
|
|
3091
|
+
const word = match[0].toLowerCase();
|
|
3092
|
+
if (word === query) record(0);
|
|
3093
|
+
else if (word.startsWith(query)) record(10 + searchWordLengthPenalty(word, query));
|
|
3094
|
+
else if (word.includes(query)) record(100 + searchWordLengthPenalty(word, query));
|
|
3095
|
+
}
|
|
3096
|
+
return best;
|
|
2990
3097
|
}
|
|
2991
|
-
return
|
|
3098
|
+
return text.includes(query) ? 0 : undefined;
|
|
2992
3099
|
}
|
|
2993
3100
|
|
|
2994
3101
|
function matchSessionSearch(session: QolSessionSearchSession, parsed: QolParsedSessionQuery): { matches: boolean; score: number } {
|
|
@@ -2996,21 +3103,12 @@ function matchSessionSearch(session: QolSessionSearchSession, parsed: QolParsedS
|
|
|
2996
3103
|
if (parsed.mode === "regex") {
|
|
2997
3104
|
if (!parsed.regex) return { matches: false, score: 0 };
|
|
2998
3105
|
const index = text.search(parsed.regex);
|
|
2999
|
-
return index < 0 ? { matches: false, score: 0 } : { matches: true, score:
|
|
3106
|
+
return index < 0 ? { matches: false, score: 0 } : { matches: true, score: 0 };
|
|
3000
3107
|
}
|
|
3001
3108
|
if (parsed.tokens.length === 0) return { matches: true, score: 0 };
|
|
3002
3109
|
let score = 0;
|
|
3003
|
-
let normalized: string | undefined;
|
|
3004
3110
|
for (const token of parsed.tokens) {
|
|
3005
|
-
|
|
3006
|
-
normalized ??= normalizeSearchText(text);
|
|
3007
|
-
const phrase = normalizeSearchText(token.value);
|
|
3008
|
-
const index = normalized.indexOf(phrase);
|
|
3009
|
-
if (index < 0) return { matches: false, score: 0 };
|
|
3010
|
-
score += index * 0.1;
|
|
3011
|
-
continue;
|
|
3012
|
-
}
|
|
3013
|
-
const tokenScore = simpleFuzzyScore(token.value, text);
|
|
3111
|
+
const tokenScore = searchStringScore(token.value, text);
|
|
3014
3112
|
if (tokenScore === undefined) return { matches: false, score: 0 };
|
|
3015
3113
|
score += tokenScore;
|
|
3016
3114
|
}
|
|
@@ -3025,9 +3123,10 @@ function sessionSnippetSource(session: QolSessionSearchSession): string {
|
|
|
3025
3123
|
return (session.allMessagesText || session.firstMessage || "").replace(/\s+/g, " ").trim();
|
|
3026
3124
|
}
|
|
3027
3125
|
|
|
3028
|
-
function snippetAround(text: string, start: number, length: number, width: number): string {
|
|
3029
|
-
const
|
|
3030
|
-
const
|
|
3126
|
+
function snippetAround(text: string, start: number, length: number, width: number, lead = Math.floor(width / 3)): string {
|
|
3127
|
+
const safeLead = Math.max(0, Math.min(width, lead));
|
|
3128
|
+
const safeStart = Math.max(0, start - safeLead);
|
|
3129
|
+
const safeEnd = Math.min(text.length, Math.max(start + length, safeStart + width));
|
|
3031
3130
|
const prefix = safeStart > 0 ? "…" : "";
|
|
3032
3131
|
const suffix = safeEnd < text.length ? "…" : "";
|
|
3033
3132
|
return `${prefix}${text.slice(safeStart, safeEnd)}${suffix}`;
|
|
@@ -3079,11 +3178,107 @@ function searchQolSessions(sessions: QolSessionSearchSession[], query: string, c
|
|
|
3079
3178
|
if (!match.matches) continue;
|
|
3080
3179
|
results.push({ ...session, rank: match.score, snippets: buildSessionSnippets(session, parsed, snippetLimit) });
|
|
3081
3180
|
}
|
|
3082
|
-
|
|
3083
|
-
results.sort((a, b) => sortMode === "recent" ? b.modified.getTime() - a.modified.getTime() : a.rank - b.rank || b.modified.getTime() - a.modified.getTime());
|
|
3181
|
+
results.sort((a, b) => a.rank - b.rank || b.modified.getTime() - a.modified.getTime());
|
|
3084
3182
|
return results.slice(0, limit);
|
|
3085
3183
|
}
|
|
3086
3184
|
|
|
3185
|
+
function resultFromSession(session: QolSessionSearchSession, rank = 0, snippets: string[] = []): QolSessionSearchResult {
|
|
3186
|
+
return { ...session, rank, snippets };
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
function matchTextSearch(text: string, parsed: QolParsedSessionQuery): { matches: boolean; score: number } {
|
|
3190
|
+
if (parsed.mode === "regex") {
|
|
3191
|
+
if (!parsed.regex) return { matches: false, score: 0 };
|
|
3192
|
+
const index = text.search(parsed.regex);
|
|
3193
|
+
return index < 0 ? { matches: false, score: 0 } : { matches: true, score: 0 };
|
|
3194
|
+
}
|
|
3195
|
+
if (parsed.tokens.length === 0) return { matches: true, score: 0 };
|
|
3196
|
+
let score = 0;
|
|
3197
|
+
for (const token of parsed.tokens) {
|
|
3198
|
+
const tokenScore = searchStringScore(token.value, text);
|
|
3199
|
+
if (tokenScore === undefined) return { matches: false, score: 0 };
|
|
3200
|
+
score += tokenScore;
|
|
3201
|
+
}
|
|
3202
|
+
return { matches: true, score };
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
function sessionTitleSearchText(session: QolSessionSearchSession): string {
|
|
3206
|
+
return [session.name ?? "", sessionResumeTitle(session), sessionDisplayName(session)].join("\n");
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
function matchPromptSearch(_session: QolSessionSearchSession, message: QolSessionUserMessage, parsed: QolParsedSessionQuery): { matches: boolean; score: number } {
|
|
3210
|
+
return matchTextSearch(message.text, parsed);
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
function buildPromptSnippet(message: QolSessionUserMessage, parsed: QolParsedSessionQuery): string {
|
|
3214
|
+
const source = message.text.replace(/\s+/g, " ").trim();
|
|
3215
|
+
if (!source) return "";
|
|
3216
|
+
if (parsed.mode === "regex" && parsed.regex) {
|
|
3217
|
+
parsed.regex.lastIndex = 0;
|
|
3218
|
+
const match = parsed.regex.exec(source);
|
|
3219
|
+
return match ? snippetAround(source, match.index, match[0].length, 160, 24) : source.slice(0, 160);
|
|
3220
|
+
}
|
|
3221
|
+
if (parsed.tokens.length === 0) return source.slice(0, 160);
|
|
3222
|
+
const lower = source.toLowerCase();
|
|
3223
|
+
for (const token of parsed.tokens) {
|
|
3224
|
+
const value = normalizeSearchText(token.value);
|
|
3225
|
+
if (!value) continue;
|
|
3226
|
+
const index = lower.indexOf(value.toLowerCase());
|
|
3227
|
+
if (index >= 0) return snippetAround(source, index, value.length, 160, 24);
|
|
3228
|
+
}
|
|
3229
|
+
return source.slice(0, 160);
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
function promptRecencyTime(hit: QolSessionSearchHit): number {
|
|
3233
|
+
if (typeof hit.message.timestamp === "number" && Number.isFinite(hit.message.timestamp)) return hit.message.timestamp;
|
|
3234
|
+
return hit.result.modified.getTime();
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
function compareSessionSearchHitsRecent(a: QolSessionSearchHit, b: QolSessionSearchHit): number {
|
|
3238
|
+
return promptRecencyTime(b) - promptRecencyTime(a)
|
|
3239
|
+
|| b.result.modified.getTime() - a.result.modified.getTime()
|
|
3240
|
+
|| b.message.index - a.message.index;
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
function searchQolSessionHits(sessions: QolSessionSearchSession[], query: string, cwd: string): QolSessionSearchHit[] {
|
|
3244
|
+
const limit = Math.max(1, Math.floor(settingNumber("sessionSearch.resultLimit", DEFAULT_SESSION_SEARCH_LIMIT, cwd)));
|
|
3245
|
+
const parsed = parseSessionSearchQuery(query);
|
|
3246
|
+
if (parsed.error) return [];
|
|
3247
|
+
const hits: QolSessionSearchHit[] = [];
|
|
3248
|
+
if (!query.trim()) {
|
|
3249
|
+
for (const session of sessions) {
|
|
3250
|
+
const result = resultFromSession(session);
|
|
3251
|
+
for (const message of userMessagesForResult(result)) {
|
|
3252
|
+
const snippet = buildPromptSnippet(message, parsed);
|
|
3253
|
+
hits.push({ message, rank: 0, result: resultFromSession(session, 0, [snippet]), snippet });
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
hits.sort(compareSessionSearchHitsRecent);
|
|
3257
|
+
return hits.slice(0, limit);
|
|
3258
|
+
}
|
|
3259
|
+
for (const session of sessions) {
|
|
3260
|
+
let addedPromptHit = false;
|
|
3261
|
+
for (const message of userMessagesForResult(resultFromSession(session))) {
|
|
3262
|
+
const match = matchPromptSearch(session, message, parsed);
|
|
3263
|
+
if (!match.matches) continue;
|
|
3264
|
+
const snippet = buildPromptSnippet(message, parsed);
|
|
3265
|
+
hits.push({ message, rank: match.score, result: resultFromSession(session, match.score, [snippet]), snippet });
|
|
3266
|
+
addedPromptHit = true;
|
|
3267
|
+
}
|
|
3268
|
+
if (!addedPromptHit) {
|
|
3269
|
+
const titleMatch = matchTextSearch(sessionTitleSearchText(session), parsed);
|
|
3270
|
+
if (!titleMatch.matches) continue;
|
|
3271
|
+
const messages = userMessagesForResult(resultFromSession(session));
|
|
3272
|
+
const message = messages[messages.length - 1];
|
|
3273
|
+
if (!message) continue;
|
|
3274
|
+
const snippet = buildPromptSnippet(message, { mode: "tokens", tokens: [] });
|
|
3275
|
+
hits.push({ message, rank: titleMatch.score, result: resultFromSession(session, titleMatch.score, [snippet]), snippet });
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
hits.sort((a, b) => a.rank - b.rank || compareSessionSearchHitsRecent(a, b));
|
|
3279
|
+
return hits.slice(0, limit);
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3087
3282
|
function styleSessionSnippet(snippet: string, query: string, theme: Theme): string {
|
|
3088
3283
|
let styled = snippet;
|
|
3089
3284
|
const parsed = parseSessionSearchQuery(query);
|
|
@@ -3143,11 +3338,14 @@ function isPrintableInput(data: string): boolean {
|
|
|
3143
3338
|
}
|
|
3144
3339
|
|
|
3145
3340
|
class QolSessionSearchComponent {
|
|
3146
|
-
private screen: "search" | "messages" | "actions" | "
|
|
3341
|
+
private screen: "search" | "messages" | "actions" | "confirmContext" | "confirmFork" = "search";
|
|
3147
3342
|
private searchState: QolSessionSearchState;
|
|
3148
3343
|
private messagesState: QolSessionMessagesState | undefined;
|
|
3149
3344
|
private actionState: QolSessionActionState | undefined;
|
|
3150
|
-
private
|
|
3345
|
+
private actionReturnScreen: "search" | "messages" = "messages";
|
|
3346
|
+
private contextConfirmState: QolSessionContextConfirmState | undefined;
|
|
3347
|
+
private forkConfirmState: QolSessionForkConfirmState | undefined;
|
|
3348
|
+
|
|
3151
3349
|
|
|
3152
3350
|
constructor(
|
|
3153
3351
|
private readonly done: (action: QolSessionPaletteAction) => void,
|
|
@@ -3158,12 +3356,15 @@ class QolSessionSearchComponent {
|
|
|
3158
3356
|
initialQuery = "",
|
|
3159
3357
|
) {
|
|
3160
3358
|
const query = initialQuery.trim();
|
|
3359
|
+
const scope = defaultSessionSearchScope(cwd);
|
|
3360
|
+
const scopedSessions = this.sessionsForScope(scope);
|
|
3161
3361
|
this.searchState = {
|
|
3162
3362
|
cursor: query.length,
|
|
3163
3363
|
query,
|
|
3164
|
-
results:
|
|
3364
|
+
results: searchQolSessionHits(scopedSessions, query, cwd),
|
|
3165
3365
|
selected: 0,
|
|
3166
|
-
|
|
3366
|
+
scope,
|
|
3367
|
+
total: scopedSessions.length,
|
|
3167
3368
|
};
|
|
3168
3369
|
}
|
|
3169
3370
|
|
|
@@ -3172,16 +3373,19 @@ class QolSessionSearchComponent {
|
|
|
3172
3373
|
private maxOverlayRows(): number {
|
|
3173
3374
|
const terminalRows = Number(this.tui.terminal?.rows ?? process.stdout.rows ?? 30);
|
|
3174
3375
|
const safeRows = Number.isFinite(terminalRows) && terminalRows > 0 ? terminalRows : 30;
|
|
3175
|
-
return Math.max(
|
|
3376
|
+
return Math.max(9, Math.floor(safeRows * SESSION_SEARCH_OVERLAY_HEIGHT_RATIO));
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
private compactSearchLayout(): boolean {
|
|
3380
|
+
return this.maxOverlayRows() < 18;
|
|
3176
3381
|
}
|
|
3177
3382
|
|
|
3178
3383
|
private searchMaxVisibleRows(): number {
|
|
3179
3384
|
const configured = Math.max(1, Math.floor(settingNumber("sessionSearch.maxVisible", 8, this.cwd)));
|
|
3180
|
-
//
|
|
3181
|
-
//
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
const responsive = Math.max(1, Math.floor((this.maxOverlayRows() - 12) / 4));
|
|
3385
|
+
// Each hit is two rows plus a separator between hits. Chrome is aggressively
|
|
3386
|
+
// reduced on short terminals so the popup can shrink to one visible hit.
|
|
3387
|
+
const chromeRows = this.compactSearchLayout() ? 7 : 11;
|
|
3388
|
+
const responsive = Math.max(1, Math.floor((this.maxOverlayRows() - chromeRows) / 3));
|
|
3185
3389
|
return Math.max(1, Math.min(configured, responsive));
|
|
3186
3390
|
}
|
|
3187
3391
|
|
|
@@ -3193,20 +3397,31 @@ class QolSessionSearchComponent {
|
|
|
3193
3397
|
return Math.max(1, Math.min(configured, responsive));
|
|
3194
3398
|
}
|
|
3195
3399
|
|
|
3400
|
+
private forkConfirmMaxVisibleRows(): number {
|
|
3401
|
+
const configured = Math.max(1, Math.floor(settingNumber("sessionSearch.messageMaxVisible", 12, this.cwd)));
|
|
3402
|
+
const responsive = Math.max(1, this.maxOverlayRows() - 13);
|
|
3403
|
+
return Math.max(1, Math.min(configured, responsive));
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3196
3406
|
render(width: number): string[] {
|
|
3197
3407
|
const configured = Math.max(70, Math.floor(settingNumber("sessionSearch.overlayWidth", 104, this.cwd)));
|
|
3198
3408
|
const renderWidth = Math.min(Math.max(48, width), configured);
|
|
3199
3409
|
if (this.screen === "messages" && this.messagesState) return this.renderMessages(renderWidth, this.messagesState);
|
|
3200
3410
|
if (this.screen === "actions" && this.actionState) return this.renderActions(renderWidth, this.actionState);
|
|
3201
|
-
if (this.screen === "
|
|
3411
|
+
if (this.screen === "confirmContext" && this.contextConfirmState) return this.renderContextConfirm(renderWidth, this.contextConfirmState);
|
|
3412
|
+
if (this.screen === "confirmFork" && this.forkConfirmState) return this.renderForkConfirm(renderWidth, this.forkConfirmState);
|
|
3202
3413
|
return this.renderSearch(renderWidth);
|
|
3203
3414
|
}
|
|
3204
3415
|
|
|
3205
3416
|
handleInput(data: string): void {
|
|
3206
|
-
if (this.screen === "messages") this.handleMessagesInput(data);
|
|
3207
|
-
else if (this.screen === "actions") this.handleActionInput(data);
|
|
3208
|
-
else if (this.screen === "
|
|
3209
|
-
else this.
|
|
3417
|
+
if (this.screen === "messages" && this.messagesState) this.handleMessagesInput(data);
|
|
3418
|
+
else if (this.screen === "actions" && this.actionState) this.handleActionInput(data);
|
|
3419
|
+
else if (this.screen === "confirmContext" && this.contextConfirmState) this.handleContextConfirmInput(data);
|
|
3420
|
+
else if (this.screen === "confirmFork" && this.forkConfirmState) this.handleForkConfirmInput(data);
|
|
3421
|
+
else {
|
|
3422
|
+
this.screen = "search";
|
|
3423
|
+
this.handleSearchInput(data);
|
|
3424
|
+
}
|
|
3210
3425
|
this.tui.requestRender();
|
|
3211
3426
|
}
|
|
3212
3427
|
|
|
@@ -3214,12 +3429,29 @@ class QolSessionSearchComponent {
|
|
|
3214
3429
|
this.searchState.selected = Math.max(0, Math.min(this.searchState.selected, Math.max(0, this.searchState.results.length - 1)));
|
|
3215
3430
|
}
|
|
3216
3431
|
|
|
3432
|
+
private sessionsForScope(scope = this.searchState.scope): QolSessionSearchSession[] {
|
|
3433
|
+
if (scope === "all") return this.sessions;
|
|
3434
|
+
return this.sessions.filter((session) => sameSessionSearchProject(session.cwd, this.cwd));
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
private updateResults(resetSelection = true): void {
|
|
3438
|
+
const state = this.searchState;
|
|
3439
|
+
const scopedSessions = this.sessionsForScope(state.scope);
|
|
3440
|
+
state.results = searchQolSessionHits(scopedSessions, state.query, this.cwd);
|
|
3441
|
+
state.total = scopedSessions.length;
|
|
3442
|
+
if (resetSelection) state.selected = 0;
|
|
3443
|
+
this.clampSelection();
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
private toggleScope(): void {
|
|
3447
|
+
this.searchState.scope = this.searchState.scope === "current" ? "all" : "current";
|
|
3448
|
+
this.updateResults(true);
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3217
3451
|
private updateQuery(query: string, cursor: number): void {
|
|
3218
3452
|
this.searchState.query = query;
|
|
3219
3453
|
this.searchState.cursor = Math.max(0, Math.min(cursor, query.length));
|
|
3220
|
-
this.
|
|
3221
|
-
this.searchState.selected = 0;
|
|
3222
|
-
this.clampSelection();
|
|
3454
|
+
this.updateResults(true);
|
|
3223
3455
|
}
|
|
3224
3456
|
|
|
3225
3457
|
private selectedMessageIndex(messages: QolSessionUserMessage[], query: string): number {
|
|
@@ -3234,12 +3466,15 @@ class QolSessionSearchComponent {
|
|
|
3234
3466
|
return Math.max(0, messages.length - 1);
|
|
3235
3467
|
}
|
|
3236
3468
|
|
|
3237
|
-
private openMessages(result: QolSessionSearchResult): void {
|
|
3469
|
+
private openMessages(result: QolSessionSearchResult, preferredMessage?: QolSessionUserMessage): void {
|
|
3238
3470
|
const messages = userMessagesForResult(result);
|
|
3471
|
+
const preferredIndex = preferredMessage
|
|
3472
|
+
? messages.findIndex((message) => message.index === preferredMessage.index || (message.entryId && message.entryId === preferredMessage.entryId))
|
|
3473
|
+
: -1;
|
|
3239
3474
|
this.messagesState = {
|
|
3240
3475
|
messages,
|
|
3241
3476
|
result,
|
|
3242
|
-
selected: this.selectedMessageIndex(messages, this.searchState.query),
|
|
3477
|
+
selected: preferredIndex >= 0 ? preferredIndex : this.selectedMessageIndex(messages, this.searchState.query),
|
|
3243
3478
|
};
|
|
3244
3479
|
this.screen = "messages";
|
|
3245
3480
|
}
|
|
@@ -3248,16 +3483,101 @@ class QolSessionSearchComponent {
|
|
|
3248
3483
|
return `Focus on prompt #${message.index}: ${message.text}`;
|
|
3249
3484
|
}
|
|
3250
3485
|
|
|
3486
|
+
private openContextConfirm(type: "summarize" | "newSession", result: QolSessionSearchResult, message: QolSessionUserMessage, returnScreen: "search" | "messages" | "actions"): void {
|
|
3487
|
+
this.contextConfirmState = { message, result, returnScreen, type };
|
|
3488
|
+
this.screen = "confirmContext";
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
private returnFromContextConfirm(state: QolSessionContextConfirmState): void {
|
|
3492
|
+
this.contextConfirmState = undefined;
|
|
3493
|
+
if (state.returnScreen === "actions" && this.actionState) this.screen = "actions";
|
|
3494
|
+
else if (state.returnScreen === "messages" && this.messagesState) this.screen = "messages";
|
|
3495
|
+
else this.screen = "search";
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3498
|
+
private confirmContextAction(state: QolSessionContextConfirmState): void {
|
|
3499
|
+
this.done({ customPrompt: this.selectedFocusText(state.message), message: state.message, result: state.result, type: state.type });
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
private openForkConfirm(result: QolSessionSearchResult, message: QolSessionUserMessage, returnScreen: "search" | "messages" | "actions"): void {
|
|
3503
|
+
const messages = userMessagesForResult(result);
|
|
3504
|
+
const preferredIndex = messages.findIndex((candidate) => candidate.index === message.index || (message.entryId && candidate.entryId === message.entryId));
|
|
3505
|
+
this.forkConfirmState = {
|
|
3506
|
+
messages,
|
|
3507
|
+
result,
|
|
3508
|
+
returnScreen,
|
|
3509
|
+
selected: preferredIndex >= 0 ? preferredIndex : 0,
|
|
3510
|
+
};
|
|
3511
|
+
this.screen = "confirmFork";
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
private returnFromForkConfirm(state: QolSessionForkConfirmState): void {
|
|
3515
|
+
this.forkConfirmState = undefined;
|
|
3516
|
+
if (state.returnScreen === "actions" && this.actionState) this.screen = "actions";
|
|
3517
|
+
else if (state.returnScreen === "messages" && this.messagesState) this.screen = "messages";
|
|
3518
|
+
else this.screen = "search";
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
private confirmForkAction(state: QolSessionForkConfirmState): void {
|
|
3522
|
+
const message = state.messages[state.selected];
|
|
3523
|
+
if (!message) return;
|
|
3524
|
+
this.done({ message, result: state.result, type: "fork" });
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
private returnFromActions(): void {
|
|
3528
|
+
this.actionState = undefined;
|
|
3529
|
+
if (this.actionReturnScreen === "messages" && this.messagesState) this.screen = "messages";
|
|
3530
|
+
else this.screen = "search";
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3251
3533
|
private handleSearchInput(data: string): void {
|
|
3252
3534
|
const state = this.searchState;
|
|
3253
3535
|
if (matchesKey(data, "escape")) {
|
|
3254
3536
|
this.done({ type: "cancel" });
|
|
3255
3537
|
return;
|
|
3256
3538
|
}
|
|
3539
|
+
if (matchesKey(data, "tab")) {
|
|
3540
|
+
this.toggleScope();
|
|
3541
|
+
return;
|
|
3542
|
+
}
|
|
3257
3543
|
if (matchesKey(data, "return") || matchesKey(data, "enter")) {
|
|
3258
|
-
const
|
|
3259
|
-
if (!
|
|
3260
|
-
this.openMessages(result);
|
|
3544
|
+
const hit = state.results[state.selected];
|
|
3545
|
+
if (!hit) return;
|
|
3546
|
+
this.openMessages(hit.result, hit.message);
|
|
3547
|
+
return;
|
|
3548
|
+
}
|
|
3549
|
+
if (matchesKey(data, "alt+c")) {
|
|
3550
|
+
const hit = state.results[state.selected];
|
|
3551
|
+
if (hit) this.done({ message: hit.message, result: hit.result, type: "copy" });
|
|
3552
|
+
return;
|
|
3553
|
+
}
|
|
3554
|
+
if (matchesKey(data, "alt+f")) {
|
|
3555
|
+
const hit = state.results[state.selected];
|
|
3556
|
+
if (hit) this.openForkConfirm(hit.result, hit.message, "search");
|
|
3557
|
+
return;
|
|
3558
|
+
}
|
|
3559
|
+
if (matchesKey(data, "alt+r")) {
|
|
3560
|
+
const hit = state.results[state.selected];
|
|
3561
|
+
if (hit) this.done({ result: hit.result, type: "resume" });
|
|
3562
|
+
return;
|
|
3563
|
+
}
|
|
3564
|
+
if (matchesKey(data, "alt+i")) {
|
|
3565
|
+
const hit = state.results[state.selected];
|
|
3566
|
+
if (hit) this.openContextConfirm("summarize", hit.result, hit.message, "search");
|
|
3567
|
+
return;
|
|
3568
|
+
}
|
|
3569
|
+
if (matchesKey(data, "alt+n")) {
|
|
3570
|
+
const hit = state.results[state.selected];
|
|
3571
|
+
if (hit) this.openContextConfirm("newSession", hit.result, hit.message, "search");
|
|
3572
|
+
return;
|
|
3573
|
+
}
|
|
3574
|
+
if (matchesKey(data, "alt+a")) {
|
|
3575
|
+
const hit = state.results[state.selected];
|
|
3576
|
+
if (hit) {
|
|
3577
|
+
this.actionState = { message: hit.message, result: hit.result };
|
|
3578
|
+
this.actionReturnScreen = "search";
|
|
3579
|
+
this.screen = "actions";
|
|
3580
|
+
}
|
|
3261
3581
|
return;
|
|
3262
3582
|
}
|
|
3263
3583
|
if (matchesKey(data, "up")) {
|
|
@@ -3265,7 +3585,7 @@ class QolSessionSearchComponent {
|
|
|
3265
3585
|
return;
|
|
3266
3586
|
}
|
|
3267
3587
|
if (matchesKey(data, "down")) {
|
|
3268
|
-
state.selected = Math.min(state.results.length - 1, state.selected + 1);
|
|
3588
|
+
state.selected = Math.min(Math.max(0, state.results.length - 1), state.selected + 1);
|
|
3269
3589
|
return;
|
|
3270
3590
|
}
|
|
3271
3591
|
if (matchesKey(data, "-") || matchesKey(data, "pageup")) {
|
|
@@ -3273,7 +3593,7 @@ class QolSessionSearchComponent {
|
|
|
3273
3593
|
return;
|
|
3274
3594
|
}
|
|
3275
3595
|
if (matchesKey(data, "=") || matchesKey(data, "pagedown")) {
|
|
3276
|
-
state.selected = Math.min(state.results.length - 1, state.selected + this.searchMaxVisibleRows());
|
|
3596
|
+
state.selected = Math.min(Math.max(0, state.results.length - 1), state.selected + this.searchMaxVisibleRows());
|
|
3277
3597
|
return;
|
|
3278
3598
|
}
|
|
3279
3599
|
if (matchesKey(data, "left")) {
|
|
@@ -3326,14 +3646,35 @@ class QolSessionSearchComponent {
|
|
|
3326
3646
|
if (matchesKey(data, "return") || matchesKey(data, "enter")) {
|
|
3327
3647
|
const message = state.messages[state.selected];
|
|
3328
3648
|
if (!message) return;
|
|
3329
|
-
this.actionState = {
|
|
3649
|
+
this.actionState = { message, result: state.result };
|
|
3650
|
+
this.actionReturnScreen = "messages";
|
|
3330
3651
|
this.screen = "actions";
|
|
3331
3652
|
return;
|
|
3332
3653
|
}
|
|
3333
|
-
if (data
|
|
3654
|
+
if (matchesKey(data, "alt+r")) {
|
|
3334
3655
|
this.done({ result: state.result, type: "resume" });
|
|
3335
3656
|
return;
|
|
3336
3657
|
}
|
|
3658
|
+
if (matchesKey(data, "alt+c")) {
|
|
3659
|
+
const message = state.messages[state.selected];
|
|
3660
|
+
if (message) this.done({ message, result: state.result, type: "copy" });
|
|
3661
|
+
return;
|
|
3662
|
+
}
|
|
3663
|
+
if (matchesKey(data, "alt+f")) {
|
|
3664
|
+
const message = state.messages[state.selected];
|
|
3665
|
+
if (message) this.openForkConfirm(state.result, message, "messages");
|
|
3666
|
+
return;
|
|
3667
|
+
}
|
|
3668
|
+
if (matchesKey(data, "alt+i")) {
|
|
3669
|
+
const message = state.messages[state.selected];
|
|
3670
|
+
if (message) this.openContextConfirm("summarize", state.result, message, "messages");
|
|
3671
|
+
return;
|
|
3672
|
+
}
|
|
3673
|
+
if (matchesKey(data, "alt+n")) {
|
|
3674
|
+
const message = state.messages[state.selected];
|
|
3675
|
+
if (message) this.openContextConfirm("newSession", state.result, message, "messages");
|
|
3676
|
+
return;
|
|
3677
|
+
}
|
|
3337
3678
|
if (matchesKey(data, "up")) {
|
|
3338
3679
|
state.selected = Math.max(0, state.selected - 1);
|
|
3339
3680
|
return;
|
|
@@ -3363,146 +3704,198 @@ class QolSessionSearchComponent {
|
|
|
3363
3704
|
const state = this.actionState;
|
|
3364
3705
|
if (!state) return;
|
|
3365
3706
|
if (matchesKey(data, "escape") || matchesKey(data, "backspace")) {
|
|
3366
|
-
this.
|
|
3707
|
+
this.returnFromActions();
|
|
3367
3708
|
return;
|
|
3368
3709
|
}
|
|
3369
|
-
if (matchesKey(data, "
|
|
3370
|
-
state.
|
|
3710
|
+
if (matchesKey(data, "alt+c")) {
|
|
3711
|
+
this.done({ message: state.message, result: state.result, type: "copy" });
|
|
3371
3712
|
return;
|
|
3372
3713
|
}
|
|
3373
|
-
if (matchesKey(data, "
|
|
3374
|
-
state.
|
|
3714
|
+
if (matchesKey(data, "alt+f")) {
|
|
3715
|
+
this.openForkConfirm(state.result, state.message, "actions");
|
|
3375
3716
|
return;
|
|
3376
3717
|
}
|
|
3377
|
-
if (matchesKey(data, "
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
this.screen = "focus";
|
|
3718
|
+
if (matchesKey(data, "alt+r")) {
|
|
3719
|
+
this.done({ result: state.result, type: "resume" });
|
|
3720
|
+
return;
|
|
3721
|
+
}
|
|
3722
|
+
if (matchesKey(data, "alt+i")) {
|
|
3723
|
+
this.openContextConfirm("summarize", state.result, state.message, "actions");
|
|
3724
|
+
return;
|
|
3725
|
+
}
|
|
3726
|
+
if (matchesKey(data, "alt+n")) {
|
|
3727
|
+
this.openContextConfirm("newSession", state.result, state.message, "actions");
|
|
3728
|
+
return;
|
|
3389
3729
|
}
|
|
3390
3730
|
}
|
|
3391
3731
|
|
|
3392
|
-
private
|
|
3393
|
-
const state = this.
|
|
3732
|
+
private handleContextConfirmInput(data: string): void {
|
|
3733
|
+
const state = this.contextConfirmState;
|
|
3394
3734
|
if (!state) return;
|
|
3395
|
-
if (matchesKey(data, "escape")) {
|
|
3396
|
-
this.
|
|
3735
|
+
if (matchesKey(data, "escape") || matchesKey(data, "backspace")) {
|
|
3736
|
+
this.returnFromContextConfirm(state);
|
|
3397
3737
|
return;
|
|
3398
3738
|
}
|
|
3399
3739
|
if (matchesKey(data, "return") || matchesKey(data, "enter")) {
|
|
3400
|
-
this.
|
|
3401
|
-
return;
|
|
3402
|
-
}
|
|
3403
|
-
if (matchesKey(data, "left")) {
|
|
3404
|
-
state.cursor = Math.max(0, state.cursor - 1);
|
|
3405
|
-
return;
|
|
3740
|
+
this.confirmContextAction(state);
|
|
3406
3741
|
}
|
|
3407
|
-
|
|
3408
|
-
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
private handleForkConfirmInput(data: string): void {
|
|
3745
|
+
const state = this.forkConfirmState;
|
|
3746
|
+
if (!state) return;
|
|
3747
|
+
if (matchesKey(data, "escape") || matchesKey(data, "backspace")) {
|
|
3748
|
+
this.returnFromForkConfirm(state);
|
|
3409
3749
|
return;
|
|
3410
3750
|
}
|
|
3411
|
-
if (matchesKey(data, "
|
|
3412
|
-
state
|
|
3751
|
+
if (matchesKey(data, "return") || matchesKey(data, "enter")) {
|
|
3752
|
+
this.confirmForkAction(state);
|
|
3413
3753
|
return;
|
|
3414
3754
|
}
|
|
3415
|
-
if (matchesKey(data, "
|
|
3416
|
-
state.
|
|
3755
|
+
if (matchesKey(data, "up")) {
|
|
3756
|
+
state.selected = Math.max(0, state.selected - 1);
|
|
3417
3757
|
return;
|
|
3418
3758
|
}
|
|
3419
|
-
if (matchesKey(data, "
|
|
3420
|
-
state.
|
|
3421
|
-
state.cursor = 0;
|
|
3759
|
+
if (matchesKey(data, "down")) {
|
|
3760
|
+
state.selected = Math.min(Math.max(0, state.messages.length - 1), state.selected + 1);
|
|
3422
3761
|
return;
|
|
3423
3762
|
}
|
|
3424
|
-
if (matchesKey(data, "
|
|
3425
|
-
|
|
3426
|
-
const after = state.prompt.slice(state.cursor);
|
|
3427
|
-
let i = before.length;
|
|
3428
|
-
while (i > 0 && /\s/.test(before[i - 1]!)) i -= 1;
|
|
3429
|
-
while (i > 0 && !/\s/.test(before[i - 1]!)) i -= 1;
|
|
3430
|
-
state.prompt = `${before.slice(0, i)}${after}`;
|
|
3431
|
-
state.cursor = i;
|
|
3763
|
+
if (matchesKey(data, "-") || matchesKey(data, "pageup")) {
|
|
3764
|
+
state.selected = Math.max(0, state.selected - this.forkConfirmMaxVisibleRows());
|
|
3432
3765
|
return;
|
|
3433
3766
|
}
|
|
3434
|
-
if (matchesKey(data, "
|
|
3435
|
-
|
|
3436
|
-
state.prompt = `${state.prompt.slice(0, state.cursor - 1)}${state.prompt.slice(state.cursor)}`;
|
|
3437
|
-
state.cursor -= 1;
|
|
3438
|
-
}
|
|
3767
|
+
if (matchesKey(data, "=") || matchesKey(data, "pagedown")) {
|
|
3768
|
+
state.selected = Math.min(Math.max(0, state.messages.length - 1), state.selected + this.forkConfirmMaxVisibleRows());
|
|
3439
3769
|
return;
|
|
3440
3770
|
}
|
|
3441
|
-
if (matchesKey(data, "
|
|
3442
|
-
|
|
3771
|
+
if (matchesKey(data, "home") || matchesKey(data, "ctrl+a")) {
|
|
3772
|
+
state.selected = 0;
|
|
3443
3773
|
return;
|
|
3444
3774
|
}
|
|
3445
|
-
if (
|
|
3446
|
-
state.
|
|
3447
|
-
state.cursor += data.length;
|
|
3775
|
+
if (matchesKey(data, "end") || matchesKey(data, "ctrl+e")) {
|
|
3776
|
+
state.selected = Math.max(0, state.messages.length - 1);
|
|
3448
3777
|
}
|
|
3449
3778
|
}
|
|
3450
3779
|
|
|
3451
3780
|
private renderSearch(width: number): string[] {
|
|
3452
|
-
const { bottom, divider, empty, filledRow, inner, row,
|
|
3781
|
+
const { bottom, divider, empty, filledRow, inner, row, top } = boxParts(width, this.theme);
|
|
3453
3782
|
const state = this.searchState;
|
|
3454
|
-
const
|
|
3783
|
+
const compact = this.compactSearchLayout();
|
|
3784
|
+
const lines: string[] = [top("Session Search", `${state.results.length} hits`), empty()];
|
|
3455
3785
|
const accent = (s: string) => this.theme.fg("accent", s);
|
|
3456
3786
|
const dim = (s: string) => this.theme.fg("dim", s);
|
|
3457
3787
|
const muted = (s: string) => this.theme.fg("muted", s);
|
|
3458
|
-
const pair = (left: string, right: string) => {
|
|
3459
|
-
const leftWidth = Math.max(1, inner - visibleWidth(right) - 1);
|
|
3460
|
-
const clippedLeft = truncateToWidth(left, leftWidth, "…");
|
|
3461
|
-
const gap = Math.max(1, inner - visibleWidth(clippedLeft) - visibleWidth(right));
|
|
3462
|
-
return `${clippedLeft}${" ".repeat(gap)}${right}`;
|
|
3463
|
-
};
|
|
3464
3788
|
const cursorChar = state.query[state.cursor] ?? " ";
|
|
3465
3789
|
const queryDisplay = `${state.query.slice(0, state.cursor)}${this.theme.inverse(cursorChar)}${state.query.slice(state.cursor + (state.cursor < state.query.length ? 1 : 0))}`;
|
|
3790
|
+
lines.push(row(this.renderScopeTabs(inner)));
|
|
3791
|
+
if (!compact) lines.push(empty());
|
|
3466
3792
|
lines.push(filledRow(` > ${queryDisplay}`));
|
|
3467
|
-
lines.push(row(dim(
|
|
3468
|
-
lines.push(
|
|
3793
|
+
if (!compact) lines.push(row(dim(`tokens · re:<pattern> regex · "phrase" exact`)));
|
|
3794
|
+
lines.push(divider());
|
|
3795
|
+
|
|
3469
3796
|
if (state.results.length === 0) {
|
|
3470
|
-
lines.push(row(muted(state.query.trim() ? "No
|
|
3797
|
+
lines.push(row(muted(state.query.trim() ? "No prompts match your search" : "No prompts found")));
|
|
3471
3798
|
} else {
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
const right = selected ? this.theme.fg("text", rightPlain) : dim(rightPlain);
|
|
3482
|
-
const titleBudget = Math.max(12, rowBudget - visibleWidth(right) - 1);
|
|
3483
|
-
const titleText = truncateToWidth(title, titleBudget, "…");
|
|
3484
|
-
const left = selected ? this.theme.bold(accent(titleText)) : this.theme.bold(titleText);
|
|
3485
|
-
const titleRow = pair(left, right);
|
|
3486
|
-
lines.push(selected ? selectedRow(titleRow) : row(titleRow));
|
|
3487
|
-
|
|
3488
|
-
const folder = shortPathForUi(result.cwd || dirname(result.path));
|
|
3489
|
-
const project = sessionDisplayName(result);
|
|
3490
|
-
lines.push(row(muted(truncateToWidth(`${project} · ${folder}`, inner, "…"))));
|
|
3491
|
-
|
|
3492
|
-
const snippet = state.query.trim() ? (result.snippets[0] || lastUserMessageSnippet(result)) : lastUserMessageSnippet(result);
|
|
3493
|
-
const label = state.query.trim() ? "match" : "last";
|
|
3494
|
-
const snippetPrefix = `${dim(`${label}:`)} `;
|
|
3495
|
-
lines.push(row(`${snippetPrefix}${truncateToWidth(styleSessionSnippet(snippet, state.query, this.theme), inner - visibleWidth(snippetPrefix), "…")}`));
|
|
3496
|
-
if (i < end - 1) lines.push(row(this.theme.fg("borderMuted", "─".repeat(Math.max(8, inner)))));
|
|
3497
|
-
}
|
|
3498
|
-
if (state.results.length > maxVisible) lines.push(empty(), row(dim(`${state.selected + 1}/${state.results.length} ${state.query.trim() ? "matches" : "recent sessions"}`)));
|
|
3799
|
+
lines.push(...this.renderSearchHitPane(inner, row, dim, muted, accent));
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
lines.push(divider());
|
|
3803
|
+
if (compact) {
|
|
3804
|
+
lines.push(row(`${ansiYellow("alt+c/f/r/i/n/a")} ${dim("actions")} ${ansiYellow("tab")} ${dim("scope")}`));
|
|
3805
|
+
} else {
|
|
3806
|
+
lines.push(row(`${ansiYellow("-/=")} ${dim("page")} ${ansiYellow("alt+c")} ${dim("copy")} ${ansiYellow("alt+f")} ${dim("fork")} ${ansiYellow("alt+r")} ${dim("resume")}`));
|
|
3807
|
+
lines.push(row(`${ansiYellow("alt+i")} ${dim("inject+ctx")} ${ansiYellow("alt+n")} ${dim("new+ctx")} ${ansiYellow("alt+a")} ${dim("actions")} ${ansiYellow("tab")} ${dim("scope")}`));
|
|
3499
3808
|
}
|
|
3500
|
-
lines.push(divider(), empty());
|
|
3501
|
-
lines.push(row(`${ansiYellow("-/=")} ${dim("page")} ${ansiYellow("enter")} ${dim("prompts")} ${ansiYellow("ctrl+u")} ${dim("clear")} ${ansiYellow("esc")} ${dim("close")}`));
|
|
3502
3809
|
lines.push(bottom());
|
|
3503
3810
|
return lines;
|
|
3504
3811
|
}
|
|
3505
3812
|
|
|
3813
|
+
private renderSearchHitPane(
|
|
3814
|
+
inner: number,
|
|
3815
|
+
row: (content?: string) => string,
|
|
3816
|
+
dim: (s: string) => string,
|
|
3817
|
+
muted: (s: string) => string,
|
|
3818
|
+
accent: (s: string) => string,
|
|
3819
|
+
): string[] {
|
|
3820
|
+
const state = this.searchState;
|
|
3821
|
+
const compact = this.compactSearchLayout();
|
|
3822
|
+
const maxVisible = this.searchMaxVisibleRows();
|
|
3823
|
+
const start = Math.max(0, Math.min(state.selected - Math.floor(maxVisible / 2), state.results.length - maxVisible));
|
|
3824
|
+
const end = Math.min(start + maxVisible, state.results.length);
|
|
3825
|
+
const gap = ` ${dim("│")} `;
|
|
3826
|
+
const twoPane = inner >= 72;
|
|
3827
|
+
const rightWidth = twoPane ? Math.max(24, Math.floor(inner * 0.43)) : 0;
|
|
3828
|
+
const leftWidth = twoPane ? Math.max(24, inner - rightWidth - visibleWidth(gap)) : inner;
|
|
3829
|
+
const paneRows = Math.max(1, (end - start) * 3 - 1);
|
|
3830
|
+
const selectedHit = state.results[state.selected];
|
|
3831
|
+
const rightLines = twoPane ? this.renderPromptPreviewLines(selectedHit, rightWidth, paneRows, dim, muted, accent) : [];
|
|
3832
|
+
const fixed = (content: string, colWidth: number, selected = false): string => {
|
|
3833
|
+
const safe = content.replace(/[\r\n\t]+/g, " ");
|
|
3834
|
+
const clipped = truncateToWidth(safe, colWidth, "");
|
|
3835
|
+
const padded = clipped + " ".repeat(Math.max(0, colWidth - visibleWidth(clipped)));
|
|
3836
|
+
return selected ? this.theme.bg("selectedBg", padded) : padded;
|
|
3837
|
+
};
|
|
3838
|
+
const leftLines: string[] = [];
|
|
3839
|
+
for (let i = start; i < end; i++) {
|
|
3840
|
+
const hit = state.results[i]!;
|
|
3841
|
+
const selected = i === state.selected;
|
|
3842
|
+
const promptDate = new Date(promptRecencyTime(hit));
|
|
3843
|
+
const metadata = dim(`#${hit.message.index} · ${formatSessionSearchDate(promptDate)}`);
|
|
3844
|
+
const titleText = truncateToWidth(sessionResumeTitle(hit.result), Math.max(8, leftWidth - visibleWidth(metadata) - 1), "…");
|
|
3845
|
+
const title = accent(titleText);
|
|
3846
|
+
const snippet = styleSessionSnippet(hit.snippet || hit.message.text, state.query, this.theme);
|
|
3847
|
+
leftLines.push(fixed(selected ? this.theme.bold(snippet) : snippet, leftWidth, selected));
|
|
3848
|
+
leftLines.push(fixed(`${title} ${metadata}`, leftWidth, selected));
|
|
3849
|
+
if (i < end - 1) leftLines.push(fixed(dim("─".repeat(Math.max(8, leftWidth))), leftWidth));
|
|
3850
|
+
}
|
|
3851
|
+
const output: string[] = [];
|
|
3852
|
+
for (let i = 0; i < leftLines.length; i++) {
|
|
3853
|
+
if (twoPane) output.push(row(`${leftLines[i] ?? ""}${gap}${rightLines[i] ?? " ".repeat(rightWidth)}`));
|
|
3854
|
+
else output.push(row(leftLines[i] ?? ""));
|
|
3855
|
+
}
|
|
3856
|
+
if (!twoPane && selectedHit) {
|
|
3857
|
+
output.push(row(dim("─".repeat(Math.max(8, inner)))));
|
|
3858
|
+
for (const line of this.renderPromptPreviewLines(selectedHit, inner, Math.min(5, paneRows), dim, muted, accent)) output.push(row(line));
|
|
3859
|
+
}
|
|
3860
|
+
if (!compact && state.results.length > maxVisible) output.push(row(dim(`${state.selected + 1}/${state.results.length} ${state.query.trim() ? "matches" : "recent prompts"}`)));
|
|
3861
|
+
return output;
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
private renderPromptPreviewLines(
|
|
3865
|
+
hit: QolSessionSearchHit | undefined,
|
|
3866
|
+
width: number,
|
|
3867
|
+
maxLines: number,
|
|
3868
|
+
dim: (s: string) => string,
|
|
3869
|
+
muted: (s: string) => string,
|
|
3870
|
+
accent: (s: string) => string,
|
|
3871
|
+
): string[] {
|
|
3872
|
+
const fixed = (content: string): string => {
|
|
3873
|
+
const clipped = truncateToWidth(content.replace(/[\r\n\t]+/g, " "), width, "…");
|
|
3874
|
+
return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
3875
|
+
};
|
|
3876
|
+
if (!hit) return Array.from({ length: maxLines }, () => fixed(""));
|
|
3877
|
+
const title = accent("Prompt preview");
|
|
3878
|
+
const cwd = muted(truncateToWidth(shortPathForUi(hit.result.cwd || hit.result.path), width, "…"));
|
|
3879
|
+
const bodyBudget = Math.max(1, maxLines - 3);
|
|
3880
|
+
const body = wrapVisible(styleSessionSnippet(hit.message.text, this.searchState.query, this.theme), width, bodyBudget);
|
|
3881
|
+
const lines = [fixed(title), fixed(cwd), fixed(dim("─".repeat(Math.max(8, width)))), ...body.map(fixed)];
|
|
3882
|
+
while (lines.length < maxLines) lines.push(fixed(""));
|
|
3883
|
+
return lines.slice(0, maxLines);
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
private renderScopeTabs(inner: number): string {
|
|
3887
|
+
const tabs: { id: QolSessionSearchScope; label: string }[] = [
|
|
3888
|
+
{ id: "current", label: "Project" },
|
|
3889
|
+
{ id: "all", label: "All" },
|
|
3890
|
+
];
|
|
3891
|
+
const parts = tabs.map((tab) => {
|
|
3892
|
+
const label = ` ${truncateToWidth(tab.label, 18, "…")} `;
|
|
3893
|
+
if (tab.id === this.searchState.scope) return this.theme.fg("accent", this.theme.inverse(this.theme.bold(label)));
|
|
3894
|
+
return this.theme.bg("selectedBg", this.theme.fg("accent", label));
|
|
3895
|
+
});
|
|
3896
|
+
return truncateToWidth(parts.join(" "), inner, "");
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3506
3899
|
private renderMessages(width: number, state: QolSessionMessagesState): string[] {
|
|
3507
3900
|
const { bottom, divider, empty, inner, row, selectedRow, top } = boxParts(width, this.theme);
|
|
3508
3901
|
const accent = (s: string) => this.theme.fg("accent", s);
|
|
@@ -3516,11 +3909,10 @@ class QolSessionSearchComponent {
|
|
|
3516
3909
|
const gap = Math.max(1, inner - visibleWidth(clippedLeft) - visibleWidth(right));
|
|
3517
3910
|
return `${clippedLeft}${" ".repeat(gap)}${right}`;
|
|
3518
3911
|
};
|
|
3519
|
-
const right = dim(
|
|
3912
|
+
const right = dim(formatSessionSearchDate(result.modified));
|
|
3520
3913
|
const title = truncateToWidth(sessionResumeTitle(result), Math.max(12, inner - visibleWidth(right) - 1), "…");
|
|
3521
3914
|
lines.push(row(pair(this.theme.bold(accent(title)), right)));
|
|
3522
3915
|
lines.push(row(muted(truncateToWidth(shortPathForUi(result.cwd || result.path), inner, "…"))));
|
|
3523
|
-
lines.push(row(dim(`${state.messages.length} user prompt${state.messages.length === 1 ? "" : "s"} · enter opens prompt actions · r resumes latest session state`)));
|
|
3524
3916
|
lines.push(empty(), divider(), empty());
|
|
3525
3917
|
|
|
3526
3918
|
const maxVisible = this.messageMaxVisibleRows();
|
|
@@ -3536,9 +3928,9 @@ class QolSessionSearchComponent {
|
|
|
3536
3928
|
const messageRow = `${number} ${selected ? this.theme.bold(accent(text)) : text}`;
|
|
3537
3929
|
lines.push(selected ? selectedRow(messageRow) : row(messageRow));
|
|
3538
3930
|
}
|
|
3539
|
-
if (state.messages.length > maxVisible) lines.push(empty(), row(dim(`${state.selected + 1}/${state.messages.length} user prompts`)));
|
|
3540
3931
|
lines.push(divider(), empty());
|
|
3541
|
-
lines.push(row(`${ansiYellow("-/=")} ${dim("page")} ${ansiYellow("
|
|
3932
|
+
lines.push(row(`${ansiYellow("-/=")} ${dim("page")} ${ansiYellow("alt+c")} ${dim("copy")} ${ansiYellow("alt+f")} ${dim("fork")} ${ansiYellow("alt+r")} ${dim("resume")}`));
|
|
3933
|
+
lines.push(row(`${ansiYellow("alt+i")} ${dim("inject+ctx")} ${ansiYellow("alt+n")} ${dim("new+ctx")}`));
|
|
3542
3934
|
lines.push(bottom());
|
|
3543
3935
|
return lines;
|
|
3544
3936
|
}
|
|
@@ -3556,46 +3948,85 @@ class QolSessionSearchComponent {
|
|
|
3556
3948
|
lines.push(row(dim("Selected prompt")));
|
|
3557
3949
|
const wrapped = wrapVisible(state.message.text, inner, 4);
|
|
3558
3950
|
for (const line of wrapped) lines.push(row(line));
|
|
3559
|
-
lines.push(empty());
|
|
3560
|
-
lines.push(row(dim("Resume Session From Here restores this prompt in the selected session.")));
|
|
3561
|
-
lines.push(row(dim("Inject Context adds a summary here; New Session + Context imports it into a new session.")));
|
|
3562
3951
|
lines.push(empty(), divider(), empty());
|
|
3563
|
-
const
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3952
|
+
const helpLines = [
|
|
3953
|
+
"Fork session from here opens the source session at the point before this prompt and places the prompt in the editor; submit to branch from there.",
|
|
3954
|
+
"Copy Prompt copies this prompt into your current editor.",
|
|
3955
|
+
"Inject + Context summarizes the source session into the current session, focused on this prompt.",
|
|
3956
|
+
"New + Context creates a fresh session and imports that focused summary there.",
|
|
3957
|
+
];
|
|
3958
|
+
for (const help of helpLines) {
|
|
3959
|
+
for (const line of wrapVisible(dim(help), inner, 2)) lines.push(row(line));
|
|
3960
|
+
}
|
|
3961
|
+
lines.push(empty(), divider(), empty());
|
|
3962
|
+
lines.push(row(`${ansiYellow("alt+c")} ${dim("copy")} ${ansiYellow("alt+f")} ${dim("fork")} ${ansiYellow("alt+r")} ${dim("resume")}`));
|
|
3963
|
+
lines.push(row(`${ansiYellow("alt+i")} ${dim("inject+ctx")} ${ansiYellow("alt+n")} ${dim("new+ctx")}`));
|
|
3964
|
+
lines.push(bottom());
|
|
3965
|
+
return lines;
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3968
|
+
private renderForkConfirm(width: number, state: QolSessionForkConfirmState): string[] {
|
|
3969
|
+
const { bottom, divider, empty, inner, row, selectedRow, top } = boxParts(width, this.theme);
|
|
3970
|
+
const accent = (s: string) => this.theme.fg("accent", s);
|
|
3971
|
+
const dim = (s: string) => this.theme.fg("dim", s);
|
|
3972
|
+
const muted = (s: string) => this.theme.fg("muted", s);
|
|
3973
|
+
const lines: string[] = [top("Fork Session", `${state.selected + 1}/${state.messages.length}`), empty()];
|
|
3974
|
+
const title = truncateToWidth(sessionResumeTitle(state.result), inner, "…");
|
|
3975
|
+
lines.push(row(this.theme.bold(accent(title))));
|
|
3976
|
+
lines.push(row(muted(truncateToWidth(shortPathForUi(state.result.cwd || state.result.path), inner, "…"))));
|
|
3977
|
+
lines.push(empty(), divider(), empty());
|
|
3978
|
+
for (const line of wrapVisible(dim("Choose the prompt to fork from. Pi will open the source session before that prompt and place the prompt in the editor."), inner, 2)) {
|
|
3979
|
+
lines.push(row(line));
|
|
3980
|
+
}
|
|
3568
3981
|
lines.push(empty());
|
|
3569
|
-
|
|
3982
|
+
|
|
3983
|
+
const maxVisible = this.forkConfirmMaxVisibleRows();
|
|
3984
|
+
const start = Math.max(0, Math.min(state.selected - Math.floor(maxVisible / 2), state.messages.length - maxVisible));
|
|
3985
|
+
const end = Math.min(start + maxVisible, state.messages.length);
|
|
3986
|
+
for (let i = start; i < end; i++) {
|
|
3987
|
+
const message = state.messages[i]!;
|
|
3988
|
+
const selected = i === state.selected;
|
|
3989
|
+
const numberPlain = `#${message.index}`;
|
|
3990
|
+
const number = selected ? this.theme.fg("text", numberPlain) : dim(numberPlain);
|
|
3991
|
+
const textWidth = Math.max(12, inner - visibleWidth(number) - 1);
|
|
3992
|
+
const text = truncateToWidth(message.text, textWidth, "…");
|
|
3993
|
+
const messageRow = `${number} ${selected ? this.theme.bold(accent(text)) : text}`;
|
|
3994
|
+
lines.push(selected ? selectedRow(messageRow) : row(messageRow));
|
|
3995
|
+
}
|
|
3996
|
+
if (state.messages.length > maxVisible) lines.push(row(dim(`${state.selected + 1}/${state.messages.length} prompts`)));
|
|
3997
|
+
lines.push(divider(), empty());
|
|
3998
|
+
lines.push(row(`${ansiYellow("backspace")} ${dim("back")}`));
|
|
3570
3999
|
lines.push(bottom());
|
|
3571
4000
|
return lines;
|
|
3572
4001
|
}
|
|
3573
4002
|
|
|
3574
|
-
private
|
|
3575
|
-
const { bottom, divider, empty,
|
|
4003
|
+
private renderContextConfirm(width: number, state: QolSessionContextConfirmState): string[] {
|
|
4004
|
+
const { bottom, divider, empty, inner, row, top } = boxParts(width, this.theme);
|
|
3576
4005
|
const accent = (s: string) => this.theme.fg("accent", s);
|
|
3577
4006
|
const dim = (s: string) => this.theme.fg("dim", s);
|
|
3578
4007
|
const muted = (s: string) => this.theme.fg("muted", s);
|
|
3579
|
-
const action = state.type === "newSession" ? "New
|
|
3580
|
-
const
|
|
3581
|
-
lines
|
|
3582
|
-
lines.push(row(
|
|
4008
|
+
const action = state.type === "newSession" ? "New + Context" : "Inject + Context";
|
|
4009
|
+
const verb = state.type === "newSession" ? "create new session with context" : "inject context";
|
|
4010
|
+
const lines: string[] = [top(action), empty()];
|
|
4011
|
+
lines.push(row(`${accent(sessionResumeTitle(state.result))} ${dim(`#${state.message.index}`)}`));
|
|
4012
|
+
lines.push(row(muted(truncateToWidth(shortPathForUi(state.result.cwd || state.result.path), inner, "…"))));
|
|
3583
4013
|
lines.push(empty(), divider(), empty());
|
|
3584
|
-
const
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
for (
|
|
4014
|
+
const details = [
|
|
4015
|
+
"This summarizes the entire source session, including messages after this prompt.",
|
|
4016
|
+
"The selected prompt is used as the focus for the summary, not as a cutoff point.",
|
|
4017
|
+
state.type === "newSession"
|
|
4018
|
+
? "New + Context creates a fresh session and imports that focused summary there."
|
|
4019
|
+
: "Inject + Context adds that focused summary to your current session.",
|
|
4020
|
+
];
|
|
4021
|
+
for (const detail of details) {
|
|
4022
|
+
for (const line of wrapVisible(dim(detail), inner, 2)) lines.push(row(line));
|
|
3593
4023
|
}
|
|
3594
4024
|
lines.push(empty(), divider(), empty());
|
|
3595
|
-
lines.push(row(`${ansiYellow("
|
|
4025
|
+
lines.push(row(`${ansiYellow("backspace")} ${dim("back")}`));
|
|
3596
4026
|
lines.push(bottom());
|
|
3597
4027
|
return lines;
|
|
3598
4028
|
}
|
|
4029
|
+
|
|
3599
4030
|
}
|
|
3600
4031
|
|
|
3601
4032
|
function wrapVisible(text: string, width: number, maxLines: number): string[] {
|
|
@@ -3660,7 +4091,6 @@ class QolSessionSearchLoadingComponent {
|
|
|
3660
4091
|
empty(),
|
|
3661
4092
|
divider(),
|
|
3662
4093
|
empty(),
|
|
3663
|
-
row(`${ansiYellow("esc")} ${dim("cancel")} ${ansiYellow("ctrl+c")} ${dim("cancel")}`),
|
|
3664
4094
|
bottom(),
|
|
3665
4095
|
];
|
|
3666
4096
|
}
|
|
@@ -3783,7 +4213,7 @@ async function buildSessionSearchContextMessageWithLoader(ctx: ExtensionContext,
|
|
|
3783
4213
|
|
|
3784
4214
|
async function injectSessionSearchContext(pi: ExtensionAPI, ctx: ExtensionContext, result: QolSessionSearchResult, customPrompt?: string): Promise<void> {
|
|
3785
4215
|
const title = sessionDisplayName(result);
|
|
3786
|
-
const message = await buildSessionSearchContextMessageWithLoader(ctx, result, customPrompt, "Inject Context");
|
|
4216
|
+
const message = await buildSessionSearchContextMessageWithLoader(ctx, result, customPrompt, "Inject + Context");
|
|
3787
4217
|
pi.sendMessage({ customType: SESSION_SEARCH_CONTEXT_TYPE, content: message.content, details: message.details, display: true }, { deliverAs: "followUp", triggerTurn: false });
|
|
3788
4218
|
ctx.ui.notify(`Session context injected: ${title}`, "info");
|
|
3789
4219
|
}
|
|
@@ -3792,9 +4222,48 @@ function asCommandContext(ctx: ExtensionContext): (ExtensionContext & Partial<Ex
|
|
|
3792
4222
|
return ctx as ExtensionContext & Partial<ExtensionCommandContext>;
|
|
3793
4223
|
}
|
|
3794
4224
|
|
|
4225
|
+
function queueSessionSearchCommandAction(ctx: ExtensionContext, action: QolSessionPaletteAction): void {
|
|
4226
|
+
const id = `ss-${Date.now().toString(36)}-${(++qolSessionSearchPendingActionCounter).toString(36)}`;
|
|
4227
|
+
qolSessionSearchPendingActions.set(id, action);
|
|
4228
|
+
ctx.ui.setEditorText(`/search:resume-pending ${id}`);
|
|
4229
|
+
ctx.ui.notify(`${sessionDisplayName(action.result!)} — press Enter to ${action.type === "fork" ? "fork" : "resume"}`, "info");
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
async function runSessionSearchResumeOrFork(ctx: ExtensionContext, action: QolSessionPaletteAction): Promise<boolean> {
|
|
4233
|
+
if (!action.result || (action.type !== "resume" && action.type !== "fork")) return true;
|
|
4234
|
+
const commandCtx = asCommandContext(ctx);
|
|
4235
|
+
if (typeof commandCtx.switchSession !== "function") return false;
|
|
4236
|
+
const targetTitle = sessionDisplayName(action.result);
|
|
4237
|
+
const selectedMessage = action.type === "fork" ? action.message : undefined;
|
|
4238
|
+
let replacementStarted = false;
|
|
4239
|
+
try {
|
|
4240
|
+
const result = await commandCtx.switchSession(action.result.path, {
|
|
4241
|
+
withSession: async (replacementCtx: any) => {
|
|
4242
|
+
replacementStarted = true;
|
|
4243
|
+
if (selectedMessage) {
|
|
4244
|
+
const manager = replacementCtx.sessionManager as any;
|
|
4245
|
+
if (selectedMessage.entryId && selectedMessage.parentId && typeof manager?.branch === "function") manager.branch(selectedMessage.parentId);
|
|
4246
|
+
else if (selectedMessage.entryId && selectedMessage.parentId === null && typeof manager?.resetLeaf === "function") manager.resetLeaf();
|
|
4247
|
+
replacementCtx.ui.setEditorText(selectedMessage.text);
|
|
4248
|
+
const timer = setTimeout(() => {
|
|
4249
|
+
replacementCtx.ui.notify(`Forked session: ${targetTitle} at prompt #${selectedMessage.index}. Submit to branch from here.`, "info");
|
|
4250
|
+
}, 0);
|
|
4251
|
+
timer.unref?.();
|
|
4252
|
+
return;
|
|
4253
|
+
}
|
|
4254
|
+
replacementCtx.ui.notify(`Resumed session: ${targetTitle}`, "info");
|
|
4255
|
+
},
|
|
4256
|
+
});
|
|
4257
|
+
if (result.cancelled) ctx.ui.notify(selectedMessage ? "Fork cancelled" : "Resume cancelled", "info");
|
|
4258
|
+
} catch (error) {
|
|
4259
|
+
if (!replacementStarted) ctx.ui.notify(`${selectedMessage ? "Fork" : "Resume"} failed: ${stringifyError(error)}`, "error");
|
|
4260
|
+
}
|
|
4261
|
+
return true;
|
|
4262
|
+
}
|
|
4263
|
+
|
|
3795
4264
|
async function createNewSessionWithSearchContext(pi: ExtensionAPI, ctx: ExtensionContext, result: QolSessionSearchResult, customPrompt?: string): Promise<void> {
|
|
3796
4265
|
const title = sessionDisplayName(result);
|
|
3797
|
-
const message = await buildSessionSearchContextMessageWithLoader(ctx, result, customPrompt, "New
|
|
4266
|
+
const message = await buildSessionSearchContextMessageWithLoader(ctx, result, customPrompt, "New + Context");
|
|
3798
4267
|
|
|
3799
4268
|
const commandCtx = asCommandContext(ctx);
|
|
3800
4269
|
if (typeof commandCtx.newSession === "function") {
|
|
@@ -3823,7 +4292,7 @@ async function createNewSessionWithSearchContext(pi: ExtensionAPI, ctx: Extensio
|
|
|
3823
4292
|
}
|
|
3824
4293
|
setPendingSessionSearchMessage(message);
|
|
3825
4294
|
ctx.ui.setEditorText("/new");
|
|
3826
|
-
ctx.ui.notify(`${title} — press
|
|
4295
|
+
ctx.ui.notify(`${title} — press enter to create a new session with this context`, "info");
|
|
3827
4296
|
}
|
|
3828
4297
|
|
|
3829
4298
|
async function consumePendingSessionSearchContext(pi: ExtensionAPI, ctx: ExtensionContext, reason: unknown): Promise<void> {
|
|
@@ -3857,35 +4326,8 @@ async function openQolSessionSearch(pi: ExtensionAPI, ctx: ExtensionContext, ini
|
|
|
3857
4326
|
releaseModalLock();
|
|
3858
4327
|
}
|
|
3859
4328
|
if (!action || action.type === "cancel" || !action.result) return;
|
|
3860
|
-
if (action.type === "resume") {
|
|
3861
|
-
|
|
3862
|
-
if (typeof commandCtx.switchSession === "function") {
|
|
3863
|
-
const targetTitle = sessionDisplayName(action.result);
|
|
3864
|
-
const selectedMessage = action.message;
|
|
3865
|
-
let replacementStarted = false;
|
|
3866
|
-
try {
|
|
3867
|
-
const result = await commandCtx.switchSession(action.result.path, {
|
|
3868
|
-
withSession: async (replacementCtx: any) => {
|
|
3869
|
-
replacementStarted = true;
|
|
3870
|
-
if (selectedMessage?.entryId) {
|
|
3871
|
-
const manager = replacementCtx.sessionManager as any;
|
|
3872
|
-
if (selectedMessage.parentId && typeof manager?.branch === "function") manager.branch(selectedMessage.parentId);
|
|
3873
|
-
else if (selectedMessage.parentId === null && typeof manager?.resetLeaf === "function") manager.resetLeaf();
|
|
3874
|
-
replacementCtx.ui.setEditorText(selectedMessage.text);
|
|
3875
|
-
replacementCtx.ui.notify(`Resumed ${targetTitle} from prompt #${selectedMessage.index}. Submit to branch from here.`, "info");
|
|
3876
|
-
return;
|
|
3877
|
-
}
|
|
3878
|
-
replacementCtx.ui.notify(`Resumed session: ${targetTitle}`, "info");
|
|
3879
|
-
},
|
|
3880
|
-
});
|
|
3881
|
-
if (result.cancelled) ctx.ui.notify("Resume cancelled", "info");
|
|
3882
|
-
} catch (error) {
|
|
3883
|
-
if (!replacementStarted) ctx.ui.notify(`Resume failed: ${stringifyError(error)}`, "error");
|
|
3884
|
-
}
|
|
3885
|
-
return;
|
|
3886
|
-
}
|
|
3887
|
-
ctx.ui.setEditorText(`/resume ${quoteSessionSearchArg(action.result.path)}`);
|
|
3888
|
-
ctx.ui.notify(`${sessionDisplayName(action.result)} — press Enter to resume`, "info");
|
|
4329
|
+
if (action.type === "resume" || action.type === "fork") {
|
|
4330
|
+
if (!(await runSessionSearchResumeOrFork(ctx, action))) queueSessionSearchCommandAction(ctx, action);
|
|
3889
4331
|
return;
|
|
3890
4332
|
}
|
|
3891
4333
|
if (action.type === "copy") {
|
|
@@ -3899,7 +4341,7 @@ async function openQolSessionSearch(pi: ExtensionAPI, ctx: ExtensionContext, ini
|
|
|
3899
4341
|
await injectSessionSearchContext(pi, ctx, action.result, action.customPrompt);
|
|
3900
4342
|
} catch (error) {
|
|
3901
4343
|
const message = stringifyError(error);
|
|
3902
|
-
ctx.ui.notify(/cancelled/i.test(message) ? "Inject
|
|
4344
|
+
ctx.ui.notify(/cancelled/i.test(message) ? "Inject + Context cancelled" : `Inject + Context failed: ${message}`, /cancelled/i.test(message) ? "info" : "error");
|
|
3903
4345
|
ctx.ui.setStatus(SESSION_SEARCH_STATUS_KEY, undefined);
|
|
3904
4346
|
}
|
|
3905
4347
|
return;
|
|
@@ -3909,16 +4351,12 @@ async function openQolSessionSearch(pi: ExtensionAPI, ctx: ExtensionContext, ini
|
|
|
3909
4351
|
await createNewSessionWithSearchContext(pi, ctx, action.result, action.customPrompt);
|
|
3910
4352
|
} catch (error) {
|
|
3911
4353
|
const message = stringifyError(error);
|
|
3912
|
-
ctx.ui.notify(/cancelled/i.test(message) ? "New
|
|
4354
|
+
ctx.ui.notify(/cancelled/i.test(message) ? "New + Context cancelled" : `New + Context failed: ${message}`, /cancelled/i.test(message) ? "info" : "error");
|
|
3913
4355
|
ctx.ui.setStatus(SESSION_SEARCH_STATUS_KEY, undefined);
|
|
3914
4356
|
}
|
|
3915
4357
|
}
|
|
3916
4358
|
}
|
|
3917
4359
|
|
|
3918
|
-
function quoteSessionSearchArg(value: string): string {
|
|
3919
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
3920
|
-
}
|
|
3921
|
-
|
|
3922
4360
|
function renderSessionSearchContextMessage(message: any, options: any, theme: Theme): Text {
|
|
3923
4361
|
const raw = typeof message.content === "string"
|
|
3924
4362
|
? message.content
|
|
@@ -4505,6 +4943,19 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
4505
4943
|
description: "Refresh the session search index",
|
|
4506
4944
|
handler: async (_args, ctx) => handleSearchCommand("refresh", ctx),
|
|
4507
4945
|
});
|
|
4946
|
+
pi.registerCommand("search:resume-pending", {
|
|
4947
|
+
description: "Run a pending session-search resume or fork action",
|
|
4948
|
+
handler: async (args, ctx) => {
|
|
4949
|
+
const id = args.trim();
|
|
4950
|
+
const action = qolSessionSearchPendingActions.get(id);
|
|
4951
|
+
if (!action) {
|
|
4952
|
+
ctx.ui.notify("No pending session-search resume/fork action found.", "warning");
|
|
4953
|
+
return;
|
|
4954
|
+
}
|
|
4955
|
+
qolSessionSearchPendingActions.delete(id);
|
|
4956
|
+
if (!(await runSessionSearchResumeOrFork(ctx, action))) ctx.ui.notify("Session resume/fork is unavailable in this context.", "error");
|
|
4957
|
+
},
|
|
4958
|
+
});
|
|
4508
4959
|
const shortcut = sessionSearchShortcut();
|
|
4509
4960
|
if (shortcut) {
|
|
4510
4961
|
pi.registerShortcut(shortcut, {
|