Commit Graph
4168 Commits
Author SHA1 Message Date
Matt Vollmer d9976768db fix(site): copy token value from modal (#25399) 2026-05-15 16:52:55 -04:00
Jaayden Halko 2c18e07e39 feat: add theme mode dropdown (#25183)
## Summary

- Wire the Appearance settings page to the new theme mode dropdown and
sync or single theme selectors.
- Update Appearance page tests and stories for theme mode behavior.
- Update the user settings e2e test to exercise single theme selection.

## Dependencies

- Depends on #25076, #25180, #25181, and #25182.
- This PR targets helper branch `pr25077/05-theme-mode-dropdown-base`,
which contains dependency commits only, so this PR diff stays focused on
final dropdown wiring. Rebase and retarget after the dependency PRs
merge.

## Validation

- `pnpm -C site exec vitest run --project=unit
src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx
src/theme/themeMode.test.ts src/api/queries/users.test.ts`
- `pnpm -C site lint:types`
- `pnpm -C site storybook:ci`
- `pnpm -C site build`
- `pnpm -C site playwright:test -- e2e/tests/users/userSettings.spec.ts`
- Pre-commit hook passed on the branch commit.
2026-05-15 16:15:06 +01:00
Danielle Maywood b94cb38504 style(site): use terminal icon for shell tool (#25392) 2026-05-15 13:39:51 +00:00
Jaayden Halko 8650a2ee22 fix: rollback appearance mutation cache updates (#25182)
## Summary

- Add appearance mutation context so optimistic cache updates can be
rolled back on failure.
- Restore previous appearance settings, or remove the optimistic cache
entry when no previous data exists.
- Merge successful server responses back into the appearance cache while
preserving request fields when responses are partial.

## Dependencies

- Stacked on #25076 because the tests and generated types use the new
appearance theme mode fields.

## Validation

- `pnpm -C site exec vitest run --project=unit
src/api/queries/users.test.ts`
- `pnpm -C site lint:types`
- Pre-commit hook passed on the branch commit.
2026-05-15 10:50:29 +01:00
Jaayden Halko e8cfff40b4 feat(site): add theme mode frontend foundation (#25181)
## Summary

- Add theme mode helpers for legacy migration, active theme resolution,
draft conversion, and mode switching.
- Add `usePreferredColorScheme` and refactor `ThemeProvider` to use the
shared theme mode resolver.
- Add reusable Appearance theme picker components plus isolated
Storybook coverage.

## Dependencies

- Stacked on #25180.

## Validation

- `pnpm -C site exec vitest run --project=unit
src/theme/themeMode.test.ts src/theme/usePreferredColorScheme.test.tsx`
- `pnpm -C site lint:types`
- `pnpm -C site lint:knip`
- Pre-commit hook passed on the branch commit.
2026-05-15 10:04:48 +01:00
Yevhenii Shcherbina 238968cfa0 feat: add per-group AI budget table and endpoints (#25203)
Closes
https://linear.app/codercom/issue/AIGOV-284/add-group-budgets-table-and-crud-api

## Summary

Adds the `group_ai_budgets` table and the following endpoints:

- `GET /api/v2/groups/{group}/ai/budget`
- `PUT /api/v2/groups/{group}/ai/budget`
- `DELETE /api/v2/groups/{group}/ai/budget`

Each group may have at most one budget row. If no row exists, no budget
is enforced.

### Feature gate
  
Added `RequireFeatureMW(FeatureAIBridge)` on the `/ai/budget` sub-route.

## RBAC

Authorization reuses `rbac.ResourceGroup` with the existing
`.InOrganization(...).WithID(...)` scoping model.

The `dbauthz` wrappers load the parent `groups` row and authorize
against it.

No new resource type is introduced. As a result, anyone with
`group:update` permissions (Owner, OrgAdmin, or UserAdmin within the
organization) can manage AI budgets for that group.

## Read access for group members

`database.Group.RBACObject()` grants `policy.ActionRead` to all members
of the group through the group ACL:

```go
func (g Group) RBACObject() rbac.Object {
	return rbac.ResourceGroup.WithID(g.ID).
		InOrg(g.OrganizationID).
		// Group members can read the group.
		WithGroupACL(map[string][]policy.Action{
			g.ID.String(): {
				policy.ActionRead,
			},
		})
}
```

Because the `GET` endpoint authorizes against the same loaded `Group`
object, any group member can call:

```text
GET /api/v2/groups/{group}/ai/budget
```

`PUT` and `DELETE` remain admin-only. The group ACL grants only
`ActionRead`, so write operations continue to require role-based
`group:update` permissions.

## Alternative considered

A dedicated `rbac.ResourceGroupAiBudget` resource would allow budget
management to be separated from general group administration.

We decided not to add that complexity for now.
2026-05-14 15:54:37 -04:00
TylerandTJ d79cfcfe61 fix(site): move docs link to primary header on observability page (#25313)
Move the "Read the docs" button from the Audit Logging subsection up to
the primary Observability header's `actions` prop, matching the layout
pattern used by General, Network, and other deployment settings pages.

Also updates the docs URL from `/admin/security/audit-logs` to
`/admin/monitoring` to reflect the page-level scope.

> Generated by Coder Agents on behalf of @designertyler

---------

Co-authored-by: TJ <tracy@coder.com>
2026-05-14 13:57:01 -05:00
Danielle Maywood 68baf84b8c fix: hide empty execute tool calls (#25346) 2026-05-14 18:19:12 +01:00
Danny Kopping 841b777ccd feat: add ai_providers table, queries, dbauthz, audit, RBAC (#24892) 2026-05-14 16:10:46 +02:00
Jaayden Halko acf57b3b35 feat(site): add legacy auto sync helper and tritan cleanup (#25180)
## Summary

- Add `legacyAutoToSync` to map legacy auto-family theme preferences to
light and dark sync pairs.
- Expand colorblind theme tests for legacy auto-family handling.
- Move tritan success and git-added surfaces onto the sky-blue palette
in theme roles and CSS variables.

## Dependencies

- None. This can merge independently before the dropdown work.

## Validation

- `pnpm -C site exec vitest run --project=unit
src/theme/colorblind.test.ts`
- `pnpm -C site lint:types`
- Pre-commit hook passed on the branch commit.
2026-05-14 14:43:13 +01:00
Danielle Maywood 25a803221e feat: add shell tool display mode preference (#25029) 2026-05-14 14:25:07 +01:00
Jaayden Halko 3a070a83dd fix(site/src/pages/AgentsPage): theme diff changed-line backgrounds (#25179)
## Summary

- Update AgentsPage diff viewer CSS so changed lines use theme-aware git
added and deleted surfaces.
- Add unit coverage for the changed-line CSS variables and line-type
selectors.

## Dependencies

- None. This is independent of the theme mode API and dropdown stack.

## Validation

- `pnpm -C site exec vitest run --project=unit
src/pages/AgentsPage/components/ChatElements/tools/utils.test.ts`
- Pre-commit hook passed on the branch commit.
2026-05-14 14:11:30 +01:00
Michael Suchacz cb37047dce feat: dedicated /prompts endpoint for chat history cycle (#25083)
Follow-up to #25004. The merged change cycles only through messages
already loaded in the in-memory chat store (page size 50). Long chats
and chats whose oldest turns have rolled out of the page lose access to
their earlier prompts in the composer's up/down arrow cycle. This PR
adds a dedicated server endpoint that returns the full prompt history,
newest first, and rewires the composer to use it.

## What changed

### Endpoint

`GET /api/experimental/chats/{chat}/prompts?limit=N`

```go
type ChatPrompt struct { ID int64; Text string }
type ChatPromptsResponse struct { Prompts []ChatPrompt }
```

- `limit`: `0..2000`. `0` (the default) is treated as the server-side
default of 500; out-of-range values return `400`. Negative values are
rejected by the SDK's `PositiveInt32` parser before reaching the
handler.
- Auth: parent-chat read in `dbauthz`, mirroring
`GetChatMessagesByChatID`.
- The SQL filters `role='user'`, `deleted=false`, `visibility IN
('user','both')`, guards the lateral with `jsonb_typeof(content) =
'array'` so legacy V0 scalar-string rows are silently skipped, then
unrolls `content` JSONB with `WITH ORDINALITY` and concatenates only
`type='text'` parts in original order via `string_agg(... ORDER BY
ordinality)`. Messages whose joined text is whitespace-only are dropped
via `HAVING ... ~ '\S'` so cycling never lands on a blank entry.

### Partial index (migration `000494`)

```sql
CREATE INDEX idx_chat_messages_user_prompts
ON chat_messages (chat_id, id DESC)
WHERE deleted = false
  AND role = 'user'
  AND visibility IN ('user', 'both');
```

The partial WHERE matches the query's filter exactly and the key order
matches `ORDER BY id DESC`, so the planner gets both the filter and the
ordering from the index without a sort step.

`EXPLAIN ANALYZE` on a synthetic 51-chat × 5,000-message dataset (≈260k
rows, 10k user prompts in the target chat, `random_page_cost=1.1`):

| | Plan | Buffers hit | Time |
|---|---|---|---|
| Without index | `Index Scan Backward using chat_messages_pkey`,
**250,848 rows removed by filter** | 6,683 | 32.4 ms |
| With index | `Index Scan using idx_chat_messages_user_prompts`, no
filter | 38 | 1.3 ms |

≈25× faster, 175× fewer buffer hits.

### Frontend

- `chatPromptsKey` / `chatPromptsQuery` factories in
`site/src/api/queries/chats.ts` (`staleTime: 30s`, `enabled: chatId !==
""`, asks the server for 500 prompts).
- `ChatPageContent.tsx` replaces the in-memory derivation with
`useQuery(chatPromptsQuery(chatId ?? ""))`. The composer's existing
`cycleHistorySnapshotRef` anchors the in-flight cycle so a refetch
arriving mid-cycle cannot shift the indexed prompt out from under the
user.
- `getEditableUserMessagePayload` now concatenates user-message text
parts verbatim, mirroring the server's `string_agg(part->>'text', ''
ORDER BY ordinality)`, instead of routing through the streaming-oriented
`parseMessageContent` / `appendText` pipeline (which drops
whitespace-only chunks — correct for assistant streams, wrong for a
user's persisted message). This keeps the cycle and the edit path in
agreement on the same message. File blocks are still pulled separately
via
`parseMessageContent(...).blocks.filter(isEditableUserMessageFileBlock)`.
- Cache invalidation in `createChatMessage.onSuccess`,
`editChatMessage.onSettled`, and `useChatStore.upsertCacheMessages`
(only when an upserted message has `role === "user"`).
- Page-level stories pre-seed `chatPromptsKey(CHAT_ID)` from the same
`messagesData` to keep them offline.

## Tests

- New `TestGetChatUserPrompts` in `coderd/exp_chats_test.go` with five
subtests:
- `NewestFirstFiltering` — multi-part concatenation, non-text parts
skipped, whitespace-only filtered, soft-deleted excluded, `model`-only
visibility excluded, assistant-role excluded by `cm.role = 'user'`,
legacy V0 scalar row silently excluded by the `jsonb_typeof` guard,
ordering newest first.
- `LimitClampsResults` — explicit `limit=2` returns the two newest
prompts.
  - `InvalidLimitRejected` — `limit=5000` is `400 Bad Request`.
- `NotFoundForOtherUsers` — a separate user in the same org gets `404`,
not the prompts.
- `EmptyResultIsJSONArray` — zero-message chat and assistant-only chat
both return `Prompts: []` (non-nil, empty).
- New unit test in `messageParsing.test.ts` asserting that
`getEditableUserMessagePayload(["hello", " ", "world"])` returns `"hello
world"`, locking in the agreement with the SQL `string_agg`.
- `dbauthz_test.go` adds the
`MethodTestSuite.TestChats/GetChatUserPromptsByChatID` entry, asserting
parent-chat `policy.ActionRead`.
- `pnpm test src/pages/AgentsPage` — 1159 passed, 2 skipped.
- `make gen` produces no diff.

## Manual verification

Seeded a dev chat with Claude Sonnet 4.6 via the aibridge Anthropic
provider and posted 20 user prompts end-to-end. Verified that the
`/prompts` endpoint returns 20 rows newest-first, that `limit=10` clamps
correctly, that `limit=0` uses the server default of 500, and that the
up/down keyboard cycle in the composer walks the same sequence (and
reverses correctly back to the empty draft).

## Out of scope

- Cross-chat history.
- Per-user opt-out for the cycle.
- File-reference / attachment cycling — the cycle continues to reproduce
plain text only, by design.

<details>
<summary>Implementation plan</summary>

# CODAGT-319 Follow-up — Dedicated `/prompts` endpoint

## Context

The merged feature ([#25004](https://github.com/coder/coder/pull/25004)
/ [d32842f](https://github.com/coder/coder/commit/d32842f)) cycles only
through messages already loaded in the in-memory chat store, which is
capped at the first 50 messages of the current page. Long chats and
chats whose oldest turns have rolled out of the page can no longer
recall their full prompt history. This follow-up exposes a dedicated
server endpoint that returns the user-authored prompts in a chat, newest
first, and rewires the composer to use it.

## Design

### Endpoint

`GET /api/experimental/chats/{chat}/prompts?limit=N`

Returns:

```go
type ChatPrompt struct {
    ID   int64
    Text string
}
type ChatPromptsResponse struct {
    Prompts []ChatPrompt
}
```

- `limit`: `0..2000`. `0` (the default) → server-side default of 500.
The wire-level default is encoded in SQL as `COALESCE(NULLIF($limit, 0),
500)`. Negatives are rejected upstream by `PositiveInt32`; the handler
only caps the upper bound.
- Auth: parent-chat read in `dbauthz`, mirroring
`GetChatMessagesByChatID`.
- Listed under the experimental router so we can iterate without API
guarantees.

### SQL

The query lives in `coderd/database/queries/chats.sql` as
`GetChatUserPromptsByChatID`:

- Filters `role='user'`, `deleted=false`, `visibility IN
('user','both')` to mirror the composer's "what the user actually typed
and can re-send" contract.
- Guards the lateral with `jsonb_typeof(content) = 'array'` so legacy V0
rows whose content is a scalar JSON string (predates migration `000434`)
are silently excluded instead of raising `"cannot extract elements from
a scalar"`.
- Unrolls `content` JSONB with `jsonb_array_elements WITH ORDINALITY`
and concatenates only `type='text'` parts, preserving original order via
`string_agg(... ORDER BY ordinality)`.
- Casts the result to `text` so sqlc emits a `string` field instead of
`[]byte`.
- Drops whitespace-only prompts via `HAVING string_agg(...) ~ '\S'` so
cycling never lands on a blank entry.
- Orders by `cm.id DESC` (`id` is a sequence, so this is "newest first"
without relying on `created_at`).

### Index

New partial index added in migration `000494`:

```sql
CREATE INDEX idx_chat_messages_user_prompts
ON chat_messages (chat_id, id DESC)
WHERE deleted = false
  AND role = 'user'
  AND visibility IN ('user', 'both');
```

The partial WHERE clause matches the query's filter exactly, so the
planner can use the index for both filtering and ordering without a sort
step.

### Frontend

- `chatPromptsKey(chatId)` and `chatPromptsQuery(chatId)` factories in
`site/src/api/queries/chats.ts`. `staleTime: 30s`, `enabled: chatId !==
""`. Asks the server for 500 prompts (well below the 2000 max, plenty
for the cycle).
- `ChatPageContent.tsx` replaces the in-memory derivation with
`useQuery(chatPromptsQuery(chatId ?? ""))`. The composer's
`cycleHistorySnapshotRef` already takes a stable snapshot at cycle
entry, so a refetch arriving mid-cycle cannot shift the indexed prompt
out from under the user.
- `getEditableUserMessagePayload` extracts the edit-path text from raw
user-message parts (filter `type === "text"`, join verbatim) instead of
going through `parseMessageContent` / `appendText`, which is built for
assistant streams and intentionally drops whitespace-only chunks.
Without this, cycling and clicking Edit on the same message could
produce different draft text for messages with whitespace-only
interleaved text parts.
- Cache invalidation: `createChatMessage.onSuccess`,
`editChatMessage.onSettled`, and `useChatStore.upsertCacheMessages`
(when at least one upserted message has `role === "user"`) all
invalidate `chatPromptsKey(chatId)`.

### Tests

- `TestGetChatUserPrompts` (`coderd/exp_chats_test.go`) covers:
- `NewestFirstFiltering` — multi-part concatenation, non-text parts
skipped, whitespace-only filtered, soft-deleted excluded, `model`-only
visibility excluded, assistant-role excluded by `cm.role = 'user'`,
legacy V0 scalar row silently excluded by the `jsonb_typeof` guard,
ordering newest first.
- `LimitClampsResults` — explicit `limit=2` returns the two newest
prompts.
  - `InvalidLimitRejected` — `limit=5000` is `400 Bad Request`.
- `NotFoundForOtherUsers` — a separate user in the same org gets `404`,
not the prompts.
- `EmptyResultIsJSONArray` — zero-message chat and assistant-only chat
both return `Prompts: []` (non-nil, empty).
- `messageParsing.test.ts` adds a unit test asserting that
`getEditableUserMessagePayload(["hello", " ", "world"])` returns `"hello
world"`, locking in the agreement with the SQL `string_agg`.
- `dbauthz_test.go` adds the
`MethodTestSuite.TestChats/GetChatUserPromptsByChatID` entry, asserting
the parent-chat `policy.ActionRead`.

## Out of scope

- Cross-chat history.
- Per-user opt-out for the cycle.
- File-reference / attachment cycling — the cycle still reproduces plain
text only, by design.

</details>

<details>
<summary>coder-agents-review history</summary>

Four review rounds, eight unique findings, all addressed in this PR
(approved twice). Rebased onto `main` twice after R4: first to pick up
new migrations `000491` / `000492`, then again for
`000493_idx_chat_diff_statuses_url_lower`. The prompts-index migration
was renumbered `000491 → 000493 → 000494` via
`coderd/database/migrations/fix_migration_numbers.sh`; no other diff
changes.

| Round | Head | Outcome |
|---|---|---|
| R1 | `725422ab` | `COMMENTED` — 7 findings (DEREM-1..7) |
| R2 | `ab2a8936` | `COMMENTED` — 1 new (DEREM-10) + 1 reraised
(DEREM-5) |
| R3 | `648c5d1f` | **`APPROVED`** — 7 fixed, DEREM-5 deferred via
#25125 |
| R4 | `93b6f450` | **`APPROVED`** — DEREM-5 also fixed in-PR, #25125
closed |

| ID | Where | Resolution |
|---|---|---|
| DEREM-1 | `chats.sql` | Added `jsonb_typeof(content) = 'array'` guard
against V0 scalar rows |
| DEREM-2 | `exp_chats.go` | Removed dead `limit < 0` branch (SDK
rejects upstream) |
| DEREM-3 | `useChatStore.ts` | Rewrote misleading invalidation comment
|
| DEREM-4 | `exp_chats_test.go` | `NewestFirstFiltering` now inserts an
assistant-role message so the `role='user'` filter is exercised
end-to-end |
| DEREM-5 | `messageParsing.ts` | Rewrote
`getEditableUserMessagePayload` to concatenate text parts verbatim,
mirroring the SQL `string_agg` |
| DEREM-6 | `exp_chats.go` | Tightened swagger doc + error message to
spell out the 0–2000 range |
| DEREM-7 | `exp_chats_test.go` | Added `EmptyResultIsJSONArray` subtest
|
| DEREM-10 | `exp_chats_test.go` | `NewestFirstFiltering` now inserts a
raw V0 scalar-content row; verified locally that removing the guard
makes the test fail |

</details>

---

This PR was created on behalf of @ibetitsmike by Coder Agents.
2026-05-14 12:43:12 +02:00
Danielle Maywood f7d1ecaece refactor(site): inline single-use class string constant in AgentSetupNotice (#25086) 2026-05-14 10:18:50 +01:00
Jaayden Halko 024132e8a4 feat: add theme_mode, theme_light, theme_dark to UserAppearanceSettings (#25076)
Part 1: Backend portion of a change broken into 2 PRs.
Part 2: #25077 

Adds three new UserAppearanceSettings fields (theme_mode, theme_light,
theme_dark) on top of the existing theme_preference and terminal_font.
Replaces GetUserThemePreference and GetUserTerminalFont with a single
GetUserAppearanceSettings aggregate query. The PUT handler is wrapped in
db.InTx so sync-mode's mode + slot writes can never half-apply.
2026-05-14 05:44:05 +01:00
Ethan d147dd3bdd feat(site/src/pages/AgentsPage/components): show workspace quota in usage indicator (#25168)
This updates the Agents sidebar usage indicator to surface workspace
quota alongside AI spend limits. When both signals are active, the
compact trigger renders stacked bars in the same order as the dropdown
instead of collapsing them into a single percent.

The dropdown still shows the full labels, percentages, and details for
each usage section, and Storybook coverage now exercises the combined
sidebar state.


<img width="315" height="261" alt="image"
src="https://github.com/user-attachments/assets/e5cfc276-2cc0-4dc9-9400-6d1b829e75e2"
/>

<img width="320" height="243" alt="image"
src="https://github.com/user-attachments/assets/506ae8ad-3d93-4857-9cdb-b3cf4142772d"
/>

<img width="314" height="353" alt="image"
src="https://github.com/user-attachments/assets/5af3644f-f155-43a4-bae9-91b33a0a4333"
/>

<img width="322" height="349" alt="image"
src="https://github.com/user-attachments/assets/9ae4ae55-55aa-4a2f-856e-f462793f389e"
/>




Relates to CODAGT-197
2026-05-14 11:59:00 +10:00
Kayla はな 2943bf5f21 fix(site): use ExternalImage for icon URLs (#25315) 2026-05-13 17:52:05 -06:00
Danielle Maywood ef1093d0dd fix(site): hide sticky metadata user messages (#25316) 2026-05-13 22:27:38 +01:00
Zach e0be9bf213 feat: surface missing coder_secret requirements on resolve-autostart (#25081)
Adds `dynamicparameters.EvaluateSecretMismatch` as a shared helper on
top of the existing renderer, then wires it into the resolve-autostart
handler so the UI can surface unsatisfied `coder_secret` requirements in
a template alongside parameter mismatch for autostart.

The lifecycle executor changes will land in a follow-up that depend
on this helper. The UI changes that consume the new `secret_mismatch`
field is also a follow-up.

Generated with assistance from Coder Agents.
2026-05-13 14:20:02 -06:00
Kayla はな 660fa9478f style(site): use shorthand for boolean JSX props (#25096) 2026-05-13 10:56:50 -06:00
Danielle Maywood 7fe4d97fd0 fix(site): align streaming thinking spacing (#25291) 2026-05-13 17:05:34 +01:00
Thomas KosiewskiandClaude Opus 4.7 b9b8d763e3 refactor(site/src/pages/AgentsPage): break AgentChatPage circular dep (#25287)
`AgentChatPageView.tsx` imported `getPersistedSidebarTabId` /
`savePersistedSidebarTabId` from `AgentChatPage.tsx`, which already
imports `AgentChatPageView`, closing a cycle that `pnpm run
lint:circular-deps`
reports but doesn't fail on (dpdm defaults to exit code 0; the script
is missing `--exit-code circular:1`).

Move the three sidebar-tab localStorage helpers and the key prefix into
`utils/sidebarTabStorage.ts` alongside `draftStorage.ts` and the other
per-chat storage modules. Pure code move, no behavior change.

After this change, `pnpm run lint:circular-deps` reports zero cycles.

---------

Signed-off-by: Thomas Kosiewski <tk@coder.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:13:32 +02:00
Jaayden Halko 36200b625e fix: fix flaky storybook test (#25272) 2026-05-13 16:08:18 +01:00
Danielle Maywood b52c0bdb56 fix(site/src/pages/AgentsPage/components): unify live thinking spacing and sizing (#25192) 2026-05-13 12:51:49 +01:00
Kayla はな 9810d299d2 chore: make dynamic parameters TemplateEmbedPage the default (#25069) 2026-05-12 18:28:36 +00:00
Thomas Kosiewski 969da320ec feat: export Coder Agents debug logs (#25039)
Adds JSON export actions to the Coder Agents Debug panel so users can download either the current chat's recent debug runs or one expanded run for support sharing.

The export reuses the existing chat debug endpoints and react-query cache, adds Storybook and unit coverage for the JSON envelope, and updates the chat debug logging docs with UI and cURL instructions.

Refs CODAGT-280.

Generated by Coder Agents.

<details>
<summary>Implementation notes</summary>

- Chat-level export fetches full detail for each listed debug run with `queryClient.fetchQuery(chatDebugRun(chatId, run.id))` and writes a single JSON file.
- Run-level export uses the already-loaded detail query data from an expanded run card.
- The JSON envelope includes `version`, `scope`, `exported_at`, `chat_id`, and either `runs` or `run`.
- The chat-level export reflects the current backend list endpoint behavior, up to the 100 newest debug runs.
- Agent-browser dogfooding verified files were downloaded and that `jq` validated the chat-level and run-level JSON contents.

</details>
2026-05-12 17:39:57 +02:00
Yevhenii Shcherbina b5e1ea33d8 feat: add AI budget policy and period deployment config (#25122)
Closes
https://linear.app/codercom/issue/AIGOV-283/add-deployment-config-for-ai-budget-policy-and-period

Adds `CODER_AI_BUDGET_POLICY` and `CODER_AI_BUDGET_PERIOD` deployment
options for AI Governance cost controls.
2026-05-12 10:48:36 -04:00
Ethan fabf7d31fc test: use default provider in TestPatchChatMessage/ChangesModel (#25189)
`TestPatchChatMessage/ChangesModel` hardcoded `"openai"` as the provider
for the override model config. After #25171, the shared chat test
harness registers a single `"openai-compat"` provider by default, so
calling `createAdditionalChatModelConfig(..., "openai", ...)` fails with
HTTP 400 `Chat provider is not configured` before the test can exercise
the model-change path. The subtest was added in #25084 after #25171 was
reviewed, so the harness change and the new hardcoded provider only met
on `main`.

Use `defaultModel.Provider` so the override always matches whatever
provider the harness registered. This mirrors every other call site of
`createAdditionalChatModelConfig` in the file.

Closes https://github.com/coder/internal/issues/1530
2026-05-12 14:05:08 +00:00
Thomas Kosiewski 38091f1d82 fix(site/src/pages/AgentsPage/components/ChatConversation): remove attachment copy actions (#25119)
Messages with chat file attachments showed a `Copy message` action even
though the action only copied `parsed.markdown` and omitted the
attachment content.

Hide the message copy action whenever a parsed message contains file
attachments, and add regression coverage for both user and assistant
attachment messages.

Refs
https://linear.app/codercom/issue/CODAGT-344/remove-or-fix-copy-buttons-on-file-attachments

Generated by Coder Agents.
2026-05-12 15:46:39 +02:00
Atif Ali 97ee54a8c1 fix(site): clarify deleted workspace actions (#25186)
## Summary
Updates the deleted workspace banner so its CTA recreates from the
original template instead of sending users to the generic templates
list.

## Changes
- Change the CTA to `Create another from <template>`.
- Link the CTA to the original template workspace create flow.
- Shorten the banner copy to `This workspace has been deleted.`

## Validation
- `pnpm exec biome check --error-on-warnings
src/pages/WorkspacePage/Workspace.tsx
src/pages/WorkspacePage/WorkspaceDeletedBanner.tsx
src/pages/WorkspacePage/WorkspaceDeletedBanner.stories.tsx`
- `pnpm run lint:types`
- `git diff --check`
- `git commit` pre-commit hooks

> 🤖 This PR was created with the help of Coder Agents, and needs a human
review. 🧑💻
2026-05-12 18:45:20 +05:00
Kyle Carberry b0b07536fc feat: add opt-in Coder identity headers for MCP servers (#25153) 2026-05-12 08:54:53 -04:00
Michael Suchacz f1d160c7f4 fix: allow changing model when editing earlier chat message (#25084)
Editing a previous user message and selecting a different model in the
picker silently kept using the original model: the selection was dropped
on the frontend, in the SDK, and in the backend, so both the replacement
user message and the assistant turn that followed ran against the old
model.

Plumb the selected model through all three layers (`AgentChatPage`,
`codersdk.EditChatMessageRequest`, `chatd.EditMessageOptions` /
`Server.EditMessage`), defaulting to the original message's model when
the client does not specify one. The existing `InsertChatMessages` CTE
already advances `chats.last_model_config_id` when the inserted
message's model differs, so the assistant turn picks up the new
selection without further changes. The new model is validated inside the
transaction, so an unknown ID rolls the edit back and returns a 400
`Invalid model config ID.`, mirroring the `SendMessage` path.

Refs: CODAGT-345

This change was generated by a Coder agent.

<details>
<summary>Implementation plan</summary>

# CODAGT-345: Editing an earlier message cannot change model

## Problem

When editing a previous user message in a chat, the user can change the
model in the model picker, but the backend keeps using the original
message's model. The model selection is dropped at three layers:

1. **Frontend:** `AgentChatPage.tsx`'s edit branch builds an
`EditChatMessageRequest` that omits `model_config_id`. The new-message
branch (a few lines below) does include it.
2. **SDK:** `codersdk.EditChatMessageRequest` has no `ModelConfigID`
field at all.
3. **Backend:** `chatd.EditMessageOptions` has no model field, and
`Server.EditMessage` always copies the original message's
`ModelConfigID` into the replacement message.

Once the replacement user message is inserted with the original model,
the `InsertChatMessages` CTE leaves `chats.last_model_config_id`
unchanged, so the assistant turn that follows runs against the old
model.

## Fix

Plumb the selected model through all three layers, defaulting to the
original message's model when the client doesn't override it. This
mirrors the `SendMessage` path, which already accepts a
`model_config_id` and validates it via
`resolveSendMessageModelConfigID`.

### Backend

- `codersdk/chats.go`: add `ModelConfigID *uuid.UUID` to
`EditChatMessageRequest`.
- `coderd/x/chatd/chatd.go`:
  - Add `ModelConfigID uuid.UUID` to `EditMessageOptions`.
- In `EditMessage`, after fetching the edited message, resolve the
model: if `opts.ModelConfigID != uuid.Nil`, validate it exists with
`tx.GetChatModelConfigByID` (using `chatdModelConfigLookupContext`),
otherwise keep `editedMsg.ModelConfigID.UUID`. Pass the resolved ID into
`newChatMessage(...)`.
  - Reuse the existing `ErrInvalidModelConfigID` sentinel.
- `coderd/exp_chats.go` (`patchChatMessage`):
- Read `req.ModelConfigID` (nil-safe), pass into
`chatd.EditMessageOptions`.
- Add a `case xerrors.Is(editErr, chatd.ErrInvalidModelConfigID)` arm
returning 400 `Invalid model config ID.`, matching the
`postChatMessages` handler.

### Frontend

- `site/src/pages/AgentsPage/AgentChatPage.tsx`:
- In the edit branch, set `model_config_id: effectiveSelectedModel ||
undefined` on the `EditChatMessageRequest`.
- On success, persist the chosen model to `lastModelConfigIDStorageKey`
so the next chat from this browser keeps the same default. Mirrors the
new-message branch.

### Generated

- `make site/src/api/typesGenerated.ts` and `make
coderd/apidoc/swagger.json` produce the updated `EditChatMessageRequest`
schema in `typesGenerated.ts`, `coderd/apidoc/{docs.go,swagger.json}`,
and `docs/reference/api/{chats.md,schemas.md}`.

## Tests

- `coderd/x/chatd/chatd_test.go`:
- `TestEditMessageWithModelConfigOverride`: edit with a different model
-> replacement message and `chats.LastModelConfigID` use the new model.
- `TestEditMessagePreservesModelConfigByDefault`: edit without
`ModelConfigID` -> original model preserved.
- `TestEditMessageRejectsUnknownModelConfig`: passes a random UUID ->
`ErrInvalidModelConfigID`, original message still present,
`LastModelConfigID` unchanged (rollback).
- `coderd/exp_chats_test.go` (under `TestPatchChatMessage`):
- `ChangesModel`: end-to-end via SDK; `edited.Message.ModelConfigID` and
`chat.LastModelConfigID` both match the new model.
- `InvalidModelConfigID`: random UUID -> 400 `Invalid model config ID.`.

</details>
2026-05-12 14:51:55 +02:00
Jaayden Halko 8ba24e0e54 feat(site): add collapsible agent sections (#25173)
closes CODAGT-395

<img width="426" height="480" alt="Screenshot 2026-05-12 at 19 04 03"
src="https://github.com/user-attachments/assets/ba336ecf-3e90-48b0-b5d3-b10499909953"
/>

Adds collapsible section headers to the Agents sidebar, including
visible counts and chevrons for pinned and time-grouped chats.

The section header toggle keeps nested chat expansion state independent,
preserves the existing filter dropdown behavior, and adds Storybook
coverage for counts, collapse or expand interactions, and filter menu
behavior.
2026-05-12 13:07:57 +01:00
Danielle Maywood a55430b8fd fix(site/src/pages/AgentsPage): suppress last-message spacer during active stream (#25120) 2026-05-12 12:31:40 +01:00
Cian Johnston caabb3c4ab fix(site): show Organizations in admin dropdown for single-org OSS deployments (#25175)
Fixes https://linear.app/codercom/issue/CODAGT-350

On OSS or no-license single-org deployments, the Organizations admin
link was hidden because `canViewOrganizationSettings` was gated on
`showOrganizations`, which requires either a multi-org entitlement or >1
org. The page was still reachable via direct URL, but the members view
displayed a raw "Template RBAC is a Premium feature. Contact sales!"
error from the groups API.

Two fixes:

1. Always render the Organizations link inside the `DeploymentDropdown`.
The dropdown itself is only shown to users with admin-level permissions,
so Organizations is effectively gated on having admin access.

2. Remove `groupsByUserIdQuery.error` from the error chain on the
members page. The groups endpoint is gated behind
`templateRBACEnabledMW` on enterprise, returning a 403 on OSS. The
groups data is already optional, so the page renders fine without it.

> Generated by Coder Agents
2026-05-12 12:28:43 +01:00
Thomas Kosiewski 5c3b59151e feat: add Cmd/Ctrl+Enter send setting (#25062)
Adds an Agents General setting to require Cmd/Ctrl+Enter before sending
chat messages. When enabled, plain Enter inserts a newline in agent chat
inputs while the send button remains available.

The preference is now persisted server-side through
`/api/v2/users/{user}/preferences`, alongside the existing user
preference settings, and is applied to both the create-agent input and
existing chat composer. Storybook and API coverage verify the setting,
keyboard behavior, validation, and persistence.

<details>
<summary>Coder Agents notes</summary>

Generated by Coder Agents from a Slack request. Dogfooded with
agent-browser against the Storybook settings and chat input stories.

</details>
2026-05-12 10:09:34 +02:00
Yevhenii Shcherbina 592e45dcfb chore: bump coder-guts dependency (#25154)
Bump coder/guts to v1.7.0.
Related PR: https://github.com/coder/guts/pull/81
2026-05-11 19:18:44 -04:00
Michael Suchacz 7a7b196b21 fix(site/src/pages/AgentsPage/components/Sidebar): keep subtitle in sync during streaming lifecycle (#25144)
The Agents sidebar shows the per-chat turn-end summary as a subtitle,
but the cached `last_turn_summary` is not always in sync with the live
status:

- **Resuming a turn**: when a chat goes back to `running` or `pending`,
the sidebar would keep displaying the previous turn-end label (e.g.,
"Chat is idle") while the agent is already streaming again.
- **Stream end → next summary**: status flips to `waiting`
synchronously, but the new `last_turn_summary` is generated by an async
finalizer that calls the model. For a beat, the row still carries the
previous turn's summary text, which would briefly flash before the new
text arrives.

This PR addresses both:

1. Override the subtitle with `{model} streaming…` while `chat.status`
is `running` or `pending`, matching the same `isStreaming` definition
`ChatPageContent` uses. Errors still take precedence so failure context
is not hidden.
2. Capture the cached summary observed during streaming and suppress an
exact match on the post-stream renders until the new summary lands.
State is adjusted during render so the first post-stream paint already
hides the stale string instead of flashing it for a frame. A 10s timeout
safety net releases the suppression in the corner case where the
regenerated summary equals the previous one byte-for-byte.

<details>
<summary>Decision log</summary>

- **Frontend-only fix**: the server intentionally does not bump
`updated_at` when it writes the new `last_turn_summary` (see
`UpdateChatLastTurnSummary` in `coderd/database/queries/chats.sql`), and
SSE delivers the status flip ahead of the new label. The sidebar knows
the live status, so the rendering layer is the right place to mask the
inconsistency.
- **`isStreaming = running || pending`** mirrors `ChatPageContent.tsx`
so subagents and queued continuations also flip to the live label.
- **State, not refs**, for the suppression bookkeeping: the React
Compiler rejects ref writes during render, and React's "adjust state
during render" pattern is purpose-built for storing information from
previous renders (state setters in render bail out and re-render
synchronously before paint).
- **10s timeout** is a safety net for the rare byte-for-byte equality
case; the equality-based release handles every other shape of update.
- Added Storybook stories covering both fixes
(`ChatStreamingOverridesTurnSummary` and
`StaleTurnSummaryAfterStreamingIsSuppressed`). All existing legacy
subtitle stories still pass because they use terminal statuses.

</details>

> This PR was created by Coder Agents on behalf of @ibetitsmike.
2026-05-12 00:51:22 +02:00
Sushant P 81561454d6 revert: "fix(site): enlarge checkbox click target in workspace and task tables" (#25159)
Reverts coder/coder#24739
2026-05-11 13:59:04 -07:00
Thomas Kosiewski e56381eb61 feat: stream advisor tool output (#25032)
Stream advisor output into the advisor tool card while the nested
advisor call is still running.

This keeps the advisor implementation intentionally advisor-specific:
the parent model still receives the same final structured tool result,
while the frontend receives transient `tool-result.result_delta` parts
to render partial advisor text in the expanded card. The final persisted
chat history remains unchanged.

Refs CODAGT-322.

Generated by Coder Agents.

<details>
<summary>Implementation plan</summary>

- Publish advisor text deltas from the nested `chatloop.Run` via
`RunAdvisorOptions.OnAdviceDelta`.
- Forward those deltas through `chatadvisor.Tool` with the parent
advisor tool call ID.
- Emit transient `ChatMessagePartTypeToolResult` websocket parts with
`ResultDelta` from `chatd`.
- Add `result_delta` to the generated tool-result TypeScript variant.
- Accumulate tool result deltas in frontend stream state and keep the
tool running until the final result arrives.
- Render streamed advisor advice in the existing advisor card using
streaming markdown mode, while retaining the updated advisor UI.

</details>
2026-05-11 20:18:49 +02:00
Steven Masley 19573e8aee feat!: patchTemplateMeta to use optional fields (#24984)
Closes https://github.com/coder/coder/issues/13112

**Breaking Change**: Removed status code `StatusNotModified` when no
diffs occur in a patch. Now the patch is always applied and a template
is always returned.
2026-05-11 12:43:52 -05:00
TJ c2dfaa406a fix(site): enlarge checkbox click target in workspace and task tables (#24739)
Fixes a UX issue where clicking near (but not exactly on) the
bulk-action
checkbox in the Workspaces or Tasks table would navigate to the
workspace/task page instead of toggling the checkbox. Clicking back then
clears all previous selections.

## Changes

Wraps each row checkbox in a `div` that:
- Calls `e.stopPropagation()` on `click` and `keydown` so near-miss
  clicks toggle the checkbox instead of navigating.
- Uses `h-[72px]` to fill the full row height for vertical coverage.
- Uses `pr-4 -mr-4` to extend the safe zone to the right without
  shifting layout.
- Sets `cursor-default` so the pointer hand does not appear in the
  safe zone.

Applied to both:
- `WorkspacesTable.tsx` (workspaces page)
- `TasksTable.tsx` (tasks page)

> This PR was authored by Coder Agents.
2026-05-11 16:37:00 +00:00
Jeremy Ruppel a1dbd758bc feat: add template builder deployment config and telemetry types (#25082) 2026-05-11 09:48:55 -04:00
Marcin Tojek febabfb8b2 feat: add request/response dump support to aibridgeproxyd (#24837)
Closes https://github.com/coder/coder/issues/24335
2026-05-11 10:59:26 +02:00
Kyle Carberry aaa0dacdb3 fix: infer workspace claim time from build history for /agents delete dialog (#25057)
Closes
[CODAGT-317](https://linear.app/codercom/issue/CODAGT-317/pr-workspaces-sometimes-require-name-confirmation-to-delete).

## Problem

The `/agents` archive-and-delete molly-guard (typing the workspace name)
was firing for chats that had clearly created their own workspace. The
heuristic in `resolveArchiveAndDeleteAction` decides whether
confirmation is needed by comparing the workspace's `created_at` against
the chat's `created_at`:

```ts
return new Date(workspaceCreatedAt) >= new Date(chatCreatedAt);
```

That assumption breaks for **prebuilt workspaces**.
`ClaimPrebuiltWorkspace` rewrites `owner_id`, `name`, `updated_at`,
`last_used_at`, etc., but **never touches `created_at`**, which still
reflects when the prebuild was provisioned by the reconciler, often
hours before the chat exists. Result: every prebuild-claimed workspace
looks pre-existing, so the molly-guard fires.

Concrete example from a real chat:

| Field | Value |
|---|---|
| `chat.created_at` | `2026-05-07T15:12:23Z` |
| `workspace.created_at` (provision) | `2026-05-07T14:22:24Z` |
| `latest_build.created_at` (claim) | `2026-05-07T15:19:09Z` |

`14:22:24 < 15:12:23` so `isWorkspaceAutoCreated` returned false even
though the chat issued the claim.

## Fix (frontend-only)

Derive the moment a workspace was acquired from existing build history
rather than relying on `workspace.created_at`:

- Build #1 initiator = prebuilds system user → workspace was a prebuild
→ use `build_2.created_at` (the claim build) as the acquisition time.
- Build #1 initiator = real user → workspace was created from scratch →
use `workspace.created_at` (unchanged behavior).
- Unclaimed prebuild or no build history → return `null` (force
confirmation; safe degradation for a destructive flow).

The resolver fetches the build list via the existing
`getWorkspaceBuilds` endpoint when the dialog might fire. No new column,
no migration, no schema change. Works retroactively for all existing
claimed prebuilds; no backfill needed.

The prebuilds system user UUID is exposed via
`codersdk.PrebuildsSystemUserID` and typegen'd to `typesGenerated.ts`.
`coderd/database.PrebuildsSystemUserID` parses that constant via
`uuid.MustParse` so the two cannot drift; if the codersdk literal ever
changes, package init fails fast.

## History

The first draft of this PR added a `workspaces.claimed_at` column
populated by `ClaimPrebuiltWorkspace`. After review feedback from
@johnstcn pointing out that the same fact is already implicit in build
history, I pivoted to the frontend-only approach. Subsequent review
notes consolidated the prebuilds system user UUID into a single
typegen'd constant.

## Why not the other open PRs

- **#25055** (`chatKey` cache fallback) only fixes a different
cache-miss path; it explicitly notes it does not address `created_at <
chat.created_at`.
- **#25053** (`chats.workspace_auto_created` boolean) puts the truth on
the wrong side of the schema: "this workspace was claimed at time T" is
a property of the workspace, not the chat. The MCP plumbing it adds is
also unnecessary now that the same answer is available from build
history.

## Test plan

- `pnpm vitest run --project=unit
src/pages/AgentsPage/utils/agentWorkspaceUtils.test.ts` — 40/40 pass;
new cases cover prebuild claim before/after chat, unclaimed prebuild,
missing-build-history fallback, and the fetch-skip when the chat is not
in cache.
- `pnpm lint:types`, `pnpm check`, `make pre-commit`.

<details>
<summary>Disclosure</summary>

Opened on behalf of @kylecarbs by [Coder
Agents](https://coder.com/coder-agents).
</details>
2026-05-10 11:04:55 -04:00
Yevhenii Shcherbina 4124d1137d feat: add ai_model_prices table (#24932)
# Summary

Implements
https://linear.app/codercom/issue/AIGOV-282/add-ai-model-price-table-and-seed-generator

This PR lays the groundwork for AI Bridge cost controls (per the AI
Governance RFC). It adds the foundation needed for future cost tracking:
a place to store per-model token prices, a way to keep those prices in
sync with upstream pricing data, and a startup mechanism that ensures
every deployment has prices loaded before AI Bridge starts processing
requests.

The price data comes from [models.dev](https://models.dev/), a
community-maintained catalogue of AI provider pricing. A generator
script fetches the latest prices, filters to Anthropic and OpenAI for
now, and produces a seed file checked into the repository.

On every server startup the seed is applied to the database, so new
releases automatically pick up any price corrections that landed since
the previous one. Existing rows are overwritten with the latest prices;
rows for models no longer in the seed are left untouched.

# Batching the AI model price seed: three approaches

Context: at server startup we seed the `ai_model_prices` table from an
embedded JSON price book (~70 rows today, will grow as we add providers,
potentially 4000+).

Each row is:

```text
(provider, model, input_price, output_price, cache_read_price, cache_write_price)
```

Any of the four price columns can be:

- `NULL` → “price unknown for this dimension”
- explicit `0` → “free”

The batch must be an UPSERT so re-running is idempotent and existing
rows pick up new prices.

We considered three implementations.

---

## Approach 1 — Per-row UPSERT in a Go loop

```go
for _, row := range rows {
    if err := db.UpsertAIModelPrice(ctx, database.UpsertAIModelPriceParams{
        Provider:   row.Provider,
        Model:      row.Model,
        InputPrice: nullInt64(row.InputPrice),
        // ...
    }); err != nil {
        return err
    }
}
```

### Pros

- Trivial.
- NULL handling falls out naturally from `sql.NullInt64`.

### Cons

- `N` round-trips per seed.
- With ~70 rows that means ~70 statement executions on every startup,
even inside a transaction.
- Doesn't scale gracefully as the price book grows, potentially 4000+.

---

## Approach 2 — `UNNEST` with parallel arrays

Pass each column as a separate Go slice. Postgres unnests them in
parallel into a virtual table, then `INSERT ... SELECT`.

```sql
INSERT INTO ai_model_prices (
    provider,
    model,
    input_price,
    output_price,
    cache_read_price,
    cache_write_price
)
SELECT
    UNNEST(@providers::text[]),
    UNNEST(@models::text[]),
    NULLIF(UNNEST(@input_prices::bigint[]), -1),
    NULLIF(UNNEST(@output_prices::bigint[]), -1),
    NULLIF(UNNEST(@cache_read_prices::bigint[]), -1),
    NULLIF(UNNEST(@cache_write_prices::bigint[]), -1)
ON CONFLICT (provider, model) DO UPDATE SET
    input_price       = EXCLUDED.input_price,
    output_price      = EXCLUDED.output_price,
    cache_read_price  = EXCLUDED.cache_read_price,
    cache_write_price = EXCLUDED.cache_write_price,
    updated_at        = NOW();
```

Go side: flatten rows into six parallel slices.

Use a sentinel (`-1`) for “missing”, since `lib/pq` can't encode `NULL`
into a `bigint[]` element.

```go
providers := make([]string, len(rows))
models    := make([]string, len(rows))
inputs    := make([]int64,  len(rows))
outputs   := make([]int64,  len(rows))
cacheR    := make([]int64,  len(rows))
cacheW    := make([]int64,  len(rows))

for i, r := range rows {
    providers[i] = r.Provider
    models[i]    = r.Model

    inputs[i] = -1
    if r.InputPrice != nil {
        inputs[i] = *r.InputPrice
    }

    outputs[i] = -1
    if r.OutputPrice != nil {
        outputs[i] = *r.OutputPrice
    }

    cacheR[i] = -1
    if r.CacheReadPrice != nil {
        cacheR[i] = *r.CacheReadPrice
    }

    cacheW[i] = -1
    if r.CacheWritePrice != nil {
        cacheW[i] = *r.CacheWritePrice
    }
}

return db.UpsertAIModelPrices(ctx, database.UpsertAIModelPricesParams{
    Providers:        providers,
    Models:           models,
    InputPrices:      inputs,
    OutputPrices:     outputs,
    CacheReadPrices:  cacheR,
    CacheWritePrices: cacheW,
})
```

### Pros

- Single round-trip.

### Cons

- The generated `sqlc` params become plain `[]int64`, which can't
represent `NULL`.

---

## Approach 3 — `jsonb_array_elements` over a single `@seed::jsonb`
(chosen)

Pass the raw seed JSON as one parameter; let Postgres expand and parse
it.

```sql
INSERT INTO ai_model_prices (
    provider,
    model,
    input_price,
    output_price,
    cache_read_price,
    cache_write_price
)
SELECT
    elem->>'provider',
    elem->>'model',
    (elem->>'input_price')::bigint,
    (elem->>'output_price')::bigint,
    (elem->>'cache_read_price')::bigint,
    (elem->>'cache_write_price')::bigint
FROM jsonb_array_elements(@seed::jsonb) AS elem
ON CONFLICT (provider, model) DO UPDATE SET
    input_price       = EXCLUDED.input_price,
    output_price      = EXCLUDED.output_price,
    cache_read_price  = EXCLUDED.cache_read_price,
    cache_write_price = EXCLUDED.cache_write_price,
    updated_at        = NOW();
```

Go side reduces to:

```go
return db.UpsertAIModelPrices(ctx, seedJSON)
```

### Pros

- Single round-trip.
- NULLs fall out naturally:
  - `(elem->>'cache_write_price')::bigint` becomes `NULL`
  - no sentinels
- The seed is already JSON:
- Existing precedent:
  - `jsonb_array_elements` is already used elsewhere in the codebase

### Cons

- Less type-safe at the SQL boundary than `UNNEST`
- Slightly less standard than `UNNEST`
- Readers need familiarity with:
  - `jsonb_array_elements`
  - `->>` extraction syntax
- Postgres pays JSON parse cost
  - negligible at our scale

---

---

# Decision

We picked Approach 3.

It collapses the round-trips like `UNNEST` does, but without:

- nullable-array workarounds
- sentinel values
2026-05-08 16:45:14 -04:00
Kayla はな 638e2220e9 chore: refactor BuildIcon and remove useClassName (#25017) 2026-05-08 14:11:49 -06:00
Jeremy Ruppel a638f099c8 fix(site): show running script count instead of log source count in agent log badge (#25079)
The badge next to the loading spinner in the agent logs section was
showing `agent.log_sources.length` (total log sources registered on the
agent). This is a static count unrelated to what's actively running.

Now it shows the count of startup scripts still in progress: scripts
where `run_on_start` is true and `status` is not yet set. Scripts
without a `status` haven't completed; completed scripts receive `"ok"`,
`"exit_failure"`, `"timed_out"`, or `"pipes_left_open"`. The badge also
hides when zero scripts are running.

> [!NOTE]
> Generated by Coder Agents
2026-05-08 09:37:03 -04:00
Ethan 987d415be3 feat(site): show workspace quota failures in chats (#25020)
Create and start workspace tool cards now recognize `INSUFFICIENT_QUOTA`
results and use the server-provided quota failure title in the build-log
dropdown. The existing warning icon and tooltip remain, while the
assistant response remains the place for the detailed recovery guidance.

Adds Storybook coverage for quota-reached create and start workspace
results.

https://github.com/coder/coder/pull/24956 added the necessary backend
changes.
<img width="897" height="505" alt="image"
src="https://github.com/user-attachments/assets/6cab8798-393d-429f-a3c3-a8ed50402d42"
/>



Closes CODAGT-20
2026-05-08 13:25:53 +10:00
Danielle Maywood e7958713a9 feat: add code diff display mode preference (#25027) 2026-05-07 20:15:28 +01:00