diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eacc82..a0779bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,33 @@ format is based on [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] ### Added +- **Symbol extraction.** Captured changes now record the functions/classes they + touched (via VS Code's document symbol provider), so symbol history, the + clickable symbol tags, and symbol-aware search actually work. Also records the + active function/class for each change. +- **Inline history CodeLens.** Unobtrusive lenses above a file ("N changes in + history") and its functions/classes ("k versions"), gated by + `codeHistorian.ui.showInlineHistory`. Click to jump to the file or symbol + history. +- **Undo a restore.** Restoring now offers a one-click Undo via a native + notification (backed by the existing backup mechanism). +- **Native diff actions.** "Compare with current" opens VS Code's diff editor + (file now โ†” before this change); "Open diff" shows the change's unified diff + with syntax highlighting. +- **`codeHistorian.debug` setting** to opt into verbose DEBUG logging. +- DB-layer integration tests (compression, BM25 ranking, bookmarks, retention, + symbol/file lookup) running against sql.js in Node. + +### Fixed +- **Duplicate embeddings.** Changes were embedded before their rows were + inserted, leaving `embedding_id` NULL and causing re-embedding (a duplicate + vector) on every restart. Embedding now happens after the flush. +- **File history** used an absolute path against relative-path records and + returned nothing; it now matches correctly. +- API keys and full settings are no longer written to the output channel, and + the output panel no longer pops open on every settings save. + +### Added (previously) - **Zero-config built-in embeddings.** A new `local` embedding provider (now the default) produces deterministic vectors with no API key, model download, or local server, so semantic search works on first run. diff --git a/README.md b/README.md index 948ce3c..3557d4e 100644 --- a/README.md +++ b/README.md @@ -48,10 +48,17 @@ ### โช **Code Restoration** - Restore any previous version of your code with one click -- Preview changes before restoring +- **One-click Undo** after a restore +- Preview changes, or open them in VS Code's native diff editor + (*Compare with current* / *Open diff*) - Automatic backup creation before restoration - Works seamlessly with your existing git workflow +### ๐Ÿ”Ž **Inline History (CodeLens)** +- *"N changes in history"* above a file and *"k versions"* above each + function/class โ€” click to jump straight to that file's or symbol's history +- Toggle with `codeHistorian.ui.showInlineHistory` + ### ๐Ÿ“Š **Visual Timeline** - Beautiful, modern timeline view of all changes - **Multiple view modes**: Timeline, Cards, or Compact list diff --git a/package.json b/package.json index cd779ea..1f8f4ec 100644 --- a/package.json +++ b/package.json @@ -400,7 +400,12 @@ "codeHistorian.ui.showInlineHistory": { "type": "boolean", "default": true, - "description": "Show inline history decorations in editor" + "description": "Show inline history CodeLens above files and functions" + }, + "codeHistorian.debug": { + "type": "boolean", + "default": false, + "description": "Enable verbose DEBUG logging in the Code Historian output channel" } } }, diff --git a/src/constants.ts b/src/constants.ts index b2268ff..615a692 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -389,6 +389,7 @@ export const WEBVIEW_IDS = { // Event names export const EVENTS = { CHANGE_CAPTURED: 'change:captured', + CHANGES_FLUSHED: 'changes:flushed', SESSION_STARTED: 'session:started', SESSION_ENDED: 'session:ended', SEARCH_COMPLETED: 'search:completed', diff --git a/src/database/metadata.ts b/src/database/metadata.ts index 50b458f..f6fed24 100644 --- a/src/database/metadata.ts +++ b/src/database/metadata.ts @@ -573,6 +573,26 @@ export class MetadataDatabase { return result; } + /** + * Get changes for a specific file by its workspace-relative path (exact match). + */ + getChangesForFile(workspaceId: string, relativePath: string, limit: number = 100): ChangeRecord[] { + if (!this.db) throw new Error('Database not initialized'); + + const results = this.db.exec( + `SELECT * FROM ${TABLES.CHANGES} + WHERE workspace_id = ? AND file_path = ? + ORDER BY timestamp DESC + LIMIT ?`, + [workspaceId, relativePath, limit] + ); + + if (results.length === 0) { + return []; + } + return results[0].values.map(row => this.resultToChangeRecord(results[0].columns, row)); + } + /** * Find the history of a specific symbol (function/class/variable name). * Matches the stored `symbols` JSON array and the searchable text. diff --git a/src/extension.ts b/src/extension.ts index a49c591..ac1fc5f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -12,10 +12,11 @@ import { EmbeddingService } from './services/embedding'; import { SearchEngine } from './services/search'; import { LLMOrchestrator } from './services/llm'; import { RestorationEngine } from './services/restoration'; +import { HistoryCodeLensProvider } from './services/codeLens'; import { ChatParticipant } from './chat/participant'; import { WebviewProvider } from './webview/provider'; import { logger, LogLevel } from './utils/logger'; -import { generateWorkspaceId } from './utils'; +import { generateWorkspaceId, getRelativePath } from './utils'; import { eventEmitter } from './utils/events'; import { secrets } from './services/secrets'; import { LOCAL_EMBEDDING_DIMENSIONS } from './services/localEmbedding'; @@ -36,6 +37,7 @@ let embeddingService: EmbeddingService; let searchEngine: SearchEngine; let llmOrchestrator: LLMOrchestrator; let restorationEngine: RestorationEngine; +let codeLensProvider: HistoryCodeLensProvider | undefined; let _chatParticipant: ChatParticipant; let webviewProvider: WebviewProvider; let currentSession: Session | null = null; @@ -77,8 +79,9 @@ export async function activate(context: vscode.ExtensionContext): Promise context.subscriptions.push(outputChannel); logger.initialize(outputChannel); - // Enable debug logging to help diagnose issues - logger.setLevel(LogLevel.DEBUG); + // Default to INFO; set codeHistorian.debug to surface DEBUG diagnostics. + const debugEnabled = vscode.workspace.getConfiguration(EXTENSION_ID).get('debug', false); + logger.setLevel(debugEnabled ? LogLevel.DEBUG : LogLevel.INFO); logger.info('Activating Code Historian extension'); @@ -228,22 +231,32 @@ async function initializeServices(context: vscode.ExtensionContext): Promise { + eventEmitter.on(EVENTS.CHANGES_FLUSHED, async (changes: ChangeRecord[]) => { try { - // Only process if embedding service is configured if (embeddingService.isConfigured()) { - await embeddingService.processChange(change); - logger.debug(`Generated embedding for change: ${change.id}`); + await embeddingService.processChanges(changes); + // Refresh CodeLens so new history shows up inline. + codeLensProvider?.refresh(); } } catch (error) { - logger.warn(`Failed to generate embedding for change ${change.id}:`, error as Error); + logger.warn('Failed to generate embeddings for flushed changes:', error as Error); // Don't throw - embedding failure shouldn't block capture } }) @@ -306,19 +319,17 @@ function registerCommands(context: vscode.ExtensionContext): void { return; } - const changes = metadataDb.getChanges( - workspaceId, - { - filePatterns: [fileUri.fsPath], - }, - 100 - ); + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || ''; + const relativePath = getRelativePath(fileUri.fsPath, workspaceRoot); + const changes = metadataDb.getChangesForFile(workspaceId, relativePath, 200); if (changes.length === 0) { vscode.window.showInformationMessage('No history found for this file'); return; } + await vscode.commands.executeCommand('workbench.view.extension.codeHistorian'); + // Show results in webview webviewProvider.postMessage({ type: 'searchResults', @@ -415,22 +426,27 @@ function registerCommands(context: vscode.ExtensionContext): void { // Show symbol history command โ€” time-travel a single function/class/variable context.subscriptions.push( - vscode.commands.registerCommand(COMMANDS.SHOW_SYMBOL_HISTORY, async () => { - // Default to the word under the cursor, if any. - const editor = vscode.window.activeTextEditor; - let defaultSymbol = ''; - if (editor) { - const range = editor.document.getWordRangeAtPosition(editor.selection.active); - if (range) { - defaultSymbol = editor.document.getText(range); + vscode.commands.registerCommand(COMMANDS.SHOW_SYMBOL_HISTORY, async (symbolArg?: string) => { + let symbol = typeof symbolArg === 'string' ? symbolArg : undefined; + + // No argument (invoked from palette/context menu): prompt, defaulting to + // the word under the cursor. + if (!symbol) { + const editor = vscode.window.activeTextEditor; + let defaultSymbol = ''; + if (editor) { + const range = editor.document.getWordRangeAtPosition(editor.selection.active); + if (range) { + defaultSymbol = editor.document.getText(range); + } } - } - const symbol = await vscode.window.showInputBox({ - prompt: 'Show history for symbol (function, class, or variable name)', - placeHolder: 'e.g. parseConfig', - value: defaultSymbol, - }); + symbol = await vscode.window.showInputBox({ + prompt: 'Show history for symbol (function, class, or variable name)', + placeHolder: 'e.g. parseConfig', + value: defaultSymbol, + }); + } if (!symbol) { return; @@ -637,6 +653,24 @@ async function handleWebviewMessage(message: WebviewToExtensionMessage): Promise if (result.success) { await webviewProvider.sendToast('success', `Restored ${result.linesRestored} line(s)`); + codeLensProvider?.refresh(); + + // Offer a one-click Undo via a native notification when a backup + // was created. + if (result.backupId) { + const backupId = result.backupId; + void vscode.window + .showInformationMessage(`Restored ${result.linesRestored} line(s).`, 'Undo') + .then(async choice => { + if (choice === 'Undo') { + const undone = await restorationEngine.undoRestoration(backupId); + await webviewProvider.sendToast( + undone ? 'info' : 'error', + undone ? 'Restore undone' : 'Could not undo restore' + ); + } + }); + } } else { await webviewProvider.sendToast('error', `Restoration failed: ${result.error}`); } @@ -647,18 +681,39 @@ async function handleWebviewMessage(message: WebviewToExtensionMessage): Promise break; } + case 'compareWithCurrent': { + // Reuse the restoration preview, which opens VS Code's native diff + // editor (current file โ†” the file as it was before this change). + try { + await restorationEngine.previewRestoration(message.data.changeId); + } catch (error) { + await webviewProvider.sendToast( + 'error', + `Cannot compare: ${(error as Error).message}` + ); + } + break; + } + + case 'openDiff': { + try { + await restorationEngine.openDiff(message.data.changeId); + } catch (error) { + await webviewProvider.sendToast('error', `Cannot open diff: ${(error as Error).message}`); + } + break; + } + case 'getSettings': { await webviewProvider.postMessage({ type: 'settings', data: getCurrentSettings() }); break; } case 'updateSettings': { - logger.info('Received updateSettings from webview:', JSON.stringify(message.data, null, 2)); - logger.show(); // Show the output channel for debugging + // Note: settings payloads can contain API keys, so we don't log them. await updateSettings(message.data); // Send back the confirmed settings from VS Code configuration const confirmedSettings = getCurrentSettings(); - logger.info('Sending back confirmed settings:', JSON.stringify(confirmedSettings, null, 2)); await webviewProvider.postMessage({ type: 'settings', data: confirmedSettings }); await webviewProvider.sendToast('success', 'Settings saved'); break; @@ -1172,8 +1227,6 @@ function getCurrentSettings(): SettingsData { async function updateSettings(settings: Partial): Promise { const config = vscode.workspace.getConfiguration(EXTENSION_ID); - logger.info('updateSettings called with:', JSON.stringify(settings, null, 2)); - // Capture settings if (settings.capture) { if (settings.capture.enabled !== undefined) { @@ -1303,7 +1356,6 @@ async function updateSettings(settings: Partial): Promise { // Using a small delay to ensure VS Code has updated the configuration await new Promise(resolve => setTimeout(resolve, 100)); const llmConfig = getLLMConfig(); - logger.info('LLM config after save:', JSON.stringify(llmConfig)); llmOrchestrator.updateConfig(llmConfig); } @@ -1332,12 +1384,7 @@ async function updateSettings(settings: Partial): Promise { // Wait for VS Code configuration to fully propagate await new Promise(resolve => setTimeout(resolve, 200)); - // Log the final settings after all updates - const finalSettings = getCurrentSettings(); - logger.info( - 'Settings saved successfully. Final settings:', - JSON.stringify(finalSettings, null, 2) - ); + logger.info('Settings saved'); } /** diff --git a/src/services/capture.ts b/src/services/capture.ts index 58d23dd..9878908 100644 --- a/src/services/capture.ts +++ b/src/services/capture.ts @@ -366,6 +366,12 @@ export class CaptureEngine { // Get git info const gitInfo = await this.gitService.getGitInfo(); + // Extract the symbols touched by this change (via the language server). + const { symbols, activeFunction, activeClass } = await this.getChangedSymbols( + document, + diffResult.hunks + ); + // Create change record const change: ChangeRecord = { id: generateId(), @@ -384,12 +390,14 @@ export class CaptureEngine { contentBefore: undefined, // Don't store full content by default contentAfter: undefined, contextLines: this.extractContextLines(contentAfter, diffResult), - symbols: [], // Will be populated by AST parser + symbols, imports: [], + activeFunction, + activeClass, gitBranch: gitInfo.branch, gitCommit: gitInfo.commit, gitAuthor: gitInfo.author, - searchableText: createSearchableText(relativePath, diffResult.diff, []), + searchableText: createSearchableText(relativePath, diffResult.diff, symbols), metadata: this.createMetadata(isAutoSave), }; @@ -506,6 +514,106 @@ export class CaptureEngine { return contextLines.slice(0, 20); // Limit context lines } + /** + * Extract the code symbols (functions, classes, methods, โ€ฆ) that the change + * touched, using VS Code's document symbol provider (the language server that + * is already running for the file). Falls back to an empty list if no + * provider is available or it errors. + */ + private async getChangedSymbols( + document: vscode.TextDocument, + hunks: Array<{ newStart: number; newLines: number }> + ): Promise<{ symbols: string[]; activeFunction?: string; activeClass?: string }> { + try { + const docSymbols = await vscode.commands.executeCommand< + Array + >('vscode.executeDocumentSymbolProvider', document.uri); + + if (!docSymbols || docSymbols.length === 0) { + return { symbols: [] }; + } + + // Changed line ranges (0-based, half-open) from the diff hunks. + const changedRanges = hunks.map(h => ({ + start: Math.max(0, h.newStart - 1), + end: h.newStart - 1 + Math.max(h.newLines, 1), + })); + const overlaps = (range: vscode.Range): boolean => + changedRanges.some(r => range.start.line < r.end && range.end.line >= r.start); + + // Symbol kinds worth recording (skip noisy ones like Variable/Constant). + const keepKinds = new Set([ + vscode.SymbolKind.Function, + vscode.SymbolKind.Method, + vscode.SymbolKind.Constructor, + vscode.SymbolKind.Class, + vscode.SymbolKind.Interface, + vscode.SymbolKind.Enum, + vscode.SymbolKind.Struct, + vscode.SymbolKind.Property, + vscode.SymbolKind.Field, + vscode.SymbolKind.Namespace, + ]); + + const names = new Set(); + let activeFunction: string | undefined; + let activeClass: string | undefined; + // Track the smallest enclosing function/class by line span. + let bestFnSpan = Infinity; + let bestClassSpan = Infinity; + + const visit = ( + sym: vscode.DocumentSymbol | vscode.SymbolInformation, + parent?: string + ): void => { + // DocumentSymbol has `range` + `children`; SymbolInformation has `location`. + const range = + 'range' in sym ? sym.range : (sym as vscode.SymbolInformation).location.range; + const children = 'children' in sym ? sym.children : undefined; + + if (overlaps(range)) { + if (keepKinds.has(sym.kind)) { + names.add(sym.name); + } + const span = range.end.line - range.start.line; + if ( + (sym.kind === vscode.SymbolKind.Function || + sym.kind === vscode.SymbolKind.Method || + sym.kind === vscode.SymbolKind.Constructor) && + span < bestFnSpan + ) { + bestFnSpan = span; + activeFunction = sym.name; + } + if ( + (sym.kind === vscode.SymbolKind.Class || + sym.kind === vscode.SymbolKind.Interface || + sym.kind === vscode.SymbolKind.Struct) && + span < bestClassSpan + ) { + bestClassSpan = span; + activeClass = sym.name; + } + } + + if (children) { + for (const child of children) { + visit(child, sym.name); + } + } + }; + + for (const sym of docSymbols) { + visit(sym); + } + + return { symbols: [...names].slice(0, 25), activeFunction, activeClass }; + } catch (error) { + logger.debug(`Symbol extraction failed for ${document.uri.fsPath}: ${String(error)}`); + return { symbols: [] }; + } + } + /** * Create change metadata */ @@ -549,6 +657,10 @@ export class CaptureEngine { ); } + // Emit only after the rows are durably inserted, so downstream consumers + // (embedding generation) can safely link by change id. + eventEmitter.emit(EVENTS.CHANGES_FLUSHED, changes); + logger.debug(`Flushed ${changes.length} changes to database`); } catch (error) { logger.error('Failed to flush changes', error as Error); diff --git a/src/services/codeLens.ts b/src/services/codeLens.ts new file mode 100644 index 0000000..dc0b754 --- /dev/null +++ b/src/services/codeLens.ts @@ -0,0 +1,128 @@ +/** + * Inline history CodeLens provider. + * + * Adds unobtrusive CodeLenses above a file and its functions/classes: + * "โŸฒ N changes in history" (top of file โ†’ opens the file's timeline) + * "โŸฒ k versions" (above a symbol โ†’ opens that symbol's history) + * + * Gated by the `codeHistorian.ui.showInlineHistory` setting. + */ + +import * as vscode from 'vscode'; +import { MetadataDatabase } from '../database/metadata'; +import { EXTENSION_ID } from '../constants'; +import { getRelativePath } from '../utils'; +import { logger } from '../utils/logger'; + +export class HistoryCodeLensProvider implements vscode.CodeLensProvider { + private _onDidChangeCodeLenses = new vscode.EventEmitter(); + readonly onDidChangeCodeLenses = this._onDidChangeCodeLenses.event; + + constructor( + private readonly database: MetadataDatabase, + private readonly workspaceId: string, + private readonly workspaceRoot: string + ) {} + + /** Ask VS Code to re-query lenses (e.g. after new changes are captured). */ + refresh(): void { + this._onDidChangeCodeLenses.fire(); + } + + async provideCodeLenses( + document: vscode.TextDocument, + token: vscode.CancellationToken + ): Promise { + if (document.uri.scheme !== 'file') { + return []; + } + + const config = vscode.workspace.getConfiguration(EXTENSION_ID); + if (!config.get('ui.showInlineHistory', true)) { + return []; + } + + const relativePath = getRelativePath(document.uri.fsPath, this.workspaceRoot); + + let changes; + try { + changes = this.database.getChangesForFile(this.workspaceId, relativePath, 500); + } catch (error) { + logger.debug(`CodeLens lookup failed for ${relativePath}: ${String(error)}`); + return []; + } + + if (changes.length === 0) { + return []; + } + + const lenses: vscode.CodeLens[] = []; + + // File-level lens at the top of the document. + lenses.push( + new vscode.CodeLens(new vscode.Range(0, 0, 0, 0), { + title: `$(history) ${changes.length} change${changes.length === 1 ? '' : 's'} in history`, + command: 'codeHistorian.showFileHistory', + arguments: [document.uri], + }) + ); + + // Per-symbol lenses. Count changes mentioning each symbol name. + const counts = new Map(); + for (const change of changes) { + for (const symbol of change.symbols) { + counts.set(symbol, (counts.get(symbol) ?? 0) + 1); + } + } + + if (counts.size > 0 && !token.isCancellationRequested) { + try { + const docSymbols = await vscode.commands.executeCommand< + Array + >('vscode.executeDocumentSymbolProvider', document.uri); + + const keepKinds = new Set([ + vscode.SymbolKind.Function, + vscode.SymbolKind.Method, + vscode.SymbolKind.Class, + vscode.SymbolKind.Interface, + vscode.SymbolKind.Struct, + ]); + + const visit = (sym: vscode.DocumentSymbol | vscode.SymbolInformation): void => { + const range = + 'range' in sym ? sym.range : (sym as vscode.SymbolInformation).location.range; + const count = counts.get(sym.name); + if (count && keepKinds.has(sym.kind)) { + lenses.push( + new vscode.CodeLens(range, { + title: `$(history) ${count} version${count === 1 ? '' : 's'}`, + command: 'codeHistorian.showSymbolHistory', + arguments: [sym.name], + }) + ); + } + if ('children' in sym && sym.children) { + for (const child of sym.children) { + visit(child); + } + } + }; + + if (docSymbols) { + for (const sym of docSymbols) { + visit(sym); + } + } + } catch (error) { + logger.debug(`CodeLens symbol resolution failed: ${String(error)}`); + } + } + + return lenses; + } + + dispose(): void { + this._onDidChangeCodeLenses.dispose(); + } +} diff --git a/src/services/restoration.ts b/src/services/restoration.ts index b7dfde8..5bf489a 100644 --- a/src/services/restoration.ts +++ b/src/services/restoration.ts @@ -101,6 +101,7 @@ export class RestorationEngine { filePath, linesRestored: change.linesAdded + change.linesDeleted, backupCreated: backup !== null, + backupId: backup?.id, branchCreated: createdBranch, }; @@ -493,6 +494,35 @@ export class RestorationEngine { ); } + /** + * Open a change's unified diff in the editor (rendered with diff syntax + * highlighting). Works even when the file no longer exists, since it shows the + * stored diff rather than reconstructing file content. + */ + async openDiff(changeId: string): Promise { + const change = this.database.getChange(changeId); + if (!change) { + throw new Error(`Change not found: ${changeId}`); + } + + const tempDir = path.join(this.storagePath, 'temp'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + + const safeName = path.basename(change.filePath).replace(/[^\w.-]/g, '_'); + const tempFile = path.join(tempDir, `${safeName}.${changeId.slice(-8)}.diff`); + const header = + `# ${change.filePath}\n` + + `# ${new Date(change.timestamp).toLocaleString()} ยท ` + + `+${change.linesAdded}/-${change.linesDeleted}\n`; + fs.writeFileSync(tempFile, header + change.diff, 'utf-8'); + + const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(tempFile)); + await vscode.languages.setTextDocumentLanguage(doc, 'diff'); + await vscode.window.showTextDocument(doc, { preview: true }); + } + /** * Batch restore multiple changes */ diff --git a/src/types/index.ts b/src/types/index.ts index c43e6c0..12bc2c3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -220,6 +220,7 @@ export interface RestoreResult { filePath: string; linesRestored: number; backupCreated: boolean; + backupId?: string; // ID of the backup created, for undo branchCreated?: string; error?: string; } diff --git a/src/utils/events.ts b/src/utils/events.ts index f1521b7..36ace79 100644 --- a/src/utils/events.ts +++ b/src/utils/events.ts @@ -10,6 +10,7 @@ type EventCallback = (data: T) => void; interface EventMap { [EVENTS.CHANGE_CAPTURED]: ChangeRecord; + [EVENTS.CHANGES_FLUSHED]: ChangeRecord[]; [EVENTS.SESSION_STARTED]: Session; [EVENTS.SESSION_ENDED]: Session; [EVENTS.SEARCH_COMPLETED]: SearchResult[]; diff --git a/src/webview/types.ts b/src/webview/types.ts index 2d390e2..d894eb5 100644 --- a/src/webview/types.ts +++ b/src/webview/types.ts @@ -28,6 +28,8 @@ export type WebviewToExtensionMessage = | { type: 'search'; data: SearchRequest } | { type: 'restore'; data: RestoreRequest } | { type: 'previewRestore'; data: { changeId: string } } + | { type: 'compareWithCurrent'; data: { changeId: string } } + | { type: 'openDiff'; data: { changeId: string } } | { type: 'getSettings' } | { type: 'updateSettings'; data: Partial } | { type: 'exportHistory'; data: ExportRequest } diff --git a/src/webview/ui/Timeline.tsx b/src/webview/ui/Timeline.tsx index 95ea930..cd34d59 100644 --- a/src/webview/ui/Timeline.tsx +++ b/src/webview/ui/Timeline.tsx @@ -269,6 +269,14 @@ export const Timeline: React.FC = ({ }); }, []); + const handleCompare = useCallback((changeId: string) => { + vscode.postMessage({ type: 'compareWithCurrent', data: { changeId } }); + }, []); + + const handleOpenDiff = useCallback((changeId: string) => { + vscode.postMessage({ type: 'openDiff', data: { changeId } }); + }, []); + if (!data && loading) { return (
@@ -704,6 +712,8 @@ export const Timeline: React.FC = ({ onClose={handleCloseDetails} onOpenFile={handleOpenFile} onRestore={handleRestore} + onCompare={handleCompare} + onOpenDiff={handleOpenDiff} onToggleBookmark={onToggleBookmark} onSymbolClick={onSymbolClick} /> @@ -1119,6 +1129,8 @@ interface ChangeDetailPanelProps { onClose: () => void; onOpenFile: (filePath: string) => void; onRestore: (changeId: string, filePath: string) => void; + onCompare?: (changeId: string) => void; + onOpenDiff?: (changeId: string) => void; onToggleBookmark?: (changeId: string) => void; onSymbolClick?: (symbol: string) => void; } @@ -1131,6 +1143,8 @@ const ChangeDetailPanel: React.FC = ({ onClose, onOpenFile, onRestore, + onCompare, + onOpenDiff, onToggleBookmark, onSymbolClick, }) => { @@ -1177,6 +1191,18 @@ const ChangeDetailPanel: React.FC = ({ Restore + {onCompare && ( + + )} + {onOpenDiff && ( + + )} {onToggleBookmark && (