mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
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:
@@ -19,6 +19,7 @@ import { context, contextNonInteractive } from './commands/context/index.js'
|
|||||||
import diff from './commands/diff/index.js'
|
import diff from './commands/diff/index.js'
|
||||||
import doctor from './commands/doctor/index.js'
|
import doctor from './commands/doctor/index.js'
|
||||||
import memory from './commands/memory/index.js'
|
import memory from './commands/memory/index.js'
|
||||||
|
import mode from './commands/mode/index.js'
|
||||||
import help from './commands/help/index.js'
|
import help from './commands/help/index.js'
|
||||||
import ide from './commands/ide/index.js'
|
import ide from './commands/ide/index.js'
|
||||||
import init from './commands/init.js'
|
import init from './commands/init.js'
|
||||||
@@ -327,6 +328,7 @@ const COMMANDS = memoize((): Command[] => [
|
|||||||
mcp,
|
mcp,
|
||||||
memory,
|
memory,
|
||||||
mobile,
|
mobile,
|
||||||
|
mode,
|
||||||
model,
|
model,
|
||||||
outputStyle,
|
outputStyle,
|
||||||
remoteEnv,
|
remoteEnv,
|
||||||
|
|||||||
13
src/commands/mode/index.ts
Normal file
13
src/commands/mode/index.ts
Normal 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
|
||||||
79
src/commands/mode/mode.tsx
Normal file
79
src/commands/mode/mode.tsx
Normal 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
181
src/modes/defaults.ts
Normal 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
133
src/modes/store.ts
Normal 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
21
src/modes/types.ts
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user