mirror of
https://github.com/coder/coder.git
synced 2026-06-03 13:08:25 +00:00
48ab492f49
Adds real-time git status watching for workspace agents, so the frontend
can subscribe over WebSocket and show
git file changes in near real-time.
1. Subscription is scoped to a **chat** via `GET
/api/experimental/chats/{chat}/git/watch`.
2. The workspace agent automatically determines which paths to watch
based on tool calls made by the chat (and its ancestor chats).
3. Workspace agent polls subscribed repo working trees on a 30s
interval, on tools calls, and on explicit `refresh` from the client.
4. Scans are rate-limited to at most once per second.
5. Edited paths are tracked **in-memory** inside the workspace agent.
There is no database persistence — state is lost on agent restart. This
will be addresses in a future PR.
6. Messages sent over WebSocket include a full-repo snapshot (unified
diff, branch, origin). A new message is emitted only when the snapshot
changes.
This PR was implemented with AI with me closely controlling what it's
doing. The code follows a plan file that was updated continuously during
implementation. Here's the file if you'd like to see it:
[project.md](https://gist.github.com/hugodutka/8722cf80c92f8a56555f7bc595b770e2).
It reflects the current state of the PR.
137 lines
3.0 KiB
Go
137 lines
3.0 KiB
Go
package agentgit
|
|
|
|
import (
|
|
"sort"
|
|
"sync"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// PathStore tracks which file paths each chat has touched.
|
|
// It is safe for concurrent use.
|
|
type PathStore struct {
|
|
mu sync.RWMutex
|
|
chatPaths map[uuid.UUID]map[string]struct{}
|
|
subscribers map[uuid.UUID][]chan<- struct{}
|
|
}
|
|
|
|
// NewPathStore creates a new PathStore.
|
|
func NewPathStore() *PathStore {
|
|
return &PathStore{
|
|
chatPaths: make(map[uuid.UUID]map[string]struct{}),
|
|
subscribers: make(map[uuid.UUID][]chan<- struct{}),
|
|
}
|
|
}
|
|
|
|
// AddPaths adds paths to every chat in chatIDs and notifies
|
|
// their subscribers. Zero-value UUIDs are silently skipped.
|
|
func (ps *PathStore) AddPaths(chatIDs []uuid.UUID, paths []string) {
|
|
affected := make([]uuid.UUID, 0, len(chatIDs))
|
|
for _, id := range chatIDs {
|
|
if id != uuid.Nil {
|
|
affected = append(affected, id)
|
|
}
|
|
}
|
|
if len(affected) == 0 {
|
|
return
|
|
}
|
|
|
|
ps.mu.Lock()
|
|
for _, id := range affected {
|
|
m, ok := ps.chatPaths[id]
|
|
if !ok {
|
|
m = make(map[string]struct{})
|
|
ps.chatPaths[id] = m
|
|
}
|
|
for _, p := range paths {
|
|
m[p] = struct{}{}
|
|
}
|
|
}
|
|
ps.mu.Unlock()
|
|
|
|
ps.notifySubscribers(affected)
|
|
}
|
|
|
|
// Notify sends a signal to all subscribers of the given chat IDs
|
|
// without adding any paths. Zero-value UUIDs are silently skipped.
|
|
func (ps *PathStore) Notify(chatIDs []uuid.UUID) {
|
|
affected := make([]uuid.UUID, 0, len(chatIDs))
|
|
for _, id := range chatIDs {
|
|
if id != uuid.Nil {
|
|
affected = append(affected, id)
|
|
}
|
|
}
|
|
if len(affected) == 0 {
|
|
return
|
|
}
|
|
ps.notifySubscribers(affected)
|
|
}
|
|
|
|
// notifySubscribers sends a non-blocking signal to all subscriber
|
|
// channels for the given chat IDs.
|
|
func (ps *PathStore) notifySubscribers(chatIDs []uuid.UUID) {
|
|
ps.mu.RLock()
|
|
toNotify := make([]chan<- struct{}, 0)
|
|
for _, id := range chatIDs {
|
|
toNotify = append(toNotify, ps.subscribers[id]...)
|
|
}
|
|
ps.mu.RUnlock()
|
|
|
|
for _, ch := range toNotify {
|
|
select {
|
|
case ch <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetPaths returns all paths tracked for a chat, deduplicated
|
|
// and sorted lexicographically.
|
|
func (ps *PathStore) GetPaths(chatID uuid.UUID) []string {
|
|
ps.mu.RLock()
|
|
defer ps.mu.RUnlock()
|
|
|
|
m := ps.chatPaths[chatID]
|
|
if len(m) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(m))
|
|
for p := range m {
|
|
out = append(out, p)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// Len returns the number of chat IDs that have tracked paths.
|
|
func (ps *PathStore) Len() int {
|
|
ps.mu.RLock()
|
|
defer ps.mu.RUnlock()
|
|
return len(ps.chatPaths)
|
|
}
|
|
|
|
// Subscribe returns a channel that receives a signal whenever
|
|
// paths change for chatID, along with an unsubscribe function
|
|
// that removes the channel.
|
|
func (ps *PathStore) Subscribe(chatID uuid.UUID) (<-chan struct{}, func()) {
|
|
ch := make(chan struct{}, 1)
|
|
|
|
ps.mu.Lock()
|
|
ps.subscribers[chatID] = append(ps.subscribers[chatID], ch)
|
|
ps.mu.Unlock()
|
|
|
|
unsub := func() {
|
|
ps.mu.Lock()
|
|
defer ps.mu.Unlock()
|
|
subs := ps.subscribers[chatID]
|
|
for i, s := range subs {
|
|
if s == ch {
|
|
ps.subscribers[chatID] = append(subs[:i], subs[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return ch, unsub
|
|
}
|