Skip to content

Commit 72c176f

Browse files
authored
Merge pull request #2 from KirtiJha/claude/symbols-codelens-undo-AAlOm
2 parents 78b749e + 9c1fda7 commit 72c176f

16 files changed

Lines changed: 639 additions & 49 deletions

File tree

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,33 @@ format is based on [Keep a Changelog](https://keepachangelog.com/).
66
## [Unreleased]
77

88
### Added
9+
- **Symbol extraction.** Captured changes now record the functions/classes they
10+
touched (via VS Code's document symbol provider), so symbol history, the
11+
clickable symbol tags, and symbol-aware search actually work. Also records the
12+
active function/class for each change.
13+
- **Inline history CodeLens.** Unobtrusive lenses above a file ("N changes in
14+
history") and its functions/classes ("k versions"), gated by
15+
`codeHistorian.ui.showInlineHistory`. Click to jump to the file or symbol
16+
history.
17+
- **Undo a restore.** Restoring now offers a one-click Undo via a native
18+
notification (backed by the existing backup mechanism).
19+
- **Native diff actions.** "Compare with current" opens VS Code's diff editor
20+
(file now ↔ before this change); "Open diff" shows the change's unified diff
21+
with syntax highlighting.
22+
- **`codeHistorian.debug` setting** to opt into verbose DEBUG logging.
23+
- DB-layer integration tests (compression, BM25 ranking, bookmarks, retention,
24+
symbol/file lookup) running against sql.js in Node.
25+
26+
### Fixed
27+
- **Duplicate embeddings.** Changes were embedded before their rows were
28+
inserted, leaving `embedding_id` NULL and causing re-embedding (a duplicate
29+
vector) on every restart. Embedding now happens after the flush.
30+
- **File history** used an absolute path against relative-path records and
31+
returned nothing; it now matches correctly.
32+
- API keys and full settings are no longer written to the output channel, and
33+
the output panel no longer pops open on every settings save.
34+
35+
### Added (previously)
936
- **Zero-config built-in embeddings.** A new `local` embedding provider (now the
1037
default) produces deterministic vectors with no API key, model download, or
1138
local server, so semantic search works on first run.

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,17 @@
4848

4949
### **Code Restoration**
5050
- Restore any previous version of your code with one click
51-
- Preview changes before restoring
51+
- **One-click Undo** after a restore
52+
- Preview changes, or open them in VS Code's native diff editor
53+
(*Compare with current* / *Open diff*)
5254
- Automatic backup creation before restoration
5355
- Works seamlessly with your existing git workflow
5456

57+
### 🔎 **Inline History (CodeLens)**
58+
- *"N changes in history"* above a file and *"k versions"* above each
59+
function/class — click to jump straight to that file's or symbol's history
60+
- Toggle with `codeHistorian.ui.showInlineHistory`
61+
5562
### 📊 **Visual Timeline**
5663
- Beautiful, modern timeline view of all changes
5764
- **Multiple view modes**: Timeline, Cards, or Compact list

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,12 @@
400400
"codeHistorian.ui.showInlineHistory": {
401401
"type": "boolean",
402402
"default": true,
403-
"description": "Show inline history decorations in editor"
403+
"description": "Show inline history CodeLens above files and functions"
404+
},
405+
"codeHistorian.debug": {
406+
"type": "boolean",
407+
"default": false,
408+
"description": "Enable verbose DEBUG logging in the Code Historian output channel"
404409
}
405410
}
406411
},

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ export const WEBVIEW_IDS = {
389389
// Event names
390390
export const EVENTS = {
391391
CHANGE_CAPTURED: 'change:captured',
392+
CHANGES_FLUSHED: 'changes:flushed',
392393
SESSION_STARTED: 'session:started',
393394
SESSION_ENDED: 'session:ended',
394395
SEARCH_COMPLETED: 'search:completed',

src/database/metadata.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,26 @@ export class MetadataDatabase {
573573
return result;
574574
}
575575

576+
/**
577+
* Get changes for a specific file by its workspace-relative path (exact match).
578+
*/
579+
getChangesForFile(workspaceId: string, relativePath: string, limit: number = 100): ChangeRecord[] {
580+
if (!this.db) throw new Error('Database not initialized');
581+
582+
const results = this.db.exec(
583+
`SELECT * FROM ${TABLES.CHANGES}
584+
WHERE workspace_id = ? AND file_path = ?
585+
ORDER BY timestamp DESC
586+
LIMIT ?`,
587+
[workspaceId, relativePath, limit]
588+
);
589+
590+
if (results.length === 0) {
591+
return [];
592+
}
593+
return results[0].values.map(row => this.resultToChangeRecord(results[0].columns, row));
594+
}
595+
576596
/**
577597
* Find the history of a specific symbol (function/class/variable name).
578598
* Matches the stored `symbols` JSON array and the searchable text.

src/extension.ts

Lines changed: 90 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import { EmbeddingService } from './services/embedding';
1212
import { SearchEngine } from './services/search';
1313
import { LLMOrchestrator } from './services/llm';
1414
import { RestorationEngine } from './services/restoration';
15+
import { HistoryCodeLensProvider } from './services/codeLens';
1516
import { ChatParticipant } from './chat/participant';
1617
import { WebviewProvider } from './webview/provider';
1718
import { logger, LogLevel } from './utils/logger';
18-
import { generateWorkspaceId } from './utils';
19+
import { generateWorkspaceId, getRelativePath } from './utils';
1920
import { eventEmitter } from './utils/events';
2021
import { secrets } from './services/secrets';
2122
import { LOCAL_EMBEDDING_DIMENSIONS } from './services/localEmbedding';
@@ -36,6 +37,7 @@ let embeddingService: EmbeddingService;
3637
let searchEngine: SearchEngine;
3738
let llmOrchestrator: LLMOrchestrator;
3839
let restorationEngine: RestorationEngine;
40+
let codeLensProvider: HistoryCodeLensProvider | undefined;
3941
let _chatParticipant: ChatParticipant;
4042
let webviewProvider: WebviewProvider;
4143
let currentSession: Session | null = null;
@@ -77,8 +79,9 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
7779
context.subscriptions.push(outputChannel);
7880
logger.initialize(outputChannel);
7981

80-
// Enable debug logging to help diagnose issues
81-
logger.setLevel(LogLevel.DEBUG);
82+
// Default to INFO; set codeHistorian.debug to surface DEBUG diagnostics.
83+
const debugEnabled = vscode.workspace.getConfiguration(EXTENSION_ID).get<boolean>('debug', false);
84+
logger.setLevel(debugEnabled ? LogLevel.DEBUG : LogLevel.INFO);
8285

8386
logger.info('Activating Code Historian extension');
8487

@@ -228,22 +231,32 @@ async function initializeServices(context: vscode.ExtensionContext): Promise<voi
228231
// Initialize restoration engine
229232
restorationEngine = new RestorationEngine(workspaceRoot, metadataDb, storagePath);
230233

234+
// Register the inline-history CodeLens provider.
235+
codeLensProvider = new HistoryCodeLensProvider(metadataDb, workspaceId, workspaceRoot);
236+
context.subscriptions.push(
237+
vscode.languages.registerCodeLensProvider({ scheme: 'file' }, codeLensProvider),
238+
codeLensProvider
239+
);
240+
231241
// Initialize capture engine
232242
const captureConfig = getCaptureConfig();
233243
captureEngine = new CaptureEngine(context, workspaceRoot, metadataDb, captureConfig);
234244

235-
// Connect capture engine to embedding service via events
236-
// When a change is captured, process it to generate embeddings
245+
// Connect capture engine to embedding service via events.
246+
// We embed on CHANGES_FLUSHED (after the rows are inserted) rather than on
247+
// CHANGE_CAPTURED. Embedding earlier would call updateEmbeddingId() before the
248+
// row exists, leaving embedding_id NULL and causing the change to be
249+
// re-embedded (a duplicate vector) on every restart.
237250
context.subscriptions.push(
238-
eventEmitter.on(EVENTS.CHANGE_CAPTURED, async change => {
251+
eventEmitter.on(EVENTS.CHANGES_FLUSHED, async (changes: ChangeRecord[]) => {
239252
try {
240-
// Only process if embedding service is configured
241253
if (embeddingService.isConfigured()) {
242-
await embeddingService.processChange(change);
243-
logger.debug(`Generated embedding for change: ${change.id}`);
254+
await embeddingService.processChanges(changes);
255+
// Refresh CodeLens so new history shows up inline.
256+
codeLensProvider?.refresh();
244257
}
245258
} catch (error) {
246-
logger.warn(`Failed to generate embedding for change ${change.id}:`, error as Error);
259+
logger.warn('Failed to generate embeddings for flushed changes:', error as Error);
247260
// Don't throw - embedding failure shouldn't block capture
248261
}
249262
})
@@ -306,19 +319,17 @@ function registerCommands(context: vscode.ExtensionContext): void {
306319
return;
307320
}
308321

309-
const changes = metadataDb.getChanges(
310-
workspaceId,
311-
{
312-
filePatterns: [fileUri.fsPath],
313-
},
314-
100
315-
);
322+
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
323+
const relativePath = getRelativePath(fileUri.fsPath, workspaceRoot);
324+
const changes = metadataDb.getChangesForFile(workspaceId, relativePath, 200);
316325

317326
if (changes.length === 0) {
318327
vscode.window.showInformationMessage('No history found for this file');
319328
return;
320329
}
321330

331+
await vscode.commands.executeCommand('workbench.view.extension.codeHistorian');
332+
322333
// Show results in webview
323334
webviewProvider.postMessage({
324335
type: 'searchResults',
@@ -415,22 +426,27 @@ function registerCommands(context: vscode.ExtensionContext): void {
415426

416427
// Show symbol history command — time-travel a single function/class/variable
417428
context.subscriptions.push(
418-
vscode.commands.registerCommand(COMMANDS.SHOW_SYMBOL_HISTORY, async () => {
419-
// Default to the word under the cursor, if any.
420-
const editor = vscode.window.activeTextEditor;
421-
let defaultSymbol = '';
422-
if (editor) {
423-
const range = editor.document.getWordRangeAtPosition(editor.selection.active);
424-
if (range) {
425-
defaultSymbol = editor.document.getText(range);
429+
vscode.commands.registerCommand(COMMANDS.SHOW_SYMBOL_HISTORY, async (symbolArg?: string) => {
430+
let symbol = typeof symbolArg === 'string' ? symbolArg : undefined;
431+
432+
// No argument (invoked from palette/context menu): prompt, defaulting to
433+
// the word under the cursor.
434+
if (!symbol) {
435+
const editor = vscode.window.activeTextEditor;
436+
let defaultSymbol = '';
437+
if (editor) {
438+
const range = editor.document.getWordRangeAtPosition(editor.selection.active);
439+
if (range) {
440+
defaultSymbol = editor.document.getText(range);
441+
}
426442
}
427-
}
428443

429-
const symbol = await vscode.window.showInputBox({
430-
prompt: 'Show history for symbol (function, class, or variable name)',
431-
placeHolder: 'e.g. parseConfig',
432-
value: defaultSymbol,
433-
});
444+
symbol = await vscode.window.showInputBox({
445+
prompt: 'Show history for symbol (function, class, or variable name)',
446+
placeHolder: 'e.g. parseConfig',
447+
value: defaultSymbol,
448+
});
449+
}
434450

435451
if (!symbol) {
436452
return;
@@ -637,6 +653,24 @@ async function handleWebviewMessage(message: WebviewToExtensionMessage): Promise
637653

638654
if (result.success) {
639655
await webviewProvider.sendToast('success', `Restored ${result.linesRestored} line(s)`);
656+
codeLensProvider?.refresh();
657+
658+
// Offer a one-click Undo via a native notification when a backup
659+
// was created.
660+
if (result.backupId) {
661+
const backupId = result.backupId;
662+
void vscode.window
663+
.showInformationMessage(`Restored ${result.linesRestored} line(s).`, 'Undo')
664+
.then(async choice => {
665+
if (choice === 'Undo') {
666+
const undone = await restorationEngine.undoRestoration(backupId);
667+
await webviewProvider.sendToast(
668+
undone ? 'info' : 'error',
669+
undone ? 'Restore undone' : 'Could not undo restore'
670+
);
671+
}
672+
});
673+
}
640674
} else {
641675
await webviewProvider.sendToast('error', `Restoration failed: ${result.error}`);
642676
}
@@ -647,18 +681,39 @@ async function handleWebviewMessage(message: WebviewToExtensionMessage): Promise
647681
break;
648682
}
649683

684+
case 'compareWithCurrent': {
685+
// Reuse the restoration preview, which opens VS Code's native diff
686+
// editor (current file ↔ the file as it was before this change).
687+
try {
688+
await restorationEngine.previewRestoration(message.data.changeId);
689+
} catch (error) {
690+
await webviewProvider.sendToast(
691+
'error',
692+
`Cannot compare: ${(error as Error).message}`
693+
);
694+
}
695+
break;
696+
}
697+
698+
case 'openDiff': {
699+
try {
700+
await restorationEngine.openDiff(message.data.changeId);
701+
} catch (error) {
702+
await webviewProvider.sendToast('error', `Cannot open diff: ${(error as Error).message}`);
703+
}
704+
break;
705+
}
706+
650707
case 'getSettings': {
651708
await webviewProvider.postMessage({ type: 'settings', data: getCurrentSettings() });
652709
break;
653710
}
654711

655712
case 'updateSettings': {
656-
logger.info('Received updateSettings from webview:', JSON.stringify(message.data, null, 2));
657-
logger.show(); // Show the output channel for debugging
713+
// Note: settings payloads can contain API keys, so we don't log them.
658714
await updateSettings(message.data);
659715
// Send back the confirmed settings from VS Code configuration
660716
const confirmedSettings = getCurrentSettings();
661-
logger.info('Sending back confirmed settings:', JSON.stringify(confirmedSettings, null, 2));
662717
await webviewProvider.postMessage({ type: 'settings', data: confirmedSettings });
663718
await webviewProvider.sendToast('success', 'Settings saved');
664719
break;
@@ -1172,8 +1227,6 @@ function getCurrentSettings(): SettingsData {
11721227
async function updateSettings(settings: Partial<SettingsData>): Promise<void> {
11731228
const config = vscode.workspace.getConfiguration(EXTENSION_ID);
11741229

1175-
logger.info('updateSettings called with:', JSON.stringify(settings, null, 2));
1176-
11771230
// Capture settings
11781231
if (settings.capture) {
11791232
if (settings.capture.enabled !== undefined) {
@@ -1303,7 +1356,6 @@ async function updateSettings(settings: Partial<SettingsData>): Promise<void> {
13031356
// Using a small delay to ensure VS Code has updated the configuration
13041357
await new Promise(resolve => setTimeout(resolve, 100));
13051358
const llmConfig = getLLMConfig();
1306-
logger.info('LLM config after save:', JSON.stringify(llmConfig));
13071359
llmOrchestrator.updateConfig(llmConfig);
13081360
}
13091361

@@ -1332,12 +1384,7 @@ async function updateSettings(settings: Partial<SettingsData>): Promise<void> {
13321384
// Wait for VS Code configuration to fully propagate
13331385
await new Promise(resolve => setTimeout(resolve, 200));
13341386

1335-
// Log the final settings after all updates
1336-
const finalSettings = getCurrentSettings();
1337-
logger.info(
1338-
'Settings saved successfully. Final settings:',
1339-
JSON.stringify(finalSettings, null, 2)
1340-
);
1387+
logger.info('Settings saved');
13411388
}
13421389

13431390
/**

0 commit comments

Comments
 (0)