mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 00:09:30 +01:00
cache llm generated thinking headers (#304619)
* cache llm generated titles as fallback for rerendering * store llm generated headers so cli will not re-generate * address some comments, scope to non-local sessions
This commit is contained in:
@@ -116,7 +116,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
|
||||
* Extracts subagent info (description, agentName, prompt) from a tool invocation.
|
||||
*/
|
||||
private static extractSubagentInfo(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { description: string; agentName: string | undefined; prompt: string | undefined; modelName: string | undefined } {
|
||||
const defaultDescription = localize('chat.subagent.defaultDescription', 'Running subagent...');
|
||||
const defaultDescription = localize('chat.subagent.defaultDescription', 'Running subagent');
|
||||
|
||||
// Only parent subagent tools contain the full subagent info
|
||||
if (!ChatSubagentContentPart.isParentSubagentTool(toolInvocation)) {
|
||||
|
||||
@@ -34,9 +34,11 @@ import { IChatMarkdownAnchorService } from './chatMarkdownAnchorService.js';
|
||||
import { ChatMessageRole, ILanguageModelsService } from '../../../common/languageModels.js';
|
||||
import './media/chatThinkingContent.css';
|
||||
import { IHoverService } from '../../../../../../platform/hover/browser/hover.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js';
|
||||
import { extractImagesFromToolInvocationOutputDetails } from '../../../common/chatImageExtraction.js';
|
||||
import { IChatCollapsibleIODataPart } from './chatToolInputOutputContentPart.js';
|
||||
import { ChatThinkingExternalResourceWidget } from './chatThinkingExternalResourcesWidget.js';
|
||||
import { LocalChatSessionUri, chatSessionResourceToId } from '../../../common/model/chatUri.js';
|
||||
import { IEditSessionDiffStats } from '../../../common/editing/chatEditingService.js';
|
||||
|
||||
|
||||
@@ -140,6 +142,10 @@ interface ILazyThinkingItem {
|
||||
type ILazyItem = ILazyToolItem | ILazyThinkingItem;
|
||||
const THINKING_SCROLL_MAX_HEIGHT = 200;
|
||||
|
||||
const TITLE_CACHE_STORAGE_KEY = 'chat.thinkingTitleCache';
|
||||
const TITLE_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
const TITLE_CACHE_MAX_ENTRIES = 1000;
|
||||
|
||||
const enum WorkingMessageCategory {
|
||||
Thinking = 'thinking',
|
||||
Terminal = 'terminal',
|
||||
@@ -284,6 +290,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
@IChatMarkdownAnchorService private readonly chatMarkdownAnchorService: IChatMarkdownAnchorService,
|
||||
@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,
|
||||
@IHoverService hoverService: IHoverService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
) {
|
||||
const initialText = extractTextFromPart(content);
|
||||
const extractedTitle = extractTitleFromThinkingContent(initialText)
|
||||
@@ -897,6 +904,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
return;
|
||||
}
|
||||
|
||||
// Reuse any existing generated title from tool invocations or thinking parts.
|
||||
const existingTitle = this.toolInvocations.find(t => t.generatedTitle)?.generatedTitle
|
||||
?? this.allThinkingParts.find(t => t.generatedTitle)?.generatedTitle;
|
||||
if (existingTitle) {
|
||||
@@ -907,6 +915,27 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
return;
|
||||
}
|
||||
|
||||
// Only check the persisted cache when re-rendering
|
||||
// (all tool invocations are serialized), not during live streaming.
|
||||
const allSerialized = this.toolInvocations.length > 0
|
||||
&& this.toolInvocations.every(t => t.kind === 'toolInvocationSerialized');
|
||||
if (allSerialized) {
|
||||
// Fallback: check the persisted title cache using the last tool call (non-local sessions only)
|
||||
if (!LocalChatSessionUri.isLocalSession(this.element.sessionResource)) {
|
||||
const lastToolInvocation = this.toolInvocations[this.toolInvocations.length - 1];
|
||||
if (lastToolInvocation) {
|
||||
const cachedTitle = this.getCachedTitle(lastToolInvocation.toolCallId);
|
||||
if (cachedTitle) {
|
||||
this.currentTitle = cachedTitle;
|
||||
this.content.generatedTitle = cachedTitle;
|
||||
this.setGeneratedTitleOnAllParts(cachedTitle);
|
||||
this.setFinalizedTitle(cachedTitle);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// case where we only have one item (tool or edit) in the thinking container and no thinking parts, we want to move it back to its original position
|
||||
if (this.toolInvocationCount === 1 && this.hookCount === 0 && this.currentThinkingValue.trim() === '') {
|
||||
// If singleItemInfo wasn't set (item was lazy/deferred), materialize it now
|
||||
@@ -966,6 +995,55 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
}
|
||||
}
|
||||
|
||||
private loadTitleCache(): Record<string, { title: string; storedAt: number }> {
|
||||
return this.storageService.getObject<Record<string, { title: string; storedAt: number }>>(TITLE_CACHE_STORAGE_KEY, StorageScope.PROFILE) ?? {};
|
||||
}
|
||||
|
||||
private saveTitleCache(cache: Record<string, { title: string; storedAt: number }>): void {
|
||||
if (Object.keys(cache).length === 0) {
|
||||
this.storageService.remove(TITLE_CACHE_STORAGE_KEY, StorageScope.PROFILE);
|
||||
} else {
|
||||
this.storageService.store(TITLE_CACHE_STORAGE_KEY, JSON.stringify(cache), StorageScope.PROFILE, StorageTarget.MACHINE);
|
||||
}
|
||||
}
|
||||
|
||||
private getTitleCacheKey(toolCallId: string): string {
|
||||
return `${chatSessionResourceToId(this.element.sessionResource)}:${toolCallId}`;
|
||||
}
|
||||
|
||||
private getCachedTitle(toolCallId: string): string | undefined {
|
||||
const entry = this.loadTitleCache()[this.getTitleCacheKey(toolCallId)];
|
||||
if (!entry || (Date.now() - entry.storedAt) > TITLE_CACHE_TTL_MS) {
|
||||
return undefined;
|
||||
}
|
||||
return entry.title;
|
||||
}
|
||||
|
||||
private setCachedTitle(toolCallId: string, title: string): void {
|
||||
const cache = this.loadTitleCache();
|
||||
const now = Date.now();
|
||||
|
||||
// Evict expired entries on write
|
||||
for (const key of Object.keys(cache)) {
|
||||
if ((now - cache[key].storedAt) > TITLE_CACHE_TTL_MS) {
|
||||
delete cache[key];
|
||||
}
|
||||
}
|
||||
|
||||
cache[this.getTitleCacheKey(toolCallId)] = { title, storedAt: now };
|
||||
|
||||
// Cap size by dropping oldest entries
|
||||
const keys = Object.keys(cache);
|
||||
if (keys.length > TITLE_CACHE_MAX_ENTRIES) {
|
||||
const sorted = keys.sort((a, b) => cache[a].storedAt - cache[b].storedAt);
|
||||
for (let i = 0; i < sorted.length - TITLE_CACHE_MAX_ENTRIES; i++) {
|
||||
delete cache[sorted[i]];
|
||||
}
|
||||
}
|
||||
|
||||
this.saveTitleCache(cache);
|
||||
}
|
||||
|
||||
private async generateTitleViaLLM(): Promise<void> {
|
||||
const cts = new CancellationTokenSource();
|
||||
const timeout = setTimeout(() => cts.cancel(), 5000);
|
||||
@@ -1123,6 +1201,15 @@ ${this.hookCount > 0 ? `EXAMPLES WITH BLOCKED CONTENT (from hooks):
|
||||
this.setFinalizedTitle(generatedTitle);
|
||||
this.content.generatedTitle = generatedTitle;
|
||||
this.setGeneratedTitleOnAllParts(generatedTitle);
|
||||
|
||||
// Persist to storage for non-local sessions only
|
||||
if (!LocalChatSessionUri.isLocalSession(this.element.sessionResource)) {
|
||||
const lastTool = this.toolInvocations[this.toolInvocations.length - 1];
|
||||
if (lastTool) {
|
||||
this.setCachedTitle(lastTool.toolCallId, generatedTitle);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -144,7 +144,7 @@ suite('ChatSubagentContentPart', () => {
|
||||
prompt: 'Test prompt'
|
||||
},
|
||||
originMessage: undefined,
|
||||
invocationMessage: options.invocationMessage ?? 'Running subagent...',
|
||||
invocationMessage: options.invocationMessage ?? 'Running subagent',
|
||||
pastTenseMessage: undefined,
|
||||
source: ToolDataSource.Internal,
|
||||
toolId: options.toolId ?? RunSubagentTool.Id,
|
||||
@@ -180,7 +180,7 @@ suite('ChatSubagentContentPart', () => {
|
||||
result: 'Test result text'
|
||||
},
|
||||
originMessage: undefined,
|
||||
invocationMessage: 'Running subagent...',
|
||||
invocationMessage: 'Running subagent',
|
||||
pastTenseMessage: undefined,
|
||||
resultDetails: undefined,
|
||||
isConfirmed: { type: ToolConfirmKind.ConfirmationNotNeeded },
|
||||
|
||||
Reference in New Issue
Block a user