feat(jef-92): custom AI prompt in Settings for cover letters and chat - #250
Conversation
Adds a user-authored instruction (customAiPrompt) surfaced in Settings → AI features, spliced into the system prompt for both AI-generated prose paths: GenerateCoverLetterUseCase and ChatWithAssistantUseCase. Structured-extraction paths (job description parsing, resume match scoring) are intentionally left untouched since they're JSON-mode, not prose the user reads. Threaded through: domain entity, schema + migration, IUserRepository, DrizzleUserRepository, UpdateProfileUseCase (reused for the write path, +MAX_AI_PROMPT_LENGTH validation), UserType/UserMapper, and the updateProfile mutation. Frontend follows the Settings area's existing hand-maintained query/mutation-string pattern (no codegen).
|
Warning Review limit reached
Next review available in: 40 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
WalkthroughAdds a nullable ChangesCustom AI prompt
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Settings
participant GraphQL
participant UserResolver
participant UpdateProfileUseCase
participant DrizzleUserRepository
participant ChatWithAssistantUseCase
participant LLM
Settings->>GraphQL: Save customAiPrompt
GraphQL->>UserResolver: Call updateProfile
UserResolver->>UpdateProfileUseCase: Execute validated update
UpdateProfileUseCase->>DrizzleUserRepository: Persist prompt
ChatWithAssistantUseCase->>DrizzleUserRepository: Load user prompt
ChatWithAssistantUseCase->>LLM: Send prompt as a system message
Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
⚔️ Resolve merge conflicts 💡
🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
apps/api/src/__tests__/application/user/UpdateProfileUseCase.test.ts (1)
151-161: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winTest the accepted 500-character boundary.
This test only proves that 501 characters are rejected. Add a test that submits 500 characters and verifies that
userRepository.updatereceives the value. This protects the stated maximum from an>=regression.🤖 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 `@apps/api/src/__tests__/application/user/UpdateProfileUseCase.test.ts` around lines 151 - 161, Add a test alongside the existing UpdateProfileUseCase maximum-length test that executes with a 500-character customAiPrompt and verifies userRepository.update is called with that value, preserving the documented inclusive boundary and existing rejection coverage for 501 characters.apps/api/src/use-cases/chat/ChatWithAssistantUseCase.ts (1)
74-81: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueOptional: load the user and the history in parallel.
Line 76 and line 81 await two independent queries in sequence. Both are needed before the messages are assembled.
Promise.allremoves one round trip from the chat request path.♻️ Proposed refactor
- // Fetched once up front: needed both for the custom-AI-prompt system - // message below and as the defaultLlmProvider fallback inside complete(). - const user = await this.deps.userRepository.findById(input.userId); - - // Stored history only ever contains 'user'/'assistant' turns we wrote - // ourselves — the per-turn tool-call scratchpad below is rebuilt fresh - // each time and never persisted. - const history = await this.deps.messageRepository.findAllByConversationId(input.conversationId); + // The user is fetched once up front: it is needed both for the + // custom-AI-prompt system message below and as the defaultLlmProvider + // fallback inside complete(). + // Stored history only ever contains 'user'/'assistant' turns we wrote + // ourselves — the per-turn tool-call scratchpad below is rebuilt fresh + // each time and never persisted. + const [user, history] = await Promise.all([ + this.deps.userRepository.findById(input.userId), + this.deps.messageRepository.findAllByConversationId(input.conversationId), + ]);🤖 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 `@apps/api/src/use-cases/chat/ChatWithAssistantUseCase.ts` around lines 74 - 81, Update the user and history loading in ChatWithAssistantUseCase to start both independent repository queries concurrently with Promise.all, then retain their existing values for prompt construction and complete().
🤖 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 `@apps/web/src/routes/_authenticated/settings/integrations.tsx`:
- Around line 358-364: Update the “Custom instructions” label and the textarea
registered through customAiPromptForm.register so they share a stable
association: assign a unique id to the textarea and set the label’s htmlFor to
the same id.
---
Nitpick comments:
In `@apps/api/src/__tests__/application/user/UpdateProfileUseCase.test.ts`:
- Around line 151-161: Add a test alongside the existing UpdateProfileUseCase
maximum-length test that executes with a 500-character customAiPrompt and
verifies userRepository.update is called with that value, preserving the
documented inclusive boundary and existing rejection coverage for 501
characters.
In `@apps/api/src/use-cases/chat/ChatWithAssistantUseCase.ts`:
- Around line 74-81: Update the user and history loading in
ChatWithAssistantUseCase to start both independent repository queries
concurrently with Promise.all, then retain their existing values for prompt
construction and complete().
🪄 Autofix
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 Plus
Run ID: 155f2d85-d951-4d65-94ca-122dc3a578a8
📒 Files selected for processing (22)
apps/api/drizzle/0010_red_steel_serpent.sqlapps/api/drizzle/meta/0010_snapshot.jsonapps/api/drizzle/meta/_journal.jsonapps/api/src/__tests__/application/chat/ChatWithAssistantUseCase.test.tsapps/api/src/__tests__/application/coverLetter/GenerateCoverLetterUseCase.test.tsapps/api/src/__tests__/application/user/UpdateProfileUseCase.test.tsapps/api/src/__tests__/helpers/createTestDb.tsapps/api/src/__tests__/helpers/mocks.tsapps/api/src/domain/user/User.tsapps/api/src/http/schema/mutations/userMutations.tsapps/api/src/http/schema/types/UserType.tsapps/api/src/infrastructure/db/repositories/DrizzleUserRepository.tsapps/api/src/infrastructure/db/schema.tsapps/api/src/interface-adapters/mappers/UserMapper.tsapps/api/src/interface-adapters/resolvers/UserResolver.tsapps/api/src/use-cases/chat/ChatWithAssistantUseCase.tsapps/api/src/use-cases/coverLetter/GenerateCoverLetterUseCase.tsapps/api/src/use-cases/ports/IUserRepository.tsapps/api/src/use-cases/user/IUpdateProfileUseCase.tsapps/api/src/use-cases/user/UpdateProfileUseCase.tsapps/web/src/routes/_authenticated/settings/-components/shared.tsapps/web/src/routes/_authenticated/settings/integrations.tsx
| <label className={labelCls}>Custom instructions</label> | ||
| <textarea | ||
| {...customAiPromptForm.register('customAiPrompt')} | ||
| className={inputCls} | ||
| rows={3} | ||
| placeholder="e.g. Keep cover letters under 200 words and write in a casual tone." | ||
| /> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Associate the label with the textarea.
The label does not reference the textarea. Screen readers can expose the textarea without the “Custom instructions” name.
Add a stable id to the textarea. Set htmlFor on the label to that ID.
Proposed fix
- <label className={labelCls}>Custom instructions</label>
+ <label htmlFor="custom-ai-prompt" className={labelCls}>
+ Custom instructions
+ </label>
<textarea
+ id="custom-ai-prompt"
{...customAiPromptForm.register('customAiPrompt')}📝 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.
| <label className={labelCls}>Custom instructions</label> | |
| <textarea | |
| {...customAiPromptForm.register('customAiPrompt')} | |
| className={inputCls} | |
| rows={3} | |
| placeholder="e.g. Keep cover letters under 200 words and write in a casual tone." | |
| /> | |
| <label htmlFor="custom-ai-prompt" className={labelCls}> | |
| Custom instructions | |
| </label> | |
| <textarea | |
| id="custom-ai-prompt" | |
| {...customAiPromptForm.register('customAiPrompt')} | |
| className={inputCls} | |
| rows={3} | |
| placeholder="e.g. Keep cover letters under 200 words and write in a casual tone." | |
| /> |
🤖 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 `@apps/web/src/routes/_authenticated/settings/integrations.tsx` around lines
358 - 364, Update the “Custom instructions” label and the textarea registered
through customAiPromptForm.register so they share a stable association: assign a
unique id to the textarea and set the label’s htmlFor to the same id.
…pt-for-the-ai # Conflicts: # apps/api/drizzle/meta/0010_snapshot.json # apps/api/drizzle/meta/_journal.json
Summary
customAiPromptfield, editable in Settings → AI features, that gets spliced into the system prompt for the two AI paths that generate prose the user reads:GenerateCoverLetterUseCaseandChatWithAssistantUseCase.ParseJobDescriptionUseCase,ComputeResumeMatchScoreUseCase) are intentionally left untouched — they're JSON-mode extraction, not prose a custom tone/style instruction should affect.customAiPrompt: string | nullthreaded through the domain entity → Drizzle schema (+ generated migration0010_red_steel_serpent.sql) →IUserRepository/DrizzleUserRepository→ reusedUpdateProfileUseCase(addsMAX_AI_PROMPT_LENGTH = 500validation, same trim/empty-to-null normalization asname/targetRole) →UserType/UserMapper→updateProfilemutation (new optional 4th arg).GenerateCoverLetterUseCasegained auserRepositorydep (fetched concurrently with the existing profile-buildingPromise.all) and appends the prompt as a second system message when set.ChatWithAssistantUseCase's user fetch moved earlier inexecute()(previously fetched lazily insidecomplete()only when the conversation had no locked provider) so it can also drive the system-prompt splice; the existingdefaultLlmProviderfallback logic is unchanged in behavior..graphql+ codegen pipeline) — new textarea inintegrations.tsx's "AI features" section,react-hook-form+ zod, mirroring the existing LLM API key form already in that file.Linear: JEF-92
Test plan
UpdateProfileUseCase.test.ts(trim/clear/max-length for the new field),GenerateCoverLetterUseCase.test.ts(prompt included/omitted as 2nd system message),ChatWithAssistantUseCase.test.ts(prompt spliced in as 2nd system message; updated the provider-fallback test since the user is now always fetched).pnpm typecheckclean across all 4 packages (api, web, cli, extension).pnpm lint— 0 errors (pre-existing unrelated warnings only).container.test.ts, times out only when run as part of the full 169-file parallel suite on this machine — reproduced identically on a clean, unmodifiedmaincheckout, so it's pre-existing environment flakiness unrelated to this change, not a regression.)SettingsIntegrationsPage/SettingsProfilePagesmoke tests.db:migrate, started the API server, registered a user, calledupdateProfile(customAiPrompt: "..."), and confirmedme { customAiPrompt }round-trips correctly. Also confirmed the 500-char validation rejects an over-limit prompt with aVALIDATIONerror.Summary by CodeRabbit
New Features
Bug Fixes
Tests