feat(web): per-user chat preferences for response style#1243
feat(web): per-user chat preferences for response style#1243wilson101xx wants to merge 7 commits into
Conversation
Adds two nullable-by-design fields on the User model in preparation for the per-user chat preferences feature: - `chatPreferences Json @default("{}")` — JSON map keyed by response-style dimension (depth, codeVisibility, vocabulary, citationDensity, outputStructure, diagrams), each value a 3-level enum string. Defaults to an empty object so existing users observe no behavior change. - `chatCustomInstructions String?` — optional free-text instructions appended to the agent's system prompt. Length is capped at the application layer (zod schema in a subsequent commit). Migration is a pure additive `ALTER TABLE ... ADD COLUMN` and is safe to run against existing databases. Refs sourcebot-dev#1242 Co-authored-by: Cursor <cursoragent@cursor.com>
Introduces the data layer for per-user chat preferences.
- `features/chat/userPreferences.ts` defines a single `CHAT_PREFERENCE_SPEC`
constant that captures all 6 dimensions, their level values, UI labels,
and the soft-bias text that gets injected into the agent's system
prompt. The zod schema, TypeScript types, and prompt-rendering helper
are all derived from this spec so they cannot drift apart.
Six dimensions × three levels each: depth, codeVisibility, vocabulary,
citationDensity, outputStructure, diagrams. Custom instructions are a
separate free-text field, length-capped at 1000 chars at the schema
layer.
`renderChatPreferencesPromptBlock()` returns a `<user_preferences>`
block phrased as a soft bias. It returns `null` when the user has set
nothing, so callers can omit the block entirely with no behavioral
change for existing users.
- `features/chat/actions.ts` adds two server actions:
- `getChatPreferences()` — auth-required, defensively parses the stored
JSONB so a corrupt value degrades to an empty object rather than
crashing the settings page.
- `updateChatPreferences()` — auth-required, validates the full
payload (rejecting unknown keys and out-of-range levels) before
writing to the user-scoped prisma client.
No UI consumer yet. Wired up in subsequent commits.
Refs sourcebot-dev#1242
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds Settings > Chat Preferences, a new per-user page where signed-in users can tune how Sourcebot writes its answers. - `page.tsx` is a thin server wrapper that loads the user's current preferences via `getChatPreferences()` and hands them to the client component as initial props. - `chatPreferencesPage.tsx` renders one row per visible dimension as a single-select `ToggleGroup`, plus a free-text "Custom instructions" `Textarea` with a live character counter and a hard cap at `CHAT_CUSTOM_INSTRUCTIONS_MAX_LENGTH` (1000). All rows derive from `CHAT_PREFERENCE_SPEC`, so adding a future dimension is a one-place change. - The form uses explicit "Save changes" and "Discard changes" controls rather than per-toggle immediate persist. A multi-field form benefits from an explicit commit point, and it keeps round-trips to a minimum. - Save handler clears empty/whitespace-only custom instructions to `null` so they're not stored as the string "". - The `diagrams` dimension is intentionally gated behind `HIDDEN_CHAT_PREFERENCE_DIMENSIONS` until inline mermaid rendering (PR sourcebot-dev#1241) lands. The data model still accepts the value. - Nav entry added under `getSidebarNavItems` immediately after "API Keys". Unconditional so every signed-in user sees it. Refs sourcebot-dev#1242 Co-authored-by: Cursor <cursoragent@cursor.com>
Threads the signed-in user's chat preferences through the agent so the final answer is biased by their saved settings. - `agent.ts` exports a new `ResolvedChatUserPreferences` interface and accepts it as an optional field on `CreateMessageStreamResponseProps`, `AgentOptions`, and `createPrompt`. When present, `renderChatPreferencesPromptBlock()` produces a `<user_preferences>` block that is appended after `</answer_instructions>` in the system prompt, where the model sees it last. When absent, no block is emitted and behavior is identical to today. - `api/(server)/chat/route.ts` loads `chatPreferences` and `chatCustomInstructions` from the user's row (only when `user` is defined, which excludes anonymous chats), parses the JSONB through `chatPreferencesSchema` defensively, and passes the result to `createMessageStream`. The query is `select`-narrowed to the two columns the agent actually needs. - `features/mcp/askCodebase.ts` does not load preferences. MCP has no user-settings surface yet; the call site has an explicit comment pointing back to sourcebot-dev#1242 so a future maintainer knows it was a conscious omission rather than an oversight. Soft-bias framing in the prompt block is intentional: preferences are described as guidance that shapes the final answer and must never override correctness or the explicit user message. This keeps existing agent behavior intact even when preferences are aggressive (e.g. "one-paragraph summary" + "describe code only" together). Refs sourcebot-dev#1242 Co-authored-by: Cursor <cursoragent@cursor.com>
Registers `wa_chat_preferences_saved` in `PosthogEventMap` and fires it on a successful save from the Chat Preferences page. The `wa_` prefix is correct here: the event can only originate from the web app. Properties capture adoption signals without leaking the actual preference content: - `dimensionsSet`: count of dimensions the user has chosen (0-6) - `hasCustomInstructions`: boolean flag for free-text usage - `customInstructionsLength`: length of the trimmed custom instructions so we can later distinguish drive-by notes from heavy use No preference values, level names, or instruction text are sent. Refs sourcebot-dev#1242 Co-authored-by: Cursor <cursoragent@cursor.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
WalkthroughThis PR implements per-user chat preferences (six style dimensions and a custom-instructions field), persists them on the User model, validates and exposes server actions to read/update them, threads resolved preferences into the chat agent's system prompt, and adds a settings UI, navigation, mocks, analytics event, and changelog entry. ChangesChat Preferences Feature
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Adds a new `### Added` heading under `[Unreleased]` (the section previously only had `### Changed`) for the per-user Chat Preferences feature. Refs sourcebot-dev#1242, sourcebot-dev#1243 Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/web/src/app/`(app)/settings/chatPreferences/chatPreferencesPage.tsx:
- Around line 103-106: The saved snapshot is storing the raw customInstructions,
but the persisted value uses trimmedCustom (or null), causing discard to restore
an unpersisted state; update the setSavedSnapshot call inside the save flow to
store the normalized value (use the trimmedCustom variable or null) instead of
customInstructions so the snapshot matches what is actually persisted (refer to
setSavedSnapshot, customInstructions, and trimmedCustom).
In `@packages/web/src/app/api/`(server)/chat/route.ts:
- Around line 110-114: The DB value row.chatCustomInstructions is forwarded raw
in route.ts when building userPreferences; validate it is a string and enforce
the 1000-character contract (or a configured MAX_CUSTOM_INSTRUCTIONS constant)
before using it. Use chatPreferencesSchema.safeParse for preferences as done,
then coerce/validate chatCustomInstructions (e.g., ensure typeof === "string",
trim, and truncate to 1000 chars) and assign that sanitized value to
userPreferences.customInstructions so oversized or malformed DB values cannot
bloat prompts.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 12251215-44ab-462c-bc7f-3cad8eb84fa2
📒 Files selected for processing (12)
CHANGELOG.mdpackages/db/prisma/migrations/20260528092627_add_chat_preferences/migration.sqlpackages/db/prisma/schema.prismapackages/web/src/app/(app)/settings/chatPreferences/chatPreferencesPage.tsxpackages/web/src/app/(app)/settings/chatPreferences/page.tsxpackages/web/src/app/(app)/settings/layout.tsxpackages/web/src/app/api/(server)/chat/route.tspackages/web/src/features/chat/actions.tspackages/web/src/features/chat/agent.tspackages/web/src/features/chat/userPreferences.tspackages/web/src/features/mcp/askCodebase.tspackages/web/src/lib/posthogEvents.ts
| setSavedSnapshot({ | ||
| preferences, | ||
| customInstructions, | ||
| }); |
There was a problem hiding this comment.
Normalize saved custom instructions to persisted value.
Line 103 stores customInstructions as typed, but Line 91 persists trimmedCustom (or null). That can make “Discard changes” restore text that was never actually stored.
Proposed fix
if (isServiceError(result)) {
toast({
title: "Failed to save chat preferences",
description: result.message,
variant: "destructive",
});
return;
}
+ const normalizedCustomInstructions = trimmedCustom.length > 0 ? trimmedCustom : "";
+ setCustomInstructions(normalizedCustomInstructions);
setSavedSnapshot({
preferences,
- customInstructions,
+ customInstructions: normalizedCustomInstructions,
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| setSavedSnapshot({ | |
| preferences, | |
| customInstructions, | |
| }); | |
| if (isServiceError(result)) { | |
| toast({ | |
| title: "Failed to save chat preferences", | |
| description: result.message, | |
| variant: "destructive", | |
| }); | |
| return; | |
| } | |
| const normalizedCustomInstructions = trimmedCustom.length > 0 ? trimmedCustom : ""; | |
| setCustomInstructions(normalizedCustomInstructions); | |
| setSavedSnapshot({ | |
| preferences, | |
| customInstructions: normalizedCustomInstructions, | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/web/src/app/`(app)/settings/chatPreferences/chatPreferencesPage.tsx
around lines 103 - 106, The saved snapshot is storing the raw
customInstructions, but the persisted value uses trimmedCustom (or null),
causing discard to restore an unpersisted state; update the setSavedSnapshot
call inside the save flow to store the normalized value (use the trimmedCustom
variable or null) instead of customInstructions so the snapshot matches what is
actually persisted (refer to setSavedSnapshot, customInstructions, and
trimmedCustom).
| const parsed = chatPreferencesSchema.safeParse(row.chatPreferences); | ||
| userPreferences = { | ||
| preferences: parsed.success ? parsed.data : {}, | ||
| customInstructions: row.chatCustomInstructions, | ||
| }; |
There was a problem hiding this comment.
Validate and cap chatCustomInstructions before passing to the agent.
Line 113 forwards DB text as-is. If stored data is malformed/oversized, this bypasses the 1000-char contract and can unnecessarily bloat prompt tokens.
💡 Suggested fix
if (row) {
const parsed = chatPreferencesSchema.safeParse(row.chatPreferences);
+ const customInstructions =
+ typeof row.chatCustomInstructions === 'string' &&
+ row.chatCustomInstructions.length <= 1000
+ ? row.chatCustomInstructions
+ : null;
userPreferences = {
preferences: parsed.success ? parsed.data : {},
- customInstructions: row.chatCustomInstructions,
+ customInstructions,
};
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const parsed = chatPreferencesSchema.safeParse(row.chatPreferences); | |
| userPreferences = { | |
| preferences: parsed.success ? parsed.data : {}, | |
| customInstructions: row.chatCustomInstructions, | |
| }; | |
| const parsed = chatPreferencesSchema.safeParse(row.chatPreferences); | |
| const customInstructions = | |
| typeof row.chatCustomInstructions === 'string' && | |
| row.chatCustomInstructions.length <= 1000 | |
| ? row.chatCustomInstructions | |
| : null; | |
| userPreferences = { | |
| preferences: parsed.success ? parsed.data : {}, | |
| customInstructions, | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/web/src/app/api/`(server)/chat/route.ts around lines 110 - 114, The
DB value row.chatCustomInstructions is forwarded raw in route.ts when building
userPreferences; validate it is a string and enforce the 1000-character contract
(or a configured MAX_CUSTOM_INSTRUCTIONS constant) before using it. Use
chatPreferencesSchema.safeParse for preferences as done, then coerce/validate
chatCustomInstructions (e.g., ensure typeof === "string", trim, and truncate to
1000 chars) and assign that sanitized value to
userPreferences.customInstructions so oversized or malformed DB values cannot
bloat prompts.
`next build` (which runs `tsc` strictly) caught three issues that
the looser ESLint pass missed. None are behavior changes — all are
type-correctness fixes.
1. `chatPreferencesPage.tsx`: `toggleVariants` only defines
`size: { default: ... }` (an icon-sized square), so `size="sm"`
on `<ToggleGroup>` was a type error. Removed the prop and overrode
sizing on each `<ToggleGroupItem>` via `className="h-9 w-auto
min-w-0 px-3"` so the text labels actually fit.
2. `features/chat/actions.ts` and `app/api/(server)/chat/route.ts`:
`chatPreferencesSchema` is built dynamically with
`z.enum(string[])`, which widens each level value to `string` in
the inferred type. Assigning `parsed.data` to the narrower
`ChatPreferences` literal-union map therefore failed strict
typecheck. Added an `as ChatPreferences` cast in both consumers
with a comment explaining why the cast is sound (runtime
validation still constrains each value to its per-dimension
level list).
3. `__mocks__/prisma.ts`: `MOCK_USER_WITH_ACCOUNTS` was missing the
two new User fields. Added `chatPreferences: {}` and
`chatCustomInstructions: null` so test code compiles against the
updated Prisma type.
Refs sourcebot-dev#1242, sourcebot-dev#1243
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Actionable comments posted: 0 |
Fixes #1242
What this adds
A per-user Chat Preferences settings page that lets each signed-in user tune how Sourcebot's chat agent writes its answers. The selections are appended to the agent's system prompt as a
<user_preferences>block phrased as a soft bias, so they never override correctness or the explicit user message.Why
Today the agent writes in one voice for everyone. PMs and VPs asking high-level questions get the same dense, code-heavy answers as engineers asking implementation questions. This is the most-flagged piece of feedback we hear internally from non-engineering users. A per-user preference set lets the same agent serve a wider audience without needing per-team or per-role presets.
Design
Six dimensions, three levels each (one level is the "default" leftmost, but the user can leave any row unset to keep the truly-default behavior):
Plus a free-text "Custom instructions" textarea, capped at 1000 chars at the schema layer.
Key design choices
apiKeys/linked-accounts. There's no realistic external-script use case for reading/writing another user's prompt-style settings. Easy to promote later if that changes.ToggleGroup type="single"rather than introducing a newRadioGroupprimitive. The codebase doesn't have one, and a button-bar is fine for 3 options. Open to feedback if you'd prefer I add a realRadioGroup.HIDDEN_CHAT_PREFERENCE_DIMENSIONSbecause inline mermaid rendering (PR feat(web): render mermaid diagrams in chat answers #1241) hasn't merged. The data model accepts the value either way, so it's a one-line flip when feat(web): render mermaid diagrams in chat answers #1241 lands.undefinedkeeps MCP behavior identical. There's a comment at the call site pointing back to [FR] Per-user chat preferences (response depth, code visibility, custom instructions) #1242.<user_preferences>block is emitted — zero behavior change for everyone who hasn't visited the settings page.getChatPreferences()and the chat route both run the stored JSON throughchatPreferencesSchema.safeParse()so a corrupt or out-of-spec value degrades to{}rather than crashing.Commits
Split into reviewable chunks. Each commit compiles, lints, and is self-contained:
feat(db): add chatPreferences fields to User model— pure additive migrationfeat(web): add chat preferences validation schema and server actions— types/zod/prompt helper +get/updateserver actionsfeat(web): add Chat Preferences settings page— UI + nav entryfeat(web): apply chat preferences to agent system prompt— agent integration, chat route loads prefs, MCP path intentionally untouchedfeat(web): add posthog event for chat preferences saved—wa_chat_preferences_savedwith adoption-only properties (no preference content sent)Test plan
<user_preferences>block should appear in the system prompt.yarn workspace @sourcebot/web lint— 0 new errors.Things I'd particularly appreciate feedback on
<user_preferences>block after</answer_instructions>so the model sees it last (recency bias). Open to moving it.Out of scope (deferred, happy to follow up)
CHANGELOG
Follow-up commit to come once the PR number is assigned, per the workflow used for #1241.
Summary by CodeRabbit