diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index 626f2d01..4d272275 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -106,6 +106,18 @@ let watcher: FSWatcher | null = null; const changeQueue = new Set(); let debounceTimer: ReturnType | 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 */ @@ -284,6 +296,9 @@ export function stopServicesWatcher(): void { * Add new handlers here as you add channels to IPCChannels */ export function setupIpcHandlers() { + // Forward knowledge commit events to renderer for panel refresh + versionHistory.onCommit(() => emitKnowledgeCommitEvent()); + registerIpcHandlers({ 'app:getVersions': async () => { // args is null for this channel (no request payload) diff --git a/apps/x/apps/renderer/src/components/version-history-panel.tsx b/apps/x/apps/renderer/src/components/version-history-panel.tsx index 980448c8..2b942c90 100644 --- a/apps/x/apps/renderer/src/components/version-history-panel.tsx +++ b/apps/x/apps/renderer/src/components/version-history-panel.tsx @@ -1,6 +1,5 @@ -import * as React 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 { Button } from '@/components/ui/button' @@ -57,6 +56,13 @@ export function VersionHistoryPanel({ loadHistory() }, [loadHistory]) + // Refresh when new commits land + useEffect(() => { + return window.ipc.on('knowledge:didCommit', () => { + loadHistory() + }) + }, [loadHistory]) + const handleSelectCommit = useCallback(async (oid: string, isLatest: boolean) => { if (isLatest) { setSelectedOid(null) @@ -135,7 +141,7 @@ export function VersionHistoryPanel({ >
{!isLatest && ( - + )} {date} · {time} diff --git a/apps/x/packages/core/src/config/config.ts b/apps/x/packages/core/src/config/config.ts index 852d430c..4a91e101 100644 --- a/apps/x/packages/core/src/config/config.ts +++ b/apps/x/packages/core/src/config/config.ts @@ -96,4 +96,4 @@ ensureWelcomeFile(); // Initialize version history repo (async, fire-and-forget on startup) import('../knowledge/version_history.js').then(m => m.initRepo()).catch(err => { console.error('[VersionHistory] Failed to init repo:', err); -}); \ No newline at end of file +}); diff --git a/apps/x/packages/core/src/index.ts b/apps/x/packages/core/src/index.ts index c4611a78..0eab08e3 100644 --- a/apps/x/packages/core/src/index.ts +++ b/apps/x/packages/core/src/index.ts @@ -8,4 +8,4 @@ export * as watcher from './workspace/watcher.js'; export { initConfigs } from './config/initConfigs.js'; // Knowledge version history -export * as versionHistory from './knowledge/version_history.js'; \ No newline at end of file +export * as versionHistory from './knowledge/version_history.js'; diff --git a/apps/x/packages/core/src/knowledge/version_history.ts b/apps/x/packages/core/src/knowledge/version_history.ts index 79de5655..a6504f67 100644 --- a/apps/x/packages/core/src/knowledge/version_history.ts +++ b/apps/x/packages/core/src/knowledge/version_history.ts @@ -5,6 +5,21 @@ import { WorkDir } from '../config/config.js'; const KNOWLEDGE_DIR = path.join(WorkDir, 'knowledge'); +// Simple promise-based mutex to serialize commits +let commitLock: Promise = 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. * 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. + * Serialized via a promise lock to prevent concurrent git index corruption. */ export async function commitAll(message: string, authorName: string): Promise { + 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 { const matrix = await git.statusMatrix({ fs, dir: KNOWLEDGE_DIR }); let hasChanges = false; @@ -98,6 +127,10 @@ export async function commitAll(message: string, authorName: string): Promise { // Normalize path separators for git (always forward slashes) @@ -128,6 +164,8 @@ export async function getFileHistory(knowledgeRelPath: string): Promise= MAX_FILE_HISTORY) break; + const commit = commits[i]!; const parentCommit = commits[i + 1]; // undefined for the first (oldest) commit diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 9a3f9aed..b5803ffc 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -417,6 +417,10 @@ const ipcSchemas = { req: z.object({ path: RelPath, oid: z.string() }), res: z.object({ ok: z.literal(true) }), }, + 'knowledge:didCommit': { + req: z.object({}), + res: z.null(), + }, // Search channels 'search:query': { req: z.object({