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 |
This commit is contained in:
Kyle Carberry
2026-03-13 08:35:46 -04:00
committed by GitHub
parent 0e7e0a959e
commit 690e3a87d8
15 changed files with 312 additions and 334 deletions
+24 -18
View File
@@ -176,7 +176,7 @@ func TestSubagentChatExcludesWorkspaceProvisioningTools(t *testing.T) {
if getErr != nil {
return false
}
if got.Chat.Status != codersdk.ChatStatusWaiting && got.Chat.Status != codersdk.ChatStatusError {
if got.Status != codersdk.ChatStatusWaiting && got.Status != codersdk.ChatStatusError {
return false
}
// Also ensure the subagent LLM call has been made.
@@ -1055,32 +1055,35 @@ func TestCreateWorkspaceTool_EndToEnd(t *testing.T) {
})
require.NoError(t, err)
var chatWithMessages codersdk.ChatWithMessages
var chatResult codersdk.Chat
require.Eventually(t, func() bool {
got, getErr := client.GetChat(ctx, chat.ID)
if getErr != nil {
return false
}
chatWithMessages = got
return got.Chat.Status == codersdk.ChatStatusWaiting || got.Chat.Status == codersdk.ChatStatusError
chatResult = got
return got.Status == codersdk.ChatStatusWaiting || got.Status == codersdk.ChatStatusError
}, testutil.WaitLong, testutil.IntervalFast)
if chatWithMessages.Chat.Status == codersdk.ChatStatusError {
if chatResult.Status == codersdk.ChatStatusError {
lastError := ""
if chatWithMessages.Chat.LastError != nil {
lastError = *chatWithMessages.Chat.LastError
if chatResult.LastError != nil {
lastError = *chatResult.LastError
}
require.FailNowf(t, "chat run failed", "last_error=%q", lastError)
}
require.NotNil(t, chatWithMessages.Chat.WorkspaceID)
workspaceID := *chatWithMessages.Chat.WorkspaceID
require.NotNil(t, chatResult.WorkspaceID)
workspaceID := *chatResult.WorkspaceID
workspace, err := client.Workspace(ctx, workspaceID)
require.NoError(t, err)
require.Equal(t, workspaceName, workspace.Name)
chatMsgs, err := client.GetChatMessages(ctx, chat.ID)
require.NoError(t, err)
var foundCreateWorkspaceResult bool
for _, message := range chatWithMessages.Messages {
for _, message := range chatMsgs.Messages {
if message.Role != "tool" {
continue
}
@@ -1223,33 +1226,36 @@ func TestStartWorkspaceTool_EndToEnd(t *testing.T) {
})
require.NoError(t, err)
var chatWithMessages codersdk.ChatWithMessages
var chatResult codersdk.Chat
require.Eventually(t, func() bool {
got, getErr := client.GetChat(ctx, chat.ID)
if getErr != nil {
return false
}
chatWithMessages = got
return got.Chat.Status == codersdk.ChatStatusWaiting || got.Chat.Status == codersdk.ChatStatusError
chatResult = got
return got.Status == codersdk.ChatStatusWaiting || got.Status == codersdk.ChatStatusError
}, testutil.WaitSuperLong, testutil.IntervalFast)
if chatWithMessages.Chat.Status == codersdk.ChatStatusError {
if chatResult.Status == codersdk.ChatStatusError {
lastError := ""
if chatWithMessages.Chat.LastError != nil {
lastError = *chatWithMessages.Chat.LastError
if chatResult.LastError != nil {
lastError = *chatResult.LastError
}
require.FailNowf(t, "chat run failed", "last_error=%q", lastError)
}
// Verify the workspace was started.
require.NotNil(t, chatWithMessages.Chat.WorkspaceID)
require.NotNil(t, chatResult.WorkspaceID)
updatedWorkspace, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
require.Equal(t, codersdk.WorkspaceTransitionStart, updatedWorkspace.LatestBuild.Transition)
chatMsgs, err := client.GetChatMessages(ctx, chat.ID)
require.NoError(t, err)
// Verify start_workspace tool result exists in the chat messages.
var foundStartWorkspaceResult bool
for _, message := range chatWithMessages.Messages {
for _, message := range chatMsgs.Messages {
if message.Role != "tool" {
continue
}
+13 -9
View File
@@ -92,17 +92,19 @@ func TestAnthropicWebSearchRoundTrip(t *testing.T) {
// Verify the chat completed and messages were persisted.
chatData, err := client.GetChat(ctx, chat.ID)
require.NoError(t, err)
chatMsgs, err := client.GetChatMessages(ctx, chat.ID)
require.NoError(t, err)
t.Logf("Chat status after step 1: %s, messages: %d",
chatData.Chat.Status, len(chatData.Messages))
logMessages(t, chatData.Messages)
chatData.Status, len(chatMsgs.Messages))
logMessages(t, chatMsgs.Messages)
require.Equal(t, codersdk.ChatStatusWaiting, chatData.Chat.Status,
require.Equal(t, codersdk.ChatStatusWaiting, chatData.Status,
"chat should be in waiting status after step 1")
// Find the first assistant message and verify it has the
// content parts the UI needs to render web search results:
// tool-call(PE), source, tool-result(PE), and text.
assistantMsg := findAssistantWithText(t, chatData.Messages)
assistantMsg := findAssistantWithText(t, chatMsgs.Messages)
require.NotNil(t, assistantMsg,
"expected an assistant message with text content after step 1")
@@ -152,17 +154,19 @@ func TestAnthropicWebSearchRoundTrip(t *testing.T) {
// Verify the follow-up completed and produced content.
chatData2, err := client.GetChat(ctx, chat.ID)
require.NoError(t, err)
chatMsgs2, err := client.GetChatMessages(ctx, chat.ID)
require.NoError(t, err)
t.Logf("Chat status after step 2: %s, messages: %d",
chatData2.Chat.Status, len(chatData2.Messages))
logMessages(t, chatData2.Messages)
chatData2.Status, len(chatMsgs2.Messages))
logMessages(t, chatMsgs2.Messages)
require.Equal(t, codersdk.ChatStatusWaiting, chatData2.Chat.Status,
require.Equal(t, codersdk.ChatStatusWaiting, chatData2.Status,
"chat should be in waiting status after step 2")
require.Greater(t, len(chatData2.Messages), len(chatData.Messages),
require.Greater(t, len(chatMsgs2.Messages), len(chatMsgs.Messages),
"follow-up should have added more messages")
// The last assistant message should have text.
lastAssistant := findLastAssistantWithText(t, chatData2.Messages)
lastAssistant := findLastAssistantWithText(t, chatMsgs2.Messages)
require.NotNil(t, lastAssistant,
"expected an assistant message with text in the follow-up")