This commit is contained in:
Arjun 2026-02-25 08:56:20 +05:30
parent 3cdcafdf97
commit 3ccdbb614c
6 changed files with 68 additions and 5 deletions

View file

@ -106,6 +106,18 @@ let watcher: FSWatcher | null = null;
const changeQueue = new Set<string>(); const changeQueue = new Set<string>();
let debounceTimer: ReturnType<typeof setTimeout> | null = null; let debounceTimer: ReturnType<typeof setTimeout> | null = null;
/**
* Emit knowledge commit event to all renderer windows
*/
function emitKnowledgeCommitEvent(): void {
const windows = BrowserWindow.getAllWindows();
for (const win of windows) {
if (!win.isDestroyed() && win.webContents) {
win.webContents.send('knowledge:didCommit', {});
}
}
}
/** /**
* Emit workspace change event to all renderer windows * Emit workspace change event to all renderer windows
*/ */
@ -284,6 +296,9 @@ export function stopServicesWatcher(): void {
* Add new handlers here as you add channels to IPCChannels * Add new handlers here as you add channels to IPCChannels
*/ */
export function setupIpcHandlers() { export function setupIpcHandlers() {
// Forward knowledge commit events to renderer for panel refresh
versionHistory.onCommit(() => emitKnowledgeCommitEvent());
registerIpcHandlers({ registerIpcHandlers({
'app:getVersions': async () => { 'app:getVersions': async () => {
// args is null for this channel (no request payload) // args is null for this channel (no request payload)

View file

@ -1,6 +1,5 @@
import * as React from 'react'
import { useEffect, useState, useCallback } from 'react' import { useEffect, useState, useCallback } from 'react'
import { X, Lock } from 'lucide-react' import { X, Clock } from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@ -57,6 +56,13 @@ export function VersionHistoryPanel({
loadHistory() loadHistory()
}, [loadHistory]) }, [loadHistory])
// Refresh when new commits land
useEffect(() => {
return window.ipc.on('knowledge:didCommit', () => {
loadHistory()
})
}, [loadHistory])
const handleSelectCommit = useCallback(async (oid: string, isLatest: boolean) => { const handleSelectCommit = useCallback(async (oid: string, isLatest: boolean) => {
if (isLatest) { if (isLatest) {
setSelectedOid(null) setSelectedOid(null)
@ -135,7 +141,7 @@ export function VersionHistoryPanel({
> >
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{!isLatest && ( {!isLatest && (
<Lock className="h-3 w-3 text-muted-foreground shrink-0" /> <Clock className="h-3 w-3 text-muted-foreground shrink-0" />
)} )}
<span className="text-sm text-foreground"> <span className="text-sm text-foreground">
{date} &middot; {time} {date} &middot; {time}

View file

@ -96,4 +96,4 @@ ensureWelcomeFile();
// Initialize version history repo (async, fire-and-forget on startup) // Initialize version history repo (async, fire-and-forget on startup)
import('../knowledge/version_history.js').then(m => m.initRepo()).catch(err => { import('../knowledge/version_history.js').then(m => m.initRepo()).catch(err => {
console.error('[VersionHistory] Failed to init repo:', err); console.error('[VersionHistory] Failed to init repo:', err);
}); });

View file

@ -8,4 +8,4 @@ export * as watcher from './workspace/watcher.js';
export { initConfigs } from './config/initConfigs.js'; export { initConfigs } from './config/initConfigs.js';
// Knowledge version history // Knowledge version history
export * as versionHistory from './knowledge/version_history.js'; export * as versionHistory from './knowledge/version_history.js';

View file

@ -5,6 +5,21 @@ import { WorkDir } from '../config/config.js';
const KNOWLEDGE_DIR = path.join(WorkDir, 'knowledge'); const KNOWLEDGE_DIR = path.join(WorkDir, 'knowledge');
// Simple promise-based mutex to serialize commits
let commitLock: Promise<void> = Promise.resolve();
// Commit listeners for notifying other layers (e.g. renderer refresh)
type CommitListener = () => void;
const commitListeners: CommitListener[] = [];
export function onCommit(listener: CommitListener): () => void {
commitListeners.push(listener);
return () => {
const idx = commitListeners.indexOf(listener);
if (idx >= 0) commitListeners.splice(idx, 1);
};
}
/** /**
* Initialize a git repo in the knowledge directory if one doesn't exist. * Initialize a git repo in the knowledge directory if one doesn't exist.
* Stages all existing .md files and makes an initial commit. * Stages all existing .md files and makes an initial commit.
@ -66,8 +81,22 @@ function getAllMdFiles(baseDir: string, relDir: string): string[] {
/** /**
* Stage all changes to .md files and commit. No-op if nothing changed. * Stage all changes to .md files and commit. No-op if nothing changed.
* Serialized via a promise lock to prevent concurrent git index corruption.
*/ */
export async function commitAll(message: string, authorName: string): Promise<void> { export async function commitAll(message: string, authorName: string): Promise<void> {
const prev = commitLock;
let resolve: () => void;
commitLock = new Promise(r => { resolve = r; });
await prev;
try {
await commitAllInner(message, authorName);
} finally {
resolve!();
}
}
async function commitAllInner(message: string, authorName: string): Promise<void> {
const matrix = await git.statusMatrix({ fs, dir: KNOWLEDGE_DIR }); const matrix = await git.statusMatrix({ fs, dir: KNOWLEDGE_DIR });
let hasChanges = false; let hasChanges = false;
@ -98,6 +127,10 @@ export async function commitAll(message: string, authorName: string): Promise<vo
message, message,
author: { name: authorName, email: 'local' }, author: { name: authorName, email: 'local' },
}); });
for (const listener of commitListeners) {
try { listener(); } catch { /* ignore */ }
}
} }
export interface CommitInfo { export interface CommitInfo {
@ -107,9 +140,12 @@ export interface CommitInfo {
author: string; author: string;
} }
const MAX_FILE_HISTORY = 50;
/** /**
* Get commit history for a specific file. * Get commit history for a specific file.
* Returns commits where the file content changed, most recent first. * Returns commits where the file content changed, most recent first.
* Capped at MAX_FILE_HISTORY entries.
*/ */
export async function getFileHistory(knowledgeRelPath: string): Promise<CommitInfo[]> { export async function getFileHistory(knowledgeRelPath: string): Promise<CommitInfo[]> {
// Normalize path separators for git (always forward slashes) // Normalize path separators for git (always forward slashes)
@ -128,6 +164,8 @@ export async function getFileHistory(knowledgeRelPath: string): Promise<CommitIn
// Walk through commits and check if file changed between consecutive commits // Walk through commits and check if file changed between consecutive commits
for (let i = 0; i < commits.length; i++) { for (let i = 0; i < commits.length; i++) {
if (result.length >= MAX_FILE_HISTORY) break;
const commit = commits[i]!; const commit = commits[i]!;
const parentCommit = commits[i + 1]; // undefined for the first (oldest) commit const parentCommit = commits[i + 1]; // undefined for the first (oldest) commit

View file

@ -417,6 +417,10 @@ const ipcSchemas = {
req: z.object({ path: RelPath, oid: z.string() }), req: z.object({ path: RelPath, oid: z.string() }),
res: z.object({ ok: z.literal(true) }), res: z.object({ ok: z.literal(true) }),
}, },
'knowledge:didCommit': {
req: z.object({}),
res: z.null(),
},
// Search channels // Search channels
'search:query': { 'search:query': {
req: z.object({ req: z.object({