feat: watch vault file changes

This commit is contained in:
2026-06-28 22:53:27 +08:00
parent 5a74809ab7
commit 1d521251a2
4 changed files with 410 additions and 2 deletions
+33 -2
View File
@@ -20,6 +20,7 @@ import (
"github.com/verstak/verstak-desktop/internal/core/events"
"github.com/verstak/verstak-desktop/internal/core/externalopen"
corefiles "github.com/verstak/verstak-desktop/internal/core/files"
"github.com/verstak/verstak-desktop/internal/core/filewatcher"
"github.com/verstak/verstak-desktop/internal/core/permissions"
"github.com/verstak/verstak-desktop/internal/core/plugin"
"github.com/verstak/verstak-desktop/internal/core/pluginstate"
@@ -60,6 +61,7 @@ type App struct {
workbench *coreworkbench.Router
workspace *workspace.Manager
syncSvc *syncsvc.Service
fileWatcher *filewatcher.Service
debug bool
activityEvents map[string]bool
}
@@ -100,10 +102,12 @@ func NewApp(
workbench: coreworkbench.NewRouter(workbenchPrefsFromSettings(appSettingsMgr)),
workspace: workspaceMgr,
syncSvc: syncService,
fileWatcher: filewatcher.NewService(bus, 0),
debug: debugEnabled,
activityEvents: make(map[string]bool),
}
app.ensureActivityProviderSubscriptions()
app.startFileWatcherForOpenVault()
return app
}
@@ -699,7 +703,11 @@ func (a *App) CreateVault(path string) error {
if a.vault == nil {
return fmt.Errorf("vault service not initialized")
}
return a.vault.CreateVault(path)
if err := a.vault.CreateVault(path); err != nil {
return err
}
a.startFileWatcherForOpenVault()
return nil
}
// OpenVault opens an existing vault at the given path.
@@ -707,7 +715,11 @@ func (a *App) OpenVault(path string) error {
if a.vault == nil {
return fmt.Errorf("vault service not initialized")
}
return a.vault.OpenVault(path)
if err := a.vault.OpenVault(path); err != nil {
return err
}
a.startFileWatcherForOpenVault()
return nil
}
// CloseVault closes the current vault.
@@ -715,6 +727,9 @@ func (a *App) CloseVault() error {
if a.vault == nil {
return fmt.Errorf("vault service not initialized")
}
if a.fileWatcher != nil {
a.fileWatcher.Stop()
}
a.vault.CloseVault()
return nil
}
@@ -1408,9 +1423,25 @@ func (a *App) SetCurrentVault(path string) string {
log.Printf("[api] SetCurrentVault: failed to register workspace capability: %v", err)
}
}
a.startFileWatcherForOpenVault()
return ""
}
func (a *App) startFileWatcherForOpenVault() {
if a == nil || a.vault == nil || a.eventBus == nil {
return
}
if a.vault.GetVaultStatus() != vault.StatusOpen {
return
}
if a.fileWatcher == nil {
a.fileWatcher = filewatcher.NewService(a.eventBus, 0)
}
if err := a.fileWatcher.Start(a.vault.GetVaultPath()); err != nil {
log.Printf("[api] file watcher start failed: %v", err)
}
}
// ─── Workspace API ─────────────────────────────────────────
// ListWorkspaces returns top-level physical workspace folders.
+58
View File
@@ -10,6 +10,7 @@ import (
"path/filepath"
"strings"
"testing"
"time"
"github.com/verstak/verstak-desktop/internal/core/appsettings"
"github.com/verstak/verstak-desktop/internal/core/capability"
@@ -1067,6 +1068,63 @@ func TestSetCurrentVaultInitializesWorkspaceWhenMissingAtStartup(t *testing.T) {
}
}
func TestSetCurrentVaultStartsLiveFileWatcher(t *testing.T) {
tmpDir := t.TempDir()
vaultParent := filepath.Join(tmpDir, "vault-parent")
if err := os.MkdirAll(vaultParent, 0o755); err != nil {
t.Fatal(err)
}
bus := events.NewBus()
vaultService := vault.NewVault(bus)
if err := vaultService.CreateVault(vaultParent); err != nil {
t.Fatalf("CreateVault: %v", err)
}
vaultService.CloseVault()
settings := appsettings.NewManager(filepath.Join(tmpDir, "config.json"))
if err := settings.Load(); err != nil {
t.Fatalf("settings Load: %v", err)
}
received := make(chan events.Event, 4)
bus.Subscribe("file.changed", func(event events.Event) {
received <- event
})
app := &App{
capRegistry: capability.NewRegistry(),
eventBus: bus,
vault: vaultService,
appSettings: settings,
}
if errStr := app.SetCurrentVault(vaultParent); errStr != "" {
t.Fatalf("SetCurrentVault: %s", errStr)
}
t.Cleanup(func() {
if app.fileWatcher != nil {
app.fileWatcher.Stop()
}
})
if err := os.WriteFile(filepath.Join(vaultService.GetVaultPath(), "external.md"), []byte("changed"), 0o644); err != nil {
t.Fatal(err)
}
select {
case event := <-received:
payload, ok := event.Payload.(map[string]interface{})
if !ok {
t.Fatalf("payload type = %T, want map", event.Payload)
}
if payload["path"] != "external.md" || payload["operation"] != "external.create" {
t.Fatalf("event payload = %#v", payload)
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for watcher event")
}
}
func TestWorkspaceAPIUsesTopLevelFoldersAndMetadataSnapshot(t *testing.T) {
app, vaultDir := newFilesTestApp(t, []string{"files.read"})
app.workspace = workspace.NewManager(vaultDir)