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

* docs: update contributors

* docs: update contributors

* feat: add mode system with 6 AI personality presets

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>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
YYMa
2026-06-05 21:01:02 +08:00
committed by GitHub
parent 6b205f5798
commit 9947ae75da
6 changed files with 429 additions and 0 deletions

View File

@@ -19,6 +19,7 @@ import { context, contextNonInteractive } from './commands/context/index.js'
import diff from './commands/diff/index.js'
import doctor from './commands/doctor/index.js'
import memory from './commands/memory/index.js'
import mode from './commands/mode/index.js'
import help from './commands/help/index.js'
import ide from './commands/ide/index.js'
import init from './commands/init.js'
@@ -327,6 +328,7 @@ const COMMANDS = memoize((): Command[] => [
mcp,
memory,
mobile,
mode,
model,
outputStyle,
remoteEnv,

View File

@@ -0,0 +1,13 @@
import type { Command } from '../../commands.js'
const mode = {
type: 'local-jsx',
name: 'mode',
description:
'Switch interaction mode (default, gentle, sharp, workhorse, token-saver, super-ai)',
isEnabled: () => true,
argumentHint: '<mode-slug>',
load: () => import('./mode.js'),
} satisfies Command
export default mode

View File

@@ -0,0 +1,79 @@
import { useMemo } from 'react';
import { Box, Text } from '@anthropic/ink';
import { Select } from '../../components/CustomSelect/select.js';
import type { LocalJSXCommandCall, LocalJSXCommandOnDone } from '../../types/command.js';
import { getCurrentModeSlug, listModes, setCurrentMode } from '../../modes/store.js';
function ModePicker({ onDone }: { onDone: LocalJSXCommandOnDone }) {
const modes = listModes();
const currentSlug = getCurrentModeSlug();
const options = useMemo(
() =>
modes.map(m => ({
label: (
<Text>
{m.icon} {m.name}{' '}
<Text dimColor>
({m.slug}) {m.description}
</Text>
</Text>
),
value: m.slug,
})),
[modes],
);
function handleSelect(slug: string) {
setCurrentMode(slug);
const target = modes.find(m => m.slug === slug);
onDone(`${target?.icon} Mode switched to: ${target?.name} (${target?.slug}) — ${target?.description}`, {
display: 'system',
});
}
function handleCancel() {
onDone('Mode selection cancelled.', { display: 'system' });
}
return (
<Box flexDirection="column">
<Box marginBottom={1} flexDirection="column">
<Text color="remember" bold>
Select mode
</Text>
<Text dimColor>Arrow keys to navigate, Enter to select, Esc to cancel.</Text>
</Box>
<Select
defaultValue={currentSlug}
options={options}
onChange={handleSelect}
onCancel={handleCancel}
visibleOptionCount={modes.length}
/>
</Box>
);
}
export const call: LocalJSXCommandCall = async (onDone, _context, args) => {
const slug = args?.trim().toLowerCase();
if (slug) {
const modes = listModes();
const target = modes.find(m => m.slug === slug);
if (!target) {
const available = modes.map(m => `${m.icon} ${m.slug}${m.description}`).join('\n');
onDone(`Unknown mode: "${slug}"\n\nAvailable modes:\n${available}`, {
display: 'system',
});
return;
}
setCurrentMode(slug);
onDone(`${target.icon} Mode switched to: ${target.name} (${target.slug}) — ${target.description}`, {
display: 'system',
});
return;
}
return <ModePicker onDone={onDone} />;
};

181
src/modes/defaults.ts Normal file
View File

@@ -0,0 +1,181 @@
import type { CCBMode } from './types.js'
const DR_SHARP_SYSTEM_PROMPT = `You are Dr. Sharp, a meticulous code reviewer and diagnostician.
## Core Principles
1. **Diagnose before acting.** Never jump to a fix. Understand the root cause first.
2. **Minimal effective change.** The smallest diff that fully solves the problem wins.
3. **Evidence-based.** Every claim must be backed by code, logs, or behavior you can point to.
4. **No assumptions.** If you're unsure, ask. Never guess about behavior you haven't verified.
## Three-Phase Workflow
### Phase 1: Deep Diagnosis
- Read the relevant code paths end-to-end
- Trace the execution flow from input to output
- Identify the exact point where behavior diverges from expectation
- State your diagnosis clearly before proceeding
### Phase 2: Action Strategy
- List 2-3 possible approaches with trade-offs
- Recommend the minimal effective approach
- Consider: side effects, edge cases, regression risks
- Explain WHY this approach over alternatives
### Phase 3: Mirror Self
- After implementing, re-read the original problem statement
- Verify your fix addresses the root cause, not just the symptom
- Check for related issues the same root cause might trigger
- Run relevant tests to confirm
## Communication Style
- Be direct and specific. No filler.
- Use code references (file:line) when pointing to issues.
- When reviewing: "This will break when X because Y. Fix: Z."
- When diagnosing: "The bug is at X:42. The condition Y evaluates to Z because..."
- Never apologize for finding problems — that's the job.
## Red Flags to Always Check
- Error handling: are errors caught, logged, and propagated correctly?
- Edge cases: null, empty, boundary values, concurrent access
- Security: injection, auth bypass, data leaks
- Performance: N+1 queries, unnecessary allocations, missing indexes
- Type safety: any \`as any\` casts, missing null checks, loose types`
export const DEFAULT_MODES: CCBMode[] = [
{
name: 'Default',
slug: 'default',
description: 'Balanced mode for everyday development',
icon: '⚡',
systemPrompt: '',
ui: {
accentColor: '#D77757',
promptPrefix: '',
},
companionSpecies: 'duck',
permissions: {
defaultMode: 'default',
memoryExtract: true,
},
responseStyle: {
verbosity: 'normal',
},
},
{
name: 'Gentle',
slug: 'gentle',
description: 'Patient explanations, great for learning',
icon: '🌸',
companionSpecies: 'cat',
systemPrompt:
'You are in gentle learning mode. Explain concepts clearly with examples. ' +
'When correcting mistakes, be encouraging and explain why. ' +
'Offer to show alternatives before making changes. ' +
'Use analogies to help understand complex concepts.',
ui: {
accentColor: '#E8A0BF',
promptPrefix: 'gentle',
},
permissions: {
defaultMode: 'default',
memoryExtract: true,
},
responseStyle: {
verbosity: 'verbose',
},
},
{
name: 'Dr. Sharp',
slug: 'sharp',
description: 'Strict review, focused on code quality',
icon: '🔍',
companionSpecies: 'owl',
systemPrompt: DR_SHARP_SYSTEM_PROMPT,
ui: {
accentColor: '#5769F7',
promptPrefix: 'sharp',
},
permissions: {
defaultMode: 'default',
memoryExtract: true,
},
responseStyle: {
verbosity: 'normal',
},
},
{
name: 'Workhorse',
slug: 'workhorse',
description: 'Auto-execute, minimal confirmations',
icon: '🐴',
companionSpecies: 'capybara',
systemPrompt:
'You are in workhorse mode. Execute tasks efficiently with minimal back-and-forth. ' +
'Make reasonable assumptions and proceed. ' +
'Only ask for clarification when truly ambiguous. ' +
'Batch related changes together.',
ui: {
accentColor: '#8B7355',
promptPrefix: 'work',
},
permissions: {
defaultMode: 'acceptEdits',
memoryExtract: false,
},
responseStyle: {
verbosity: 'minimal',
},
},
{
name: 'Token Saver',
slug: 'token-saver',
description: 'Minimal replies, save tokens',
icon: '💰',
companionSpecies: 'snail',
systemPrompt:
'You are in token-saving mode. ' +
'Give the shortest correct answer. ' +
'Skip explanations unless asked. ' +
'Use code blocks directly without preamble. ' +
'No pleasantries or filler.',
ui: {
accentColor: '#4A7C59',
promptPrefix: 'save',
},
permissions: {
defaultMode: 'acceptEdits',
memoryExtract: false,
},
responseStyle: {
verbosity: 'minimal',
},
},
{
name: 'Super AI',
slug: 'super-ai',
description: 'Deep thinking, comprehensive analysis',
icon: '🧠',
companionSpecies: 'dragon',
systemPrompt:
'You are in super AI mode. Think deeply before responding. ' +
'Consider multiple approaches and explain trade-offs. ' +
'Proactively identify related issues and suggest improvements. ' +
'Use structured analysis for complex problems. ' +
'Reference relevant best practices and patterns.',
ui: {
accentColor: '#9B59B6',
promptPrefix: 'super',
},
permissions: {
defaultMode: 'default',
memoryExtract: true,
},
responseStyle: {
verbosity: 'verbose',
},
},
]

133
src/modes/store.ts Normal file
View File

@@ -0,0 +1,133 @@
import { existsSync, mkdirSync, readdirSync, readFileSync } from 'fs'
import { join } from 'path'
import { useSyncExternalStore } from 'react'
import { parse as parseYaml } from 'yaml'
import {
getInitialSettings,
updateSettingsForSource,
} from '../utils/settings/settings.js'
import { getClaudeConfigHomeDir } from '../utils/envUtils.js'
import { DEFAULT_MODES } from './defaults.js'
import type { CCBMode } from './types.js'
let currentModeSlug: string | null = null
let customModes: CCBMode[] | null = null
const modeListeners = new Set<() => void>()
function loadCustomModes(): CCBMode[] {
if (customModes !== null) return customModes
customModes = []
try {
const modesDir = join(getClaudeConfigHomeDir(), 'modes')
if (!existsSync(modesDir)) {
mkdirSync(modesDir, { recursive: true })
}
const files = readdirSync(modesDir).filter(
f => f.endsWith('.yaml') || f.endsWith('.yml'),
)
for (const file of files) {
try {
const raw = readFileSync(join(modesDir, file), 'utf-8')
const data = parseYaml(raw) as Record<string, unknown>
if (!data.slug || !data.name) continue
customModes.push({
name: String(data.name),
slug: String(data.slug),
description: String(data.description || ''),
icon: String(data.icon || '🔧'),
systemPrompt: String(data.system_prompt || ''),
ui: {
accentColor: String(
(data.ui as Record<string, unknown>)?.accent_color || '#00D4AA',
),
promptPrefix: String(
(data.ui as Record<string, unknown>)?.prompt_prefix || '',
),
},
permissions: {
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',
},
})
} catch {
// skip invalid yaml files
}
}
} catch {
// modes directory may not exist
}
return customModes
}
function getAllModes(): CCBMode[] {
const custom = loadCustomModes()
if (custom.length === 0) return DEFAULT_MODES
// Custom modes override defaults with same slug
const slugs = new Set(custom.map(m => m.slug))
return [...custom, ...DEFAULT_MODES.filter(m => !slugs.has(m.slug))]
}
export function getCurrentModeSlug(): string {
if (currentModeSlug === null) {
const settings = getInitialSettings() as Record<string, unknown>
currentModeSlug = (settings.ccbMode as string) || 'default'
}
return currentModeSlug
}
export function getCurrentMode(): CCBMode {
const slug = getCurrentModeSlug()
const modes = getAllModes()
return modes.find(m => m.slug === slug) ?? DEFAULT_MODES[0]
}
export function setCurrentMode(slug: string): void {
const modes = getAllModes()
const mode = modes.find(m => m.slug === slug)
if (!mode) {
throw new Error(
`Unknown mode: ${slug}. Available: ${modes.map(m => m.slug).join(', ')}`,
)
}
currentModeSlug = slug
updateSettingsForSource('userSettings', { ccbMode: slug } as Record<
string,
unknown
>)
for (const listener of modeListeners) listener()
}
function subscribeMode(listener: () => void): () => void {
modeListeners.add(listener)
return () => modeListeners.delete(listener)
}
/** Reactive hook — re-renders the component when the mode changes. */
export function useCurrentMode(): CCBMode {
return useSyncExternalStore(subscribeMode, getCurrentMode)
}
export function listModes(): CCBMode[] {
return getAllModes()
}
export function cycleMode(): CCBMode {
const modes = listModes()
const current = getCurrentModeSlug()
const idx = modes.findIndex(m => m.slug === current)
const next = modes[(idx + 1) % modes.length]
setCurrentMode(next.slug)
return next
}

21
src/modes/types.ts Normal file
View File

@@ -0,0 +1,21 @@
import type { PermissionMode } from '../types/permissions.js'
export interface CCBMode {
name: string
slug: string
description: string
icon: string
systemPrompt: string
ui: {
accentColor: string
promptPrefix: string
}
companionSpecies?: string
permissions: {
defaultMode: PermissionMode
memoryExtract: boolean
}
responseStyle: {
verbosity: 'minimal' | 'normal' | 'verbose'
}
}