@vanillagreen/pi-qol 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/assets/session-search.gif +0 -0
- package/extensions/qol.ts +738 -290
- package/package.json +18 -21
- 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
|
|
|
@@ -125,7 +125,7 @@ const DEFAULT_PERMISSION_GATE_PREVIEW_CHARS = 1200;
|
|
|
125
125
|
const DEFAULT_PERMISSION_GATE_PREVIEW_LINE_WIDTH = 120;
|
|
126
126
|
const DEFAULT_SESSION_SEARCH_LIMIT = 40;
|
|
127
127
|
const DEFAULT_SESSION_SEARCH_PREVIEW_SNIPPETS = 6;
|
|
128
|
-
const DEFAULT_SESSION_SEARCH_SHORTCUT = "
|
|
128
|
+
const DEFAULT_SESSION_SEARCH_SHORTCUT = "f2";
|
|
129
129
|
const DEFAULT_SESSION_SEARCH_SUMMARY_INPUT_CHARS = 180_000;
|
|
130
130
|
const DEFAULT_SESSION_SEARCH_SUMMARY_MAX_TOKENS = 4096;
|
|
131
131
|
const DEFAULT_SESSION_SEARCH_CACHE_TTL_SECONDS = 0;
|
|
@@ -1307,6 +1307,14 @@ function setTmuxWindowOption(target: string, option: string, value: string): voi
|
|
|
1307
1307
|
execFile("tmux", ["set-option", "-wq", "-t", target, option, value], { timeout: 1000 }, () => undefined);
|
|
1308
1308
|
}
|
|
1309
1309
|
|
|
1310
|
+
function readTmuxWindowName(target: string, callback: (name: string | undefined) => void): void {
|
|
1311
|
+
execFile("tmux", ["display-message", "-p", "-t", target, "#{window_name}"], { timeout: 1000 }, (error, stdout) => callback(error ? undefined : stdout.replace(/\r?\n$/, "")));
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function setTmuxWindowName(target: string, name: string): void {
|
|
1315
|
+
execFile("tmux", ["rename-window", "-t", target, name], { timeout: 1000 }, () => undefined);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1310
1318
|
function cavemanIconTone(mode: string, active: boolean): "muted" | "text" | "success" | "thinkingHigh" | "error" {
|
|
1311
1319
|
if (!active) return "muted";
|
|
1312
1320
|
switch (mode) {
|
|
@@ -1371,6 +1379,32 @@ function compactionProfileInstructions(profile: QolSummaryProfile): string {
|
|
|
1371
1379
|
return "Be complete but not verbose. Preserve enough detail for a future assistant to continue without the old transcript.";
|
|
1372
1380
|
}
|
|
1373
1381
|
|
|
1382
|
+
function stripThinkingForSummary(messages: Message[]): Message[] {
|
|
1383
|
+
return messages.map((message) => {
|
|
1384
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) return message;
|
|
1385
|
+
return {
|
|
1386
|
+
...message,
|
|
1387
|
+
content: message.content.filter((part: any) => part?.type !== "thinking"),
|
|
1388
|
+
};
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function serializeMessagesForSummary(messages: AgentMessage[]): string {
|
|
1393
|
+
return serializeConversation(stripThinkingForSummary(convertToLlm(messages)));
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function customMessageContentToText(content: unknown): string {
|
|
1397
|
+
if (typeof content === "string") return content;
|
|
1398
|
+
if (!Array.isArray(content)) return "";
|
|
1399
|
+
const parts: string[] = [];
|
|
1400
|
+
for (const part of content) {
|
|
1401
|
+
if (part?.type === "text" && typeof part.text === "string") parts.push(part.text);
|
|
1402
|
+
else if (part?.type === "image") parts.push(`[image${typeof part.mimeType === "string" ? ` ${part.mimeType}` : ""}]`);
|
|
1403
|
+
else if (part?.type) parts.push(`[${String(part.type)}]`);
|
|
1404
|
+
}
|
|
1405
|
+
return parts.join("\n").trim();
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1374
1408
|
function buildSummaryPrompt(options: {
|
|
1375
1409
|
conversationText: string;
|
|
1376
1410
|
customInstructions?: string;
|
|
@@ -1497,7 +1531,7 @@ async function handleQolCompaction(event: any, ctx: ExtensionContext): Promise<a
|
|
|
1497
1531
|
const tokensBefore = typeof preparation.tokensBefore === "number" ? preparation.tokensBefore : 0;
|
|
1498
1532
|
compactionNotify(ctx, `QOL compaction: summarizing ${messages.length} message(s), ${tokensBefore.toLocaleString()} token(s).`, "info");
|
|
1499
1533
|
try {
|
|
1500
|
-
const conversationText =
|
|
1534
|
+
const conversationText = serializeMessagesForSummary(messages);
|
|
1501
1535
|
const result = await generateQolSummary(ctx, {
|
|
1502
1536
|
conversationText,
|
|
1503
1537
|
customInstructions: event.customInstructions,
|
|
@@ -1529,10 +1563,10 @@ async function handleQolCompaction(event: any, ctx: ExtensionContext): Promise<a
|
|
|
1529
1563
|
}
|
|
1530
1564
|
|
|
1531
1565
|
function summarizeEntryForBranch(entry: any): string[] {
|
|
1532
|
-
if (entry?.type === "message" && entry.message) return [
|
|
1566
|
+
if (entry?.type === "message" && entry.message) return [serializeMessagesForSummary([entry.message])];
|
|
1533
1567
|
if (entry?.type === "compaction" && typeof entry.summary === "string") return [`[Compaction summary]: ${entry.summary}`];
|
|
1534
1568
|
if (entry?.type === "branch_summary" && typeof entry.summary === "string") return [`[Branch summary]: ${entry.summary}`];
|
|
1535
|
-
if (entry?.type === "custom_message") return [`[Custom message${entry.customType ? `:${entry.customType}` : ""}]: ${
|
|
1569
|
+
if (entry?.type === "custom_message") return [`[Custom message${entry.customType ? `:${entry.customType}` : ""}]: ${customMessageContentToText(entry.content) || "[empty]"}`];
|
|
1536
1570
|
return [];
|
|
1537
1571
|
}
|
|
1538
1572
|
|
|
@@ -2131,39 +2165,6 @@ function getQuestionService(): QuestionServiceLike | undefined {
|
|
|
2131
2165
|
return undefined;
|
|
2132
2166
|
}
|
|
2133
2167
|
|
|
2134
|
-
function entryToHandoffMessage(entry: SessionEntry): AgentMessage | undefined {
|
|
2135
|
-
if (entry.type === "message") return entry.message;
|
|
2136
|
-
if (entry.type === "compaction") {
|
|
2137
|
-
return {
|
|
2138
|
-
role: "compactionSummary",
|
|
2139
|
-
summary: entry.summary,
|
|
2140
|
-
tokensBefore: entry.tokensBefore,
|
|
2141
|
-
timestamp: new Date(entry.timestamp).getTime(),
|
|
2142
|
-
};
|
|
2143
|
-
}
|
|
2144
|
-
return undefined;
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
function getHandoffMessages(branch: SessionEntry[]): AgentMessage[] {
|
|
2148
|
-
let compactionIndex = -1;
|
|
2149
|
-
for (let i = branch.length - 1; i >= 0; i -= 1) {
|
|
2150
|
-
if (branch[i]?.type === "compaction") {
|
|
2151
|
-
compactionIndex = i;
|
|
2152
|
-
break;
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
if (compactionIndex < 0) return branch.map(entryToHandoffMessage).filter((message): message is AgentMessage => message !== undefined);
|
|
2156
|
-
|
|
2157
|
-
const compaction = branch[compactionIndex];
|
|
2158
|
-
const firstKeptIndex = compaction?.type === "compaction" ? branch.findIndex((entry) => entry.id === compaction.firstKeptEntryId) : -1;
|
|
2159
|
-
const compactedBranch = [
|
|
2160
|
-
compaction,
|
|
2161
|
-
...(firstKeptIndex >= 0 ? branch.slice(firstKeptIndex, compactionIndex) : []),
|
|
2162
|
-
...branch.slice(compactionIndex + 1),
|
|
2163
|
-
];
|
|
2164
|
-
return compactedBranch.map(entryToHandoffMessage).filter((message): message is AgentMessage => message !== undefined);
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
2168
|
async function runHandoff(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
2168
2169
|
if (!ctx.hasUI) {
|
|
2169
2170
|
ctx.ui.notify("handoff requires interactive mode", "error");
|
|
@@ -2181,15 +2182,15 @@ async function runHandoff(args: string, ctx: ExtensionCommandContext): Promise<v
|
|
|
2181
2182
|
return;
|
|
2182
2183
|
}
|
|
2183
2184
|
|
|
2184
|
-
const
|
|
2185
|
+
const built = (ctx.sessionManager as any).buildSessionContext?.();
|
|
2186
|
+
const messages = Array.isArray(built?.messages) ? built.messages as AgentMessage[] : [];
|
|
2185
2187
|
|
|
2186
2188
|
if (messages.length === 0) {
|
|
2187
2189
|
ctx.ui.notify("No conversation to hand off", "error");
|
|
2188
2190
|
return;
|
|
2189
2191
|
}
|
|
2190
2192
|
|
|
2191
|
-
const
|
|
2192
|
-
const conversationText = serializeConversation(llmMessages);
|
|
2193
|
+
const conversationText = serializeMessagesForSummary(messages);
|
|
2193
2194
|
const currentSessionFile = ctx.sessionManager.getSessionFile();
|
|
2194
2195
|
|
|
2195
2196
|
const releaseModalLock = acquireVstackModalLock();
|
|
@@ -2272,7 +2273,7 @@ function statusMessage(ctx: ExtensionContext): string {
|
|
|
2272
2273
|
return [
|
|
2273
2274
|
"Pi QOL status",
|
|
2274
2275
|
`Statusline: ${settingBoolean("replaceFooter", true, ctx.cwd) ? "replaces footer" : "footer preserved"}; prompt=${settingBoolean("compactPrompt", true, ctx.cwd) ? "π compact" : "default chrome"}`,
|
|
2275
|
-
`
|
|
2276
|
+
`shift+enter newline: ${settingBoolean("newlineOnShiftEnter", true, ctx.cwd) ? "enabled" : "disabled"}`,
|
|
2276
2277
|
`Fallback newline key: ${newlineFallbackKey(ctx.cwd)}`,
|
|
2277
2278
|
`Pending queue preview: ${settingBoolean("pendingQueue.asciiGreen", true, ctx.cwd) ? "ANSI green" : "Pi default"}`,
|
|
2278
2279
|
`Image chips: ${settingBoolean("showImageChips", true, ctx.cwd) ? "filled (placeholders and existing image paths)" : "off"}`,
|
|
@@ -2288,23 +2289,12 @@ function statusMessage(ctx: ExtensionContext): string {
|
|
|
2288
2289
|
`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"}`,
|
|
2289
2290
|
`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"}`,
|
|
2290
2291
|
`Thinking timer: ${settingBoolean("thinkingTimer.enabled", true, ctx.cwd) ? "enabled" : "disabled"}`,
|
|
2291
|
-
"If
|
|
2292
|
+
"If shift+enter still submits, configure your terminal/tmux to send a distinct shift+enter sequence or use the fallback key.",
|
|
2292
2293
|
].join("\n");
|
|
2293
2294
|
}
|
|
2294
2295
|
|
|
2295
2296
|
|
|
2296
|
-
type
|
|
2297
|
-
|
|
2298
|
-
type QolSessionAction = "resume" | "copy" | "summarize" | "newSession" | "back";
|
|
2299
|
-
|
|
2300
|
-
const QOL_SESSION_ACTIONS: QolSessionAction[] = ["resume", "copy", "summarize", "newSession", "back"];
|
|
2301
|
-
const QOL_SESSION_ACTION_LABELS: Record<QolSessionAction, string> = {
|
|
2302
|
-
resume: "Resume Session From Here",
|
|
2303
|
-
copy: "Copy Prompt",
|
|
2304
|
-
summarize: "Inject Context",
|
|
2305
|
-
newSession: "New Session + Context",
|
|
2306
|
-
back: "Back",
|
|
2307
|
-
};
|
|
2297
|
+
type QolSessionSearchScope = "current" | "all";
|
|
2308
2298
|
|
|
2309
2299
|
interface QolSessionSearchSession {
|
|
2310
2300
|
allMessagesText: string;
|
|
@@ -2324,6 +2314,13 @@ interface QolSessionSearchResult extends QolSessionSearchSession {
|
|
|
2324
2314
|
snippets: string[];
|
|
2325
2315
|
}
|
|
2326
2316
|
|
|
2317
|
+
interface QolSessionSearchHit {
|
|
2318
|
+
message: QolSessionUserMessage;
|
|
2319
|
+
rank: number;
|
|
2320
|
+
result: QolSessionSearchResult;
|
|
2321
|
+
snippet: string;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2327
2324
|
interface QolParsedSessionQuery {
|
|
2328
2325
|
mode: "tokens" | "regex";
|
|
2329
2326
|
tokens: Array<{ kind: "fuzzy" | "phrase"; value: string }>;
|
|
@@ -2334,8 +2331,9 @@ interface QolParsedSessionQuery {
|
|
|
2334
2331
|
interface QolSessionSearchState {
|
|
2335
2332
|
cursor: number;
|
|
2336
2333
|
query: string;
|
|
2337
|
-
results:
|
|
2334
|
+
results: QolSessionSearchHit[];
|
|
2338
2335
|
selected: number;
|
|
2336
|
+
scope: QolSessionSearchScope;
|
|
2339
2337
|
total: number;
|
|
2340
2338
|
}
|
|
2341
2339
|
|
|
@@ -2354,35 +2352,56 @@ interface QolSessionMessagesState {
|
|
|
2354
2352
|
}
|
|
2355
2353
|
|
|
2356
2354
|
interface QolSessionActionState {
|
|
2357
|
-
actionIndex: number;
|
|
2358
2355
|
message: QolSessionUserMessage;
|
|
2359
2356
|
result: QolSessionSearchResult;
|
|
2360
2357
|
}
|
|
2361
2358
|
|
|
2362
|
-
interface
|
|
2363
|
-
cursor: number;
|
|
2359
|
+
interface QolSessionContextConfirmState {
|
|
2364
2360
|
message: QolSessionUserMessage;
|
|
2365
|
-
prompt: string;
|
|
2366
2361
|
result: QolSessionSearchResult;
|
|
2362
|
+
returnScreen: "search" | "messages" | "actions";
|
|
2367
2363
|
type: "summarize" | "newSession";
|
|
2368
2364
|
}
|
|
2369
2365
|
|
|
2366
|
+
interface QolSessionForkConfirmState {
|
|
2367
|
+
messages: QolSessionUserMessage[];
|
|
2368
|
+
result: QolSessionSearchResult;
|
|
2369
|
+
returnScreen: "search" | "messages" | "actions";
|
|
2370
|
+
selected: number;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2370
2373
|
interface QolSessionSearchPendingMessage {
|
|
2371
2374
|
content: string;
|
|
2372
2375
|
details: Record<string, unknown>;
|
|
2373
2376
|
}
|
|
2374
2377
|
|
|
2375
2378
|
interface QolSessionPaletteAction {
|
|
2376
|
-
type: "cancel" | "resume" | "copy" | "summarize" | "newSession";
|
|
2379
|
+
type: "cancel" | "resume" | "fork" | "copy" | "summarize" | "newSession";
|
|
2377
2380
|
customPrompt?: string;
|
|
2381
|
+
keepCurrentModel?: boolean;
|
|
2378
2382
|
message?: QolSessionUserMessage;
|
|
2379
2383
|
result?: QolSessionSearchResult;
|
|
2380
2384
|
}
|
|
2381
2385
|
|
|
2386
|
+
function pinSessionModel(sessionPath: string, model: NonNullable<ExtensionContext["model"]>, thinkingLevel?: string): void {
|
|
2387
|
+
const manager = SessionManager.open(sessionPath);
|
|
2388
|
+
const context = manager.buildSessionContext();
|
|
2389
|
+
if (context.model?.provider !== model.provider || context.model?.modelId !== model.id) {
|
|
2390
|
+
manager.appendModelChange(model.provider, model.id);
|
|
2391
|
+
}
|
|
2392
|
+
if (thinkingLevel) {
|
|
2393
|
+
const branch = manager.getBranch();
|
|
2394
|
+
const lastThinking = [...branch].reverse().find((entry: any) => entry?.type === "thinking_level_change") as { thinkingLevel?: string } | undefined;
|
|
2395
|
+
if (lastThinking?.thinkingLevel !== thinkingLevel) manager.appendThinkingLevelChange(thinkingLevel as any);
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2382
2399
|
let qolSessionSearchCache: QolSessionSearchSession[] = [];
|
|
2383
2400
|
let qolSessionSearchLoadedAt = 0;
|
|
2384
2401
|
let qolSessionSearchLoading: Promise<QolSessionSearchSession[]> | undefined;
|
|
2385
2402
|
const qolSessionUserMessagesCache = new Map<string, QolSessionUserMessage[]>();
|
|
2403
|
+
const qolSessionSearchPendingActions = new Map<string, QolSessionPaletteAction>();
|
|
2404
|
+
let qolSessionSearchPendingActionCounter = 0;
|
|
2386
2405
|
|
|
2387
2406
|
function getPendingSessionSearchMessage(): QolSessionSearchPendingMessage | undefined {
|
|
2388
2407
|
return (globalThis as unknown as Record<PropertyKey, unknown>)[SESSION_SEARCH_PENDING_SYMBOL] as QolSessionSearchPendingMessage | undefined;
|
|
@@ -2422,11 +2441,12 @@ function resolveSettingsRelativePath(value: string, settingsPath: string): strin
|
|
|
2422
2441
|
}
|
|
2423
2442
|
|
|
2424
2443
|
function sessionSearchShortcut(cwd?: string): string | undefined {
|
|
2425
|
-
// Legacy escape hatch from the original
|
|
2444
|
+
// Legacy escape hatch from the original ctrl+f setting. If users disabled it,
|
|
2426
2445
|
// keep shortcuts disabled even though the default shortcut is now conflict-free.
|
|
2427
2446
|
if (!settingBoolean("sessionSearch.ctrlFShortcut", true, cwd)) return undefined;
|
|
2428
2447
|
const shortcut = settingStringAllowEmpty("sessionSearch.shortcutKey", DEFAULT_SESSION_SEARCH_SHORTCUT, cwd).trim().toLowerCase();
|
|
2429
2448
|
if (!shortcut || shortcut === "none" || shortcut === "off" || shortcut === "false") return undefined;
|
|
2449
|
+
if (shortcut === "f3") return DEFAULT_SESSION_SEARCH_SHORTCUT;
|
|
2430
2450
|
return shortcut;
|
|
2431
2451
|
}
|
|
2432
2452
|
|
|
@@ -2448,6 +2468,23 @@ function configuredSessionDir(cwd: string): string | undefined {
|
|
|
2448
2468
|
return configured;
|
|
2449
2469
|
}
|
|
2450
2470
|
|
|
2471
|
+
function canonicalPathForSessionSearch(path: string | undefined): string | undefined {
|
|
2472
|
+
if (!path) return undefined;
|
|
2473
|
+
try {
|
|
2474
|
+
return realpathSync.native(path);
|
|
2475
|
+
} catch {
|
|
2476
|
+
return resolve(path);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
function sameSessionSearchProject(sessionCwd: string | undefined, cwd: string): boolean {
|
|
2481
|
+
return canonicalPathForSessionSearch(sessionCwd) === canonicalPathForSessionSearch(cwd);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
function defaultSessionSearchScope(cwd?: string): QolSessionSearchScope {
|
|
2485
|
+
return settingString("sessionSearch.defaultScope", "current", cwd).toLowerCase() === "all" ? "all" : "current";
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2451
2488
|
async function loadQolSessionSearchSessions(ctx: ExtensionContext, onProgress?: (loaded: number, total: number) => void): Promise<QolSessionSearchSession[]> {
|
|
2452
2489
|
const customSessionDir = configuredSessionDir(ctx.cwd);
|
|
2453
2490
|
const infos = customSessionDir
|
|
@@ -2932,6 +2969,16 @@ function messageContentText(content: unknown): string {
|
|
|
2932
2969
|
}).filter(Boolean).join(" ");
|
|
2933
2970
|
}
|
|
2934
2971
|
|
|
2972
|
+
function sessionMessageTimestamp(entry: any, message: any): number | undefined {
|
|
2973
|
+
if (typeof message?.timestamp === "number" && Number.isFinite(message.timestamp)) return message.timestamp;
|
|
2974
|
+
if (typeof entry?.timestamp === "number" && Number.isFinite(entry.timestamp)) return entry.timestamp;
|
|
2975
|
+
if (typeof entry?.timestamp === "string") {
|
|
2976
|
+
const parsed = new Date(entry.timestamp).getTime();
|
|
2977
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
2978
|
+
}
|
|
2979
|
+
return undefined;
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2935
2982
|
function sessionUserMessages(sessionPath: string): QolSessionUserMessage[] {
|
|
2936
2983
|
const cached = qolSessionUserMessagesCache.get(sessionPath);
|
|
2937
2984
|
if (cached) return cached;
|
|
@@ -2951,7 +2998,7 @@ function sessionUserMessages(sessionPath: string): QolSessionUserMessage[] {
|
|
|
2951
2998
|
index: messages.length + 1,
|
|
2952
2999
|
parentId: typeof entry.parentId === "string" || entry.parentId === null ? entry.parentId : undefined,
|
|
2953
3000
|
text,
|
|
2954
|
-
timestamp:
|
|
3001
|
+
timestamp: sessionMessageTimestamp(entry, message),
|
|
2955
3002
|
});
|
|
2956
3003
|
}
|
|
2957
3004
|
} catch {
|
|
@@ -3042,21 +3089,28 @@ function parseSessionSearchQuery(query: string): QolParsedSessionQuery {
|
|
|
3042
3089
|
return { mode: "tokens", tokens };
|
|
3043
3090
|
}
|
|
3044
3091
|
|
|
3045
|
-
function
|
|
3046
|
-
|
|
3047
|
-
|
|
3092
|
+
function searchWordLengthPenalty(word: string, query: string): number {
|
|
3093
|
+
return Math.max(0, word.length - query.length);
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
function searchStringScore(needle: string, haystack: string): number | undefined {
|
|
3097
|
+
const query = normalizeSearchText(needle);
|
|
3098
|
+
const text = normalizeSearchText(haystack);
|
|
3048
3099
|
if (!query) return 0;
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
const
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3100
|
+
let best: number | undefined;
|
|
3101
|
+
const record = (score: number) => {
|
|
3102
|
+
best = best === undefined ? score : Math.min(best, score);
|
|
3103
|
+
};
|
|
3104
|
+
if (/^[a-z0-9_]+$/i.test(query)) {
|
|
3105
|
+
for (const match of text.matchAll(/[a-z0-9_]+/gi)) {
|
|
3106
|
+
const word = match[0].toLowerCase();
|
|
3107
|
+
if (word === query) record(0);
|
|
3108
|
+
else if (word.startsWith(query)) record(10 + searchWordLengthPenalty(word, query));
|
|
3109
|
+
else if (word.includes(query)) record(100 + searchWordLengthPenalty(word, query));
|
|
3110
|
+
}
|
|
3111
|
+
return best;
|
|
3058
3112
|
}
|
|
3059
|
-
return
|
|
3113
|
+
return text.includes(query) ? 0 : undefined;
|
|
3060
3114
|
}
|
|
3061
3115
|
|
|
3062
3116
|
function matchSessionSearch(session: QolSessionSearchSession, parsed: QolParsedSessionQuery): { matches: boolean; score: number } {
|
|
@@ -3064,21 +3118,12 @@ function matchSessionSearch(session: QolSessionSearchSession, parsed: QolParsedS
|
|
|
3064
3118
|
if (parsed.mode === "regex") {
|
|
3065
3119
|
if (!parsed.regex) return { matches: false, score: 0 };
|
|
3066
3120
|
const index = text.search(parsed.regex);
|
|
3067
|
-
return index < 0 ? { matches: false, score: 0 } : { matches: true, score:
|
|
3121
|
+
return index < 0 ? { matches: false, score: 0 } : { matches: true, score: 0 };
|
|
3068
3122
|
}
|
|
3069
3123
|
if (parsed.tokens.length === 0) return { matches: true, score: 0 };
|
|
3070
3124
|
let score = 0;
|
|
3071
|
-
let normalized: string | undefined;
|
|
3072
3125
|
for (const token of parsed.tokens) {
|
|
3073
|
-
|
|
3074
|
-
normalized ??= normalizeSearchText(text);
|
|
3075
|
-
const phrase = normalizeSearchText(token.value);
|
|
3076
|
-
const index = normalized.indexOf(phrase);
|
|
3077
|
-
if (index < 0) return { matches: false, score: 0 };
|
|
3078
|
-
score += index * 0.1;
|
|
3079
|
-
continue;
|
|
3080
|
-
}
|
|
3081
|
-
const tokenScore = simpleFuzzyScore(token.value, text);
|
|
3126
|
+
const tokenScore = searchStringScore(token.value, text);
|
|
3082
3127
|
if (tokenScore === undefined) return { matches: false, score: 0 };
|
|
3083
3128
|
score += tokenScore;
|
|
3084
3129
|
}
|
|
@@ -3093,9 +3138,10 @@ function sessionSnippetSource(session: QolSessionSearchSession): string {
|
|
|
3093
3138
|
return (session.allMessagesText || session.firstMessage || "").replace(/\s+/g, " ").trim();
|
|
3094
3139
|
}
|
|
3095
3140
|
|
|
3096
|
-
function snippetAround(text: string, start: number, length: number, width: number): string {
|
|
3097
|
-
const
|
|
3098
|
-
const
|
|
3141
|
+
function snippetAround(text: string, start: number, length: number, width: number, lead = Math.floor(width / 3)): string {
|
|
3142
|
+
const safeLead = Math.max(0, Math.min(width, lead));
|
|
3143
|
+
const safeStart = Math.max(0, start - safeLead);
|
|
3144
|
+
const safeEnd = Math.min(text.length, Math.max(start + length, safeStart + width));
|
|
3099
3145
|
const prefix = safeStart > 0 ? "…" : "";
|
|
3100
3146
|
const suffix = safeEnd < text.length ? "…" : "";
|
|
3101
3147
|
return `${prefix}${text.slice(safeStart, safeEnd)}${suffix}`;
|
|
@@ -3147,11 +3193,107 @@ function searchQolSessions(sessions: QolSessionSearchSession[], query: string, c
|
|
|
3147
3193
|
if (!match.matches) continue;
|
|
3148
3194
|
results.push({ ...session, rank: match.score, snippets: buildSessionSnippets(session, parsed, snippetLimit) });
|
|
3149
3195
|
}
|
|
3150
|
-
|
|
3151
|
-
results.sort((a, b) => sortMode === "recent" ? b.modified.getTime() - a.modified.getTime() : a.rank - b.rank || b.modified.getTime() - a.modified.getTime());
|
|
3196
|
+
results.sort((a, b) => a.rank - b.rank || b.modified.getTime() - a.modified.getTime());
|
|
3152
3197
|
return results.slice(0, limit);
|
|
3153
3198
|
}
|
|
3154
3199
|
|
|
3200
|
+
function resultFromSession(session: QolSessionSearchSession, rank = 0, snippets: string[] = []): QolSessionSearchResult {
|
|
3201
|
+
return { ...session, rank, snippets };
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
function matchTextSearch(text: string, parsed: QolParsedSessionQuery): { matches: boolean; score: number } {
|
|
3205
|
+
if (parsed.mode === "regex") {
|
|
3206
|
+
if (!parsed.regex) return { matches: false, score: 0 };
|
|
3207
|
+
const index = text.search(parsed.regex);
|
|
3208
|
+
return index < 0 ? { matches: false, score: 0 } : { matches: true, score: 0 };
|
|
3209
|
+
}
|
|
3210
|
+
if (parsed.tokens.length === 0) return { matches: true, score: 0 };
|
|
3211
|
+
let score = 0;
|
|
3212
|
+
for (const token of parsed.tokens) {
|
|
3213
|
+
const tokenScore = searchStringScore(token.value, text);
|
|
3214
|
+
if (tokenScore === undefined) return { matches: false, score: 0 };
|
|
3215
|
+
score += tokenScore;
|
|
3216
|
+
}
|
|
3217
|
+
return { matches: true, score };
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
function sessionTitleSearchText(session: QolSessionSearchSession): string {
|
|
3221
|
+
return [session.name ?? "", sessionResumeTitle(session), sessionDisplayName(session)].join("\n");
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
function matchPromptSearch(_session: QolSessionSearchSession, message: QolSessionUserMessage, parsed: QolParsedSessionQuery): { matches: boolean; score: number } {
|
|
3225
|
+
return matchTextSearch(message.text, parsed);
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
function buildPromptSnippet(message: QolSessionUserMessage, parsed: QolParsedSessionQuery): string {
|
|
3229
|
+
const source = message.text.replace(/\s+/g, " ").trim();
|
|
3230
|
+
if (!source) return "";
|
|
3231
|
+
if (parsed.mode === "regex" && parsed.regex) {
|
|
3232
|
+
parsed.regex.lastIndex = 0;
|
|
3233
|
+
const match = parsed.regex.exec(source);
|
|
3234
|
+
return match ? snippetAround(source, match.index, match[0].length, 160, 24) : source.slice(0, 160);
|
|
3235
|
+
}
|
|
3236
|
+
if (parsed.tokens.length === 0) return source.slice(0, 160);
|
|
3237
|
+
const lower = source.toLowerCase();
|
|
3238
|
+
for (const token of parsed.tokens) {
|
|
3239
|
+
const value = normalizeSearchText(token.value);
|
|
3240
|
+
if (!value) continue;
|
|
3241
|
+
const index = lower.indexOf(value.toLowerCase());
|
|
3242
|
+
if (index >= 0) return snippetAround(source, index, value.length, 160, 24);
|
|
3243
|
+
}
|
|
3244
|
+
return source.slice(0, 160);
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
function promptRecencyTime(hit: QolSessionSearchHit): number {
|
|
3248
|
+
if (typeof hit.message.timestamp === "number" && Number.isFinite(hit.message.timestamp)) return hit.message.timestamp;
|
|
3249
|
+
return hit.result.modified.getTime();
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
function compareSessionSearchHitsRecent(a: QolSessionSearchHit, b: QolSessionSearchHit): number {
|
|
3253
|
+
return promptRecencyTime(b) - promptRecencyTime(a)
|
|
3254
|
+
|| b.result.modified.getTime() - a.result.modified.getTime()
|
|
3255
|
+
|| b.message.index - a.message.index;
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
function searchQolSessionHits(sessions: QolSessionSearchSession[], query: string, cwd: string): QolSessionSearchHit[] {
|
|
3259
|
+
const limit = Math.max(1, Math.floor(settingNumber("sessionSearch.resultLimit", DEFAULT_SESSION_SEARCH_LIMIT, cwd)));
|
|
3260
|
+
const parsed = parseSessionSearchQuery(query);
|
|
3261
|
+
if (parsed.error) return [];
|
|
3262
|
+
const hits: QolSessionSearchHit[] = [];
|
|
3263
|
+
if (!query.trim()) {
|
|
3264
|
+
for (const session of sessions) {
|
|
3265
|
+
const result = resultFromSession(session);
|
|
3266
|
+
for (const message of userMessagesForResult(result)) {
|
|
3267
|
+
const snippet = buildPromptSnippet(message, parsed);
|
|
3268
|
+
hits.push({ message, rank: 0, result: resultFromSession(session, 0, [snippet]), snippet });
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
hits.sort(compareSessionSearchHitsRecent);
|
|
3272
|
+
return hits.slice(0, limit);
|
|
3273
|
+
}
|
|
3274
|
+
for (const session of sessions) {
|
|
3275
|
+
let addedPromptHit = false;
|
|
3276
|
+
for (const message of userMessagesForResult(resultFromSession(session))) {
|
|
3277
|
+
const match = matchPromptSearch(session, message, parsed);
|
|
3278
|
+
if (!match.matches) continue;
|
|
3279
|
+
const snippet = buildPromptSnippet(message, parsed);
|
|
3280
|
+
hits.push({ message, rank: match.score, result: resultFromSession(session, match.score, [snippet]), snippet });
|
|
3281
|
+
addedPromptHit = true;
|
|
3282
|
+
}
|
|
3283
|
+
if (!addedPromptHit) {
|
|
3284
|
+
const titleMatch = matchTextSearch(sessionTitleSearchText(session), parsed);
|
|
3285
|
+
if (!titleMatch.matches) continue;
|
|
3286
|
+
const messages = userMessagesForResult(resultFromSession(session));
|
|
3287
|
+
const message = messages[messages.length - 1];
|
|
3288
|
+
if (!message) continue;
|
|
3289
|
+
const snippet = buildPromptSnippet(message, { mode: "tokens", tokens: [] });
|
|
3290
|
+
hits.push({ message, rank: titleMatch.score, result: resultFromSession(session, titleMatch.score, [snippet]), snippet });
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
hits.sort((a, b) => a.rank - b.rank || compareSessionSearchHitsRecent(a, b));
|
|
3294
|
+
return hits.slice(0, limit);
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3155
3297
|
function styleSessionSnippet(snippet: string, query: string, theme: Theme): string {
|
|
3156
3298
|
let styled = snippet;
|
|
3157
3299
|
const parsed = parseSessionSearchQuery(query);
|
|
@@ -3211,11 +3353,14 @@ function isPrintableInput(data: string): boolean {
|
|
|
3211
3353
|
}
|
|
3212
3354
|
|
|
3213
3355
|
class QolSessionSearchComponent {
|
|
3214
|
-
private screen: "search" | "messages" | "actions" | "
|
|
3356
|
+
private screen: "search" | "messages" | "actions" | "confirmContext" | "confirmFork" = "search";
|
|
3215
3357
|
private searchState: QolSessionSearchState;
|
|
3216
3358
|
private messagesState: QolSessionMessagesState | undefined;
|
|
3217
3359
|
private actionState: QolSessionActionState | undefined;
|
|
3218
|
-
private
|
|
3360
|
+
private actionReturnScreen: "search" | "messages" = "messages";
|
|
3361
|
+
private contextConfirmState: QolSessionContextConfirmState | undefined;
|
|
3362
|
+
private forkConfirmState: QolSessionForkConfirmState | undefined;
|
|
3363
|
+
|
|
3219
3364
|
|
|
3220
3365
|
constructor(
|
|
3221
3366
|
private readonly done: (action: QolSessionPaletteAction) => void,
|
|
@@ -3226,12 +3371,15 @@ class QolSessionSearchComponent {
|
|
|
3226
3371
|
initialQuery = "",
|
|
3227
3372
|
) {
|
|
3228
3373
|
const query = initialQuery.trim();
|
|
3374
|
+
const scope = defaultSessionSearchScope(cwd);
|
|
3375
|
+
const scopedSessions = this.sessionsForScope(scope);
|
|
3229
3376
|
this.searchState = {
|
|
3230
3377
|
cursor: query.length,
|
|
3231
3378
|
query,
|
|
3232
|
-
results:
|
|
3379
|
+
results: searchQolSessionHits(scopedSessions, query, cwd),
|
|
3233
3380
|
selected: 0,
|
|
3234
|
-
|
|
3381
|
+
scope,
|
|
3382
|
+
total: scopedSessions.length,
|
|
3235
3383
|
};
|
|
3236
3384
|
}
|
|
3237
3385
|
|
|
@@ -3240,16 +3388,19 @@ class QolSessionSearchComponent {
|
|
|
3240
3388
|
private maxOverlayRows(): number {
|
|
3241
3389
|
const terminalRows = Number(this.tui.terminal?.rows ?? process.stdout.rows ?? 30);
|
|
3242
3390
|
const safeRows = Number.isFinite(terminalRows) && terminalRows > 0 ? terminalRows : 30;
|
|
3243
|
-
return Math.max(
|
|
3391
|
+
return Math.max(9, Math.floor(safeRows * SESSION_SEARCH_OVERLAY_HEIGHT_RATIO));
|
|
3392
|
+
}
|
|
3393
|
+
|
|
3394
|
+
private compactSearchLayout(): boolean {
|
|
3395
|
+
return this.maxOverlayRows() < 18;
|
|
3244
3396
|
}
|
|
3245
3397
|
|
|
3246
3398
|
private searchMaxVisibleRows(): number {
|
|
3247
3399
|
const configured = Math.max(1, Math.floor(settingNumber("sessionSearch.maxVisible", 8, this.cwd)));
|
|
3248
|
-
//
|
|
3249
|
-
//
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
const responsive = Math.max(1, Math.floor((this.maxOverlayRows() - 12) / 4));
|
|
3400
|
+
// Each hit is two rows plus a separator between hits. Chrome is aggressively
|
|
3401
|
+
// reduced on short terminals so the popup can shrink to one visible hit.
|
|
3402
|
+
const chromeRows = this.compactSearchLayout() ? 7 : 11;
|
|
3403
|
+
const responsive = Math.max(1, Math.floor((this.maxOverlayRows() - chromeRows) / 3));
|
|
3253
3404
|
return Math.max(1, Math.min(configured, responsive));
|
|
3254
3405
|
}
|
|
3255
3406
|
|
|
@@ -3261,20 +3412,31 @@ class QolSessionSearchComponent {
|
|
|
3261
3412
|
return Math.max(1, Math.min(configured, responsive));
|
|
3262
3413
|
}
|
|
3263
3414
|
|
|
3415
|
+
private forkConfirmMaxVisibleRows(): number {
|
|
3416
|
+
const configured = Math.max(1, Math.floor(settingNumber("sessionSearch.messageMaxVisible", 12, this.cwd)));
|
|
3417
|
+
const responsive = Math.max(1, this.maxOverlayRows() - 13);
|
|
3418
|
+
return Math.max(1, Math.min(configured, responsive));
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3264
3421
|
render(width: number): string[] {
|
|
3265
3422
|
const configured = Math.max(70, Math.floor(settingNumber("sessionSearch.overlayWidth", 104, this.cwd)));
|
|
3266
3423
|
const renderWidth = Math.min(Math.max(48, width), configured);
|
|
3267
3424
|
if (this.screen === "messages" && this.messagesState) return this.renderMessages(renderWidth, this.messagesState);
|
|
3268
3425
|
if (this.screen === "actions" && this.actionState) return this.renderActions(renderWidth, this.actionState);
|
|
3269
|
-
if (this.screen === "
|
|
3426
|
+
if (this.screen === "confirmContext" && this.contextConfirmState) return this.renderContextConfirm(renderWidth, this.contextConfirmState);
|
|
3427
|
+
if (this.screen === "confirmFork" && this.forkConfirmState) return this.renderForkConfirm(renderWidth, this.forkConfirmState);
|
|
3270
3428
|
return this.renderSearch(renderWidth);
|
|
3271
3429
|
}
|
|
3272
3430
|
|
|
3273
3431
|
handleInput(data: string): void {
|
|
3274
|
-
if (this.screen === "messages") this.handleMessagesInput(data);
|
|
3275
|
-
else if (this.screen === "actions") this.handleActionInput(data);
|
|
3276
|
-
else if (this.screen === "
|
|
3277
|
-
else this.
|
|
3432
|
+
if (this.screen === "messages" && this.messagesState) this.handleMessagesInput(data);
|
|
3433
|
+
else if (this.screen === "actions" && this.actionState) this.handleActionInput(data);
|
|
3434
|
+
else if (this.screen === "confirmContext" && this.contextConfirmState) this.handleContextConfirmInput(data);
|
|
3435
|
+
else if (this.screen === "confirmFork" && this.forkConfirmState) this.handleForkConfirmInput(data);
|
|
3436
|
+
else {
|
|
3437
|
+
this.screen = "search";
|
|
3438
|
+
this.handleSearchInput(data);
|
|
3439
|
+
}
|
|
3278
3440
|
this.tui.requestRender();
|
|
3279
3441
|
}
|
|
3280
3442
|
|
|
@@ -3282,12 +3444,29 @@ class QolSessionSearchComponent {
|
|
|
3282
3444
|
this.searchState.selected = Math.max(0, Math.min(this.searchState.selected, Math.max(0, this.searchState.results.length - 1)));
|
|
3283
3445
|
}
|
|
3284
3446
|
|
|
3447
|
+
private sessionsForScope(scope = this.searchState.scope): QolSessionSearchSession[] {
|
|
3448
|
+
if (scope === "all") return this.sessions;
|
|
3449
|
+
return this.sessions.filter((session) => sameSessionSearchProject(session.cwd, this.cwd));
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
private updateResults(resetSelection = true): void {
|
|
3453
|
+
const state = this.searchState;
|
|
3454
|
+
const scopedSessions = this.sessionsForScope(state.scope);
|
|
3455
|
+
state.results = searchQolSessionHits(scopedSessions, state.query, this.cwd);
|
|
3456
|
+
state.total = scopedSessions.length;
|
|
3457
|
+
if (resetSelection) state.selected = 0;
|
|
3458
|
+
this.clampSelection();
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
private toggleScope(): void {
|
|
3462
|
+
this.searchState.scope = this.searchState.scope === "current" ? "all" : "current";
|
|
3463
|
+
this.updateResults(true);
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3285
3466
|
private updateQuery(query: string, cursor: number): void {
|
|
3286
3467
|
this.searchState.query = query;
|
|
3287
3468
|
this.searchState.cursor = Math.max(0, Math.min(cursor, query.length));
|
|
3288
|
-
this.
|
|
3289
|
-
this.searchState.selected = 0;
|
|
3290
|
-
this.clampSelection();
|
|
3469
|
+
this.updateResults(true);
|
|
3291
3470
|
}
|
|
3292
3471
|
|
|
3293
3472
|
private selectedMessageIndex(messages: QolSessionUserMessage[], query: string): number {
|
|
@@ -3302,12 +3481,15 @@ class QolSessionSearchComponent {
|
|
|
3302
3481
|
return Math.max(0, messages.length - 1);
|
|
3303
3482
|
}
|
|
3304
3483
|
|
|
3305
|
-
private openMessages(result: QolSessionSearchResult): void {
|
|
3484
|
+
private openMessages(result: QolSessionSearchResult, preferredMessage?: QolSessionUserMessage): void {
|
|
3306
3485
|
const messages = userMessagesForResult(result);
|
|
3486
|
+
const preferredIndex = preferredMessage
|
|
3487
|
+
? messages.findIndex((message) => message.index === preferredMessage.index || (message.entryId && message.entryId === preferredMessage.entryId))
|
|
3488
|
+
: -1;
|
|
3307
3489
|
this.messagesState = {
|
|
3308
3490
|
messages,
|
|
3309
3491
|
result,
|
|
3310
|
-
selected: this.selectedMessageIndex(messages, this.searchState.query),
|
|
3492
|
+
selected: preferredIndex >= 0 ? preferredIndex : this.selectedMessageIndex(messages, this.searchState.query),
|
|
3311
3493
|
};
|
|
3312
3494
|
this.screen = "messages";
|
|
3313
3495
|
}
|
|
@@ -3316,16 +3498,106 @@ class QolSessionSearchComponent {
|
|
|
3316
3498
|
return `Focus on prompt #${message.index}: ${message.text}`;
|
|
3317
3499
|
}
|
|
3318
3500
|
|
|
3501
|
+
private openContextConfirm(type: "summarize" | "newSession", result: QolSessionSearchResult, message: QolSessionUserMessage, returnScreen: "search" | "messages" | "actions"): void {
|
|
3502
|
+
this.contextConfirmState = { message, result, returnScreen, type };
|
|
3503
|
+
this.screen = "confirmContext";
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
private returnFromContextConfirm(state: QolSessionContextConfirmState): void {
|
|
3507
|
+
this.contextConfirmState = undefined;
|
|
3508
|
+
if (state.returnScreen === "actions" && this.actionState) this.screen = "actions";
|
|
3509
|
+
else if (state.returnScreen === "messages" && this.messagesState) this.screen = "messages";
|
|
3510
|
+
else this.screen = "search";
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3513
|
+
private confirmContextAction(state: QolSessionContextConfirmState): void {
|
|
3514
|
+
this.done({ customPrompt: this.selectedFocusText(state.message), message: state.message, result: state.result, type: state.type });
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
private openForkConfirm(result: QolSessionSearchResult, message: QolSessionUserMessage, returnScreen: "search" | "messages" | "actions"): void {
|
|
3518
|
+
const messages = userMessagesForResult(result);
|
|
3519
|
+
const preferredIndex = messages.findIndex((candidate) => candidate.index === message.index || (message.entryId && candidate.entryId === message.entryId));
|
|
3520
|
+
this.forkConfirmState = {
|
|
3521
|
+
messages,
|
|
3522
|
+
result,
|
|
3523
|
+
returnScreen,
|
|
3524
|
+
selected: preferredIndex >= 0 ? preferredIndex : 0,
|
|
3525
|
+
};
|
|
3526
|
+
this.screen = "confirmFork";
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
private returnFromForkConfirm(state: QolSessionForkConfirmState): void {
|
|
3530
|
+
this.forkConfirmState = undefined;
|
|
3531
|
+
if (state.returnScreen === "actions" && this.actionState) this.screen = "actions";
|
|
3532
|
+
else if (state.returnScreen === "messages" && this.messagesState) this.screen = "messages";
|
|
3533
|
+
else this.screen = "search";
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
private confirmForkAction(state: QolSessionForkConfirmState): void {
|
|
3537
|
+
const message = state.messages[state.selected];
|
|
3538
|
+
if (!message) return;
|
|
3539
|
+
this.done({ message, result: state.result, type: "fork" });
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
private returnFromActions(): void {
|
|
3543
|
+
this.actionState = undefined;
|
|
3544
|
+
if (this.actionReturnScreen === "messages" && this.messagesState) this.screen = "messages";
|
|
3545
|
+
else this.screen = "search";
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3319
3548
|
private handleSearchInput(data: string): void {
|
|
3320
3549
|
const state = this.searchState;
|
|
3321
3550
|
if (matchesKey(data, "escape")) {
|
|
3322
3551
|
this.done({ type: "cancel" });
|
|
3323
3552
|
return;
|
|
3324
3553
|
}
|
|
3554
|
+
if (matchesKey(data, "tab")) {
|
|
3555
|
+
this.toggleScope();
|
|
3556
|
+
return;
|
|
3557
|
+
}
|
|
3325
3558
|
if (matchesKey(data, "return") || matchesKey(data, "enter")) {
|
|
3326
|
-
const
|
|
3327
|
-
if (!
|
|
3328
|
-
this.openMessages(result);
|
|
3559
|
+
const hit = state.results[state.selected];
|
|
3560
|
+
if (!hit) return;
|
|
3561
|
+
this.openMessages(hit.result, hit.message);
|
|
3562
|
+
return;
|
|
3563
|
+
}
|
|
3564
|
+
if (matchesKey(data, "alt+c")) {
|
|
3565
|
+
const hit = state.results[state.selected];
|
|
3566
|
+
if (hit) this.done({ message: hit.message, result: hit.result, type: "copy" });
|
|
3567
|
+
return;
|
|
3568
|
+
}
|
|
3569
|
+
if (matchesKey(data, "alt+f")) {
|
|
3570
|
+
const hit = state.results[state.selected];
|
|
3571
|
+
if (hit) this.openForkConfirm(hit.result, hit.message, "search");
|
|
3572
|
+
return;
|
|
3573
|
+
}
|
|
3574
|
+
if (matchesKey(data, "alt+r")) {
|
|
3575
|
+
const hit = state.results[state.selected];
|
|
3576
|
+
if (hit) this.done({ result: hit.result, type: "resume" });
|
|
3577
|
+
return;
|
|
3578
|
+
}
|
|
3579
|
+
if (matchesKey(data, "alt+m")) {
|
|
3580
|
+
const hit = state.results[state.selected];
|
|
3581
|
+
if (hit) this.done({ keepCurrentModel: true, result: hit.result, type: "resume" });
|
|
3582
|
+
return;
|
|
3583
|
+
}
|
|
3584
|
+
if (matchesKey(data, "alt+i")) {
|
|
3585
|
+
const hit = state.results[state.selected];
|
|
3586
|
+
if (hit) this.openContextConfirm("summarize", hit.result, hit.message, "search");
|
|
3587
|
+
return;
|
|
3588
|
+
}
|
|
3589
|
+
if (matchesKey(data, "alt+n")) {
|
|
3590
|
+
const hit = state.results[state.selected];
|
|
3591
|
+
if (hit) this.openContextConfirm("newSession", hit.result, hit.message, "search");
|
|
3592
|
+
return;
|
|
3593
|
+
}
|
|
3594
|
+
if (matchesKey(data, "alt+a")) {
|
|
3595
|
+
const hit = state.results[state.selected];
|
|
3596
|
+
if (hit) {
|
|
3597
|
+
this.actionState = { message: hit.message, result: hit.result };
|
|
3598
|
+
this.actionReturnScreen = "search";
|
|
3599
|
+
this.screen = "actions";
|
|
3600
|
+
}
|
|
3329
3601
|
return;
|
|
3330
3602
|
}
|
|
3331
3603
|
if (matchesKey(data, "up")) {
|
|
@@ -3333,7 +3605,7 @@ class QolSessionSearchComponent {
|
|
|
3333
3605
|
return;
|
|
3334
3606
|
}
|
|
3335
3607
|
if (matchesKey(data, "down")) {
|
|
3336
|
-
state.selected = Math.min(state.results.length - 1, state.selected + 1);
|
|
3608
|
+
state.selected = Math.min(Math.max(0, state.results.length - 1), state.selected + 1);
|
|
3337
3609
|
return;
|
|
3338
3610
|
}
|
|
3339
3611
|
if (matchesKey(data, "-") || matchesKey(data, "pageup")) {
|
|
@@ -3341,7 +3613,7 @@ class QolSessionSearchComponent {
|
|
|
3341
3613
|
return;
|
|
3342
3614
|
}
|
|
3343
3615
|
if (matchesKey(data, "=") || matchesKey(data, "pagedown")) {
|
|
3344
|
-
state.selected = Math.min(state.results.length - 1, state.selected + this.searchMaxVisibleRows());
|
|
3616
|
+
state.selected = Math.min(Math.max(0, state.results.length - 1), state.selected + this.searchMaxVisibleRows());
|
|
3345
3617
|
return;
|
|
3346
3618
|
}
|
|
3347
3619
|
if (matchesKey(data, "left")) {
|
|
@@ -3394,14 +3666,39 @@ class QolSessionSearchComponent {
|
|
|
3394
3666
|
if (matchesKey(data, "return") || matchesKey(data, "enter")) {
|
|
3395
3667
|
const message = state.messages[state.selected];
|
|
3396
3668
|
if (!message) return;
|
|
3397
|
-
this.actionState = {
|
|
3669
|
+
this.actionState = { message, result: state.result };
|
|
3670
|
+
this.actionReturnScreen = "messages";
|
|
3398
3671
|
this.screen = "actions";
|
|
3399
3672
|
return;
|
|
3400
3673
|
}
|
|
3401
|
-
if (data
|
|
3674
|
+
if (matchesKey(data, "alt+r")) {
|
|
3402
3675
|
this.done({ result: state.result, type: "resume" });
|
|
3403
3676
|
return;
|
|
3404
3677
|
}
|
|
3678
|
+
if (matchesKey(data, "alt+m")) {
|
|
3679
|
+
this.done({ keepCurrentModel: true, result: state.result, type: "resume" });
|
|
3680
|
+
return;
|
|
3681
|
+
}
|
|
3682
|
+
if (matchesKey(data, "alt+c")) {
|
|
3683
|
+
const message = state.messages[state.selected];
|
|
3684
|
+
if (message) this.done({ message, result: state.result, type: "copy" });
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3687
|
+
if (matchesKey(data, "alt+f")) {
|
|
3688
|
+
const message = state.messages[state.selected];
|
|
3689
|
+
if (message) this.openForkConfirm(state.result, message, "messages");
|
|
3690
|
+
return;
|
|
3691
|
+
}
|
|
3692
|
+
if (matchesKey(data, "alt+i")) {
|
|
3693
|
+
const message = state.messages[state.selected];
|
|
3694
|
+
if (message) this.openContextConfirm("summarize", state.result, message, "messages");
|
|
3695
|
+
return;
|
|
3696
|
+
}
|
|
3697
|
+
if (matchesKey(data, "alt+n")) {
|
|
3698
|
+
const message = state.messages[state.selected];
|
|
3699
|
+
if (message) this.openContextConfirm("newSession", state.result, message, "messages");
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3405
3702
|
if (matchesKey(data, "up")) {
|
|
3406
3703
|
state.selected = Math.max(0, state.selected - 1);
|
|
3407
3704
|
return;
|
|
@@ -3431,146 +3728,202 @@ class QolSessionSearchComponent {
|
|
|
3431
3728
|
const state = this.actionState;
|
|
3432
3729
|
if (!state) return;
|
|
3433
3730
|
if (matchesKey(data, "escape") || matchesKey(data, "backspace")) {
|
|
3434
|
-
this.
|
|
3731
|
+
this.returnFromActions();
|
|
3435
3732
|
return;
|
|
3436
3733
|
}
|
|
3437
|
-
if (matchesKey(data, "
|
|
3438
|
-
state.
|
|
3734
|
+
if (matchesKey(data, "alt+c")) {
|
|
3735
|
+
this.done({ message: state.message, result: state.result, type: "copy" });
|
|
3439
3736
|
return;
|
|
3440
3737
|
}
|
|
3441
|
-
if (matchesKey(data, "
|
|
3442
|
-
state.
|
|
3738
|
+
if (matchesKey(data, "alt+f")) {
|
|
3739
|
+
this.openForkConfirm(state.result, state.message, "actions");
|
|
3443
3740
|
return;
|
|
3444
3741
|
}
|
|
3445
|
-
if (matchesKey(data, "
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
}
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3742
|
+
if (matchesKey(data, "alt+r")) {
|
|
3743
|
+
this.done({ result: state.result, type: "resume" });
|
|
3744
|
+
return;
|
|
3745
|
+
}
|
|
3746
|
+
if (matchesKey(data, "alt+m")) {
|
|
3747
|
+
this.done({ keepCurrentModel: true, result: state.result, type: "resume" });
|
|
3748
|
+
return;
|
|
3749
|
+
}
|
|
3750
|
+
if (matchesKey(data, "alt+i")) {
|
|
3751
|
+
this.openContextConfirm("summarize", state.result, state.message, "actions");
|
|
3752
|
+
return;
|
|
3753
|
+
}
|
|
3754
|
+
if (matchesKey(data, "alt+n")) {
|
|
3755
|
+
this.openContextConfirm("newSession", state.result, state.message, "actions");
|
|
3756
|
+
return;
|
|
3457
3757
|
}
|
|
3458
3758
|
}
|
|
3459
3759
|
|
|
3460
|
-
private
|
|
3461
|
-
const state = this.
|
|
3760
|
+
private handleContextConfirmInput(data: string): void {
|
|
3761
|
+
const state = this.contextConfirmState;
|
|
3462
3762
|
if (!state) return;
|
|
3463
|
-
if (matchesKey(data, "escape")) {
|
|
3464
|
-
this.
|
|
3763
|
+
if (matchesKey(data, "escape") || matchesKey(data, "backspace")) {
|
|
3764
|
+
this.returnFromContextConfirm(state);
|
|
3465
3765
|
return;
|
|
3466
3766
|
}
|
|
3467
3767
|
if (matchesKey(data, "return") || matchesKey(data, "enter")) {
|
|
3468
|
-
this.
|
|
3469
|
-
return;
|
|
3768
|
+
this.confirmContextAction(state);
|
|
3470
3769
|
}
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
if (
|
|
3476
|
-
|
|
3770
|
+
}
|
|
3771
|
+
|
|
3772
|
+
private handleForkConfirmInput(data: string): void {
|
|
3773
|
+
const state = this.forkConfirmState;
|
|
3774
|
+
if (!state) return;
|
|
3775
|
+
if (matchesKey(data, "escape") || matchesKey(data, "backspace")) {
|
|
3776
|
+
this.returnFromForkConfirm(state);
|
|
3477
3777
|
return;
|
|
3478
3778
|
}
|
|
3479
|
-
if (matchesKey(data, "
|
|
3480
|
-
state
|
|
3779
|
+
if (matchesKey(data, "return") || matchesKey(data, "enter")) {
|
|
3780
|
+
this.confirmForkAction(state);
|
|
3481
3781
|
return;
|
|
3482
3782
|
}
|
|
3483
|
-
if (matchesKey(data, "
|
|
3484
|
-
state.
|
|
3783
|
+
if (matchesKey(data, "up")) {
|
|
3784
|
+
state.selected = Math.max(0, state.selected - 1);
|
|
3485
3785
|
return;
|
|
3486
3786
|
}
|
|
3487
|
-
if (matchesKey(data, "
|
|
3488
|
-
state.
|
|
3489
|
-
state.cursor = 0;
|
|
3787
|
+
if (matchesKey(data, "down")) {
|
|
3788
|
+
state.selected = Math.min(Math.max(0, state.messages.length - 1), state.selected + 1);
|
|
3490
3789
|
return;
|
|
3491
3790
|
}
|
|
3492
|
-
if (matchesKey(data, "
|
|
3493
|
-
|
|
3494
|
-
const after = state.prompt.slice(state.cursor);
|
|
3495
|
-
let i = before.length;
|
|
3496
|
-
while (i > 0 && /\s/.test(before[i - 1]!)) i -= 1;
|
|
3497
|
-
while (i > 0 && !/\s/.test(before[i - 1]!)) i -= 1;
|
|
3498
|
-
state.prompt = `${before.slice(0, i)}${after}`;
|
|
3499
|
-
state.cursor = i;
|
|
3791
|
+
if (matchesKey(data, "-") || matchesKey(data, "pageup")) {
|
|
3792
|
+
state.selected = Math.max(0, state.selected - this.forkConfirmMaxVisibleRows());
|
|
3500
3793
|
return;
|
|
3501
3794
|
}
|
|
3502
|
-
if (matchesKey(data, "
|
|
3503
|
-
|
|
3504
|
-
state.prompt = `${state.prompt.slice(0, state.cursor - 1)}${state.prompt.slice(state.cursor)}`;
|
|
3505
|
-
state.cursor -= 1;
|
|
3506
|
-
}
|
|
3795
|
+
if (matchesKey(data, "=") || matchesKey(data, "pagedown")) {
|
|
3796
|
+
state.selected = Math.min(Math.max(0, state.messages.length - 1), state.selected + this.forkConfirmMaxVisibleRows());
|
|
3507
3797
|
return;
|
|
3508
3798
|
}
|
|
3509
|
-
if (matchesKey(data, "
|
|
3510
|
-
|
|
3799
|
+
if (matchesKey(data, "home") || matchesKey(data, "ctrl+a")) {
|
|
3800
|
+
state.selected = 0;
|
|
3511
3801
|
return;
|
|
3512
3802
|
}
|
|
3513
|
-
if (
|
|
3514
|
-
state.
|
|
3515
|
-
state.cursor += data.length;
|
|
3803
|
+
if (matchesKey(data, "end") || matchesKey(data, "ctrl+e")) {
|
|
3804
|
+
state.selected = Math.max(0, state.messages.length - 1);
|
|
3516
3805
|
}
|
|
3517
3806
|
}
|
|
3518
3807
|
|
|
3519
3808
|
private renderSearch(width: number): string[] {
|
|
3520
|
-
const { bottom, divider, empty, filledRow, inner, row,
|
|
3809
|
+
const { bottom, divider, empty, filledRow, inner, row, top } = boxParts(width, this.theme);
|
|
3521
3810
|
const state = this.searchState;
|
|
3522
|
-
const
|
|
3811
|
+
const compact = this.compactSearchLayout();
|
|
3812
|
+
const lines: string[] = [top("Session Search", `${state.results.length} hits`), empty()];
|
|
3523
3813
|
const accent = (s: string) => this.theme.fg("accent", s);
|
|
3524
3814
|
const dim = (s: string) => this.theme.fg("dim", s);
|
|
3525
3815
|
const muted = (s: string) => this.theme.fg("muted", s);
|
|
3526
|
-
const pair = (left: string, right: string) => {
|
|
3527
|
-
const leftWidth = Math.max(1, inner - visibleWidth(right) - 1);
|
|
3528
|
-
const clippedLeft = truncateToWidth(left, leftWidth, "…");
|
|
3529
|
-
const gap = Math.max(1, inner - visibleWidth(clippedLeft) - visibleWidth(right));
|
|
3530
|
-
return `${clippedLeft}${" ".repeat(gap)}${right}`;
|
|
3531
|
-
};
|
|
3532
3816
|
const cursorChar = state.query[state.cursor] ?? " ";
|
|
3533
3817
|
const queryDisplay = `${state.query.slice(0, state.cursor)}${this.theme.inverse(cursorChar)}${state.query.slice(state.cursor + (state.cursor < state.query.length ? 1 : 0))}`;
|
|
3818
|
+
lines.push(row(this.renderScopeTabs(inner)));
|
|
3819
|
+
if (!compact) lines.push(empty());
|
|
3534
3820
|
lines.push(filledRow(` > ${queryDisplay}`));
|
|
3535
|
-
lines.push(row(dim(
|
|
3536
|
-
lines.push(
|
|
3821
|
+
if (!compact) lines.push(row(dim(`tokens · re:<pattern> regex · "phrase" exact`)));
|
|
3822
|
+
lines.push(divider());
|
|
3823
|
+
|
|
3537
3824
|
if (state.results.length === 0) {
|
|
3538
|
-
lines.push(row(muted(state.query.trim() ? "No
|
|
3825
|
+
lines.push(row(muted(state.query.trim() ? "No prompts match your search" : "No prompts found")));
|
|
3539
3826
|
} else {
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
const right = selected ? this.theme.fg("text", rightPlain) : dim(rightPlain);
|
|
3550
|
-
const titleBudget = Math.max(12, rowBudget - visibleWidth(right) - 1);
|
|
3551
|
-
const titleText = truncateToWidth(title, titleBudget, "…");
|
|
3552
|
-
const left = selected ? this.theme.bold(accent(titleText)) : this.theme.bold(titleText);
|
|
3553
|
-
const titleRow = pair(left, right);
|
|
3554
|
-
lines.push(selected ? selectedRow(titleRow) : row(titleRow));
|
|
3555
|
-
|
|
3556
|
-
const folder = shortPathForUi(result.cwd || dirname(result.path));
|
|
3557
|
-
const project = sessionDisplayName(result);
|
|
3558
|
-
lines.push(row(muted(truncateToWidth(`${project} · ${folder}`, inner, "…"))));
|
|
3559
|
-
|
|
3560
|
-
const snippet = state.query.trim() ? (result.snippets[0] || lastUserMessageSnippet(result)) : lastUserMessageSnippet(result);
|
|
3561
|
-
const label = state.query.trim() ? "match" : "last";
|
|
3562
|
-
const snippetPrefix = `${dim(`${label}:`)} `;
|
|
3563
|
-
lines.push(row(`${snippetPrefix}${truncateToWidth(styleSessionSnippet(snippet, state.query, this.theme), inner - visibleWidth(snippetPrefix), "…")}`));
|
|
3564
|
-
if (i < end - 1) lines.push(row(this.theme.fg("borderMuted", "─".repeat(Math.max(8, inner)))));
|
|
3565
|
-
}
|
|
3566
|
-
if (state.results.length > maxVisible) lines.push(empty(), row(dim(`${state.selected + 1}/${state.results.length} ${state.query.trim() ? "matches" : "recent sessions"}`)));
|
|
3827
|
+
lines.push(...this.renderSearchHitPane(inner, row, dim, muted, accent));
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
lines.push(divider());
|
|
3831
|
+
if (compact) {
|
|
3832
|
+
lines.push(row(`${ansiYellow("alt+c/f/r/m/i/n/a")} ${dim("actions")} ${ansiYellow("tab")} ${dim("scope")}`));
|
|
3833
|
+
} else {
|
|
3834
|
+
lines.push(row(`${ansiYellow("-/=")} ${dim("page")} ${ansiYellow("alt+c")} ${dim("copy")} ${ansiYellow("alt+f")} ${dim("fork")} ${ansiYellow("alt+r")} ${dim("resume")}`));
|
|
3835
|
+
lines.push(row(`${ansiYellow("alt+m")} ${dim("resume+model")} ${ansiYellow("alt+i")} ${dim("inject+ctx")} ${ansiYellow("alt+n")} ${dim("new+ctx")} ${ansiYellow("alt+a")} ${dim("actions")} ${ansiYellow("tab")} ${dim("scope")}`));
|
|
3567
3836
|
}
|
|
3568
|
-
lines.push(divider(), empty());
|
|
3569
|
-
lines.push(row(`${ansiYellow("-/=")} ${dim("page")} ${ansiYellow("enter")} ${dim("prompts")} ${ansiYellow("ctrl+u")} ${dim("clear")} ${ansiYellow("esc")} ${dim("close")}`));
|
|
3570
3837
|
lines.push(bottom());
|
|
3571
3838
|
return lines;
|
|
3572
3839
|
}
|
|
3573
3840
|
|
|
3841
|
+
private renderSearchHitPane(
|
|
3842
|
+
inner: number,
|
|
3843
|
+
row: (content?: string) => string,
|
|
3844
|
+
dim: (s: string) => string,
|
|
3845
|
+
muted: (s: string) => string,
|
|
3846
|
+
accent: (s: string) => string,
|
|
3847
|
+
): string[] {
|
|
3848
|
+
const state = this.searchState;
|
|
3849
|
+
const compact = this.compactSearchLayout();
|
|
3850
|
+
const maxVisible = this.searchMaxVisibleRows();
|
|
3851
|
+
const start = Math.max(0, Math.min(state.selected - Math.floor(maxVisible / 2), state.results.length - maxVisible));
|
|
3852
|
+
const end = Math.min(start + maxVisible, state.results.length);
|
|
3853
|
+
const gap = ` ${dim("│")} `;
|
|
3854
|
+
const twoPane = inner >= 72;
|
|
3855
|
+
const rightWidth = twoPane ? Math.max(24, Math.floor(inner * 0.43)) : 0;
|
|
3856
|
+
const leftWidth = twoPane ? Math.max(24, inner - rightWidth - visibleWidth(gap)) : inner;
|
|
3857
|
+
const paneRows = Math.max(1, (end - start) * 3 - 1);
|
|
3858
|
+
const selectedHit = state.results[state.selected];
|
|
3859
|
+
const rightLines = twoPane ? this.renderPromptPreviewLines(selectedHit, rightWidth, paneRows, dim, muted, accent) : [];
|
|
3860
|
+
const fixed = (content: string, colWidth: number, selected = false): string => {
|
|
3861
|
+
const safe = content.replace(/[\r\n\t]+/g, " ");
|
|
3862
|
+
const clipped = truncateToWidth(safe, colWidth, "");
|
|
3863
|
+
const padded = clipped + " ".repeat(Math.max(0, colWidth - visibleWidth(clipped)));
|
|
3864
|
+
return selected ? this.theme.bg("selectedBg", padded) : padded;
|
|
3865
|
+
};
|
|
3866
|
+
const leftLines: string[] = [];
|
|
3867
|
+
for (let i = start; i < end; i++) {
|
|
3868
|
+
const hit = state.results[i]!;
|
|
3869
|
+
const selected = i === state.selected;
|
|
3870
|
+
const promptDate = new Date(promptRecencyTime(hit));
|
|
3871
|
+
const metadata = dim(`#${hit.message.index} · ${formatSessionSearchDate(promptDate)}`);
|
|
3872
|
+
const titleText = truncateToWidth(sessionResumeTitle(hit.result), Math.max(8, leftWidth - visibleWidth(metadata) - 1), "…");
|
|
3873
|
+
const title = accent(titleText);
|
|
3874
|
+
const snippet = styleSessionSnippet(hit.snippet || hit.message.text, state.query, this.theme);
|
|
3875
|
+
leftLines.push(fixed(selected ? this.theme.bold(snippet) : snippet, leftWidth, selected));
|
|
3876
|
+
leftLines.push(fixed(`${title} ${metadata}`, leftWidth, selected));
|
|
3877
|
+
if (i < end - 1) leftLines.push(fixed(dim("─".repeat(Math.max(8, leftWidth))), leftWidth));
|
|
3878
|
+
}
|
|
3879
|
+
const output: string[] = [];
|
|
3880
|
+
for (let i = 0; i < leftLines.length; i++) {
|
|
3881
|
+
if (twoPane) output.push(row(`${leftLines[i] ?? ""}${gap}${rightLines[i] ?? " ".repeat(rightWidth)}`));
|
|
3882
|
+
else output.push(row(leftLines[i] ?? ""));
|
|
3883
|
+
}
|
|
3884
|
+
if (!twoPane && selectedHit) {
|
|
3885
|
+
output.push(row(dim("─".repeat(Math.max(8, inner)))));
|
|
3886
|
+
for (const line of this.renderPromptPreviewLines(selectedHit, inner, Math.min(5, paneRows), dim, muted, accent)) output.push(row(line));
|
|
3887
|
+
}
|
|
3888
|
+
if (!compact && state.results.length > maxVisible) output.push(row(dim(`${state.selected + 1}/${state.results.length} ${state.query.trim() ? "matches" : "recent prompts"}`)));
|
|
3889
|
+
return output;
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
private renderPromptPreviewLines(
|
|
3893
|
+
hit: QolSessionSearchHit | undefined,
|
|
3894
|
+
width: number,
|
|
3895
|
+
maxLines: number,
|
|
3896
|
+
dim: (s: string) => string,
|
|
3897
|
+
muted: (s: string) => string,
|
|
3898
|
+
accent: (s: string) => string,
|
|
3899
|
+
): string[] {
|
|
3900
|
+
const fixed = (content: string): string => {
|
|
3901
|
+
const clipped = truncateToWidth(content.replace(/[\r\n\t]+/g, " "), width, "…");
|
|
3902
|
+
return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
3903
|
+
};
|
|
3904
|
+
if (!hit) return Array.from({ length: maxLines }, () => fixed(""));
|
|
3905
|
+
const title = accent("Prompt preview");
|
|
3906
|
+
const cwd = muted(truncateToWidth(shortPathForUi(hit.result.cwd || hit.result.path), width, "…"));
|
|
3907
|
+
const bodyBudget = Math.max(1, maxLines - 3);
|
|
3908
|
+
const body = wrapVisible(styleSessionSnippet(hit.message.text, this.searchState.query, this.theme), width, bodyBudget);
|
|
3909
|
+
const lines = [fixed(title), fixed(cwd), fixed(dim("─".repeat(Math.max(8, width)))), ...body.map(fixed)];
|
|
3910
|
+
while (lines.length < maxLines) lines.push(fixed(""));
|
|
3911
|
+
return lines.slice(0, maxLines);
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3914
|
+
private renderScopeTabs(inner: number): string {
|
|
3915
|
+
const tabs: { id: QolSessionSearchScope; label: string }[] = [
|
|
3916
|
+
{ id: "current", label: "Project" },
|
|
3917
|
+
{ id: "all", label: "All" },
|
|
3918
|
+
];
|
|
3919
|
+
const parts = tabs.map((tab) => {
|
|
3920
|
+
const label = ` ${truncateToWidth(tab.label, 18, "…")} `;
|
|
3921
|
+
if (tab.id === this.searchState.scope) return this.theme.fg("accent", this.theme.inverse(this.theme.bold(label)));
|
|
3922
|
+
return this.theme.bg("selectedBg", this.theme.fg("accent", label));
|
|
3923
|
+
});
|
|
3924
|
+
return truncateToWidth(parts.join(" "), inner, "");
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3574
3927
|
private renderMessages(width: number, state: QolSessionMessagesState): string[] {
|
|
3575
3928
|
const { bottom, divider, empty, inner, row, selectedRow, top } = boxParts(width, this.theme);
|
|
3576
3929
|
const accent = (s: string) => this.theme.fg("accent", s);
|
|
@@ -3584,11 +3937,10 @@ class QolSessionSearchComponent {
|
|
|
3584
3937
|
const gap = Math.max(1, inner - visibleWidth(clippedLeft) - visibleWidth(right));
|
|
3585
3938
|
return `${clippedLeft}${" ".repeat(gap)}${right}`;
|
|
3586
3939
|
};
|
|
3587
|
-
const right = dim(
|
|
3940
|
+
const right = dim(formatSessionSearchDate(result.modified));
|
|
3588
3941
|
const title = truncateToWidth(sessionResumeTitle(result), Math.max(12, inner - visibleWidth(right) - 1), "…");
|
|
3589
3942
|
lines.push(row(pair(this.theme.bold(accent(title)), right)));
|
|
3590
3943
|
lines.push(row(muted(truncateToWidth(shortPathForUi(result.cwd || result.path), inner, "…"))));
|
|
3591
|
-
lines.push(row(dim(`${state.messages.length} user prompt${state.messages.length === 1 ? "" : "s"} · enter opens prompt actions · r resumes latest session state`)));
|
|
3592
3944
|
lines.push(empty(), divider(), empty());
|
|
3593
3945
|
|
|
3594
3946
|
const maxVisible = this.messageMaxVisibleRows();
|
|
@@ -3604,9 +3956,9 @@ class QolSessionSearchComponent {
|
|
|
3604
3956
|
const messageRow = `${number} ${selected ? this.theme.bold(accent(text)) : text}`;
|
|
3605
3957
|
lines.push(selected ? selectedRow(messageRow) : row(messageRow));
|
|
3606
3958
|
}
|
|
3607
|
-
if (state.messages.length > maxVisible) lines.push(empty(), row(dim(`${state.selected + 1}/${state.messages.length} user prompts`)));
|
|
3608
3959
|
lines.push(divider(), empty());
|
|
3609
|
-
lines.push(row(`${ansiYellow("-/=")} ${dim("page")} ${ansiYellow("
|
|
3960
|
+
lines.push(row(`${ansiYellow("-/=")} ${dim("page")} ${ansiYellow("alt+c")} ${dim("copy")} ${ansiYellow("alt+f")} ${dim("fork")} ${ansiYellow("alt+r")} ${dim("resume")}`));
|
|
3961
|
+
lines.push(row(`${ansiYellow("alt+m")} ${dim("resume+model")} ${ansiYellow("alt+i")} ${dim("inject+ctx")} ${ansiYellow("alt+n")} ${dim("new+ctx")}`));
|
|
3610
3962
|
lines.push(bottom());
|
|
3611
3963
|
return lines;
|
|
3612
3964
|
}
|
|
@@ -3624,46 +3976,85 @@ class QolSessionSearchComponent {
|
|
|
3624
3976
|
lines.push(row(dim("Selected prompt")));
|
|
3625
3977
|
const wrapped = wrapVisible(state.message.text, inner, 4);
|
|
3626
3978
|
for (const line of wrapped) lines.push(row(line));
|
|
3627
|
-
lines.push(empty());
|
|
3628
|
-
lines.push(row(dim("Resume Session From Here restores this prompt in the selected session.")));
|
|
3629
|
-
lines.push(row(dim("Inject Context adds a summary here; New Session + Context imports it into a new session.")));
|
|
3630
3979
|
lines.push(empty(), divider(), empty());
|
|
3631
|
-
const
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3980
|
+
const helpLines = [
|
|
3981
|
+
"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.",
|
|
3982
|
+
"Copy Prompt copies this prompt into your current editor.",
|
|
3983
|
+
"Inject + Context summarizes the source session into the current session, focused on this prompt.",
|
|
3984
|
+
"New + Context creates a fresh session and imports that focused summary there.",
|
|
3985
|
+
];
|
|
3986
|
+
for (const help of helpLines) {
|
|
3987
|
+
for (const line of wrapVisible(dim(help), inner, 2)) lines.push(row(line));
|
|
3988
|
+
}
|
|
3989
|
+
lines.push(empty(), divider(), empty());
|
|
3990
|
+
lines.push(row(`${ansiYellow("alt+c")} ${dim("copy")} ${ansiYellow("alt+f")} ${dim("fork")} ${ansiYellow("alt+r")} ${dim("resume")} ${ansiYellow("alt+m")} ${dim("resume+model")}`));
|
|
3991
|
+
lines.push(row(`${ansiYellow("alt+i")} ${dim("inject+ctx")} ${ansiYellow("alt+n")} ${dim("new+ctx")}`));
|
|
3992
|
+
lines.push(bottom());
|
|
3993
|
+
return lines;
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
private renderForkConfirm(width: number, state: QolSessionForkConfirmState): string[] {
|
|
3997
|
+
const { bottom, divider, empty, inner, row, selectedRow, top } = boxParts(width, this.theme);
|
|
3998
|
+
const accent = (s: string) => this.theme.fg("accent", s);
|
|
3999
|
+
const dim = (s: string) => this.theme.fg("dim", s);
|
|
4000
|
+
const muted = (s: string) => this.theme.fg("muted", s);
|
|
4001
|
+
const lines: string[] = [top("Fork Session", `${state.selected + 1}/${state.messages.length}`), empty()];
|
|
4002
|
+
const title = truncateToWidth(sessionResumeTitle(state.result), inner, "…");
|
|
4003
|
+
lines.push(row(this.theme.bold(accent(title))));
|
|
4004
|
+
lines.push(row(muted(truncateToWidth(shortPathForUi(state.result.cwd || state.result.path), inner, "…"))));
|
|
4005
|
+
lines.push(empty(), divider(), empty());
|
|
4006
|
+
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)) {
|
|
4007
|
+
lines.push(row(line));
|
|
4008
|
+
}
|
|
3636
4009
|
lines.push(empty());
|
|
3637
|
-
|
|
4010
|
+
|
|
4011
|
+
const maxVisible = this.forkConfirmMaxVisibleRows();
|
|
4012
|
+
const start = Math.max(0, Math.min(state.selected - Math.floor(maxVisible / 2), state.messages.length - maxVisible));
|
|
4013
|
+
const end = Math.min(start + maxVisible, state.messages.length);
|
|
4014
|
+
for (let i = start; i < end; i++) {
|
|
4015
|
+
const message = state.messages[i]!;
|
|
4016
|
+
const selected = i === state.selected;
|
|
4017
|
+
const numberPlain = `#${message.index}`;
|
|
4018
|
+
const number = selected ? this.theme.fg("text", numberPlain) : dim(numberPlain);
|
|
4019
|
+
const textWidth = Math.max(12, inner - visibleWidth(number) - 1);
|
|
4020
|
+
const text = truncateToWidth(message.text, textWidth, "…");
|
|
4021
|
+
const messageRow = `${number} ${selected ? this.theme.bold(accent(text)) : text}`;
|
|
4022
|
+
lines.push(selected ? selectedRow(messageRow) : row(messageRow));
|
|
4023
|
+
}
|
|
4024
|
+
if (state.messages.length > maxVisible) lines.push(row(dim(`${state.selected + 1}/${state.messages.length} prompts`)));
|
|
4025
|
+
lines.push(divider(), empty());
|
|
4026
|
+
lines.push(row(`${ansiYellow("backspace")} ${dim("back")}`));
|
|
3638
4027
|
lines.push(bottom());
|
|
3639
4028
|
return lines;
|
|
3640
4029
|
}
|
|
3641
4030
|
|
|
3642
|
-
private
|
|
3643
|
-
const { bottom, divider, empty,
|
|
4031
|
+
private renderContextConfirm(width: number, state: QolSessionContextConfirmState): string[] {
|
|
4032
|
+
const { bottom, divider, empty, inner, row, top } = boxParts(width, this.theme);
|
|
3644
4033
|
const accent = (s: string) => this.theme.fg("accent", s);
|
|
3645
4034
|
const dim = (s: string) => this.theme.fg("dim", s);
|
|
3646
4035
|
const muted = (s: string) => this.theme.fg("muted", s);
|
|
3647
|
-
const action = state.type === "newSession" ? "New
|
|
3648
|
-
const
|
|
3649
|
-
lines
|
|
3650
|
-
lines.push(row(
|
|
4036
|
+
const action = state.type === "newSession" ? "New + Context" : "Inject + Context";
|
|
4037
|
+
const verb = state.type === "newSession" ? "create new session with context" : "inject context";
|
|
4038
|
+
const lines: string[] = [top(action), empty()];
|
|
4039
|
+
lines.push(row(`${accent(sessionResumeTitle(state.result))} ${dim(`#${state.message.index}`)}`));
|
|
4040
|
+
lines.push(row(muted(truncateToWidth(shortPathForUi(state.result.cwd || state.result.path), inner, "…"))));
|
|
3651
4041
|
lines.push(empty(), divider(), empty());
|
|
3652
|
-
const
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
for (
|
|
4042
|
+
const details = [
|
|
4043
|
+
"This summarizes the entire source session, including messages after this prompt.",
|
|
4044
|
+
"The selected prompt is used as the focus for the summary, not as a cutoff point.",
|
|
4045
|
+
state.type === "newSession"
|
|
4046
|
+
? "New + Context creates a fresh session and imports that focused summary there."
|
|
4047
|
+
: "Inject + Context adds that focused summary to your current session.",
|
|
4048
|
+
];
|
|
4049
|
+
for (const detail of details) {
|
|
4050
|
+
for (const line of wrapVisible(dim(detail), inner, 2)) lines.push(row(line));
|
|
3661
4051
|
}
|
|
3662
4052
|
lines.push(empty(), divider(), empty());
|
|
3663
|
-
lines.push(row(`${ansiYellow("
|
|
4053
|
+
lines.push(row(`${ansiYellow("backspace")} ${dim("back")}`));
|
|
3664
4054
|
lines.push(bottom());
|
|
3665
4055
|
return lines;
|
|
3666
4056
|
}
|
|
4057
|
+
|
|
3667
4058
|
}
|
|
3668
4059
|
|
|
3669
4060
|
function wrapVisible(text: string, width: number, maxLines: number): string[] {
|
|
@@ -3728,7 +4119,6 @@ class QolSessionSearchLoadingComponent {
|
|
|
3728
4119
|
empty(),
|
|
3729
4120
|
divider(),
|
|
3730
4121
|
empty(),
|
|
3731
|
-
row(`${ansiYellow("esc")} ${dim("cancel")} ${ansiYellow("ctrl+c")} ${dim("cancel")}`),
|
|
3732
4122
|
bottom(),
|
|
3733
4123
|
];
|
|
3734
4124
|
}
|
|
@@ -3769,7 +4159,7 @@ function messagesForSessionSummary(sessionPath: string): AgentMessage[] {
|
|
|
3769
4159
|
async function buildSessionSearchContextMessage(ctx: ExtensionContext, result: QolSessionSearchResult, customPrompt?: string, signal?: AbortSignal): Promise<QolSessionSearchPendingMessage> {
|
|
3770
4160
|
const messages = messagesForSessionSummary(result.path);
|
|
3771
4161
|
const fallbackText = result.allMessagesText || result.firstMessage;
|
|
3772
|
-
const conversationText = messages.length > 0 ?
|
|
4162
|
+
const conversationText = messages.length > 0 ? serializeMessagesForSummary(messages) : fallbackText;
|
|
3773
4163
|
if (!conversationText.trim()) throw new Error("Selected session has no text content to summarize");
|
|
3774
4164
|
const focus = customPrompt?.trim();
|
|
3775
4165
|
const summary = await generateQolSummary(ctx, {
|
|
@@ -3851,7 +4241,7 @@ async function buildSessionSearchContextMessageWithLoader(ctx: ExtensionContext,
|
|
|
3851
4241
|
|
|
3852
4242
|
async function injectSessionSearchContext(pi: ExtensionAPI, ctx: ExtensionContext, result: QolSessionSearchResult, customPrompt?: string): Promise<void> {
|
|
3853
4243
|
const title = sessionDisplayName(result);
|
|
3854
|
-
const message = await buildSessionSearchContextMessageWithLoader(ctx, result, customPrompt, "Inject Context");
|
|
4244
|
+
const message = await buildSessionSearchContextMessageWithLoader(ctx, result, customPrompt, "Inject + Context");
|
|
3855
4245
|
pi.sendMessage({ customType: SESSION_SEARCH_CONTEXT_TYPE, content: message.content, details: message.details, display: true }, { deliverAs: "followUp", triggerTurn: false });
|
|
3856
4246
|
ctx.ui.notify(`Session context injected: ${title}`, "info");
|
|
3857
4247
|
}
|
|
@@ -3860,9 +4250,52 @@ function asCommandContext(ctx: ExtensionContext): (ExtensionContext & Partial<Ex
|
|
|
3860
4250
|
return ctx as ExtensionContext & Partial<ExtensionCommandContext>;
|
|
3861
4251
|
}
|
|
3862
4252
|
|
|
4253
|
+
function queueSessionSearchCommandAction(ctx: ExtensionContext, action: QolSessionPaletteAction): void {
|
|
4254
|
+
const id = `ss-${Date.now().toString(36)}-${(++qolSessionSearchPendingActionCounter).toString(36)}`;
|
|
4255
|
+
qolSessionSearchPendingActions.set(id, action);
|
|
4256
|
+
ctx.ui.setEditorText(`/search:resume-pending ${id}`);
|
|
4257
|
+
ctx.ui.notify(`${sessionDisplayName(action.result!)} — press Enter to ${action.type === "fork" ? "fork" : "resume"}`, "info");
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
async function runSessionSearchResumeOrFork(pi: ExtensionAPI, ctx: ExtensionContext, action: QolSessionPaletteAction): Promise<boolean> {
|
|
4261
|
+
if (!action.result || (action.type !== "resume" && action.type !== "fork")) return true;
|
|
4262
|
+
const commandCtx = asCommandContext(ctx);
|
|
4263
|
+
if (typeof commandCtx.switchSession !== "function") return false;
|
|
4264
|
+
const targetTitle = sessionDisplayName(action.result);
|
|
4265
|
+
const selectedMessage = action.type === "fork" ? action.message : undefined;
|
|
4266
|
+
const currentModel = action.keepCurrentModel ? ctx.model : undefined;
|
|
4267
|
+
const currentThinking = action.keepCurrentModel && typeof pi.getThinkingLevel === "function" ? pi.getThinkingLevel() : undefined;
|
|
4268
|
+
if (currentModel) pinSessionModel(action.result.path, currentModel, currentThinking);
|
|
4269
|
+
let replacementStarted = false;
|
|
4270
|
+
try {
|
|
4271
|
+
const result = await commandCtx.switchSession(action.result.path, {
|
|
4272
|
+
withSession: async (replacementCtx: any) => {
|
|
4273
|
+
replacementStarted = true;
|
|
4274
|
+
if (currentModel) replacementCtx.ui.notify(`Using current model: ${currentModel.provider}/${currentModel.id}`, "info");
|
|
4275
|
+
if (selectedMessage) {
|
|
4276
|
+
const manager = replacementCtx.sessionManager as any;
|
|
4277
|
+
if (selectedMessage.entryId && selectedMessage.parentId && typeof manager?.branch === "function") manager.branch(selectedMessage.parentId);
|
|
4278
|
+
else if (selectedMessage.entryId && selectedMessage.parentId === null && typeof manager?.resetLeaf === "function") manager.resetLeaf();
|
|
4279
|
+
replacementCtx.ui.setEditorText(selectedMessage.text);
|
|
4280
|
+
const timer = setTimeout(() => {
|
|
4281
|
+
replacementCtx.ui.notify(`Forked session: ${targetTitle} at prompt #${selectedMessage.index}. Submit to branch from here.`, "info");
|
|
4282
|
+
}, 0);
|
|
4283
|
+
timer.unref?.();
|
|
4284
|
+
return;
|
|
4285
|
+
}
|
|
4286
|
+
replacementCtx.ui.notify(`Resumed session: ${targetTitle}${currentModel ? " with current model" : ""}`, "info");
|
|
4287
|
+
},
|
|
4288
|
+
});
|
|
4289
|
+
if (result.cancelled) ctx.ui.notify(selectedMessage ? "Fork cancelled" : "Resume cancelled", "info");
|
|
4290
|
+
} catch (error) {
|
|
4291
|
+
if (!replacementStarted) ctx.ui.notify(`${selectedMessage ? "Fork" : "Resume"} failed: ${stringifyError(error)}`, "error");
|
|
4292
|
+
}
|
|
4293
|
+
return true;
|
|
4294
|
+
}
|
|
4295
|
+
|
|
3863
4296
|
async function createNewSessionWithSearchContext(pi: ExtensionAPI, ctx: ExtensionContext, result: QolSessionSearchResult, customPrompt?: string): Promise<void> {
|
|
3864
4297
|
const title = sessionDisplayName(result);
|
|
3865
|
-
const message = await buildSessionSearchContextMessageWithLoader(ctx, result, customPrompt, "New
|
|
4298
|
+
const message = await buildSessionSearchContextMessageWithLoader(ctx, result, customPrompt, "New + Context");
|
|
3866
4299
|
|
|
3867
4300
|
const commandCtx = asCommandContext(ctx);
|
|
3868
4301
|
if (typeof commandCtx.newSession === "function") {
|
|
@@ -3891,7 +4324,7 @@ async function createNewSessionWithSearchContext(pi: ExtensionAPI, ctx: Extensio
|
|
|
3891
4324
|
}
|
|
3892
4325
|
setPendingSessionSearchMessage(message);
|
|
3893
4326
|
ctx.ui.setEditorText("/new");
|
|
3894
|
-
ctx.ui.notify(`${title} — press
|
|
4327
|
+
ctx.ui.notify(`${title} — press enter to create a new session with this context`, "info");
|
|
3895
4328
|
}
|
|
3896
4329
|
|
|
3897
4330
|
async function consumePendingSessionSearchContext(pi: ExtensionAPI, ctx: ExtensionContext, reason: unknown): Promise<void> {
|
|
@@ -3925,35 +4358,8 @@ async function openQolSessionSearch(pi: ExtensionAPI, ctx: ExtensionContext, ini
|
|
|
3925
4358
|
releaseModalLock();
|
|
3926
4359
|
}
|
|
3927
4360
|
if (!action || action.type === "cancel" || !action.result) return;
|
|
3928
|
-
if (action.type === "resume") {
|
|
3929
|
-
|
|
3930
|
-
if (typeof commandCtx.switchSession === "function") {
|
|
3931
|
-
const targetTitle = sessionDisplayName(action.result);
|
|
3932
|
-
const selectedMessage = action.message;
|
|
3933
|
-
let replacementStarted = false;
|
|
3934
|
-
try {
|
|
3935
|
-
const result = await commandCtx.switchSession(action.result.path, {
|
|
3936
|
-
withSession: async (replacementCtx: any) => {
|
|
3937
|
-
replacementStarted = true;
|
|
3938
|
-
if (selectedMessage?.entryId) {
|
|
3939
|
-
const manager = replacementCtx.sessionManager as any;
|
|
3940
|
-
if (selectedMessage.parentId && typeof manager?.branch === "function") manager.branch(selectedMessage.parentId);
|
|
3941
|
-
else if (selectedMessage.parentId === null && typeof manager?.resetLeaf === "function") manager.resetLeaf();
|
|
3942
|
-
replacementCtx.ui.setEditorText(selectedMessage.text);
|
|
3943
|
-
replacementCtx.ui.notify(`Resumed ${targetTitle} from prompt #${selectedMessage.index}. Submit to branch from here.`, "info");
|
|
3944
|
-
return;
|
|
3945
|
-
}
|
|
3946
|
-
replacementCtx.ui.notify(`Resumed session: ${targetTitle}`, "info");
|
|
3947
|
-
},
|
|
3948
|
-
});
|
|
3949
|
-
if (result.cancelled) ctx.ui.notify("Resume cancelled", "info");
|
|
3950
|
-
} catch (error) {
|
|
3951
|
-
if (!replacementStarted) ctx.ui.notify(`Resume failed: ${stringifyError(error)}`, "error");
|
|
3952
|
-
}
|
|
3953
|
-
return;
|
|
3954
|
-
}
|
|
3955
|
-
ctx.ui.setEditorText(`/resume ${quoteSessionSearchArg(action.result.path)}`);
|
|
3956
|
-
ctx.ui.notify(`${sessionDisplayName(action.result)} — press Enter to resume`, "info");
|
|
4361
|
+
if (action.type === "resume" || action.type === "fork") {
|
|
4362
|
+
if (!(await runSessionSearchResumeOrFork(pi, ctx, action))) queueSessionSearchCommandAction(ctx, action);
|
|
3957
4363
|
return;
|
|
3958
4364
|
}
|
|
3959
4365
|
if (action.type === "copy") {
|
|
@@ -3967,7 +4373,7 @@ async function openQolSessionSearch(pi: ExtensionAPI, ctx: ExtensionContext, ini
|
|
|
3967
4373
|
await injectSessionSearchContext(pi, ctx, action.result, action.customPrompt);
|
|
3968
4374
|
} catch (error) {
|
|
3969
4375
|
const message = stringifyError(error);
|
|
3970
|
-
ctx.ui.notify(/cancelled/i.test(message) ? "Inject
|
|
4376
|
+
ctx.ui.notify(/cancelled/i.test(message) ? "Inject + Context cancelled" : `Inject + Context failed: ${message}`, /cancelled/i.test(message) ? "info" : "error");
|
|
3971
4377
|
ctx.ui.setStatus(SESSION_SEARCH_STATUS_KEY, undefined);
|
|
3972
4378
|
}
|
|
3973
4379
|
return;
|
|
@@ -3977,16 +4383,12 @@ async function openQolSessionSearch(pi: ExtensionAPI, ctx: ExtensionContext, ini
|
|
|
3977
4383
|
await createNewSessionWithSearchContext(pi, ctx, action.result, action.customPrompt);
|
|
3978
4384
|
} catch (error) {
|
|
3979
4385
|
const message = stringifyError(error);
|
|
3980
|
-
ctx.ui.notify(/cancelled/i.test(message) ? "New
|
|
4386
|
+
ctx.ui.notify(/cancelled/i.test(message) ? "New + Context cancelled" : `New + Context failed: ${message}`, /cancelled/i.test(message) ? "info" : "error");
|
|
3981
4387
|
ctx.ui.setStatus(SESSION_SEARCH_STATUS_KEY, undefined);
|
|
3982
4388
|
}
|
|
3983
4389
|
}
|
|
3984
4390
|
}
|
|
3985
4391
|
|
|
3986
|
-
function quoteSessionSearchArg(value: string): string {
|
|
3987
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
3988
|
-
}
|
|
3989
|
-
|
|
3990
4392
|
function renderSessionSearchContextMessage(message: any, options: any, theme: Theme): Text {
|
|
3991
4393
|
const raw = typeof message.content === "string"
|
|
3992
4394
|
? message.content
|
|
@@ -4182,6 +4584,10 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
4182
4584
|
let tmuxChangedPaneBorderStatus = false;
|
|
4183
4585
|
let tmuxChangedPaneBorderFormat = false;
|
|
4184
4586
|
let tmuxLastPaneTitle: string | undefined;
|
|
4587
|
+
let tmuxOriginalWindowNameTitle: string | undefined;
|
|
4588
|
+
let tmuxOriginalAutomaticRename: string | undefined;
|
|
4589
|
+
let tmuxChangedAutomaticRename = false;
|
|
4590
|
+
let tmuxLastWindowNameTitle: string | undefined;
|
|
4185
4591
|
let lastSessionTitle: string | undefined;
|
|
4186
4592
|
|
|
4187
4593
|
const requestRender = () => activeTui?.requestRender();
|
|
@@ -4208,9 +4614,17 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
4208
4614
|
const target = tmuxPaneTitleTarget;
|
|
4209
4615
|
if (!target) return;
|
|
4210
4616
|
const nextTitle = sessionTitle ? formatTmuxSessionTitle(sessionTitle) : tmuxOriginalPaneTitle;
|
|
4211
|
-
if (nextTitle
|
|
4212
|
-
|
|
4213
|
-
|
|
4617
|
+
if (nextTitle !== undefined && nextTitle !== tmuxLastPaneTitle) {
|
|
4618
|
+
tmuxLastPaneTitle = nextTitle;
|
|
4619
|
+
setTmuxPaneTitle(target, nextTitle);
|
|
4620
|
+
}
|
|
4621
|
+
if (settingBoolean("showSessionNameWindow", true, ctx.cwd)) {
|
|
4622
|
+
const nextWindow = sessionTitle ? formatTmuxSessionTitle(sessionTitle) : tmuxOriginalWindowNameTitle;
|
|
4623
|
+
if (nextWindow !== undefined && nextWindow !== tmuxLastWindowNameTitle) {
|
|
4624
|
+
tmuxLastWindowNameTitle = nextWindow;
|
|
4625
|
+
setTmuxWindowName(target, nextWindow);
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4214
4628
|
};
|
|
4215
4629
|
|
|
4216
4630
|
const installSessionTitle = (ctx: ExtensionContext) => {
|
|
@@ -4239,6 +4653,21 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
4239
4653
|
setTmuxWindowOption(tmuxTarget, "pane-border-format", TMUX_SESSION_TITLE_BORDER_FORMAT);
|
|
4240
4654
|
}
|
|
4241
4655
|
});
|
|
4656
|
+
if (settingBoolean("showSessionNameWindow", true, ctx.cwd)) {
|
|
4657
|
+
readTmuxWindowName(tmuxTarget, (name) => {
|
|
4658
|
+
if (tmuxPaneTitleTarget !== tmuxTarget) return;
|
|
4659
|
+
tmuxOriginalWindowNameTitle = name;
|
|
4660
|
+
syncSessionTitle(ctx);
|
|
4661
|
+
});
|
|
4662
|
+
readTmuxWindowOption(tmuxTarget, "automatic-rename", (value) => {
|
|
4663
|
+
if (tmuxPaneTitleTarget !== tmuxTarget) return;
|
|
4664
|
+
tmuxOriginalAutomaticRename = value;
|
|
4665
|
+
if (value !== "off") {
|
|
4666
|
+
tmuxChangedAutomaticRename = true;
|
|
4667
|
+
setTmuxWindowOption(tmuxTarget, "automatic-rename", "off");
|
|
4668
|
+
}
|
|
4669
|
+
});
|
|
4670
|
+
}
|
|
4242
4671
|
return;
|
|
4243
4672
|
}
|
|
4244
4673
|
ctx.ui.setHeader((tui, theme) => {
|
|
@@ -4264,6 +4693,8 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
4264
4693
|
if (tmuxPaneTitleTarget && tmuxOriginalPaneTitle !== undefined) setTmuxPaneTitle(tmuxPaneTitleTarget, tmuxOriginalPaneTitle);
|
|
4265
4694
|
if (tmuxPaneTitleTarget && tmuxChangedPaneBorderStatus && tmuxOriginalPaneBorderStatus !== undefined) setTmuxWindowOption(tmuxPaneTitleTarget, "pane-border-status", tmuxOriginalPaneBorderStatus);
|
|
4266
4695
|
if (tmuxPaneTitleTarget && tmuxChangedPaneBorderFormat && tmuxOriginalPaneBorderFormat !== undefined) setTmuxWindowOption(tmuxPaneTitleTarget, "pane-border-format", tmuxOriginalPaneBorderFormat);
|
|
4696
|
+
if (tmuxPaneTitleTarget && tmuxOriginalWindowNameTitle !== undefined && tmuxLastWindowNameTitle !== undefined) setTmuxWindowName(tmuxPaneTitleTarget, tmuxOriginalWindowNameTitle);
|
|
4697
|
+
if (tmuxPaneTitleTarget && tmuxChangedAutomaticRename && tmuxOriginalAutomaticRename !== undefined) setTmuxWindowOption(tmuxPaneTitleTarget, "automatic-rename", tmuxOriginalAutomaticRename);
|
|
4267
4698
|
tmuxPaneTitleTarget = undefined;
|
|
4268
4699
|
tmuxOriginalPaneTitle = undefined;
|
|
4269
4700
|
tmuxOriginalPaneBorderStatus = undefined;
|
|
@@ -4271,6 +4702,10 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
4271
4702
|
tmuxChangedPaneBorderStatus = false;
|
|
4272
4703
|
tmuxChangedPaneBorderFormat = false;
|
|
4273
4704
|
tmuxLastPaneTitle = undefined;
|
|
4705
|
+
tmuxOriginalWindowNameTitle = undefined;
|
|
4706
|
+
tmuxOriginalAutomaticRename = undefined;
|
|
4707
|
+
tmuxChangedAutomaticRename = false;
|
|
4708
|
+
tmuxLastWindowNameTitle = undefined;
|
|
4274
4709
|
lastSessionTitle = undefined;
|
|
4275
4710
|
ctx.ui.setStatus(SESSION_MANAGER_STATUS_KEY, undefined);
|
|
4276
4711
|
ctx.ui.setWidget("statusline", undefined);
|
|
@@ -4573,6 +5008,19 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
4573
5008
|
description: "Refresh the session search index",
|
|
4574
5009
|
handler: async (_args, ctx) => handleSearchCommand("refresh", ctx),
|
|
4575
5010
|
});
|
|
5011
|
+
pi.registerCommand("search:resume-pending", {
|
|
5012
|
+
description: "Run a pending session-search resume or fork action",
|
|
5013
|
+
handler: async (args, ctx) => {
|
|
5014
|
+
const id = args.trim();
|
|
5015
|
+
const action = qolSessionSearchPendingActions.get(id);
|
|
5016
|
+
if (!action) {
|
|
5017
|
+
ctx.ui.notify("No pending session-search resume/fork action found.", "warning");
|
|
5018
|
+
return;
|
|
5019
|
+
}
|
|
5020
|
+
qolSessionSearchPendingActions.delete(id);
|
|
5021
|
+
if (!(await runSessionSearchResumeOrFork(pi, ctx, action))) ctx.ui.notify("Session resume/fork is unavailable in this context.", "error");
|
|
5022
|
+
},
|
|
5023
|
+
});
|
|
4576
5024
|
const shortcut = sessionSearchShortcut();
|
|
4577
5025
|
if (shortcut) {
|
|
4578
5026
|
pi.registerShortcut(shortcut, {
|