Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 120 additions & 31 deletions cmd/agentsview/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -874,43 +874,132 @@ func collectWatchRoots(cfg config.Config) (roots []watchRoot, unwatchedDirs []st
continue
}
for _, d := range cfg.ResolveDirs(def.Type) {
if def.ShallowWatchRootsFunc != nil {
for _, watchDir := range def.ShallowWatchRootsFunc(d) {
if _, err := os.Stat(watchDir); err == nil {
addRoot(d, watchDir, true)
}
}
}
if def.WatchRootsFunc != nil {
watchDirs := def.WatchRootsFunc(d)
if len(watchDirs) == 0 {
unwatchedDirs = append(unwatchedDirs, d)
continue
}
for _, watchDir := range watchDirs {
if _, err := os.Stat(watchDir); err == nil {
addRoot(d, watchDir, def.ShallowWatch)
continue
}
unwatchedDirs = append(unwatchedDirs, d)
}
if providerWatched, providerUnwatched := collectProviderWatchRoots(def, d, addRoot); providerWatched {
unwatchedDirs = append(unwatchedDirs, providerUnwatched...)
continue
}
if len(def.WatchSubdirs) == 0 {
if _, err := os.Stat(d); err == nil {
addRoot(d, d, def.ShallowWatch)
}
continue
fallbackUnwatched := collectLegacyWatchRoots(def, d, addRoot)
unwatchedDirs = append(unwatchedDirs, fallbackUnwatched...)
}
}
return roots, unwatchedDirs
}

func collectProviderWatchRoots(
def parser.AgentDef,
dir string,
addRoot func(dir, root string, shallow bool),
) (bool, []string) {
factory, ok := parser.ProviderFactoryByType(def.Type)
if !ok {
return false, nil
}
provider := factory.NewProvider(parser.ProviderConfig{
Roots: []string{dir},
})
plan, err := provider.WatchPlan(context.Background())
if err != nil || len(plan.Roots) == 0 {
if err != nil && !errors.Is(err, parser.ErrUnsupportedProviderFeature) {
log.Printf("%s provider watch plan: %v", def.Type, err)
}
return false, nil
}
added := false
var addedRoots []watchRoot
var missingRoots []string
for _, providerRoot := range plan.Roots {
root := filepath.Clean(providerRoot.Path)
if root == "" || root == "." {
continue
}
if _, err := os.Stat(root); err == nil {
addRoot(dir, root, !providerRoot.Recursive)
added = true
addedRoots = append(addedRoots, watchRoot{
root: root,
shallow: !providerRoot.Recursive,
})
continue
}
missingRoots = append(missingRoots, root)
}
if !added {
return false, nil
}
// A watch target that does not exist yet but lives under an already-watched
// root needs no separate polling only when the ancestor is recursive or
// when a shallow root can observe creation of the missing root itself. A
// shallow ancestor sees only immediate child creation, so it cannot cover a
// missing nested provider root.
for _, missing := range missingRoots {
if !pathCoveredByAnyWatchRootCreation(missing, addedRoots) {
return true, []string{dir}
}
}
return true, nil
}

// pathCoveredByAnyWatchRootCreation reports whether path is covered by an
// existing watch root strongly enough to observe creation of the missing root.
// Recursive roots cover the whole subtree. Shallow roots only cover direct
// children because fsnotify can report that immediate directory creation, after
// which the next watcher setup can add the provider's deeper watch root.
func pathCoveredByAnyWatchRootCreation(path string, roots []watchRoot) bool {
for _, root := range roots {
if root.shallow {
if filepath.Dir(path) == root.root {
return true
}
for _, sub := range def.WatchSubdirs {
watchDir := filepath.Join(d, sub)
if _, err := os.Stat(watchDir); err == nil {
addRoot(d, watchDir, def.ShallowWatch)
}
continue
}
if path == root.root ||
strings.HasPrefix(path, root.root+string(filepath.Separator)) {
return true
}
}
return false
}

func collectLegacyWatchRoots(
def parser.AgentDef,
dir string,
addRoot func(dir, root string, shallow bool),
) []string {
var unwatchedDirs []string
if def.ShallowWatchRootsFunc != nil {
for _, watchDir := range def.ShallowWatchRootsFunc(dir) {
if _, err := os.Stat(watchDir); err == nil {
addRoot(dir, watchDir, true)
}
}
}
return roots, unwatchedDirs
if def.WatchRootsFunc != nil {
watchDirs := def.WatchRootsFunc(dir)
if len(watchDirs) == 0 {
return append(unwatchedDirs, dir)
}
for _, watchDir := range watchDirs {
if _, err := os.Stat(watchDir); err == nil {
addRoot(dir, watchDir, def.ShallowWatch)
continue
}
unwatchedDirs = append(unwatchedDirs, dir)
}
return unwatchedDirs
}
if len(def.WatchSubdirs) == 0 {
if _, err := os.Stat(dir); err == nil {
addRoot(dir, dir, def.ShallowWatch)
}
return unwatchedDirs
}
for _, sub := range def.WatchSubdirs {
watchDir := filepath.Join(dir, sub)
if _, err := os.Stat(watchDir); err == nil {
addRoot(dir, watchDir, def.ShallowWatch)
}
}
return unwatchedDirs
}

func startPeriodicSync(
Expand Down
25 changes: 25 additions & 0 deletions cmd/agentsview/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,31 @@ func TestCollectWatchRootsHermesSessionsWatchesStateDBParent(t *testing.T) {
assert.Equal(t, []string{sessionsDir}, roots[1].dirs)
}

func TestCollectWatchRootsUsesProviderWatchPlan(t *testing.T) {
root := t.TempDir()
for _, dir := range []string{"brain", "conversations", "implicit"} {
require.NoError(t, os.Mkdir(filepath.Join(root, dir), 0o755), "mkdir %s", dir)
}
cfg := config.Config{
AgentDirs: map[parser.AgentType][]string{
parser.AgentAntigravityCLI: {root},
},
}

roots, unwatchedDirs := collectWatchRoots(cfg)

require.Empty(t, unwatchedDirs, "unwatched dirs before watcher setup")
require.Len(t, roots, 4)
assert.Equal(t, filepath.Join(root, "brain"), roots[0].root)
assert.False(t, roots[0].shallow)
assert.Equal(t, filepath.Join(root, "conversations"), roots[1].root)
assert.True(t, roots[1].shallow)
assert.Equal(t, root, roots[2].root)
assert.True(t, roots[2].shallow, "history.jsonl root should be watched shallowly")
assert.Equal(t, filepath.Join(root, "implicit"), roots[3].root)
assert.True(t, roots[3].shallow)
}

func TestResyncCoversSignals(t *testing.T) {
tests := []struct {
name string
Expand Down
63 changes: 12 additions & 51 deletions internal/parser/antigravity.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,53 +30,6 @@ var antigravityUUIDLikeRE = regexp.MustCompile(
`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`,
)

// DiscoverAntigravitySessions returns one DiscoveredFile per
// conversations/<uuid>.db under the IDE root.
func DiscoverAntigravitySessions(root string) []DiscoveredFile {
if root == "" {
return nil
}
dir := filepath.Join(root, "conversations")
entries, err := os.ReadDir(dir)
if err != nil {
return nil
}
var files []DiscoveredFile
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
if !strings.HasSuffix(name, ".db") {
continue
}
id := strings.TrimSuffix(name, ".db")
if !IsValidSessionID(id) {
continue
}
files = append(files, DiscoveredFile{
Path: filepath.Join(dir, name),
Agent: AgentAntigravity,
})
}
sort.Slice(files, func(i, j int) bool {
return files[i].Path < files[j].Path
})
return files
}

// FindAntigravitySourceFile locates a session DB by id.
func FindAntigravitySourceFile(root, id string) string {
if root == "" || !IsValidSessionID(id) {
return ""
}
p := filepath.Join(root, "conversations", id+".db")
if _, err := os.Stat(p); err == nil {
return p
}
return ""
}

// AntigravityFileInfo returns the effective file info for an IDE
// session .db, combining the main file with its -wal/-shm sidecars,
// the annotations/<id>.pbtxt sidecar, and the brain/<id> artifacts
Expand All @@ -89,21 +42,29 @@ func AntigravityFileInfo(path string) (os.FileInfo, error) {
if err != nil {
return nil, err
}
return antigravityCLICombinedFileInfo(
info,
antigravityIDECompanionPaths(path)...,
), nil
}

func antigravityIDECompanionPaths(path string) []string {
id := strings.TrimSuffix(filepath.Base(path), ".db")
root := filepath.Dir(filepath.Dir(path))
companions := []string{
path + "-wal",
path + "-shm",
filepath.Join(root, "annotations", id+".pbtxt"),
}
companions = append(companions, antigravityBrainCompanions(
return append(companions, antigravityBrainCompanions(
filepath.Join(root, "brain", id),
)...)
return antigravityCLICombinedFileInfo(info, companions...), nil
}

// ParseAntigravitySession parses one IDE session DB.
func ParseAntigravitySession(
// parseSession parses one IDE session DB. It is owned by the
// antigravityProvider; the package-level ParseAntigravitySession
// entrypoint was folded onto the provider.
func (p *antigravityProvider) parseSession(
path, project, machine string,
) (*ParsedSession, []ParsedMessage, []ParsedUsageEvent, error) {
info, err := os.Stat(path)
Expand Down
Loading