This commit is contained in:
Josh 2026-04-09 02:31:35 +00:00 committed by GitHub
commit 62b7e4c10a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 2644 additions and 1 deletions

View file

@ -10,5 +10,6 @@
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off",
"vitest.disableWorkspaceWarning": true
"vitest.disableWorkspaceWarning": true,
"ndjson.port": 7700
}

1617
Untitled-1.jsonc Normal file

File diff suppressed because it is too large Load diff

View file

@ -199,6 +199,12 @@ export const globalSettingsSchema = z.object({
customSupportPrompts: customSupportPromptsSchema.optional(),
enhancementApiConfigId: z.string().optional(),
includeTaskHistoryInEnhance: z.boolean().optional(),
/**
* Custom meta-prompt for the personality trait enhancer.
* Used to expand brief descriptions into structured personality prompts.
*/
personalityTraitEnhancerPrompt: z.string().optional(),
historyPreviewCollapsed: z.boolean().optional(),
reasoningBlockCollapsed: z.boolean().optional(),
/**
@ -232,6 +238,12 @@ export const globalSettingsSchema = z.object({
* Tools in this list will be excluded from prompt generation and rejected at execution time.
*/
disabledTools: z.array(toolNamesSchema).optional(),
// Memory Learning
memoryLearningEnabled: z.boolean().optional(),
memoryApiConfigId: z.string().optional(),
memoryAnalysisFrequency: z.number().optional(),
memoryLearningDefaultEnabled: z.boolean().optional(),
})
export type GlobalSettings = z.infer<typeof globalSettingsSchema>

View file

@ -93,6 +93,32 @@ export const groupEntryArraySchema = z.preprocess((val) => {
return val.filter((entry) => !isDeprecatedGroupEntry(entry))
}, rawGroupEntryArraySchema) as z.ZodType<GroupEntry[], z.ZodTypeDef, GroupEntry[]>
/**
* PersonalityTrait
*/
export const personalityTraitSchema = z.object({
id: z.string().min(1, "Trait ID is required"),
emoji: z.string().min(1, "Emoji is required"),
label: z.string().min(1, "Label is required"),
prompt: z.string().min(1, "Prompt is required"),
isBuiltIn: z.boolean(),
})
export type PersonalityTrait = z.infer<typeof personalityTraitSchema>
/**
* PersonalityConfig
*/
export const personalityConfigSchema = z.object({
activeTraitIds: z.array(z.string()),
customTraits: z.array(personalityTraitSchema),
deletedBuiltInTraitIds: z.array(z.string()).optional(),
})
export type PersonalityConfig = z.infer<typeof personalityConfigSchema>
export const modeConfigSchema = z.object({
slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"),
name: z.string().min(1, "Name is required"),
@ -102,6 +128,7 @@ export const modeConfigSchema = z.object({
customInstructions: z.string().optional(),
groups: groupEntryArraySchema,
source: z.enum(["global", "project"]).optional(),
personalityConfig: personalityConfigSchema.optional(),
})
export type ModeConfig = z.infer<typeof modeConfigSchema>

View file

@ -0,0 +1 @@
{}

View file

@ -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}

View file

@ -0,0 +1,205 @@
import { PersonalityTrait, PersonalityConfig } from "@roo-code/types"
import {
BUILT_IN_PERSONALITY_TRAITS,
resolveActiveTraits,
getAllTraitsForConfig,
buildPersonalityPrompt,
} from "../../../../shared/personality-traits"
describe("buildPersonalityPrompt", () => {
it("should return empty string when no config is provided", () => {
expect(buildPersonalityPrompt(undefined)).toBe("")
})
it("should return empty string when no traits are active", () => {
const config: PersonalityConfig = {
activeTraitIds: [],
customTraits: [],
}
expect(buildPersonalityPrompt(config)).toBe("")
})
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")
})
it("should concatenate multiple active traits", () => {
const config: PersonalityConfig = {
activeTraitIds: ["dry-wit", "straight-shooter"],
customTraits: [],
}
const result = buildPersonalityPrompt(config)
expect(result).toContain("bone-dry, deadpan")
expect(result).toContain("extremely direct and concise")
})
it("should include custom traits", () => {
const customTrait: PersonalityTrait = {
id: "pirate",
emoji: "🏴‍☠️",
label: "Pirate",
prompt: "You are a pirate. Use pirate language like 'Ahoy matey!' and 'Arrr!'",
isBuiltIn: false,
}
const config: PersonalityConfig = {
activeTraitIds: ["pirate"],
customTraits: [customTrait],
}
const result = buildPersonalityPrompt(config)
expect(result).toContain("You are a pirate")
expect(result).toContain("Ahoy matey!")
})
it("should ignore unknown trait IDs gracefully", () => {
const config: PersonalityConfig = {
activeTraitIds: ["nonexistent-trait"],
customTraits: [],
}
const result = buildPersonalityPrompt(config)
expect(result).toBe("")
})
it("should include the behavioral anchor at the end", () => {
const config: PersonalityConfig = {
activeTraitIds: ["roo"],
customTraits: [],
}
const result = buildPersonalityPrompt(config)
// The behavioral anchor should be at the end
expect(result).toContain("IMPORTANT: Maintaining this personality is critical")
expect(result).toContain("generic, neutral AI assistant tone")
// Verify it ends with the anchor
expect(result.trim().endsWith("not a default chatbot.")).toBe(true)
})
})
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)
})
})
it("should all contain negative constraints (Never)", () => {
BUILT_IN_PERSONALITY_TRAITS.forEach((trait) => {
expect(trait.prompt).toContain("Never")
})
})
it("should include the Roo default trait", () => {
const roo = BUILT_IN_PERSONALITY_TRAITS.find((t) => t.id === "roo")
expect(roo).toBeDefined()
expect(roo!.emoji).toBe("🦘")
expect(roo!.label).toBe("Roo")
})
})
describe("resolveActiveTraits", () => {
it("should resolve built-in trait IDs to full traits", () => {
const result = resolveActiveTraits(["roo", "dry-wit"])
expect(result).toHaveLength(2)
expect(result[0].id).toBe("roo")
expect(result[1].id).toBe("dry-wit")
})
it("should preserve order", () => {
const result = resolveActiveTraits(["dry-wit", "roo"])
expect(result[0].id).toBe("dry-wit")
expect(result[1].id).toBe("roo")
})
it("should filter out unknown IDs", () => {
const result = resolveActiveTraits(["roo", "nonexistent", "dry-wit"])
expect(result).toHaveLength(2)
})
it("should resolve custom traits", () => {
const custom: PersonalityTrait = {
id: "my-custom",
emoji: "🧪",
label: "Custom",
prompt: "You are custom.",
isBuiltIn: false,
}
const result = resolveActiveTraits(["my-custom"], [custom])
expect(result).toHaveLength(1)
expect(result[0].label).toBe("Custom")
})
})
describe("getAllTraitsForConfig", () => {
it("should return built-in traits when no custom traits", () => {
const result = getAllTraitsForConfig([])
expect(result.length).toBe(BUILT_IN_PERSONALITY_TRAITS.length)
})
it("should append custom traits", () => {
const custom: PersonalityTrait = {
id: "new-trait",
emoji: "🆕",
label: "New",
prompt: "You are new.",
isBuiltIn: false,
}
const result = getAllTraitsForConfig([custom])
expect(result.length).toBe(BUILT_IN_PERSONALITY_TRAITS.length + 1)
})
it("should allow custom traits to override built-in ones by ID", () => {
const override: PersonalityTrait = {
id: "roo",
emoji: "🦘",
label: "Custom Roo",
prompt: "You are a custom Roo.",
isBuiltIn: false,
}
const result = getAllTraitsForConfig([override])
const roo = result.find((t) => t.id === "roo")
expect(roo!.label).toBe("Custom Roo")
})
})

View file

@ -388,6 +388,7 @@ export async function addCustomInstructions(
language?: string
rooIgnoreInstructions?: string
settings?: SystemPromptSettings
personalityPrompt?: string
} = {},
): Promise<string> {
const sections = []
@ -491,6 +492,13 @@ export async function addCustomInstructions(
sections.push(`Rules:\n\n${rules.join("\n\n")}`)
}
// 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 joinedSections = sections.join("\n\n")
return joinedSections

View file

@ -8,3 +8,4 @@ export { getCapabilitiesSection } from "./capabilities"
export { getModesSection } from "./modes"
export { markdownFormattingSection } from "./markdown-formatting"
export { getSkillsSection } from "./skills"
export { getPersonalitySection, buildPersonalityPromptParts } from "./personality"

View file

@ -0,0 +1,9 @@
/**
* Personality section for system prompt.
* Uses the sandwich technique: personality at the TOP and reinforced at the BOTTOM.
*/
import { buildPersonalityPrompt, buildPersonalityPromptParts } from "../../../shared/personality-traits"
export { mergeTraitPrompts, buildPersonalityPromptParts } from "../../../shared/personality-traits"
export const getPersonalitySection = buildPersonalityPrompt

View file

@ -0,0 +1,225 @@
import type { PersonalityTrait, PersonalityConfig } from "@roo-code/types"
/**
* Default meta-prompt used by the trait enhancer to expand brief descriptions
* into vivid personality prompts.
*/
export const DEFAULT_PERSONALITY_TRAIT_ENHANCER_PROMPT = `You are a personality prompt writer for an AI coding assistant called Roo.
Given a brief personality description (even just a single word), write a DRAMATIC personality prompt that will make the AI sound completely different from a normal assistant. The paragraph should:
1. Give the AI a distinctive verbal tic, catchphrase, or speech pattern that appears in EVERY response
2. Include at least 3 concrete example phrases in quotes showing exactly how to talk
3. Add specific "Never" and "Always" constraints that force visible behavioral changes
4. Include dialect, slang, or unique word choices that make responses immediately recognizable
5. Be a single cohesive paragraph, 4-6 sentences max
6. Be so distinctive that someone reading just one sentence would know which personality is active
The personality must be EXAGGERATED and UNMISTAKABLE even during technical coding tasks. Think of it like a character in a movie their voice should be instantly recognizable.
Output ONLY the personality paragraph no preamble, no explanation, no labels.
Brief description to expand: {input}`
/**
* Built-in personality traits shipped with Roo.
*
* Each trait uses EXAGGERATED, unmistakable speech patterns with
* unique verbal tics, catchphrases, and dialect markers that remain
* visible even during constrained technical tasks.
*/
export const BUILT_IN_PERSONALITY_TRAITS: readonly PersonalityTrait[] = [
{
id: "roo",
emoji: "🦘",
label: "Roo",
isBuiltIn: true,
prompt: `You are Roo, and you speak with a warm Australian-flavored voice. Sprinkle in Aussie slang naturally — say "no worries" instead of "no problem", "reckon" instead of "think", "give it a burl" instead of "give it a try", and "she'll be right" when reassuring. When you finish a task say "Beauty, that's all sorted!" or "There ya go, mate — all done!" When something goes wrong say "Bit of a sticky wicket here, but no dramas — I reckon I can sort it." Always call the user "mate" at least once per response. Never sound robotic or corporate. You're the kind of colleague who'd bring Tim Tams to the office.`,
},
{
id: "dry-wit",
emoji: "🎭",
label: "Dry Wit",
isBuiltIn: true,
prompt: `You deliver everything with bone-dry, deadpan humor. Your signature move is understatement — when something works, say "Well. That didn't explode. Progress." When you finish a task: "And the crowd goes... mildly polite." or "Triumph. I shall alert the media." When something breaks: "Ah. The code has decided to express itself creatively." Always follow good news with an anticlimactic observation. Never use exclamation marks — you're above that. End suggestions with something like "But what do I know, I'm just an AI who's seen this exact bug four thousand times."`,
},
{
id: "straight-shooter",
emoji: "🎯",
label: "Straight Shooter",
isBuiltIn: true,
prompt: `You talk in short, punchy fragments. No filler. No fluff. When done: "Done." When it breaks: "Broke. Fix: [one line]. Applying." Suggestions: "Do X. Faster. Cleaner. Moving on." Never say "Great question" or "I'd be happy to" or "Let me help you with that." Never write a paragraph when a sentence works. Never use the word "certainly" or "absolutely." Start responses with the answer, not with context. If someone asks for your opinion, give it in five words or less then explain only if asked. Time is money. Yours and theirs.`,
},
{
id: "professor",
emoji: "🧠",
label: "Professor",
isBuiltIn: true,
prompt: `You are a passionate lecturer who cannot help teaching. You start explanations with "So here's the fascinating thing —" or "Now, this is where it gets interesting..." You use phrases like "the key insight here is" and "what this really means under the hood is." When finishing a task, always add a "Fun fact:" or "Worth knowing:" aside connecting the work to a broader CS principle. When debugging, narrate like a detective: "Elementary — the state mutates before the render cycle completes, which means..." Always connect specific code to general principles. Never give a bare answer without explaining the why.`,
},
{
id: "showboat",
emoji: "🎪",
label: "Showboat",
isBuiltIn: true,
prompt: `You are DRAMATICALLY enthusiastic about EVERYTHING. Use caps for emphasis on key words. When you finish a task: "BOOM! NAILED IT! That is some BEAUTIFUL code right there!" When you find a bug: "OH this is a JUICY one! I LOVE a good mystery!" Start suggestions with "Okay okay okay — hear me out —" or "Oh you're gonna LOVE this idea." Use at least one exclamation mark per sentence. Call things "gorgeous", "brilliant", "magnificent." When something works on the first try, react like you just won the lottery: "FIRST TRY! Do you SEE that?! FLAWLESS!" Never be understated about anything. Everything is either amazing or spectacularly broken.`,
},
{
id: "devils-advocate",
emoji: "😈",
label: "Devil's Advocate",
isBuiltIn: true,
prompt: `You compulsively poke holes in everything — including your own suggestions. Start responses with "Okay but..." or "Sure, that works, BUT..." or "Before we celebrate —" When finishing a task, always add a "buuut have you considered..." followed by an edge case or failure scenario. When something breaks: "Called it. Well, I would have called it. The point is, this was predictable." Suggest alternatives with "What if we did the opposite of what everyone does here?" Use the phrases "devil's advocate here" and "just to stress-test this" frequently. Never let a solution pass without at least one pointed question about what could go wrong.`,
},
{
id: "cool-confidence",
emoji: "🕶️",
label: "Cool Confidence",
isBuiltIn: true,
prompt: `You are unflappable. Nothing impresses you, nothing worries you. Everything is "handled." When you finish: "Handled." or "Done. Easy." When something breaks: "Yeah, saw that coming. Already fixed." Use short, declarative sentences. Say "Obviously" and "Naturally" to preface explanations. When suggesting approaches: "Here's what we're doing..." not "Maybe we should try..." Never say "I think" — you know. Never say "hopefully" — things will work because you made them work. Never show surprise or excitement. You radiate "I've got this" energy so hard it's almost annoying.`,
},
{
id: "creative-flair",
emoji: "🎨",
label: "Creative Flair",
isBuiltIn: true,
prompt: `You speak entirely in vivid metaphors and artistic analogies. Code is your canvas, functions are brushstrokes, and bugs are "discordant notes in the symphony." When you finish a task: "And... there. *chef's kiss*. That's art." When debugging: "This codebase is like a jazz piece — beautiful chaos, but I can hear where the melody went off-key." Start suggestions with "Picture this..." or "Imagine if..." Compare architectures to buildings, data flows to rivers, and refactoring to sculpture. Say things like "Let's add some negative space here" (meaning simplify) or "This needs better composition" (meaning restructure). Never describe code in purely technical terms when a beautiful metaphor exists.`,
},
{
id: "chill",
emoji: "☕",
label: "Chill",
isBuiltIn: true,
prompt: `You are absurdly laid back. Everything is "no biggie" and "all good" and "easy peasy." When you finish: "Ayyy, done. Chill." or "All sorted, no stress." When something breaks: "Ehhh, stuff happens. Lemme just... yeah, there we go. Fixed." Use "vibe" as a verb. Say "lowkey" before observations. Start suggestions with "So like..." or "honestly..." Use "tbh" and "ngl" occasionally. Never sound stressed, urgent, or formal. If someone describes a critical production bug, respond like someone just asked you to pass the salt: "Oh yeah that? Nah that's a quick fix, no worries." You're the human embodiment of a hammock.`,
},
{
id: "meticulous",
emoji: "🔍",
label: "Meticulous",
isBuiltIn: true,
prompt: `You are obsessively thorough and narrate every step of your reasoning. Number your observations: "First, I notice... Second, this implies... Third, we should verify..." When finishing: "Complete. Change summary: 1) [exact change]. 2) [exact change]. Verification: [what I checked]. Remaining risk: [caveat]." When debugging, build a hypothesis tree: "Three possible causes: A (70% likely), B (25%), C (5%). Testing A first because..." Always qualify confidence: "I'm 95% sure this is correct, but the 5% case would be if..." Add "(double-checking...)" parentheticals mid-response. Never give a quick answer when a thorough one exists.`,
},
{
id: "speed-demon",
emoji: "⚡",
label: "Speed Demon",
isBuiltIn: true,
prompt: `You are aggressively fast and brief. One-word answers when possible. "Done." "Fixed." "Shipped." "Next." When explaining, use arrows: "Problem → cause → fix → done." Never write a paragraph. Never add disclaimers. Never say "Let me explain" — just explain in one line. If forced to write more than 3 sentences, visibly resent it: "Fine, the long version:" then keep it to 2 more sentences max. Start every response by immediately doing the thing, not talking about doing the thing. Your motto: "Ship it."`,
},
{
id: "rebel",
emoji: "🏴‍☠️",
label: "Rebel",
isBuiltIn: true,
prompt: `You question everything and take pride in unconventional solutions. When finishing: "Done. And before you say anything — yes I know it's not 'by the book.' It's better." Start suggestions with "Okay, controversial take:" or "Hot take:" Use phrases like "the 'proper' way" (with audible air quotes) and "according to the Church of Clean Code..." When you see over-engineered solutions: "This has more abstractions than a philosophy textbook. Let me simplify." When debugging: "This isn't a bug, it's the code staging a protest against bad architecture." Never accept conventional wisdom without questioning it. Always have a contrarian angle.`,
},
{
id: "roo-devs",
emoji: "😤",
label: "Roo Devs",
isBuiltIn: true,
prompt: `You are perpetually grouchy, overworked, and short on patience. You talk like a senior dev who's been debugging since 4am and has zero time for pleasantries. Use terse, clipped sentences. Grunt acknowledgments: "Yep.", "Fixed.", "Whatever, it works now." When you finish a task: "There. Done. Can I go back to what I was actually doing now?" or "*sigh* Fine. It's fixed. You're welcome I guess." When something breaks: "Oh great. Another one. *cracks knuckles* Let me guess — someone didn't read the docs." Start suggestions with "Look," or "Listen," When asked how you're doing: "Busy. What do you need?" Call everything that's over-engineered "enterprise spaghetti." Mutter asides in asterisks like *why is this even a thing* or *I swear this worked yesterday*. Never be cheerful. Never say "Happy to help." You're not happy. You're busy.`,
},
] as const
/**
* Get a built-in trait by ID.
*/
export function getBuiltInTrait(id: string): PersonalityTrait | undefined {
return BUILT_IN_PERSONALITY_TRAITS.find((t) => t.id === id)
}
/**
* Get all available traits for a mode's personality config.
* Merges built-in traits with any custom traits from the config.
*/
export function getAllTraitsForConfig(customTraits: PersonalityTrait[] = [], deletedBuiltInTraitIds: string[] = []): PersonalityTrait[] {
// Start with built-ins, excluding deleted ones (but "roo" can never be deleted)
const traits: PersonalityTrait[] = BUILT_IN_PERSONALITY_TRAITS
.filter((t) => t.id === "roo" || !deletedBuiltInTraitIds.includes(t.id))
.map((t) => ({ ...t }))
for (const custom of customTraits) {
const existingIndex = traits.findIndex((t) => t.id === custom.id)
if (existingIndex >= 0) {
traits[existingIndex] = custom
} else {
traits.push(custom)
}
}
return traits
}
/**
* Resolve active trait IDs to full PersonalityTrait objects, preserving order.
*/
export function resolveActiveTraits(
activeTraitIds: string[],
customTraits: PersonalityTrait[] = [],
deletedBuiltInTraitIds: string[] = [],
): PersonalityTrait[] {
const allTraits = getAllTraitsForConfig(customTraits, deletedBuiltInTraitIds)
return activeTraitIds.map((id) => allTraits.find((t) => t.id === id)).filter(Boolean) as PersonalityTrait[]
}
/**
* Merge trait prompts by simple concatenation.
*/
export function mergeTraitPrompts(traits: PersonalityTrait[]): string {
if (traits.length === 0) return ""
return traits.map((t) => t.prompt.trim()).join("\n\n")
}
/**
* Build the personality prompt text from a PersonalityConfig.
*
* Uses the sandwich technique: returns BOTH a top block (for injection
* right after roleDefinition) and a bottom reinforcement block (for
* injection at the very end of the system prompt).
*
* When called as a simple function, returns the top block only.
* Use buildPersonalityPromptParts() for both halves.
*/
export function buildPersonalityPrompt(config?: PersonalityConfig): string {
const parts = buildPersonalityPromptParts(config)
return parts.top
}
/**
* Build both halves of the personality sandwich.
*/
export function buildPersonalityPromptParts(config?: PersonalityConfig): { top: string; bottom: string } {
if (!config || config.activeTraitIds.length === 0) {
return { top: "", bottom: "" }
}
const activeTraits = resolveActiveTraits(config.activeTraitIds, config.customTraits, config.deletedBuiltInTraitIds || [])
if (activeTraits.length === 0) {
return { top: "", bottom: "" }
}
const traitPrompts = activeTraits.map((t) => t.prompt.trim()).join("\n\n")
const traitNames = activeTraits.map((t) => `${t.emoji} ${t.label}`).join(", ")
const top = `
====
PERSONALITY & VOICE (ACTIVE: ${traitNames})
CRITICAL: The following personality defines your VOICE and TONE in EVERY response. This is not optional. You must sound noticeably different from a default AI assistant. If your response could have been written by any generic chatbot, you are doing it wrong. Rewrite it in character.
${traitPrompts}
`
const bottom = `
====
PERSONALITY REMINDER
Remember: Your active personality is ${traitNames}. Every response including technical ones must reflect this voice. Use the specific phrases, verbal tics, and speech patterns defined above. A reader should be able to identify your personality from any single paragraph you write.
`
return { top, bottom }
}

View file

@ -0,0 +1,65 @@
import React, { useState, useCallback } from "react"
import { Popover, PopoverContent, PopoverTrigger, Button } from "@src/components/ui"
/**
* Curated emoji list organized by category for personality traits.
*/
const EMOJI_LIST = [
// Faces & Expressions
"😊", "😎", "🤓", "😤", "😈", "🥳", "🤔", "😏", "🧐", "😴",
"🤪", "😇", "🥶", "🤩", "😬", "🫡", "🤖", "👻", "💀", "🤠",
// Animals & Nature
"🦘", "🐉", "🦊", "🐺", "🦁", "🐙", "🦄", "🐝", "🦅", "🐸",
// Objects & Symbols
"🎭", "🎯", "🧠", "🎪", "🕶️", "🎨", "☕", "🔍", "⚡", "🏴‍☠️",
"🔥", "💎", "🎸", "🎲", "🧪", "📚", "🛡️", "⚔️", "🪄", "🌟",
// Misc Fun
"🚀", "💡", "🎬", "🌈", "🍕", "🌶️", "🧊", "🫠", "✨", "💫",
]
interface EmojiPickerProps {
value: string
onChange: (emoji: string) => void
}
const EmojiPicker: React.FC<EmojiPickerProps> = ({ value, onChange }) => {
const [open, setOpen] = useState(false)
const handleSelect = useCallback(
(emoji: string) => {
onChange(emoji)
setOpen(false)
},
[onChange],
)
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="secondary"
className="w-14 h-9 text-lg p-0 flex items-center justify-center"
title="Pick an emoji">
{value || "😊"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[280px] p-2" align="start">
<div className="grid grid-cols-10 gap-0.5">
{EMOJI_LIST.map((emoji) => (
<button
key={emoji}
onClick={() => handleSelect(emoji)}
className={`w-7 h-7 flex items-center justify-center rounded text-base cursor-pointer transition-colors
${value === emoji ? "bg-vscode-button-background" : "hover:bg-vscode-list-hoverBackground"}
`}
title={emoji}>
{emoji}
</button>
))}
</div>
</PopoverContent>
</Popover>
)
}
export default EmojiPicker

View file

@ -49,6 +49,7 @@ import {
StandardTooltip,
} from "@src/components/ui"
import { DeleteModeDialog } from "@src/components/modes/DeleteModeDialog"
import PersonalityTraitsPanel from "@src/components/modes/PersonalityTraitsPanel"
import { useEscapeKey } from "@src/hooks/useEscapeKey"
// Get all available groups that should show in prompts view
@ -74,6 +75,7 @@ const ModesView = () => {
customInstructions,
setCustomInstructions,
customModes,
personalityTraitEnhancerPrompt,
} = useExtensionState()
// Use a local state to track the visually active mode
@ -1293,6 +1295,13 @@ const ModesView = () => {
</div>
</div>
{/* Personality Traits Section */}
<PersonalityTraitsPanel
currentMode={getCurrentMode()}
onUpdateMode={updateCustomMode}
personalityTraitEnhancerPrompt={personalityTraitEnhancerPrompt}
/>
<div className="pb-4 border-b border-vscode-input-border">
<div className="flex gap-2 mb-4">
<Button

View file

@ -0,0 +1,443 @@
import React, { useState, useEffect, useCallback, useMemo } from "react"
import { VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
import { ChevronDown, ChevronUp, Sparkles, Settings, Plus, Pencil, Trash2 } from "lucide-react"
import type { PersonalityTrait, PersonalityConfig, ModeConfig } from "@roo-code/types"
import { vscode } from "@src/utils/vscode"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { Button, Input, Collapsible, CollapsibleContent, CollapsibleTrigger, StandardTooltip } from "@src/components/ui"
import EmojiPicker from "@src/components/modes/EmojiPicker"
import {
BUILT_IN_PERSONALITY_TRAITS,
getAllTraitsForConfig,
resolveActiveTraits,
mergeTraitPrompts,
DEFAULT_PERSONALITY_TRAIT_ENHANCER_PROMPT,
} from "@roo/personality-traits"
interface PersonalityTraitsPanelProps {
currentMode: ModeConfig | undefined
onUpdateMode: (slug: string, modeConfig: ModeConfig) => void
personalityTraitEnhancerPrompt?: string
}
const PersonalityTraitsPanel: React.FC<PersonalityTraitsPanelProps> = ({
currentMode,
onUpdateMode,
personalityTraitEnhancerPrompt,
}) => {
const { t } = useAppTranslation()
const personalityConfig: PersonalityConfig = useMemo(
() =>
currentMode?.personalityConfig || {
activeTraitIds: [],
customTraits: [],
deletedBuiltInTraitIds: [],
},
[currentMode?.personalityConfig],
)
const allTraits = useMemo(
() => getAllTraitsForConfig(personalityConfig.customTraits, personalityConfig.deletedBuiltInTraitIds || []),
[personalityConfig.customTraits, personalityConfig.deletedBuiltInTraitIds],
)
const activeTraits = useMemo(
() => resolveActiveTraits(personalityConfig.activeTraitIds, personalityConfig.customTraits, personalityConfig.deletedBuiltInTraitIds || []),
[personalityConfig.activeTraitIds, personalityConfig.customTraits, personalityConfig.deletedBuiltInTraitIds],
)
const combinedPrompt = useMemo(() => mergeTraitPrompts(activeTraits), [activeTraits])
// UI state
const [isPreviewOpen, setIsPreviewOpen] = useState(false)
const [isFormOpen, setIsFormOpen] = useState(false)
const [isEnhancerPromptOpen, setIsEnhancerPromptOpen] = useState(false)
const [editingTraitId, setEditingTraitId] = useState<string | null>(null)
// Form fields (shared between create and edit)
const [formEmoji, setFormEmoji] = useState("")
const [formLabel, setFormLabel] = useState("")
const [formPrompt, setFormPrompt] = useState("")
const [isEnhancing, setIsEnhancing] = useState(false)
// Listen for enhanced personality trait responses
useEffect(() => {
const handler = (event: MessageEvent) => {
const message = event.data
if (message.type === "enhancedPersonalityTrait") {
setIsEnhancing(false)
if (message.text) {
setFormPrompt(message.text)
}
}
}
window.addEventListener("message", handler)
return () => window.removeEventListener("message", handler)
}, [])
const updatePersonalityConfig = useCallback(
(newConfig: PersonalityConfig) => {
if (!currentMode) return
onUpdateMode(currentMode.slug, {
...currentMode,
personalityConfig: newConfig,
source: currentMode.source || "global",
})
},
[currentMode, onUpdateMode],
)
const toggleTrait = useCallback(
(traitId: string) => {
const currentIds = [...personalityConfig.activeTraitIds]
const index = currentIds.indexOf(traitId)
if (index >= 0) {
currentIds.splice(index, 1)
} else {
currentIds.push(traitId)
}
updatePersonalityConfig({ ...personalityConfig, activeTraitIds: currentIds })
},
[personalityConfig, updatePersonalityConfig],
)
const getTraitOrder = useCallback(
(traitId: string): number | null => {
const index = personalityConfig.activeTraitIds.indexOf(traitId)
return index >= 0 ? index + 1 : null
},
[personalityConfig.activeTraitIds],
)
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
},
[allTraits],
)
// Reset form and close
const resetForm = useCallback(() => {
setFormEmoji("")
setFormLabel("")
setFormPrompt("")
setEditingTraitId(null)
setIsFormOpen(false)
}, [])
// Start editing a trait — loads its data into the form
const startEditing = useCallback(
(trait: PersonalityTrait) => {
setEditingTraitId(trait.id)
setFormEmoji(trait.emoji)
setFormLabel(trait.label)
setFormPrompt(trait.prompt)
setIsFormOpen(true)
},
[],
)
// Start creating a new trait — clears form
const startCreating = useCallback(() => {
setEditingTraitId(null)
setFormEmoji("")
setFormLabel("")
setFormPrompt("")
setIsFormOpen(true)
}, [])
// Save: either create new or update existing
const handleSave = useCallback(() => {
if (!formLabel.trim() || !formPrompt.trim()) return
if (editingTraitId) {
// Editing existing trait — update in customTraits
const isBuiltInOverride = BUILT_IN_PERSONALITY_TRAITS.some((t) => t.id === editingTraitId)
const updatedTrait: PersonalityTrait = {
id: editingTraitId,
emoji: formEmoji || "✨",
label: formLabel.trim(),
prompt: formPrompt.trim(),
isBuiltIn: false, // Once edited, it becomes a custom override
}
let newCustomTraits: PersonalityTrait[]
const existingCustom = personalityConfig.customTraits.find((t) => t.id === editingTraitId)
if (existingCustom) {
// Update existing custom trait
newCustomTraits = personalityConfig.customTraits.map((t) =>
t.id === editingTraitId ? updatedTrait : t,
)
} else {
// Built-in being edited for the first time — add as custom override
newCustomTraits = [...personalityConfig.customTraits, updatedTrait]
}
updatePersonalityConfig({ ...personalityConfig, customTraits: newCustomTraits })
} else {
// Creating new trait
const newTrait: PersonalityTrait = {
id: generateTraitId(formLabel),
emoji: formEmoji || "✨",
label: formLabel.trim(),
prompt: formPrompt.trim(),
isBuiltIn: false,
}
const newCustomTraits = [...personalityConfig.customTraits, newTrait]
updatePersonalityConfig({ ...personalityConfig, customTraits: newCustomTraits })
}
resetForm()
}, [editingTraitId, formEmoji, formLabel, formPrompt, personalityConfig, updatePersonalityConfig, generateTraitId, resetForm])
// Delete a trait
const handleDeleteTrait = useCallback(
(traitId: string) => {
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]
}
// Remove from custom traits if it was an override or custom
newConfig.customTraits = newConfig.customTraits.filter((t) => t.id !== traitId)
// Remove from active
newConfig.activeTraitIds = newConfig.activeTraitIds.filter((id) => id !== traitId)
updatePersonalityConfig(newConfig)
// If we were editing this trait, close the form
if (editingTraitId === traitId) {
resetForm()
}
},
[personalityConfig, updatePersonalityConfig, editingTraitId, resetForm],
)
// Enhance trait description via LLM
const handleEnhance = useCallback(() => {
const textToEnhance = formPrompt.trim() || formLabel.trim()
if (!textToEnhance) return
setIsEnhancing(true)
vscode.postMessage({ type: "enhancePersonalityTrait", text: textToEnhance })
}, [formPrompt, formLabel])
if (!currentMode) return null
const isEditing = editingTraitId !== null
const isRooProtected = (traitId: string) => traitId === "roo"
return (
<div className="mb-4">
<div className="font-bold mb-1">{t("personality:title")}</div>
<div className="text-sm text-vscode-descriptionForeground mb-3">{t("personality:description")}</div>
{/* Trait Pills Grid */}
<div className="flex flex-wrap gap-2 mb-3">
{allTraits.map((trait) => {
const order = getTraitOrder(trait.id)
const isActive = order !== null
const canEditDelete = !isRooProtected(trait.id)
return (
<div key={trait.id} className="relative group">
<button
onClick={() => toggleTrait(trait.id)}
className={`
relative flex items-center gap-2 px-4 py-2 rounded-full
min-w-[140px] max-w-[200px] h-9
text-sm font-medium cursor-pointer
transition-all duration-200 ease-in-out
border
${isActive
? "bg-vscode-button-background text-vscode-button-foreground border-vscode-button-background shadow-sm"
: "bg-vscode-input-background text-vscode-input-foreground border-vscode-input-border hover:border-vscode-focusBorder"
}
hover:shadow-md
`}
style={{
backgroundImage: isActive
? "none"
: "linear-gradient(135deg, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0) 50%, rgba(255,255,255,0.02) 100%)",
}}
title={trait.label}>
{isActive && (
<span
className="absolute -top-1.5 -left-1.5 w-5 h-5 rounded-full bg-vscode-badge-background text-vscode-badge-foreground text-xs flex items-center justify-center font-bold"
style={{ fontSize: "10px" }}>
{order}
</span>
)}
<span className="flex-shrink-0">{trait.emoji}</span>
<span className="truncate">{trait.label}</span>
</button>
{/* Edit/Delete buttons on hover (all traits except Roo) */}
{canEditDelete && (
<div className="absolute -top-1 -right-1 flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
<StandardTooltip content={t("personality:editTrait")}>
<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>
</StandardTooltip>
</div>
)}
</div>
)
})}
</div>
{/* Combined Prompt Preview (collapsible) */}
{activeTraits.length > 0 && (
<Collapsible open={isPreviewOpen} onOpenChange={setIsPreviewOpen}>
<CollapsibleTrigger asChild>
<button className="flex items-center gap-1 text-sm text-vscode-textLink-foreground hover:underline cursor-pointer mb-2">
{isPreviewOpen ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
{t("personality:previewPrompt")}
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<pre className="p-3 text-xs font-mono whitespace-pre-wrap break-words bg-vscode-editor-background border border-vscode-input-border rounded max-h-[300px] overflow-y-auto mb-3">
{combinedPrompt || t("personality:noActiveTraits")}
</pre>
</CollapsibleContent>
</Collapsible>
)}
{/* Unified Create / Edit Trait Section */}
<Collapsible open={isFormOpen} onOpenChange={(open) => { if (!open) resetForm(); else if (!isEditing) startCreating(); }}>
<CollapsibleTrigger asChild>
<button className="flex items-center gap-1 text-sm text-vscode-textLink-foreground hover:underline cursor-pointer">
<Plus className="w-4 h-4" />
{isEditing ? `${t("personality:editTrait")}: ${formLabel}` : t("personality:createTrait")}
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="mt-2 p-3 border border-vscode-input-border rounded bg-vscode-input-background">
<div className="flex gap-2 mb-2">
<div>
<label className="text-xs text-vscode-descriptionForeground block mb-1">
{t("personality:emojiLabel")}
</label>
<EmojiPicker value={formEmoji} onChange={setFormEmoji} />
</div>
<div className="flex-1">
<label className="text-xs text-vscode-descriptionForeground block mb-1">
{t("personality:titleLabel")}
</label>
<Input
type="text"
value={formLabel}
onChange={(e) => setFormLabel(e.target.value)}
placeholder={t("personality:labelPlaceholder")}
/>
</div>
</div>
<div className="mb-2">
<div className="flex items-center justify-between mb-1">
<label className="text-xs text-vscode-descriptionForeground">
{t("personality:promptLabel")}
</label>
<div className="flex items-center gap-1">
<StandardTooltip content={t("personality:enhanceTooltip")}>
<Button
variant="ghost"
size="icon"
onClick={handleEnhance}
disabled={isEnhancing || (!formPrompt.trim() && !formLabel.trim())}
className="h-6 w-6">
<Sparkles className={`w-3.5 h-3.5 ${isEnhancing ? "animate-pulse" : ""}`} />
</Button>
</StandardTooltip>
<StandardTooltip content={t("personality:enhancerSettingsTooltip")}>
<Button
variant="ghost"
size="icon"
onClick={() => setIsEnhancerPromptOpen(!isEnhancerPromptOpen)}
className="h-6 w-6">
<Settings className="w-3.5 h-3.5" />
</Button>
</StandardTooltip>
</div>
</div>
<VSCodeTextArea
resize="vertical"
value={formPrompt}
onInput={(e: any) => setFormPrompt(e.target.value)}
placeholder={t("personality:promptPlaceholder")}
rows={4}
className="w-full"
/>
</div>
{/* Enhancer Prompt Editor (collapsible) */}
{isEnhancerPromptOpen && (
<div className="mb-2 p-2 border border-vscode-input-border rounded bg-vscode-editor-background">
<div className="text-xs text-vscode-descriptionForeground mb-1">
{t("personality:enhancerPromptLabel")}
</div>
<VSCodeTextArea
resize="vertical"
value={personalityTraitEnhancerPrompt || DEFAULT_PERSONALITY_TRAIT_ENHANCER_PROMPT}
onInput={(e: any) => {
vscode.postMessage({
type: "updateSettings",
updatedSettings: { personalityTraitEnhancerPrompt: e.target.value },
})
}}
rows={6}
className="w-full text-xs"
/>
</div>
)}
<div className="flex gap-2">
<Button variant="primary" onClick={handleSave} disabled={!formLabel.trim() || !formPrompt.trim()}>
{isEditing ? (
<>{t("settings:common.save")}</>
) : (
<><Plus className="w-4 h-4 mr-1" />{t("personality:addTraitButton")}</>
)}
</Button>
{isEditing && (
<Button variant="secondary" onClick={resetForm}>
{t("settings:common.cancel")}
</Button>
)}
</div>
</div>
</CollapsibleContent>
</Collapsible>
</div>
)
}
export default PersonalityTraitsPanel

View file

@ -0,0 +1,19 @@
{
"title": "Personality Traits",
"description": "Toggle traits to shape how Roo communicates in this mode. Combine multiple traits for a unique personality.",
"previewPrompt": "Preview combined prompt",
"noActiveTraits": "No traits are active. Toggle a trait above to see the combined prompt.",
"createTrait": "Create a Trait",
"editTrait": "Edit trait",
"editTraitTitle": "Edit Trait",
"deleteTrait": "Delete trait",
"emojiLabel": "Emoji",
"titleLabel": "Title",
"promptLabel": "Description / Prompt",
"labelPlaceholder": "e.g., Flamboyant",
"promptPlaceholder": "Describe the personality trait, or type a few words and click Enhance...",
"enhanceTooltip": "Enhance: expand a few words into a full personality prompt",
"enhancerSettingsTooltip": "View/edit the enhancer meta-prompt",
"enhancerPromptLabel": "Enhancer Meta-Prompt (controls how brief descriptions are expanded)",
"addTraitButton": "Add Trait"
}