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 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,
|
||||
|
||||
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