diff --git a/src/brands/deepseek.ts b/src/brands/deepseek.ts new file mode 100644 index 0000000..a914cc7 --- /dev/null +++ b/src/brands/deepseek.ts @@ -0,0 +1,200 @@ +import type { TweakccConfig, Theme } from './types.js'; +import { DEFAULT_THEMES } from './defaultThemes.js'; +import { buildBrandMiscConfig } from './miscDefaults.js'; +import { buildDiffPalette } from './diffPalette.js'; +import { formatUserMessage, getUserLabel } from './userLabel.js'; + +type Rgb = { r: number; g: number; b: number }; + +const clamp = (value: number) => Math.max(0, Math.min(255, Math.round(value))); + +const hexToRgb = (hex: string): Rgb => { + const normalized = hex.replace('#', '').trim(); + if (normalized.length === 3) { + const [r, g, b] = normalized.split(''); + return { + r: clamp(parseInt(r + r, 16)), + g: clamp(parseInt(g + g, 16)), + b: clamp(parseInt(b + b, 16)), + }; + } + if (normalized.length !== 6) { + throw new Error(`Unsupported hex color: ${hex}`); + } + return { + r: clamp(parseInt(normalized.slice(0, 2), 16)), + g: clamp(parseInt(normalized.slice(2, 4), 16)), + b: clamp(parseInt(normalized.slice(4, 6), 16)), + }; +}; + +const rgb = (hex: string) => { + const { r, g, b } = hexToRgb(hex); + return `rgb(${r},${g},${b})`; +}; + +const mix = (hexA: string, hexB: string, weight: number) => { + const a = hexToRgb(hexA); + const b = hexToRgb(hexB); + const w = Math.max(0, Math.min(1, weight)); + return `rgb(${clamp(a.r + (b.r - a.r) * w)},${clamp(a.g + (b.g - a.g) * w)},${clamp(a.b + (b.b - a.b) * w)})`; +}; + +const lighten = (hex: string, weight: number) => mix(hex, '#ffffff', weight); + +type Palette = { + base: string; + surface: string; + panel: string; + border: string; + borderStrong: string; + text: string; + textMuted: string; + textDim: string; + core: string; + deep: string; + orange: string; + yellow: string; + cyan: string; + blue: string; + green: string; + red: string; +}; + +const DEEPSEEK_CORE = '#4d9de0'; + +const palette: Palette = { + base: '#080c14', + surface: '#0e1420', + panel: '#141c2c', + border: '#1e2e48', + borderStrong: '#2a4060', + text: '#e8eef8', + textMuted: '#b0c0d8', + textDim: '#6888a8', + core: DEEPSEEK_CORE, + deep: '#2563eb', + orange: '#f59e0b', + yellow: '#fbbf24', + cyan: '#06b6d4', + blue: '#3b82f6', + green: '#10b981', + red: '#ef4444', +}; + +const makeTheme = (): Theme => { + const tint = (hex: string, weight: number) => mix(palette.base, hex, weight); + return { + name: 'DeepSeek Abyss', + id: 'dark', + colors: { + autoAccept: rgb(palette.green), + bashBorder: rgb(palette.core), + claude: rgb(palette.core), + claudeShimmer: lighten(palette.core, 0.28), + claudeBlue_FOR_SYSTEM_SPINNER: rgb(palette.blue), + claudeBlueShimmer_FOR_SYSTEM_SPINNER: lighten(palette.blue, 0.3), + permission: rgb(palette.orange), + permissionShimmer: lighten(palette.orange, 0.25), + planMode: rgb(palette.core), + ide: rgb(palette.cyan), + promptBorder: rgb(palette.border), + promptBorderShimmer: rgb(palette.borderStrong), + text: rgb(palette.text), + inverseText: rgb(palette.base), + inactive: rgb(palette.textDim), + subtle: tint(palette.core, 0.18), + suggestion: rgb(palette.deep), + remember: rgb(palette.core), + background: rgb(palette.base), + success: rgb(palette.green), + error: rgb(palette.red), + warning: rgb(palette.yellow), + warningShimmer: lighten(palette.yellow, 0.2), + ...buildDiffPalette(), + red_FOR_SUBAGENTS_ONLY: rgb(palette.red), + blue_FOR_SUBAGENTS_ONLY: rgb(palette.blue), + green_FOR_SUBAGENTS_ONLY: rgb(palette.green), + yellow_FOR_SUBAGENTS_ONLY: rgb(palette.yellow), + purple_FOR_SUBAGENTS_ONLY: rgb(palette.deep), + orange_FOR_SUBAGENTS_ONLY: rgb(palette.orange), + pink_FOR_SUBAGENTS_ONLY: rgb(palette.core), + cyan_FOR_SUBAGENTS_ONLY: rgb(palette.cyan), + professionalBlue: rgb(palette.blue), + rainbow_red: rgb(palette.red), + rainbow_orange: rgb(palette.orange), + rainbow_yellow: rgb(palette.yellow), + rainbow_green: rgb(palette.green), + rainbow_blue: rgb(palette.cyan), + rainbow_indigo: rgb(palette.blue), + rainbow_violet: rgb(palette.core), + rainbow_red_shimmer: lighten(palette.red, 0.35), + rainbow_orange_shimmer: lighten(palette.orange, 0.35), + rainbow_yellow_shimmer: lighten(palette.yellow, 0.25), + rainbow_green_shimmer: lighten(palette.green, 0.35), + rainbow_blue_shimmer: lighten(palette.cyan, 0.35), + rainbow_indigo_shimmer: lighten(palette.blue, 0.35), + rainbow_violet_shimmer: lighten(palette.core, 0.35), + clawd_body: rgb(palette.core), + clawd_background: rgb(palette.base), + userMessageBackground: mix('#383838', palette.core, 0.12), + bashMessageBackgroundColor: mix('#404040', palette.core, 0.08), + memoryBackgroundColor: mix('#383838', palette.cyan, 0.1), + rate_limit_fill: rgb(palette.core), + rate_limit_empty: rgb(palette.borderStrong), + }, + }; +}; + +const abyssTheme = makeTheme(); + +export const buildDeepSeekTweakccConfig = (): TweakccConfig => ({ + ccVersion: '', + ccInstallationPath: null, + lastModified: new Date().toISOString(), + changesApplied: false, + hidePiebaldAnnouncement: true, + settings: { + themes: [abyssTheme, ...DEFAULT_THEMES.filter((t) => t.id !== abyssTheme.id)], + thinkingVerbs: { + format: '{}... ', + verbs: [ + 'Diving', + 'Surfacing', + 'Probing', + 'Seeking', + 'Fathoming', + 'Sounding', + 'Dredging', + 'Plumbing', + 'Trawling', + 'Charting', + 'Scanning', + 'Tracing', + 'Mapping', + 'Resolving', + ], + }, + thinkingStyle: { + updateInterval: 80, + phases: ['~', '≈', '~', '≋', '~', '≈'], + reverseMirror: false, + }, + userMessageDisplay: { + format: formatUserMessage(getUserLabel()), + styling: ['bold'], + foregroundColor: 'default', + backgroundColor: 'default', + borderStyle: 'topBottomDouble', + borderColor: rgb(palette.core), + paddingX: 1, + paddingY: 0, + fitBoxToContent: true, + }, + inputBox: { + removeBorder: true, + }, + misc: buildBrandMiscConfig(), + claudeMdAltNames: null, + }, +}); diff --git a/src/brands/index.ts b/src/brands/index.ts index 13e304b..a178b32 100644 --- a/src/brands/index.ts +++ b/src/brands/index.ts @@ -9,6 +9,7 @@ import { buildOllamaTweakccConfig } from './ollama.js'; import { buildGatewayzTweakccConfig } from './gatewayz.js'; import { buildVercelTweakccConfig } from './vercel.js'; import { buildNanoGPTTweakccConfig } from './nanogpt.js'; +import { buildDeepSeekTweakccConfig } from './deepseek.js'; export interface BrandPreset { key: string; @@ -30,6 +31,12 @@ const BRAND_PRESETS: Record = { description: 'Electric violet + neon cyberpunk palette', buildTweakccConfig: buildMinimaxTweakccConfig, }, + deepseek: { + key: 'deepseek', + label: 'DeepSeek Abyss', + description: 'Deep ocean blue palette with teal accents', + buildTweakccConfig: buildDeepSeekTweakccConfig, + }, kimi: { key: 'kimi', label: 'Kimi Teal', diff --git a/src/providers/index.ts b/src/providers/index.ts index e34026e..65f737a 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -43,6 +43,7 @@ export const PROVIDER_DISPLAY_ORDER = [ 'kimi', 'minimax', 'zai', + 'deepseek', 'openrouter', 'vercel', 'ollama', @@ -124,6 +125,25 @@ const PROVIDERS: Record = { }, apiKeyLabel: 'Kimi API key', }, + deepseek: { + key: 'deepseek', + label: 'DeepSeek', + description: 'DeepSeek-V3.2 via DeepSeek Anthropic API', + baseUrl: 'https://api.deepseek.com/anthropic', + env: { + API_TIMEOUT_MS: '600000', + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1, + ANTHROPIC_MODEL: 'deepseek-chat', + ANTHROPIC_SMALL_FAST_MODEL: 'deepseek-chat', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'deepseek-chat', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'deepseek-reasoner', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'deepseek-chat', + CC_MIRROR_SPLASH: 1, + CC_MIRROR_PROVIDER_LABEL: 'DeepSeek', + CC_MIRROR_SPLASH_STYLE: 'deepseek', + }, + apiKeyLabel: 'DeepSeek API key', + }, openrouter: { key: 'openrouter', label: 'OpenRouter', diff --git a/src/tui/content/providers.ts b/src/tui/content/providers.ts index 2332bd1..eaf8775 100644 --- a/src/tui/content/providers.ts +++ b/src/tui/content/providers.ts @@ -104,6 +104,32 @@ export const PROVIDER_EDUCATION: Record = { 'Subscribe to Kimi Code, create an API key in the console, and set ANTHROPIC_BASE_URL to https://api.kimi.com/coding/.', }, + deepseek: { + headline: 'DeepSeek — Deep Seek, Deep Think', + tagline: 'Ocean depths, sharp reasoning', + features: [ + 'Official Anthropic-compatible API endpoint', + 'deepseek-reasoner for Opus (reasoning mode)', + 'deepseek-chat for Sonnet/Haiku (fast mode)', + 'DeepSeek-V3.2 under the hood', + 'Ocean blue-themed interface', + ], + bestFor: 'Cost-effective reasoning and coding with DeepSeek V3.2', + models: { + opus: 'deepseek-reasoner', + sonnet: 'deepseek-chat', + haiku: 'deepseek-chat', + }, + requiresMapping: false, + hasPromptPack: false, + setupLinks: { + subscribe: 'https://platform.deepseek.com', + apiKey: 'https://platform.deepseek.com/api_keys', + docs: 'https://api-docs.deepseek.com/guides/anthropic_api', + }, + setupNote: 'Create an account at platform.deepseek.com, top up credits, then generate an API key.', + }, + openrouter: { headline: 'OpenRouter — One API, Any Model', tagline: 'Many paths, one door', @@ -257,6 +283,7 @@ export const PROVIDER_COMPARISON = { 'kimi', 'minimax', 'zai', + 'deepseek', 'openrouter', 'vercel', 'ollama', diff --git a/src/tui/screens/ModelConfigScreen.tsx b/src/tui/screens/ModelConfigScreen.tsx index aadded5..22f4c3b 100644 --- a/src/tui/screens/ModelConfigScreen.tsx +++ b/src/tui/screens/ModelConfigScreen.tsx @@ -67,6 +67,11 @@ function getPlaceholder(providerKey: string | undefined, model: 'opus' | 'sonnet sonnet: 'MiniMax-M2.5', haiku: 'MiniMax-M2.5', }, + deepseek: { + opus: 'deepseek-reasoner', + sonnet: 'deepseek-chat', + haiku: 'deepseek-chat', + }, openrouter: { opus: 'anthropic/claude-3-opus', sonnet: 'anthropic/claude-3.5-sonnet',