Skip to content

feat: add mode system with 6 AI personality presets#1255

Merged
claude-code-best merged 4 commits into
claude-code-best:mainfrom
YuanyuanMa03:feat/mode-system
Jun 5, 2026
Merged

feat: add mode system with 6 AI personality presets#1255
claude-code-best merged 4 commits into
claude-code-best:mainfrom
YuanyuanMa03:feat/mode-system

Conversation

@YuanyuanMa03
Copy link
Copy Markdown
Contributor

@YuanyuanMa03 YuanyuanMa03 commented Jun 4, 2026

Summary

  • Add a /mode command that lets users switch between 6 interaction modes
  • Each mode has a distinct system prompt, UI accent color, permission defaults, and response verbosity
  • Custom modes can be defined via YAML files in ~/.claude/modes/

Modes

Mode Icon System Prompt Permissions Verbosity
Default (none) default normal
Gentle 🌸 Patient, encouraging explanations default verbose
Dr. Sharp 🔍 3-phase code review (Diagnose → Strategy → Mirror) default normal
Workhorse 🐴 Auto-execute, minimal back-and-forth acceptEdits minimal
Token Saver 💰 Shortest correct answer, no filler acceptEdits minimal
Super AI 🧠 Deep analysis, proactive suggestions default verbose

New files

  • src/modes/types.tsCCBMode interface
  • src/modes/defaults.ts — 6 built-in mode presets with inline Dr. Sharp prompt
  • src/modes/store.ts — Mode state management using useSyncExternalStore, loads custom YAML modes from ~/.claude/modes/, persists selection to settings.json
  • src/commands/mode/index.ts — Command registration
  • src/commands/mode/mode.tsx — Mode picker UI with Select component

Modified files

  • src/commands.ts — Register mode command

Usage

/mode              → Interactive mode picker
/mode sharp        → Switch directly to Dr. Sharp
/mode workhorse    → Switch to Workhorse mode

Custom modes

Users can create ~/.claude/modes/my-mode.yaml:

slug: my-mode
name: My Custom Mode
description: Tailored for my workflow
icon: "🎯"
system_prompt: |
  You are configured for my specific workflow...
ui:
  accent_color: "#FF6B6B"
  prompt_prefix: custom
permissions:
  default_mode: default
  memory_extract: true
response_style:
  verbosity: normal

Test plan

  • Run /mode and verify interactive picker shows all 6 modes
  • Run /mode sharp and verify direct switch works
  • Run /mode invalid and verify error message with available modes
  • Create a custom YAML mode and verify it appears in the picker
  • Run bun run precheck — all checks pass

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a mode command allowing users to switch between different chat interaction modes.
    • Introduced six pre-configured modes: Default, Gentle, Dr. Sharp, Workhorse, Token Saver, and Super AI, each with distinct system prompts, UI customization, and response styles.
    • Added a mode selector UI for easy mode switching.
    • Implemented support for custom user-defined modes.

YuanyuanMa03 and others added 4 commits May 25, 2026 04:07
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>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

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

Changes

Chat Mode Selection Feature

Layer / File(s) Summary
Mode type contract
src/modes/types.ts
CCBMode interface defines mode identity, UI styling, system prompt, optional companion species, permission defaults, and response verbosity constraints.
Default modes data
src/modes/defaults.ts
Six predefined modes (Default, Gentle, Dr. Sharp, Workhorse, Token Saver, Super AI) each with distinct system prompts, accent colors, prompt prefixes, permission settings, and response verbosity.
Mode management and state
src/modes/store.ts
Loads custom modes from YAML files on disk, merges with defaults (custom modes override by slug), manages current mode slug in user settings with persistence, provides sync and async getters/setters, React hook via useSyncExternalStore, and utilities to list and cycle through modes.
Mode selection command
src/commands/mode/index.ts, src/commands/mode/mode.tsx
ModePicker component renders interactive selectable list of modes; call handler validates slug arguments or renders the picker UI, persists selection, and reports completion.
Command system integration
src/commands.ts
Imports and registers the mode command in the memoized COMMANDS array for discovery and filtering.

Sequence Diagram

sequenceDiagram
  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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • KonghaYao

Poem

🐰 Six modes in the warren, each with its own beat,
From Gentle whispers to Sharp advise so neat!
Custom YAML modes hop into the store,
Select your companion, explore mode lore!
The picker stands ready—your chat, reimagined.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% 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 describes the main feature being added: a mode system with 6 AI personality presets, which aligns with the primary changes introducing modes functionality.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between d8892f1 and b88e83e.

📒 Files selected for processing (6)
  • src/commands.ts
  • src/commands/mode/index.ts
  • src/commands/mode/mode.tsx
  • src/modes/defaults.ts
  • src/modes/store.ts
  • src/modes/types.ts

Comment on lines +59 to +64
const slug = args?.trim().toLowerCase();

if (slug) {
const modes = listModes();
const target = modes.find(m => m.slug === slug);
if (!target) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment thread src/modes/store.ts
Comment on lines +48 to +61
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',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment thread src/modes/store.ts
Comment on lines +104 to +109
currentModeSlug = slug
updateSettingsForSource('userSettings', { ccbMode: slug } as Record<
string,
unknown
>)
for (const listener of modeListeners) listener()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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

@claude-code-best claude-code-best merged commit 9947ae7 into claude-code-best:main Jun 5, 2026
2 checks passed
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.

2 participants