Skip to content

feat(jef-92): custom AI prompt in Settings for cover letters and chat - #250

Merged
mankatcheung merged 2 commits into
mainfrom
feat/jef-92-user-prompt-for-the-ai
Aug 6, 2026
Merged

mankatcheung merged 2 commits into
mainfrom
feat/jef-92-user-prompt-for-the-ai

Conversation

@mankatcheung

@mankatcheung mankatcheung commented Aug 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Adds a user-authored customAiPrompt field, editable in Settings → AI features, that gets spliced into the system prompt for the two AI paths that generate prose the user reads: GenerateCoverLetterUseCase and ChatWithAssistantUseCase.
  • Structured-extraction use cases (ParseJobDescriptionUseCase, ComputeResumeMatchScoreUseCase) are intentionally left untouched — they're JSON-mode extraction, not prose a custom tone/style instruction should affect.
  • Backend: customAiPrompt: string | null threaded through the domain entity → Drizzle schema (+ generated migration 0010_red_steel_serpent.sql) → IUserRepository/DrizzleUserRepository → reused UpdateProfileUseCase (adds MAX_AI_PROMPT_LENGTH = 500 validation, same trim/empty-to-null normalization as name/targetRole) → UserType/UserMapper → updateProfile mutation (new optional 4th arg).
  • GenerateCoverLetterUseCase gained a userRepository dep (fetched concurrently with the existing profile-building Promise.all) and appends the prompt as a second system message when set.
  • ChatWithAssistantUseCase's user fetch moved earlier in execute() (previously fetched lazily inside complete() only when the conversation had no locked provider) so it can also drive the system-prompt splice; the existing defaultLlmProvider fallback logic is unchanged in behavior.
  • Frontend follows the Settings area's existing hand-maintained query/mutation-string pattern (this area bypasses the .graphql + codegen pipeline) — new textarea in integrations.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

  • New/updated unit tests: 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 typecheck clean across all 4 packages (api, web, cli, extension).
  • pnpm lint — 0 errors (pre-existing unrelated warnings only).
  • Full API suite: 1006/1006 passing in isolation. (One test, 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, unmodified main checkout, so it's pre-existing environment flakiness unrelated to this change, not a regression.)
  • Full web suite: 243/243 passing, including the existing SettingsIntegrationsPage/SettingsProfilePage smoke tests.
  • Manually verified the full backend chain against a real (non-mocked) local SQLite DB: ran db:migrate, started the API server, registered a user, called updateProfile(customAiPrompt: "..."), and confirmed me { customAiPrompt } round-trips correctly. Also confirmed the 500-char validation rejects an over-limit prompt with a VALIDATION error.
  • Did not visually exercise the new Settings textarea in a browser (no browser tooling available in this session) — worth a quick manual look before merging.

Summary by CodeRabbit

  • New Features

    • Added a custom AI instructions field in account integrations settings.
    • Saved instructions are applied to AI assistant conversations and cover letter generation.
    • Instructions can be updated or cleared, with a 500-character limit and input validation.
    • Saved instructions are displayed when revisiting settings.
  • Bug Fixes

    • Improved provider selection so conversation settings remain authoritative while user defaults are used when appropriate.
  • Tests

    • Added coverage for saving, trimming, clearing, limiting, and applying custom instructions.

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).
@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@mankatcheung, you've reached your PR review limit, so we couldn't start this review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 76ad3ea3-17e4-4f11-ae13-766c7170d3ff

📥 Commits

Reviewing files that changed from the base of the PR and between 18027b4 and ac5d21c.

📒 Files selected for processing (8)
  • apps/api/drizzle/0011_opposite_speed_demon.sql
  • apps/api/drizzle/meta/0011_snapshot.json
  • apps/api/drizzle/meta/_journal.json
  • apps/api/src/__tests__/application/chat/ChatWithAssistantUseCase.test.ts
  • apps/api/src/__tests__/helpers/createTestDb.ts
  • apps/api/src/__tests__/helpers/mocks.ts
  • apps/api/src/infrastructure/db/schema.ts
  • apps/api/src/use-cases/chat/ChatWithAssistantUseCase.ts

Walkthrough

Adds a nullable customAiPrompt field to users. The field can be edited in settings, validated and persisted through GraphQL, and inserted into assistant chat and cover-letter generation prompts.

Changes

Custom AI prompt

Layer / File(s) Summary
Prompt persistence and profile API
apps/api/drizzle/*, apps/api/src/domain/user/User.ts, apps/api/src/infrastructure/db/*, apps/api/src/interface-adapters/*, apps/api/src/use-cases/user/*, apps/api/src/http/schema/*
Adds the nullable field to the database, user model, repository, DTO, GraphQL type, and profile update flow. Trims values, converts whitespace-only values to null, and rejects values over 500 characters.
Prompt application in AI generation
apps/api/src/use-cases/chat/ChatWithAssistantUseCase.ts, apps/api/src/use-cases/coverLetter/GenerateCoverLetterUseCase.ts
Loads the user and adds customAiPrompt as an additional system message when configured. Chat provider selection uses the conversation provider before the user default provider.
Settings prompt form
apps/web/src/routes/_authenticated/settings/-components/shared.ts, apps/web/src/routes/_authenticated/settings/integrations.tsx
Adds GraphQL query and mutation support, client-side validation, a custom-instructions textarea, save handling, cache invalidation, and status messages.
Prompt behaviour tests and fixtures
apps/api/src/__tests__/application/*, apps/api/src/__tests__/helpers/*
Covers prompt insertion, omission, trimming, clearing, length validation, provider handling, database schema, and user fixtures.

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
Loading

Possibly related issues

  • #247 — Directly covers the custom AI prompt feature implemented by this PR.

Possibly related PRs

Poem

A rabbit saved a prompt with care,
Then sent it through the model’s lair.
Chat and letters took the cue,
Settings made the change feel new.
Five hundred chars, neatly trimmed—
The prompt now hops where thoughts are skimmed.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the custom AI prompt setting and its use in cover letters and chat.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feat/jef-92-user-prompt-for-the-ai
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/jef-92-user-prompt-for-the-ai

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mankatcheung
mankatcheung marked this pull request as ready for review August 5, 2026 23:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/api/src/__tests__/application/user/UpdateProfileUseCase.test.ts (1)

151-161: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test 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.update receives 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 value

Optional: 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.all removes 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

📥 Commits

Reviewing files that changed from the base of the PR and between 517fa0d and 18027b4.

📒 Files selected for processing (22)
  • apps/api/drizzle/0010_red_steel_serpent.sql
  • apps/api/drizzle/meta/0010_snapshot.json
  • apps/api/drizzle/meta/_journal.json
  • apps/api/src/__tests__/application/chat/ChatWithAssistantUseCase.test.ts
  • apps/api/src/__tests__/application/coverLetter/GenerateCoverLetterUseCase.test.ts
  • apps/api/src/__tests__/application/user/UpdateProfileUseCase.test.ts
  • apps/api/src/__tests__/helpers/createTestDb.ts
  • apps/api/src/__tests__/helpers/mocks.ts
  • apps/api/src/domain/user/User.ts
  • apps/api/src/http/schema/mutations/userMutations.ts
  • apps/api/src/http/schema/types/UserType.ts
  • apps/api/src/infrastructure/db/repositories/DrizzleUserRepository.ts
  • apps/api/src/infrastructure/db/schema.ts
  • apps/api/src/interface-adapters/mappers/UserMapper.ts
  • apps/api/src/interface-adapters/resolvers/UserResolver.ts
  • apps/api/src/use-cases/chat/ChatWithAssistantUseCase.ts
  • apps/api/src/use-cases/coverLetter/GenerateCoverLetterUseCase.ts
  • apps/api/src/use-cases/ports/IUserRepository.ts
  • apps/api/src/use-cases/user/IUpdateProfileUseCase.ts
  • apps/api/src/use-cases/user/UpdateProfileUseCase.ts
  • apps/web/src/routes/_authenticated/settings/-components/shared.ts
  • apps/web/src/routes/_authenticated/settings/integrations.tsx

Comment on lines +358 to +364
<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."
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
<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
@mankatcheung
mankatcheung merged commit e729f91 into main Aug 6, 2026
11 checks passed
@mankatcheung
mankatcheung deleted the feat/jef-92-user-prompt-for-the-ai branch August 6, 2026 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant