Skip to content

feat: add manual chat title regeneration#23633

Merged
ibetitsmike merged 49 commits intomainfrom
mike/regenerate-chat-title
Mar 27, 2026
Merged

feat: add manual chat title regeneration#23633
ibetitsmike merged 49 commits intomainfrom
mike/regenerate-chat-title

Conversation

@ibetitsmike
Copy link
Copy Markdown
Collaborator

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

  • New endpoint: POST /api/experimental/chats/{chatID}/title/regenerate returns the updated Chat with a regenerated title
  • Manual title algorithm: Extracts useful user/assistant text turns → selects first user turn + last 3 turns → builds context with gap markers → renders prompt with anti-recency guidance → calls lightweight model → normalizes output
  • Helpers: extractManualTitleTurns, selectManualTitleTurnIndexes, buildManualTitleContext, renderManualTitlePrompt, generateManualTitle — all private, with the public Server.RegenerateChatTitle method
  • SDK: ExperimentalClient.RegenerateChatTitle(ctx, chatID) (Chat, error)
  • Persists title via existing UpdateChatByID and broadcasts ChatEventKindTitleChange

Frontend

  • API client method + React Query mutation with cache invalidation
  • "Generate new title" menu item (with wand icon) in both TopBar and Sidebar dropdown menus
  • Loading/disabled state while regeneration is in-flight
  • Error toast on failure
  • Stories updated for both menus

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

  • The existing auto-title path (maybeGenerateChatTitle, titleInput) is completely unchanged
  • Manual regeneration uses richer context (first user turn + last 3 turns + gap markers) vs the auto path's single first message
  • Endpoint is experimental and marked with @x-apidocgen {"skip": true}

@ibetitsmike ibetitsmike changed the title feat(coderd/x/chatd): add manual chat title regeneration feat: add manual chat title regeneration Mar 25, 2026
@ibetitsmike
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread coderd/x/chatd/chatd.go Outdated
Comment thread coderd/x/chatd/quickgen.go Outdated
@ibetitsmike
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread site/src/api/queries/chats.ts
@ibetitsmike
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread coderd/x/chatd/chatd.go Outdated
Comment thread coderd/x/chatd/quickgen.go Outdated
@ibetitsmike
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread site/src/pages/AgentsPage/AgentsPage.tsx
Comment thread site/src/api/queries/chats.test.ts Outdated
@ibetitsmike
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread coderd/x/chatd/chatd.go Outdated
@ibetitsmike
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread coderd/x/chatd/chatd.go Outdated
Comment thread coderd/x/chatd/quickgen.go Outdated
@ibetitsmike
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread coderd/x/chatd/quickgen.go Outdated
@ibetitsmike
Copy link
Copy Markdown
Collaborator Author

@codex review

1 similar comment
@ibetitsmike
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread coderd/x/chatd/chatd.go Outdated
Comment thread site/src/pages/AgentsPage/components/AgentDetail/TopBar.tsx Outdated
@ibetitsmike
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread coderd/x/chatd/chatd.go Outdated
Comment thread site/src/pages/AgentsPage/AgentDetail.tsx
@ibetitsmike
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread coderd/x/chatd/chatd.go
Comment thread coderd/x/chatd/chatd.go Outdated
@ibetitsmike
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread coderd/x/chatd/chatd.go Outdated
@ibetitsmike
Copy link
Copy Markdown
Collaborator Author

@codex review

@ibetitsmike ibetitsmike marked this pull request as ready for review March 26, 2026 10:39
@coder-tasks
Copy link
Copy Markdown
Contributor

coder-tasks Bot commented Mar 26, 2026

Documentation Check

Updates Needed

  • docs/ai-coder/agents/chats-api.md — The new POST /api/experimental/chats/{chat}/title/regenerate endpoint is not documented here. This file manually lists all experimental chat API endpoints (since they are marked @x-apidocgen {"skip": true} and excluded from auto-generated docs). A short entry under Endpoints describing the regenerate-title endpoint, its parameters, and response would keep the reference complete.

    ⚠️ Still unaddressed — no documentation changes found in this PR


Automated review via Coder Tasks

@ibetitsmike
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown
Member

@johnstcn johnstcn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
  • publishChatPubsubEvent being invoked with ChatEventKindTitleChange
  • The title == chat.Title early-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 UsageLimitExceededError409 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 useEffect additions — all state flows through mutations and props. Clean React patterns.
  • Cache invalidationonSuccess optimistically updates both per-chat and infinite list caches; onSettled invalidates. Matches the established archive/unarchive pattern.
  • Triple-layer double-click protection — disabled menu item, guard in handler, isPending check. 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.

@ibetitsmike ibetitsmike force-pushed the mike/regenerate-chat-title branch from 9b0ceae to 947ffd6 Compare March 27, 2026 00:33
@ibetitsmike ibetitsmike force-pushed the mike/regenerate-chat-title branch from 947ffd6 to 717bc5e Compare March 27, 2026 00:33
@ibetitsmike ibetitsmike merged commit 2312e5c into main Mar 27, 2026
28 of 29 checks passed
@ibetitsmike ibetitsmike deleted the mike/regenerate-chat-title branch March 27, 2026 00:47
@github-actions github-actions Bot locked and limited conversation to collaborators Mar 27, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants