## What
Adds a new admin-only **PR Insights** page for the `/agents` analytics
view — a dashboard for engineering leaders to understand code shipped by
AI agents.
### Backend
- `GET /api/v2/chats/insights/pull-requests` — admin-only endpoint
- 4 SQL queries in `chatinsights.sql` aggregating `chat_diff_statuses`
joined with chat cost data (via root chat tree rollup)
- Runs 5 parallel DB queries: current summary, previous summary (for
trends), time series, per-model breakdown, recent PRs
- SDK types auto-generate to TypeScript
### Frontend (`PRInsightsView`)
- **Stat cards**: PRs created, Merged, Merge rate, Lines shipped,
Cost/merged PR — with trend badges comparing to previous period
- **Activity chart**: Stacked area chart (created/merged/closed) using
git color tokens (`git-added-bright`, `git-merged-bright`,
`git-deleted-bright`)
- **Model performance table**: Per-model PR counts, inline merge rate
bars, diff stats, cost breakdown
- **Recent PRs table**: Status badges, review state icons, author info,
external links
- **Time range filter**: 7d/14d/30d/90d button group
- **4 Storybook stories**: Default, HighPerformance, LowVolume, NoPRs
### Data source
All PR data comes from the existing `chat_diff_statuses` table
(populated by the `gitsync.Worker` background job that polls GitHub
every 120s). No new data collection required.
### Screenshot
View in Storybook: `pages/AgentsPage/PRInsightsView`
## Description
Implements the server-side merge logic for the `merge_strategy`
attribute added to `coder_env` in [terraform-provider-coder
v2.15.0](https://github.com/coder/terraform-provider-coder/pull/489).
This allows template authors to control how duplicate environment
variable names are combined across multiple `coder_env` resources.
Relates to https://github.com/coder/coder/issues/21885
## Supported strategies
| Strategy | Behavior |
|----------|----------|
| `replace` (default) | Last value wins — backward compatible |
| `append` | Joins values with `:` separator (e.g. PATH additions) |
| `prepend` | Prepends value with `:` separator |
| `error` | Fails the build if the variable is already defined |
## Example
```hcl
resource "coder_env" "path_tools" {
agent_id = coder_agent.dev.id
name = "PATH"
value = "/home/coder/tools/bin"
merge_strategy = "append"
}
```
## Changes
- **Proto**: Added `merge_strategy` field to `Env` message in
`provisioner.proto`
- **State reader**: Updated `agentEnvAttributes` struct and proto
construction in `resources.go`
- **Merge logic**: Added `mergeExtraEnvs()` function in
`provisionerdserver.go` with strategy-aware merging for both agent envs
and devcontainer subagent envs
- **Tests**: 15 unit tests covering all strategies, edge cases (empty
values, mixed strategies, multiple appends)
- **Dependency**: Bumped `terraform-provider-coder` v2.14.0 → v2.15.0
- **Fixtures**: Updated `duplicate-env-keys` test fixtures and golden
files
## Ordering
When multiple resources `append` or `prepend` to the same key, they are
processed in alphabetical order by Terraform resource address (per the
determinism fix in #22706).
## Summary
- add a hidden deployment config option for chat acquire batch size
(`CODER_CHAT_ACQUIRE_BATCH_SIZE` / `chat.acquireBatchSize`)
- thread the configured value into chatd startup while preserving the
existing default of `10`
- clamp the deployment value to the `int32` range before passing it into
chatd
- regenerate the API/docs/types/testdata artifacts for the new config
field
## Why
`chatd` currently acquires pending chats in batches of `10` via a
compile-time default. This change makes that batch size
operator-configurable from deployment config, so we can tune acquisition
behavior without another code change.
Both message parsers accepted untyped input and relied on scattered
asRecord/asString calls to extract fields at runtime. With the
discriminated ChatMessagePart union, both accept typed input directly
and narrow via switch (part.type).
parseMessageContent narrows from (content: unknown) to
(content: readonly ChatMessagePart[] | undefined), removing legacy
input shape handling the Go backend normalizes away.
applyMessagePartToStreamState narrows from Record<string, unknown>
to ChatMessagePart.
The SSE type guards had a & Record<string, unknown> intersection
that widened everything untyped downstream. Since the data comes
from our own API, the intersection was removed and all handlers in
ChatContext now use generated types directly.
Fixes tool_call_id and tool_name variant tags in codersdk/chats.go:
marked optional to match reality (Go guards against empty values,
omitempty omits them at the wire level).
Refs #23168, #23175
## Summary
The `AgentsPageView: Opens Analytics For Admins` story was flaky because
the analytics header renders a rolling 30-day date range in the
top-right corner. Since that range was based on the current date, the
story output changed every day.
This change makes the story deterministic by:
- adding an optional `analyticsNow` prop to `AgentsPageView`
- passing that value through to `AnalyticsPageContent` when the
analytics panel is shown
- setting a fixed local-noon timestamp in the story so the rendered
range label stays stable across timezones
## Summary
- include the current agent chat ID in VS Code and Cursor deep links
opened from the agent detail page
- extend `getVSCodeHref` so `chatId` is added only when provided
- add focused tests for deep-link generation with and without `chatId`
## Testing
- `pnpm -C site run format -- src/modules/apps/apps.ts
src/modules/apps/apps.test.ts src/pages/AgentsPage/AgentDetail.tsx`
- `pnpm -C site run check -- src/modules/apps/apps.ts
src/modules/apps/apps.test.ts src/pages/AgentsPage/AgentDetail.tsx`
- `pnpm -C site exec vitest run src/modules/apps/apps.test.ts`
- `pnpm -C site run lint:types`
---
_Generated with [`mux`](https://github.com/coder/mux) • Model:
`openai:gpt-5.4` • Thinking: `high`_
When a PR is detected for a chat, display a compact PR badge in the
AgentDetail TopBar. On mobile it is always visible; on desktop it is
hidden when the sidebar panel is open (which already surfaces PR info)
and shown when the panel is closed.
The badge shows a state-colored icon (open, draft, merged, closed) and
the PR title or number, linking to the PR URL. Only URLs confirmed as
real PRs (via explicit `pull_request_state` or a `/pull/<number>`
pathname) trigger the badge.
## Changes
- **`TopBar.tsx`** — Added `diffStatusData` prop, `PrStateIcon` helper,
and a PR link badge between the title and actions area. Hidden on
desktop when the sidebar panel is open.
- **`AgentDetailView.tsx`** — Pass `diffStatusData` through to
`AgentDetailTopBar`.
- **`TopBar.stories.tsx`** — Added stories for open, draft, merged, and
closed PR states.
RenderBlock's file-reference variant diverged from the API (camelCase
vs snake_case), and both file variants were defined inline duplicating
the generated ChatFilePart and ChatFileReferencePart types. The
thinking and file-reference variants carried dead fields (title, text)
that were never populated by the backend.
Replace inline definitions with references to generated types, remove
dead fields, and simplify ReasoningDisclosure (disclosure button path
was dead without title).
Refs #23168
The flat ChatMessagePart interface had 20+ optional fields, preventing
TypeScript from narrowing types on switch(part.type). Each consumer
needed runtime validation, type assertions, or defensive ?. chains.
Add `variants` struct tags to ChatMessagePart fields declaring which
union variants include each field. A codegen mutation in apitypings
reads these tags via reflect and generates per-variant sub-interfaces
(ChatTextPart, ChatReasoningPart, etc.) plus a union type alias.
A test validates every field has a variants tag or is explicitly
excluded, and every part type is covered.
Remove dead frontend code: normalizeBlockType, alias case branches
("thinking", "toolcall", "toolresult"), legacy field fallbacks
(line_number, typedBlock.name/id/input/output), and result_delta
handling. Add test coverage for args_delta streaming, provider_executed
skip logic, and source part parsing.
Adds a new `site_config` entry that controls whether the virtual desktop
feature for Coder Agents is enabled. It can be set via a new
`/api/experimental/chats/config/desktop-enabled` endpoint, which will be
used by the frontend.
## Summary
Remove the `hidden` tag from the `PromptCacheKey` field on
`ChatModelOpenAIProviderOptions` so the auto-generated JSON schema
no longer marks it as hidden. This allows the admin model
configuration UI to render a "Prompt Cache Key" text input for
OpenAI models alongside other visible options like Reasoning Effort,
Service Tier, and Web Search.
## Changes
- **`codersdk/chats.go`**: Remove `hidden:"true"` from `PromptCacheKey`
struct tag.
- **`site/src/api/chatModelOptionsGenerated.json`**: Regenerated via
`make gen` — `hidden: true` removed from the `prompt_cache_key` entry.
- **`modelConfigFormLogic.test.ts`**: Extend existing "all fields set"
tests to cover extract and build roundtrip for `promptCacheKey`.
## How it works
The `hidden` Go struct tag propagates through the code generation
pipeline:
1. Go struct tag → `scripts/modeloptionsgen` →
`chatModelOptionsGenerated.json`
2. The frontend `getVisibleProviderFields()` filters out fields with
`hidden: true`
3. Removing the tag makes the field visible in the schema-driven form
renderer
No new UI components are needed — the existing `ModelConfigFields`
component
automatically renders the field as a text input based on the schema
(`type: "string"`, `input_type: "input"`).
The field appears as **"Prompt Cache Key"** with description
"Key for enabling cross-request prompt caching" in the OpenAI provider
section of the admin model configuration form.
Fixes#18573
## Changes
When a `coder_app` resource sets `open_in = "tab"`, clicking the app
link now opens in a new browser tab instead of navigating in the same
tab.
`target="_blank"` and `rel="noreferrer"` are set inline on the
`<a>` elements in `AppLink.tsx`, gated on `app.open_in === "tab"`. This
follows the codebase convention of co-locating `target` and `rel` at the
render site.
`noreferrer` suppresses the Referer header to avoid leaking workspace IDs
to destination servers and implies `noopener`.
`noopener` prevents tabnabbing — without it, the opened page can
redirect the Coder dashboard tab via `window.opener`. This is especially
relevant for same-origin path-based apps, which would otherwise have
full DOM access to the dashboard.
> **Future enhancement**: template admins could opt into sending the
referrer via a `coder_app` setting, enabling feedback pages built around
workspace context.
## Tests
A vitest case is added in `AppLink.test.tsx` (rather than a Storybook
story, since the assertions are purely behavioral with no visual
component):
- **`sets target=_blank and rel=noopener noreferrer when open_in is
tab`** — renders the app link with `open_in: "tab"` and asserts
`target="_blank"` and `rel="noreferrer"` are present on the
anchor.
## Slim-window behavior
The `slim-window` test case and the `openAppInNewWindow()` comment in
`apps.ts` have been split out into a follow-up PR for separate review,
since the `window.open()` / `noopener` tradeoffs there deserve dedicated
discussion.
---------
Co-authored-by: Kayla はな <kayla@tree.camp>
Introduce a three-way workspace sharing setting (none, everyone,
service_accounts) replacing the boolean workspace_sharing_disabled.
In service_accounts mode, only service account-owned workspaces can be
shared while regular members' share permissions are removed. Adds a
new organization-service-account system role with per-org permissions
reconciled alongside the existing organization-member system role.
Related to:
https://linear.app/codercom/issue/PLAT-28/feat-service-accounts-sharing-mode-and-rbac-role
---------
Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com>
Co-authored-by: Kayla はな <mckayla@hey.com>
## Summary
Fixes four interrelated issues that caused scroll position jumps and
phantom scroll growth when paginating older chat messages.
## Changes
### 1. Removed client-side message windowing (`useMessageWindow`)
There were two competing sentinel systems: server-side pagination and
client-side windowing. The client windowing sentinel was nested deep
inside the timeline with no explicit IntersectionObserver `root`,
causing scroll position jumps when messages were prepended. Blink
(coder/blink) has no client-side windowing. Removed it entirely; server
pagination + `contentVisibility` handled performance.
### 2. Removed `contentVisibility: "auto"` from message sections
Each section had `contentVisibility: "auto"` with `containIntrinsicSize:
"1px 600px"`, causing the scroll region to grow/shrink as the browser
swapped 600px placeholders for actual heights while scrolling. This
created phantom scroll growth with no fetch involved.
### 3. Gated WebSocket on initial REST data
The WebSocket `Subscribe` snapshot calls `GetChatMessagesByChatID` (no
LIMIT) which returns every message when `afterMessageID` is 0. The
WebSocket effect opened before the REST page resolved, so
`lastMessageIdRef` was undefined, causing the server to replay the
entire history and defeating pagination. Added `initialDataLoaded` guard
so the socket waits for the first REST page.
### 4. Manual scroll position restoration
Replaced unreliable CSS scroll anchoring in `flex-col-reverse` with a
`ScrollAnchoredContainer` that snapshots `scrollHeight` before fetch and
restores `scrollTop` via `useLayoutEffect` after render. Disabled
browser scroll anchoring (`overflow-anchor: none`) to prevent conflicts.
File references in user messages now render as inline chips (matching
the chat input style) instead of in a separate bordered section at the
bottom of the message bubble.
This reimplements #23131 which was accidentally reverted during the
merge of #23072 (the spend-limit UI PR resolved a merge conflict by
dropping the inline chip logic).
## Changes
- **FileReferenceNode.tsx**: Export `FileReferenceChip` so it can be
imported for read-only use (no remove button when `onRemove` is
omitted).
- **ConversationTimeline.tsx**: Iterate through `parsed.blocks` in
document order, rendering `response` blocks as text and `file-reference`
blocks as inline `FileReferenceChip` components. Removes the old
separated file-reference section with `border-t` divider.
- **ConversationTimeline.stories.tsx**: Added
`UserMessageWithInlineFileRef` and
`UserMessageWithMultipleInlineFileRefs` stories.
## Problem
The WebSocket handler for `diff_status_change` events in
`AgentsPage.tsx` was triggering a burst of redundant HTTP requests on
every event:
1. **`invalidateChatListQueries(queryClient)`** — Full refetch of the
chats list endpoint. Unnecessary because `updateInfiniteChatsCache`
already writes `diff_status` into the sidebar cache optimistically on
every event.
2. **`invalidateQueries({ queryKey: chatKey(id) })`** — Refetch of the
individual chat. Also unnecessary — the SSE event carries `diff_status`
in its payload and the optimistic updater writes it into the `chatKey`
cache directly. Worse, this call was missing `exact: true`, so TanStack
Query's prefix matching cascaded the invalidation to `chatMessagesKey`,
`chatDiffContentsKey`, and every other query under `["chats", id]`.
Since diff status changes fire frequently during active agent work, this
spammed the chats list endpoint and caused redundant refetches of
messages and diff contents on every single event.
## Fix
Strip the handler down to the one invalidation that's actually needed —
`chatDiffContentsKey` (the file-level diff contents aren't in the SSE
payload):
```typescript
if (chatEvent.kind === "diff_status_change") {
void queryClient.invalidateQueries({
queryKey: chatDiffContentsKey(updatedChat.id),
exact: true,
});
}
```
## Why tests didn't catch this
The existing tests in `chats.test.ts` cover query utilities in isolation
(e.g. `invalidateChatListQueries` scoping, mutation invalidation). The
WebSocket event handler lives in the `AgentsPage` component — there was
no test covering what the `diff_status_change` code path actually
invalidates.
Added regression tests verifying that `exact: true` prevents
prefix-match cascade vs the old behavior.
Replace the ad-hoc camelCase file block shape ({ mediaType, fileId, data })
with snake_case fields matching ChatMessagePart from the API types.
The RenderBlock file variant now uses media_type/file_id instead of
mediaType/fileId. The parsers in messageParsing.ts and streamState.ts
pass validated ChatMessagePart objects through directly instead of
destructuring and reassembling with renamed fields. This eliminates
the needless API → camelCase → snake_case roundtrip that the edit
flow previously required.
Refs #22735
handleSubmit (triggered via Enter key) didn't check isUploading, so
messages could be sent while an image upload was still in progress.
The send button was correctly disabled via canSend, but the keyboard
shortcut bypassed that guard.
QueuedMessagesList used untyped extraction helpers that fell through to
JSON.stringify for attachment-only messages. Replace them with a single
getQueuedMessageInfo function using typed ChatMessagePart access.
Show an attachment badge (ImageIcon + count) for file parts, and use a
consistent "[Queued message]" placeholder for all no-text situations.
Editing a queued message with file attachments silently dropped all
attachments because handleStartQueueEdit only accepted text. Thread
file blocks from QueuedMessagesList through the edit callback into
handleStartQueueEdit, which now calls setEditingFileBlocks. The
existing useEffect in AgentDetailInput picks these up and populates
the attachment UI. Also clear editingFileBlocks in handleCancelQueueEdit
and handleSendFromInput.
Frontend for agent chat spend limiting on `/agents`.
## Changes
- add the limits management UI, API hooks, and validation for
deployment, group, and user overrides
- show spend limit status in Agents analytics and usage summaries
- surface limit-related chat errors consistently in the agent detail
experience
- add shared currency and usage-limit messaging helpers plus related
stories/tests
## Problem
`updateInfiniteChatsCache`, `prependToInfiniteChatsCache`, and
`readInfiniteChatsCache` use `setQueriesData({ queryKey: ["chats"] })`
which prefix-matches **all** queries starting with `"chats"`, including
`["chats", chatId, "messages"]`.
After #23083 converted chat messages to `useInfiniteQuery`, the cached
messages data gained a `.pages` property containing
`ChatMessagesResponse` objects (not `Chat[]` arrays). The `if
(!prev.pages)` guard no longer bailed out, and the updater called
`.map()` on these objects — `TypeError: Z.map is not a function`.
## Fix
Extract the `isChatListQuery` predicate that already existed inline in
`invalidateChatListQueries` and apply it to all four cache helpers. This
scopes them to sidebar queries (`["chats"]` or `["chats",
<filterOpts>]`) and skips per-chat queries (`["chats", <id>, ...]`).
## VS Code iframe embed auth via postMessage + setSessionToken
Adds embed auth for VS Code iframe integration, allowing the Coder agent
chat UI to be embedded in VS Code webviews without manual login — using
direct header auth instead of cookies.
### How it works
1. **Parent frame** (VS Code webview) loads an iframe pointing to
`/agents/:agentId/embed`
2. **Embed page** detects the user is signed out and posts
`coder:vscode-ready` to the parent
3. **Parent** responds with `coder:vscode-auth-bootstrap` containing the
user's Coder API token
4. **Embed page** calls `API.setSessionToken(token)` to set the
`Coder-Session-Token` header on all subsequent axios requests
5. **Embed page** fetches user + permissions, sets them in the React
Query cache atomically, and renders the authenticated agent chat UI
No cookies, no CSRF, no backend endpoint needed. The token is passed via
postMessage and used as a header on every API request.
### What changed
**Frontend** (`site/src/pages/AgentsPage/`):
- `AgentEmbedPage.tsx` — added postMessage bootstrap directly in the
embed page: listens for `coder:vscode-auth-bootstrap`, calls
`API.setSessionToken(token)`, fetches user/permissions atomically to
avoid race conditions
- `EmbedContext.tsx` — React context signaling embed mode (from previous
commit, unchanged)
- `AgentDetail/TopBar.tsx` — conditionally hides navigation elements in
embed mode (from previous commit, unchanged)
- Both `/agents/:agentId/embed` and `/agents/:agentId/embed/session`
routes live outside `RequireAuth`
**Auth bootstrap** (`site/src/api/queries/users.ts`):
- `bootstrapChatEmbedSessionFn` now calls `API.setSessionToken(token)`
instead of posting to a backend endpoint
- Fetches user and permissions directly via `API.getAuthenticatedUser()`
and `API.checkAuthorization()`, then sets both in the query cache
atomically — this avoids a race where `isSignedIn` flips before
permissions are loaded
**Removed** (no longer needed):
- `coderd/embedauth.go` — the `POST
/api/experimental/chats/embed-session` handler
- `coderd/embedauth_test.go` — backend tests for the endpoint
- `codersdk/embedauth.go` — `EmbedSessionTokenRequest` SDK type
- `site/src/api/api.ts` — `postChatEmbedSession` method
- `docs/user-guides/workspace-access/vscode-embed-auth.md` — doc page
for the old cookie flow
- Swagger/API doc entries for the endpoint
### Why not cookies?
The initial implementation used a backend endpoint to set an HttpOnly
session cookie. This required `SameSite=None; Secure` for cross-origin
iframes, which doesn't work over HTTP in development (Chrome requires
HTTPS for `Secure` cookies). The `setSessionToken` approach bypasses
cookies entirely — the token is set as an axios default header, and
header-based auth also naturally bypasses CSRF protection.
### Dogfooding
Tested end-to-end with a VS Code extension that:
1. Registers a `/openChat` deep link handler
(`vscode://coder.coder-remote/openChat?url=...&token=...&agentId=...`)
2. Starts a local HTTP reverse proxy (to work around VS Code webview
iframe sandboxing)
3. Loads `/agents/:agentId/embed` in an iframe through the proxy
4. Relays the postMessage handshake between the iframe and the extension
host
5. The embed page receives the token, calls `setSessionToken`, and
renders the chat
Verified: chat title, messages, and input field all display correctly in
VS Code's secondary sidebar panel.
Adds cursor-based pagination to the chat messages endpoint.
## Backend
- New `GetChatMessagesByChatIDPaginated` SQL query: returns messages in
`id DESC` order with a `before_id` keyset cursor and configurable
`limit`
- Handler parses `?before_id=N&limit=N` query params, uses the `LIMIT
N+1` trick to set `has_more` without a separate COUNT query
- Queued messages only returned on the first page (no cursor) since
they're always the most recent
- SDK client updated with `ChatMessagesPaginationOptions`
- Fully backward compatible: omitting params returns the 50 newest
messages
## Frontend
- Switches `getChatMessages` from `useQuery` to `useInfiniteQuery` with
cursor chaining via `getNextPageParam`
- Pages flattened and sorted by `id` ascending for chronological display
- `MessagesPaginationSentinel` component uses `IntersectionObserver`
(200px rootMargin prefetch) inside the existing `flex-col-reverse`
scroll container
- `flex-col-reverse` handles scroll anchoring natively when older
messages are prepended — no manual `scrollTop` adjustment needed (same
pattern as coder/blink)
## Why cursor-based instead of offset/limit
Offset-based pagination breaks when new messages arrive while paginating
backward (offsets shift, causing duplicates or missed messages). The
`before_id` cursor is stable regardless of inserts — each page is
deterministic.
The completion chime on `/agents` was enabled by default for new users
(or when no localStorage preference existed). This changes the default
to disabled, so users must explicitly opt in via the sound toggle
button.
## Changes
- `getChimeEnabled()` now returns `false` when no preference is stored
(was `true`)
- `catch` fallback also returns `false` (was `true`)
- Updated tests to reflect the new default and explicitly enable the
chime in `maybePlayChime` tests
## Problem
The sidebar diff status (PR icon, +additions/-deletions, file count) was
not updating in real-time. Users had to reload the page to see changes.
Two root causes:
1. **Frontend**: The `diff_status_change` WebSocket handler in
`AgentsPage.tsx` had an early `return` (line 398) that skipped
`updateInfiniteChatsCache`, so the sidebar's cache was never updated.
Even for other event types, the cache merge only spread `status` and
`title` — never `diff_status`.
2. **Server**: `publishChatPubsubEvent` in `chatd.go` constructed a
minimal `Chat` payload without `DiffStatus`, so even if the frontend
consumed the event, `updatedChat.diff_status` would be `undefined`.
## Fix
### Server (`coderd/chatd/chatd.go`)
- `publishChatPubsubEvent` now accepts an optional
`*codersdk.ChatDiffStatus` parameter; when non-nil it's set on the
outgoing `Chat` payload.
- `PublishDiffStatusChange` fetches the diff status from the DB,
converts it, and passes it through.
- Added `convertDBChatDiffStatus` (mirrors `coderd/chats.go`'s converter
to avoid circular import).
- All other callers pass `nil`.
### Frontend (`site/src/pages/AgentsPage/AgentsPage.tsx`)
- Removed the early `return` so `diff_status_change` events fall through
to the cache update logic.
- Added `isDiffStatusEvent` flag and spread `diff_status` into both the
infinite chats cache (sidebar) and the individual chat cache.
The `/chats/{chat}/diff-status` endpoint was redundant because:
- The `Chat` type already has a `DiffStatus` field
- Listing chats already resolves and returns `diff_status`
- The `getChat` endpoint was the only one not resolving it (passing
`nil`)
## Changes
**Backend:**
- `getChat` now calls `resolveChatDiffStatus` and includes the result in
the response
- Removed `getChatDiffStatus` handler, route (`GET /diff-status`), and
SDK method
- Tests updated to use `GetChat` instead of `GetChatDiffStatus`
**Frontend:**
- `AgentDetail.tsx`: uses `chatQuery.data?.diff_status` instead of
separate query
- `RemoteDiffPanel.tsx`: accepts `diffStatus` as a prop instead of
fetching internally
- `AgentsPage.tsx`: `diff_status_change` events now invalidate the chat
query
- Removed `chatDiffStatus` query, `chatDiffStatusKey`, and
`getChatDiffStatus` API method