Files
kandev/apps/backend/internal/agentctl/api/workspace.go
T
João Salavisa a1013342a3 feat: Real-time Git Status Integration (#8)
* feat: implement real-time git status integration

- Add workspace tracker to monitor git changes every 2 seconds
- Implement WebSocket streaming for git status updates
- Add database persistence for git status in agent session metadata
- Compute diffs backend-side with additions/deletions per file
- Integrate git status into frontend UI (top bar, files panel, changes panel)
- Display real-time ahead/behind commits and line changes
- Show file diffs with syntax highlighting in task view
- Add retry logic for WebSocket connections during reconciliation
- Fix context lifecycle issue in workspace tracker

* refactor: remove debug and unnecessary logging

- Remove debug logs from workspace tracker
- Remove info logs from WebSocket handlers
- Remove verbose git status update logs
- Keep only essential error logs

* fix: compare git diff against remote branch instead of HEAD

- Changed git diff comparison from HEAD to remote tracking branch
- Now shows all changes including committed files that haven't been pushed
- Files committed locally but not pushed will appear in the diff
- Falls back to HEAD if no remote branch is configured

* fix: prevent stale git status when switching tasks

- Clear git status when switching to a new task
- Only accept git.status WebSocket updates for the current task
- Prevents showing git data from previous task when switching

* refactor: remove unused diff stream client code

- Remove diffConn field from agentctl Client struct
- Remove StreamDiff() method (never called)
- Remove CloseDiffStream() method
- Remove DiffUpdate type from client.go
- All diff data is already included in GitStatusUpdate.Files
- Keep server-side diff endpoint in agentctl for potential future use

* refactor: remove unused diff stream server-side code

- Remove /workspace/diff/stream endpoint from agentctl API
- Remove handleDiffStreamWS handler
- Remove DiffUpdate type from workspace_tracker.go
- Remove DiffSubscriber type
- Remove updateDiff() and getDiff() methods
- Remove SubscribeDiff(), UnsubscribeDiff() methods
- Remove notifyDiffSubscribers() method
- Remove GetCurrentDiff() method
- Remove currentDiff field and diffSubscribers map
- All diff data is already included in GitStatusUpdate.Files

* refactor: remove unused output stream code

- Remove /output and /output/stream endpoints from agentctl API
- Remove handleGetOutput and handleOutputStreamWS handlers
- Delete output.go file entirely
- Remove OutputBuffer and related code (buffer.go deleted)
- Remove outputConn field from agentctl Client
- Remove StreamOutput() and CloseOutputStream() methods
- Remove OutputLine type from client.go
- Remove OutputBufferSize config option
- Update readStderr to just log instead of buffering

Output data flows through ACP stream (StreamUpdates) which includes
all session notifications. The separate output stream was never used.
2026-01-12 23:44:29 +00:00

127 lines
3.1 KiB
Go

package api
import (
"encoding/json"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"go.uber.org/zap"
)
// GitStatusUpdate represents a git status update
type GitStatusUpdate struct {
Timestamp string `json:"timestamp"`
Modified []string `json:"modified"`
Added []string `json:"added"`
Deleted []string `json:"deleted"`
Untracked []string `json:"untracked"`
Renamed []string `json:"renamed"`
Ahead int `json:"ahead"`
Behind int `json:"behind"`
Branch string `json:"branch"`
RemoteBranch string `json:"remote_branch,omitempty"`
Files map[string]FileInfo `json:"files,omitempty"`
}
// FileInfo represents information about a file
type FileInfo struct {
Path string `json:"path"`
Status string `json:"status"` // modified, added, deleted, untracked, renamed
Additions int `json:"additions,omitempty"`
Deletions int `json:"deletions,omitempty"`
OldPath string `json:"old_path,omitempty"` // For renamed files
Diff string `json:"diff,omitempty"`
}
// handleGitStatusStreamWS streams git status updates via WebSocket
func (s *Server) handleGitStatusStreamWS(c *gin.Context) {
conn, err := s.upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
s.logger.Error("WebSocket upgrade failed", zap.Error(err))
return
}
defer conn.Close()
// Subscribe to git status updates
sub := s.procMgr.GetWorkspaceTracker().SubscribeGitStatus()
defer s.procMgr.GetWorkspaceTracker().UnsubscribeGitStatus(sub)
// Handle WebSocket close
closeCh := make(chan struct{})
go func() {
for {
_, _, err := conn.ReadMessage()
if err != nil {
close(closeCh)
return
}
}
}()
// Stream git status updates to client
for {
select {
case update, ok := <-sub:
if !ok {
return
}
data, err := json.Marshal(update)
if err != nil {
s.logger.Error("failed to marshal git status update", zap.Error(err))
continue
}
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
return
}
case <-closeCh:
return
}
}
}
// handleFilesStreamWS streams file listing updates via WebSocket
func (s *Server) handleFilesStreamWS(c *gin.Context) {
conn, err := s.upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
s.logger.Error("WebSocket upgrade failed", zap.Error(err))
return
}
defer conn.Close()
// Subscribe to file updates
sub := s.procMgr.GetWorkspaceTracker().SubscribeFiles()
defer s.procMgr.GetWorkspaceTracker().UnsubscribeFiles(sub)
// Handle WebSocket close
closeCh := make(chan struct{})
go func() {
for {
_, _, err := conn.ReadMessage()
if err != nil {
close(closeCh)
return
}
}
}()
// Stream file updates to client
for {
select {
case update, ok := <-sub:
if !ok {
return
}
data, err := json.Marshal(update)
if err != nil {
s.logger.Error("failed to marshal file update", zap.Error(err))
continue
}
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
return
}
case <-closeCh:
return
}
}
}