Commit Graph

55 Commits

Author SHA1 Message Date
Danielle Maywood 599f21afa3 feat(site): opt AgentsPage and ai-elements into React Compiler (#23371) 2026-03-20 19:55:35 +00:00
Danielle Maywood 25445714b3 fix(site): reduce unnecessary re-renders and network calls (#23341) 2026-03-20 09:30:17 +00:00
Danielle Maywood cf0c4d0dcf fix(site): hoist model queries out of AgentDetail (#23324) 2026-03-19 21:41:00 +00:00
Mathias Fredriksson f31a8277a9 fix: show promoted queued message in chat timeline immediately (#23232)
Two issues caused the promoted message to never appear:

1. handlePromoteQueuedMessage discarded the ChatMessage returned by
the promote API, relying on the WebSocket to deliver it.

2. Even when the WebSocket did deliver it (via upsertDurableMessage),
the queue_update event in the same batch called
updateChatQueuedMessages, which mutated the React Query cache. This
gave chatMessagesList a new reference, triggering the message sync
effect. The effect found the promoted message in the store but not in
the REST-fetched data, classified it as a stale entry (the path
designed for edit truncation), and called replaceMessages, wiping it.

Fix (1): capture the ChatMessage from the promote response and upsert
it into the store, matching handleSend for non-queued messages.

Fix (2): track the fetched message array elements across effect runs
using element-level reference comparison. Only run the
hasStaleEntries/replaceMessages path when the message objects actually
changed (e.g. a refetch producing new objects from the server), not
when only an unrelated field like queued_messages caused the query
data reference to update. Element references work because
useMemo(flatMap) preserves object identity when only non-message
fields change in the page data.
2026-03-19 15:27:03 +02:00
Michael Suchacz 5b9a9e5bdf fix(site): guard malformed agent model refs (#23252)
## Summary
- guard Agent pages against malformed model provider/model values before
trimming
- reuse a shared model-ref normalizer across Agent detail, sidebar,
list, and create flows
- add regression coverage for malformed catalog and config entries

## Validation
- `cd site && pnpm exec vitest run
src/pages/AgentsPage/modelOptions.test.ts
src/pages/AgentsPage/AgentDetail.test.ts`
- `cd site && pnpm lint:types`
2026-03-19 12:27:24 +01:00
Thomas Kosiewski 20ac96e68d feat(site): include chatId in editor deep links (#23214)
## 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`_
2026-03-18 14:25:38 +01:00
Michael Suchacz 62144d230f feat(site): show PR link in TopBar header (#23178)
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.
2026-03-18 13:40:33 +01:00
Hugo Dutka 817fb4e67a feat: virtual desktop settings toggle frontend (#23173)
Add a toggle in agents settings to enable/disable virtual desktop. The
Desktop tab (next to the Git tab) will only be visible if the feature is
enabled.

<img width="879" height="648" alt="Screenshot 2026-03-17 at 18 01 26"
src="https://github.com/user-attachments/assets/09fc3850-c88d-4c5c-b6e4-760590e53b95"
/>
2026-03-18 09:50:14 +01:00
Danielle Maywood 41d12b8aa3 feat(site): improve edit-message UX with dedicated button and confirmation (#23172) 2026-03-17 17:39:28 +00:00
Kyle Carberry eb828a6a86 fix: skip input refocus after send on mobile viewports (#23141) 2026-03-17 12:40:26 +00:00
Mathias Fredriksson 4e2d7ffaa7 refactor(site/src/pages/AgentsPage): use ChatMessagePart for editingFileBlocks (#23151)
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
2026-03-17 04:10:08 -08:00
Mathias Fredriksson 524bca4c87 fix(site/src/pages/AgentsPage): fix chat image paste bugs and refactor queued message display (#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.
2026-03-17 14:00:31 +02:00
Michael Suchacz 7cca2b6176 feat(site): add chat spend limit UI (#23072)
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
2026-03-17 02:01:51 +01:00
Kyle Carberry 741af057dc feat: paginate chat messages endpoint with cursor-based infinite scroll (#23083)
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.
2026-03-16 16:40:59 +00:00
Kyle Carberry 27cbf5474b refactor: remove /diff-status endpoint, include diff_status in chat payload (#23082)
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
2026-03-16 14:40:22 +00:00
Hugo Dutka 85509733f3 feat: chat desktop frontend (#23006)
https://github.com/user-attachments/assets/26f9c210-01ad-4685-aff1-7629cf3854f1
2026-03-13 19:01:50 +00:00
Kyle Carberry 690e3a87d8 feat: move chat messages to dedicated /chats/{id}/messages endpoint (#23021)
## Summary

Moves the messages response out of `GET /chats/{id}` and into a
dedicated `GET /chats/{id}/messages` endpoint.

### Backend
- `GET /chats/{id}` now returns just the `Chat` object (no messages)
- `GET /chats/{id}/messages` is a new endpoint returning
`ChatMessagesResponse` with `messages` and `queued_messages`
- Added `ChatMessagesResponse` SDK type and `GetChatMessages` client
method

### Frontend
- `getChat()` API method returns `Chat` instead of `ChatWithMessages`
- Added `getChatMessages()` API method for the new endpoint
- Split `chatQuery` into two: `chatQuery` (metadata) and
`chatMessagesQuery` (messages)
- Updated all cache mutations, optimistic updates, and websocket
handlers
- Updated tests and stories

### Files changed
| File | Change |
|---|---|
| `coderd/coderd.go` | Register `GET /messages` route |
| `coderd/chats.go` | Simplify `getChat`, add `getChatMessages` handler
|
| `codersdk/chats.go` | New type + method, update `GetChat` return |
| `site/src/api/api.ts` | New method, update `getChat` |
| `site/src/api/queries/chats.ts` | New query, update cache mutations |
| `site/src/pages/AgentsPage/AgentDetail.tsx` | Use separate queries |
| `site/src/pages/AgentsPage/AgentDetail/ChatContext.ts` | Update types
and cache writes |
| `site/src/pages/AgentsPage/AgentsPage.tsx` | Update websocket cache
handler |
2026-03-13 08:35:46 -04:00
Danielle Maywood 3aada03f52 fix(site): prevent layout shift when agent chat right panel loads (#22983) 2026-03-12 14:09:12 +00:00
Kyle Carberry b898e45ec4 feat(site): rewrite localhost URLs in agent chat to port-forward links (#22891)
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 |
2026-03-10 12:57:59 +00:00
Danielle Maywood d61772dc52 refactor(site): separate AgentsPage and AgentDetail into container/view pairs (#22812) 2026-03-10 12:09:48 +00:00
Danielle Maywood 26adc26a26 refactor(site): compute selected model as derived state in AgentDetail (#22816) 2026-03-09 14:57:55 +00:00
Kyle Carberry 3f939375fa refactor: unify agent sidebar into generic tabbed panel with Git sub-views (#22837)
## 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
2026-03-09 10:44:56 -04:00
Cian Johnston 5b7ba93cb2 fix(site): only use git watcher when workspace agent connected (#22714)
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.
2026-03-09 08:53:00 +00:00
Kyle Carberry 2ad0e74e67 feat(site): add diff line reference and annotation system for agents chat (#22697)
## 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
2026-03-08 15:38:37 -04:00
Danielle Maywood 6509fb2574 fix(site): use declarative title elements in AgentsPage (#22806) 2026-03-08 18:31:22 +00:00
Danielle Maywood 69a4a8825d refactor(site): add readonly to array props in AgentsPage (#22815) 2026-03-08 17:46:03 +00:00
Kyle Carberry 3608064600 fix: prevent agents right panel from covering chat on mobile (#22744) 2026-03-06 17:08:12 -08:00
Danielle Maywood b199ef1b69 fix(site): polish agents diff panel UI (#22723) 2026-03-06 21:10:12 +00:00
Kyle Carberry 5712faaa2c fix: always open git panel from the right, full width on mobile (#22718)
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`.
2026-03-06 15:11:08 -05:00
Mathias Fredriksson a104d608a3 feat: add file/image attachment support to chat input (#22604)
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.
2026-03-06 21:05:26 +02:00
Hugo Dutka 6665944740 feat: agents git watch frontend (#22570)
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.
2026-03-06 15:17:14 +01:00
Kyle Carberry 76076de1ca feat(site): polish right panel tabs, diff stats, and file headers (#22636)
Polishes the right panel UI introduced in #22633:

<img width="3138" height="1596" alt="image"
src="https://github.com/user-attachments/assets/d3947db0-6600-4469-b7e2-6eb80aadb7bc"
/>

Over 2k lines of this is just the Seti font definition.

The file tree view isn't actually adjusting much, it's just a scroll
helper. Soon I'll add a comment system so users can leave agents
feedback directly from the code.
2026-03-05 07:38:49 -05:00
Kyle Carberry 6afcc7b904 refactor(site): replace DiffRightPanel with generic tabbed RightPanel (#22633)
Replace the single-purpose DiffRightPanel with a generic RightPanel
component that supports tabs, drag-resize, drag-to-snap, and
drag-to-collapse-sidebar.

## Changes

- **New `RightPanel.tsx`**: generic tabbed panel with:
  - Drag handle with pointer capture for smooth resizing
  - Snap thresholds: drag past max → expand, drag below min → close
- Live sidebar collapse when dragging to the left viewport edge (and
reverses if dragged back)
  - Persisted width via localStorage
- `onVisualExpandedChange` callback so parent syncs sibling visibility
during drag (not just on pointer-up)
- **Deleted `DiffRightPanel.tsx`**
- **Updated `AgentDetail.tsx`**: uses `RightPanel` with `tabContent`
record, tracks `dragVisualExpanded` for live chat section hiding
- **Updated `FilesChangedPanel.tsx`**: removed border/background (now
handled by RightPanel wrapper)

## Drag behavior

| Gesture | Effect |
|---|---|
| Drag left past 70vw + 80px | Snap to expanded (fullscreen within
parent) |
| Drag right below 360px - 80px | Snap to closed |
| Drag to left viewport edge (<80px) | Collapse sidebar live |
| Drag back from left edge | Uncollapse sidebar live |
| Start expanded, drag right | Live resize back to normal |
2026-03-05 00:37:17 +00:00
Kyle Carberry 1635b18856 fix: persist draft message in localStorage on agent detail page (#22600)
## Problem

On the `/agents/:agentId` detail page, text typed into the chat input
was lost when navigating away and returning. The empty-state page
(`/agents`) already persisted drafts via `localStorage`, but individual
conversation pages did not.

## Solution

Adds per-conversation draft persistence to `useConversationEditingState`
in `AgentDetail.tsx`, following the same patterns used elsewhere in the
agents page:

- Drafts are stored under `agents.draft-input.<chatID>` keys
- The saved draft is read as the editor's initial value on mount
- `localStorage` is updated on every content change
- The key is removed when the input is cleared or a message is sent
successfully
2026-03-04 14:42:13 +00:00
Danielle Maywood 90f686d684 feat(agents): add unarchive agent support (#22579) 2026-03-04 14:08:12 +00:00
Kyle Carberry 012a0497ce fix(agents): remove optimistic message rendering and fix auto-promote delivery (#22588)
## Problem

Two bugs in the agents chat flow:

1. **Optimistic rendering glitch**: When sending a message while the
agent is busy, a fake message with a negative ID appears in the
timeline, then gets rolled back to the queued state. This causes a
jarring flash.

2. **Auto-promoted messages not appearing**: When the server
auto-promotes a queued message after finishing a task, the promoted user
message doesn't show up in the timeline until the LLM finishes its
response.

## Root Causes

**Bug 1**: The optimistic rendering system injected placeholder messages
with `id: -Date.now()` into the store. When the server responded with
`queued: true`, the optimistic message was rolled back — but the user
had already seen it flash in the timeline.

**Bug 2**: In `processChat`'s deferred cleanup, the auto-promoted
message was published via `publishEvent()`, which only delivers to local
in-process stream subscribers. The SSE subscriber goroutine only
forwards `message_part` events from the local channel — it ignores
`message` events. Durable events reach the SSE client via pubsub → DB
read, but `publishEvent` doesn't trigger a pubsub notification. The
explicit `PromoteQueued` endpoint correctly used `publishMessage()`
(which does both), but the auto-promote path did not.

## Changes

### Frontend (`site/`)
- **AgentDetail.tsx**: Remove optimistic message injection from send and
edit flows. Instead, use the `CreateChatMessageResponse.message` from
the POST response to insert the real server message into the store
immediately.
- **ChatContext.ts**: Remove the negative-ID cleanup logic from
`upsertDurableMessage` that stripped optimistic placeholders when real
messages arrived.
- **chatStore.test.ts**: Remove 2 tests for negative-ID optimistic
message behavior.

### Backend (`coderd/chatd/`)
- **chatd.go**: In `processChat` cleanup, replace `publishEvent()` with
`publishMessage()` for auto-promoted messages. This ensures the pubsub
notification (`AfterMessageID`) is sent, so SSE subscribers read the new
message from the DB immediately.
2026-03-04 07:49:39 -05:00
Danielle Maywood 2882e36222 fix(site): move chat input outside flex-col-reverse scroller (#22585) 2026-03-04 01:04:04 +00:00
Matt Vollmer 39bde165b8 fix(site): open View Workspace link in new window on agents page (#22578)
On the `/agents` page, the "View Workspace" link in the header dropdown
menu was navigating in the same tab via `navigate()`. This changes it to
`window.open(workspaceRoute, "_blank")` so it opens in a new browser
window/tab instead.

It's frustrating when I want to view my workspace and then I have to go
back and find my chat.
2026-03-03 17:10:11 -05:00
Kyle Carberry 2ceac319b8 fix(site): eagerly fetch API key for Open in Cursor/VS Code buttons (#22554)
## Problem

The **Open in Cursor** and **Open in VS Code** buttons on the agent
detail page were broken. Clicking them did nothing.

### Root Cause

The `handleOpenInEditor` handler in `AgentDetail.tsx` called
`window.location.assign()` with a custom protocol URI (`vscode://` or
`cursor://`) **after** an `await API.getApiKey()` call. This creates an
async boundary that breaks the browser's user gesture chain, causing
custom protocol navigations (`vscode://`, `cursor://`) to be silently
blocked by the browser.

The handler was invoked from a Radix `DropdownMenuItem.onSelect`, which
adds another layer of event indirection that makes the gesture chain
more fragile.

In contrast, the workspace page's `VSCodeDesktopButton` works because it
uses a direct `onClick` handler on a button element.

## Fix

- **Eagerly fetch and cache the API key** via `useQuery` when workspace
and agent data is available
- **Make `handleOpenInEditor` synchronous** — it reads the cached key
instead of awaiting a network call, keeping `window.location.assign()`
within the original user gesture context
- **Disable buttons** while the API key is still loading
(`canOpenEditors` now gates on key availability)
- **Simplify** the `onOpenInEditor` callback (remove `void` async
wrapper)
2026-03-03 16:54:27 +00:00
Cian Johnston 3c4a416b55 feat(site): add Open Terminal and Copy SSH Command to agent chat TopBar (#22529)
Adds two new items to the agent chat TopBar dropdown menu:

- **Open Terminal**: opens the workspace web terminal in a new browser
window, reusing the existing `getTerminalHref`/`openAppInNewWindow`
infra.
- **Copy SSH Command**: copies the SSH command (e.g. `ssh
agent.workspace.owner.suffix`) to the clipboard with a toast
confirmation. Only shown when the deployment SSH hostname suffix is
configured.

Both items appear after a separator below the existing editor/workspace
actions.

## Changes

| File | What |
|---|---|
| `TopBar.tsx` | Added `Open Terminal` and `Copy SSH Command` dropdown
items with separator, `TerminalIcon`/`CopyIcon`, toast on copy |
| `AgentDetail.tsx` | Wired up `getTerminalHref`, `openAppInNewWindow`,
`deploymentSSHConfig` query, and passed new props to TopBar in all 3
render paths |
| `TopBar.stories.tsx` | Added new fields to default story props |
2026-03-03 11:44:39 +00:00
Danielle Maywood c483bfa24f refactor(site): convert archive agent callbacks to React Query mutations (#22542) 2026-03-03 11:03:59 +00:00
Kyle Carberry b8a74a4fcb feat: add confirmation dialog when archiving chat with workspace (#22524)
When archiving a chat that has an attached workspace, a dialog now pops
up asking whether to also delete the associated workspace.

## Changes

### New file: `ArchiveAgentDialog.tsx`
A Radix-based dialog component that appears when archiving a chat that
has a `workspace_id`. It provides:
- A checkbox to opt into deleting the associated workspace
- **Cancel** — closes without archiving
- **Archive only** — archives the chat, leaves the workspace intact
- **Archive & Delete Workspace** — archives the chat and triggers
workspace deletion (enabled only when checkbox is checked)

### Modified: `AgentsPage.tsx`
- Extracted archive logic into a `performArchive` helper
- `requestArchiveAgent` now checks if the chat has a `workspace_id`:
  - If yes, opens the `ArchiveAgentDialog`
  - If no, proceeds with archiving directly (existing behavior)
- Added `handleArchiveOnly`, `handleArchiveAndDeleteWorkspace`, and
`handleCloseArchiveDialog` handlers
- Renders the `<ArchiveAgentDialog>` at the page level

Chats without a workspace are archived immediately as before — no UX
change for those.
2026-03-02 18:52:51 -05:00
Kyle Carberry 7e0895a1ee fix(site): roll back optimistic message when server queues it (#22522)
## Problem

When a user sends a message while the agent is busy, the message appears
in the chat timeline as if it was sent and being processed (with the
"Thinking..." shimmer), instead of appearing in the queued messages list
above the input.

## Root Cause

`handleSend` in `AgentDetail.tsx` unconditionally injects an optimistic
user message into the conversation timeline and sets chat status to
`"pending"` **before** awaiting the server response. However, the server
can respond with `{ queued: true, queued_message: {...} }` (via
`CreateChatMessageResponse`) when the agent is already busy — meaning
the message was queued, not processed.

The client never inspected `response.queued` after the request
succeeded, so the optimistic message stayed in the timeline even though
the server queued it.

## Fix

After `sendMutation.mutateAsync(request)` resolves, check
`response.queued`. If true, roll back the optimistic message and restore
the previous chat status. The `queue_update` SSE event from the
WebSocket stream handles adding it to the queued messages list.

## Changes

- **`site/src/pages/AgentsPage/AgentDetail.tsx`**: Capture the response
from `sendMutation.mutateAsync` and roll back the optimistic message +
status when `response.queued === true`.
2026-03-02 16:31:12 -05:00
Kyle Carberry e3c5d734ba fix(site): move gradient mask below title bar in agent detail (#22515)
The gradient mask overlay was positioned at the top of the parent
container (`absolute top-0`), causing it to overlap the title bar
instead of fading the scroll content beneath it.

**Changes:**
- Wrap the TopBar, archived banner, and gradient in a `relative z-10
shrink-0 overflow-visible` container
- Change the gradient from `top-0` to `top-full` so it anchors to the
bottom of the title bar and fades downward over the message area
2026-03-02 16:16:09 -05:00
Kyle Carberry 2f684002b8 fix(site): stay on archived chat instead of redirecting (#22505)
When archiving a chat, the frontend no longer navigates away to a
different chat. Instead it stays on the current chat and shows an
archived state.

## Changes

**AgentsPage.tsx** — Removed the redirect logic from
`requestArchiveAgent`. After a successful archive, invalidates the
individual chat query so the detail view picks up the `archived` flag
immediately.

**AgentDetail.tsx** — Detects `chatRecord.archived` and:
- Disables the chat input
- Shows a banner: "This agent has been archived and is read-only."
- Passes `isArchived` to the top bar
- Guards `handleArchiveAgentAction` against double-archiving

**AgentDetail/TopBar.tsx** — When `isArchived`:
- Shows an "Archived" badge next to the chat title
- Hides the "Archive Agent" dropdown menu item

**AgentDetail/TopBar.stories.tsx** — Added an `Archived` story variant.
2026-03-02 19:43:45 +00:00
Kyle Carberry 897f178a5c feat(site): replace Agent chat textarea with Lexical editor (#22449)
## Summary

Replaces the plain `<TextareaAutosize>` in the Agent chat input
(`AgentChatInput`) with a Lexical-based editor component, matching the
pattern used in [coder/blink](https://github.com/coder/blink).

## What changed

### New component: `ChatMessageInput`
`site/src/components/ChatMessageInput/ChatMessageInput.tsx`

A Lexical-powered text input that behaves as a plain-text editor with:
- **Enter** submits, **Shift+Enter** inserts newline
- Rich-text formatting disabled (Cmd+B/I/U blocked)
- Paste sanitization (strips formatting, inserts plain text)
- Undo/redo via HistoryPlugin
- Imperative ref API: `insertText()`, `clear()`, `focus()`, `getValue()`

### Updated components
- **`AgentChatInput.tsx`** — Swapped `<TextareaAutosize>` for
`<ChatMessageInput>`. Moved from controlled `value`/`onChange` to
ref-based pattern with `initialValue`/`onContentChange`.
- **`AgentDetail.tsx`** — Updated to use `useRef` for input value
tracking and `editorInitialValue` state for editor resets (edit/cancel
flows).
- **`AgentsPage.tsx`** — Updated to use `useRef` + `initialValue`
pattern.
- **`AgentChatInput.stories.tsx`** — Updated prop names.

### Why Lexical?
This lays the groundwork for features that a native `<textarea>` can't
support:
- Ghost text / inline autocomplete suggestions
- @-mentions and slash commands
- Programmatic text insertion (e.g. from speech-to-text)
- Custom inline decorators (chips, pills, badges)
- Syntax-highlighted code blocks

No adornments are added in this PR — it's a drop-in replacement that
matches existing behavior.

---------

Co-authored-by: Coder <coder@coder.com>
2026-02-28 22:18:36 -05:00
Danielle Maywood 7860b99597 refactor(site): refactor AgentsPage createPortal soup (#22438) 2026-02-28 22:11:11 +00:00
Kyle Carberry 34d9392e37 chore(db): remove workspace_agent_id from chats table (#22442)
## Summary

Remove the `workspace_agent_id` column from the `chats` table and
dynamically look up the first workspace agent instead.

## Problem

When a workspace is stopped and restarted, the workspace agent gets a
new ID. The `workspace_agent_id` stored on the chat at creation time
becomes stale, making the agent unreachable. This caused chats to break
after workspace restarts.

## Solution

Instead of persisting the agent ID, dynamically look up the first agent
from the workspace's latest build via
`GetWorkspaceAgentsInLatestBuildByWorkspaceID` whenever an agent
connection is needed. The `workspace_id` on the chat remains stable
across restarts.

This behavior may be refined later (e.g., agent selection heuristics),
but picking the first agent resolves the immediate breakage.

## Changes

- **Migration 000425**: Drop `workspace_agent_id` column from `chats`
- **SQL queries**: Remove `workspace_agent_id` from `InsertChat` and
`UpdateChatWorkspace`
- **chatd.go**: `getWorkspaceConn` and `resolveInstructions` now look up
agents dynamically from workspace ID
- **chatd.go**: Remove `refreshChatWorkspaceSnapshot` (no longer needed)
- **createworkspace.go**: Stop persisting agent ID when associating
workspace with chat
- **subagent.go**: Stop passing agent ID to child chats
- **SDK/frontend**: Remove `WorkspaceAgentID` / `workspace_agent_id`
from Chat type

---------

Co-authored-by: Kyle Carberry <kylecarbs@gmail.com>
2026-02-28 16:46:51 -05:00
Kyle Carberry 0ad2f9ecd7 feat(chatd): persist last_error on chats table (#22436)
Adds a nullable `last_error` column to the `chats` table so error
reasons survive page reloads.

**Backend:**
- Migration adds `last_error TEXT` (nullable) to chats
- `UpdateChatStatus` writes the error reason when status transitions to
`error`, clears it (NULL) on recovery
- `convertChat` maps `sql.NullString` to `*string` in the SDK

**Frontend:**
- Sidebar falls back to `chat.last_error` when no stream error reason is
cached
- Chat detail page does the same for `persistedErrorReason`
- Fixtures updated for new required field
2026-02-28 12:27:26 -05:00
Danielle Maywood 1dec6da358 refactor(site): simplify AgentChatInput into a controlled component (#22426) 2026-02-28 10:32:48 +00:00