feat: add manual chat title regeneration#23633
Conversation
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b3f2716058
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 961c2449db
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ca1ea4ef2d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d03c928f19
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7782b3ac63
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f6219a2ea
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 984ec7b084
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
1 similar comment
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9506521c09
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e31ca3becb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a377ff7f04
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 597e69456e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
Documentation CheckUpdates Needed
Automated review via Coder Tasks |
|
@codex review |
johnstcn
left a comment
There was a problem hiding this comment.
Deep Review: PR #23633 — Regenerate Chat Title
Critical
1. Missing authz test for UpdateChatLastModelConfigByID (dbauthz_test.go)
Every other UpdateChat* method has a corresponding authz test in dbauthz_test.go. This new method does not. CI's method coverage checker may catch it, but the PR should include it. This is the only new DB mutation that lacks test coverage of its permission gate.
2. Authorization check ordering leaks server state (exp_chats.go:1961-1971)
if api.chatDaemon == nil { // ← checked FIRST
httpapi.Write(ctx, rw, http.StatusInternalServerError, ...)
return
}
if !api.Authorize(r, policy.ActionUpdate, chat.RBACObject()) { // ← checked SECOND
httpapi.ResourceNotFound(rw)
return
}A principal that can read but not update the chat receives a 500 "Chat processor is unavailable" instead of 404. Authz must precede business-logic guards. Swap the two blocks.
Major
3. No happy-path test — anywhere (exp_chats_test.go, chatd_internal_test.go)
The HTTP test suite has only three error-path subcases (ChatNotFound, NotFoundForDifferentUser, NoDaemon). The chatd_internal_test.go test only exercises the "no user prompt → empty title → return chat unchanged" path. There is zero test coverage of:
- A new title being generated, persisted, and returned
publishChatPubsubEventbeing invoked withChatEventKindTitleChange- The
title == chat.Titleearly-return path (same title → skip DB update) - The error-on-generate-but-record-usage path
4. recordErr masks the original generation error (chatd.go)
if err != nil {
if recordErr := p.recordManualTitleUsage(ctx, chat, modelConfig, usage); recordErr != nil {
return database.Chat{}, xerrors.Errorf("record manual title usage: %w", recordErr)
// ↑ original `err` is silently discarded
}
return database.Chat{}, xerrors.Errorf("generate manual title: %w", err)
}If both fail, the caller sees "record manual title usage: ..." with no mention of the underlying generation failure. Use errors.Join or wrap both.
5. Usage debit + title update are not atomic (chatd.go)
recordManualTitleUsage runs in its own transaction, then UpdateChatByID runs standalone. If the usage recording succeeds but the title update fails, tokens are consumed for a title that was never applied. Either wrap both in a single transaction or move usage recording after the title update.
6. generatePushSummary has no input length limit (quickgen.go:473)
assistantText is unbounded. A multi-hundred-line code generation response produces a very large prompt. generateShortText caps output at 256 tokens but there's no cap on input. This wastes tokens and risks exceeding context windows on smaller models.
7. Test coverage gaps in quickgen.go
The following core functions have zero direct test coverage: titleInput, normalizeTitleOutput, fallbackChatTitle, truncateRunes, maybeGenerateChatTitle, generatePushSummary. The most important function (titleInput) and the main orchestrator (maybeGenerateChatTitle) are completely untested.
8. "NoDaemon" test name is wrong — it doesn't test nil branch (exp_chats_test.go:3098)
coderdtest.New() always initializes chatDaemon. The 500 comes from RegenerateChatTitle failing internally, not from the nil guard. The chatDaemon == nil branch has zero test coverage. Rename to "RegenerationFailure" and add a separate nil-daemon test.
Minor
9. Inconsistent first-page cursor guard in SQL (chats.sql)
The DescPaginated query uses a defensive CASE WHEN @before_id::bigint > 0 THEN ... ELSE true END. The new AscPaginated relies on id > 0 being vacuously true. Should use the same pattern for symmetry.
10. UpdateChatLastModelConfigByID omits updated_at = NOW() without comment (chats.sql)
Every other UpdateChat* mutation sets updated_at = NOW(). The omission here is intentional (this is a restore operation that shouldn't change list ordering), but it needs a SQL comment to prevent a future maintainer from "fixing" it.
11. Dead code branch in maybeGenerateChatTitle (quickgen.go:105)
if title == "" || title == chat.Title {generateTitle returns xerrors.New("generated title was empty") when the title is empty, so title == "" after a successful call is unreachable. Dead logic obscures control flow.
12. Double quote-stripping between generateShortText and normalizeTitleOutput (quickgen.go)
Both functions strip `"'``. Redundant cleanup violates single-responsibility and creates confusion about who owns normalization.
13. Empty chat wastes DB queries before early-return (chatd.go)
When a chat has zero messages, resolveChatModel (two DB queries via errgroup) runs before discovering there's nothing to generate. Early-return on len(messages) == 0 before calling resolveChatModel saves wasted I/O.
14. manualTitleTurn.role is string, not the typed enum (quickgen.go:268)
Using database.ChatMessageRole would provide compile-time safety and avoid repetitive string(...) casts throughout the code.
15. Prompt injection via --- delimiters (quickgen.go:377-390)
renderManualTitlePrompt embeds user-controlled text between --- markers. A user can inject --- on its own line to break the delimiter structure. More robust delimiters (triple-backtick fencing, random nonce) would be safer.
16. No accessible announcement when title changes (frontend)
The title text gains animate-pulse and a <Spinner> — purely visual feedback. No aria-live region announces the change to screen readers. The spinner also lacks an aria-label.
17. Global mutation lock blocks ALL chats (frontend)
isRegenerateTitleDisabled uses the global isPending flag. If chat A is regenerating, chat B's menu item is also disabled. The regeneratingTitleChatId is already available — scope the disabled state per-chat.
18. NotFoundForDifferentUser tests middleware, not handler authz (exp_chats_test.go:3076)
The 404 comes from ExtractChatParam middleware (ActionRead), not the handler's explicit Authorize(ActionUpdate). The handler's authz check has no dedicated test coverage.
19. Missing unauthenticated access test (exp_chats_test.go)
Other test suites (e.g., TestStreamChat) include explicit Unauthenticated subcases. This is missing here.
20. maybeWriteLimitErr branch untested (exp_chats.go:1975)
The UsageLimitExceededError → 409 path has no test coverage.
21. Optional typing weakens contract (frontend AgentsOutletContext)
isRegeneratingTitle?: boolean and regeneratingTitleChatId?: string | null are optional but every real consumer provides them. Remove the ? modifiers.
22. No story for regenerating/disabled states (frontend TopBar.stories.tsx)
The new GenerateTitle story only verifies the menu item exists. No stories for isRegeneratingTitle={true} (spinner + pulse) or the disabled state.
Nit
23. Inconsistent title length ranges in prompts
titleGenerationPrompt says "2-8 words". renderManualTitlePrompt says "2-5 words". The same UI element gets different length constraints depending on auto vs. manual generation.
24. pushSummaryPrompt says "under 100 characters" but output isn't validated
If the model exceeds 100 chars, the push notification body gets platform-truncated unpredictably.
25. Test case name misleading (quickgen_test.go:118)
"dedupes and sorts when first user is in the trailing window" — the first user (index 2) is before the trailing window [3,4,5]. No deduplication occurs. Name should be "prepends first user when before trailing window".
26. deadlineCapturingModel naming (quickgen_test.go:469)
Used by three tests for different purposes. A name like fakeModel or stubModel would be more accurate.
27. Sidebar pulse animation but no spinner — inconsistency with TopBar (frontend)
TopBar shows both animate-pulse and <Spinner>. Sidebar shows only animate-pulse. Deliberate space constraint decision, or accidental?
28. fallbackChatTitle can truncate its own ellipsis (quickgen.go:229-233)
If the 6-word title + "…" exceeds 80 runes, truncateRunes cuts past the ellipsis. Edge case, cosmetic.
29. renderManualTitlePrompt has fragile coupling with caller truncation (quickgen.go:387)
The function re-truncates firstUserText to 1000 for comparison but doesn't truncate it in the prompt itself. The real truncation contract should be at the function boundary.
30. AgentEmbedPage stubs are correct but uncommented
onRegenerateTitle: () => {},
isRegeneratingTitle: false,
regeneratingTitleChatId: null,These are never consumed (dropdown hidden in embed mode). A // Not supported in embed mode. comment clarifies intent.
Scoreboard
| Severity | Count |
|---|---|
| Critical | 2 |
| Major | 6 |
| Minor | 14 |
| Nit | 8 |
What's Done Well
- Zero
useEffectadditions — all state flows through mutations and props. Clean React patterns. - Cache invalidation —
onSuccessoptimistically updates both per-chat and infinite list caches;onSettledinvalidates. Matches the established archive/unarchive pattern. - Triple-layer double-click protection — disabled menu item, guard in handler,
isPendingcheck. Users cannot spam the button. - Correct authz wrappers — both new DB methods properly gate on parent chat access with correct policy actions.
- No schema migrations needed — operates on existing columns and tables.
- Consistent SDK method — signature and behavior match established patterns.
- Manual regeneration uses richer context — first user turn + last 3 turns + gap markers is a smart design for title quality.
9b0ceae to
947ffd6
Compare
947ffd6 to
717bc5e
Compare
Summary
Adds a "Generate new title" action that lets users manually regenerate a chat's title using richer conversation context than the automatic first-message title path.
Changes
Backend
POST /api/experimental/chats/{chatID}/title/regeneratereturns the updated Chat with a regenerated titleextractManualTitleTurns,selectManualTitleTurnIndexes,buildManualTitleContext,renderManualTitlePrompt,generateManualTitle— all private, with the publicServer.RegenerateChatTitlemethodExperimentalClient.RegenerateChatTitle(ctx, chatID) (Chat, error)UpdateChatByIDand broadcastsChatEventKindTitleChangeFrontend
Tests
quickgen_test.go: Table-driven tests for all 4 helper functions (turn extraction, index selection, context building, prompt rendering)exp_chats_test.go: Handler tests (ChatNotFound, NotFoundForDifferentUser, NoDaemon)Design notes
maybeGenerateChatTitle,titleInput) is completely unchanged@x-apidocgen {"skip": true}