feat: add personality traits system with per-mode configuration#11979
feat: add personality traits system with per-mode configuration#11979jthweny wants to merge 1 commit intoRooCodeInc:mainfrom
Conversation
- 13 built-in personality traits (dry-wit, professor, straight-shooter, etc.) - Custom trait creation with emoji picker and prompt editor - Per-mode personality configuration via PersonalityConfig on ModeConfig - LLM-powered trait enhancement for expanding brief descriptions - Sandwich technique: personality prompt injected at top and bottom of system prompt - PersonalityTraitsPanel component with pill-grid UI, active order badges - Full test coverage for trait resolution, merging, and prompt building Made-with: Cursor
There was a problem hiding this comment.
Pull request overview
Adds a personality traits system to Roo Code, including UI for selecting/creating traits, persistence via mode config/types, and prompt-building helpers intended to inject personality into the system prompt.
Changes:
- Introduces built-in traits + helpers for resolving/merging traits and building “sandwich” prompt parts.
- Adds a new Modes UI panel to manage traits (toggle, preview, create/edit/delete) and an emoji picker.
- Extends types/settings schemas to support per-mode
personalityConfigand a global enhancer meta-prompt.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| webview-ui/src/i18n/locales/en/personality.json | Adds EN strings for the personality traits UI. |
| webview-ui/src/components/modes/PersonalityTraitsPanel.tsx | New UI panel for trait selection, preview, and custom trait CRUD. |
| webview-ui/src/components/modes/ModesView.tsx | Mounts the new personality panel into the modes view. |
| webview-ui/src/components/modes/EmojiPicker.tsx | New popover emoji picker used in trait editor. |
| src/shared/personality-traits.ts | Adds built-in traits and prompt-building/trait-resolution utilities. |
| src/core/prompts/sections/personality.ts | Exposes personality prompt section entrypoints for system prompt building. |
| src/core/prompts/sections/index.ts | Exports personality section utilities. |
| src/core/prompts/sections/custom-instructions.ts | Adds optional “personalityPrompt” injection at end of custom instructions. |
| src/core/prompts/sections/tests/personality.spec.ts | Adds tests for trait resolution and prompt building. |
| packages/types/src/mode.ts | Adds PersonalityTrait, PersonalityConfig, and ModeConfig.personalityConfig. |
| packages/types/src/global-settings.ts | Adds personalityTraitEnhancerPrompt to global settings schema. |
| path/to/data/raft_state.json | Adds a data file (appears to be runtime state). |
| path/to/data/aliases/data.json | Adds a data file (appears to be runtime state). |
| Untitled-1.jsonc | Adds a large VS Code theme JSONC file. |
| .vscode/settings.json | Adds ndjson.port to workspace settings. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| it("should return formatted section for a single active built-in trait", () => { | ||
| const config: PersonalityConfig = { | ||
| activeTraitIds: ["roo"], | ||
| customTraits: [], | ||
| } | ||
|
|
||
| const result = buildPersonalityPrompt(config) | ||
|
|
||
| expect(result).toContain("Personality & Communication Style:") | ||
| expect(result).toContain("non-negotiable") | ||
| expect(result).toContain("You are Roo") | ||
| expect(result).toContain("IMPORTANT: Maintaining this personality is critical") | ||
| }) |
There was a problem hiding this comment.
This test suite’s expectations don’t match the implementation in shared/personality-traits.ts. For example, buildPersonalityPrompt now emits PERSONALITY & VOICE (ACTIVE: ...) and CRITICAL: wording, so assertions like Personality & Communication Style: / non-negotiable / the anchor text will fail. Update the expected strings to reflect the new prompt format (or adjust the implementation to match the intended spec).
| describe("Built-in traits", () => { | ||
| it("should have 12 built-in traits", () => { | ||
| expect(BUILT_IN_PERSONALITY_TRAITS).toHaveLength(12) | ||
| }) | ||
|
|
||
| it("should have unique IDs", () => { | ||
| const ids = BUILT_IN_PERSONALITY_TRAITS.map((t) => t.id) | ||
| expect(new Set(ids).size).toBe(ids.length) | ||
| }) | ||
|
|
||
| it("should all be marked as isBuiltIn", () => { | ||
| BUILT_IN_PERSONALITY_TRAITS.forEach((trait) => { | ||
| expect(trait.isBuiltIn).toBe(true) | ||
| }) | ||
| }) | ||
|
|
||
| it("should all use direct natural-language format (no section markers)", () => { | ||
| BUILT_IN_PERSONALITY_TRAITS.forEach((trait) => { | ||
| // No [SECTION_KEY] markers should be present | ||
| expect(trait.prompt).not.toMatch(/\[COMMUNICATION_STYLE\]/) | ||
| expect(trait.prompt).not.toMatch(/\[TASK_COMPLETION\]/) | ||
| expect(trait.prompt).not.toMatch(/\[ERROR_HANDLING\]/) | ||
| expect(trait.prompt).not.toMatch(/\[SUGGESTIONS\]/) | ||
| }) | ||
| }) | ||
|
|
||
| it("should all start with identity-first framing (You are/You have/You speak/You prioritize/You question)", () => { | ||
| BUILT_IN_PERSONALITY_TRAITS.forEach((trait) => { | ||
| const startsWithIdentity = /^You (are|have|speak|prioritize|question|see)\b/.test(trait.prompt.trim()) | ||
| expect(startsWithIdentity).toBe(true) | ||
| }) |
There was a problem hiding this comment.
The built-in traits assertions are inconsistent with BUILT_IN_PERSONALITY_TRAITS in shared/personality-traits.ts: the list currently contains 13 entries (including roo-devs), but the test expects 12. Additionally, the “identity-first framing” regex only allows You are|have|speak|prioritize|question|see, but multiple prompts start with You deliver / You talk etc. Align these tests with the actual built-in trait definitions (or standardize the prompts to satisfy the test constraints).
| { | ||
| "$schema": "vscode://schemas/color-theme", | ||
| "type": "light", | ||
| "colors": { | ||
| "activityBarBadge.background": "#007acc", | ||
| "editor.background": "#ffffff", | ||
| "editor.foreground": "#000000", | ||
| "editor.inactiveSelectionBackground": "#e5ebf1", |
There was a problem hiding this comment.
Untitled-1.jsonc is a large VS Code color theme dump and doesn’t appear related to the personality traits feature. If this was added accidentally, remove it from the PR (and consider adding editor-export artifacts to .gitignore if they’re commonly generated).
| const generateTraitId = useCallback( | ||
| (label: string): string => { | ||
| const baseId = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") | ||
| let id = baseId | ||
| let attempt = 0 | ||
| while (allTraits.some((t) => t.id === id)) { | ||
| attempt++ | ||
| id = `${baseId}-${attempt}` | ||
| } | ||
| return id | ||
| }, |
There was a problem hiding this comment.
generateTraitId() can return an empty string when the label has no [a-z0-9] characters (e.g. "!!!" or non‑Latin text), which would violate the PersonalityTrait schema (id min length 1) and can create duplicate/invalid traits. Add a fallback baseId (e.g. "trait") when slugification results in an empty string, and consider trimming/collapsing consecutive dashes before uniqueness checks.
| <button | ||
| onClick={(e) => { | ||
| e.stopPropagation() | ||
| startEditing(trait) | ||
| }} | ||
| className="w-5 h-5 rounded-full bg-vscode-badge-background text-vscode-badge-foreground flex items-center justify-center hover:bg-vscode-button-hoverBackground transition-colors"> | ||
| <Pencil className="w-3 h-3" /> | ||
| </button> | ||
| </StandardTooltip> | ||
| <StandardTooltip content={t("personality:deleteTrait")}> | ||
| <button | ||
| onClick={(e) => { | ||
| e.stopPropagation() | ||
| handleDeleteTrait(trait.id) | ||
| }} | ||
| className="w-5 h-5 rounded-full bg-vscode-badge-background text-vscode-badge-foreground flex items-center justify-center hover:bg-vscode-errorForeground transition-colors"> | ||
| <Trash2 className="w-3 h-3" /> | ||
| </button> |
There was a problem hiding this comment.
The icon-only edit/delete buttons rely on tooltip/visual affordances but don’t set aria-label attributes. Add explicit aria-labels so keyboard/screen reader users can discover and use these actions reliably.
| // Inject personality prompt LAST for maximum recency effect. | ||
| // This is the last thing the model reads before generating, | ||
| // which research shows produces the strongest behavioral adherence. | ||
| if (options.personalityPrompt && options.personalityPrompt.trim()) { | ||
| sections.push(options.personalityPrompt.trim()) | ||
| } | ||
|
|
There was a problem hiding this comment.
The new personalityPrompt option is never passed by any call sites (no matches for personalityPrompt: in src/), so the “inject personality prompt last” behavior won’t run in production. Ensure the system prompt generation path builds the personality bottom block and passes it into addCustomInstructions (or otherwise injects it).
| // Inject personality prompt LAST for maximum recency effect. | |
| // This is the last thing the model reads before generating, | |
| // which research shows produces the strongest behavioral adherence. | |
| if (options.personalityPrompt && options.personalityPrompt.trim()) { | |
| sections.push(options.personalityPrompt.trim()) | |
| } |
| const isBuiltIn = BUILT_IN_PERSONALITY_TRAITS.some((t) => t.id === traitId) | ||
| let newConfig = { ...personalityConfig } | ||
|
|
||
| if (isBuiltIn) { | ||
| // Mark built-in as deleted (can be restored later) | ||
| newConfig.deletedBuiltInTraitIds = [...(newConfig.deletedBuiltInTraitIds || []), traitId] |
There was a problem hiding this comment.
When deleting a built-in trait, deletedBuiltInTraitIds appends the id without checking for duplicates. Repeated deletes will accumulate duplicates and make the config noisy. Consider guarding with includes() (or using a Set-like update) and also defensively prevent deleting the protected "roo" trait at this handler level.
| const isBuiltIn = BUILT_IN_PERSONALITY_TRAITS.some((t) => t.id === traitId) | |
| let newConfig = { ...personalityConfig } | |
| if (isBuiltIn) { | |
| // Mark built-in as deleted (can be restored later) | |
| newConfig.deletedBuiltInTraitIds = [...(newConfig.deletedBuiltInTraitIds || []), traitId] | |
| // Prevent deletion of protected built-in trait | |
| if (traitId === "roo") { | |
| return | |
| } | |
| const isBuiltIn = BUILT_IN_PERSONALITY_TRAITS.some((t) => t.id === traitId) | |
| let newConfig = { ...personalityConfig } | |
| if (isBuiltIn) { | |
| // Mark built-in as deleted (can be restored later), avoiding duplicates | |
| const existingDeleted = newConfig.deletedBuiltInTraitIds || [] | |
| if (!existingDeleted.includes(traitId)) { | |
| newConfig.deletedBuiltInTraitIds = [...existingDeleted, traitId] | |
| } |
| @@ -0,0 +1 @@ | |||
| {"state":{"hard_state":{"term":0,"vote":0,"commit":0},"conf_state":{"voters":[4166060179281456],"learners":[],"voters_outgoing":[],"learners_next":[],"auto_leave":false}},"latest_snapshot_meta":{"term":0,"index":0},"apply_progress_queue":null,"first_voter":4166060179281456,"peer_address_by_id":{},"peer_metadata_by_id":{},"this_peer_id":4166060179281456} No newline at end of file | |||
There was a problem hiding this comment.
This looks like generated runtime state (Raft hard_state/conf_state) rather than source code. Committing it will create noisy diffs and potentially leak environment-specific IDs. Remove it from the PR and add the containing path to .gitignore if it’s produced locally.
| {"state":{"hard_state":{"term":0,"vote":0,"commit":0},"conf_state":{"voters":[4166060179281456],"learners":[],"voters_outgoing":[],"learners_next":[],"auto_leave":false}},"latest_snapshot_meta":{"term":0,"index":0},"apply_progress_queue":null,"first_voter":4166060179281456,"peer_address_by_id":{},"peer_metadata_by_id":{},"this_peer_id":4166060179281456} | |
| {} |
| @@ -0,0 +1 @@ | |||
| {} No newline at end of file | |||
There was a problem hiding this comment.
This appears to be local/generated data committed accidentally (empty aliases DB). If it’s runtime state, it should not be versioned; remove it from the PR and add an ignore rule for this path.
| {} | |
| { | |
| "_meta": { | |
| "description": "Aliases configuration file. This file is intentionally versioned; add alias mappings under the 'aliases' key.", | |
| "version": 1 | |
| }, | |
| "aliases": { | |
| } | |
| } |
| "vitest.disableWorkspaceWarning": true, | ||
| "ndjson.port": 7700 |
There was a problem hiding this comment.
The ndjson.port setting looks like a developer-local VS Code extension preference rather than a project-wide requirement. Consider removing it from the repo-level .vscode/settings.json (or documenting why it must be shared) to avoid forcing local preferences on contributors.
| "vitest.disableWorkspaceWarning": true, | |
| "ndjson.port": 7700 | |
| "vitest.disableWorkspaceWarning": true |
Made-with: Cursor
Related GitHub Issue
Closes: #
Roo Code Task Context (Optional)
Description
Test Procedure
Pre-Submission Checklist
Screenshots / Videos
Documentation Updates
Additional Notes
Get in Touch
Interactively review PR in Roo Code Cloud