Commit Graph
4319 Commits
Author SHA1 Message Date
Jake Howell 3fca20df65 feat: move <UserAutocomplete /> to <Combobox /> (#22590)
This pull-request takes our super MUI-based `<UserAutocomplete />` and
migrates it to a common `<Combobox />` so that things will render
correctly inline and use the new `shadcn` style system.

| Old | New |
| --- | --- |
| <img width="1139" height="220"
src="https://github.com/user-attachments/assets/046b32d0-702c-4f03-b039-91f4b1e30cd1"
/> | <img width="1139" height="220"
src="https://github.com/user-attachments/assets/74244768-198a-4a28-acb9-a93ff570e010"
/> |
2026-03-05 14:22:37 +11: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 30d534b36b fix(chatd): fix relay race conditions, extract enterprise relay logic, move pubsub to OSS (#22589)
## Summary

Fixes a bug where interrupting a streaming chat and sending a new
message
left the relay connected to the wrong replica. Expanded into a broader
refactor that cleanly separates concerns:

- **OSS** owns pubsub subscription, message catch-up, queue updates,
  status forwarding, and local parts merging.
- **Enterprise** (`enterprise/coderd/chatd`) only manages relay dialing,
  reconnection, and stale-dial discarding for cross-replica streaming.

## Architecture

### OSS `coderd/chatd/chatd.go`

`Subscribe()` builds the initial snapshot then runs a single merge
goroutine that handles:

- Pubsub subscription for durable events (status, messages, queue,
errors)
- Message catch-up via `AfterMessageID`
- Local `message_part` forwarding
- Relay events from enterprise (when `SubscribeFn` is set)
- Sends `StatusNotification` to enterprise so it can manage relay
lifecycle

Key types:

- `SubscribeFn` — enterprise hook, returns relay-only events channel
- `SubscribeFnParams` — `ChatID`, `Chat`, `WorkerID`,
`StatusNotifications`, `RequestHeader`, `DB`, `Logger`
- `StatusNotification` — `Status` + `WorkerID`, sent to enterprise on
pubsub status changes

### Enterprise `enterprise/coderd/chatd/chatd.go`

`NewMultiReplicaSubscribeFn(cfg MultiReplicaSubscribeConfig)` returns a
`SubscribeFn` that:

- Opens an initial synchronous relay if the chat is running on a remote
worker
- Reads `StatusNotifications` from OSS to open/close relay connections
- Handles async dial, reconnect timers, stale-dial discarding
- Returns only relay `message_part` events

## Bug fixes

### Original bug: stale relay dial after interrupt

`openRelayAsync` goroutines used `mergedCtx` (subscription-level), not a
per-dial context. `closeRelay()` could not cancel in-flight dials. When
the user interrupts and a new replica picks up the chat, the old dial
goroutine could complete after the new one and deliver a stale
`relayResult`.

**Fix**: per-dial `dialCtx`/`dialCancel`, `expectedWorkerID` tracking,
`workerID` on `relayResult`. `closeRelay()` cancels the dial context and
drains `relayReadyCh`. Merge loop rejects mismatched worker IDs.

### Additional fixes

- `statusNotifications` send-on-closed-channel race — goroutine now owns
  `close()` via defer
- Enterprise spin-loop on `StatusNotifications` close — two-value
receive
  with nil-out
- `hasPubsub` set from `p.pubsub != nil` instead of subscription success
  — now tracks actual subscription result
- `lastMessageID` not initialized from `afterMessageID` — caused
  duplicate messages on catch-up
- `wrappedParts` goroutine leaked remote connection on `dialCtx` cancel
- `closeRelay()` did not drain `relayReadyCh`
- `setChatWaiting` race with `SendMessage(Interrupt)` — wrapped in
`InTx`
- `processChat` post-TX side effects fired when chat was taken by
another
  worker — added `errChatTakenByOtherWorker` sentinel
- Cancel closure data race on `reconnectTimer`
- Bare blocking send on pubsub error path
- `localParts` hot-spin after channel close
- No-pubsub branch dropped relay events and initial snapshot
- Failed relay dial caused permanent stall (no reconnect retry)
- DB error during reconnect timer caused permanent stall
- `time.NewTimer` replaced with `quartz.Clock` for testable timing

## Tests

9 enterprise tests covering:

- Relay reconnect on drop (mock clock)
- Async dial does not block merge loop
- Relay snapshot delivery
- Stale dial discarded after interrupt
- Cancel during in-flight dial
- Running-to-running worker switch
- Failed dial retries (mock clock)
- Local worker closes relay
- Multiple consecutive reconnects (mock clock)

All pass with `-race`.
2026-03-04 18:42:28 -05:00
Kyle Carberry 0ccfc4da06 feat(site): add specialized renderer for process_output tool in agent chats (#22628)
Adds a `ProcessOutputTool` component that renders `process_output` tool
calls with a clean terminal-style output block instead of falling
through to the generic JSON renderer.

## Changes

**New file:** `ProcessOutputTool.tsx`
- Output shown directly with no header
- Copy button and status indicators float top-right on hover
- Collapsible output with the same expand/collapse chevron bar used by
`ExecuteTool`
- Exit code badge shown only for non-zero exits
- Spinner shown while process is still running

**Modified files:**
- `Tool.tsx` — `ProcessOutputRenderer` + registered in `toolRenderers`
map
- `ToolIcon.tsx` — `process_output` falls through to `TerminalIcon`
- `ToolLabel.tsx` — shows "Reading process output" label
2026-03-04 18:04:34 -05:00
Danielle Maywood 96926cf189 fix(site): add optimistic updates for chat archive/unarchive (#22622) 2026-03-04 21:51:33 +00:00
Danielle Maywood 93e5d04896 fix(site): only play completion chime for top-level chat agents (#22623) 2026-03-04 21:17:11 +00:00
Kyle Carberry e4bdfbebd3 feat(site): improve agent chat header design (#22621)
## Changes

- **User dropdown → sidebar bottom**: Moved from the TopBar into the
sidebar footer with avatar + display name, whole row clickable to open
the dropdown menu
- **Diff stats inline badge**: Compact green/red pill badge next to the
chat title showing `+additions −deletions`, clickable to toggle the diff
panel
- **Reordered TopBar actions**: Ellipsis menu first, then drawer toggle
button on the far right
- **Notification bell scoped**: Removed from individual chat pages
(remains on `/agents` listing)
- **Cleanup**: Removed unused `signOut`/`buildInfo` destructuring from
AgentsPage

### Files changed
- `site/src/pages/AgentsPage/AgentDetail/TopBar.tsx`
- `site/src/pages/AgentsPage/AgentsPage.tsx`
- `site/src/pages/AgentsPage/AgentsSidebar.tsx`

<img width="1876" height="1597" alt="image"
src="https://github.com/user-attachments/assets/8ec33955-f8b4-4064-9767-19147951b3ff"
/>
2026-03-04 13:35:55 -05:00
Kayla はな e35717bc19 fix: show a notice when workspace sharing is disabled globally in organization settings (#22580) 2026-03-04 11:14:52 -07:00
Kyle Carberry 9db39fb358 fix: remove queue message button and clear localStorage draft on submit (#22615)
Fixes two bugs in the agents chat input:

1. **Remove queue message button next to stop button** — The send button
(which showed a ListPlusIcon during streaming) is now hidden when
streaming and not editing a queued message. Messages are still queued
via Enter key; only the visual button is removed. The stop button
remains.

2. **Clear localStorage draft on submit** — The `agents.empty-input`
localStorage key is now cleared synchronously in `handleSend` before the
async `onCreateChat` call. Previously, the draft was only cleared inside
the async `handleCreateChat` after `mutateAsync` resolved, allowing
Lexical editor change events to re-persist the draft during the async
gap.
2026-03-04 16:30:37 +00:00
Danielle Maywood 474e80b646 feat(site): add completion chime for agent tasks (#22608) 2026-03-04 16:23:31 +00:00
Kyle Carberry 17d214b4a4 fix(site): resolve WS/HTTP race condition on workspace parameters page (#22556)
## Problem

Flaky e2e test: `update workspace, new required, mutable parameter
added`

```
Error: Timed out 15000ms waiting for expect(locator).toHaveValue(expected)
Locator: getByTestId('parameter-field-Sixth parameter').locator('input')
Expected string: "99"
Received string: ""
```

## Root Cause

When the workspace parameters page loads, the WebSocket sends an initial
response with template defaults. For parameters with no default (like
`sixth_parameter`), the server returns `{valid: false, value: ""}`. On
first render, `useSyncFormParameters` sees this invalid server value and
overwrites the form's correctly-autofilled value ("99" from the previous
build) with "".

## Fix

When the server value is `{valid: false}`, preserve the current form
value instead of overwriting with "". This prevents the sync hook from
clobbering autofilled values before the server has had a chance to
process them.

## Verification

- TypeScript: zero type errors
- Biome lint: clean
- Unit tests: 2/2 passing
- **E2E soak test: 849/854 passed across 854 runs (99.5% pass rate)**
  - 0 occurrences of the original flake (empty value on settings page)
- 5 residual failures are a separate pre-existing race in
`fillParameters` where user input is overwritten during the 500ms
debounce window
2026-03-04 16:20:49 +00:00
Danielle Maywood 619023f5fc fix(site): disable chat input editor on archived agent chats (#22609) 2026-03-04 16:19:18 +00:00
Danielle Maywood ac298a2537 perf(site): improve FilesChangedPanel rendering for large diffs (#22610) 2026-03-04 16:07:13 +00:00
Kyle Carberry f56563b406 fix(site): replace modal delete confirmation with inline UI in agents admin (#22587)
## Problem

The agents admin panel (`/agents` → Admin button) is rendered inside a
Radix Dialog (`ConfigureAgentsDialog`). Deleting a model or provider
previously opened a MUI `DeleteDialog` on top, creating a modal-on-modal
situation. The two dialog systems (Radix and MUI) don't coordinate focus
trapping, scroll locking, or backdrop behavior, so the delete
confirmation was broken.

## Solution

Replace the modal `DeleteDialog` in both `ModelForm` and `ProviderForm`
with an inline confirmation strip rendered in the footer area. Clicking
"Delete" now swaps the footer to show:

- A warning message ("Are you sure? This action is irreversible.")
- Cancel and a destructive confirm button with loading spinner

This keeps everything within the existing Radix Dialog content pane — no
layering issues, no second modal.

## Changes

| File | Change |
|---|---|
| `ModelForm.tsx` | Added `isDeleting` prop, changed `onDeleteModel`
signature to async, added `confirmingDelete` state, inline confirmation
footer |
| `ProviderForm.tsx` | Removed `DeleteDialog` import/usage, replaced
with inline confirmation footer |
| `ModelsSection.tsx` | Removed `DeleteDialog` import/usage, removed
`modelToDelete` state, passes new props to `ModelForm` |
2026-03-04 10:09:13 -05: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
Jeremy Ruppel f07fdce20a flake: add page event logging to e2e tests (#22569)
I'm having a hard time reproducing [this
Heisenbug](https://github.com/coder/internal/issues/1154) in PR CI, but
it seems to happen pretty often on main, so I would like to add some
logging for a few more page events to the ones we already have.
2026-03-04 07:39:20 -05:00
Danielle Maywood a0b3a32cd3 fix(site): refactor agents sidebar timestamp/action cell (#22595) 2026-03-04 11:36:54 +00:00
Sas Swart cfcb81fb0f fix: user status change chart accommodates DST (#22191)
closes https://github.com/coder/internal/issues/464

# Summary

This PR resolves a flaky test that was sensitive to DST transitions in
various time zones. The root of the flake was:
* a bug; the query and its tests assume 24 hours per day
* the tests used local system time, which resulted in failures for dates
proximal to DST transitions

# Changes

Query:

The original query assumed 24 hour intervals between each day, which is
not a valid assumption. It now increments `1 day` at a time.

Database tests:

Database level tests for the query all assumed 24 hour days. They now
increment in DST-aware days instead. Instead of using time.Now() as a
base for testing, the test uses a series of dates over the course of an
entire year, to ensure that DST transition dates are present in every
test run.

# API Endpoint

The endpoint that delivers the user status chart now accepts an IANA
timezone name as a parameter and passes it, keeping the existing offset
as a fallback, to the database query.

API level tests were added to ensure the correct response form and error
behaviour. Correctness of content is tested at the database level.
2026-03-04 12:54:39 +02:00
Danielle Maywood 2882e36222 fix(site): move chat input outside flex-col-reverse scroller (#22585) 2026-03-04 01:04:04 +00:00
Kyle Carberry 47199ab475 refactor(site): replace bespoke chat model provider UI with schema-driven rendering (#22577)
## Summary

Replace hand-coded per-provider field components, form state types,
validation schemas, and builder functions with generic schema-driven
code that reads from the auto-generated
`chatModelOptionsGenerated.json`.

## Changes

### `ModelConfigFields.tsx` (492 → 341 lines)
- Remove 6 per-provider components (`OpenAIFields`, `AnthropicFields`,
`GoogleFields`, `OpenAICompatFields`, `OpenRouterFields`,
`VercelFields`)
- Remove exported option arrays (`modelConfigReasoningEffortOptions`,
etc.)
- Add `renderSchemaField()` that dispatches to
`InputField`/`SelectField`/`JSONField` based on `field.input_type` from
the generated schema
- `ModelConfigFields` now calls `getVisibleProviderFields()` instead of
a switch statement
- `GeneralModelConfigFields` now calls `getVisibleGeneralFields()`
instead of hard-coding 6 InputField instances

### `modelConfigFormLogic.ts` (742 → 525 lines)
- Remove 6 per-provider form state types and empty defaults
- Remove 6 per-provider Yup validation schemas
- Remove 6 per-provider builder functions (`buildOpenAIOptions`, etc.)
- Remove 2 switch-case dispatch blocks (validation + build)
- Add `buildEmptyProviderState()` that walks schema fields to create
empty form state
- Add schema-driven `extractModelConfigFormState()` and
`buildModelConfigFromForm()`
- Add `yupTestForField()` + `buildYupSchema()` generating Yup validation
from field metadata
- Lazy-cache per-provider Yup schemas for performance

### `modelConfigFormLogic.test.ts`
- All 83 tests updated for the new nested state shape
- Uses `toContain` for error message assertions since labels now come
from schema descriptions

## Motivation

The auto-generated schema (`chatModelOptionsGenerated.json`) was merged
in #22568 but not yet consumed by the UI. This PR wires it up so that
when a new provider or field is added in Go (`codersdk/chats.go`),
running `make gen` regenerates the JSON schema and the UI automatically
picks up the new fields — no manual TypeScript changes needed.

**Production code reduced from 1234 to 866 lines (-30%).**
2026-03-03 17:52:35 -05:00
Kyle Carberry 4ee5306eca fix(site): request notification permission before push subscription (#22576)
## Problem

The subscribe flow in `useWebpushNotifications` called
`pushManager.subscribe()` without first requesting the `Notification`
permission. When the browser permission state is `"denied"` (e.g. from a
previous prompt dismissal), the browser throws:

```
DOMException: Registration failed - permission denied
```

This surfaced as a confusing error toast on the agents page. The error
has nothing to do with Coder RBAC roles — it's the browser denying the
push subscription because notification permission was previously
declined. An admin who had granted browser permission wouldn't see this;
a user who previously dismissed or denied the prompt would.

## Fix

Added an explicit `Notification.requestPermission()` call before
`pushManager.subscribe()`. This:

1. **Re-prompts** the user if the permission state is `"default"` (not
yet decided)
2. **Throws a clear, actionable error** if the permission is `"denied"`:
*"Notifications are blocked by your browser. Please allow notifications
for this site in your browser settings."*
3. **Only proceeds** to `pushManager.subscribe()` after permission is
confirmed as `"granted"`

## Tests

New test file `useWebpushNotifications.jest.ts`:
- **requests notification permission before subscribing** — verifies
`requestPermission()` is called before `pushManager.subscribe()`
- **throws a clear error when permission is denied** — verifies the
user-friendly error message
- **does not call pushManager.subscribe when permission is denied** —
verifies we bail out early
2026-03-03 17:13:31 -05: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 f758443f44 feat(codersdk): generate chat model provider options schema from Go structs (#22568) 2026-03-03 21:29:58 +00:00
Danielle Maywood f98761ff67 refactor(site): use button instead of div role="button" (#22575) 2026-03-03 21:26:01 +00:00
Danielle Maywood d2d956edb1 fix: add archived query parameter to chat list endpoint (#22562)
Despite the SDK type having an `Archived` field for chats, this data was
never fetched from the database — the `GetChatsByOwnerID` query
hardcoded `AND archived = false`, and the `convertChat` function never
mapped the field.

This PR adds an optional `archived` query parameter to `GET
/api/experimental/chats`:

| Value | Behavior |
|-------|----------|
| *(not provided)* | Returns all chats (active and archived) |
| `archived=false` | Returns only non-archived chats |
| `archived=true` | Returns only archived chats |

This follows the same pattern used by template versions
(`sqlc.narg('archived')` nullable boolean).

Also fixes `convertChat` to populate the `Archived` field in API
responses, which was never being set despite existing on the SDK type.
2026-03-03 20:39:19 +00:00
Kyle Carberry 8a2635285b fix(site): remove stale ArchivedAgentsSearchAutoExpands story (#22573)
The search input was removed from `AgentsSidebar` but the
`ArchivedAgentsSearchAutoExpands` story still referenced the `Search
agents...` placeholder, causing the Storybook interaction test to fail:

```
within(<div#storybook-root>).getByPlaceholderText("Search agents...")
Unable to find an element with the placeholder text of: Search agents...
```

This PR removes the stale story.
2026-03-03 20:28:04 +00:00
Kyle Carberry 810b509290 feat: refactor the agents admin UI layout (#22567)
I am working on a subsequent change to make the fields auto-generated
with `make gen` from the Go code itself, rather than us needing to
create a UI compatibility layer.

Once the above is done, I'll be adding in the payload so users can very
easily just click "Opus 4.6" to add the model, and the config values
will be set appropriately.

This is really just UI changes, nothing functionally should change here.
But the code will be cleaned up a lot post the above changes.

<img width="1197" height="978" alt="image"
src="https://github.com/user-attachments/assets/45f9afff-89bb-47a6-b9a1-534f50a9676e"
/>
<img width="1180" height="949" alt="image"
src="https://github.com/user-attachments/assets/b3fd963f-1c1d-4d2c-b501-ac8118b019ec"
/>
<img width="1185" height="957" alt="image"
src="https://github.com/user-attachments/assets/08faca29-2b38-476a-adab-0bd8ab17ddcc"
/>
2026-03-03 15:19:07 -05:00
Jeremy Ruppel b73f21662b flake: verify parameters in parallel in e2e tests (#22557)
This is an attempt to address coder/internal#1154

Tests appear to fail often on `verifyParameters`, which asserts input
visibility and value in series for all expected parameters. This change
makes the same assertions in parallel, hopefully completing before
timeout.
2026-03-03 14:56:41 -05:00
Danielle Maywood 6acdd6ca7d fix: wire agents-tab-visible metadata to experiments flag (#22553) 2026-03-03 13:51:10 -06:00
Kyle Carberry c05fbfec6c feat(site): restyle agents sidebar New Agent button (#22555)
Removes the search input and restyles the New Agent button in the agents
sidebar:

- Removed the search input box
- Replaced the outlined button with a subtle, left-aligned button
featuring a `SquarePenIcon`
- Button icon and text alignment matches the tree node items in the
sidebar

<img width="769" height="337" alt="image"
src="https://github.com/user-attachments/assets/2284c8c0-6294-4823-9ce0-5cc72b0d0054"
/>
2026-03-03 13:26:34 -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 8a095ae722 fix(site): remove ugly webpush button (#22560) 2026-03-03 16:44:46 +00:00
Danielle Maywood cdb7145982 feat(site): add collapsible archived agents section to sidebar (#22551) 2026-03-03 15:30:59 +00:00
Jake Howell e2a3b99d3a fix: remove mui components from <WorkspaceScheduleForm /> (#22232)
This pull-request removes the last instance of the
`@mui/material/Switch` from the codebase, whilst also cleaning up the
`<WorkspaceScheduleForm />` page of MUI.

<img width="1067" height="666" alt="image"
src="https://github.com/user-attachments/assets/b32094f6-f1a4-42fc-b927-64749e131f1b"
/>
2026-03-04 00:30:02 +11:00
Jake Howell 02328605a9 feat: <RequestLogsPage /> not enabled message (#22546)
This pull-request implements a message that alerts when the user has AI
Governance entitled in their license but not necessarily enabled, this
is an improvement to #22385 where the content simply didn't render (due
to a failed request).

<img width="1403" height="400" alt="image"
src="https://github.com/user-attachments/assets/54685ce1-5cc1-421f-b290-de9b3d160c03"
/>

<img width="1411" height="824" alt="image"
src="https://github.com/user-attachments/assets/4d44fa18-0914-4e51-a415-c511e5b13e98"
/>
2026-03-04 00:25:38 +11:00
Danielle Maywood 17f5fa452e fix: remove opacity-50 from agents page logo so it renders black (#22547) 2026-03-03 13:10:12 +00:00
Jake Howell b4a53acfd6 fix: resolve <Checkbox /> classes (#22540)
This pull-request cleans up various issues we I noticed while using the
`<Checkbox />` element.

* Margins were missing around the outsides of the `<Checkbox />`
components.
* Resolved the story of the `WithLabel` so things are lined up.
* Lowered the `borderRadius` down to `2px` (This can be cleaned up by
Tailwind 4 later).
* Refined checkbox styling across focus, disabled, checked, and
indeterminate states to be inline with Figma.
* Simplified checkbox indicator rendering and centered the icon with
absolute positioning.
2026-03-03 23:54:28 +11:00
Danielle Maywood 2a3b6643ea fix(site): cap chat input height and make it scrollable (#22545) 2026-03-03 12:35:39 +00:00
Jake Howell 8aebd73466 feat: implement new default monospace font Geist Mono (#22081)
This pull-request follows up #22060

Felt wrong to only make use of Geist when there is a Monospace variant
here too. Felt best we default to this as the default font as its inline
with the rest of the application. This also updates the lower line for
Workspace Statistics 🙂
2026-03-03 12:00:50 +00:00
Jake Howell aa9fafa372 fix: resolve <Avatar /> incorrect sizes (#22538)
This pull-request updates our icons to be inline with the Figma file.
They were slightly too small in the two variants of `--avatar-default`
and `--avatar-sm`. Now these are inline with what we have defined and
using the correct variants in the breadcrumbs.
2026-03-03 22:48:41 +11: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
Kayla はな 2bdf80d452 fix: disable sharing ui when sharing is unavailable (#22390)
Currently the sharing UI is only hidden under certain circumstances,
rather than on a permission basis. This makes it permissions based, and
makes some backend changes to make sure permissions are correct.
2026-03-03 02:04:55 +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 5eebd3829f fix: use cursor-based query for chat stream notifications (#22510)
## Problem

The pubsub notification handler in `chatd` re-fetched **all** messages
from the DB on every new message notification, then filtered in Go with
`msg.ID > lastMessageID`. This grows linearly with conversation length —
every new message triggers a full table scan of that chat's history.

The `AfterMessageID` field in the pubsub notification payload was
clearly designed for cursor-based fetching, but no matching query
existed.

## Fix

- Add `GetChatMessagesByChatIDAfter` SQL query with `WHERE id >
@after_id`, so the database does the filtering instead of Go.
- Use it in the pubsub notification handler in `chatd.go`, passing
`lastMessageID` as the cursor.
- Implement the dbauthz wrapper (was a `panic("not implemented")` stub
from codegen) with the same read-check-on-parent-chat pattern as
adjacent methods.
- Add dbauthz test coverage for the new method.

**Not changed:** The initial snapshot in `Subscribe()` still loads all
messages — that's correct, since a newly-connecting client needs the
full conversation state. The waste was only in the ongoing notification
path.
2026-03-02 16:31:04 -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 CarberryandDanielle Maywood c4a4ad6008 feat(site): add smooth streaming text engine for LLM responses (#22503)
## Problem

LLM responses currently stream in bulk chunks — multiple `message_part`
events arrive per WebSocket frame, get batched into a single
`startTransition` state update, and render as a visual jump. This looks
janky compared to smooth character-by-character reveal.

## Solution

Port the jitter-buffer approach from
[coder/mux](https://github.com/coder/mux) into a single self-contained
file: `SmoothText.ts`.

### What's in the file

| Component | Purpose |
|---|---|
| `STREAM_SMOOTHING` constants | Tuning knobs (72–420 cps adaptive rate,
120 char max visual lag, 48 char frame cap) |
| `SmoothTextEngine` class | Pure state machine — two-clock model
(ingestion vs presentation) with budget-gated adaptive reveal |
| `useSmoothStreamingText` hook | React bridge via
`requestAnimationFrame` loop, single `useState<number>`, grapheme-safe
slicing |

### How the engine works

- **Adaptive rate:** Linear interpolation from 72 → 420 chars/sec based
on backlog pressure (how far behind the display is from ingested text)
- **Budget accumulation:** Fractional character budget accrues per RAF
tick. Only reveals when ≥1 whole character is ready. This makes it
frame-rate invariant — 60Hz and 240Hz displays reveal the same amount
over wall-clock time (tested to ≤2 char deviation)
- **Max visual lag:** Hard cap of 120 chars. If the gap exceeds this,
the visible pointer jumps forward immediately
- **Clean flush:** When streaming ends, remaining buffer appears
instantly — no trailing animation
- **Grapheme safety:** Uses `Intl.Segmenter` (with codepoint fallback)
to never split emoji mid-animation

### Integration

To wire this up, wrap the `<Response>` component in
`ConversationTimeline.tsx` with the hook:

```tsx
const SmoothedResponse: FC<{text: string; isStreaming: boolean; streamKey: string}> =
    ({ text, isStreaming, streamKey }) => {
        const { visibleText } = useSmoothStreamingText({
            fullText: text,
            isStreaming,
            bypassSmoothing: false,
            streamKey,
        });
        return <Response>{visibleText}</Response>;
    };
```

### Tests

8 engine tests covering: steady reveal, adaptive acceleration, max lag
cap, immediate flush on stream end, bypass mode, content shrink,
sub-char budget gating, and frame-rate invariance.

---------

Co-authored-by: Danielle Maywood <danielle@themaywoods.com>
2026-03-02 15:18:30 -05:00