## Summary
Refactors the admin-only "Configure Agents" dialog into a unified
**Settings** dialog accessible to all users via a gear icon in the
sidebar.
### What changed
- **Settings gear in sidebar**: A gear icon now appears in the
bottom-left of the sidebar (next to the user avatar dropdown). Clicking
it opens the Settings dialog. This replaces the admin-only "Admin"
button that was in the top toolbar.
- **Custom Prompt tab** (all users): A new "Custom Prompt" tab is always
visible in the dialog. Users can write personal instructions that are
applied to all their new chats (stored per-user via the
`/api/experimental/chats/config/user-prompt` endpoint).
- **Admin tabs remain gated**: The Providers, Models, and Behavior
(system prompt) tabs only appear for admin users, preserving the
existing RBAC model.
- **API + query hooks**: Added `getUserChatCustomPrompt` /
`updateUserChatCustomPrompt` methods to the TypeScript API client and
corresponding React Query hooks.
### Files changed
| File | Change |
|------|--------|
| `site/src/api/api.ts` | Added GET/PUT methods for user custom prompt |
| `site/src/api/queries/chats.ts` | Added query/mutation hooks for user
custom prompt |
| `site/src/pages/AgentsPage/ConfigureAgentsDialog.tsx` | Added "Custom
Prompt" tab, renamed to "Settings" |
| `site/src/pages/AgentsPage/AgentsSidebar.tsx` | Added settings gear
button next to user dropdown |
| `site/src/pages/AgentsPage/AgentsPageView.tsx` | Removed "Admin"
button, pass `onOpenSettings` to sidebar |
| `site/src/pages/AgentsPage/AgentsPage.tsx` | Wired up user prompt
state, removed admin-only guard on dialog |
| `*.stories.tsx` | Updated to match new prop interfaces |
## Description
When a workspace name is pre-filled via the `?name=` URL parameter
(embed links), the Formik form did not mark the name field as "touched".
This meant that Yup validation errors (e.g., name too long) were hidden
from the user, and the form would submit to the server, which returned a
generic "Validation failed" error banner instead of a clear inline
message.
## Fix
Include `name` in `initialTouched` when `defaultName` is provided from
the URL, so validation errors display inline immediately — matching the
behavior of manually typed names.
## Changes
- `site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx`:
Modified `initialTouched` to include `{ name: true }` when `defaultName`
is set via URL parameter
Fixes#22346
---------
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: Charlie Voiselle <464492+angrycub@users.noreply.github.com>
- Adds `_API_BASE_URL` to `CODER_EXTERNAL_AUTH_CONFIG_`
- Extracts and refactors existing GitHub PR sync logic to new packages
`coderd/gitsync` and `coderd/externalauth/gitprovider`
- Associated wiring and tests
Created using Opus 4.6
## Problem
When a user navigates away from a chat and its queued messages are
processed server-side, switching back shows stale queued messages until
a hard page refresh. The issue is purely frontend state — the backend is
correct.
### Root cause
Three things conspire to cause the bug:
1. **Stale React Query cache** — the `chatKey(chatId)` cache entry
retains the old `queued_messages` from the last fetch. When the user is
on a different chat, no refetch or WebSocket updates the cache for the
inactive chat.
2. **One-shot hydration guard** — `queuedMessagesHydratedChatIDRef`
blocks all REST-sourced re-hydration after the first hydration for a
given chat ID. This was designed to prevent a stale REST refetch from
overwriting a fresher `queue_update` from the WebSocket, but it also
blocks the corrected data that arrives when the query actually refetches
from the server.
3. **No unsolicited `queue_update`** — the WebSocket only sends
`queue_update` events when the queue changes. If the queue was already
drained before the WebSocket connected, no event is ever sent, so the
stale data persists.
## Fix
Add a `wsQueueUpdateReceivedRef` flag that tracks whether the WebSocket
has delivered a `queue_update` for the current chat. The hydration guard
now only blocks REST re-hydration **after** a `queue_update` has been
received (since the stream is authoritative at that point). Before any
`queue_update` arrives, REST refetches are allowed through to correct
stale cached data.
The flag is reset on chat switch alongside the existing hydration guard
reset.
## Changes
- **`ChatContext.ts`**: Add `wsQueueUpdateReceivedRef`, update hydration
guard condition, set flag on `queue_update` events, reset on chat
switch.
- **`ChatContext.test.tsx`**: Add test covering the exact scenario —
stale cached queued messages are corrected by a REST refetch when no
`queue_update` has arrived.
Removes the auto-open/close behavior that would force the right-side
panel open whenever diff status or git repository data appeared.
Instead, the panel's visibility is now persisted via the
`agents.right-panel-open` localStorage key (matching the existing
`agents.right-panel-width` pattern for the panel width).
This gives users a consistent UX when switching between chats — the
panel stays in whatever state they last set it to.
## Changes
- **Removed** two auto-open blocks in `AgentDetailView` that tracked
`prevHasDiffStatus` / `prevHasGitRepos` and forced `showSidebarPanel =
true`
- **Added** `localStorage` persistence for the panel open/closed state
under key `agents.right-panel-open`
- Initial state is read from localStorage on mount (defaults to closed)
- Every toggle/close writes through to localStorage via
`handleSetShowSidebarPanel`
- Panel width was already persisted via `agents.right-panel-width` in
`RightPanel.tsx` — no changes needed there
## Problem
Chat sidebar title/status updates from WebSocket events don't take
effect immediately — they only appear after a full server re-fetch.
**Root cause:** All `setQueryData(chatsKey, ...)` calls write to cache
key `["chats"]`, but the rendered chat list reads from
`useInfiniteQuery(infiniteChats())` on key `["chats", undefined]`.
TanStack Query v5 `setQueryData` requires an exact key match, so these
are different cache entries.
WebSocket events (`title_change`, `status_change`, `created`, `deleted`)
and `updateSidebarChat` were all updating a cache entry that nothing
rendered from. The only way changes reached the UI was via
`invalidateQueries` (which prefix-matches), triggering a full server
re-fetch. This caused visible flicker when the re-fetch raced with
subsequent events.
## Fix
Add `updateInfiniteChatsCache()` helper that uses `setQueriesData({
queryKey: chatsKey })` — this **prefix-matches** all infinite query
variants (`["chats", undefined]`, `["chats", { archived: true }]`, etc.)
and correctly updates the `{ pages, pageParams }` structure.
Replace all direct `setQueryData(chatsKey, ...)` calls:
- WebSocket handler in `AgentsPage.tsx` (deleted, created, title_change,
status_change events)
- `updateSidebarChat` in `ChatContext.ts`
- Archive/unarchive optimistic updates in `chats.ts`
Also adds `readInfiniteChatsCache()` helper for reading the flat chat
list from the infinite query (used by the chime status lookup).
## Files changed
| File | Change |
|------|--------|
| `site/src/api/queries/chats.ts` | Added helpers, updated
archive/unarchive mutations |
| `site/src/pages/AgentsPage/AgentsPage.tsx` | WebSocket handler uses
new helpers |
| `site/src/pages/AgentsPage/AgentDetail/ChatContext.ts` |
`updateSidebarChat` uses new helper |
| `site/src/api/queries/chats.test.ts` | Tests seed/read infinite query
format |
| `site/src/pages/AgentsPage/AgentDetail/ChatContext.test.tsx` | Tests
seed/read infinite query format |
Adds a user-level custom prompt to the database.
I'll be doing a follow-up for the UI, as we currently do not have
user-level settings (it's just admin). I'll also make it very obvious
for chats where there is a user-level prompt, but I don't know how yet.
Fixes a race condition in `DiffHunksRenderer` where a stale async
highlight callback overwrites the render cache with an old diff, causing
a hunk count mismatch:
```
DiffHunksRenderer.renderHunks: lineHunk doesn't exist
```
## Root cause
The `DiffHunksRenderer` in `@pierre/diffs@1.0.11` caches highlighted AST
results keyed by diff object reference. When the shiki highlighter isn't
fully loaded, it fires `asyncHighlight(diff)` which captures the current
diff in a closure. If the diff changes before that promise resolves,
`onHighlightSuccess` unconditionally overwrites `renderCache` with the
stale diff/result pair. The subsequent `rerender()` then iterates the
new diff's hunks against the old result's `code.hunks` array, crashing
at an out-of-bounds index.
## Fix
Upgrades `@pierre/diffs` from `1.0.11` to `1.1.0-beta.19`, which
completely refactors the rendering pipeline:
- Replaces the per-hunk `code.hunks[hunkIndex]` lookup with flat
`additionLines`/`deletionLines` arrays indexed directly by line index
- Uses a new `iterateOverDiff` callback pattern instead of the
`renderHunks` method
- The `lineHunk doesn't exist` error is gone from the codebase entirely
The only code change on our side is adapting `extractDiffContent()` in
`FilesChangedPanel.tsx` to the new `ChangeContent`/`ContextContent`
types where `deletions`, `additions`, and `lines` are now counts with
index pointers into top-level
`FileDiffMetadata.deletionLines`/`additionLines` arrays.
Fixes several small UI issues on the agent detail and sidebar pages:
- **Sidebar lines changed indicator**: removed monospace font, matched
styling to model text (text-[13px] leading-4)
- **Git panel**: always shown instead of "No panels available" fallback
- **Git tab active state**: added `text-content-primary` so the tab
looks selected
- **Attachment button**: switched to `subtle` variant (lighter color, no
border)
- **Context indicator / attachment button**: matched sizes (`size-7`
container, `size-icon-sm` icon) and swapped positions
Adds offset and cursor-based pagination to the `GET
/api/experimental/chats` endpoint, following the exact same patterns
used by `GetUsers` and `GetTemplateVersionsByTemplateID`.
## Changes
### Database
- Add `after_id`, `offset_opt`, `limit_opt` params to
`GetChatsByOwnerID` SQL query
- Use composite `(updated_at, id) DESC` cursor for stable, deterministic
pagination
- Add migration with composite index on `chats (owner_id, updated_at
DESC, id DESC)`
### Backend
- Use `ParsePagination()` in `listChats` handler (matches `users.go`
pattern)
- Add `Pagination` field to `ListChatsOptions` SDK struct
### Frontend
- Add `infiniteChats()` query factory using `useInfiniteQuery` with
offset-based page params (same pattern as `infiniteWorkspaceBuilds`)
- Update `AgentsPage` to use `useInfiniteQuery`
- Add "Show more" button at the bottom of the agents sidebar (matches
`HistorySidebar` pattern)
- Keep existing `chats()` query for non-paginated uses (e.g., parent
chat lookup in `AgentDetail`)
### Tests
- Add `TestListChats/Pagination` covering `limit`, `after_id` cursor,
`offset`, and no-limit behavior
Uses streamdown's built-in `urlTransform` prop to intercept
`http://localhost:PORT` URLs in agent chat messages and rewrite them to
port-forwarded workspace URLs.
When the agent outputs a bare URL like `http://localhost:3000` or a
markdown link like `[app](http://localhost:8080/path)`, the URL is
rewritten to the workspace's port-forward subdomain (e.g.
`https://3000--agent--workspace--user.wildcard.host`). This makes links
clickable directly from the chat without manual port-forwarding.
## How it works
The transform is built in `AgentDetail` where workspace and proxy
context are available, then threaded as an optional prop through the
component tree:
```
AgentDetail → AgentDetailView → AgentDetailTimeline → ConversationTimeline → Response → Streamdown
```
- Uses streamdown's first-class `urlTransform` API — no monkey-patching
or rehype plugins
- Reuses the existing `portForwardURL()` utility from
`utils/portForward`
- Matches the same localhost detection as the terminal page
(`localhost`, `127.0.0.1`, `0.0.0.0`)
- Preserves pathname and search params
- Gracefully degrades: when any required context is missing (no
workspace, no wildcard proxy host), URLs pass through unchanged
## What gets transformed
| Markdown input | Transformed? |
|---|---|
| `http://localhost:8080` (bare URL, auto-linked by remark-gfm) | Yes |
| `[my app](http://localhost:3000/path)` (explicit link) | Yes |
| `\`http://localhost:8080\`` (inline code) | No (correct — code spans
are literal) |
| `https://example.com` (non-localhost) | No |
## Problem
The Admin → Agents → System Prompt textarea saved only to the browser's
`localStorage`. The value was never sent to the backend, never stored in
the database, and never injected into chats. Entering text, clicking
Save, and refreshing the page showed no changes — the prompt was
effectively a no-op.
## Root Cause
Three disconnected layers:
1. **Frontend** wrote to `localStorage`, never called an API.
2. **`handleCreateChat`** never read `savedSystemPrompt`.
3. **Backend** hardcoded `chatd.DefaultSystemPrompt` on every chat
creation — no field in `CreateChatRequest` accepted a custom prompt.
## Changes
### Database
- Added `GetChatSystemPrompt` / `UpsertChatSystemPrompt` queries on the
existing `site_configs` table (no migration needed).
### API
- `GET /api/experimental/chats/system-prompt` — returns the configured
prompt (any authenticated user).
- `PUT /api/experimental/chats/system-prompt` — sets the prompt
(admin-only, `rbac: deployment_config update`).
- Input validation: max 32 KiB prompt length.
### Backend
- `resolvedChatSystemPrompt(ctx)` checks for a custom prompt in the DB,
falls back to `chatd.DefaultSystemPrompt` when empty/unset.
- Logs a warning on DB errors instead of silently swallowing them.
- Replaced the hardcoded `defaultChatSystemPrompt()` call in chat
creation.
### Frontend
- Replaced `localStorage` read/write with React Query
`useQuery`/`useMutation` backed by the new endpoints.
- Fixed `useEffect` draft sync to avoid clobbering in-progress user
edits on refetch.
- Added `try/catch` error handling on save (draft stays dirty for
retry).
- Save button disabled during mutation (`isSavingSystemPrompt`).
- Query key follows kebab-case convention (`chat-system-prompt`).
### UX
- Added hint: "When empty, the built-in default prompt is used."
### Tests
- `TestChatSystemPrompt`: GET returns empty when unset, admin can set,
non-admin gets 403.
- dbauthz `TestMethodTestSuite` coverage for both new querier methods.
The Button icon variant applies [&>svg]:size-icon-sm (18px) and
the base applies [&>svg]:p-0.5, both of which silently override
h-*/w-* set directly on child SVGs. This caused the stop icon to
render at 18px instead of 12px and the send arrow to shift
off-center due to uncleared padding.
Pin each icon size via !important on the parent className so the
values are deterministic regardless of Tailwind class order:
- Attach: !size-icon-sm (18px, unchanged visual)
- Stop: !size-3 (12px, matches original intent)
- Send: !size-5 (20px, matches prior visual after padding)
Add Streaming and StreamingInterruptPending stories for the stop
button.
The Playwright e2e `webServer` starts the Coder server via
`go run -tags embed`, which must compile before serving. The default 60s
timeout leaves no margin when the CI runner is slow.
Failed run:
https://github.com/coder/coder/actions/runs/22782592241/job/66091950715
Successful run:
https://github.com/coder/coder/actions/runs/22782107623/job/66090828826
The server started and printed its banner, but with only ~4s left on the
clock the health check (`/api/v2/deployment/config`) could not complete
before the timeout fired. The same ~2x slowdown shows in the
`make site/e2e/bin/coder` step (45s vs 67s), confirming this is runner
performance variability.
Increase timeout to 120s.
Refs #22727
## Problem
Two network requests were blocking the initial page render with
fullscreen `<Loader fullscreen />` spinners:
1. **`POST /api/v2/authcheck`** (permissions) — blocked in `RequireAuth`
via `AuthProvider.isLoading`
2. **`GET /api/v2/organizations`** — blocked in `DashboardProvider`
All other bootstrap queries (`user`, `entitlements`, `appearance`,
`experiments`, `build-info`, `regions`) already used server-side
metadata injection via `index.html` meta tags and resolved instantly.
These two did not.
## Solution
Follow the existing `cachedQuery` + `<meta>` tag pattern to inject both
datasets server-side:
### Server-side (`site/site.go`)
- Add `Permissions` and `Organizations` fields to `htmlState`
- Fetch organizations via `GetOrganizationsByUserID` in parallel with
existing queries
- Evaluate all `permissionChecks` using the RBAC authorizer directly
- Inject results as HTML-escaped JSON into `<meta>` tags
### Frontend
- Register `permissions` and `organizations` in `useEmbeddedMetadata`
- Update `checkAuthorization()` to accept optional metadata and use
`disabledRefetchOptions` when available
- Update `organizations()` to accept optional metadata and use
`cachedQuery` when available
- Wire metadata through `AuthProvider` and `DashboardProvider`
### Note
The Go `permissionChecks` map in `site/site.go` mirrors
`site/src/modules/permissions/index.ts` and must be kept in sync.
## Summary
The backend requires `context_limit` to be a positive integer when
creating a model config, but the frontend form did not visually indicate
this to the user. This caused a confusing error after submission
("Context limit is required. context_limit must be greater than zero.").
## Changes
- Added required asterisk (`*`) to the **Context Limit** label, matching
the existing **Model Identifier** field pattern
- Added Yup `.required()` validation to the `contextLimit` field so the
form catches the missing value client-side before submission
## Before
The "Context Limit" label had no required indicator. Users could submit
the form without filling it in, only to receive a backend error.
## After
The "Context Limit" label now shows a red `*` (consistent with "Model
Identifier"), and the form validates the field as required before
allowing submission.
Created on behalf of @uzair-coder07
---------
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
## Summary
Refactors the right-side panel in the Agents page into a generic tabbed
container with a unified Git panel.
### Changes
**Architecture**
- `SidebarTabView` is now a generic tabbed container with no
git-specific logic, ready for additional tabs
- All Git content lives in a new `GitPanel` component with an internal
Remote/Local segmented control
**Git Panel**
- Remote view: branch/PR diff via `FilesChangedPanel`
- Local view: working tree changes with per-repo headers, commit &
refresh actions
- Split/unified diff toggle restored in the toolbar
- `DiffStatBadge` rendered inside the Remote/Local segmented buttons
(full-height, no rounding, inactive opacity 50%)
**Visual polish**
- Active/inactive/hover states match the sidebar agent selection styles
(`bg-surface-quaternary/25`, `hover:bg-surface-tertiary/50`)
- Inactive tab text uses `text-content-secondary` (not primary)
- Tab button sizing fixed: `min-w-0` + `px-2` to prevent inflated width
- Chat title centered via absolute positioning when panel is fullscreen
- Polished empty states with boxed icons (`GitCompareArrowsIcon` for
Remote, `FileDiffIcon` for Local)
- Unified header styles between Remote and Local sections (both use
`bg-surface-secondary` with consistent icon/text sizing)
- Panel toggle always visible in top bar (not gated on having diff data)
**Cleanup**
- Removed dead code: `DiffStatsInline`, `computeDiffStats` export,
`workingDiffStats` memo, `ChatDiffStatusResponse` import
- Simplified `RepoChangesPanel` to a pure `DiffViewer` wrapper
- Simplified `TopBar` to use a generic `panel` prop instead of
diff-specific props
When the chat WebSocket reconnects, the server replays all buffered
`message_part` events in the initial snapshot. The client's `onOpen`
callback only cleared the stream error but **not** the stream state, so
replayed parts appended to the stale accumulator, doubling (or further
multiplying) the visible text with each reconnect. A page refresh would
clear the issue temporarily since it creates a fresh `ChatStore`.
This was caused by:
- **Server** (`coderd/chatd/chatd.go`): `Subscribe()` unconditionally
includes all buffered `message_part` events in the snapshot sent to
new connections. The `afterMessageID` parameter only filters durable
DB messages, not ephemeral stream parts.
- **Client** (`ChatContext.ts`): The `onOpen` callback in
`createReconnectingWebSocket` called `store.clearStreamError()` but
not `store.clearStreamState()`. When the reconnected stream replays
buffered `message_part` events, `applyMessagePartToStreamState`
blindly appends text via `appendTextBlock`.
The fix was to add `store.clearStreamState()` in the `onOpen` callback
so replayed parts build from a clean slate instead of appending to stale
content.
A red/green verification test was added to ensure the fix works as
expected.
Fixes a regression where image attachments in user chat messages were
rendered twice, once inside the bubble container and once outside it.
- **ConversationTimeline.tsx**: Remove 43 duplicate lines (outer image
block + second fade overlay) from the `ChatMessageItem` user-message
branch.
- **ConversationTimeline.stories.tsx** (new): Add focused stories for
`ConversationTimeline` with `play` function assertions on image
thumbnail counts to guard against this class of regression.
Adds a guard + some unit tests to ensure that we don't try to fetch git
changes if there's no workspace agent from which to do so.
Generated by Claude Opus 4.6 but read using Cian's eyeballs.
## Summary
Adds a line-reference and annotation system for diffs in the Agents UI.
Users can click line numbers in the Git diff panel to open an inline
prompt input, type a comment, and have a reference chip + text added to
the chat message input.
## Changes
### Backend
- Added `diff-comment` type to `ChatInputPart` and `ChatMessagePart` in
`codersdk/chats.go` with `FileName`, `StartLine`, `EndLine`, `Side`
fields
### Frontend
- **`DiffCommentContext`**: React context/provider managing pending diff
comments with `addReference`, `removeComment`, `restoreComment`,
`clearComments`
- **`DiffCommentNode`**: Lexical `DecoratorNode` rendering inline chips
in the chat input showing file:line references. Chips are clickable
(scroll to line in diff), removable, and support undo/redo via mutation
tracking
- **`InlinePromptInput`**: Textarea annotation rendered inline under
clicked lines in the diff. Supports multiline (Shift+Enter), submit
(Enter), cancel (Escape)
- **`FilesChangedPanel`**: Line click/drag-select handlers open the
inline input. On submit, a badge chip + plain text are inserted into the
Lexical editor
- **`AgentDetail`**: Bidirectional sync between DiffCommentContext and
Lexical editor. Comments are sent as `diff-comment` parts on message
submit
- **`ConversationTimeline`**: Renders `diff-comment` message parts with
file:line labels
## How it works
1. Click a line number in the diff → inline textarea appears below that
line
2. Type a comment and press Enter → reference chip appears in chat input
with your text after it
3. Send the message → diff-comment parts are included alongside the
message text
## Root Cause
The `createAndBuildTemplateVersion` mutation calls
`waitBuildToBeFinished`, which polls `getTemplateVersion` behind a real
`delay()` call:
```ts
await delay(jobStatus === "pending" ? 250 : 1000);
```
On the first iteration, `jobStatus` is `undefined` (not `"pending"`), so
the delay is **1000 ms**. The `waitFor` assertion in the test uses the
default `@testing-library` timeout, which is also **1000 ms**. The
`toast.success` call fires right at or after the timeout boundary,
making the test flaky under CI load.
## Fix
Mock `utils/delay` to resolve immediately at the top of the test file.
This eliminates the 1 s wall-clock wait in `waitBuildToBeFinished`, so
the async submit chain completes in microtasks and the `toast.success`
spy is called well within the `waitFor` window.
## Verification
- Both tests pass (`renders with variables` + `user submits the form
successfully`)
- **50/50 passes** under stress testing (sequential runs with
`--no-cache`)
- Submit test time dropped from ~2000 ms to ~1400 ms
Split from #22693 per review feedback.
Fixes SSE error handling and adds WebSocket reconnection with
exponential backoff to the AgentsPage chat list watcher.
The Button base styles apply `[&>svg]:p-0.5` (2px padding) to child
SVGs. In the small `size-7` rounded attach button, this extra padding
shifts the 16x16 ImageIcon off-center. Override with `[&>svg]:p-0` to
remove it.
On small viewports (below `xl`) the git/changes panel was expanding as a
bottom sheet. This changes it to always appear from the right side:
- **Mobile (<`sm`/640px):** Panel opens full-width (`w-[100vw]`) as a
right-side overlay
- **`sm`+ (640px+):** Panel uses the persisted width (`--panel-width`)
with min 360px / max 70vw, drag handle enabled
- Parent flex container is always `flex-row` instead of `flex-col
xl:flex-row`
### Changes
- `AgentDetail.tsx`: Removed `flex-col xl:flex-row` responsive switch,
always uses `flex-row`
- `RightPanel.tsx`: Replaced bottom-sheet layout (`h-[42dvh]`) with
right-side panel at all breakpoints. Full viewport width below `sm`,
resizable width at `sm`+. Drag handle activates at `sm` instead of `xl`.
This change adds support for image attachments to chat via add button
and clipboard paste. Files are stored in a new `chat_files` table and
referenced by ID in message content. File data is resolved from storage
at LLM dispatch time, keeping the message content column small.
Upload validates MIME types via content type or content sniffing against
an allowlist (png, jpeg, gif, webp). The retrieval endpoint serves files
with immutable caching headers. On the frontend, uploads start eagerly
on attach with a background fetch to pre-warm the browser HTTP cache so
the timeline renders instantly after send.
Replaces the single-purpose PR diff right panel with a tabbed sidebar
that shows both the existing PR diff and real-time git repository
changes from the workspace agent.
There's an accompanying backend PR
[here](https://github.com/coder/coder/pull/22565).
https://github.com/user-attachments/assets/bbd53f1c-d753-4574-a159-6dad5989e5e3
## Backend surface
One endpoint drives this feature:
- **`WS /api/experimental/workspaceagents/{id}/git/watch`** —
bidirectional WebSocket. The client sends `refresh` messages; the agent
responds with `changes` messages containing per-repo branch and unified
diff. The workspace agent also automatically pushes changes as they
occur in the workspace.