This pull-request moves using to using the plain `radix-ui` package over
`@radix-ui/react-*` packages. Put simply, now we're not going to run
into issues with inconsistent radix dependencies. This will have no
effect to how the code is built, but will give us a single place to
import from.
This refactors `<Tabs />` into two clearer patterns: link tabs for route
navigation and Radix tabs for stateful tab panels. That gives us proper
accessibility semantics where we need them without overloading simple
navigation tabs.
As part of that split, this updates several consumers, adds coverage for
both variants, and cleans up some nearby styling.
- introduce Radix-backed tabs primitives for tabbed content
- move router-based tabs to `LinkTabs`
- update notifications, IdP sync, and workspace build pages to use
semantic tabs
- preserve route navigation tabs for groups and templates
- add stories/tests for both tab implementations
- simplify related layout and styling in touched components
Closes#22244
This pull-request makes our `<Alert />`'s more inline with the Figma
style-system, we're looking to ensure that these are vertically rendered
now and not horizontal WCAG nightmares.
---------
Co-authored-by: Danielle Maywood <danielle@themaywoods.com>
Sometimes clicking **Edit** on a chat message does not populate the
composer with the message text, and the edit flow had a few timing bugs
around Lexical hydration. The composer was relying on
`key={initialValue}` on `LexicalComposer`, so re-editing the same text
could produce no state change, no remount, and an empty editor.
This PR keeps the editor mounted and switches edit flows to an
imperative `setValue()` API on `ChatMessageInputRef`. It also hardens
that API so draft reads and writes stay correct across initial
hydration: canceling edit no longer refocuses on mobile, pre-edit draft
snapshots preserve persisted drafts, early `setValue()` calls buffer
until the editor is ready, and `getValue()` falls back before readiness
but reads live editor state after attach.
Closes#22189
GFM alerts (e.g., `> [!IMPORTANT]`) in Markdown content failed to render
when the alert body contained inline formatting like `**bold**`,
`*italic*`, or `` `code` ``. The alert marker and subsequent text were
merged into a single string node by the parser, causing the type
detection to fail and fall back to a plain blockquote.
Additionally, multi-line alert content (`> line one\n> line two`) lost
its line breaks — all lines collapsed into one.
- Split the alert marker from trailing content in shared string nodes so
type detection works with inline formatting
- Preserve embedded newlines as `<br/>` elements to match GitHub's GFM
alert rendering
- Wrap plain-text children instead of splitting on `\n` to avoid
stripping newline information early
<img width="447" height="187" alt="image"
src="https://github.com/user-attachments/assets/d2fa3495-0b31-483c-97d8-12fed6819e24"
/>
Closes#22136
This pull-request implements a `<ClientFilter />` to our `Request Logs`
page for AI Bridge. This will allow the user to select a client which
they wish to filter against. Technically the backend is able to actually
filter against multiple clients at once however the frontend doesn't
currently have a nice way of supporting this (future improvement).
<img width="1447" height="831" alt="image"
src="https://github.com/user-attachments/assets/0be234e2-25f2-4a89-b971-d74817395da1"
/>
---------
Co-authored-by: Jeremy Ruppel <jeremy.ruppel@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses chat page rendering performance. Profiling with React Profiler
showed `AgentChat` actual render times of 20–31ms (exceeding the
16ms/60fps
budget), with `StickyUserMessage` as the #1 component bottleneck at
35.7%
of self time.
## Changes
**Hoist `createComponents` to module scope** (`response.tsx`):
Previously every `<Response>` instance called `createComponents()` per
render, creating a fresh components map that forced Streamdown to
discard
its cached render tree. Now both light/dark variants are precomputed
once
at module scope.
**Wrap `StickyUserMessage` in `memo()`** (`ConversationTimeline.tsx`):
Profile-confirmed #1 bottleneck. Each instance carries
IntersectionObserver
+ ResizeObserver + scroll handlers; skipping re-render avoids all that
setup.
**Wrap `ConversationTimeline` in `memo()`**
(`ConversationTimeline.tsx`):
Prevents cascade re-renders from the parent when props haven't changed.
**Remove duplicate `buildSubagentTitles`** (`ConversationTimeline.tsx` →
`AgentDetailContent.tsx`): Was computed in both `AgentDetailTimeline`
and
`ConversationTimeline`. Now computed once and passed as a prop.
<details>
<summary>Profiling data & analysis</summary>
### Profiler Metrics
| Metric | Value |
|--------|-------|
| INP (Interaction to Next Paint) | 82ms |
| Processing duration (event handlers) | 52ms |
| AgentChat actual render | 20–31ms (budget: 16ms) |
| AgentChat base render (no memo) | ~100ms |
### Top Bottleneck Components (self-time %)
| Component | Self Time | % |
|-----------|-----------|---|
| StickyUserMessage | 11.0ms | 35.7% |
| ForwardRef (radix-ui) | 7.4ms | 24.0% |
| Presence (radix-ui) | 2.0ms | 6.5% |
| AgentChatInput | 1.4ms | 4.5% |
### Decision log
- Chose module-scope precomputation over `useMemo` for
`createComponents`
because there are only two possible theme variants and they're static.
- Did not add virtualization — sticky user messages + scroll anchoring
make it complex. The memoization fixes should be measured first.
- Did not wrap `BlockList` in `memo()` — the React Compiler (enabled for
`pages/AgentsPage/`) already auto-memoizes JSX elements inside it.
- Phase 2 (verify React Compiler effectiveness on
`parseMessagesWithMergedTools`)
and Phase 3 (radix-ui Tooltip lazy-mounting) deferred to follow-up PRs.
</details>
Add a per-MCP-server `model_intent` toggle that wraps tool schemas with
a
`model_intent` field, requiring the LLM to provide a human-readable
description of each tool call's purpose. The intent string is shown as a
status label in the UI instead of opaque tool names, and is
transparently
stripped before the call reaches the remote MCP server.
Built-in tools have rich specialized renderers (terminal blocks, file
diffs,
etc.) and don't need this. MCP tools hit `GenericToolRenderer` which
only
shows raw tool names and JSON — that's where model_intent adds value.
The model learns what to provide via the JSON Schema `description` on
the
`model_intent` property itself — no system prompt changes needed.
<details>
<summary>Implementation details</summary>
### Architecture
Inspired by the `withModelIntent()` pattern from `coder/blink`, adapted
for
Go + React. The wrapping is entirely in the `mcpclient` layer — tool
implementations never see `model_intent`.
**Schema wrapping** (`mcpToolWrapper.Info()`): When enabled, wraps the
original tool parameters under a `properties` key and adds a
`model_intent`
string field with a rich description that teaches the model inline.
**Input unwrapping** (`mcpToolWrapper.Run()`): Strips `model_intent` and
unwraps `properties` before forwarding to the remote MCP server. Handles
three input shapes models may produce:
1. `{ model_intent, properties: {...} }` — correct format
2. `{ model_intent, key: val, ... }` — flat, no wrapper
3. Malformed — falls through gracefully
**Frontend extraction**: `streamState.ts` extracts `model_intent` from
incrementally parsed streaming JSON. `messageParsing.ts` extracts it
from
persisted tool call args.
**UI rendering**: `GenericToolRenderer` shows the capitalized intent
string
as the primary label when available, falling back to the raw tool name.
### Changes
- Database: `model_intent` boolean column on `mcp_server_configs`
- SDK: `ModelIntent` field on config/create/update types
- API: pass-through in create/update handlers + converter
- mcpclient: schema wrapping in `Info()`, input unwrapping in `Run()`
- Frontend: extraction from streaming + persisted args
- UI: intent label in `GenericToolRenderer`, toggle in admin panel
- Tests: 6 new tests (schema wrapping, unwrapping, passthrough,
fallback)
### Decision log
- **Option lives on MCPServerConfig, not model config**: Built-in tools
already have rich renderers; only MCP tools benefit from model_intent.
- **No system prompt changes**: The JSON Schema `description` on the
`model_intent` property teaches the model inline.
- **Pointer bool on update request**: Follows existing pattern (`*bool`)
so PATCH requests don't reset the value when omitted.
</details>
## Summary
Add site-wide banners for AI Governance seat usage thresholds:
1. **90% capacity warning (admin-only):** When actual AI Governance
seats are ≥90% and <100% of the license limit, admins see:
> "You have used 90% of your AI governance add-on seats."
2. **Over-limit banner (admin-only):** When actual seats exceed the
license limit, admins see a prominent warning:
> "Your organization is using {actual} / {limit} AI Governance user
seats ({X}% over the limit). Contact sales@coder.com"
- Uses floor whole percentage (Go int division / `Math.floor`)
- Includes a clickable `mailto:sales@coder.com` link
Adds preview cards for the `spawn_computer_use_agent` tool. Spawning
that agent now renders a rich saying "Spawning computer use sub-agent".
The "waiting for" tool now displays an inline desktop preview which can
be clicked to reveal the desktop in the sidebar.
https://github.com/user-attachments/assets/e486ca0e-a569-4142-bb12-db3b707967b8
- Adds `GET /api/experimental/chats/by-workspace` endpoint that returns
workspace_id → latest chat_id mapping
- Modifies FE to fetch this alongside the workspace list, gated on
`agents` experiment and render an "Agent" badge similar to the existing
"Task" badge in `WorkspacesTable`
- Badge links to the "latest chat" linked to the given workspace.
Notes:
- Intentionally uses `fetchWithPostFilter` for RBAC to decouple from
workspaces API — will migrate to `workspaces_expanded` view later.
- If users have multiple chats linked to the same workspace, the badge
will link to the most recently updated one.
> 🤖 This PR was created with the help of Coder Agents, and has been
reviewed by my human. 🧑💻
Previously, long bash commands in the execute tool were truncated with
an ellipsis and could not be viewed in full. The only way to see the
full command was to copy it via the copy button.
Adds overflow detection and an inline expand/collapse chevron next to
the copy button. Clicking the command text or the chevron toggles
between truncated and wrapped views. Short commands that fit on one line
are visually unchanged.
https://github.com/user-attachments/assets/88ec6cd4-5212-4608-9a90-9ce217d5dce7
EDIT: couldn't be bothered re-recording the video but the chevron is
hidden until hovered now, like the copy button.
## Summary
Tool call failures in `/agents` previously displayed alarming red
styling (red icons, red text, red alert icons) that made it look like
the user did something wrong. This PR replaces the scary error
presentation with a calm, unified style and adds a dedicated timeout
display for subagent tools.
## Changes
### Unified failure style (all tools)
- Replace red `CircleAlertIcon` + `text-content-destructive` with a
muted `TriangleAlertIcon` in `text-content-secondary` across **all 11
tool renderers**.
- Remove red icon/label recoloring on error from `ToolIcon` and all
specialized tool components.
- Error details remain accessible via tooltip on hover.
### Subagent timeout display
- `ClockIcon` with "Timed out waiting for [Title]" instead of a generic
error display.
- `CircleXIcon` for non-timeout subagent errors with proper error verbs
("Failed to spawn", "Failed waiting for", etc.) instead of the
misleading running verb ("Waiting for").
- Timeout detection from result string/error field containing "timed
out".
### Title resolution for historical messages
- `ConversationTimeline` now computes `subagentTitles` via
`useMemo(buildSubagentTitles(...))` and passes it to historical
`ChatMessageItem` rendering, so `wait_agent` can resolve the actual
agent title from a prior `spawn_agent` result even outside streaming
mode.
### Stories
8 new stories: `GenericToolFailed`, `GenericToolFailedNoResult`,
`SubagentWaitTimedOut`, `SubagentWaitTimedOutWithTitle`,
`SubagentWaitTimedOutTitleFromMap`, `SubagentSpawnError`,
`SubagentWaitError`, `MCPToolFailedUnifiedStyle`.
## Files changed (15)
- `tool/Tool.tsx` — GenericToolRenderer + SubagentRenderer
- `tool/SubagentTool.tsx` — timeout/error verbs, icon changes
- `tool/ToolIcon.tsx` — remove destructive recoloring
- `tool/*.tsx` (10 specialized tools) — unified warning icon
- `ConversationTimeline.tsx` — pass subagentTitles to historical
rendering
- `tool.stories.tsx` — 8 new stories, updated existing assertions
Wraps the `GenericToolRenderer` (used for MCP and unrecognized tools) in
`ToolCollapsible` so the result content is hidden behind a
click-to-expand chevron, matching the pattern used by `read_file`,
`write_file`, and other built-in tool renderers.
### Changes
- Move `ToolIcon` + `ToolLabel` into the `ToolCollapsible` `header` prop
- Compute `hasContent` from `writeFileDiff` / `fileContent` /
`resultOutput` — when there's no content, the header renders as a plain
div with no chevron
- Remove `ml-6` from `ScrollArea` classNames (the `ToolCollapsible`
button handles its own layout)
- `defaultExpanded` is `false` by default in `ToolCollapsible`, so
results start collapsed
### Before
MCP tool results were always fully visible inline.
### After
MCP tool results are collapsed by default with a chevron toggle,
consistent with `read_file`, `edit_files`, `list_templates`, etc.
Adds a `propose_plan` tool that presents a workspace markdown file as a
dedicated plan card in the agent UI.
The workflow is: the agent uses `write_file`/`edit_files` to build a
plan file (e.g. `/home/coder/PLAN.md`), then calls `propose_plan(path)`
to present it. The backend reads the file via `ReadFile` and the
frontend renders it as an expanded markdown preview card.
**Backend** (`coderd/x/chatd/chattool/proposeplan.go`): new tool
registered as root-chat-only. Validates `.md` suffix, requires an
absolute path, reads raw file content from the workspace agent. Includes
1 MiB size cap.
**Frontend** (`site/src/components/ai-elements/tool/`): dedicated
`ProposePlanTool` component with `ToolCollapsible` + `ScrollArea` +
`Response` markdown renderer, expanded by default. Custom icon
(`ClipboardListIcon`) and filename-based label.
**System prompt** (`coderd/x/chatd/prompt.go`): added `<planning>`
section guiding the agent to research → write plan file → iterate → call
`propose_plan`.
Omit rehype-raw from the Streamdown rehype plugin list so
HTML-like syntax in LLM output is escaped as text instead of
being parsed by the HTML5 engine and stripped by rehype-sanitize.
When the LLM writes JSX fragments like <Component prop={val} />
outside code fences, remark-parse tags them as html nodes.
rehype-raw then feeds them to parse5, and rehype-sanitize strips
the unknown elements, silently destroying content. Without
rehype-raw, Streamdown auto-injects a remark plugin that converts
html nodes to text, preserving them as visible escaped text.
Markdown formatting (bold, italic, links, code blocks, tables)
is unaffected since those go through remark/rehype directly.