feat: add mode system with 6 AI personality presets#1255
Conversation
Add a /mode command that lets users switch between 6 interaction modes, each with distinct system prompts, UI themes, permission defaults, and response verbosity: - Default (⚡) — balanced, everyday development - Gentle (🌸) — patient explanations for learning - Dr. Sharp (🔍) — strict 3-phase code review workflow - Workhorse (🐴) — auto-execute, minimal confirmations - Token Saver (💰) — minimal replies to save tokens - Super AI (🧠) — deep analysis, proactive suggestions Custom modes can be defined via YAML files in ~/.claude/modes/. New files: - src/modes/types.ts — CCBMode interface - src/modes/defaults.ts — 6 built-in mode presets - src/modes/store.ts — mode state management with useSyncExternalStore - src/commands/mode/index.ts — command registration - src/commands/mode/mode.tsx — mode picker UI Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR introduces a complete chat modes feature, enabling users to switch between six predefined interaction modes (Default, Gentle, Dr. Sharp, Workhorse, Token Saver, Super AI) with distinct system prompts, UI theming, and response styles. It includes custom mode loading from YAML, persistent mode selection via user settings, state management with React integration, and an interactive mode-picker command. ChangesChat Mode Selection Feature
Sequence DiagramsequenceDiagram
participant User
participant ModePicker
participant Store
participant Settings
participant Disk
User->>ModePicker: Select mode or provide slug
ModePicker->>Store: setCurrentMode(slug)
Store->>Store: Validate slug against getAllModes()
Store->>Settings: updateSettingsForSource(ccbMode)
Settings->>Disk: Persist mode choice
Store->>User: Notify subscribers
User->>Store: useCurrentMode hook
Store->>User: Current mode + re-render on change
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 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 |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 `@src/commands/mode/mode.tsx`:
- Around line 59-64: The code lowercases the user input into slug then compares
it directly to stored custom mode slugs, so case-variants like "MyMode" won't
match; change the lookup in listModes() to compare case-insensitively (e.g.,
compare slug.toLowerCase() against m.slug.toLowerCase()) and when calling
setCurrentMode use the canonical stored slug (target.slug) rather than the
lowercased input; update the block around the slug variable, the modes.find(...)
call, and the setCurrentMode invocation accordingly.
In `@src/modes/store.ts`:
- Around line 104-109: The code sets currentModeSlug and then calls
updateSettingsForSource('userSettings', { ccbMode: slug }) without handling its
error, so if persistence fails you must not notify listeners or leave
currentModeSlug changed; instead call updateSettingsForSource and inspect its
return (or await and catch), and only if it succeeds assign currentModeSlug and
invoke modeListeners; on failure, restore previous currentModeSlug (or keep it
unchanged), do not call modeListeners, and rethrow or propagate the write error
so callers see the failure (referencing currentModeSlug,
updateSettingsForSource, modeListeners and userSettings.ccbMode).
- Around line 48-61: The YAML values for default_mode and
response_style.verbosity are being cast straight into CCBMode fields
(defaultMode and responseStyle.verbosity) which can allow invalid strings; add
runtime validation functions or checks that narrow allowed enum values before
constructing the CCBMode object and fall back to 'default' for defaultMode and
'normal' for verbosity when values are invalid or missing. Locate the
construction of CCBMode (references: CCBMode, defaultMode,
responseStyle.verbosity) and replace the direct casts with a safe parse/guard:
check the incoming (data.permissions?.default_mode) and
(data.response_style?.verbosity) against the accepted set of CCBMode
permissions/defaultMode and responseStyle/verbosity values, use the validated
value if it matches, otherwise use the specified fallbacks.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: ab92fb32-e7d8-49ad-98e1-3515a53b3508
📒 Files selected for processing (6)
src/commands.tssrc/commands/mode/index.tssrc/commands/mode/mode.tsxsrc/modes/defaults.tssrc/modes/store.tssrc/modes/types.ts
| const slug = args?.trim().toLowerCase(); | ||
|
|
||
| if (slug) { | ||
| const modes = listModes(); | ||
| const target = modes.find(m => m.slug === slug); | ||
| if (!target) { |
There was a problem hiding this comment.
Fix case-normalization mismatch for direct custom mode selection.
Line 59 lowercases user input, but custom slugs are stored as-is. A custom slug like MyMode won’t match via /mode MyMode after normalization. Match case-insensitively and pass canonical slug to setCurrentMode.
Suggested fix
export const call: LocalJSXCommandCall = async (onDone, _context, args) => {
- const slug = args?.trim().toLowerCase();
+ const rawSlug = args?.trim();
+ const normalizedSlug = rawSlug?.toLowerCase();
- if (slug) {
+ if (normalizedSlug) {
const modes = listModes();
- const target = modes.find(m => m.slug === slug);
+ const target = modes.find(m => m.slug.toLowerCase() === normalizedSlug);
if (!target) {
const available = modes.map(m => `${m.icon} ${m.slug} — ${m.description}`).join('\n');
- onDone(`Unknown mode: "${slug}"\n\nAvailable modes:\n${available}`, {
+ onDone(`Unknown mode: "${rawSlug}"\n\nAvailable modes:\n${available}`, {
display: 'system',
});
return;
}
- setCurrentMode(slug);
+ setCurrentMode(target.slug);🤖 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 `@src/commands/mode/mode.tsx` around lines 59 - 64, The code lowercases the
user input into slug then compares it directly to stored custom mode slugs, so
case-variants like "MyMode" won't match; change the lookup in listModes() to
compare case-insensitively (e.g., compare slug.toLowerCase() against
m.slug.toLowerCase()) and when calling setCurrentMode use the canonical stored
slug (target.slug) rather than the lowercased input; update the block around the
slug variable, the modes.find(...) call, and the setCurrentMode invocation
accordingly.
| defaultMode: | ||
| ((data.permissions as Record<string, unknown>) | ||
| ?.default_mode as CCBMode['permissions']['defaultMode']) || | ||
| 'default', | ||
| memoryExtract: Boolean( | ||
| (data.permissions as Record<string, unknown>)?.memory_extract ?? | ||
| true, | ||
| ), | ||
| }, | ||
| responseStyle: { | ||
| verbosity: | ||
| ((data.response_style as Record<string, unknown>) | ||
| ?.verbosity as CCBMode['responseStyle']['verbosity']) || | ||
| 'normal', |
There was a problem hiding this comment.
Validate YAML enum fields before constructing CCBMode.
Line 49 and Line 59 cast unknown YAML values directly into constrained fields. Invalid strings can slip through and break downstream mode handling. Add runtime narrowing for defaultMode and verbosity with safe fallbacks.
Suggested fix
+function toVerbosity(value: unknown): CCBMode['responseStyle']['verbosity'] {
+ return value === 'minimal' || value === 'normal' || value === 'verbose'
+ ? value
+ : 'normal'
+}
+
+function toPermissionMode(
+ value: unknown,
+ fallback: CCBMode['permissions']['defaultMode'] = 'default',
+): CCBMode['permissions']['defaultMode'] {
+ const allowed = new Set(DEFAULT_MODES.map(m => m.permissions.defaultMode))
+ return typeof value === 'string' && allowed.has(value as CCBMode['permissions']['defaultMode'])
+ ? (value as CCBMode['permissions']['defaultMode'])
+ : fallback
+}
+
customModes.push({
@@
permissions: {
- defaultMode:
- ((data.permissions as Record<string, unknown>)
- ?.default_mode as CCBMode['permissions']['defaultMode']) ||
- 'default',
+ defaultMode: toPermissionMode(
+ (data.permissions as Record<string, unknown>)?.default_mode,
+ ),
memoryExtract: Boolean(
(data.permissions as Record<string, unknown>)?.memory_extract ??
true,
),
},
responseStyle: {
- verbosity:
- ((data.response_style as Record<string, unknown>)
- ?.verbosity as CCBMode['responseStyle']['verbosity']) ||
- 'normal',
+ verbosity: toVerbosity(
+ (data.response_style as Record<string, unknown>)?.verbosity,
+ ),
},
})🤖 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 `@src/modes/store.ts` around lines 48 - 61, The YAML values for default_mode
and response_style.verbosity are being cast straight into CCBMode fields
(defaultMode and responseStyle.verbosity) which can allow invalid strings; add
runtime validation functions or checks that narrow allowed enum values before
constructing the CCBMode object and fall back to 'default' for defaultMode and
'normal' for verbosity when values are invalid or missing. Locate the
construction of CCBMode (references: CCBMode, defaultMode,
responseStyle.verbosity) and replace the direct casts with a safe parse/guard:
check the incoming (data.permissions?.default_mode) and
(data.response_style?.verbosity) against the accepted set of CCBMode
permissions/defaultMode and responseStyle/verbosity values, use the validated
value if it matches, otherwise use the specified fallbacks.
| currentModeSlug = slug | ||
| updateSettingsForSource('userSettings', { ccbMode: slug } as Record< | ||
| string, | ||
| unknown | ||
| >) | ||
| for (const listener of modeListeners) listener() |
There was a problem hiding this comment.
Handle settings write failures before updating observers.
Line 105 ignores updateSettingsForSource(...).error. If persistence fails, mode appears switched in memory and listeners fire, but userSettings.ccbMode remains stale. Roll back and throw on write error.
Suggested fix
export function setCurrentMode(slug: string): void {
@@
+ const previousSlug = currentModeSlug
currentModeSlug = slug
- updateSettingsForSource('userSettings', { ccbMode: slug } as Record<
- string,
- unknown
- >)
+ const { error } = updateSettingsForSource('userSettings', {
+ ccbMode: slug,
+ } as Record<string, unknown>)
+ if (error) {
+ currentModeSlug = previousSlug
+ throw error
+ }
for (const listener of modeListeners) listener()
}🤖 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 `@src/modes/store.ts` around lines 104 - 109, The code sets currentModeSlug and
then calls updateSettingsForSource('userSettings', { ccbMode: slug }) without
handling its error, so if persistence fails you must not notify listeners or
leave currentModeSlug changed; instead call updateSettingsForSource and inspect
its return (or await and catch), and only if it succeeds assign currentModeSlug
and invoke modeListeners; on failure, restore previous currentModeSlug (or keep
it unchanged), do not call modeListeners, and rethrow or propagate the write
error so callers see the failure (referencing currentModeSlug,
updateSettingsForSource, modeListeners and userSettings.ccbMode).
Summary
/modecommand that lets users switch between 6 interaction modes~/.claude/modes/Modes
New files
src/modes/types.ts—CCBModeinterfacesrc/modes/defaults.ts— 6 built-in mode presets with inline Dr. Sharp promptsrc/modes/store.ts— Mode state management usinguseSyncExternalStore, loads custom YAML modes from~/.claude/modes/, persists selection tosettings.jsonsrc/commands/mode/index.ts— Command registrationsrc/commands/mode/mode.tsx— Mode picker UI with Select componentModified files
src/commands.ts— RegistermodecommandUsage
Custom modes
Users can create
~/.claude/modes/my-mode.yaml:Test plan
/modeand verify interactive picker shows all 6 modes/mode sharpand verify direct switch works/mode invalidand verify error message with available modesbun run precheck— all checks pass🤖 Generated with Claude Code
Summary by CodeRabbit