feat: show agent turn summary in agents sidebar (#24942)

Persists the agent-generated turn-end summary on `chats` and shows it as
the Agents sidebar subtitle when present, falling back to the model
name. Errors still take precedence.

> Mux is acting on Mike's behalf.

## What changes

**Storage.** New nullable `last_turn_summary` column on `chats`
(migration `000486`). New `UpdateChatLastTurnSummary` query normalizes
blank/whitespace input to `NULL`, preserves `updated_at` (so the chat
does not jump to the top of the sidebar on summary writes), and uses an
`expected_updated_at` stale-write guard so an older async summary cannot
overwrite a newer turn.

**Backend.** `coderd/x/chatd/chatd.go` decouples summary generation from
webpush. Generated summaries persist for completed parent turns even
when webpush is unconfigured or has no subscriptions. The same generated
text is reused as the webpush body when webpush is configured, so the
summary model is not called twice. Generic fallback push text is no
longer persisted; it clears any stale summary instead.
Error/interrupt/pending-action terminal paths clear `last_turn_summary`
for the latest turn.

**Frontend.** `AgentsSidebar.tsx` subtitle priority is now `errorReason
|| lastTurnSummary || modelName`, normalized via the existing
`asNonEmptyString` helper from `blockUtils.ts`.

## Tests

- `TestUpdateChatLastTurnSummary` (database): success,
whitespace-to-NULL, stale guard rejects, `updated_at` preserved.
- `TestUpdateLastTurnSummaryRejectsStaleWrites` (chatd internal): direct
stale-`expected_updated_at` test.
- `TestSuccessfulChatPersistsTurnSummaryWithoutWebPush`: persistence
works without webpush subscriptions.
- `TestSuccessfulChatSendsWebPushWithSummary`: same generated text
drives both DB and push body.
-
`TestSuccessfulChatSendsWebPushFallbackWithoutSummaryForEmptyAssistantText`:
fallback text is not persisted.
- `TestErroredChatClearsLastTurnSummaryAndSendsWebPush`: error path
clears the field.
- `TestInterruptChatDoesNotSendWebPushNotification`: interrupt path
clears the field, no push fires.
- `AgentsSidebar.test.tsx`: subtitle priority for summary-present,
error-wins, no-summary fallback, whitespace fallback.
- `AgentsSidebar.stories.tsx`: `ChatWithTurnSummary` and
`ChatWithTurnSummaryAndError`.

## Notes

- No backfill. Existing chats keep showing the model name until their
next turn completes.
- Parent chats only in this iteration; the field is rendered on any
`Chat` if a future change extends generation to children.
- Decoupling generation from webpush adds quickgen model calls for
completed parent turns that previously skipped generation when no
subscriptions existed. Existing parent-only, assistant-text-present,
`PushSummaryModel` configured, and bounded-timeout gates keep this
behavior bounded.
This commit is contained in:
Michael Suchacz
2026-05-06 16:43:35 +02:00
committed by GitHub
parent 369a191972
commit 0bfb9f6f13
40 changed files with 1313 additions and 115 deletions
+11
View File
@@ -140,6 +140,7 @@ Experimental: this endpoint is subject to change.
}
],
"last_model_config_id": "30ebb95f-c255-4759-9429-89aa4ec1554c",
"last_turn_summary": "string",
"mcp_server_ids": [
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
],
@@ -261,6 +262,7 @@ Status Code **200**
| `»» type` | [codersdk.ChatMessagePartType](schemas.md#codersdkchatmessageparttype) | false | | |
| `»» url` | string | false | | |
| `» last_model_config_id` | string(uuid) | false | | |
| `» last_turn_summary` | string | false | | |
| `» mcp_server_ids` | array | false | | |
| `» organization_id` | string(uuid) | false | | |
| `» owner_id` | string(uuid) | false | | |
@@ -468,6 +470,7 @@ Experimental: this endpoint is subject to change.
}
],
"last_model_config_id": "30ebb95f-c255-4759-9429-89aa4ec1554c",
"last_turn_summary": "string",
"mcp_server_ids": [
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
],
@@ -591,6 +594,7 @@ Experimental: this endpoint is subject to change.
}
],
"last_model_config_id": "30ebb95f-c255-4759-9429-89aa4ec1554c",
"last_turn_summary": "string",
"mcp_server_ids": [
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
],
@@ -865,6 +869,7 @@ Experimental: this endpoint is subject to change.
}
],
"last_model_config_id": "30ebb95f-c255-4759-9429-89aa4ec1554c",
"last_turn_summary": "string",
"mcp_server_ids": [
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
],
@@ -1042,6 +1047,7 @@ Experimental: this endpoint is subject to change.
}
],
"last_model_config_id": "30ebb95f-c255-4759-9429-89aa4ec1554c",
"last_turn_summary": "string",
"mcp_server_ids": [
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
],
@@ -1165,6 +1171,7 @@ Experimental: this endpoint is subject to change.
}
],
"last_model_config_id": "30ebb95f-c255-4759-9429-89aa4ec1554c",
"last_turn_summary": "string",
"mcp_server_ids": [
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
],
@@ -1423,6 +1430,7 @@ Experimental: this endpoint is subject to change.
}
],
"last_model_config_id": "30ebb95f-c255-4759-9429-89aa4ec1554c",
"last_turn_summary": "string",
"mcp_server_ids": [
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
],
@@ -1546,6 +1554,7 @@ Experimental: this endpoint is subject to change.
}
],
"last_model_config_id": "30ebb95f-c255-4759-9429-89aa4ec1554c",
"last_turn_summary": "string",
"mcp_server_ids": [
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
],
@@ -2583,6 +2592,7 @@ Experimental: this endpoint is subject to change.
}
],
"last_model_config_id": "30ebb95f-c255-4759-9429-89aa4ec1554c",
"last_turn_summary": "string",
"mcp_server_ids": [
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
],
@@ -2706,6 +2716,7 @@ Experimental: this endpoint is subject to change.
}
],
"last_model_config_id": "30ebb95f-c255-4759-9429-89aa4ec1554c",
"last_turn_summary": "string",
"mcp_server_ids": [
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
],
+7 -3
View File
@@ -2197,6 +2197,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
}
],
"last_model_config_id": "30ebb95f-c255-4759-9429-89aa4ec1554c",
"last_turn_summary": "string",
"mcp_server_ids": [
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
],
@@ -2320,6 +2321,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
}
],
"last_model_config_id": "30ebb95f-c255-4759-9429-89aa4ec1554c",
"last_turn_summary": "string",
"mcp_server_ids": [
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
],
@@ -2358,6 +2360,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `last_error` | [codersdk.ChatError](#codersdkchaterror) | false | | |
| `last_injected_context` | array of [codersdk.ChatMessagePart](#codersdkchatmessagepart) | false | | Last injected context holds the most recently persisted injected context parts (AGENTS.md files and skills). It is updated only when context changes, on first workspace attach or agent change. |
| `last_model_config_id` | string | false | | |
| `last_turn_summary` | string | false | | |
| `mcp_server_ids` | array of string | false | | |
| `organization_id` | string | false | | |
| `owner_id` | string | false | | |
@@ -3731,6 +3734,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
}
],
"last_model_config_id": "30ebb95f-c255-4759-9429-89aa4ec1554c",
"last_turn_summary": "string",
"mcp_server_ids": [
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
],
@@ -3777,9 +3781,9 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
#### Enumerated Values
| Value(s) |
|------------------------------------------------------------------------------------------------|
| `action_required`, `created`, `deleted`, `diff_status_change`, `status_change`, `title_change` |
| Value(s) |
|------------------------------------------------------------------------------------------------------------------|
| `action_required`, `created`, `deleted`, `diff_status_change`, `status_change`, `summary_change`, `title_change` |
## codersdk.ConnectionLatency