mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-13 17:22:37 +02:00
fixes
This commit is contained in:
parent
3cdcafdf97
commit
3ccdbb614c
6 changed files with 68 additions and 5 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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} · {time}
|
{date} · {time}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue