Improve chat session switching and history pagination, with most of the aggressive limits scoped to the VS Code webview where the freezes were observed. Session history loading and pagination: - Reduce the VS Code message page size to 30 records so switching sessions does not immediately hydrate large histories into the webview. - Keep manual Load older messages in VS Code fixed at 30 records per request instead of growing the request size over time. - Add a bounded VS Code initial-tail expansion path from 30 to 50, 80, and 120 records only when the initial page has no user-message turn boundary, preventing large final turns from rendering as an empty chat. - Lower the normal web message page size from 200 to 150 for a mild shared optimization without adopting the aggressive VS Code limits. - Make session pagination metadata reactive per session so ChatContainer receives cursor updates from materialization and reconnect paths without requiring a switch away and back. - Write pagination metadata before publishing newly materialized messages so the first render sees the correct has-more state. - Store cursor information from direct materialization and reconnect message fetches in the shared session prefetch metadata cache. VS Code cache and memory pressure reductions: - Use a shared per-directory session recency map so cache eviction is based on app-level recency instead of whichever useSync instance happened to run. - Limit VS Code warm session cache retention to 4 sessions and evict heavy inactive message caches after switching away from a large session. - Disable sidebar session prefetch in VS Code because warming extra sessions was increasing webview memory and GC pressure during navigation. - Remove dropdown background message prefetch so opening the switcher does not start additional session materialization work. - Drop cached session-message-record snapshots when evicting session data so stale derived records do not remain after the raw session cache is cleared. - Add bounded LRU caching for session message record snapshots, with much smaller VS Code limits and a VS Code cap that avoids caching snapshots above 30 messages. - Bound the turn-window model cache in VS Code and avoid caching turn models for sessions above the VS Code message-page size. Chat render-path reductions: - Reuse ChatContainer's already-materialized message records in plan detection instead of adding a second active-session message subscription. - Add a no-op guard when marking session plan availability so repeated detections do not create new Map references and fan out renders. - Add no-op guards for session switcher and dropdown open state updates to avoid unnecessary store updates and renders. - Convert several session-specific hooks to useSyncExternalStore with empty-session no-subscribe behavior so empty IDs do not subscribe to broad store updates. - Remount the chat viewport when the current session changes, isolating per-session viewport and list state. - Change the virtualized message-list fallback to render only a tail window when the virtualizer has not produced rows yet, instead of rendering an entire large history. VS Code layout and header improvements: - Remove the broad useSessions subscription from the VS Code layout header path and subscribe only to the active session title and initial-session existence. - Unmount the compact VS Code session sidebar when the user is in chat view instead of keeping the hidden session list mounted and subscribed. - Compute the latest assistant model and latest context-token usage in a single reverse scan of current-session messages instead of scanning the same list twice. - Remove switcher git-status warmup work so the switcher reads already-loaded branch labels without starting extra background git status requests. Markdown and file-reference safeguards: - Skip expensive syntax highlighting for very large code blocks, with a 200-line cap in VS Code and a softer 1200-line cap in web. - Add an LRU cap to file-reference stat lookups so the cache cannot grow without bound across many rendered messages. - Limit the number of file references annotated per render to 40 in VS Code and 200 in web to prevent large assistant outputs from spawning too many stat checks. - Clear file-link annotations when file-reference mode is disabled so stale attributes and handlers do not remain on previously annotated nodes. Assistant-message action and preview reductions: - Skip preview URL scanning on VS Code, mobile, and mini-chat surfaces so assistant text and tool output are not scanned where the preview action is unavailable. - Skip Save-as-Plan project lookup on VS Code, mini-chat, and mobile surfaces. - Hide Save-as-Plan and Start MultiRun assistant-message actions on VS Code, mini-chat, and mobile surfaces. - Resolve the current session directory on demand for assistant actions instead of subscribing each assistant message to the full session list. Tool and task rendering optimizations: - Prefer finalized task metadata summaries without fetching child-session messages when the summary is already present. - Avoid polling or final-fetching task child sessions once a final metadata summary is available. - Use VS Code-specific task child fetch limits of 30 records for initial, active, and idle fetches. - Parse diff stats by scanning patch text line-by-line instead of splitting large patches into arrays. - Count write-tool lines by scanning content instead of allocating a split array for large files. - Avoid trimming large patch strings just to test whether they contain content. - Memoize diff and write statistics so unchanged tool parts do not recalculate them on every render. VS Code bridge improvements: - Return JSON and text proxy responses through the VS Code bridge as bodyText instead of base64 so the webview avoids synchronous base64 decoding for common API responses. - Keep binary responses on the base64 path while making bodyBase64 optional in the bridge contract. - Strip content-length, content-encoding, and transfer-encoding headers from proxied responses because the bridge reconstructs the Response body. Validation: - bun run type-check - bun run lint - bun run vscode:build
199 lines
5.7 KiB
TypeScript
199 lines
5.7 KiB
TypeScript
declare const acquireVsCodeApi: () => {
|
|
postMessage: (message: unknown) => void;
|
|
getState: () => unknown;
|
|
setState: (state: unknown) => void;
|
|
};
|
|
|
|
interface VSCodeAPI {
|
|
postMessage: (message: unknown) => void;
|
|
}
|
|
|
|
let vscodeApi: VSCodeAPI | null = null;
|
|
|
|
function getVSCodeAPI(): VSCodeAPI {
|
|
if (!vscodeApi) {
|
|
vscodeApi = acquireVsCodeApi();
|
|
}
|
|
return vscodeApi;
|
|
}
|
|
|
|
// Export vscode API for direct use
|
|
export const vscode = {
|
|
postMessage: (message: unknown) => getVSCodeAPI().postMessage(message),
|
|
};
|
|
|
|
interface BridgeRequest {
|
|
id: string;
|
|
type: string;
|
|
payload?: unknown;
|
|
}
|
|
|
|
interface BridgeResponse {
|
|
id: string;
|
|
type: string;
|
|
success: boolean;
|
|
data?: unknown;
|
|
error?: string;
|
|
}
|
|
|
|
const pendingRequests = new Map<string, {
|
|
resolve: (value: unknown) => void;
|
|
reject: (reason: Error) => void;
|
|
timeout?: ReturnType<typeof setTimeout>;
|
|
}>();
|
|
|
|
let requestIdCounter = 0;
|
|
|
|
window.addEventListener('message', (event: MessageEvent<BridgeResponse>) => {
|
|
const response = event.data;
|
|
if (!response || typeof response.id !== 'string') return;
|
|
|
|
const messageId = (response as BridgeResponse & { _msgId?: unknown })._msgId;
|
|
if (typeof messageId === 'string' && messageId.length > 0) {
|
|
getVSCodeAPI().postMessage({ type: 'bridge:ack', _msgId: messageId });
|
|
}
|
|
|
|
const pending = pendingRequests.get(response.id);
|
|
if (pending) {
|
|
pendingRequests.delete(response.id);
|
|
if (pending.timeout) {
|
|
clearTimeout(pending.timeout);
|
|
}
|
|
if (response.success) {
|
|
pending.resolve(response.data);
|
|
} else {
|
|
pending.reject(new Error(response.error || 'Unknown error'));
|
|
}
|
|
}
|
|
});
|
|
|
|
export function sendBridgeMessage<T = unknown>(type: string, payload?: unknown): Promise<T> {
|
|
return sendBridgeMessageWithOptions<T>(type, payload);
|
|
}
|
|
|
|
export function sendBridgeMessageWithOptions<T = unknown>(
|
|
type: string,
|
|
payload?: unknown,
|
|
options?: { timeoutMs?: number }
|
|
): Promise<T> {
|
|
return new Promise((resolve, reject) => {
|
|
const id = `req_${++requestIdCounter}_${Date.now()}`;
|
|
const request: BridgeRequest = { id, type, payload };
|
|
|
|
const pending: {
|
|
resolve: (value: unknown) => void;
|
|
reject: (reason: Error) => void;
|
|
timeout?: ReturnType<typeof setTimeout>;
|
|
} = {
|
|
resolve: resolve as (value: unknown) => void,
|
|
reject,
|
|
};
|
|
pendingRequests.set(id, pending);
|
|
|
|
const timeoutMs = typeof options?.timeoutMs === 'number' ? options.timeoutMs : 30000;
|
|
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
pending.timeout = setTimeout(() => {
|
|
if (pendingRequests.has(id)) {
|
|
pendingRequests.delete(id);
|
|
reject(new Error(`Request ${type} timed out`));
|
|
}
|
|
}, timeoutMs);
|
|
}
|
|
|
|
getVSCodeAPI().postMessage(request);
|
|
});
|
|
}
|
|
|
|
export type ProxiedApiResponse = {
|
|
status: number;
|
|
headers: Record<string, string>;
|
|
bodyBase64?: string;
|
|
bodyText?: string;
|
|
};
|
|
|
|
export async function proxyApiRequest(options: {
|
|
method: string;
|
|
path: string;
|
|
headers?: Record<string, string>;
|
|
bodyBase64?: string;
|
|
}): Promise<ProxiedApiResponse> {
|
|
// Do not impose a bridge-level timeout. Let the original fetch's AbortSignal
|
|
// (or OpenCode server response timing) control the lifecycle.
|
|
return sendBridgeMessageWithOptions<ProxiedApiResponse>('api:proxy', options, { timeoutMs: 0 });
|
|
}
|
|
|
|
export async function proxySessionMessageRequest(options: {
|
|
path: string;
|
|
headers?: Record<string, string>;
|
|
bodyText: string;
|
|
}): Promise<ProxiedApiResponse> {
|
|
// Keep parity with server-side direct forwarder: let extension host control timeout.
|
|
return sendBridgeMessageWithOptions<ProxiedApiResponse>('api:session:message', options, { timeoutMs: 0 });
|
|
}
|
|
|
|
export type ProxiedSseStartResponse = {
|
|
status: number;
|
|
headers: Record<string, string>;
|
|
streamId: string | null;
|
|
error?: string;
|
|
};
|
|
|
|
export async function startSseProxy(options: {
|
|
path: string;
|
|
headers?: Record<string, string>;
|
|
}): Promise<ProxiedSseStartResponse> {
|
|
return sendBridgeMessage<ProxiedSseStartResponse>('api:sse:start', options);
|
|
}
|
|
|
|
export async function stopSseProxy(options: { streamId: string }): Promise<{ stopped: boolean }> {
|
|
return sendBridgeMessage<{ stopped: boolean }>('api:sse:stop', options);
|
|
}
|
|
|
|
export async function executeVSCodeCommand(command: string, args?: unknown[]): Promise<{ result?: unknown }> {
|
|
return sendBridgeMessage<{ result?: unknown }>('vscode:command', { command, args });
|
|
}
|
|
|
|
export async function openVSCodeExternalUrl(url: string): Promise<void> {
|
|
await sendBridgeMessage('vscode:openExternalUrl', { url });
|
|
}
|
|
|
|
type CommandHandler = (payload: unknown) => void;
|
|
const commandHandlers = new Map<string, CommandHandler>();
|
|
|
|
export function onCommand(command: string, handler: CommandHandler): () => void {
|
|
commandHandlers.set(command, handler);
|
|
return () => commandHandlers.delete(command);
|
|
}
|
|
|
|
window.addEventListener('message', (event: MessageEvent) => {
|
|
const message = event.data;
|
|
if (message?.type === 'command' && message.command) {
|
|
const handler = commandHandlers.get(message.command);
|
|
if (handler) {
|
|
handler(message.payload);
|
|
}
|
|
}
|
|
});
|
|
|
|
type ThemeChangePayload =
|
|
| 'light'
|
|
| 'dark'
|
|
| {
|
|
kind?: 'light' | 'dark' | 'high-contrast';
|
|
shikiThemes?: { light?: Record<string, unknown>; dark?: Record<string, unknown> } | null;
|
|
};
|
|
type ThemeChangeHandler = (theme: ThemeChangePayload) => void;
|
|
let themeChangeHandler: ThemeChangeHandler | null = null;
|
|
|
|
export function onThemeChange(handler: ThemeChangeHandler): () => void {
|
|
themeChangeHandler = handler;
|
|
return () => { themeChangeHandler = null; };
|
|
}
|
|
|
|
window.addEventListener('message', (event: MessageEvent) => {
|
|
const message = event.data;
|
|
if (message?.type === 'themeChange' && themeChangeHandler) {
|
|
themeChangeHandler(message.theme);
|
|
}
|
|
});
|