feat: agents git watch backend by hugodutka · Pull Request #22565 · coder/coder

Summary

Adds real-time git status watching for workspace agents, so the frontend can show file changes as AI tools edit code.

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. It reflects the current state of the PR.

There's an accompanying frontend PR here.

How the frontend consumes this

  1. User opens a chat. Frontend fetches GET /chats/{chat}/edited-paths to get file paths edited in previous sessions.
  2. Frontend opens a bidirectional WebSocket to GET /workspaceagents/{agent}/git/watch and sends a subscribe message with those paths.
  3. The agent resolves paths to git repo roots, scans worktrees, and streams back changes messages containing dirty files, diffs, and branch info — only deltas since the last update.
  4. As the AI edits files during the session, tool callbacks persist new paths to the DB. The frontend re-fetches edited-paths and sends incremental subscribe messages for any newly appeared paths.
  5. The frontend can send refresh at any time (e.g. after a tool call completes) to trigger an immediate scan instead of waiting for the 30s fallback poll.

Why the backend is shaped this way

Two separate concerns drive the architecture: "what paths were edited" (chat-scoped, persisted) and "what's the current git state of those paths" (agent-scoped, live). This is why there are two endpoints rather than one.

Edited paths live in the DB because the frontend needs them to bootstrap WebSocket subscriptions when reopening a chat. Without persistence, the frontend has no idea what the agent edited in previous sessions. Tool callbacks on write_file and edit_files capture paths on every successful write. Callbacks are failure-tolerant — persistence errors log warnings but never fail the tool call.

Subagent aggregation — a root chat can spawn subagent chats that edit files in the same workspace. The GetChatEditedPathsWithDescendants query joins via root_chat_id so reopening the root chat picks up everything, not just what the root agent directly edited.

Edited paths are a separate endpoint (GET /chats/{chat}/edited-paths) rather than embedded in ChatWithMessages. They're consumed by a different frontend subsystem (the git watcher) with a different refresh cadence, and adding an extra DB query to every getChat call for clients that don't need edited paths would be wasteful.

The git watch WebSocket is agent-scoped because git state belongs to the agent's filesystem, not to any particular chat. This follows the existing coderd-to-agent proxy pattern (containers/watch, listening-ports) and keeps the chat stream unchanged.